"""CRUD des postes. `lead_account_id` (référent) ne peut désigner qu'un compte `Role.LEAD` de la même édition — voir `_check_lead_account`. C'est le seul moyen de donner à un bénévole une vue référent (`/mobile-referent`) à la connexion ; voir `api/accounts.py:update_account` pour le faire passer de VOLUNTEER à LEAD.""" import uuid from fastapi import APIRouter, Depends, HTTPException, Response, status from sqlmodel import Session, select from app.api._common import owned_or_404 from app.api._shiftgen import generate_missing_shifts from app.api.deps import get_edition from app.api.schemas import ( GenerateShiftsResult, PositionCreate, PositionRead, PositionUpdate, ) from app.auth.deps import ( current_account_in_edition, ensure_can_write_position, require_coordinator, require_staff, ) from app.db import get_session from app.models import Account, Edition, Position, Role router = APIRouter( prefix="/editions/{edition_id}/positions", tags=["positions"] ) def _check_lead_account( session: Session, edition: Edition, lead_account_id: uuid.UUID | None ) -> None: """`lead_account_id` doit désigner un compte RÉFÉRENT (`Role.LEAD`) de cette même édition — sinon le poste se retrouve sans référent utilisable (cf. `ensure_can_write_position`, `auth/deps.py`).""" if lead_account_id is None: return lead = session.get(Account, lead_account_id) if lead is None or lead.edition_id != edition.id or lead.role is not Role.LEAD: raise HTTPException( status_code=422, detail={"code": "position.lead_must_be_referent_account"} ) @router.post("", response_model=PositionRead, status_code=status.HTTP_201_CREATED) def create_position( payload: PositionCreate, edition: Edition = Depends(get_edition), _guard: Account = Depends(require_coordinator), session: Session = Depends(get_session), ) -> Position: _check_lead_account(session, edition, payload.lead_account_id) position = Position(edition_id=edition.id, **payload.model_dump()) session.add(position) session.flush() # Un créneau à besoin 0/0 par jour et par créneau type de l'édition. generate_missing_shifts(session, edition, position) session.commit() session.refresh(position) return position @router.get("", response_model=list[PositionRead]) def list_positions( edition: Edition = Depends(get_edition), _guard: Account = Depends(require_staff), session: Session = Depends(get_session), ) -> list[Position]: return list( session.exec( select(Position) .where(Position.edition_id == edition.id) .order_by(Position.name) ) ) @router.get("/{position_id}", response_model=PositionRead) def get_position( position_id: uuid.UUID, edition: Edition = Depends(get_edition), _guard: Account = Depends(require_staff), session: Session = Depends(get_session), ) -> Position: return owned_or_404(session.get(Position, position_id), edition.id) @router.patch("/{position_id}", response_model=PositionRead) def update_position( position_id: uuid.UUID, payload: PositionUpdate, edition: Edition = Depends(get_edition), account: Account = Depends(current_account_in_edition), session: Session = Depends(get_session), ) -> Position: position = owned_or_404(session.get(Position, position_id), edition.id) ensure_can_write_position(account, position) changes = payload.model_dump(exclude_unset=True) _check_lead_account(session, edition, changes.get("lead_account_id")) for field, value in changes.items(): setattr(position, field, value) session.commit() session.refresh(position) return position @router.post( "/{position_id}/generate-shifts", response_model=GenerateShiftsResult ) def generate_shifts( position_id: uuid.UUID, edition: Edition = Depends(get_edition), account: Account = Depends(current_account_in_edition), session: Session = Depends(get_session), ) -> GenerateShiftsResult: """(Re)génère les créneaux manquants du poste après un ajout de créneau type ou une extension des dates de l'édition. N'écrase rien d'existant.""" position = owned_or_404(session.get(Position, position_id), edition.id) ensure_can_write_position(account, position) created = generate_missing_shifts(session, edition, position) session.commit() return GenerateShiftsResult(created=created) @router.delete("/{position_id}", status_code=status.HTTP_204_NO_CONTENT) def delete_position( position_id: uuid.UUID, edition: Edition = Depends(get_edition), _guard: Account = Depends(require_coordinator), session: Session = Depends(get_session), ) -> Response: position = owned_or_404(session.get(Position, position_id), edition.id) session.delete(position) session.commit() return Response(status_code=status.HTTP_204_NO_CONTENT)