- Fichier unique, processus en deux temps : POST /editions/{id}/import/analyze
(colonnes détectées, aperçu, mapping suggéré, demi-journées de l'édition)
puis POST /editions/{id}/import/execute (mapping validé)
- Parser CSV (délimiteur et encodage devinés, BOM) et XLSX (openpyxl),
détection XLSX par nombre magique
- Vocabulaire des valeurs de disponibilité (tri-état, accents/casse) et des
tailles de t-shirt ; compétences créées à la volée dans le référentiel
- Rapport ligne par ligne : codes de traduction + paramètres, jamais de
phrase. Doublons : upsert sur email, signalement sur (nom, prénom) sans
email, doublons intra-fichier. Mode simulation (rollback, rien de persisté)
- 40 tests : parser, valeurs, service (tous les cas), endpoints
- Dépendances : python-multipart, openpyxl
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
14 KiB
CLAUDE.md — Gestion des bénévoles
Application web de gestion des bénévoles pour un festival associatif (50–200 bénévoles, 4 jours montage/démontage inclus). Interface en français, code en anglais.
Ce fichier est le contrat de travail. Il est tenu à jour à chaque étape.
1. Vocabulaire du domaine (glossaire FR ↔ EN)
Le métier parle français, le code et la base parlent anglais. Toute entité, colonne, route et variable utilise la colonne « Code / base ».
| Métier (brief) | Code / base |
|---|---|
| Édition | Edition |
| Bénévole | Volunteer |
| Compétence | Skill |
| Poste | Position |
| Référent d'un poste | Position.lead_account_id → Account |
| Créneau | Shift |
| Disponibilité | Availability |
| Affectation | Assignment |
| Compte (utilisateur connecté) | Account |
| Découpage de journée (paramétrable) | DayPart (code, label, start_time, end_time, order) |
| Lien magique | MagicLinkToken |
Énumérations :
| Métier | Code |
|---|---|
| Statut d'affectation | PROPOSED / CONFIRMED / DECLINED / CANCELLED |
| Origine d'affectation | SOLVER / MANUAL |
| Verrouillée | Assignment.locked: bool |
| Rôle | COORDINATOR / LEAD / VOLUNTEER |
| Disponibilité (tri-état) | AVAILABLE / IF_NEEDED / UNAVAILABLE |
| Catégorie de poste | BAR / TICKETING / WELCOME / PARKING / CLEANING / CATERING / SETUP / STAGE / OTHER |
Distinction critique (ne jamais confondre, ni en base ni dans l'UI)
Availability= une déclaration du bénévole : « je peux venir tel jour, telle demi-journée ».Assignment= une décision de l'organisation : « tu tiens le bar samedi 18 h – 22 h ».
Deux tables distinctes, deux écrans distincts, deux vocabulaires distincts.
2. Modèle de domaine
Défini dans backend/app/models/ (enums.py, base.py, domain.py).
Clés primaires UUID. Champs d'audit created_at / updated_at sur toutes
les tables sauf les tables d'association.
Edition:year,name(opt.),start_date,end_date,retention_days(durée de rétention des données nominatives en jours,NULL= pas de purge).DayPart: découpage d'une journée propre à l'édition (nombre et bornes libres, paramétrable par la coordination).code,label,start_time,end_time(end_time < start_time⇒ passe minuit),order. Unique (edition_id,code).Volunteer:email(unique par édition, nullable pour les bénévoles créés à la main — plusieursNULLautorisés),first_name,last_name,phone,tshirt_size,diet,comment. Pas de date de naissance.Skill: référentiel par édition (code,label). N-NVolunteer(volunteer_skill). N-NShift(shift_required_skill).Position:name,location,category(PositionCategory),lead_account_id(→Account,SET NULLà la suppression du compte).Shift:position_id,starts_at,ends_at(peut finir le lendemain),min_staff,ideal_staff, compétences requises (N-NSkill).Availability:volunteer_id,date,day_part_id,status(AvailabilityStatus). Unique (volunteer_id,date,day_part_id).Assignment:volunteer_id,shift_id,status(AssignmentStatus, défautPROPOSED),source(AssignmentSource, défautMANUAL),locked(défautfalse). Unique (volunteer_id,shift_id).Account:email(unique par édition),role(Role),edition_id,volunteer_id(nullable, unique).MagicLinkToken:account_id,token_hash(unique),expires_at,used_at.
Conventions techniques :
- Multi-édition : toute entité métier porte un
edition_id. - Énumérations stockées en
VARCHAR+CHECK(native_enum=False), pas de type ENUM natif PostgreSQL. - Suppression en cascade au niveau base (
ON DELETE CASCADE) sur tous lesedition_idet sur les liens versvolunteer,shift,position,skill,account. La suppression complète d'un bénévole (RGPD) efface donc sesAvailability,Assignment, liens de compétence et sonAccount. - Dates/heures naïves, heure locale Europe/Paris (
TZdu conteneur +tzdata).app.models.local_now()pour les horodatages.
Rôles et accès
COORDINATOR: accès complet, construit et publie les plannings.LEAD(référent) : lecture sur toute l'édition, écriture limitée à ses propres postes.VOLUNTEER: accès à ses seules données.
Authentification par lien magique envoyé par email, sans mot de passe. L'envoi
d'email est stubé pour l'instant (le lien est écrit dans les logs). Un
bénévole sans email n'a pas de Account et ne se connecte pas ; il est géré
entièrement par la coordination.
3. Moteur de validation des affectations — cœur du produit
Module isolé backend/app/validation/. Tests écrits avant l'implémentation,
un cas passant + un cas déclenchant par règle.
Retourne une liste de violations typées BLOCKING ou WARNING. Chaque
violation est un code + des paramètres structurés, jamais une phrase (la
traduction se fait via locales/fr.json).
| Code | Sévérité | Règle |
|---|---|---|
SHIFT_OVERLAP |
BLOCKING |
Deux créneaux d'un même bénévole se chevauchent. |
OUTSIDE_AVAILABILITY |
WARNING |
Un DayPart couvert par le créneau est UNAVAILABLE ou sans déclaration. |
DAILY_HOURS_EXCEEDED |
WARNING |
> 8 h 00 d'affectations cumulées sur une même journée. |
INSUFFICIENT_REST |
WARNING |
< 8 h 00 entre la fin d'un créneau et le début du suivant. |
MISSING_REQUIRED_SKILL |
WARNING |
Le bénévole n'a pas une compétence requise par le créneau. |
Sémantique figée :
- Statuts pris en compte pour chevauchement / cumul / repos :
PROPOSED+CONFIRMED.DECLINEDetCANCELLEDsont ignorés. - « Journée » (cumul 8 h) : du début du premier
DayPartdu jour au début du premierDayPartdu lendemain. - Cumul :
> 8 h 00strict (8 h 00 pile passe). - Repos :
< 8 h 00strict. - Créneaux jointifs (
end_at == start_at) : pas un chevauchement. - Une violation
BLOCKINGempêche l'enregistrement, sans dérogation. - Une violation
WARNINGest retournée à l'appelant mais n'empêche rien (l'équipe connaît des exceptions légitimes).
4. Périmètre
Premier incrément (en cours)
- Modèle de données complet + migrations Alembic.
- Import CSV/Excel (un seul fichier bénévoles + disponibilités), en deux
temps : analyse (colonnes détectées, aperçu) puis exécution avec mapping
validé. Rapport ligne par ligne. Détection des doublons : upsert sur
email; sans email, détection sur (last_name,first_name) signalée sans upsert automatique. Mode simulation qui ne persiste rien. - API CRUD sur
Position,Shift,Volunteer,Assignment. - Moteur de validation (section 3).
- Endpoint matrice d'une édition :
[{ position, shifts: [{ shift, min_staff, ideal_staff, confirmed, proposed }] }]. - Jeu de données de démonstration : 120 bénévoles, 15 postes, 4 jours, quelques trous d'effectif, 2–3 avertissements actifs.
Hors périmètre (le modèle doit juste pouvoir les accueillir sans refonte)
Solveur OR-Tools · envoi d'emails / SMS · constructeur de formulaire · pointage sur site · fonctions d'IA.
5. Arborescence
.
├── CLAUDE.md
├── README.md
├── docker-compose.yml # dev : db + backend (hot reload)
├── .env.example
├── .github/workflows/deploy.yml # CI/CD : build → GHCR → SSH sur le VPS
├── deploy/
│ ├── docker-compose.prod.yml
│ └── README.md # secrets requis, setup VPS
├── docker/postgres-init/ # création de la base de test au 1er boot
├── backend/
│ ├── pyproject.toml # géré par uv
│ ├── Dockerfile
│ ├── alembic.ini
│ ├── app/
│ │ ├── main.py # instance FastAPI
│ │ ├── config.py # settings (pydantic-settings)
│ │ ├── db.py # engine + session
│ │ ├── logging_config.py # RGPD : pas de PII dans les logs
│ │ ├── api/ # routers (health, imports)
│ │ ├── importing/ # import CSV/Excel (parser, values, service)
│ │ ├── models/ # tables SQLModel
│ │ ├── validation/ # moteur de règles (cœur du produit)
│ │ ├── i18n/
│ │ │ ├── __init__.py # t(key, **params)
│ │ │ └── locales/fr.json
│ │ └── migrations/ # Alembic (env.py, versions/)
│ └── tests/
│ ├── conftest.py # vraie base de test PostgreSQL
│ └── ...
└── frontend/ # React + Vite + TS + Tailwind (étape ultérieure)
6. Commandes
Aucune dépendance locale requise hors Docker.
| Action | Commande |
|---|---|
| Lancer en dev | docker compose up --build |
| API + docs interactives | http://localhost:8000/docs |
| Tests backend | docker compose run --rm backend uv run pytest |
| Couverture | docker compose run --rm backend uv run pytest --cov=app |
| Lint | docker compose run --rm backend uv run ruff check . |
| Format | docker compose run --rm backend uv run ruff format . |
| Créer une migration | docker compose run --rm backend uv run alembic revision --autogenerate -m "..." |
| Appliquer les migrations | docker compose run --rm backend uv run alembic upgrade head |
| Vérifier migration ↔ modèle | docker compose run --rm backend uv run alembic check (après upgrade head) |
| Charger le jeu de démo | (étape ultérieure) docker compose run --rm backend uv run python -m app.seed |
7. Conventions
- Code, base, noms de fichiers en anglais. Le français vit dans les fichiers de traduction et les commits.
- Aucun texte visible par l'utilisateur en dur. Backend :
app.i18n.t(key)locales/fr.json. Frontend : fichier i18n dédié. Vrai même en monolingue.
- RGPD :
- Pas de log des noms, emails, téléphones. On logue des identifiants.
Edition.retention_days: durée de rétention paramétrable.- Endpoint de suppression complète d'un bénévole : hard delete en cascade.
- Dates et heures : stockées naïves, interprétées en heure locale Europe/Paris (heure murale). Les règles « 8 h », « journée », « demi-journée » sont toutes en heure locale.
- Validation TDD : tests avant implémentation, un cas passant + un cas déclenchant par règle.
- Git : un commit atomique par étape validée, message en français.
- Étapes courtes : après chaque étape, montrer le résultat et attendre la validation avant de continuer.
8. Déploiement
- Dépôt GitHub. VPS avec Docker.
.github/workflows/deploy.yml: sur chaque pushmain→ build de l'image backend → push surghcr.io/<owner>/<repo>-backend→ SSH sur le VPS →docker compose -f docker-compose.prod.yml pull && up -d.- Secrets requis (voir
deploy/README.md) :SSH_HOST,SSH_USER,SSH_KEY,DEPLOY_PATH,GHCR_TOKEN. - Le VPS porte un fichier
.env(non versionné) avecPOSTGRES_USER,POSTGRES_PASSWORD,POSTGRES_DB,JWT_SECRET.
9. Journal des étapes
| Étape | Description | État |
|---|---|---|
| 1 | Squelette : monorepo, Docker, CI/CD, backend qui démarre | validée |
| 2 | Modèle de données complet + migration Alembic initiale | validée |
| 3 | Import CSV/Excel (analyse + exécution, rapport, doublons, simulation) | à valider |
En attente : la couche d'authentification (lien magique,
Account, JWT, dépendances de rôle) fera l'objet d'une étape dédiée. Les endpoints actuels ne sont pas encore protégés.