From ef6dd130375705c3894fe25951b312b054d810e9 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Tue, 9 Jun 2026 23:15:53 +0000 Subject: [PATCH] D --- core/__pycache__/admin.cpython-311.pyc | Bin 1309 -> 1309 bytes core/__pycache__/views.cpython-311.pyc | Bin 10555 -> 11861 bytes core/management/__init__.py | 1 + .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 168 bytes core/management/commands/__init__.py | 1 + .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 177 bytes .../seed_demo_data.cpython-311.pyc | Bin 0 -> 7828 bytes core/management/commands/seed_demo_data.py | 212 ++++++++++++++++++ core/templates/core/job_list.html | 7 + core/templates/core/ops_dashboard.html | 26 +++ core/views.py | 22 +- 11 files changed, 268 insertions(+), 1 deletion(-) create mode 100644 core/management/__init__.py create mode 100644 core/management/__pycache__/__init__.cpython-311.pyc create mode 100644 core/management/commands/__init__.py create mode 100644 core/management/commands/__pycache__/__init__.cpython-311.pyc create mode 100644 core/management/commands/__pycache__/seed_demo_data.cpython-311.pyc create mode 100644 core/management/commands/seed_demo_data.py diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index 1741cb2997b929f5b9e3cb7877f5f192ae86c664..c2c75e86099e7a5cd3e680b8a8f65d9e812675ab 100644 GIT binary patch delta 19 ZcmbQsHJ6KPIWI340}wRM-N+@(3IHq$1Qh@P delta 19 ZcmbQsHJ6KPIWI340}#Z_+Q=o$3IHn|1ML6+ diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 1999a5fb0e526eb11c2d0023425c533b53dd7398..5c1f0c4e2c4ef1411e1076c9b1952c5be227487c 100644 GIT binary patch delta 2131 zcmaKsO>7%Q6vubH>s^1ZW3SgAHMTb<2~N|5whE2gP=eHoQbbY$frPI#jRSR*#Lak3 zTe_x22viUP)Iq9>q7n(E6xE7~asUaH3m3!z#b~RpEE&O>O1PrEv>bTj?Z$C}I`aHH zA8+2wynXZkVfgy1p0C|*2L)y1;v>^vZTZ$y6a^=A8L0o<1fHkxU?7RFiGPuQ+KPV^ zdubM54s64}GHuZeU$R}IDe4MTqZF+wjb2o(iyW3&5qC-gehKOUs3)af{IMj^F1#Sg zcB^VZl4%!JEhYCRN6dNyrsUaVz%~p*Dq9jZ8e}P@1;5c6cfz4mi^{01!r^5(hK~jl zP~AbddF<(W6i5$_g9PVB@R~T`3hCB zP3GnlG%=|lvZmtB=L%?YYCNw@D1|)!JNTh5MOQrcN6q0Ap~GQ2+D8^x=L=KlBsv7` zx(DhiX;r^-T~CiIND-51is?bT?z7Tu{u_f#WEEz+q)c2wm~P2R4_X+0GE((#4kf@{H53&6@6W#Y_J zcS4lQxAO9qWec6f_E^k13uk{9cg5}rerVNmxSum~c;%WHq%DdY{}PDsC9a+-_+BCr zW-|OG3w%(hMk%t8!uwSg$63ju_;Hu?5behYB^fv)CGfHo#0fUQLe4tz??$IfWlF9M z>BlOf>5`(RS+l+9gqIQEjC3a_NN-5&43!byw}ef5s%TZ<7G_!J$VD9CYC^!@F;Yxf zOJ<77c$?2+T-#P^F>Z?gO~hfHH_ScAGshZ1T4v(O2RSeK)`ICbmNe9WH^R81X`i)& zPJ7H-Z4BO`K;9r*wAI%K{r-~Jd?UkPuxM9g(?|Vz2H?#n8zUJh6KwvvOSHW~au-vqX0$vU7Pb@#c=tO`+y7PXWy|jnqA- zXq3hWlU=lo-%DPn-FWQbxzmPvG&X((HUi12E7*P@51cwR8cSI?I zZ3HCOQ8&R>0xJOt0qP{^A{YQvtow(DhlU#phC)x1xk-W}fRtmMqBYJ`AoFh#949zI zaFXC{K%OW$=o{YC7InS@?N7+yvIT?C;CLi~W06+;W?K&kmx~D3Jgxh}=ey5!FST6l zU6s>iISsmV?a7}DtNkP8{t*o*k6a%eTg}d`j^@gvISnZ1%D!Wo@7P&h=iS83qxZdB z=058#`+78A&jY9Ka13SluUu%O>9Ei4sCxXmcbD!h-m)@+=QahXF+RM~zHgTvi0D!j ze$`lq7VFkxkF7OoNmyE=Ox)rn7k8Ti)F=u2JHGEBf{tbhNFctU5D}HS>fg2X&*n0P onW??#0~jL@Hotr2ZU;~Ak?GwvY7GZyCrwn>L}VvvZARVoe}bXi{{R30 delta 1328 zcmaKs&u<$=6vtMXA(^1EPgc=miwqZc!73I36#m zkk&Mp9x5)C-T{FkwHy*8@PR0oN=RJz2U_7GV<{3K(JL;L3zZP>*>RAm#FOTuH~Zeq zcXsD%KY06_W#bE7*BHjm_RQMF!F z6pxFyJmrobib_IqO(>MIaKp;NFIF7hFU8@ZWy1HEZ9fq$*(?w5i>;K(! zmATQ81Y?fjn7b0n;uyvb_fXiAJQ+Inl7jj={H&BtJCSX5hqrugcGSt3$DF9=+m&%W zj$ypPDzS4t-srJj;BtM#6}8K*$YTw@Ox}dEsdp2qID^xkukpbZ@u7GPbCHK{Q`7Jy zRpKV((ihIp(Zc|u;up_RevvS2rHnQZ^N6M&Q5b}X%m5SDUDd(|4%l7Ke{2 z)9`!xG+dbc?#;duH2nvfc`s7FH`TbWiH;`vGCun$lITXQZX(l-B@Z&woy_x{%!>!< zawk38NzZqancLcD+Na^|a6gLOeKx|@%YP>JA+skPdynK41RZF29*xf9Tyblw>tYQn z9$d{o2^pBt7yIKc85+j3lB>e2avE-jBe0T9793^#PFma@SO`~Ota11_n;RHsC2PZ+ zWyT!ls5hk4u#Rj=@VR2ck3}1-V+PDjSUFccsxl`q?uwy&q#YcWh5G>uZj~caOFrDr zQnleqC`{!3cguDNUe8X&58Fr5ir_6 z301-g!V;qAJG;Dmepwt@T_o{oMIHLRPHmfn20;*-1Q*eyt0aqIkjsj!r`I=|D^9I> z^-{ei9B~aBZc^tS{+(>G19(`OlYb4w_CJ~BJf#P<10&vzywHub9{FU`c+3!eS%DQSE3&upP83g5+AQu nQ2C3)CO1E&G$+-rh!toS$hKmBAn}2jk&*EO1B@tQ28sayl1V7v literal 0 HcmV?d00001 diff --git a/core/management/commands/__init__.py b/core/management/commands/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/core/management/commands/__init__.py @@ -0,0 +1 @@ + diff --git a/core/management/commands/__pycache__/__init__.cpython-311.pyc b/core/management/commands/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..22b56f7cc17d204b82337d2445de78921ea6d599 GIT binary patch literal 177 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 literal 0 HcmV?d00001 diff --git a/core/management/commands/seed_demo_data.py b/core/management/commands/seed_demo_data.py new file mode 100644 index 0000000..ddc0a6d --- /dev/null +++ b/core/management/commands/seed_demo_data.py @@ -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)." + ) + ) diff --git a/core/templates/core/job_list.html b/core/templates/core/job_list.html index 7e04596..a8e6f80 100644 --- a/core/templates/core/job_list.html +++ b/core/templates/core/job_list.html @@ -34,6 +34,13 @@ {% for value,label in family_choices %}{% endfor %} +
+ + +
diff --git a/core/templates/core/ops_dashboard.html b/core/templates/core/ops_dashboard.html index 45bf119..92ce73a 100644 --- a/core/templates/core/ops_dashboard.html +++ b/core/templates/core/ops_dashboard.html @@ -21,6 +21,32 @@
+ +
{{ total_sources }}Total sources
{{ active_jobs }}Active offers
diff --git a/core/views.py b/core/views.py index 10e55f9..c87ced7 100644 --- a/core/views.py +++ b/core/views.py @@ -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, }, ) -