Artist gagne start_time/end_time (end <= start ⇒ finit le lendemain, même convention que Shift/DayPart/Availability). Migration 0008. Le solveur (app/solver/) résout maintenant les fenêtres de passage des artistes que chaque bénévole veut voir (VolunteerArtist) et pénalise toute suggestion qui chevaucherait un passage souhaité — le bénévole raterait l'artiste. Comme le reste du scoring, c'est un terme de l'objectif, pas un blocage : pour un créneau urgent sans autre candidat, le solveur propose quand même, quitte à ce que ce soit un pis-aller. - SolverVolunteer.wished_artist_windows (fenêtres ShiftInfo précalculées par le loader depuis Artist + VolunteerArtist). - Nouveau malus _ARTIST_CONFLICT_PENALTY dans engine.py. - Tests : moteur (conflit détecté/ignoré hors chevauchement/toujours suggéré en dernier recours) + intégration API bout en bout. 176 tests, ruff et alembic check au vert. CLAUDE.md mis à jour (§2 et §4). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
113 lines
3.7 KiB
Python
113 lines
3.7 KiB
Python
"""CRUD de la programmation (`Artist`) d'une édition.
|
|
|
|
La coordination constitue la liste ; elle est présentée par jour et heure de
|
|
passage (`performance_date`, `start_time`, `end_time`). Les bénévoles y
|
|
choisissent les artistes qu'ils veulent voir (vœux stockés dans
|
|
`volunteer_artist`, exposés par la fiche bénévole) — le solveur en tient
|
|
compte pour éviter de les affecter pendant le passage."""
|
|
|
|
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
|
|
from app.api.deps import get_edition
|
|
from app.api.schemas import ArtistCreate, ArtistRead, ArtistUpdate
|
|
from app.auth.deps import current_account_in_edition, require_coordinator
|
|
from app.db import get_session
|
|
from app.models import Account, Artist, Edition
|
|
|
|
router = APIRouter(prefix="/editions/{edition_id}/artists", tags=["artists"])
|
|
|
|
|
|
@router.get("", response_model=list[ArtistRead])
|
|
def list_artists(
|
|
edition: Edition = Depends(get_edition),
|
|
_account: Account = Depends(current_account_in_edition),
|
|
session: Session = Depends(get_session),
|
|
) -> list[Artist]:
|
|
return list(
|
|
session.exec(
|
|
select(Artist)
|
|
.where(Artist.edition_id == edition.id)
|
|
.order_by(Artist.performance_date, Artist.name)
|
|
)
|
|
)
|
|
|
|
|
|
@router.post("", response_model=ArtistRead, status_code=status.HTTP_201_CREATED)
|
|
def create_artist(
|
|
payload: ArtistCreate,
|
|
edition: Edition = Depends(get_edition),
|
|
_guard: Account = Depends(require_coordinator),
|
|
session: Session = Depends(get_session),
|
|
) -> Artist:
|
|
name = payload.name.strip()
|
|
if not name:
|
|
raise HTTPException(
|
|
status_code=422, detail={"code": "artist.name_required"}
|
|
)
|
|
artist = Artist(
|
|
edition_id=edition.id,
|
|
name=name,
|
|
performance_date=payload.performance_date,
|
|
start_time=payload.start_time,
|
|
end_time=payload.end_time,
|
|
)
|
|
session.add(artist)
|
|
try:
|
|
session.commit()
|
|
except IntegrityError as exc:
|
|
session.rollback()
|
|
raise HTTPException(
|
|
status_code=409, detail={"code": "artist.duplicate"}
|
|
) from exc
|
|
session.refresh(artist)
|
|
return artist
|
|
|
|
|
|
@router.patch("/{artist_id}", response_model=ArtistRead)
|
|
def update_artist(
|
|
artist_id: uuid.UUID,
|
|
payload: ArtistUpdate,
|
|
edition: Edition = Depends(get_edition),
|
|
_guard: Account = Depends(require_coordinator),
|
|
session: Session = Depends(get_session),
|
|
) -> Artist:
|
|
artist = owned_or_404(session.get(Artist, artist_id), edition.id)
|
|
changes = payload.model_dump(exclude_unset=True)
|
|
if "name" in changes:
|
|
name = (changes["name"] or "").strip()
|
|
if not name:
|
|
raise HTTPException(
|
|
status_code=422, detail={"code": "artist.name_required"}
|
|
)
|
|
changes["name"] = name
|
|
for field, value in changes.items():
|
|
setattr(artist, field, value)
|
|
try:
|
|
session.commit()
|
|
except IntegrityError as exc:
|
|
session.rollback()
|
|
raise HTTPException(
|
|
status_code=409, detail={"code": "artist.duplicate"}
|
|
) from exc
|
|
session.refresh(artist)
|
|
return artist
|
|
|
|
|
|
@router.delete("/{artist_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
def delete_artist(
|
|
artist_id: uuid.UUID,
|
|
edition: Edition = Depends(get_edition),
|
|
_guard: Account = Depends(require_coordinator),
|
|
session: Session = Depends(get_session),
|
|
) -> Response:
|
|
artist = owned_or_404(session.get(Artist, artist_id), edition.id)
|
|
# Cascade base : les vœux `volunteer_artist` sont supprimés.
|
|
session.delete(artist)
|
|
session.commit()
|
|
return Response(status_code=status.HTTP_204_NO_CONTENT)
|