Files
benevoles/backend/tests/test_shift_generation.py
AdamThomasandClaude Sonnet 5 15109acfe5 Créneaux à la main + grille des besoins en axe des temps (30 min)
Un poste ne génère plus de créneaux à sa création : la coordination crée
elle-même ceux dont elle a besoin, avec ses propres horaires de début et
de fin (ConfigGrid.tsx). La génération depuis les créneaux types reste
disponible en option (bouton « Générer les créneaux »).

La grille des besoins devient un vrai axe des temps : une colonne = 30 min,
bornée par jour à la plage réellement utilisée, avec les cases fusionnées
pour correspondre aux horaires de chaque créneau (buildTimeGrid /
placeInTimeGrid). La « Vue bénévoles » garde son ancien modèle (une colonne
par créneau distinct), plus adapté à son tableau bénévole × créneau.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 18:44:20 +02:00

120 lines
4.6 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Génération des créneaux d'un poste à partir des créneaux types.
Depuis le passage à la création de créneaux à la main (voir
`test_shifts_api.py`), un `Position` ne génère plus rien automatiquement à sa
création : `generate-shifts` reste l'outil optionnel pour repartir des
créneaux types de l'édition."""
import datetime as dt
def _positions_url(edition):
return f"/editions/{edition.id}/positions"
def _create_position(client, edition, name="Bar"):
return client.post(
_positions_url(edition), json={"name": name, "category": "BAR"}
).json()
def _generate(client, edition, position):
return client.post(f"{_positions_url(edition)}/{position['id']}/generate-shifts")
def test_creating_a_position_generates_no_shifts(client, edition):
position = _create_position(client, edition)
shifts = client.get(
f"/editions/{edition.id}/shifts", params={"position_id": position["id"]}
).json()
assert shifts == []
def test_generate_shifts_creates_one_per_day_and_type(client, edition):
position = _create_position(client, edition)
resp = _generate(client, edition, position)
# 4 jours (2026-07-09..12) x 3 créneaux types.
assert resp.json()["created"] == 12
shifts = client.get(
f"/editions/{edition.id}/shifts", params={"position_id": position["id"]}
).json()
assert len(shifts) == 12
assert all(s["min_staff"] == 0 and s["ideal_staff"] == 0 for s in shifts)
assert all(s["day_part_id"] is not None for s in shifts)
def test_generate_is_idempotent(client, edition):
position = _create_position(client, edition)
_generate(client, edition, position)
resp = _generate(client, edition, position)
assert resp.status_code == 200
assert resp.json()["created"] == 0
def test_extending_the_edition_then_generating_adds_the_new_days(client, edition):
position = _create_position(client, edition)
_generate(client, edition, position)
client.patch(f"/editions/{edition.id}", json={"end_date": "2026-07-14"})
resp = _generate(client, edition, position)
# 2 jours de plus x 3 créneaux types.
assert resp.json()["created"] == 6
shifts = client.get(
f"/editions/{edition.id}/shifts", params={"position_id": position["id"]}
).json()
assert len(shifts) == 18
def test_adding_a_day_part_then_generating_adds_it_everywhere(client, edition):
position = _create_position(client, edition)
_generate(client, edition, position)
client.post(
f"/editions/{edition.id}/day-parts",
json={"label": "Nuit", "start_time": "02:00", "end_time": "05:00"},
)
resp = _generate(client, edition, position)
assert resp.json()["created"] == 4 # 1 par jour
def test_day_part_before_the_day_boundary_rolls_to_the_next_calendar_day(client, edition):
"""La frontière du jour est le début du créneau type de plus petit `order`
(ici MORNING, 06 h — voir CLAUDE.md § 3 et `validation.engine._day_boundary`).
Un créneau type qui commence avant cette heure (ex. `Nuit` 02 h–05 h,
suite de `Soirée` 19 h–00 h) est donc la fin de la nuit du jour
*précédent* : il doit être généré sur le jour suivant, pas sur `day`."""
position = _create_position(client, edition)
_generate(client, edition, position)
client.post(
f"/editions/{edition.id}/day-parts",
json={"label": "Nuit", "start_time": "02:00", "end_time": "05:00", "order": 3},
)
_generate(client, edition, position)
shifts = client.get(
f"/editions/{edition.id}/shifts", params={"position_id": position["id"]}
).json()
nuit_shifts = [
s for s in shifts if dt.time.fromisoformat(s["starts_at"][11:19]) == dt.time(2, 0)
]
assert len(nuit_shifts) == 4
nuit_dates = sorted(dt.date.fromisoformat(s["starts_at"][:10]) for s in nuit_shifts)
# Édition du 9 au 12/07 : la nuit du 9 (Soirée 19 h) se termine le 10 à
# 05 h, ..., la nuit du 12 se termine le 13 — jamais le 9 lui-même.
assert nuit_dates == [dt.date(2026, 7, d) for d in (10, 11, 12, 13)]
def test_generation_does_not_touch_staffing_already_set(client, edition):
position = _create_position(client, edition)
_generate(client, edition, position)
shift = client.get(
f"/editions/{edition.id}/shifts", params={"position_id": position["id"]}
).json()[0]
client.patch(
f"/editions/{edition.id}/shifts/{shift['id']}",
json={"min_staff": 3, "ideal_staff": 5},
)
_generate(client, edition, position)
again = client.get(f"/editions/{edition.id}/shifts/{shift['id']}").json()
assert again["min_staff"] == 3 and again["ideal_staff"] == 5