314 lines
11 KiB
Python
314 lines
11 KiB
Python
"""Vult een fictieve demo-omgeving voor het demo-account.
|
|
|
|
Wordt aangeroepen bij 'Bekijk demo' zodat de gebruiker meteen een geloofwaardige,
|
|
gevulde cockpit ziet (profiel, vacatures, matchscores en een sollicitatiepijplijn)
|
|
in plaats van een leeg dashboard. Volledig idempotent: eenmaal geseed, wordt er
|
|
niet opnieuw geseed.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
from datetime import timedelta
|
|
from decimal import Decimal
|
|
|
|
from django.utils import timezone
|
|
|
|
from apps.profiles.models import SearchProfile
|
|
|
|
_DEMO_PROFILE_NAME = "Demo · IT-infrastructuur Limburg"
|
|
|
|
# (titel, werkgever, domein, gemeente, regio, werkplek, skills_required,
|
|
# skills_preferred, score, recommendation, positives, concerns)
|
|
_DEMO_JOBS = [
|
|
(
|
|
"Infrastructure Engineer",
|
|
"CloudNexus",
|
|
"cloudnexus.example",
|
|
"Hasselt",
|
|
"Limburg",
|
|
"hybrid",
|
|
["Microsoft 365", "VMware", "Azure"],
|
|
["Terraform"],
|
|
Decimal("92.4"),
|
|
"strong",
|
|
["Dichtbij Hasselt", "Sterke match op Microsoft 365 en VMware", "Hybride werk"],
|
|
[],
|
|
),
|
|
(
|
|
"Cloud Architect",
|
|
"Skylink Solutions",
|
|
"skylink.example",
|
|
"Remote",
|
|
"Vlaanderen",
|
|
"remote",
|
|
["Azure", "Kubernetes"],
|
|
["Bicep", "Landing Zones"],
|
|
Decimal("88.1"),
|
|
"strong",
|
|
["Volledig remote", "Azure-zwaartepunt sluit aan"],
|
|
["Iets seniorder dan gevraagd"],
|
|
),
|
|
(
|
|
"Systeembeheerder Microsoft 365",
|
|
"Gemeente Genk",
|
|
"genk.example",
|
|
"Genk",
|
|
"Limburg",
|
|
"on_site",
|
|
["Microsoft 365", "Active Directory"],
|
|
["Intune"],
|
|
Decimal("83.6"),
|
|
"strong",
|
|
["Publieke sector met stabiliteit", "M365-focus"],
|
|
["Volledig op locatie"],
|
|
),
|
|
(
|
|
"DevOps Engineer",
|
|
"Datastroom BV",
|
|
"datastroom.example",
|
|
"Leuven",
|
|
"Vlaams-Brabant",
|
|
"hybrid",
|
|
["CI/CD", "Docker", "Azure"],
|
|
["GitLab", "Observability"],
|
|
Decimal("77.9"),
|
|
"possible",
|
|
["Moderne toolchain", "Hybride"],
|
|
["Reistijd naar Leuven"],
|
|
),
|
|
(
|
|
"Network Engineer",
|
|
"Noorder Telecom",
|
|
"noordertelecom.example",
|
|
"Antwerpen",
|
|
"Antwerpen",
|
|
"on_site",
|
|
["Cisco", "Netwerkbeheer"],
|
|
["Fortinet"],
|
|
Decimal("70.4"),
|
|
"possible",
|
|
["Stevige netwerkrol"],
|
|
["Weinig cloudcomponent", "Op locatie in Antwerpen"],
|
|
),
|
|
(
|
|
"IT Support Specialist",
|
|
"MediCare Groep",
|
|
"medicare.example",
|
|
"Diepenbeek",
|
|
"Limburg",
|
|
"on_site",
|
|
["Servicedesk", "Windows"],
|
|
["ITIL"],
|
|
Decimal("62.3"),
|
|
"weak",
|
|
["Dichtbij"],
|
|
["Sterk supportgericht", "Weinig infrastructuurverdieping"],
|
|
),
|
|
]
|
|
|
|
|
|
def _digest(*parts: str) -> str:
|
|
return hashlib.sha256("::".join(parts).encode("utf-8")).hexdigest()
|
|
|
|
|
|
def demo_environment_seeded(user) -> bool:
|
|
profile = SearchProfile.objects.filter(user=user, name=_DEMO_PROFILE_NAME).first()
|
|
if profile is None:
|
|
return False
|
|
from apps.jobs.models import ScoreRun
|
|
|
|
return ScoreRun.objects.filter(profile=profile).exists()
|
|
|
|
|
|
def seed_demo_environment(user) -> None:
|
|
"""Maak of herstel een fictieve, tijdsbestendige demo-omgeving voor ``user``."""
|
|
from apps.jobs.models import Application, Employer, JobPosting, ScoreRun
|
|
from apps.sources.models import MailboxConnection, Source, SourceRun
|
|
from apps.sources.platforms import PLATFORM_ALERTS
|
|
|
|
profile, _ = SearchProfile.objects.get_or_create(
|
|
user=user,
|
|
name=_DEMO_PROFILE_NAME,
|
|
defaults={
|
|
"is_active": True,
|
|
"home_municipality": "Hasselt",
|
|
"home_latitude": Decimal("50.930700"),
|
|
"home_longitude": Decimal("5.332500"),
|
|
"max_distance_km": 60,
|
|
"experience_years": 5,
|
|
"learning_enabled": True,
|
|
"desired_titles": ["infrastructure engineer", "systeembeheerder", "cloud engineer"],
|
|
"excluded_titles": ["sales", "recruiter"],
|
|
"desired_skills": ["Microsoft 365", "VMware", "Azure"],
|
|
"allowed_employment_types": ["full_time", "permanent"],
|
|
"preferred_workplace": ["hybrid", "remote", "on_site"],
|
|
"preferred_regions": ["Limburg", "Vlaams-Brabant"],
|
|
},
|
|
)
|
|
SearchProfile.objects.filter(user=user).exclude(pk=profile.pk).update(is_active=False)
|
|
if not profile.is_active:
|
|
profile.is_active = True
|
|
profile.save(update_fields=["is_active"])
|
|
|
|
now = timezone.now()
|
|
today = timezone.localdate(now)
|
|
workplace_map = {
|
|
"hybrid": JobPosting.Workplace.HYBRID,
|
|
"remote": JobPosting.Workplace.REMOTE,
|
|
"on_site": JobPosting.Workplace.ON_SITE,
|
|
}
|
|
created_jobs: list = []
|
|
|
|
for index, (
|
|
title,
|
|
employer_name,
|
|
domain,
|
|
municipality,
|
|
region,
|
|
workplace,
|
|
req_skills,
|
|
pref_skills,
|
|
score,
|
|
recommendation,
|
|
positives,
|
|
concerns,
|
|
) in enumerate(_DEMO_JOBS):
|
|
employer, _ = Employer.objects.get_or_create(
|
|
normalized_name=employer_name.lower(),
|
|
defaults={
|
|
"name": employer_name,
|
|
"domain": domain,
|
|
"is_direct_employer": True,
|
|
"is_recruiter": False,
|
|
"confidence": 0.92,
|
|
},
|
|
)
|
|
slug = _digest("demo-job", str(index), title, employer_name)
|
|
job, _ = JobPosting.objects.update_or_create(
|
|
canonical_key=slug,
|
|
defaults={
|
|
"employer": employer,
|
|
"original_title": title,
|
|
"normalized_title": title.lower(),
|
|
"job_family": "infrastructure",
|
|
"canonical_url": f"https://{domain}/vacatures/{slug[:12]}",
|
|
"content_hash": _digest("demo-content", slug),
|
|
"description_text": (
|
|
f"{title} bij {employer_name} in {municipality}. "
|
|
"Fictieve demovacature voor de VacatureRadar-demo-omgeving."
|
|
),
|
|
"raw_location": f"{municipality}, {region}",
|
|
"region": region,
|
|
"municipality": municipality,
|
|
"workplace_type": workplace_map[workplace],
|
|
"employment_types": ["full_time", "permanent"],
|
|
"skills_required": req_skills,
|
|
"skills_preferred": pref_skills,
|
|
"direct_employer": True,
|
|
"recruiter": False,
|
|
"extraction_confidence": 0.9,
|
|
"analysis_features": {
|
|
"support_ratio": 0.1,
|
|
"public_sector_signal": 1.0 if "Gemeente" in employer_name else 0.0,
|
|
"experience_years_max": 6,
|
|
},
|
|
"status": JobPosting.Status.ACTIVE,
|
|
"first_seen": now - timedelta(days=index + 1),
|
|
"last_seen": now,
|
|
"date_posted": now - timedelta(days=index + 1),
|
|
},
|
|
)
|
|
created_jobs.append(job)
|
|
|
|
ScoreRun.objects.filter(
|
|
job=job,
|
|
profile=profile,
|
|
model_version="demo-seed",
|
|
).delete()
|
|
ScoreRun.objects.create(
|
|
job=job,
|
|
profile=profile,
|
|
profile_version=profile.version,
|
|
score=score,
|
|
confidence=Decimal("0.78"),
|
|
recommendation=recommendation,
|
|
components={
|
|
"content": float(score) * 0.45,
|
|
"skills": float(score) * 0.30,
|
|
"location": float(score) * 0.25,
|
|
},
|
|
positives=positives,
|
|
concerns=concerns,
|
|
hard_exclusions=[],
|
|
evidence={"demo": True},
|
|
model_version="demo-seed",
|
|
prompt_version="1.0.0",
|
|
created_at=now - timedelta(hours=index),
|
|
)
|
|
|
|
# Een kleine sollicitatiepijplijn voor de demo.
|
|
if created_jobs:
|
|
Application.objects.update_or_create(
|
|
user=user,
|
|
job=created_jobs[0],
|
|
defaults={
|
|
"status": Application.Status.INTERVIEW,
|
|
"applied_at": now - timedelta(days=6),
|
|
"follow_up_date": today + timedelta(days=2),
|
|
"contact_name": "Sofie Vandael",
|
|
"contact_email": "sofie.vandael@cloudnexus.example",
|
|
"notes": "Eerste gesprek gepland. Demo-data.",
|
|
},
|
|
)
|
|
if len(created_jobs) > 3:
|
|
Application.objects.update_or_create(
|
|
user=user,
|
|
job=created_jobs[3],
|
|
defaults={
|
|
"status": Application.Status.APPLIED,
|
|
"applied_at": now - timedelta(days=2),
|
|
"follow_up_date": today + timedelta(days=5),
|
|
"notes": "Sollicitatie verstuurd. Demo-data.",
|
|
},
|
|
)
|
|
|
|
# Platformmailboxen zijn zichtbare ingressconfiguratie, geen crawlbare URL-bronnen.
|
|
# De demo gebruikt uitsluitend fictieve adressen en wordt door de scheduler overgeslagen.
|
|
for platform in PLATFORM_ALERTS:
|
|
MailboxConnection.objects.update_or_create(
|
|
user=user,
|
|
platform=platform,
|
|
defaults={
|
|
"provider": MailboxConnection.Provider.OUTLOOK,
|
|
"username": "alerts@example.test",
|
|
"mailbox": f"VacatureRadar/{platform}",
|
|
"enabled": True,
|
|
"poll_interval_minutes": MailboxConnection.PollInterval.THIRTY_MINUTES,
|
|
"next_poll_at": now + timedelta(minutes=30),
|
|
"last_polled_at": now - timedelta(minutes=5),
|
|
"last_success_at": now - timedelta(minutes=5),
|
|
"last_error_category": "",
|
|
"last_error_message": "",
|
|
"lease_token": "",
|
|
"lease_expires_at": None,
|
|
},
|
|
)
|
|
source, _ = Source.objects.update_or_create(
|
|
source_type=Source.Type.EMAIL,
|
|
domain=f"{platform}.mailbox.local",
|
|
defaults={
|
|
"name": f"{PLATFORM_ALERTS[platform].label} vacaturemailbox",
|
|
"base_url": "",
|
|
"status": Source.Status.ACTIVE,
|
|
"policy": Source.Policy.ALLOW,
|
|
"policy_reason": "Mailboxingress; niet via URL-fetch gepland.",
|
|
"parser_key": "email-alert",
|
|
"failure_count": 0,
|
|
"last_failure_at": None,
|
|
"next_run_at": None,
|
|
"metadata": {"demo": True, "mailbox_ingress": True},
|
|
},
|
|
)
|
|
SourceRun.objects.filter(source=source).delete()
|