feat: personalize scoring and add skills radar
This commit is contained in:
@@ -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
@@ -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),
|
||||
]
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user