"""CRUD des créneaux.""" import uuid from fastapi import APIRouter, Depends, HTTPException, Response, status 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 ShiftCreate, ShiftRead, ShiftUpdate from app.db import get_session from app.models import Edition, Position, Shift, ShiftRequiredSkill, Skill router = APIRouter(prefix="/editions/{edition_id}/shifts", tags=["shifts"]) def _skill_codes(session: Session, shift_id: uuid.UUID) -> list[str]: return sorted( session.exec( select(Skill.code) .join(ShiftRequiredSkill, ShiftRequiredSkill.skill_id == Skill.id) .where(ShiftRequiredSkill.shift_id == shift_id) ) ) def _read(session: Session, shift: Shift) -> ShiftRead: return ShiftRead( **shift.model_dump(), required_skill_codes=_skill_codes(session, shift.id), ) def _set_skills(session: Session, shift: Shift, codes: list[str]) -> None: skills = resolve_skills(session, shift.edition_id, codes) replace_links( session, ShiftRequiredSkill, filter_column=ShiftRequiredSkill.shift_id, filter_value=shift.id, pairs=[{"shift_id": shift.id, "skill_id": s.id} for s in skills], ) def _check_range(starts_at, ends_at) -> None: if ends_at <= starts_at: raise HTTPException( status_code=422, detail={"code": "shift.ends_not_after_starts"}, ) def _require_position( session: Session, position_id: uuid.UUID, edition_id: uuid.UUID ) -> Position: position = session.get(Position, position_id) if position is None or position.edition_id != edition_id: raise HTTPException( status_code=422, detail={"code": "shift.position_wrong_edition"} ) return position @router.post("", response_model=ShiftRead, status_code=status.HTTP_201_CREATED) def create_shift( payload: ShiftCreate, edition: Edition = Depends(get_edition), session: Session = Depends(get_session), ) -> ShiftRead: _check_range(payload.starts_at, payload.ends_at) _require_position(session, payload.position_id, edition.id) shift = Shift( edition_id=edition.id, **payload.model_dump(exclude={"required_skill_codes"}), ) session.add(shift) session.flush() _set_skills(session, shift, payload.required_skill_codes) session.commit() session.refresh(shift) return _read(session, shift) @router.get("", response_model=list[ShiftRead]) def list_shifts( position_id: uuid.UUID | None = None, edition: Edition = Depends(get_edition), session: Session = Depends(get_session), ) -> list[ShiftRead]: stmt = ( select(Shift) .where(Shift.edition_id == edition.id) .order_by(Shift.starts_at) ) if position_id is not None: stmt = stmt.where(Shift.position_id == position_id) return [_read(session, s) for s in session.exec(stmt)] @router.get("/{shift_id}", response_model=ShiftRead) def get_shift( shift_id: uuid.UUID, edition: Edition = Depends(get_edition), session: Session = Depends(get_session), ) -> ShiftRead: shift = owned_or_404(session.get(Shift, shift_id), edition.id) return _read(session, shift) @router.patch("/{shift_id}", response_model=ShiftRead) def update_shift( shift_id: uuid.UUID, payload: ShiftUpdate, edition: Edition = Depends(get_edition), session: Session = Depends(get_session), ) -> ShiftRead: shift = owned_or_404(session.get(Shift, shift_id), edition.id) changes = payload.model_dump(exclude_unset=True) skill_codes = changes.pop("required_skill_codes", None) if "position_id" in changes: _require_position(session, changes["position_id"], edition.id) for field, value in changes.items(): setattr(shift, field, value) _check_range(shift.starts_at, shift.ends_at) if skill_codes is not None: _set_skills(session, shift, skill_codes) session.commit() session.refresh(shift) return _read(session, shift) @router.delete("/{shift_id}", status_code=status.HTTP_204_NO_CONTENT) def delete_shift( shift_id: uuid.UUID, edition: Edition = Depends(get_edition), session: Session = Depends(get_session), ) -> Response: shift = owned_or_404(session.get(Shift, shift_id), edition.id) session.delete(shift) session.commit() return Response(status_code=status.HTTP_204_NO_CONTENT)