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 Source, 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 = [] runs = SourceRun.objects.exclude(source__source_type=Source.Type.EMAIL) for run in runs.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)