"""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() 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": (now + timedelta(days=2)).date(), "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": (now + timedelta(days=5)).date(), "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()