Étape 2 : modèle de données complet et migration initiale

- Tables SQLModel : Edition, DayPart, Volunteer, Skill, Position, Shift,
  Availability, Assignment, Account, MagicLinkToken + tables d'association
- Multi-édition (edition_id partout), énumérations en VARCHAR + CHECK,
  suppressions ON DELETE CASCADE (droit à l'effacement)
- Dates/heures naïves en heure locale Europe/Paris (tzdata + TZ conteneur)
- Migration Alembic 0001 écrite à la main, vérifiée par `alembic check`
  (aucun écart avec les modèles) et par un aller-retour downgrade/upgrade
- Tests : graphe complet, unicité email par édition, N-N compétences,
  aller-retour d'énumération, cascade de suppression d'un bénévole

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
This commit is contained in:
AdamThomas
2026-09-09 22:25:29 +02:00
co-authored by Claude Sonnet 5
parent 09d7d70076
commit cb2e84adf0
11 changed files with 1068 additions and 28 deletions
+45 -19
View File
@@ -51,25 +51,49 @@ Deux tables distinctes, deux écrans distincts, deux vocabulaires distincts.
## 2. Modèle de domaine
- **`Edition`** : année, `start_date`, `end_date`, `retention_days`
(durée de rétention des données nominatives, paramétrable), liste de
`DayPart`.
- **`DayPart`** : découpage d'une journée propre à l'édition (matin,
après-midi, soirée… nombre et bornes libres). Une `Availability` référence
un `DayPart` + une date.
- **`Volunteer`** : `email` (identifiant fonctionnel unique **par édition**,
nullable pour les bénévoles créés à la main), `first_name`, `last_name`,
`phone`, `tshirt_size`, `diet`, `comment`. Pas de date de naissance.
- **`Skill`** : référentiel par édition (SST, permis B, régie son, langues…).
N-N avec `Volunteer`. N-N avec `Shift` (compétences requises).
- **`Position`** : `name`, `location`, `category`, `lead_account_id`.
- **`Shift`** : `position_id`, `start_at`, `end_at` (peut finir le lendemain),
`min_staff`, `ideal_staff`, compétences requises (N-N `Skill`).
- **`Assignment`** : `volunteer_id`, `shift_id`, `status`, `source`, `locked`.
- **`Account`** : `email`, `role`, `edition_id`, `volunteer_id` (nullable).
- **`MagicLinkToken`** : `account_id`, `token_hash`, `expires_at`, `used_at`.
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.
Multi-édition : toute entité métier porte un `edition_id`.
- **`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 — plusieurs `NULL` autorisés), `first_name`,
`last_name`, `phone`, `tshirt_size`, `diet`, `comment`. Pas de date de
naissance.
- **`Skill`** : référentiel par édition (`code`, `label`). N-N `Volunteer`
(`volunteer_skill`). N-N `Shift` (`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-N `Skill`).
- **`Availability`** : `volunteer_id`, `date`, `day_part_id`, `status`
(`AvailabilityStatus`). Unique (`volunteer_id`, `date`, `day_part_id`).
- **`Assignment`** : `volunteer_id`, `shift_id`, `status`
(`AssignmentStatus`, défaut `PROPOSED`), `source` (`AssignmentSource`,
défaut `MANUAL`), `locked` (défaut `false`). 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 les
`edition_id` et sur les liens vers `volunteer`, `shift`, `position`,
`skill`, `account`. La suppression complète d'un bénévole (RGPD) efface donc
ses `Availability`, `Assignment`, liens de compétence et son `Account`.
- Dates/heures **naïves**, heure locale Europe/Paris (`TZ` du conteneur +
`tzdata`). `app.models.local_now()` pour les horodatages.
### Rôles et accès
@@ -192,6 +216,7 @@ Aucune dépendance locale requise hors Docker.
| 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` |
---
@@ -234,4 +259,5 @@ Aucune dépendance locale requise hors Docker.
| Étape | Description | État |
|-------|-------------------------------------------------------|----------|
| 1 | Squelette : monorepo, Docker, CI/CD, backend qui démarre | à valider |
| 1 | Squelette : monorepo, Docker, CI/CD, backend qui démarre | validée |
| 2 | Modèle de données complet + migration Alembic initiale | à valider |