Étape 3 : import CSV/Excel des bénévoles et disponibilités

- 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
This commit is contained in:
AdamThomas
2026-09-09 22:37:30 +02:00
co-authored by Claude Sonnet 5
parent cb2e84adf0
commit bb919554ff
19 changed files with 1443 additions and 5 deletions
+55
View File
@@ -0,0 +1,55 @@
"""Interprétation des valeurs de cellule : disponibilités et tailles de t-shirt."""
from app.importing.text import normalize
from app.models import AvailabilityStatus, TshirtSize
_AVAILABLE = {
"x", "o", "oui", "y", "yes", "ok", "1", "vrai", "true",
"dispo", "disponible", "present", "presente",
"matin", "aprem", "apres-midi", "apres midi", "midi", "soir", "soiree",
"journee", "jour",
}
_IF_NEEDED = {
"si besoin", "sib", "peut-etre", "peut etre", "?", "2",
"backup", "reserve", "au besoin",
}
_UNAVAILABLE = {
"non", "n", "no", "0", "faux", "false", "indispo", "indisponible", "absent",
"-", "x-",
}
_EMPTY = {"", "na", "n/a", "nc"}
_TSHIRT = {
"XS": TshirtSize.XS,
"S": TshirtSize.S,
"M": TshirtSize.M,
"L": TshirtSize.L,
"XL": TshirtSize.XL,
"XXL": TshirtSize.XXL,
"2XL": TshirtSize.XXL,
"XXXL": TshirtSize.XXXL,
"3XL": TshirtSize.XXXL,
}
def parse_availability_cell(value: str) -> tuple[AvailabilityStatus | None, bool]:
"""(statut, reconnu).
- statut `None` + reconnu `True` → cellule vide, pas de déclaration.
- statut `None` + reconnu `False` → valeur non comprise, à signaler.
"""
norm = normalize(value)
if norm in _EMPTY:
return None, True
if norm in _AVAILABLE:
return AvailabilityStatus.AVAILABLE, True
if norm in _IF_NEEDED:
return AvailabilityStatus.IF_NEEDED, True
if norm in _UNAVAILABLE:
return AvailabilityStatus.UNAVAILABLE, True
return None, False
def parse_tshirt_size(value: str) -> TshirtSize | None:
key = normalize(value).upper().replace(" ", "").replace("-", "")
return _TSHIRT.get(key)