Démonstration — plateforme de recrutement fictive, sans affiliation avec Deutsche Post DHL Group.
Rapport de projet · 21 juillet 2026

Système de suivi des candidatures

Conception, développement et mise en production d'un ATS complet pour une direction des ressources humaines du secteur transport et logistique.

En production careers.dhl.d3v-test.me 43 tests automatisés 18/18 contrôles de recette
Télécharger le rapport en PDF (12 pages)

Chapitre 01Contexte et besoin

Le recrutement dans la messagerie et la logistique se distingue de celui des fonctions tertiaires sur quatre points, qui commandent toute la conception.

  1. Le volume. Une plateforme de tri recrute par dizaines, pas par unités. Un poste d'agent de quai attire couramment plusieurs dizaines de candidatures, souvent peu différenciées.
  2. Les prérequis réglementaires. Permis C ou CE, FIMO/FCO, carte conducteur, habilitation ADR, CACES 1/3/5, habilitation électrique : ce sont des conditions d'accès à l'emploi, pas des « plus ». Un dossier sans le titre requis ne peut pas avancer, et le vérifier tardivement coûte plusieurs jours à tout le monde.
  3. Les contraintes d'organisation. Horaires 2×8, 3×8, nuit, week-end, saisonnalité du pic de fin d'année : la disponibilité est un critère de sélection à part entière.
  4. La dispersion géographique. Plusieurs entités, une douzaine de sites, des besoins qui se déplacent d'une agence à l'autre au fil de l'activité.

Un suivi sur tableur partagé ne tient sur aucun de ces points : pas de traçabilité des décisions, pas de vérification systématique des habilitations, pas de visibilité consolidée, et surtout aucune gestion de la durée de conservation des CV — un risque RGPD direct.

Objectif du projet. Livrer un ATS complet et opérationnel : un portail carrières qui donne envie de postuler depuis un téléphone, et un back-office qui fait gagner du temps aux recruteurs sur le tri, le suivi et la traçabilité.
Avertissement. Ce projet est une démonstration technique déployée sur un domaine de test. Il n'est affilié à aucune entité du groupe Deutsche Post DHL, ne traite aucune donnée personnelle réelle, et l'ensemble des candidats et des offres qu'il contient sont fictifs.

Chapitre 02État de l'art

Une revue des plateformes du marché et des exigences de conformité a précédé la conception. Voici les constantes retenues, et ce qui en a été fait ici.

Constante du marchéTraduction dans le projet
Analyse automatique des CV en données structuréesExtraction du texte des PDF, DOCX et RTF, détection des coordonnées et de l'ancienneté
Score de correspondance candidat / posteMoteur pondéré par critère, avec critères éliminatoires, restitué en clair
Pipeline à étapes configurablesNeuf étapes en table de référence, vue kanban avec glisser-déposer
Workflow d'approbation des demandesSix statuts, transitions contrôlées, approbation réservée au manager
Piste d'audit des décisionsJournal en ajout seul et chronologie métier par candidature
Conformité RGPDConservation 24 mois, anonymisation automatique, export ZIP normalisé
Analytique temps réelTableau de bord, rapports, exports CSV
Expérience candidat mobilePortail responsive, dépôt en trois minutes, suivi par référence

Sources consultées

Chapitre 03Périmètre livré

Portail candidat

PageFonction
/Accueil : recherche, familles de métiers, offres du moment, chiffres en direct
/offresListe et filtres : mot-clé, métier, ville, contrat, horaires
/offres/{ref}Annonce complète : missions, profil, avantages, conditions
/offres/{ref}/postulerCandidature : coordonnées, CV, lettre, questions de présélection, consentement
/suiviSuivi par référence et e-mail, avec la progression étape par étape
/candidature-spontaneeDépôt hors offre, orienté vers le vivier
/confidentialitePolitique de traitement des données candidat

Back-office RH

  • Tableau de bord — offres publiées, candidatures à 30 jours, dossiers en cours, entretiens de la semaine, embauches à 90 jours, délai moyen de recrutement, taux de conversion, entonnoir, origine des candidatures, offres à surveiller.
  • Réquisitions — création, critères pondérés, questions de présélection, workflow brouillon → à approuver → approuvée → publiée → pourvue / annulée.
  • Pipeline — kanban à glisser-déposer, filtres par offre, score et profils éliminés.
  • Fiche candidature — analyse de correspondance détaillée, CV et texte extrait, réponses de présélection, chronologie, notes, évaluations multi-évaluateurs, entretiens, envoi d'e-mails depuis modèle.
  • Base de candidats — recherche plein texte dans le contenu des CV, vivier.
  • Entretiens — planification, compte rendu, agenda.
  • Rapports — volumes, entonnoir, performance par offre, efficacité des sources, motifs de refus, exports CSV.
  • Administration — utilisateurs et rôles, référentiels, modèles d'e-mails, file d'envoi, console RGPD, journal d'audit.

Rôles

RôlePortée
Administrateur RHTout, y compris administration et RGPD
Chargé de recrutementRéquisitions, pipeline, candidats, e-mails
Manager opérationnelSes réquisitions, évaluations, approbation
Lecture seuleConsultation et rapports uniquement

Chapitre 04Architecture

Le serveur cible impose deux contraintes : pas de Node.js et pas de base PostgreSQL active. Elles ont orienté toute la pile.

CoucheChoixPourquoi
ServeurStarlette + Uvicorn 100 % Python pur. Python 3.14 étant récent, toute dépendance compilée (pydantic-core en Rust, lxml en C) exposait à une roue manquante. L'application étant en formulaires rendus côté serveur, l'apport de FastAPI aurait été marginal.
RenduJinja2, CSS et JavaScript écrits à la main Aucun outil de construction, donc aucun besoin de Node. L'application reste utilisable sans JavaScript.
BaseSQLite en mode WAL Volumétrie cible très en deçà des limites, sauvegarde par instantané, exploitation nulle. Tous les accès passent par app/db.py : une bascule PostgreSQL ne toucherait que ce fichier.
Recherche CVSQLite FTS5Index plein texte natif, sans moteur externe.
Lecture des CVpypdf + zipfile/xml.etree pypdf est pur Python ; lire word/document.xml avec la bibliothèque standard évite python-docx et sa dépendance lxml compilée.
Génération de PDFÉcriture directe du format Les CV de démonstration sont de vrais PDF. Quarante lignes de code ont remplacé une dépendance lourde.
Mots de passehashlib.scrypt Robuste, dans la bibliothèque standard, aucun risque de roue absente.
SessionsCookie signé + table de sessions Signature côté client, révocation côté serveur.
Bilan des dépendances : six paquets, tous en Python pur. L'installation ne requiert aucune chaîne de compilation.

Chaîne de traitement d'une candidature

Candidat                    Application                         Base
   │                             │                                │
   ├── formulaire ──────────────▶│                                │
   │                             ├── contrôle CSRF                │
   │                             ├── validation du fichier        │
   │                             │   (extension + signature       │
   │                             │    binaire + taille)           │
   │                             ├── nom régénéré en UUID ───────▶│ /uploads (hors webroot)
   │                             ├── extraction du texte          │
   │                             │   (hors boucle d'événements)   │
   │                             ├── moteur de correspondance ───▶│ score + détail
   │                             ├── indexation FTS5 ────────────▶│ documents_fts
   │                             ├── chronologie + audit ────────▶│ application_events
   │◀── référence DHL-AAAA-NNNNNN┤                                │
   │                             └── accusé de réception ────────▶│ email_outbox

Modèle de données — 26 tables

DomaineTables
Accèsusers, sessions
Référentielsbusiness_units, sites, job_families, stages
Besoinsrequisitions, requisition_criteria, screening_questions
Candidaturescandidates, applications, application_answers, documents, documents_fts
Suiviapplication_events, evaluations, interviews, notes
Communicationemail_templates, email_outbox
Traçabilitéaudit_log, settings

L'index plein texte est tenu à jour par trois déclencheurs SQL (AFTER INSERT, AFTER UPDATE, AFTER DELETE) : aucune synchronisation applicative à maintenir.

Chapitre 05Moteur de correspondance

Le score est explicable par construction. Il ne sert pas à décider à la place du recruteur mais à trier une pile : un score bas n'écarte personne, il descend le dossier dans la liste.

Calcul

  1. Le texte du CV est normalisé : minuscules, accents supprimés.
  2. Chaque critère de la réquisition porte des synonymes, un poids de 1 à 5, et éventuellement un drapeau éliminatoire. Score de base = Σ(poids obtenus) / Σ(poids totaux) × 100.
  3. L'ancienneté détectée dans le CV donne un bonus de 0 à 10 points, proportionné au niveau demandé sur l'offre.
  4. Une réponse non conforme à une question éliminatoire, ou un critère obligatoire absent, place le dossier en éliminé : son score est plafonné à 35 et le motif est affiché explicitement.

Détection

La recherche exige une frontière de mot des deux côtés, avec tolérance au pluriel : « logistique » trouve « logistiques », mais « CAP » n'est pas déclenché par « capacité ». Ce point précis a été révélé par les tests unitaires et corrigé.

Restitution

Le recruteur ne voit jamais un nombre seul, mais chaque critère avec le terme effectivement trouvé dans le CV :

CACES 5« caces 5 »×5
CACES 1 et 3« caces 1 »×4
Expérience entrepôt« entrepot »×4
Maîtrise WMS« wms »×3
Travail en 3x8« 3x8 »×2
Score sur critères
100/100
Bonus expérience
+10
Score retenu
100/100

Chapitre 06Sécurité

MesureMise en œuvre
TransportHTTPS avec certificat Let's Encrypt, redirection permanente depuis HTTP, HSTS un an
Mots de passescrypt (n=2¹⁴, r=8, p=1), sel aléatoire de 16 octets, comparaison à temps constant
SessionsCookie signé HttpOnly + Secure + SameSite=Lax, expiration 8 h, révocation en base
CSRFJeton signé et ancré à la session sur tous les POST, y compris les formulaires publics
Injection SQLRequêtes exclusivement paramétrées
XSSÉchappement automatique Jinja2 ; les deux filtres produisant du HTML échappent avant d'insérer
Dépôts de fichiersExtension et signature binaire contrôlées, 5 Mo maximum, nom régénéré en UUID, stockage hors racine web
Accès aux CVUniquement via une route authentifiée, avec vérification anti-traversée de chemin
CloisonnementContrôle de rôle par décorateur sur chaque route du back-office
Suppression de compteRéservée à l'administrateur, refusée sur soi-même et sur le dernier administrateur actif ; le travail produit survit, détaché de son auteur, et le journal d'audit conserve une trace nominative
En-têtesCSP stricte default-src 'self', nosniff, Referrer-Policy, X-Frame-Options, Permissions-Policy
DébitLimitation nginx spécifique sur la connexion, le dépôt de candidature et le suivi ; fail2ban actif
ÉnumérationLa page de suivi renvoie une réponse identique que la référence existe ou non
Isolation du servicesystemd : NoNewPrivileges, ProtectSystem=strict, ProtectHome=read-only, PrivateTmp
TraçabilitéJournal d'audit en ajout seul : acteur, action, objet, avant/après, IP, horodatage

Deux arbitrages documentés

La snippet nginx security-headers.conf partagée par les autres sites de la machine autorise plusieurs CDN externes. L'inclure aurait produit un second en-tête CSP et affaibli la lecture de la configuration. Le vhost ne l'inclut donc pas : l'application émet sa propre CSP stricte, et nginx ne complète que le HSTS.

De même, les fichiers statiques sont servis par l'application et non par nginx : donner à www-data un droit de traversée sur /home/ubuntu pour trois fichiers aurait été un mauvais échange.

Chapitre 07Conformité RGPD

ExigenceMise en œuvre
ConsentementCase explicite non pré-cochée, horodatée, avec version de la politique conservée
InformationPage dédiée : qui traite, quoi, pourquoi, combien de temps, qui accède, quels droits
Durée de conservation24 mois (recommandation CNIL), échéance calculée à la création du dossier
EffacementAnonymisation plutôt que suppression : identité neutralisée, CV effacés du disque, notes purgées — les statistiques de recrutement restent exploitables
AutomatisationTimer systemd quotidien à 03h30 ; l'opération est tracée dans le journal d'audit
PortabilitéExport ZIP : un JSON complet (identité, candidatures, réponses, entretiens, évaluations, communications) et l'ensemble des documents
PilotageConsole RGPD : dossiers arrivant à échéance sous 60 jours, historique des purges, registre du traitement
MinimisationAucune donnée sensible collectée ; pas de sous-traitant, pas de transfert hors UE

Chapitre 08Déploiement

Cible : un serveur Ubuntu (2 vCPU, 7 Go), nginx et certbot déjà en place, en cohabitation avec d'autres applications.

Internet ──▶ nginx (443)  ──▶ uvicorn 127.0.0.1:8090 ──▶ SQLite (WAL)
              │ TLS Let's Encrypt                        │
              │ limitation de débit                      └──▶ /uploads (hors webroot)
              └ redirection 80 → 443
ComposantDétail
Servicedhl-ats.service — uvicorn sur 127.0.0.1:8090, redémarrage automatique, durcissement systemd
Frontalcareers.dhl.d3v-test.me — TLS, HSTS, limitation de débit ciblée, corps limité à 6 Mo
CertificatLet's Encrypt, émis le 21/07/2026, valide jusqu'au 19/10/2026, renouvellement automatique
Purge RGPDdhl-ats-purge.timer — quotidienne à 03h30
Sauvegardedhl-ats-backup.timer — quotidienne à 02h00, instantané SQLite cohérent et archive des CV, rétention 14 jours
Supervision/healthz (application), /nginx-health (frontal), journaux séparés

Le port 8090 a été retenu après vérification : 8000 et 27017 sont déjà occupés par des conteneurs Docker sur cette machine.

Chapitre 09Recette

Tests automatisés — 39 tests, tous passants

DomaineCouverture
Moteur de scoringPondération, critères éliminatoires, synonymes, insensibilité aux accents, frontière de mot, questions éliminatoires, bonus d'expérience, bornes du score, sérialisation, données corrompues
Extraction et sécuritéExtraction PDF et DOCX, détection des coordonnées et de l'ancienneté, robustesse aux fichiers illisibles, hachage des mots de passe
Bout en boutParcours candidat complet, anti-doublon, consentement obligatoire, rejet CSRF, rejet d'un faux PDF, suivi, cloisonnement des rôles, changement d'étape, déclenchement d'e-mail, exports CSV, workflow de réquisition et transition illégale, export et anonymisation RGPD, protection des documents
$ .venv/bin/python -m unittest discover -s tests
Ran 39 tests in 1.287s
OK

Trois défauts réels détectés et corrigés

  1. normalize() ne supprimait pas les espaces de bord, ce qui rendait inopérant tout synonyme placé après une virgule dans la liste des mots-clés — la majorité d'entre eux.
  2. contient() n'exigeait une frontière de mot qu'à gauche : « CAP » était détecté dans « capacité ».
  3. Le jeton CSRF était signé avec un identifiant anonyme que la réponse n'avait pas encore posé, si bien que le tout premier formulaire servi à un visiteur était systématiquement rejeté. Corrigé en fixant l'identifiant avant la signature.

Recette sur le site en production — 18 contrôles sur 18

— Portail public —
  OK  page d'accueil rendue
  OK  liste des offres (13 offres)
  OK  filtre par mot-clé
  OK  détail d'une offre
  OK  candidature déposée avec CV PDF (DHL-2026-000079)
  OK  suivi de candidature par référence
— Back-office RH —
  OK  connexion recruteur
  OK  la candidature apparaît dans le pipeline
  OK  pages tableau de bord, réquisitions, candidats, entretiens, rapports
  OK  recherche plein texte dans les CV
  OK  export CSV (79 lignes)
— Cloisonnement des rôles —
  OK  un compte lecture seule est refusé sur /rh/admin
  OK  un visiteur anonyme est redirigé vers la connexion
  OK  les CV ne sont pas téléchargeables sans authentification

Contrôles complémentaires : déplacement kanban accepté (200) et rejeté avec un jeton CSRF invalide (400) ; certificat TLS au bon nom ; en-tête CSP unique ; redirection HTTP 301 ; purge RGPD et sauvegarde exécutées avec succès.

Chapitre 10Volumétrie

Fichiers sources
69
Lignes Python
4 640
Lignes de gabarits
3 050
Routes HTTP
55
Tables
26
Dépendances
6
toutes en Python pur

Jeu de démonstration chargé : 15 réquisitions dont 13 publiées, 79 candidats, 79 candidatures réparties dans le pipeline, 79 CV au format PDF réellement analysés, 29 entretiens, 259 événements de chronologie.

Chapitre 11Limites connues

  • Les e-mails ne partent pas. Ils sont déposés dans une file consultable, telle qu'ils seraient expédiés. Brancher un SMTP consiste à implémenter une seule fonction.
  • Pas d'authentification à deux facteurs ni de SSO d'entreprise.
  • Analyse de CV limitée aux formats textuels. Un PDF issu d'un scan sans couche de texte n'est pas exploitable : il faudrait un moteur OCR.
  • Interface en français uniquement, alors que le groupe est international.
  • Fichiers .doc binaires anciens mal extraits ; les formats modernes sont couverts.
  • Instance unique. SQLite convient à la volumétrie visée mais ne permet pas de répartir la charge sur plusieurs serveurs.

Chapitre 12Feuille de route

HorizonChantier
Court termeEnvoi SMTP réel ; notifications aux managers ; modèles d'e-mails par entité
Moyen termeSSO Microsoft Entra ID et double authentification ; connecteur SIRH pour l'onboarding des embauchés ; diffusion automatique des offres vers les jobboards et France Travail
Moyen termePortail multilingue (anglais, allemand) ; planification des entretiens synchronisée avec les agendas
Long termeBascule PostgreSQL et exécution multi-instances ; OCR des CV scannés ; analyse des écarts de traitement à des fins de non-discrimination

Chapitre 13Accès

Comptes de démonstration

RôleIdentifiantMot de passe
Administrateur RHadmin@dhl.d3v-test.meAdminDHL2026!
Chargé de recrutementrecruteur@dhl.d3v-test.meRecrut2026!
Manager opérationnelmanager@dhl.d3v-test.meManager2026!
Lecture seulelecteur@dhl.d3v-test.meLecture2026!

Ces identifiants relèvent d'un environnement de démonstration et sont affichés sur la page de connexion. Ils devraient être supprimés avant tout usage réel.

Portail candidat

Postuler et suivre son dossier

Ouvrir le portail
Back-office

Espace recruteur

Se connecter