feat: release regional radar and mailbox integrations
deploy / deploy (push) Canceled after 0s

This commit is contained in:
Jens
2026-07-22 05:12:07 +02:00
parent 598d3ec18a
commit 551d0f46c2
131 changed files with 6209 additions and 336 deletions
+194 -36
View File
@@ -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
+24
View File
@@ -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
View File
@@ -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