Vœu de compagnon : être affecté avec 0 à N autres bénévoles
Nouveau vœu sur la fiche du bénévole — « je voudrais être affecté avec tel(s) bénévole(s) » — pris en compte à la fois pour le rendre visible et par le solveur. Backend : - `VolunteerCompanionWish` (`volunteer_id`, `companion_id`, à sens unique, `CHECK (volunteer_id <> companion_id)`) — migration 0010. - API : `wished_companion_ids` sur `VolunteerCreate`/`Update`/`Read`, mêmes règles que les autres vœux (ids inconnus → 422 `volunteer.unknown` ; se souhaiter soi-même → 422 `volunteer.cannot_wish_self`). - Solveur (`engine.py`) : bonus (+25 par correspondance) si un des compagnons souhaités du candidat est déjà affecté (décision déjà prise) au créneau — cumulable si plusieurs compagnons sont présents. Comme les autres termes de l'objectif, un bonus/malus, jamais une contrainte. - Seed de démo : ~20 % des bénévoles ont un vœu de compagnon (couple, amis). Frontend : - Fiche bénévole (`ConfigVolunteers.tsx`) : sélecteur de compagnons (ajout et édition), avec une bulle d'aide expliquant le sens unique et l'effet sur le solveur — c'est ce qui « fait apparaître » le lien entre bénévoles. - Panneau d'affectation : badge « avec N compagnon(s) souhaité(s) » sur un candidat, et bonus correspondant dans le classement par défaut (`candidateChanceScore`). - Mock (`runSolver`) : même bonus, pour que la démonstration se comporte pareil que le vrai solveur. CLAUDE.md à jour (modèle de domaine, section 4, journal). Vérifié : 194 tests backend (dont 13 nouveaux), ruff, alembic check ; typecheck/lint/build/smoke frontend ; solveur testé en direct sur le jeu de démo reseedé (0,25 s, 47 suggestions). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
b97b95c606
commit
6a4bcf8782
@@ -49,9 +49,11 @@ class VolunteerCreate(ApiModel):
|
||||
email: str | None = None
|
||||
phone: str | None = None
|
||||
comment: str | None = None
|
||||
# Vœux du bénévole : artistes à voir, postes souhaités.
|
||||
# Vœux du bénévole : artistes à voir, postes souhaités, bénévoles avec
|
||||
# qui il voudrait être affecté (à sens unique, 0 à N).
|
||||
artist_ids: list[uuid.UUID] = Field(default_factory=list)
|
||||
wished_position_ids: list[uuid.UUID] = Field(default_factory=list)
|
||||
wished_companion_ids: list[uuid.UUID] = Field(default_factory=list)
|
||||
|
||||
|
||||
class VolunteerUpdate(ApiModel):
|
||||
@@ -60,9 +62,11 @@ class VolunteerUpdate(ApiModel):
|
||||
email: str | None = None
|
||||
phone: str | None = None
|
||||
comment: str | None = None
|
||||
# None = inchangé ; [] = vider. Idem pour artist_ids / wished_position_ids.
|
||||
# None = inchangé ; [] = vider. Idem pour artist_ids / wished_position_ids
|
||||
# / wished_companion_ids.
|
||||
artist_ids: list[uuid.UUID] | None = None
|
||||
wished_position_ids: list[uuid.UUID] | None = None
|
||||
wished_companion_ids: list[uuid.UUID] | None = None
|
||||
|
||||
|
||||
class VolunteerRead(ApiModel):
|
||||
@@ -75,6 +79,7 @@ class VolunteerRead(ApiModel):
|
||||
comment: str | None
|
||||
artist_ids: list[uuid.UUID]
|
||||
wished_position_ids: list[uuid.UUID]
|
||||
wished_companion_ids: list[uuid.UUID]
|
||||
created_at: dt.datetime
|
||||
updated_at: dt.datetime
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ from app.models import (
|
||||
Role,
|
||||
Volunteer,
|
||||
VolunteerArtist,
|
||||
VolunteerCompanionWish,
|
||||
VolunteerPositionWish,
|
||||
)
|
||||
|
||||
@@ -68,11 +69,26 @@ def _wished_position_ids(
|
||||
)
|
||||
|
||||
|
||||
def _companion_ids(session: Session, volunteer_id: uuid.UUID) -> list[uuid.UUID]:
|
||||
return list(
|
||||
session.exec(
|
||||
select(Volunteer.id)
|
||||
.join(
|
||||
VolunteerCompanionWish,
|
||||
VolunteerCompanionWish.companion_id == Volunteer.id,
|
||||
)
|
||||
.where(VolunteerCompanionWish.volunteer_id == volunteer_id)
|
||||
.order_by(Volunteer.last_name, Volunteer.first_name)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _read(session: Session, volunteer: Volunteer) -> VolunteerRead:
|
||||
return VolunteerRead(
|
||||
**volunteer.model_dump(),
|
||||
artist_ids=_artist_ids(session, volunteer.id),
|
||||
wished_position_ids=_wished_position_ids(session, volunteer.id),
|
||||
wished_companion_ids=_companion_ids(session, volunteer.id),
|
||||
)
|
||||
|
||||
|
||||
@@ -133,6 +149,27 @@ def _set_wished_positions(
|
||||
)
|
||||
|
||||
|
||||
def _set_companions(
|
||||
session: Session, volunteer: Volunteer, companion_ids: list[uuid.UUID]
|
||||
) -> None:
|
||||
ids = _resolve_owned_ids(
|
||||
session, Volunteer, volunteer.edition_id, companion_ids, "volunteer.unknown"
|
||||
)
|
||||
if volunteer.id in ids:
|
||||
raise HTTPException(
|
||||
status_code=422, detail={"code": "volunteer.cannot_wish_self"}
|
||||
)
|
||||
replace_links(
|
||||
session,
|
||||
VolunteerCompanionWish,
|
||||
filter_column=VolunteerCompanionWish.volunteer_id,
|
||||
filter_value=volunteer.id,
|
||||
pairs=[
|
||||
{"volunteer_id": volunteer.id, "companion_id": cid} for cid in ids
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@router.post("", response_model=VolunteerRead, status_code=status.HTTP_201_CREATED)
|
||||
def create_volunteer(
|
||||
payload: VolunteerCreate,
|
||||
@@ -142,12 +179,15 @@ def create_volunteer(
|
||||
) -> VolunteerRead:
|
||||
volunteer = Volunteer(
|
||||
edition_id=edition.id,
|
||||
**payload.model_dump(exclude={"artist_ids", "wished_position_ids"}),
|
||||
**payload.model_dump(
|
||||
exclude={"artist_ids", "wished_position_ids", "wished_companion_ids"}
|
||||
),
|
||||
)
|
||||
session.add(volunteer)
|
||||
session.flush()
|
||||
_set_artists(session, volunteer, payload.artist_ids)
|
||||
_set_wished_positions(session, volunteer, payload.wished_position_ids)
|
||||
_set_companions(session, volunteer, payload.wished_companion_ids)
|
||||
session.commit()
|
||||
session.refresh(volunteer)
|
||||
return _read(session, volunteer)
|
||||
@@ -191,6 +231,7 @@ def update_volunteer(
|
||||
changes = payload.model_dump(exclude_unset=True)
|
||||
artist_ids = changes.pop("artist_ids", None)
|
||||
wished_position_ids = changes.pop("wished_position_ids", None)
|
||||
wished_companion_ids = changes.pop("wished_companion_ids", None)
|
||||
for field, value in changes.items():
|
||||
setattr(volunteer, field, value)
|
||||
session.flush()
|
||||
@@ -198,6 +239,8 @@ def update_volunteer(
|
||||
_set_artists(session, volunteer, artist_ids)
|
||||
if wished_position_ids is not None:
|
||||
_set_wished_positions(session, volunteer, wished_position_ids)
|
||||
if wished_companion_ids is not None:
|
||||
_set_companions(session, volunteer, wished_companion_ids)
|
||||
session.commit()
|
||||
session.refresh(volunteer)
|
||||
return _read(session, volunteer)
|
||||
|
||||
Reference in New Issue
Block a user