"""CRUD des bénévoles. La suppression est un effacement complet (RGPD).""" import uuid from fastapi import APIRouter, Depends, HTTPException, Response, status from sqlalchemy.exc import IntegrityError from sqlmodel import Session, select from app.api._common import owned_or_404, replace_links, resolve_skills from app.api.deps import get_edition from app.api.schemas import VolunteerCreate, VolunteerRead, VolunteerUpdate from app.db import get_session from app.models import Edition, Skill, Volunteer, VolunteerSkill router = APIRouter( prefix="/editions/{edition_id}/volunteers", tags=["volunteers"] ) def _skill_codes(session: Session, volunteer_id: uuid.UUID) -> list[str]: return sorted( session.exec( select(Skill.code) .join(VolunteerSkill, VolunteerSkill.skill_id == Skill.id) .where(VolunteerSkill.volunteer_id == volunteer_id) ) ) def _read(session: Session, volunteer: Volunteer) -> VolunteerRead: return VolunteerRead( **volunteer.model_dump(), skill_codes=_skill_codes(session, volunteer.id), ) def _set_skills( session: Session, volunteer: Volunteer, codes: list[str] ) -> None: skills = resolve_skills(session, volunteer.edition_id, codes) replace_links( session, VolunteerSkill, filter_column=VolunteerSkill.volunteer_id, filter_value=volunteer.id, pairs=[ {"volunteer_id": volunteer.id, "skill_id": s.id} for s in skills ], ) @router.post("", response_model=VolunteerRead, status_code=status.HTTP_201_CREATED) def create_volunteer( payload: VolunteerCreate, edition: Edition = Depends(get_edition), session: Session = Depends(get_session), ) -> VolunteerRead: volunteer = Volunteer( edition_id=edition.id, **payload.model_dump(exclude={"skill_codes"}), ) session.add(volunteer) try: session.flush() except IntegrityError as exc: session.rollback() raise HTTPException( status_code=409, detail={"code": "volunteer.duplicate_email"} ) from exc _set_skills(session, volunteer, payload.skill_codes) session.commit() session.refresh(volunteer) return _read(session, volunteer) @router.get("", response_model=list[VolunteerRead]) def list_volunteers( edition: Edition = Depends(get_edition), session: Session = Depends(get_session), ) -> list[VolunteerRead]: volunteers = session.exec( select(Volunteer) .where(Volunteer.edition_id == edition.id) .order_by(Volunteer.last_name, Volunteer.first_name) ) return [_read(session, v) for v in volunteers] @router.get("/{volunteer_id}", response_model=VolunteerRead) def get_volunteer( volunteer_id: uuid.UUID, edition: Edition = Depends(get_edition), session: Session = Depends(get_session), ) -> VolunteerRead: volunteer = owned_or_404(session.get(Volunteer, volunteer_id), edition.id) return _read(session, volunteer) @router.patch("/{volunteer_id}", response_model=VolunteerRead) def update_volunteer( volunteer_id: uuid.UUID, payload: VolunteerUpdate, edition: Edition = Depends(get_edition), session: Session = Depends(get_session), ) -> VolunteerRead: volunteer = owned_or_404(session.get(Volunteer, volunteer_id), edition.id) changes = payload.model_dump(exclude_unset=True) skill_codes = changes.pop("skill_codes", None) for field, value in changes.items(): setattr(volunteer, field, value) try: session.flush() except IntegrityError as exc: session.rollback() raise HTTPException( status_code=409, detail={"code": "volunteer.duplicate_email"} ) from exc if skill_codes is not None: _set_skills(session, volunteer, skill_codes) session.commit() session.refresh(volunteer) return _read(session, volunteer) @router.delete("/{volunteer_id}", status_code=status.HTTP_204_NO_CONTENT) def delete_volunteer( volunteer_id: uuid.UUID, edition: Edition = Depends(get_edition), session: Session = Depends(get_session), ) -> Response: volunteer = owned_or_404(session.get(Volunteer, volunteer_id), edition.id) # Effacement complet : la cascade base supprime disponibilités, # affectations, liens de compétence et compte éventuel. session.delete(volunteer) session.commit() return Response(status_code=status.HTTP_204_NO_CONTENT)