feat: add automation activity and employer intelligence
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
|
||||
from django.core.paginator import Page, Paginator
|
||||
from django.urls import reverse
|
||||
|
||||
from apps.notifications.models import DigestOutbox, ReminderOutbox
|
||||
from apps.profiles.models import ProfileRevision
|
||||
from apps.sources.models import SourceRun
|
||||
|
||||
from ..models import ApplicationTimelineEvent, Feedback, JobVersion
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ActivityEvent:
|
||||
occurred_at: datetime
|
||||
event_type: str
|
||||
title: str
|
||||
detail: str
|
||||
tone: str
|
||||
url: str
|
||||
object_label: str
|
||||
|
||||
|
||||
ACTIVITY_FILTERS = (
|
||||
("all", "Alle activiteit"),
|
||||
("source", "Bronruns"),
|
||||
("job", "Vacatures"),
|
||||
("application", "Sollicitaties"),
|
||||
("feedback", "Feedback"),
|
||||
("notification", "Notificaties"),
|
||||
("profile", "Profiel"),
|
||||
)
|
||||
|
||||
|
||||
def _source_events() -> list[ActivityEvent]:
|
||||
events = []
|
||||
for run in SourceRun.objects.select_related("source").order_by("-started_at")[:100]:
|
||||
detail = (
|
||||
f"{run.extracted_count} geëxtraheerd · {run.created_count} nieuw · "
|
||||
f"{run.duplicate_count} duplicaat"
|
||||
)
|
||||
if run.error_category:
|
||||
detail = f"{run.error_category}: {run.error_message or 'geen foutdetail'}"
|
||||
events.append(
|
||||
ActivityEvent(
|
||||
run.started_at,
|
||||
"source",
|
||||
f"Bronrun {run.get_status_display().lower()}",
|
||||
detail,
|
||||
"positive" if run.status == SourceRun.Status.SUCCESS else "attention",
|
||||
reverse("sources:list") + f"#source-details-{run.source_id}",
|
||||
run.source.name,
|
||||
)
|
||||
)
|
||||
return events
|
||||
|
||||
|
||||
def _job_events() -> list[ActivityEvent]:
|
||||
return [
|
||||
ActivityEvent(
|
||||
version.created_at,
|
||||
"job",
|
||||
"Vacature-inhoud gewijzigd" if version.changed_fields else "Vacatureversie opgeslagen",
|
||||
", ".join(version.changed_fields)
|
||||
if version.changed_fields
|
||||
else "Bronmoment vastgelegd",
|
||||
"neutral",
|
||||
reverse("jobs:detail", kwargs={"pk": version.job_id}),
|
||||
version.job.original_title,
|
||||
)
|
||||
for version in JobVersion.objects.select_related("job").order_by("-created_at")[:100]
|
||||
]
|
||||
|
||||
|
||||
def build_activity_page(
|
||||
*, user, event_type: str = "all", query: str = "", page: int | str = 1, per_page: int = 20
|
||||
) -> Page:
|
||||
"""Combine bounded trustworthy events into a user-safe read-only timeline."""
|
||||
|
||||
events: list[ActivityEvent] = []
|
||||
if event_type in {"all", "source"}:
|
||||
events.extend(_source_events())
|
||||
if event_type in {"all", "job"}:
|
||||
events.extend(_job_events())
|
||||
if event_type in {"all", "feedback"}:
|
||||
for item in (
|
||||
Feedback.objects.filter(user=user).select_related("job").order_by("-created_at")[:100]
|
||||
):
|
||||
events.append(
|
||||
ActivityEvent(
|
||||
item.created_at,
|
||||
"feedback",
|
||||
item.get_action_display(),
|
||||
item.reason or "Gebruikersactie opgeslagen",
|
||||
"neutral",
|
||||
reverse("jobs:detail", kwargs={"pk": item.job_id}),
|
||||
item.job.original_title,
|
||||
)
|
||||
)
|
||||
if event_type in {"all", "application"}:
|
||||
for item in (
|
||||
ApplicationTimelineEvent.objects.filter(user=user)
|
||||
.select_related("application__job")
|
||||
.order_by("-created_at")[:100]
|
||||
):
|
||||
events.append(
|
||||
ActivityEvent(
|
||||
item.created_at,
|
||||
"application",
|
||||
item.get_event_type_display(),
|
||||
"Sollicitatiedossier bijgewerkt",
|
||||
"positive" if item.event_type == item.EventType.STATUS_CHANGED else "neutral",
|
||||
reverse("jobs:application-edit", kwargs={"pk": item.application_id}),
|
||||
item.application.job.original_title,
|
||||
)
|
||||
)
|
||||
if event_type in {"all", "notification"}:
|
||||
for model, label in ((DigestOutbox, "Samenvatting"), (ReminderOutbox, "Herinnering")):
|
||||
for item in model.objects.filter(profile__user=user).order_by("-created_at")[:60]:
|
||||
events.append(
|
||||
ActivityEvent(
|
||||
item.created_at,
|
||||
"notification",
|
||||
f"{label} {item.get_status_display().lower()}",
|
||||
item.subject,
|
||||
"positive" if item.status == item.Status.SENT else "attention",
|
||||
reverse("dashboard:system"),
|
||||
label,
|
||||
)
|
||||
)
|
||||
if event_type in {"all", "profile"}:
|
||||
for item in (
|
||||
ProfileRevision.objects.filter(profile__user=user)
|
||||
.select_related("profile")
|
||||
.order_by("-created_at")[:60]
|
||||
):
|
||||
events.append(
|
||||
ActivityEvent(
|
||||
item.created_at,
|
||||
"profile",
|
||||
f"Zoekprofiel versie {item.version}",
|
||||
item.reason or "Profielconfiguratie opgeslagen",
|
||||
"neutral",
|
||||
reverse("profiles:edit", kwargs={"pk": item.profile_id}),
|
||||
item.profile.name,
|
||||
)
|
||||
)
|
||||
normalized_query = query.strip().casefold()
|
||||
if normalized_query:
|
||||
events = [
|
||||
item
|
||||
for item in events
|
||||
if normalized_query in f"{item.title} {item.detail} {item.object_label}".casefold()
|
||||
]
|
||||
events.sort(key=lambda item: item.occurred_at, reverse=True)
|
||||
return Paginator(events, per_page).get_page(page)
|
||||
Reference in New Issue
Block a user