Le chevauchement de créneaux (SHIFT_OVERLAP) n'est plus bloquant : il remonte en avertissement et l'affectation est enregistrée. Le solveur ne l'exclut plus mais applique un fort malus au bénévole déjà affecté ailleurs au même moment (proposé seulement sous l'effectif minimal, faute de mieux) ; il n'est jamais reproposé sur un créneau où il est déjà. Même malus dans le classement par défaut du panneau et dans le mock. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
149 lines
5.0 KiB
Python
149 lines
5.0 KiB
Python
import datetime as dt
|
||
|
||
from tests.factories import (
|
||
dt_at,
|
||
make_position,
|
||
make_shift,
|
||
make_volunteer,
|
||
)
|
||
|
||
|
||
def _url(edition, suffix=""):
|
||
return f"/editions/{edition.id}/assignments{suffix}"
|
||
|
||
|
||
def _setup(session, edition):
|
||
position = make_position(session, edition)
|
||
shift = make_shift(
|
||
session, edition, position, start=dt_at(10, 14), end=dt_at(10, 18)
|
||
)
|
||
volunteer = make_volunteer(session, edition, email="v@ex.fr")
|
||
return volunteer, shift
|
||
|
||
|
||
def test_create_assignment_clean(client, edition, session, day_parts):
|
||
volunteer, shift = _setup(session, edition)
|
||
resp = client.post(
|
||
_url(edition),
|
||
json={"volunteer_id": str(volunteer.id), "shift_id": str(shift.id)},
|
||
)
|
||
assert resp.status_code == 201
|
||
body = resp.json()
|
||
assert body["warnings"] == []
|
||
assert body["assignment"]["source"] == "MANUAL"
|
||
assert body["assignment"]["locked"] is False
|
||
|
||
|
||
def test_long_day_without_rest_raises_no_warning(client, edition, session, day_parts):
|
||
# Plus de limite d'heures par jour ni de repos minimal : 14h–18h puis
|
||
# 18h–04h (14 h d'affilée) passe sans avertissement.
|
||
volunteer, shift = _setup(session, edition)
|
||
position = make_position(session, edition, name="Bar")
|
||
following = make_shift(
|
||
session, edition, position, start=dt_at(10, 18), end=dt_at(11, 4)
|
||
)
|
||
for s in (shift, following):
|
||
resp = client.post(
|
||
_url(edition),
|
||
json={"volunteer_id": str(volunteer.id), "shift_id": str(s.id)},
|
||
)
|
||
assert resp.status_code == 201
|
||
assert resp.json()["warnings"] == []
|
||
|
||
|
||
def test_create_assignment_overlap_is_allowed_with_warning(
|
||
client, edition, session, day_parts
|
||
):
|
||
# Un bénévole peut tenir deux postes en même temps : l'affectation est
|
||
# enregistrée, le chevauchement remonte en avertissement.
|
||
volunteer, shift = _setup(session, edition)
|
||
assert client.post(
|
||
_url(edition),
|
||
json={"volunteer_id": str(volunteer.id), "shift_id": str(shift.id)},
|
||
).status_code == 201
|
||
|
||
position = make_position(session, edition, name="Parking")
|
||
overlapping = make_shift(
|
||
session, edition, position, start=dt_at(10, 16), end=dt_at(10, 20)
|
||
)
|
||
resp = client.post(
|
||
_url(edition),
|
||
json={
|
||
"volunteer_id": str(volunteer.id),
|
||
"shift_id": str(overlapping.id),
|
||
},
|
||
)
|
||
assert resp.status_code == 201
|
||
warnings = resp.json()["warnings"]
|
||
assert [w["code"] for w in warnings] == ["SHIFT_OVERLAP"]
|
||
assert warnings[0]["severity"] == "WARNING"
|
||
listed = client.get(
|
||
_url(edition), params={"shift_id": str(overlapping.id)}
|
||
).json()
|
||
assert len(listed) == 1
|
||
|
||
|
||
def test_create_duplicate_assignment_conflicts(client, edition, session, day_parts):
|
||
volunteer, shift = _setup(session, edition)
|
||
body = {"volunteer_id": str(volunteer.id), "shift_id": str(shift.id)}
|
||
assert client.post(_url(edition), json=body).status_code == 201
|
||
resp = client.post(_url(edition), json=body)
|
||
assert resp.status_code == 409
|
||
assert resp.json()["detail"]["code"] == "assignment.duplicate"
|
||
|
||
|
||
def test_assignment_wrong_edition_is_422(client, edition, session, day_parts):
|
||
from app.models import Edition
|
||
|
||
other = Edition(
|
||
year=2027, start_date=dt.date(2027, 7, 1), end_date=dt.date(2027, 7, 4)
|
||
)
|
||
session.add(other)
|
||
session.commit()
|
||
session.refresh(other)
|
||
stranger = make_volunteer(session, other, email="s@ex.fr")
|
||
_, shift = _setup(session, edition)
|
||
|
||
resp = client.post(
|
||
_url(edition),
|
||
json={"volunteer_id": str(stranger.id), "shift_id": str(shift.id)},
|
||
)
|
||
assert resp.status_code == 422
|
||
assert resp.json()["detail"]["code"] == "assignment.wrong_edition"
|
||
|
||
|
||
def test_validate_endpoint_does_not_persist(client, edition, session, day_parts):
|
||
volunteer, shift = _setup(session, edition)
|
||
resp = client.post(
|
||
_url(edition, "/validate"),
|
||
json={"volunteer_id": str(volunteer.id), "shift_id": str(shift.id)},
|
||
)
|
||
assert resp.status_code == 200
|
||
body = resp.json()
|
||
assert body["blocking"] is False
|
||
assert body["violations"] == []
|
||
assert client.get(_url(edition)).json() == []
|
||
|
||
|
||
def test_patch_locked_keeps_assignment(client, edition, session, day_parts):
|
||
volunteer, shift = _setup(session, edition)
|
||
aid = client.post(
|
||
_url(edition),
|
||
json={"volunteer_id": str(volunteer.id), "shift_id": str(shift.id)},
|
||
).json()["assignment"]["id"]
|
||
|
||
resp = client.patch(_url(edition, f"/{aid}"), json={"locked": True})
|
||
assert resp.status_code == 200
|
||
assert resp.json()["assignment"]["locked"] is True
|
||
assert resp.json()["warnings"] == []
|
||
|
||
|
||
def test_delete_assignment(client, edition, session, day_parts):
|
||
volunteer, shift = _setup(session, edition)
|
||
aid = client.post(
|
||
_url(edition),
|
||
json={"volunteer_id": str(volunteer.id), "shift_id": str(shift.id)},
|
||
).json()["assignment"]["id"]
|
||
assert client.delete(_url(edition, f"/{aid}")).status_code == 204
|
||
assert client.get(_url(edition, f"/{aid}")).status_code == 404
|