This commit is contained in:
+194
-36
@@ -1,30 +1,141 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
from django import forms
|
||||
|
||||
from .models import SearchProfile
|
||||
|
||||
TITLE_CHOICES = (
|
||||
("system engineer", "System engineer"),
|
||||
("infrastructure engineer", "Infrastructure engineer"),
|
||||
("cloud engineer", "Cloud engineer"),
|
||||
("devops engineer", "DevOps engineer"),
|
||||
("network engineer", "Network engineer"),
|
||||
("security engineer", "Security engineer"),
|
||||
("workplace engineer", "Workplace engineer"),
|
||||
("support engineer", "Support engineer"),
|
||||
("software engineer", "Software engineer"),
|
||||
("data engineer", "Data engineer"),
|
||||
("solution architect", "Solution architect"),
|
||||
("project manager", "Projectmanager"),
|
||||
)
|
||||
|
||||
EXCLUDED_TITLE_CHOICES = (
|
||||
("sales", "Sales"),
|
||||
("recruiter", "Recruiter"),
|
||||
("account manager", "Accountmanager"),
|
||||
("callcenter", "Callcenter"),
|
||||
("stage", "Stage"),
|
||||
("student", "Studentenjob"),
|
||||
)
|
||||
|
||||
SKILL_CHOICES = (
|
||||
("azure", "Microsoft Azure"),
|
||||
("aws", "AWS"),
|
||||
("microsoft 365", "Microsoft 365"),
|
||||
("active directory", "Active Directory"),
|
||||
("entra id", "Entra ID"),
|
||||
("intune", "Microsoft Intune"),
|
||||
("linux", "Linux"),
|
||||
("windows server", "Windows Server"),
|
||||
("networking", "Netwerken"),
|
||||
("security", "Security"),
|
||||
("python", "Python"),
|
||||
("powershell", "PowerShell"),
|
||||
("terraform", "Terraform"),
|
||||
("docker", "Docker"),
|
||||
("kubernetes", "Kubernetes"),
|
||||
("ci/cd", "CI/CD"),
|
||||
)
|
||||
|
||||
EXCLUDED_SKILL_CHOICES = (
|
||||
("cold calling", "Cold calling"),
|
||||
("door to door", "Deur-aan-deurverkoop"),
|
||||
("commission only", "Alleen commissieloon"),
|
||||
("night shift", "Nachtwerk"),
|
||||
)
|
||||
|
||||
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_text = forms.CharField(
|
||||
desired_titles = forms.MultipleChoiceField(
|
||||
label="Gewenste functietitels",
|
||||
required=False,
|
||||
widget=forms.Textarea(attrs={"rows": 4, "placeholder": "Eén titel per regel"}),
|
||||
choices=TITLE_CHOICES,
|
||||
widget=forms.CheckboxSelectMultiple,
|
||||
help_text="Selecteer alle rollen die bij je zoekrichting passen.",
|
||||
)
|
||||
excluded_titles_text = forms.CharField(
|
||||
label="Uitgesloten titelwoorden",
|
||||
excluded_titles = forms.MultipleChoiceField(
|
||||
label="Uitgesloten functietitels",
|
||||
required=False,
|
||||
widget=forms.Textarea(attrs={"rows": 3, "placeholder": "Eén term per regel"}),
|
||||
choices=EXCLUDED_TITLE_CHOICES,
|
||||
widget=forms.CheckboxSelectMultiple,
|
||||
help_text="Vacatures met deze titelwoorden worden hard uitgesloten.",
|
||||
)
|
||||
desired_skills_text = forms.CharField(
|
||||
desired_skills = forms.MultipleChoiceField(
|
||||
label="Gewenste skills",
|
||||
required=False,
|
||||
widget=forms.Textarea(attrs={"rows": 4, "placeholder": "Eén skill per regel"}),
|
||||
choices=SKILL_CHOICES,
|
||||
widget=forms.CheckboxSelectMultiple,
|
||||
)
|
||||
excluded_skills_text = forms.CharField(
|
||||
label="Uitgesloten skills",
|
||||
excluded_skills = forms.MultipleChoiceField(
|
||||
label="Uitgesloten kenmerken",
|
||||
required=False,
|
||||
widget=forms.Textarea(attrs={"rows": 3, "placeholder": "Eén skill per regel"}),
|
||||
choices=EXCLUDED_SKILL_CHOICES,
|
||||
widget=forms.CheckboxSelectMultiple,
|
||||
)
|
||||
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:
|
||||
@@ -33,10 +144,11 @@ class SearchProfileForm(forms.ModelForm):
|
||||
"name",
|
||||
"is_active",
|
||||
"home_postal_code",
|
||||
"home_municipality",
|
||||
"home_latitude",
|
||||
"home_longitude",
|
||||
"max_distance_km",
|
||||
"desired_titles",
|
||||
"excluded_titles",
|
||||
"desired_skills",
|
||||
"excluded_skills",
|
||||
"allowed_employment_types",
|
||||
"preferred_workplace",
|
||||
"preferred_regions",
|
||||
@@ -46,35 +158,81 @@ class SearchProfileForm(forms.ModelForm):
|
||||
"digest_time",
|
||||
"learning_enabled",
|
||||
]
|
||||
labels = {
|
||||
"name": "Naam",
|
||||
"is_active": "Actief profiel",
|
||||
"home_postal_code": "Thuispostcode",
|
||||
"max_distance_km": "Maximale afstand (km)",
|
||||
"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."
|
||||
),
|
||||
"learning_enabled": "Past alleen zachte voorkeuren aan; harde regels blijven vast.",
|
||||
}
|
||||
widgets = {
|
||||
"allowed_employment_types": forms.TextInput(
|
||||
attrs={"placeholder": '["full_time", "part_time"]'}
|
||||
"home_postal_code": forms.TextInput(
|
||||
attrs={"inputmode": "numeric", "pattern": "[0-9]{4}", "maxlength": 4}
|
||||
),
|
||||
"preferred_workplace": forms.TextInput(attrs={"placeholder": '["hybrid", "on_site"]'}),
|
||||
"preferred_regions": forms.TextInput(attrs={"placeholder": '["Limburg"]'}),
|
||||
"excluded_regions": forms.TextInput(attrs={"placeholder": '["Brussel"]'}),
|
||||
"digest_time": forms.TimeInput(attrs={"type": "time"}),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if self.instance and self.instance.pk:
|
||||
self.fields["desired_titles_text"].initial = "\n".join(self.instance.desired_titles)
|
||||
self.fields["excluded_titles_text"].initial = "\n".join(self.instance.excluded_titles)
|
||||
self.fields["desired_skills_text"].initial = "\n".join(self.instance.desired_skills)
|
||||
self.fields["excluded_skills_text"].initial = "\n".join(self.instance.excluded_skills)
|
||||
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)
|
||||
|
||||
@staticmethod
|
||||
def _lines(value: str) -> list[str]:
|
||||
return [line.strip() for line in value.splitlines() if line.strip()]
|
||||
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 field.choices
|
||||
}
|
||||
extra = [
|
||||
(str(value), str(value))
|
||||
for value in stored
|
||||
if str(value).casefold() not in existing_by_casefold
|
||||
]
|
||||
field.choices = [*field.choices, *extra]
|
||||
field.initial = [
|
||||
existing_by_casefold.get(str(value).casefold(), str(value)) for value in stored
|
||||
]
|
||||
|
||||
def save(self, commit=True):
|
||||
instance = super().save(commit=False)
|
||||
instance.desired_titles = self._lines(self.cleaned_data["desired_titles_text"])
|
||||
instance.excluded_titles = self._lines(self.cleaned_data["excluded_titles_text"])
|
||||
instance.desired_skills = self._lines(self.cleaned_data["desired_skills_text"])
|
||||
instance.excluded_skills = self._lines(self.cleaned_data["excluded_skills_text"])
|
||||
if commit:
|
||||
instance.save()
|
||||
self.save_m2m()
|
||||
return instance
|
||||
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
|
||||
|
||||
@@ -2,9 +2,33 @@ from __future__ import annotations
|
||||
|
||||
from django.db import transaction
|
||||
|
||||
from apps.jobs.services.geocoding import LocationMatchResult, resolve_cached_location
|
||||
|
||||
from .models import ProfileRevision, SearchProfile
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def resolve_profile_home_location(profile: SearchProfile) -> LocationMatchResult:
|
||||
"""Resolve a user-entered postal code without exposing coordinate fields in the UI."""
|
||||
query = profile.home_postal_code.strip()
|
||||
result = (
|
||||
resolve_cached_location(query) if query else LocationMatchResult(query=query, location=None)
|
||||
)
|
||||
location = result.location
|
||||
profile.home_municipality = location.municipality or "" if location else ""
|
||||
profile.home_latitude = location.point.latitude if location and location.point else None
|
||||
profile.home_longitude = location.point.longitude if location and location.point else None
|
||||
profile.save(
|
||||
update_fields=[
|
||||
"home_municipality",
|
||||
"home_latitude",
|
||||
"home_longitude",
|
||||
"updated_at",
|
||||
]
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def save_profile_revision(profile: SearchProfile, *, reason: str) -> ProfileRevision:
|
||||
latest = profile.revisions.order_by("-version").first()
|
||||
|
||||
+16
-2
@@ -8,7 +8,7 @@ from django.views.generic import ListView, UpdateView
|
||||
|
||||
from .forms import SearchProfileForm
|
||||
from .models import SearchProfile
|
||||
from .services import save_profile_revision
|
||||
from .services import resolve_profile_home_location, save_profile_revision
|
||||
|
||||
|
||||
class ProfileListView(LoginRequiredMixin, ListView):
|
||||
@@ -31,10 +31,24 @@ class ProfileUpdateView(LoginRequiredMixin, UpdateView):
|
||||
|
||||
def form_valid(self, form):
|
||||
response = super().form_valid(form)
|
||||
location_result = resolve_profile_home_location(self.object)
|
||||
if self.object.is_active:
|
||||
self.object.activate()
|
||||
save_profile_revision(self.object, reason="user_edit")
|
||||
messages.success(self.request, "Zoekprofiel opgeslagen.")
|
||||
if self.object.home_postal_code and location_result.ambiguous:
|
||||
messages.warning(
|
||||
self.request,
|
||||
"Zoekprofiel opgeslagen. Deze postcode omvat meerdere gemeenten; afstand blijft "
|
||||
"onbekend tot de lokale geodata eenduidig is.",
|
||||
)
|
||||
elif self.object.home_postal_code and location_result.location is None:
|
||||
messages.warning(
|
||||
self.request,
|
||||
"Zoekprofiel opgeslagen. De postcode is bewaard; lokale geodata ontbreekt nog "
|
||||
"voor een afstandsberekening.",
|
||||
)
|
||||
else:
|
||||
messages.success(self.request, "Zoekprofiel opgeslagen.")
|
||||
return response
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user