Compétences supprimées de bout en bout :
- tables `skill`, `volunteer_skill`, `shift_required_skill` ;
- `Volunteer.skill_codes`, `Shift.required_skill_codes`, colonne `skills`
de l'import, règle de validation `MISSING_REQUIRED_SKILL`, jeu de démo.
Disponibilités repensées : `Availability` passe de (`day_part_id`,
`status` tri-état) à une **fenêtre horaire libre** (`date`, `start_time`,
`end_time` ; `end <= start` ⇒ passe minuit), plusieurs par jour.
- Nouveaux endpoints `GET`/`PUT /editions/{id}/volunteers/{vid}/availability`
(PUT = remplacement complet, COORDINATION) et
`GET /editions/{id}/availability` (toute l'édition, pour la grille).
- `OUTSIDE_AVAILABILITY` (toujours WARNING) : compare le créneau à l'union
des fenêtres du bénévole ce jour-là ; renvoie `date` + `uncovered_minutes`.
`DayPart` ne sert plus qu'à la borne de « journée » du cumul 8 h.
- L'import ne gère plus les disponibilités (fenêtres saisies dans l'app) :
`values.py` supprimé, mapping réduit à nom/email/téléphone/commentaire.
Migration 0007 (recrée `availability`). `AvailabilityStatus` supprimé.
158 tests, `alembic check` et `ruff` au vert. Front à suivre.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
125 lines
4.0 KiB
Python
125 lines
4.0 KiB
Python
"""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
|
|
from app.api.deps import get_edition
|
|
from app.api.schemas import ShiftCreate, ShiftRead, ShiftUpdate
|
|
from app.auth.deps import (
|
|
current_account_in_edition,
|
|
ensure_can_write_position,
|
|
ensure_can_write_shift,
|
|
require_staff,
|
|
)
|
|
from app.db import get_session
|
|
from app.models import Account, Edition, Position, Shift
|
|
|
|
router = APIRouter(prefix="/editions/{edition_id}/shifts", tags=["shifts"])
|
|
|
|
|
|
def _read(shift: Shift) -> ShiftRead:
|
|
return ShiftRead(**shift.model_dump())
|
|
|
|
|
|
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),
|
|
account: Account = Depends(current_account_in_edition),
|
|
session: Session = Depends(get_session),
|
|
) -> ShiftRead:
|
|
_check_range(payload.starts_at, payload.ends_at)
|
|
position = _require_position(session, payload.position_id, edition.id)
|
|
ensure_can_write_position(account, position)
|
|
shift = Shift(edition_id=edition.id, **payload.model_dump())
|
|
session.add(shift)
|
|
session.commit()
|
|
session.refresh(shift)
|
|
return _read(shift)
|
|
|
|
|
|
@router.get("", response_model=list[ShiftRead])
|
|
def list_shifts(
|
|
position_id: uuid.UUID | None = None,
|
|
edition: Edition = Depends(get_edition),
|
|
_guard: Account = Depends(require_staff),
|
|
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(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),
|
|
_guard: Account = Depends(require_staff),
|
|
session: Session = Depends(get_session),
|
|
) -> ShiftRead:
|
|
shift = owned_or_404(session.get(Shift, shift_id), edition.id)
|
|
return _read(shift)
|
|
|
|
|
|
@router.patch("/{shift_id}", response_model=ShiftRead)
|
|
def update_shift(
|
|
shift_id: uuid.UUID,
|
|
payload: ShiftUpdate,
|
|
edition: Edition = Depends(get_edition),
|
|
account: Account = Depends(current_account_in_edition),
|
|
session: Session = Depends(get_session),
|
|
) -> ShiftRead:
|
|
shift = owned_or_404(session.get(Shift, shift_id), edition.id)
|
|
ensure_can_write_shift(account, session, shift)
|
|
changes = payload.model_dump(exclude_unset=True)
|
|
if "position_id" in changes:
|
|
target = _require_position(session, changes["position_id"], edition.id)
|
|
ensure_can_write_position(account, target)
|
|
for field, value in changes.items():
|
|
setattr(shift, field, value)
|
|
_check_range(shift.starts_at, shift.ends_at)
|
|
session.commit()
|
|
session.refresh(shift)
|
|
return _read(shift)
|
|
|
|
|
|
@router.delete("/{shift_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
def delete_shift(
|
|
shift_id: uuid.UUID,
|
|
edition: Edition = Depends(get_edition),
|
|
account: Account = Depends(current_account_in_edition),
|
|
session: Session = Depends(get_session),
|
|
) -> Response:
|
|
shift = owned_or_404(session.get(Shift, shift_id), edition.id)
|
|
ensure_can_write_shift(account, session, shift)
|
|
session.delete(shift)
|
|
session.commit()
|
|
return Response(status_code=status.HTTP_204_NO_CONTENT)
|