from __future__ import annotations from datetime import time from typing import Any from django.conf import settings from django.core.exceptions import ValidationError from django.db import models, transaction from apps.core.models import TimeStampedModel from .defaults import ( default_follow_up_weekdays, default_hard_rules, default_soft_preferences, default_weights, ) EXPERIENCE_YEARS_CHOICES = ( (0, "Niet ingesteld"), (1, "Minder dan 2 jaar"), (3, "2 tot 4 jaar"), (5, "5 tot 7 jaar"), (8, "8 tot 9 jaar"), (10, "10 tot 14 jaar"), (15, "15 jaar of meer"), ) class SearchProfile(TimeStampedModel): user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) name = models.CharField(max_length=120, default="Primair profiel") is_active = models.BooleanField(default=True) locale = models.CharField(max_length=10, default="nl-BE") timezone = models.CharField(max_length=64, default="Europe/Brussels") home_postal_code = models.CharField(max_length=12, blank=True) home_municipality = models.CharField(max_length=120, blank=True) home_latitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True) home_longitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True) max_distance_km = models.PositiveIntegerField(default=45) experience_years = models.PositiveSmallIntegerField( choices=EXPERIENCE_YEARS_CHOICES, default=0, ) desired_titles = models.JSONField(default=list, blank=True) excluded_titles = models.JSONField(default=list, blank=True) desired_skills = models.JSONField(default=list, blank=True) excluded_skills = models.JSONField(default=list, blank=True) allowed_employment_types = models.JSONField(default=list, blank=True) preferred_workplace = models.JSONField(default=list, blank=True) preferred_regions = models.JSONField(default=list, blank=True) excluded_regions = models.JSONField(default=list, blank=True) hard_rules = models.JSONField(default=default_hard_rules, blank=True) soft_preferences = models.JSONField(default=default_soft_preferences, blank=True) weights = models.JSONField(default=default_weights, blank=True) recommendation_threshold = models.PositiveSmallIntegerField(default=65) top_match_threshold = models.PositiveSmallIntegerField(default=90) digest_time = models.TimeField(default=time(7, 30)) quiet_hours_start = models.TimeField(default=time(22, 0)) quiet_hours_end = models.TimeField(default=time(7, 0)) reminders_enabled = models.BooleanField(default=True) top_match_reminders_enabled = models.BooleanField(default=False) follow_up_weekdays = models.JSONField(default=default_follow_up_weekdays, blank=True) learning_enabled = models.BooleanField(default=False) ai_scoring_enabled = models.BooleanField(default=False) version = models.PositiveIntegerField(default=1) class Meta: ordering = ["-is_active", "name"] constraints = [ models.UniqueConstraint(fields=["user", "name"], name="unique_profile_name_per_user") ] def __str__(self) -> str: return self.name def clean(self) -> None: errors: dict[str, str] = {} if self.recommendation_threshold > self.top_match_threshold: errors["top_match_threshold"] = ( "De topmatchdrempel moet minstens de aanbevelingsdrempel zijn." ) try: weights_valid = ( isinstance(self.weights, dict) and bool(self.weights) and all(float(value) >= 0 for value in self.weights.values()) and sum(float(value) for value in self.weights.values()) > 0 ) except (TypeError, ValueError): weights_valid = False if not weights_valid: errors["weights"] = "Gewichten moeten numeriek, niet-negatief en samen positief zijn." if self.home_latitude is not None and not (-90 <= float(self.home_latitude) <= 90): errors["home_latitude"] = "Ongeldige breedtegraad." if self.home_longitude is not None and not (-180 <= float(self.home_longitude) <= 180): errors["home_longitude"] = "Ongeldige lengtegraad." if not isinstance(self.follow_up_weekdays, list) or not all( isinstance(day, int) and 0 <= day <= 6 for day in self.follow_up_weekdays ): errors["follow_up_weekdays"] = ( "Volgweken moet uit geldige weekdagnummers (0-6) bestaan." ) if errors: raise ValidationError(errors) def snapshot(self) -> dict[str, Any]: return { "name": self.name, "locale": self.locale, "timezone": self.timezone, "home_postal_code": self.home_postal_code, "home_municipality": self.home_municipality, "home_latitude": float(self.home_latitude) if self.home_latitude is not None else None, "home_longitude": float(self.home_longitude) if self.home_longitude is not None else None, "max_distance_km": self.max_distance_km, "experience_years": self.experience_years, "desired_titles": self.desired_titles, "excluded_titles": self.excluded_titles, "desired_skills": self.desired_skills, "excluded_skills": self.excluded_skills, "allowed_employment_types": self.allowed_employment_types, "preferred_workplace": self.preferred_workplace, "preferred_regions": self.preferred_regions, "excluded_regions": self.excluded_regions, "hard_rules": self.hard_rules, "soft_preferences": self.soft_preferences, "weights": self.weights, "recommendation_threshold": self.recommendation_threshold, "top_match_threshold": self.top_match_threshold, "reminders_enabled": self.reminders_enabled, "top_match_reminders_enabled": self.top_match_reminders_enabled, "follow_up_weekdays": self.follow_up_weekdays, "digest_time": self.digest_time.isoformat(), "quiet_hours_start": self.quiet_hours_start.isoformat(), "quiet_hours_end": self.quiet_hours_end.isoformat(), "learning_enabled": self.learning_enabled, "ai_scoring_enabled": self.ai_scoring_enabled, "version": self.version, } def activate(self) -> None: with transaction.atomic(): SearchProfile.objects.filter(user=self.user, is_active=True).exclude(pk=self.pk).update( is_active=False ) if not self.is_active: self.is_active = True self.save(update_fields=["is_active", "updated_at"]) class ProfileRevision(TimeStampedModel): profile = models.ForeignKey(SearchProfile, on_delete=models.CASCADE, related_name="revisions") version = models.PositiveIntegerField() snapshot = models.JSONField(default=dict) reason = models.CharField(max_length=200, blank=True) class Meta: ordering = ["-version"] constraints = [ models.UniqueConstraint( fields=["profile", "version"], name="unique_revision_version_per_profile" ) ] def __str__(self) -> str: return f"{self.profile} v{self.version}"