D
This commit is contained in:
parent
41cce00390
commit
ef6dd13037
Binary file not shown.
Binary file not shown.
1
core/management/__init__.py
Normal file
1
core/management/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
|
||||
BIN
core/management/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
core/management/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
1
core/management/commands/__init__.py
Normal file
1
core/management/commands/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
|
||||
BIN
core/management/commands/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
core/management/commands/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
212
core/management/commands/seed_demo_data.py
Normal file
212
core/management/commands/seed_demo_data.py
Normal 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)."
|
||||
)
|
||||
)
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user