Files
VacatureRadar/apps/jobs/services/activity.py
T

160 lines
5.6 KiB
Python

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)