Files
VacatureRadar/apps/profiles/models.py
Jens b8091e59bd
deploy / deploy (push) Canceled after 0s
Initial deploy setup
2026-07-21 14:00:00 +02:00

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}"