Bénévole : vœux d'artistes (entité Artist) et vœux de postes

Nouvelle entité `Artist` (nom + jour de passage), CRUD
`/editions/{id}/artists` (lecture ouverte à tout compte de l'édition,
écriture COORDINATION), unicité (édition, nom, date).

La fiche bénévole porte deux listes de vœux, sur le modèle de
`skill_codes` (None = inchangé, [] = vider) :
- `artist_ids` : artistes que le bénévole veut voir (`volunteer_artist`) ;
- `wished_position_ids` : postes qu'il voudrait tenir
  (`volunteer_position_wish`), distinct de l'affectation.

Un id étranger à l'édition renvoie 422 (`artist.unknown` /
`position.unknown`). Suppression d'un artiste : cascade sur les vœux.
Migration 0006. Jeu de démonstration et frontend inchangés (le front
suivra).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
This commit is contained in:
AdamThomas
2026-09-10 22:12:55 +02:00
co-authored by Claude Sonnet 5
parent 609514369a
commit 24caece7df
13 changed files with 618 additions and 11 deletions
+109 -2
View File
@@ -15,7 +15,18 @@ from app.auth.deps import (
require_staff,
)
from app.db import get_session
from app.models import Account, Edition, Role, Skill, Volunteer, VolunteerSkill
from app.models import (
Account,
Artist,
Edition,
Position,
Role,
Skill,
Volunteer,
VolunteerArtist,
VolunteerPositionWish,
VolunteerSkill,
)
router = APIRouter(
prefix="/editions/{edition_id}/volunteers", tags=["volunteers"]
@@ -40,10 +51,39 @@ def _skill_codes(session: Session, volunteer_id: uuid.UUID) -> list[str]:
)
def _artist_ids(session: Session, volunteer_id: uuid.UUID) -> list[uuid.UUID]:
return list(
session.exec(
select(Artist.id)
.join(VolunteerArtist, VolunteerArtist.artist_id == Artist.id)
.where(VolunteerArtist.volunteer_id == volunteer_id)
.order_by(Artist.performance_date, Artist.name)
)
)
def _wished_position_ids(
session: Session, volunteer_id: uuid.UUID
) -> list[uuid.UUID]:
return list(
session.exec(
select(Position.id)
.join(
VolunteerPositionWish,
VolunteerPositionWish.position_id == Position.id,
)
.where(VolunteerPositionWish.volunteer_id == volunteer_id)
.order_by(Position.name)
)
)
def _read(session: Session, volunteer: Volunteer) -> VolunteerRead:
return VolunteerRead(
**volunteer.model_dump(),
skill_codes=_skill_codes(session, volunteer.id),
artist_ids=_artist_ids(session, volunteer.id),
wished_position_ids=_wished_position_ids(session, volunteer.id),
)
@@ -62,6 +102,63 @@ def _set_skills(
)
def _resolve_owned_ids(
session: Session, model, edition_id: uuid.UUID, ids: list[uuid.UUID], code: str
) -> list[uuid.UUID]:
"""Ordre d'entrée conservé, doublons retirés ; 422 si un id est étranger."""
wanted = list(dict.fromkeys(ids))
if wanted:
found = set(
session.exec(
select(model.id).where(
model.edition_id == edition_id,
model.id.in_(wanted),
)
)
)
if len(found) != len(wanted):
raise HTTPException(status_code=422, detail={"code": code})
return wanted
def _set_artists(
session: Session, volunteer: Volunteer, artist_ids: list[uuid.UUID]
) -> None:
ids = _resolve_owned_ids(
session, Artist, volunteer.edition_id, artist_ids, "artist.unknown"
)
replace_links(
session,
VolunteerArtist,
filter_column=VolunteerArtist.volunteer_id,
filter_value=volunteer.id,
pairs=[
{"volunteer_id": volunteer.id, "artist_id": aid} for aid in ids
],
)
def _set_wished_positions(
session: Session, volunteer: Volunteer, position_ids: list[uuid.UUID]
) -> None:
ids = _resolve_owned_ids(
session,
Position,
volunteer.edition_id,
position_ids,
"position.unknown",
)
replace_links(
session,
VolunteerPositionWish,
filter_column=VolunteerPositionWish.volunteer_id,
filter_value=volunteer.id,
pairs=[
{"volunteer_id": volunteer.id, "position_id": pid} for pid in ids
],
)
@router.post("", response_model=VolunteerRead, status_code=status.HTTP_201_CREATED)
def create_volunteer(
payload: VolunteerCreate,
@@ -71,7 +168,9 @@ def create_volunteer(
) -> VolunteerRead:
volunteer = Volunteer(
edition_id=edition.id,
**payload.model_dump(exclude={"skill_codes"}),
**payload.model_dump(
exclude={"skill_codes", "artist_ids", "wished_position_ids"}
),
)
session.add(volunteer)
try:
@@ -82,6 +181,8 @@ def create_volunteer(
status_code=409, detail={"code": "volunteer.duplicate_email"}
) from exc
_set_skills(session, volunteer, payload.skill_codes)
_set_artists(session, volunteer, payload.artist_ids)
_set_wished_positions(session, volunteer, payload.wished_position_ids)
session.commit()
session.refresh(volunteer)
return _read(session, volunteer)
@@ -124,6 +225,8 @@ def update_volunteer(
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)
artist_ids = changes.pop("artist_ids", None)
wished_position_ids = changes.pop("wished_position_ids", None)
for field, value in changes.items():
setattr(volunteer, field, value)
try:
@@ -135,6 +238,10 @@ def update_volunteer(
) from exc
if skill_codes is not None:
_set_skills(session, volunteer, skill_codes)
if artist_ids is not None:
_set_artists(session, volunteer, artist_ids)
if wished_position_ids is not None:
_set_wished_positions(session, volunteer, wished_position_ids)
session.commit()
session.refresh(volunteer)
return _read(session, volunteer)