from datetime import timedelta from django.core.management.base import BaseCommand from django.utils import timezone from core.models import JobPosting, JobSource class Command(BaseCommand): help = "Seed realistic demo sources and job postings for local MVP reviews." def add_arguments(self, parser): parser.add_argument( "--reset", action="store_true", help="Delete existing JobSource and JobPosting records before seeding.", ) def handle(self, *args, **options): if options["reset"]: JobPosting.objects.all().delete() JobSource.objects.all().delete() self.stdout.write(self.style.WARNING("Existing sources and jobs deleted.")) today = timezone.localdate() now = timezone.now() sources_payload = [ { "name": "France Travail – Dijon", "family": JobSource.Family.PORTAL, "url": "https://candidat.francetravail.fr/offres/recherche?lieux=21D", "status": JobSource.Status.ACTIVE, "owner": "Ops Team", "notes": "Primary public feed for Dijon area listings.", "last_checked_at": now - timedelta(minutes=22), }, { "name": "Apec Bourgogne-Franche-Comté", "family": JobSource.Family.PORTAL, "url": "https://www.apec.fr/candidat/recherche-emploi.html", "status": JobSource.Status.ACTIVE, "owner": "Ops Team", "notes": "Executive and white-collar roles.", "last_checked_at": now - timedelta(hours=2), }, { "name": "Adecco Dijon Tertiaire", "family": JobSource.Family.AGENCY, "url": "https://www.adecco.fr/offres-emploi/?k=dijon", "status": JobSource.Status.PAUSED, "owner": "Data Partner", "notes": "Paused while waiting on extraction rule update.", "last_checked_at": now - timedelta(days=2), }, { "name": "Urgo Group Careers", "family": JobSource.Family.COMPANY, "url": "https://careers.urgo-group.com/", "status": JobSource.Status.ACTIVE, "owner": "Ops Team", "notes": "Direct company careers feed.", "last_checked_at": now - timedelta(minutes=55), }, { "name": "SEB Selongey Careers", "family": JobSource.Family.COMPANY, "url": "https://www.groupe-seb.com/fr/carrieres", "status": JobSource.Status.ERROR, "owner": "Connector Squad", "notes": "Blocked by anti-bot response, requires parser fallback.", "last_checked_at": now - timedelta(days=5), }, ] source_map = {} for payload in sources_payload: source, _ = JobSource.objects.update_or_create(url=payload["url"], defaults=payload) source_map[source.name] = source jobs_payload = [ { "source": "France Travail – Dijon", "title": "Développeur Python Django (H/F)", "company": "Noveo Digital", "location": "Dijon", "contract_type": JobPosting.ContractType.CDI, "remote": True, "salary": "38k€–45k€", "apply_url": "https://example.com/jobs/python-django-dijon", "published_at": today - timedelta(days=1), "description": "Concevoir des APIs Django et maintenir un back-office métier orienté data.", "is_active": True, "duplicate_score": 4.10, }, { "source": "France Travail – Dijon", "title": "Intégrateur Front-end React", "company": "Pixel Nordic", "location": "Dijon", "contract_type": JobPosting.ContractType.CDD, "remote": False, "salary": "34k€", "apply_url": "https://example.com/jobs/react-integrator", "published_at": today - timedelta(days=2), "description": "Intégrer des interfaces performantes et accessibles au sein d'une équipe produit.", "is_active": True, "duplicate_score": 1.90, }, { "source": "Apec Bourgogne-Franche-Comté", "title": "Product Owner CMS", "company": "Cobalt Studio", "location": "Dijon", "contract_type": JobPosting.ContractType.CDI, "remote": True, "salary": "45k€–52k€", "apply_url": "https://example.com/jobs/product-owner-cms", "published_at": today - timedelta(days=3), "description": "Piloter la roadmap d'une plateforme CMS B2B et animer les rituels produit.", "is_active": True, "duplicate_score": 0.50, }, { "source": "Adecco Dijon Tertiaire", "title": "Technicien support applicatif", "company": "Helix Services", "location": "Chenôve", "contract_type": JobPosting.ContractType.INTERIM, "remote": False, "salary": "13,50€/h", "apply_url": "https://example.com/jobs/support-applicatif", "published_at": today - timedelta(days=4), "description": "Support N1/N2 sur une suite SaaS et suivi d'incidents applicatifs.", "is_active": True, "duplicate_score": 6.75, }, { "source": "Urgo Group Careers", "title": "Data Analyst RH", "company": "Urgo Group", "location": "Chenôve", "contract_type": JobPosting.ContractType.CDI, "remote": True, "salary": "40k€", "apply_url": "https://example.com/jobs/data-analyst-rh", "published_at": today - timedelta(days=5), "description": "Structurer les tableaux de bord RH et fiabiliser les flux de reporting.", "is_active": True, "duplicate_score": 0.30, }, { "source": "SEB Selongey Careers", "title": "Ingénieur QA Automatisation", "company": "Groupe SEB", "location": "Selongey", "contract_type": JobPosting.ContractType.CDI, "remote": False, "salary": "42k€–48k€", "apply_url": "https://example.com/jobs/qa-automation", "published_at": today - timedelta(days=6), "description": "Automatiser les scénarios de validation et renforcer la non-régression continue.", "is_active": False, "duplicate_score": 3.20, }, { "source": "Apec Bourgogne-Franche-Comté", "title": "Chef de projet digital", "company": "Mutualité Bourgogne", "location": "Dijon", "contract_type": JobPosting.ContractType.CDD, "remote": False, "salary": "39k€", "apply_url": "https://example.com/jobs/chef-projet-digital", "published_at": today - timedelta(days=7), "description": "Coordonner la refonte d'outils internes et piloter les prestataires externes.", "is_active": True, "duplicate_score": 2.40, }, { "source": "France Travail – Dijon", "title": "Développeur Full Stack Junior", "company": "BFC Cloud", "location": "Dijon", "contract_type": JobPosting.ContractType.APPRENTICESHIP, "remote": True, "salary": "Selon grille alternance", "apply_url": "https://example.com/jobs/fullstack-junior", "published_at": today - timedelta(days=8), "description": "Participer au développement d'un CMS métier avec Django et Vue.", "is_active": True, "duplicate_score": 5.60, }, ] seeded_jobs = 0 for payload in jobs_payload: source = source_map[payload.pop("source")] _, created = JobPosting.objects.update_or_create( source=source, title=payload["title"], company=payload["company"], defaults=payload, ) if created: seeded_jobs += 1 self.stdout.write( self.style.SUCCESS( f"Demo seed complete: {len(source_map)} sources available, {JobPosting.objects.count()} total job postings ({seeded_jobs} new)." ) )