from __future__ import annotations from django.conf import settings from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib import messages from django.contrib.auth.views import LoginView from django.db.models import Count, Q from django.http import JsonResponse from django.utils import timezone from django.views.generic import TemplateView from apps.jobs.models import Application, JobPosting, ScoreRun from apps.sources.models import Source from apps.core.rate_limit import clear_rate_limit, is_rate_limited, register_rate_limit_failure from .health import readiness from apps.sources.services.health import collect_source_health class SecurityAwareLoginView(LoginView): template_name = "registration/login.html" @staticmethod def _remaining_minutes(seconds: int) -> str: minutes = max(1, (seconds + 59) // 60) return f"{minutes} minuut" if minutes == 1 else f"{minutes} minuten" def dispatch(self, request, *args, **kwargs): if request.method == "POST": state = is_rate_limited( request, namespace="login", max_attempts=settings.AUTH_LOGIN_RATE_LIMIT_MAX_ATTEMPTS, window_seconds=settings.AUTH_LOGIN_RATE_LIMIT_WINDOW_SECONDS, block_seconds=settings.AUTH_LOGIN_RATE_LIMIT_BLOCK_SECONDS, ) if state.is_blocked: messages.error( request, ( "Te veel inlogpogingen op dit account. Wacht " f"{self._remaining_minutes(state.remaining_seconds)} en probeer daarna opnieuw." ), ) return self.form_invalid(self.get_form()) return super().dispatch(request, *args, **kwargs) def form_invalid(self, form): state = register_rate_limit_failure( self.request, namespace="login", max_attempts=settings.AUTH_LOGIN_RATE_LIMIT_MAX_ATTEMPTS, window_seconds=settings.AUTH_LOGIN_RATE_LIMIT_WINDOW_SECONDS, block_seconds=settings.AUTH_LOGIN_RATE_LIMIT_BLOCK_SECONDS, ) if state.is_blocked: messages.error( self.request, ( "Te veel inlogpogingen. Wacht " f"{self._remaining_minutes(state.remaining_seconds)} en probeer daarna opnieuw." ), ) return super().form_invalid(form) def form_valid(self, form): clear_rate_limit(self.request, namespace="login") return super().form_valid(form) def health_live(request): return JsonResponse({"ok": True, "service": "vacatureradar"}) def health_ready(request): result = readiness() return JsonResponse(result.to_dict(), status=200 if result.ok else 503) class TodayView(LoginRequiredMixin, TemplateView): template_name = "dashboard/today.html" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) latest_scores = ( ScoreRun.objects.select_related("job", "job__employer", "profile") .filter(job__status=JobPosting.Status.ACTIVE, hard_exclusions=[]) .order_by("-score", "-created_at") ) # Eén score per vacature, zonder PostgreSQL-specifieke DISTINCT ON. seen: set[str] = set() cards = [] for score in latest_scores[:250]: key = str(score.job_id) if key in seen: continue seen.add(key) cards.append(score) if len(cards) >= 20: break context.update( { "score_cards": cards, "active_jobs": JobPosting.objects.filter(status=JobPosting.Status.ACTIVE).count(), "new_today": JobPosting.objects.filter( first_seen__date=timezone.localdate() ).count(), "applications_open": Application.objects.exclude( status__in=[Application.Status.REJECTED, Application.Status.WITHDRAWN] ).count(), "source_counts": Source.objects.aggregate( total=Count("id"), unhealthy=Count("id", filter=~Q(status=Source.Status.ACTIVE)), ), } ) return context class SystemStatusView(LoginRequiredMixin, TemplateView): template_name = "system/status.html" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["health"] = readiness() context["source_summary"] = Source.objects.values("status").annotate(total=Count("id")) context["job_summary"] = JobPosting.objects.values("status").annotate(total=Count("id")) context["source_health"] = collect_source_health() return context