From 282be85498a1e5672b627e2b4e3decb41a58bfa0 Mon Sep 17 00:00:00 2001 From: AdamThomas Date: Fri, 11 Sep 2026 18:25:18 +0200 Subject: [PATCH] =?UTF-8?q?Jeu=20de=20d=C3=A9mo=20:=20programmation=20et?= =?UTF-8?q?=20v=C5=93ux=20des=20b=C3=A9n=C3=A9voles=20(manquaient)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le seed créait déjà des disponibilités, mais aucun Artist ni vœu (VolunteerPositionWish, VolunteerArtist) — la démo ne montrait donc rien du solveur ni des vœux : postes/artistes jamais choisis, programmation vide. - 3 artistes par jour de festival (pas montage/démontage), horaires échelonnés en soirée (18h-19h / 19h30-21h / 21h30-23h). - ~40 % des bénévoles ont un vœu de poste ; ~15 % de chance par artiste d'être un vœu (~1 par bénévole en moyenne sur 6 artistes). - Summary affiche désormais artists / position_wishes / artist_wishes ; tests étendus (présence + déterminisme). Vérifié en conditions réelles (`python -m app.seed` + appels API) : 6 artistes, 47 vœux de poste, 95 vœux d'artiste sur 120 bénévoles ; le solveur tourne toujours en ~0,3 s dessus. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM --- CLAUDE.md | 4 ++- backend/app/seed.py | 56 ++++++++++++++++++++++++++++++++++++++ backend/tests/test_seed.py | 7 +++++ 3 files changed, 66 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index ab702fd..68c8a4c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -297,7 +297,9 @@ affectation n'est jamais persistée par cet endpoint. 5. Endpoint matrice d'une édition : `[{ position, shifts: [{ shift, min_staff, ideal_staff, assigned }] }]`. 6. Jeu de données de démonstration : 120 bénévoles, 15 postes, 4 jours, - quelques trous d'effectif, 2–3 avertissements actifs. + quelques trous d'effectif, 2–3 avertissements actifs, programmation + (artistes les jours de festival) et vœux des bénévoles (postes, artistes) + pour que le solveur ait matière à arbitrer. 7. Solveur de suggestions OR-Tools CP-SAT (section 4) — en cours. ### Hors périmètre (le modèle doit juste pouvoir les accueillir sans refonte) diff --git a/backend/app/seed.py b/backend/app/seed.py index 48124ba..cae8777 100644 --- a/backend/app/seed.py +++ b/backend/app/seed.py @@ -19,6 +19,7 @@ from app.db import engine from app.importing.text import normalize from app.models import ( Account, + Artist, Assignment, AssignmentSource, Availability, @@ -29,6 +30,8 @@ from app.models import ( Role, Shift, Volunteer, + VolunteerArtist, + VolunteerPositionWish, ) from app.validation import ( AvailabilityWindow, @@ -188,6 +191,17 @@ _STAFF: dict[PositionCategory, tuple[int, int]] = { _C.OTHER: (1, 2), } +# Programmation : trois passages par jour de festival (pas montage/démontage). +ARTIST_NAMES = [ + "La Battante", "Orchestre Miroir", "Kntongo", "Louve", + "Les Frères Vent", "Nuage Bleu", "Sœur Machine", "Comète", +] +ARTIST_SLOTS: list[tuple[dt.time, dt.time]] = [ + (dt.time(18, 0), dt.time(19, 0)), + (dt.time(19, 30), dt.time(21, 0)), + (dt.time(21, 30), dt.time(23, 0)), +] + @dataclass class Summary: @@ -197,6 +211,9 @@ class Summary: shifts: int = 0 understaffed_shifts: int = 0 assignments: int = 0 + artists: int = 0 + position_wishes: int = 0 + artist_wishes: int = 0 warnings: Counter = field(default_factory=Counter) blocking: int = 0 @@ -209,6 +226,9 @@ class Summary: f" Créneaux : {self.shifts}", f" Créneaux sous l'effectif minimal (trous) : {self.understaffed_shifts}", f" Affectations : {self.assignments}", + f" Artistes programmés : {self.artists}", + f" Vœux de poste déclarés : {self.position_wishes}", + f" Vœux d'artiste déclarés : {self.artist_wishes}", f" Avertissements actifs : {sum(self.warnings.values())}", ] for code, count in sorted(self.warnings.items()): @@ -316,8 +336,26 @@ def build(session: Session, *, rng: random.Random | None = None) -> Summary: dates[3]: "demontage", } + artists: list[Artist] = [] + for day in dates: + if phase_of[day] != "festival": + continue + for start, end in ARTIST_SLOTS: + artist = Artist( + edition_id=edition.id, + name=ARTIST_NAMES[len(artists) % len(ARTIST_NAMES)], + performance_date=day, + start_time=start, + end_time=end, + ) + session.add(artist) + artists.append(artist) + session.flush() + volunteers: list[Volunteer] = [] used_names: set[tuple[str, str]] = set() + n_position_wishes = 0 + n_artist_wishes = 0 for i in range(120): while True: first = rng.choice(FIRST_NAMES) @@ -345,6 +383,21 @@ def build(session: Session, *, rng: random.Random | None = None) -> Summary: volunteer_id=volunteer.id, ) ) + # Vœux : poste qu'il voudrait tenir, artistes qu'il veut voir. + if rng.random() < 0.4: + session.add( + VolunteerPositionWish( + volunteer_id=volunteer.id, + position_id=rng.choice(positions).id, + ) + ) + n_position_wishes += 1 + for artist in artists: + if rng.random() < 0.15: + session.add( + VolunteerArtist(volunteer_id=volunteer.id, artist_id=artist.id) + ) + n_artist_wishes += 1 # Disponibilités : chaque bénévole déclare, pour ~80 % des jours, une # large fenêtre couvrant la journée et la nuit (06:00 → 04:00 le lendemain). _FULL_DAY = (dt.time(6, 0), dt.time(4, 0)) @@ -463,6 +516,9 @@ def build(session: Session, *, rng: random.Random | None = None) -> Summary: shifts=len(shifts), assignments=n_assignments, understaffed_shifts=sum(1 for shift in shifts if fill[shift.id] < shift.min_staff), + artists=len(artists), + position_wishes=n_position_wishes, + artist_wishes=n_artist_wishes, ) _verify(session, edition, summary) return summary diff --git a/backend/tests/test_seed.py b/backend/tests/test_seed.py index 3a84e14..5c807d6 100644 --- a/backend/tests/test_seed.py +++ b/backend/tests/test_seed.py @@ -16,6 +16,10 @@ def test_build_creates_a_realistic_dataset(session): # Au moins deux règles d'avertissement représentées, quelques alertes. assert len(summary.warnings) >= 2 assert sum(summary.warnings.values()) >= 3 + # Programmation et vœux des bénévoles : le mode démo doit les illustrer. + assert summary.artists > 0 + assert summary.position_wishes > 0 + assert summary.artist_wishes > 0 edition = session.exec( select(Edition).where(Edition.name == DEMO_EDITION_NAME) @@ -38,6 +42,9 @@ def test_build_is_deterministic_and_idempotent(session): assert second.assignments == first.assignments assert second.understaffed_shifts == first.understaffed_shifts assert dict(second.warnings) == dict(first.warnings) + assert second.artists == first.artists + assert second.position_wishes == first.position_wishes + assert second.artist_wishes == first.artist_wishes editions = session.exec( select(Edition).where(Edition.name == DEMO_EDITION_NAME)