Deux postes en même temps possibles, malus dans le solveur
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>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
d0fa1e109c
commit
0f904f5e3d
@@ -51,7 +51,11 @@ def test_long_day_without_rest_raises_no_warning(client, edition, session, day_p
|
||||
assert resp.json()["warnings"] == []
|
||||
|
||||
|
||||
def test_create_assignment_blocked_by_overlap(client, edition, session, day_parts):
|
||||
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),
|
||||
@@ -69,15 +73,14 @@ def test_create_assignment_blocked_by_overlap(client, edition, session, day_part
|
||||
"shift_id": str(overlapping.id),
|
||||
},
|
||||
)
|
||||
assert resp.status_code == 422
|
||||
detail = resp.json()["detail"]
|
||||
assert detail["code"] == "error.validation_failed"
|
||||
assert any(v["code"] == "SHIFT_OVERLAP" for v in detail["violations"])
|
||||
# Rien n'a été enregistré pour ce créneau.
|
||||
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 listed == []
|
||||
assert len(listed) == 1
|
||||
|
||||
|
||||
def test_create_duplicate_assignment_conflicts(client, edition, session, day_parts):
|
||||
|
||||
@@ -74,6 +74,40 @@ def test_fully_staffed_shift_gets_no_suggestion():
|
||||
assert solution.suggestions == ()
|
||||
|
||||
|
||||
def test_busy_volunteer_is_still_suggested_for_an_urgent_gap_as_last_resort():
|
||||
# Déjà affecté ailleurs au même moment : malus, mais seul candidat pour un
|
||||
# créneau sous son minimum ⇒ proposé quand même.
|
||||
shift = _shift(min_staff=1, ideal_staff=1)
|
||||
busy = _volunteer(
|
||||
"busy",
|
||||
existing_shifts=[ShiftInfo(id="e", starts_at=_dt(10, 15), ends_at=_dt(10, 19))],
|
||||
)
|
||||
solution = solve(SolverInput(shifts=(shift,), volunteers=(busy,)))
|
||||
assert _by_shift(solution, "s1") == ("busy",)
|
||||
|
||||
|
||||
def test_volunteer_already_on_the_shift_is_never_suggested_again():
|
||||
shift = _shift(min_staff=2, ideal_staff=2, already_assigned=1)
|
||||
on_it = _volunteer(
|
||||
"on_it",
|
||||
existing_shifts=[ShiftInfo(id="s1", starts_at=_dt(10, 14), ends_at=_dt(10, 18))],
|
||||
)
|
||||
solution = solve(SolverInput(shifts=(shift,), volunteers=(on_it,)))
|
||||
assert _by_shift(solution, "s1") == ()
|
||||
|
||||
|
||||
def test_busy_volunteer_is_not_suggested_above_the_minimum():
|
||||
# Minimum déjà atteint : le malus l'emporte, personne n'est proposé.
|
||||
shift = _shift(min_staff=1, ideal_staff=2, already_assigned=1)
|
||||
busy = _volunteer(
|
||||
"busy",
|
||||
wished_position_ids={"p1"},
|
||||
existing_shifts=[ShiftInfo(id="e", starts_at=_dt(10, 15), ends_at=_dt(10, 19))],
|
||||
)
|
||||
solution = solve(SolverInput(shifts=(shift,), volunteers=(busy,)))
|
||||
assert _by_shift(solution, "s1") == ()
|
||||
|
||||
|
||||
def test_picks_the_only_free_volunteer_over_a_busy_one():
|
||||
shift = _shift()
|
||||
busy = _volunteer(
|
||||
|
||||
@@ -43,16 +43,17 @@ def test_overlap_pass_when_shifts_are_adjacent():
|
||||
assert validate(data) == []
|
||||
|
||||
|
||||
def test_overlap_triggers_and_is_blocking():
|
||||
def test_overlap_triggers_as_a_warning_only():
|
||||
data = ValidationInput(
|
||||
candidate_shift=_shift("c", _dt(10, 14), _dt(10, 18)),
|
||||
existing_shifts=(_shift("e", _dt(10, 16), _dt(10, 20)),),
|
||||
)
|
||||
violations = validate(data)
|
||||
assert _codes(violations) == [ViolationCode.SHIFT_OVERLAP]
|
||||
assert violations[0].severity is Severity.BLOCKING
|
||||
# Deux postes en même temps restent possibles : avertissement seulement.
|
||||
assert violations[0].severity is Severity.WARNING
|
||||
assert violations[0].params["other_shift_id"] == "e"
|
||||
assert has_blocking(violations) is True
|
||||
assert has_blocking(violations) is False
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
Reference in New Issue
Block a user