from __future__ import annotations import re from django import forms from .models import SearchProfile from .taxonomy import ( EXCLUDED_SKILL_CHOICES, EXCLUDED_TITLE_CHOICES, SKILL_CHOICES, TITLE_CHOICES, iter_choices, ) EMPLOYMENT_CHOICES = ( ("full_time", "Voltijds"), ("part_time", "Deeltijds"), ("permanent", "Vast contract"), ("fixed_term", "Tijdelijk contract"), ("freelance", "Freelance"), ("internship", "Stage"), ) WORKPLACE_CHOICES = ( ("hybrid", "Hybride"), ("remote", "Volledig op afstand"), ("on_site", "Op locatie"), ) REGION_CHOICES = ( ("Antwerpen", "Antwerpen"), ("Brussels Hoofdstedelijk Gewest", "Brussel"), ("Henegouwen", "Henegouwen"), ("Limburg", "Limburg"), ("Luik", "Luik"), ("Luxemburg", "Luxemburg"), ("Namen", "Namen"), ("Oost-Vlaanderen", "Oost-Vlaanderen"), ("Vlaams-Brabant", "Vlaams-Brabant"), ("Waals-Brabant", "Waals-Brabant"), ("West-Vlaanderen", "West-Vlaanderen"), ) class SearchProfileForm(forms.ModelForm): desired_titles = forms.MultipleChoiceField( label="Gewenste functietitels", required=False, choices=TITLE_CHOICES, widget=forms.CheckboxSelectMultiple(attrs={"class": "grouped-choices"}), help_text="Selecteer alle rollen die bij je zoekrichting passen; de beste titelmatch telt.", ) excluded_titles = forms.MultipleChoiceField( label="Uitgesloten functietitels", required=False, choices=EXCLUDED_TITLE_CHOICES, widget=forms.CheckboxSelectMultiple(attrs={"class": "grouped-choices"}), help_text="Vacatures met deze titelwoorden worden hard uitgesloten.", ) desired_skills = forms.MultipleChoiceField( label="Gewenste skills", required=False, choices=SKILL_CHOICES, widget=forms.CheckboxSelectMultiple(attrs={"class": "grouped-choices"}), help_text=( "Kies je herkenbare cv-skills ruim. Maximaal vier aangetroffen skills volstaan voor " "de volledige skillcomponent; extra keuzes verwateren je score niet." ), ) excluded_skills = forms.MultipleChoiceField( label="Uitgesloten kenmerken", required=False, choices=EXCLUDED_SKILL_CHOICES, widget=forms.CheckboxSelectMultiple(attrs={"class": "grouped-choices"}), ) allowed_employment_types = forms.MultipleChoiceField( label="Toegestane contractvormen", required=False, choices=EMPLOYMENT_CHOICES, widget=forms.CheckboxSelectMultiple, ) preferred_workplace = forms.MultipleChoiceField( label="Voorkeurswerkmodel", required=False, choices=WORKPLACE_CHOICES, widget=forms.CheckboxSelectMultiple, ) preferred_regions = forms.MultipleChoiceField( label="Voorkeursregio's", required=False, choices=REGION_CHOICES, widget=forms.CheckboxSelectMultiple, ) excluded_regions = forms.MultipleChoiceField( label="Uitgesloten regio's", required=False, choices=REGION_CHOICES, widget=forms.CheckboxSelectMultiple, ) class Meta: model = SearchProfile fields = [ "name", "is_active", "home_postal_code", "max_distance_km", "experience_years", "desired_titles", "excluded_titles", "desired_skills", "excluded_skills", "allowed_employment_types", "preferred_workplace", "preferred_regions", "excluded_regions", "recommendation_threshold", "top_match_threshold", "digest_time", "learning_enabled", ] labels = { "name": "Naam", "is_active": "Actief profiel", "home_postal_code": "Thuispostcode", "max_distance_km": "Maximale afstand (km)", "experience_years": "Relevante IT-ervaring", "recommendation_threshold": "Aanbevelingsdrempel", "top_match_threshold": "Topmatchdrempel", "digest_time": "Tijdstip dagelijkse samenvatting", "learning_enabled": "Gecontroleerd leren", } help_texts = { "home_postal_code": ( "Vier cijfers volstaan. Gemeente en coördinaten worden veilig afgeleid wanneer " "lokale geodata beschikbaar is." ), "experience_years": ( "Optionele profielcontext. Deze waarde telt nooit mee voor je matchscore en " "kan een vacature niet uitsluiten." ), "learning_enabled": "Past alleen zachte voorkeuren aan; harde regels blijven vast.", } widgets = { "home_postal_code": forms.TextInput( attrs={"inputmode": "numeric", "pattern": "[0-9]{4}", "maxlength": 4} ), "digest_time": forms.TimeInput(attrs={"type": "time"}), } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.fields["home_postal_code"].widget.attrs["maxlength"] = 4 if not self.instance or not self.instance.pk: return for field_name in ( "desired_titles", "excluded_titles", "desired_skills", "excluded_skills", "allowed_employment_types", "preferred_workplace", "preferred_regions", "excluded_regions", ): self._include_existing_choices(field_name) def _include_existing_choices(self, field_name: str) -> None: field = self.fields[field_name] stored = getattr(self.instance, field_name, []) existing_by_casefold = { str(value).casefold(): str(value) for value, _label in iter_choices(field.choices) } extra = [ (str(value), str(value)) for value in stored if str(value).casefold() not in existing_by_casefold ] if extra: field.choices = [*field.choices, ("Eerder opgeslagen", tuple(extra))] field.initial = [ existing_by_casefold.get(str(value).casefold(), str(value)) for value in stored ] def clean_home_postal_code(self) -> str: value = str(self.cleaned_data.get("home_postal_code") or "").strip() if value and not re.fullmatch(r"\d{4}", value): raise forms.ValidationError("Gebruik een Belgische postcode van vier cijfers.") return value def clean(self): cleaned = super().clean() overlap_checks = ( ("desired_titles", "excluded_titles", "functietitels"), ("desired_skills", "excluded_skills", "skills"), ("preferred_regions", "excluded_regions", "regio's"), ) for desired_field, excluded_field, label in overlap_checks: overlap = set(cleaned.get(desired_field) or []) & set(cleaned.get(excluded_field) or []) if overlap: self.add_error( excluded_field, f"Dezelfde {label} kunnen niet tegelijk gewenst en uitgesloten zijn.", ) return cleaned