HTML 82.1%
Python 14.6%
TypeScript 1.9%
CSS 1%
JavaScript 0.5%
1# =============================================================================2# Job·Ka — Groupe KA3# Auteur : Simon-Pierre Boucher4# Contact : contact@spboucher.ai5# Fichier : jobka/quality.py6# Rôle : Contrôle qualité avant publication — règles anti-aberrations,7# quarantaine (offres douteuses non publiées, jamais supprimées)8# Créé : 2026-08-19 Modifié : 2026-08-199# =============================================================================10"""Quarantaine qualité (patron hérité de Lou·Ka).1112Une offre qui viole une règle de plausibilité n'est PAS publiée « à peu13près » : elle reste en base (active, resynchronisée normalement) avec la14colonne ``quarantine`` = JSON des motifs, et les points d'accès publics15l'excluent. Dès que la source corrige la donnée (ou qu'un correctif de16parseur passe), le contrôle se ré-évalue au sync suivant et l'offre est17réintégrée automatiquement.1819Règles (déterministes, bornes en CAD) :20- salaire hors bornes : < 15 $/h ou > 200 $/h équivalent ; annuel < 25 k$ ou21 > 500 k$ ;22- ville manifestement hors Québec ;23- dates incohérentes : date limite antérieure à la publication ;24- description absente ou < 50 caractères ;25- titre vide.26(Les titres tout-majuscules ne sont PAS quarantainés : la colonne27``title_clean`` corrige la casse à l'affichage.)28"""29from __future__ import annotations3031from .normalize import _NON_QC_CITIES, strip_accents # type: ignore3233MIN_HOURLY, MAX_HOURLY = 15.0, 200.034MIN_YEARLY, MAX_YEARLY = 25000.0, 500000.035MIN_DESCRIPTION = 50363738def issues(*, title: str, description: str, city: str,39 date_posted: str | None, date_deadline: str | None,40 salary_hour_min: float | None, salary_hour_max: float | None,41 salary_year_min: float | None, salary_year_max: float | None,42 salary_unit: str | None = None) -> list[str]:43 """Motifs de quarantaine d'une offre (liste vide = publiable)."""44 out: list[str] = []4546 if not (title or "").strip():47 out.append("titre_vide")4849 if len((description or "").strip()) < MIN_DESCRIPTION:50 out.append("description_courte")5152 key = " ".join(strip_accents((city or "").lower()).split())53 if key and key in _NON_QC_CITIES:54 out.append("ville_hors_quebec")5556 if date_posted and date_deadline and date_deadline < date_posted:57 out.append("dates_incoherentes")5859 # bornes salariales : les bornes HORAIRES ne s'appliquent qu'aux salaires60 # affichés à l'heure (un annuel de 460 k$ n'est pas « > 200 $/h ») ; les61 # autres unités sont jugées sur l'équivalent annuel62 if salary_unit == "hour":63 for v in (salary_hour_min, salary_hour_max):64 if v is not None and not (MIN_HOURLY <= v <= MAX_HOURLY):65 out.append("salaire_hors_bornes")66 break67 else:68 for v in (salary_year_min, salary_year_max):69 if v is not None and not (MIN_YEARLY <= v <= MAX_YEARLY):70 out.append("salaire_hors_bornes")71 break72 return out737475def issues_for_posting(job) -> list[str]:76 """Motifs de quarantaine d'un JobPosting finalisé (utilisé au sync)."""77 return issues(78 title=job.title, description=job.description, city=job.city,79 date_posted=job.date_posted, date_deadline=job.date_deadline,80 salary_hour_min=job.salary_hour_min(), salary_hour_max=job.salary_hour_max(),81 salary_year_min=job.salary_year_min(), salary_year_max=job.salary_year_max(),82 salary_unit=job.salary_unit)83