158 lines
6.9 KiB
Python
158 lines
6.9 KiB
Python
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,
|
|
)
|
|
|
|
|
|
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)
|
|
|
|
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,
|
|
"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}"
|