This commit is contained in:
Flatlogic Bot 2026-06-09 23:15:53 +00:00
parent 41cce00390
commit ef6dd13037
11 changed files with 268 additions and 1 deletions

View File

@ -0,0 +1 @@

Binary file not shown.

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,212 @@
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)."
)
)

View File

@ -34,6 +34,13 @@
{% for value,label in family_choices %}<option value="{{ value }}" {% if family == value %}selected{% endif %}>{{ label }}</option>{% endfor %}
</select>
</div>
<div class="col-lg-3">
<label class="form-label" for="source_status">Source status</label>
<select class="form-select" id="source_status" name="source_status">
<option value="">Any status</option>
{% for value,label in source_status_choices %}<option value="{{ value }}" {% if source_status == value %}selected{% endif %}>{{ label }}</option>{% endfor %}
</select>
</div>
<div class="col-lg-1 d-grid">
<button class="btn btn-dark" type="submit">Go</button>
</div>

View File

@ -21,6 +21,32 @@
<section class="section-pad pt-0">
<div class="container">
<form class="filter-bar glass-card mb-4" method="get" role="search">
<div class="row g-3 align-items-end">
<div class="col-lg-5">
<label class="form-label" for="q">Search source</label>
<input class="form-control" id="q" type="search" name="q" value="{{ query }}" placeholder="Name, owner, or URL…">
</div>
<div class="col-lg-3">
<label class="form-label" for="family">Family</label>
<select class="form-select" id="family" name="family">
<option value="">All families</option>
{% for value,label in family_choices %}<option value="{{ value }}" {% if family == value %}selected{% endif %}>{{ label }}</option>{% endfor %}
</select>
</div>
<div class="col-lg-3">
<label class="form-label" for="status">Status</label>
<select class="form-select" id="status" name="status">
<option value="">All statuses</option>
{% for value,label in status_choices %}<option value="{{ value }}" {% if status == value %}selected{% endif %}>{{ label }}</option>{% endfor %}
</select>
</div>
<div class="col-lg-1 d-grid">
<button class="btn btn-dark" type="submit">Go</button>
</div>
</div>
</form>
<div class="ops-grid mb-4">
<article class="metric-tile ops-tile"><span>{{ total_sources }}</span><small>Total sources</small></article>
<article class="metric-tile ops-tile"><span>{{ active_jobs }}</span><small>Active offers</small></article>

View File

@ -94,6 +94,7 @@ def job_list(request):
query = request.GET.get("q", "").strip()
contract = request.GET.get("contract", "").strip()
family = request.GET.get("family", "").strip()
source_status = request.GET.get("source_status", "").strip()
jobs = JobPosting.objects.select_related("source").filter(is_active=True)
if query:
@ -107,6 +108,8 @@ def job_list(request):
jobs = jobs.filter(contract_type=contract)
if family:
jobs = jobs.filter(source__family=family)
if source_status:
jobs = jobs.filter(source__status=source_status)
context = {
**_meta("Search Job Offers", "Search normalized job offers collected for Dijon and nearby opportunities."),
@ -114,8 +117,10 @@ def job_list(request):
"query": query,
"contract": contract,
"family": family,
"source_status": source_status,
"contract_choices": JobPosting.ContractType.choices,
"family_choices": JobSource.Family.choices,
"source_status_choices": JobSource.Status.choices,
"result_count": jobs.count(),
}
return render(request, "core/job_list.html", context)
@ -135,7 +140,18 @@ def job_detail(request, pk):
)
def ops_dashboard(request):
query = request.GET.get("q", "").strip()
family = request.GET.get("family", "").strip()
status = request.GET.get("status", "").strip()
sources = JobSource.objects.annotate(job_total=Count("jobs")).order_by("family", "name")
if query:
sources = sources.filter(Q(name__icontains=query) | Q(owner__icontains=query) | Q(url__icontains=query))
if family:
sources = sources.filter(family=family)
if status:
sources = sources.filter(status=status)
recent_jobs = (
JobPosting.objects.select_related("source")
.order_by("-created_at")[:8]
@ -168,6 +184,10 @@ def ops_dashboard(request):
"stale_sources": stale_sources,
"active_jobs": JobPosting.objects.filter(is_active=True).count(),
"total_sources": sources.count(),
"query": query,
"family": family,
"status": status,
"family_choices": JobSource.Family.choices,
"status_choices": JobSource.Status.choices,
},
)