207 lines
7.1 KiB
Python
207 lines
7.1 KiB
Python
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
|