Files
benevoles/backend/app/api/_common.py
T
AdamThomasandClaude Sonnet 5 74e35f0195 Étape 5 : API CRUD, matrice et validation branchée
- CRUD complet, scopé par édition, sur Position, Shift, Volunteer, Assignment
  (routers app/api/*, schémas Pydantic, dépendance get_edition partagée)
- Volunteer : gestion des compétences par codes (création à la volée),
  suppression = effacement complet en cascade (RGPD)
- Shift : contrôle fin > début, poste de la même édition, compétences requises
- Assignment : toute écriture active passe par le moteur de validation ;
  violation bloquante -> 422 error.validation_failed, avertissements renvoyés
  dans la réponse. Endpoint POST .../assignments/validate en simulation.
  app/validation/loader.py construit la ValidationInput depuis la base.
- GET /editions/{id}/matrix : postes -> créneaux -> {min, ideal, confirmed,
  proposed}, source de l'écran principal
- 30 tests d'API (CRUD, 404/409/422, cascade, blocage, avertissements, matrice,
  loader) ; 104 tests au total ; alembic check toujours vert
- Clés de traduction shift.* / assignment.* / volunteer.* ajoutées

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
2026-09-09 22:54:22 +02:00

56 lines
1.6 KiB
Python

"""Helpers partagés par les routers CRUD."""
import uuid
from collections.abc import Iterable
from fastapi import HTTPException
from sqlalchemy import delete
from sqlmodel import Session, select
from app.models import Skill
def owned_or_404(instance, edition_id: uuid.UUID):
"""404 si l'instance est absente ou rattachée à une autre édition."""
if instance is None or instance.edition_id != edition_id:
raise HTTPException(status_code=404, detail={"code": "error.not_found"})
return instance
def resolve_skills(
session: Session, edition_id: uuid.UUID, codes: Iterable[str]
) -> list[Skill]:
"""Récupère ou crée les compétences de l'édition pour les codes donnés."""
existing = {
s.code: s
for s in session.exec(
select(Skill).where(Skill.edition_id == edition_id)
)
}
resolved: list[Skill] = []
seen: set[str] = set()
for raw in codes:
code = raw.strip()
if not code or code in seen:
continue
seen.add(code)
skill = existing.get(code)
if skill is None:
skill = Skill(edition_id=edition_id, code=code, label=code)
session.add(skill)
session.flush()
existing[code] = skill
resolved.append(skill)
return resolved
def replace_links(
session: Session, link_model, *, filter_column, filter_value, pairs
) -> None:
"""Remplace toutes les lignes d'une table d'association pour une clé."""
session.execute(
delete(link_model).where(filter_column == filter_value)
)
for row in pairs:
session.add(link_model(**row))