feat: replace pipeline profile and source health
This commit is contained in:
@@ -274,6 +274,31 @@ def track_application_changes(
|
||||
return events
|
||||
|
||||
|
||||
def change_application_status(*, application: Application, user, status: str) -> Application:
|
||||
"""Persist an accessible pipeline status change and its audit event."""
|
||||
|
||||
valid_statuses = {value for value, _label in Application.Status.choices}
|
||||
if status not in valid_statuses:
|
||||
raise ValueError("Ongeldige sollicitatiestatus.")
|
||||
previous = application.status
|
||||
if previous == status:
|
||||
return application
|
||||
with transaction.atomic():
|
||||
application.status = status
|
||||
update_fields = ["status", "updated_at"]
|
||||
if status == Application.Status.APPLIED and application.applied_at is None:
|
||||
application.applied_at = timezone.now()
|
||||
update_fields.append("applied_at")
|
||||
application.save(update_fields=update_fields)
|
||||
_record_timeline_event(
|
||||
application=application,
|
||||
user=user,
|
||||
event_type=ApplicationTimelineEvent.EventType.STATUS_CHANGED,
|
||||
metadata={"from": previous, "to": status},
|
||||
)
|
||||
return application
|
||||
|
||||
|
||||
def build_print_html(application: Application) -> str:
|
||||
snapshot = application.snapshot if isinstance(application.snapshot, dict) else {}
|
||||
job = application.job
|
||||
|
||||
@@ -9,6 +9,7 @@ from .views import (
|
||||
application_delete,
|
||||
application_export,
|
||||
application_print,
|
||||
application_status,
|
||||
job_feedback,
|
||||
)
|
||||
|
||||
@@ -17,6 +18,7 @@ urlpatterns = [
|
||||
path("skills/", SkillInsightsView.as_view(), name="skill-insights"),
|
||||
path("applications/", ApplicationListView.as_view(), name="applications"),
|
||||
path("applications/<int:pk>/", ApplicationUpdateView.as_view(), name="application-edit"),
|
||||
path("applications/<int:pk>/status/", application_status, name="application-status"),
|
||||
path("applications/<int:pk>/export/", application_export, name="application-export"),
|
||||
path("applications/<int:pk>/print/", application_print, name="application-print"),
|
||||
path("applications/<int:pk>/delete/", application_delete, name="application-delete"),
|
||||
|
||||
+38
-3
@@ -7,6 +7,7 @@ from django.db.models import Count, F, Q
|
||||
from django.http import Http404, HttpResponse
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.http import url_has_allowed_host_and_scheme
|
||||
from django.views.decorators.http import require_POST
|
||||
from django.views.generic import DetailView, ListView, TemplateView, UpdateView
|
||||
@@ -18,6 +19,7 @@ from .models import Application, Feedback, JobPosting, ScoreRun
|
||||
from .services.applications import (
|
||||
build_application_export,
|
||||
build_print_html,
|
||||
change_application_status,
|
||||
delete_application_dossier,
|
||||
track_application_changes,
|
||||
)
|
||||
@@ -206,8 +208,10 @@ class ApplicationListView(LoginRequiredMixin, ListView):
|
||||
context_object_name = "applications"
|
||||
|
||||
def get_queryset(self):
|
||||
return Application.objects.filter(user=self.request.user).select_related(
|
||||
"job", "job__employer"
|
||||
return (
|
||||
Application.objects.filter(user=self.request.user)
|
||||
.select_related("job", "job__employer")
|
||||
.prefetch_related("timeline_events")
|
||||
)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
@@ -222,6 +226,16 @@ class ApplicationListView(LoginRequiredMixin, ListView):
|
||||
}
|
||||
for status, label in Application.Status.choices
|
||||
]
|
||||
context["status_choices"] = Application.Status.choices
|
||||
context["open_count"] = sum(
|
||||
item.status not in {Application.Status.REJECTED, Application.Status.WITHDRAWN}
|
||||
for item in applications
|
||||
)
|
||||
context["follow_up_count"] = sum(
|
||||
bool(item.follow_up_date and item.follow_up_date <= timezone.localdate())
|
||||
for item in applications
|
||||
)
|
||||
context["today"] = timezone.localdate()
|
||||
return context
|
||||
|
||||
|
||||
@@ -231,7 +245,11 @@ class ApplicationUpdateView(LoginRequiredMixin, UpdateView):
|
||||
template_name = "applications/edit.html"
|
||||
|
||||
def get_queryset(self):
|
||||
return Application.objects.filter(user=self.request.user).select_related("job")
|
||||
return (
|
||||
Application.objects.filter(user=self.request.user)
|
||||
.select_related("job", "job__employer")
|
||||
.prefetch_related("timeline_events")
|
||||
)
|
||||
|
||||
def form_valid(self, form):
|
||||
previous = (
|
||||
@@ -269,6 +287,23 @@ def application_export(request, pk: int):
|
||||
return response
|
||||
|
||||
|
||||
@login_required
|
||||
@require_POST
|
||||
def application_status(request, pk: int):
|
||||
application = get_object_or_404(Application, pk=pk, user=request.user)
|
||||
try:
|
||||
change_application_status(
|
||||
application=application,
|
||||
user=request.user,
|
||||
status=request.POST.get("status", ""),
|
||||
)
|
||||
except ValueError as exc:
|
||||
messages.error(request, str(exc))
|
||||
else:
|
||||
messages.success(request, "Pipelinestatus bijgewerkt.")
|
||||
return redirect("jobs:applications")
|
||||
|
||||
|
||||
@login_required
|
||||
def application_print(request, pk: int):
|
||||
application = get_object_or_404(
|
||||
|
||||
@@ -6,6 +6,8 @@ from django.shortcuts import get_object_or_404, redirect
|
||||
from django.urls import reverse_lazy
|
||||
from django.views.generic import ListView, UpdateView
|
||||
|
||||
from apps.jobs.models import JobPosting, ScoreRun
|
||||
|
||||
from .forms import SearchProfileForm
|
||||
from .models import SearchProfile
|
||||
from .services import resolve_profile_home_location, save_profile_revision
|
||||
@@ -29,6 +31,21 @@ class ProfileUpdateView(LoginRequiredMixin, UpdateView):
|
||||
def get_queryset(self):
|
||||
return SearchProfile.objects.filter(user=self.request.user)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["preview_score"] = (
|
||||
ScoreRun.objects.filter(
|
||||
profile=self.object,
|
||||
job__status=JobPosting.Status.ACTIVE,
|
||||
hard_exclusions=[],
|
||||
)
|
||||
.select_related("job", "job__employer")
|
||||
.order_by("-created_at")
|
||||
.first()
|
||||
)
|
||||
context["recent_revisions"] = list(self.object.revisions.all()[:4])
|
||||
return context
|
||||
|
||||
def form_valid(self, form):
|
||||
response = super().form_valid(form)
|
||||
location_result = resolve_profile_home_location(self.object)
|
||||
|
||||
+13
-3
@@ -4,7 +4,7 @@ from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.db.models import Count, Q
|
||||
from django.db.models import Count, Prefetch, Q
|
||||
from django.http import HttpRequest
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
@@ -17,7 +17,7 @@ from apps.core.rate_limit import clear_rate_limit, is_rate_limited, register_rat
|
||||
from apps.profiles.models import SearchProfile
|
||||
|
||||
from .forms import MailboxConnectionForm, ManualImportForm
|
||||
from .models import MailboxConnection, Source, SourcePolicyReview
|
||||
from .models import MailboxConnection, Source, SourcePolicyReview, SourceRun
|
||||
from .platforms import PLATFORM_ALERTS
|
||||
from .services.mailbox_connections import MailboxCredentialError, save_mailbox_connection
|
||||
from .services.manual_import import ManualImportError, ManualImportSummary, import_manual_source
|
||||
@@ -27,7 +27,14 @@ from .tasks import fetch_source, poll_mailbox
|
||||
|
||||
def _source_list_queryset():
|
||||
return (
|
||||
Source.objects.prefetch_related("policy_reviews")
|
||||
Source.objects.prefetch_related(
|
||||
"policy_reviews",
|
||||
Prefetch(
|
||||
"runs",
|
||||
queryset=SourceRun.objects.order_by("-started_at")[:1],
|
||||
to_attr="latest_runs",
|
||||
),
|
||||
)
|
||||
.exclude(domain="jobs.example.org", status=Source.Status.DISABLED)
|
||||
.filter(
|
||||
Q(metadata__hidden_from_source_list__isnull=True)
|
||||
@@ -69,6 +76,9 @@ def _manual_import_context(
|
||||
"active_source_count": Source.objects.filter(status=Source.Status.ACTIVE).count(),
|
||||
"source_total_count": base_queryset.count(),
|
||||
"review_pending_count": Source.objects.filter(policy=Source.Policy.REVIEW).count(),
|
||||
"failed_source_count": Source.objects.filter(
|
||||
Q(status=Source.Status.QUARANTINED) | Q(failure_count__gt=0)
|
||||
).count(),
|
||||
"watchlist_sources": watchlist_queryset,
|
||||
"watchlist_count": watchlist_queryset.count(),
|
||||
"manual_import_form": form,
|
||||
|
||||
Reference in New Issue
Block a user