178 lines
6.6 KiB
Python
178 lines
6.6 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from django.contrib.auth import get_user_model
|
|
from django.db.models import (
|
|
CharField,
|
|
Count,
|
|
DateTimeField,
|
|
DecimalField,
|
|
F,
|
|
JSONField,
|
|
OuterRef,
|
|
Q,
|
|
QuerySet,
|
|
Subquery,
|
|
)
|
|
from django.utils import timezone
|
|
|
|
from apps.profiles.models import SearchProfile
|
|
from apps.sources.models import Source, SourceRun
|
|
|
|
from ..models import Application, ApplicationTimelineEvent, JobPosting, ScoreRun
|
|
from .relevance import it_relevance_query
|
|
|
|
User = get_user_model()
|
|
|
|
|
|
def annotate_latest_profile_score(
|
|
queryset: QuerySet[JobPosting], profile: SearchProfile | None
|
|
) -> QuerySet[JobPosting]:
|
|
"""Annotate jobs with one deterministic latest score for the active profile."""
|
|
|
|
if profile is None:
|
|
return queryset
|
|
latest = ScoreRun.objects.filter(job=OuterRef("pk"), profile=profile).order_by("-created_at")
|
|
return queryset.annotate(
|
|
match_score=Subquery(
|
|
latest.values("score")[:1],
|
|
output_field=DecimalField(max_digits=5, decimal_places=2),
|
|
),
|
|
match_confidence=Subquery(
|
|
latest.values("confidence")[:1],
|
|
output_field=DecimalField(max_digits=4, decimal_places=3),
|
|
),
|
|
match_recommendation=Subquery(
|
|
latest.values("recommendation")[:1], output_field=CharField()
|
|
),
|
|
match_positives=Subquery(latest.values("positives")[:1], output_field=JSONField()),
|
|
match_concerns=Subquery(latest.values("concerns")[:1], output_field=JSONField()),
|
|
match_hard_exclusions=Subquery(
|
|
latest.values("hard_exclusions")[:1], output_field=JSONField()
|
|
),
|
|
match_scored_at=Subquery(latest.values("created_at")[:1], output_field=DateTimeField()),
|
|
)
|
|
|
|
|
|
def _recent_dashboard_activity(user: User) -> list[dict[str, Any]]:
|
|
activity: list[dict[str, Any]] = []
|
|
for run in SourceRun.objects.select_related("source").order_by("-started_at")[:4]:
|
|
activity.append(
|
|
{
|
|
"occurred_at": run.started_at,
|
|
"kind": "source",
|
|
"title": f"Bronrun {run.get_status_display().lower()}",
|
|
"detail": run.source.name,
|
|
"tone": "positive" if run.status == SourceRun.Status.SUCCESS else "attention",
|
|
}
|
|
)
|
|
for event in (
|
|
ApplicationTimelineEvent.objects.filter(user=user)
|
|
.select_related("application__job")
|
|
.order_by("-created_at")[:4]
|
|
):
|
|
activity.append(
|
|
{
|
|
"occurred_at": event.created_at,
|
|
"kind": "application",
|
|
"title": event.get_event_type_display(),
|
|
"detail": event.application.job.original_title,
|
|
"tone": "neutral",
|
|
}
|
|
)
|
|
return sorted(activity, key=lambda item: item["occurred_at"], reverse=True)[:5]
|
|
|
|
|
|
def build_dashboard_cockpit(user: User, profile: SearchProfile | None) -> dict[str, Any]:
|
|
"""Build truthful dashboard metrics and bounded read lists."""
|
|
|
|
active_jobs = JobPosting.objects.filter(status=JobPosting.Status.ACTIVE)
|
|
active_it_jobs = active_jobs.filter(it_relevance_query())
|
|
today = timezone.localdate()
|
|
latest_scores = ScoreRun.objects.none()
|
|
if profile is not None:
|
|
latest_score_id = (
|
|
ScoreRun.objects.filter(job_id=OuterRef("job_id"), profile=profile)
|
|
.order_by("-created_at")
|
|
.values("pk")[:1]
|
|
)
|
|
latest_scores = (
|
|
ScoreRun.objects.select_related("job", "job__employer")
|
|
.filter(
|
|
profile=profile,
|
|
pk=Subquery(latest_score_id),
|
|
job__status=JobPosting.Status.ACTIVE,
|
|
recommendation__in=[
|
|
ScoreRun.Recommendation.STRONG,
|
|
ScoreRun.Recommendation.POSSIBLE,
|
|
],
|
|
hard_exclusions=[],
|
|
)
|
|
.filter(it_relevance_query("job__"))
|
|
)
|
|
|
|
sources = Source.objects.exclude(domain="jobs.example.org", status=Source.Status.DISABLED)
|
|
source_counts = sources.aggregate(
|
|
total=Count("id"),
|
|
active=Count("id", filter=Q(status=Source.Status.ACTIVE)),
|
|
attention=Count("id", filter=~Q(status=Source.Status.ACTIVE)),
|
|
)
|
|
open_applications = Application.objects.filter(user=user).exclude(
|
|
status__in=[Application.Status.REJECTED, Application.Status.WITHDRAWN]
|
|
)
|
|
new_today = active_it_jobs.filter(first_seen__date=today).count()
|
|
strong_today = latest_scores.filter(
|
|
recommendation=ScoreRun.Recommendation.STRONG,
|
|
job__first_seen__date=today,
|
|
).count()
|
|
filtered_count = active_jobs.exclude(it_relevance_query()).count()
|
|
duplicate_count = JobPosting.objects.filter(status=JobPosting.Status.DUPLICATE).count()
|
|
|
|
if strong_today:
|
|
day_conclusion = f"{strong_today} sterke match(es) verdienen vandaag eerst je aandacht."
|
|
elif new_today:
|
|
day_conclusion = f"De radar vond vandaag {new_today} nieuwe relevante vacature(s)."
|
|
else:
|
|
day_conclusion = "Geen nieuw sterk signaal; je bestaande opvolging blijft prioritair."
|
|
|
|
return {
|
|
"score_cards": list(latest_scores.order_by("-score", "-created_at")[:8]),
|
|
"priority_applications": list(
|
|
open_applications.select_related("job", "job__employer").order_by(
|
|
F("follow_up_date").asc(nulls_last=True), "-updated_at"
|
|
)[:4]
|
|
),
|
|
"active_jobs": active_it_jobs.count(),
|
|
"filtered_non_it_jobs": filtered_count,
|
|
"duplicate_jobs": duplicate_count,
|
|
"new_today": new_today,
|
|
"strong_today": strong_today,
|
|
"applications_open": open_applications.count(),
|
|
"source_counts": source_counts,
|
|
"recent_activity": _recent_dashboard_activity(user),
|
|
"day_conclusion": day_conclusion,
|
|
"latest_scan": SourceRun.objects.order_by("-started_at").first(),
|
|
}
|
|
|
|
|
|
def build_job_intelligence(
|
|
*, job: JobPosting, profile: SearchProfile | None, user: User
|
|
) -> dict[str, Any]:
|
|
"""Return the bounded, user-scoped read model for explorer and detail."""
|
|
|
|
score = (
|
|
ScoreRun.objects.filter(job=job, profile=profile).order_by("-created_at").first()
|
|
if profile
|
|
else None
|
|
)
|
|
return {
|
|
"score": score,
|
|
"application": Application.objects.filter(user=user, job=job).first(),
|
|
"duplicate_count": job.duplicates.count(),
|
|
"source_count": job.source_aliases.count(),
|
|
"version_count": job.versions.count(),
|
|
"latest_versions": list(job.versions.all()[:5]),
|
|
"provenance_rows": list(job.provenance.select_related("source_alias__source")[:12]),
|
|
}
|