Retrait des compétences ; disponibilités en fenêtres horaires libres

Compétences supprimées de bout en bout :
- tables `skill`, `volunteer_skill`, `shift_required_skill` ;
- `Volunteer.skill_codes`, `Shift.required_skill_codes`, colonne `skills`
  de l'import, règle de validation `MISSING_REQUIRED_SKILL`, jeu de démo.

Disponibilités repensées : `Availability` passe de (`day_part_id`,
`status` tri-état) à une **fenêtre horaire libre** (`date`, `start_time`,
`end_time` ; `end <= start` ⇒ passe minuit), plusieurs par jour.
- Nouveaux endpoints `GET`/`PUT /editions/{id}/volunteers/{vid}/availability`
  (PUT = remplacement complet, COORDINATION) et
  `GET /editions/{id}/availability` (toute l'édition, pour la grille).
- `OUTSIDE_AVAILABILITY` (toujours WARNING) : compare le créneau à l'union
  des fenêtres du bénévole ce jour-là ; renvoie `date` + `uncovered_minutes`.
  `DayPart` ne sert plus qu'à la borne de « journée » du cumul 8 h.
- L'import ne gère plus les disponibilités (fenêtres saisies dans l'app) :
  `values.py` supprimé, mapping réduit à nom/email/téléphone/commentaire.

Migration 0007 (recrée `availability`). `AvailabilityStatus` supprimé.
158 tests, `alembic check` et `ruff` au vert. Front à suivre.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
This commit is contained in:
AdamThomas
2026-09-10 22:46:45 +02:00
co-authored by Claude Sonnet 5
parent 81609eecc5
commit be51770b51
36 changed files with 800 additions and 1199 deletions
+114
View File
@@ -0,0 +1,114 @@
from app.models import Role
from tests.factories import make_volunteer
def _url(edition, volunteer, suffix=""):
return f"/editions/{edition.id}/volunteers/{volunteer.id}/availability{suffix}"
def test_put_then_get_returns_windows_sorted(client, edition, session):
v = make_volunteer(session, edition, email="v@e.fr")
resp = client.put(
_url(edition, v),
json={
"windows": [
{"date": "2026-07-11", "start_time": "14:00", "end_time": "19:00"},
{"date": "2026-07-10", "start_time": "08:00", "end_time": "13:00"},
{"date": "2026-07-10", "start_time": "19:00", "end_time": "02:00"},
]
},
)
assert resp.status_code == 200
got = [(w["date"], w["start_time"], w["end_time"]) for w in resp.json()]
assert got == [
("2026-07-10", "08:00:00", "13:00:00"),
("2026-07-10", "19:00:00", "02:00:00"),
("2026-07-11", "14:00:00", "19:00:00"),
]
assert client.get(_url(edition, v)).json() == resp.json()
def test_put_replaces_the_whole_set_and_dedupes(client, edition, session):
v = make_volunteer(session, edition, email="v@e.fr")
client.put(
_url(edition, v),
json={
"windows": [
{"date": "2026-07-10", "start_time": "08:00", "end_time": "12:00"}
]
},
)
resp = client.put(
_url(edition, v),
json={
"windows": [
{"date": "2026-07-11", "start_time": "09:00", "end_time": "17:00"},
{"date": "2026-07-11", "start_time": "09:00", "end_time": "17:00"},
]
},
)
body = resp.json()
assert len(body) == 1
assert body[0]["date"] == "2026-07-11"
def test_put_rejects_date_outside_edition(client, edition, session):
v = make_volunteer(session, edition, email="v@e.fr")
resp = client.put(
_url(edition, v),
json={
"windows": [
{"date": "2026-08-01", "start_time": "08:00", "end_time": "12:00"}
]
},
)
assert resp.status_code == 422
assert resp.json()["detail"]["code"] == "availability.date_outside_edition"
def test_put_rejects_empty_window(client, edition, session):
v = make_volunteer(session, edition, email="v@e.fr")
resp = client.put(
_url(edition, v),
json={
"windows": [
{"date": "2026-07-10", "start_time": "09:00", "end_time": "09:00"}
]
},
)
assert resp.status_code == 422
assert resp.json()["detail"]["code"] == "availability.empty_window"
def test_edition_wide_listing(client, edition, session):
v1 = make_volunteer(session, edition, first_name="A", email="a@e.fr")
v2 = make_volunteer(session, edition, first_name="B", email="b@e.fr")
for v in (v1, v2):
client.put(
_url(edition, v),
json={
"windows": [
{"date": "2026-07-10", "start_time": "08:00", "end_time": "13:00"}
]
},
)
body = client.get(f"/editions/{edition.id}/availability").json()
assert {w["volunteer_id"] for w in body} == {str(v1.id), str(v2.id)}
def test_lead_can_read_but_not_write(edition, session, make_account, as_account):
v = make_volunteer(session, edition, email="v@e.fr")
lead = make_account(Role.LEAD, email="lead@e.fr")
api = as_account(lead)
assert api.get(_url(edition, v)).status_code == 200
resp = api.put(_url(edition, v), json={"windows": []})
assert resp.status_code == 403
def test_volunteer_reads_only_their_own(edition, session, make_account, as_account):
me = make_volunteer(session, edition, email="me@e.fr")
other = make_volunteer(session, edition, email="other@e.fr")
account = make_account(Role.VOLUNTEER, volunteer_id=me.id, email="acc@e.fr")
api = as_account(account)
assert api.get(_url(edition, me)).status_code == 200
assert api.get(_url(edition, other)).status_code == 403