feat: personalize scoring and add skills radar

This commit is contained in:
Jens
2026-07-22 19:33:29 +02:00
parent 029df89265
commit a30ecbc553
33 changed files with 1529 additions and 114 deletions
+4 -5
View File
@@ -3,13 +3,12 @@ from __future__ import annotations
def default_weights() -> dict[str, float]:
return {
"content": 25.0,
"skills": 20.0,
"location": 15.0,
"content": 30.0,
"skills": 25.0,
"location": 20.0,
"conditions": 10.0,
"employer": 10.0,
"seniority": 10.0,
"preferences": 10.0,
"preferences": 5.0,
"ai": 0.0,
}
+24 -56
View File
@@ -5,55 +5,12 @@ 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"),
from .taxonomy import (
EXCLUDED_SKILL_CHOICES,
EXCLUDED_TITLE_CHOICES,
SKILL_CHOICES,
TITLE_CHOICES,
iter_choices,
)
EMPLOYMENT_CHOICES = (
@@ -91,27 +48,31 @@ class SearchProfileForm(forms.ModelForm):
label="Gewenste functietitels",
required=False,
choices=TITLE_CHOICES,
widget=forms.CheckboxSelectMultiple,
help_text="Selecteer alle rollen die bij je zoekrichting passen.",
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,
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,
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,
widget=forms.CheckboxSelectMultiple(attrs={"class": "grouped-choices"}),
)
allowed_employment_types = forms.MultipleChoiceField(
label="Toegestane contractvormen",
@@ -145,6 +106,7 @@ class SearchProfileForm(forms.ModelForm):
"is_active",
"home_postal_code",
"max_distance_km",
"experience_years",
"desired_titles",
"excluded_titles",
"desired_skills",
@@ -163,6 +125,7 @@ class SearchProfileForm(forms.ModelForm):
"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",
@@ -173,6 +136,10 @@ class SearchProfileForm(forms.ModelForm):
"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 = {
@@ -203,14 +170,15 @@ class SearchProfileForm(forms.ModelForm):
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
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
]
field.choices = [*field.choices, *extra]
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
]
@@ -0,0 +1,18 @@
# Generated by Django 5.2.16 on 2026-07-22 13:52
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('profiles', '0004_reminder_settings'),
]
operations = [
migrations.AddField(
model_name='searchprofile',
name='experience_years',
field=models.PositiveSmallIntegerField(choices=[(0, 'Niet ingesteld'), (1, 'Minder dan 2 jaar'), (3, '2 tot 4 jaar'), (5, '5 tot 7 jaar'), (8, '8 tot 9 jaar'), (10, '10 tot 14 jaar'), (15, '15 jaar of meer')], default=0),
),
]
@@ -0,0 +1,22 @@
from django.db import migrations
def remove_seniority_weight(apps, schema_editor):
search_profile = apps.get_model("profiles", "SearchProfile")
for profile in search_profile.objects.all().iterator():
weights = dict(profile.weights or {})
if "seniority" not in weights:
continue
weights.pop("seniority", None)
profile.weights = weights
profile.save(update_fields=["weights"])
class Migration(migrations.Migration):
dependencies = [
("profiles", "0005_searchprofile_experience_years"),
]
operations = [
migrations.RunPython(remove_seniority_weight, migrations.RunPython.noop),
]
+15
View File
@@ -16,6 +16,16 @@ from .defaults import (
default_weights,
)
EXPERIENCE_YEARS_CHOICES = (
(0, "Niet ingesteld"),
(1, "Minder dan 2 jaar"),
(3, "2 tot 4 jaar"),
(5, "5 tot 7 jaar"),
(8, "8 tot 9 jaar"),
(10, "10 tot 14 jaar"),
(15, "15 jaar of meer"),
)
class SearchProfile(TimeStampedModel):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
@@ -29,6 +39,10 @@ class SearchProfile(TimeStampedModel):
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)
experience_years = models.PositiveSmallIntegerField(
choices=EXPERIENCE_YEARS_CHOICES,
default=0,
)
desired_titles = models.JSONField(default=list, blank=True)
excluded_titles = models.JSONField(default=list, blank=True)
@@ -105,6 +119,7 @@ class SearchProfile(TimeStampedModel):
if self.home_longitude is not None
else None,
"max_distance_km": self.max_distance_km,
"experience_years": self.experience_years,
"desired_titles": self.desired_titles,
"excluded_titles": self.excluded_titles,
"desired_skills": self.desired_skills,
+316
View File
@@ -0,0 +1,316 @@
from __future__ import annotations
from collections.abc import Iterable, Iterator, Sequence
Choice = tuple[str, str]
ChoiceGroup = tuple[str, tuple[Choice, ...]]
TITLE_CHOICES: tuple[ChoiceGroup, ...] = (
(
"Field service & support",
(
("it field engineer", "IT Field Engineer"),
("field service engineer", "Field Service Engineer"),
("it engineer", "IT Engineer"),
("it support engineer", "IT Support Engineer"),
("support engineer", "Support Engineer"),
("onsite support engineer", "On-site Support Engineer"),
("service desk engineer", "Service Desk Engineer"),
("technical support engineer", "Technical Support Engineer"),
("it technician", "IT Technician"),
("pc technician", "PC Technician"),
),
),
(
"Systemen & infrastructuur",
(
("system engineer", "System Engineer"),
("system network engineer", "System & Network Engineer"),
("infrastructure engineer", "Infrastructure Engineer"),
("system administrator", "System Administrator"),
("systeembeheerder", "Systeembeheerder"),
("network engineer", "Network Engineer"),
("network administrator", "Network Administrator"),
("netwerkbeheerder", "Netwerkbeheerder"),
("infrastructure consultant", "Infrastructure Consultant"),
("implementation engineer", "Implementation Engineer"),
),
),
(
"Modern Workplace & endpoint",
(
("workplace engineer", "Workplace Engineer"),
("modern workplace engineer", "Modern Workplace Engineer"),
("microsoft 365 engineer", "Microsoft 365 Engineer"),
("microsoft 365 consultant", "Microsoft 365 Consultant"),
("modern workplace consultant", "Modern Workplace Consultant"),
("endpoint engineer", "Endpoint Engineer"),
("intune engineer", "Intune Engineer"),
),
),
(
"Andere IT-richtingen",
(
("cloud engineer", "Cloud Engineer"),
("devops engineer", "DevOps Engineer"),
("security engineer", "Security Engineer"),
("platform engineer", "Platform Engineer"),
("software engineer", "Software Engineer"),
("data engineer", "Data Engineer"),
("solution architect", "Solution Architect"),
("project manager", "IT-projectmanager"),
),
),
)
EXCLUDED_TITLE_CHOICES: tuple[ChoiceGroup, ...] = (
(
"Commercieel & werving",
(
("sales", "Sales"),
("recruiter", "Recruiter"),
("account manager", "Accountmanager"),
("business developer", "Business Developer"),
("callcenter", "Callcenter"),
),
),
(
"Software, data & analyse",
(
("software developer", "Software Developer"),
("software engineer", "Software Engineer"),
("web developer", "Web Developer"),
("developer", "Developmentfuncties (breed)"),
("software", "Softwarefuncties (breed)"),
("backend", "Backendfuncties"),
("frontend", "Frontendfuncties"),
("php", "PHP-ontwikkeling"),
("data engineer", "Data Engineer"),
("data scientist", "Data Scientist"),
("data", "Datafuncties (breed)"),
("ai engineer", "AI Engineer"),
("ai", "AI-functies (breed)"),
("machine learning", "Machine Learning"),
("functional analyst", "Functional Analyst"),
("functioneel analist", "Functioneel Analist"),
("business analyst", "Business Analyst"),
),
),
(
"Management & starters",
(
("product owner", "Product Owner"),
("project manager", "Projectmanager"),
("architect", "Architectrollen"),
("service delivery manager", "Service Delivery Manager"),
("team lead", "Teamlead"),
("head of", "Head-of-rollen"),
("procurement", "Procurement"),
("stage", "Stage"),
("internship", "Internship"),
("student", "Studentenjob"),
),
),
(
"Niet-IT-techniek",
(
("electromechanical", "Elektromechanica"),
("maintenance technician", "Onderhoudstechnieker"),
("process engineer", "Process Engineer"),
("automation engineer", "Automation Engineer"),
),
),
(
"Onderzoek & beeldverwerking",
(
("research", "Onderzoeksfuncties"),
("scientist", "Scientist"),
("image processing", "Beeldverwerking"),
("remote sensing", "Remote sensing"),
),
),
(
"Cloud & businessplatformen",
(
("cloud", "Cloudfuncties (breed)"),
("devops", "DevOps"),
("azure consultant", "Azure Consultant"),
("sap", "SAP"),
("power platform", "Power Platform"),
),
),
(
"Onderwijs",
(
("lecturer", "Lecturer"),
("lector", "Lector"),
("docent", "Docent"),
("gastdocent", "Gastdocent"),
),
),
)
SKILL_CHOICES: tuple[ChoiceGroup, ...] = (
(
"Field service & uitvoering",
(
("troubleshooting", "Troubleshooting"),
("onsite support", "On-site support"),
("hardware", "Hardware & randapparatuur"),
("installations", "Installaties & roll-outs"),
("migrations", "Migraties"),
("technical documentation", "Technische documentatie"),
("customer support", "Klantondersteuning"),
("second line support", "Tweede- en derdelijnssupport"),
("customer experience", "Klanttevredenheid / CSAT"),
),
),
(
"Microsoft & endpoint",
(
("microsoft 365", "Microsoft 365"),
("exchange online", "Exchange Online"),
("sharepoint", "SharePoint"),
("microsoft teams", "Microsoft Teams"),
("entra id", "Entra ID"),
("intune", "Microsoft Intune"),
("autopilot", "Windows Autopilot"),
("windows server", "Windows Server"),
("active directory", "Active Directory"),
("group policy", "Group Policy / GPO"),
("dynamics 365", "Microsoft Dynamics 365"),
("microsoft copilot", "Microsoft Copilot"),
("microsoft defender", "Microsoft Defender"),
("microsoft sentinel", "Microsoft Sentinel"),
("conditional access", "Conditional Access"),
),
),
(
"Netwerk & security",
(
("networking", "Netwerken"),
("tcp/ip", "TCP/IP"),
("vlan", "VLAN"),
("vpn", "VPN"),
("dhcp", "DHCP"),
("dns", "DNS"),
("firewalls", "Firewalls"),
("watchguard", "WatchGuard"),
("cisco", "Cisco"),
("ubiquiti", "Ubiquiti"),
("wi-fi", "Wi-Fi"),
("routing", "Routing"),
("switching", "Switching"),
("sd-wan", "SD-WAN"),
("fortinet", "Fortinet / FortiGate"),
("palo alto", "Palo Alto Networks"),
("aruba", "HPE Aruba"),
("meraki", "Cisco Meraki"),
("sophos", "Sophos"),
("zero trust", "Zero Trust"),
),
),
(
"Back-up & monitoring",
(
("backup and restore", "Back-up & restore"),
("veeam", "Veeam"),
("windows server backup", "Windows Server Backup"),
("synology", "Synology"),
("zabbix", "Zabbix"),
("monitoring", "Infrastructuurmonitoring"),
("observability", "Observability"),
("high availability", "High availability"),
("disaster recovery", "Disaster recovery"),
),
),
(
"Virtualisatie & platform",
(
("vmware", "VMware"),
("proxmox", "Proxmox"),
("hyper-v", "Hyper-V"),
("docker", "Docker"),
("azure", "Microsoft Azure"),
("aws", "AWS"),
("linux", "Linux"),
("kubernetes", "Kubernetes"),
("terraform", "Terraform"),
("ci/cd", "CI/CD"),
("deployment", "Deployment & uitrol"),
("reliability", "Platform reliability"),
("scalability", "Schaalbaarheid"),
),
),
(
"VoIP & telefonie",
(
("voip", "VoIP"),
("3cx", "3CX"),
("innovaphone", "Innovaphone"),
("teams telephony", "Teams-telefonie"),
("yealink", "Yealink"),
("snom", "Snom"),
),
),
(
"Automation & scripting",
(
("powershell", "PowerShell"),
("bash", "Bash"),
("python", "Python"),
("c#", "C#"),
),
),
(
"IT-servicemanagement",
(
("itil", "ITIL / IT-servicemanagement"),
("incident management", "Incidentmanagement"),
("problem management", "Problemmanagement"),
("change management", "Changemanagement"),
("servicenow", "ServiceNow"),
("jira service management", "Jira Service Management"),
),
),
(
"Digitale werkplek & adoptie",
(
("digital workplace", "Digital Workplace"),
("m365 governance", "Microsoft 365-governance"),
("document management", "Documentmanagement"),
("data governance", "Datagovernance"),
("user adoption", "Gebruikersadoptie"),
("training and workshops", "Training & workshops"),
),
),
)
EXCLUDED_SKILL_CHOICES: tuple[ChoiceGroup, ...] = (
(
"Commerciële voorwaarden",
(
("cold calling", "Cold calling"),
("door to door", "Deur-aan-deurverkoop"),
("commission only", "Alleen commissieloon"),
),
),
(
"Werkregeling",
(
("night shift", "Nachtwerk"),
("rotating shifts", "Ploegendienst"),
("on-call duty", "Structurele wachtdienst"),
),
),
)
def iter_choices(choices: Iterable[Choice | tuple[str, Sequence[Choice]]]) -> Iterator[Choice]:
"""Flatten Django optgroups while retaining compatibility with legacy flat choices."""
for value, label_or_choices in choices:
if isinstance(label_or_choices, tuple | list):
yield from iter_choices(label_or_choices)
else:
yield str(value), str(label_or_choices)