Files
VacatureRadar/apps/jobs/services/cockpit.py
T
2026-07-26 05:03:53 +02:00

180 lines
6.8 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 {
# Het dagoverzicht blijft een beslissingsscherm; de volledige rangschikking
# hoort in de verkenner. Vijf kaarten houden de primaire taak scanbaar.
"score_cards": list(latest_scores.order_by("-score", "-created_at")[:5]),
"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]),
}