- 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
56 lines
1.6 KiB
Python
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))
|