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:
co-authored by
Claude Sonnet 5
parent
81609eecc5
commit
be51770b51
@@ -1,17 +1,11 @@
|
||||
import datetime as dt
|
||||
|
||||
from sqlmodel import select
|
||||
|
||||
from app.importing.parser import ParsedTable
|
||||
from app.importing.schemas import (
|
||||
AvailabilityColumnMapping,
|
||||
ColumnMapping,
|
||||
ImportMapping,
|
||||
)
|
||||
from app.importing.schemas import ColumnMapping, ImportMapping
|
||||
from app.importing.service import run_import, suggest_mapping, validate_mapping
|
||||
from app.models import Availability, Skill, Volunteer
|
||||
from app.models import Volunteer
|
||||
|
||||
HEADERS = ["Prénom", "Nom", "Email", "Taille", "Compétences", "Ven matin", "Ven aprem"]
|
||||
HEADERS = ["Prénom", "Nom", "Email", "Téléphone", "Remarque"]
|
||||
|
||||
|
||||
def _mapping(dry_run: bool = False) -> ImportMapping:
|
||||
@@ -21,18 +15,9 @@ def _mapping(dry_run: bool = False) -> ImportMapping:
|
||||
first_name="Prénom",
|
||||
last_name="Nom",
|
||||
email="Email",
|
||||
skills="Compétences",
|
||||
phone="Téléphone",
|
||||
comment="Remarque",
|
||||
),
|
||||
availability_columns=[
|
||||
AvailabilityColumnMapping(
|
||||
header="Ven matin", date=dt.date(2026, 7, 10),
|
||||
day_part_code="MORNING",
|
||||
),
|
||||
AvailabilityColumnMapping(
|
||||
header="Ven aprem", date=dt.date(2026, 7, 10),
|
||||
day_part_code="AFTERNOON",
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -58,46 +43,36 @@ def test_suggest_mapping_matches_common_headers():
|
||||
assert suggestion["phone"] == "Numéro de portable"
|
||||
|
||||
|
||||
def test_happy_path_creates_volunteers_skills_and_availabilities(session, edition):
|
||||
def test_happy_path_creates_volunteers(session, edition):
|
||||
report = run_import(
|
||||
session=session,
|
||||
edition=edition,
|
||||
table=_table(
|
||||
[
|
||||
["Alex", "Martin", "alex@ex.fr", "M", "SST, Permis B", "x", "non"],
|
||||
["Sam", "Nguyen", "sam@ex.fr", "L", "SST", "si besoin", ""],
|
||||
["Alex", "Martin", "alex@ex.fr", "0600000000", "arrive tôt"],
|
||||
["Sam", "Nguyen", "sam@ex.fr", "", ""],
|
||||
]
|
||||
),
|
||||
mapping=_mapping(),
|
||||
)
|
||||
|
||||
assert (report.created, report.updated, report.skipped) == (2, 0, 0)
|
||||
assert report.skills_created == ["PERMIS_B", "SST"]
|
||||
assert report.availabilities_written == 3 # x, non, si besoin
|
||||
|
||||
volunteers = {v.email: v for v in _volunteers(session, edition)}
|
||||
assert set(volunteers) == {"alex@ex.fr", "sam@ex.fr"}
|
||||
assert {s.code for s in volunteers["alex@ex.fr"].skills} == {"SST", "PERMIS_B"}
|
||||
|
||||
avails = session.exec(
|
||||
select(Availability).where(
|
||||
Availability.volunteer_id == volunteers["alex@ex.fr"].id
|
||||
)
|
||||
).all()
|
||||
assert {a.status.value for a in avails} == {"AVAILABLE", "UNAVAILABLE"}
|
||||
assert volunteers["alex@ex.fr"].phone == "0600000000"
|
||||
assert volunteers["alex@ex.fr"].comment == "arrive tôt"
|
||||
|
||||
|
||||
def test_dry_run_persists_nothing(session, edition):
|
||||
report = run_import(
|
||||
session=session,
|
||||
edition=edition,
|
||||
table=_table([["Alex", "Martin", "alex@ex.fr", "M", "SST", "x", "x"]]),
|
||||
table=_table([["Alex", "Martin", "alex@ex.fr", "", ""]]),
|
||||
mapping=_mapping(dry_run=True),
|
||||
)
|
||||
assert report.created == 1
|
||||
assert report.dry_run is True
|
||||
assert _volunteers(session, edition) == []
|
||||
assert session.exec(select(Skill)).all() == []
|
||||
|
||||
|
||||
def test_existing_email_is_updated_not_duplicated(session, edition):
|
||||
@@ -115,7 +90,7 @@ def test_existing_email_is_updated_not_duplicated(session, edition):
|
||||
report = run_import(
|
||||
session=session,
|
||||
edition=edition,
|
||||
table=_table([["Alex", "Martin", "ALEX@ex.fr", "M", "SST", "x", ""]]),
|
||||
table=_table([["Alex", "Martin", "ALEX@ex.fr", "", ""]]),
|
||||
mapping=_mapping(),
|
||||
)
|
||||
|
||||
@@ -133,8 +108,8 @@ def test_duplicate_email_within_file_is_skipped(session, edition):
|
||||
edition=edition,
|
||||
table=_table(
|
||||
[
|
||||
["Alex", "Martin", "dup@ex.fr", "M", "", "x", ""],
|
||||
["Alex", "Doublon", "dup@ex.fr", "L", "", "x", ""],
|
||||
["Alex", "Martin", "dup@ex.fr", "", ""],
|
||||
["Alex", "Doublon", "dup@ex.fr", "", ""],
|
||||
]
|
||||
),
|
||||
mapping=_mapping(),
|
||||
@@ -161,7 +136,7 @@ def test_row_without_email_matching_existing_name_warns_but_creates(
|
||||
report = run_import(
|
||||
session=session,
|
||||
edition=edition,
|
||||
table=_table([["jean", "DUPONT", "", "", "", "x", ""]]),
|
||||
table=_table([["jean", "DUPONT", "", "", ""]]),
|
||||
mapping=_mapping(),
|
||||
)
|
||||
assert report.created == 1
|
||||
@@ -173,7 +148,7 @@ def test_missing_required_field_is_skipped(session, edition):
|
||||
report = run_import(
|
||||
session=session,
|
||||
edition=edition,
|
||||
table=_table([["", "Martin", "x@ex.fr", "", "", "x", ""]]),
|
||||
table=_table([["", "Martin", "x@ex.fr", "", ""]]),
|
||||
mapping=_mapping(),
|
||||
)
|
||||
assert report.skipped == 1
|
||||
@@ -185,45 +160,18 @@ def test_invalid_email_is_skipped(session, edition):
|
||||
report = run_import(
|
||||
session=session,
|
||||
edition=edition,
|
||||
table=_table([["Alex", "Martin", "pas-un-email", "", "", "x", ""]]),
|
||||
table=_table([["Alex", "Martin", "pas-un-email", "", ""]]),
|
||||
mapping=_mapping(),
|
||||
)
|
||||
assert report.skipped == 1
|
||||
assert report.rows[0].issues[0].code == "import.row.invalid_email"
|
||||
|
||||
|
||||
def test_unknown_availability_value_is_flagged(session, edition):
|
||||
report = run_import(
|
||||
session=session,
|
||||
edition=edition,
|
||||
table=_table([["Alex", "Martin", "a@ex.fr", "", "", "bof", "x"]]),
|
||||
mapping=_mapping(),
|
||||
)
|
||||
codes = [i.code for i in report.rows[0].issues]
|
||||
assert "import.row.unknown_availability_value" in codes
|
||||
assert report.availabilities_written == 1 # seule "Ven aprem" a été retenue
|
||||
|
||||
|
||||
def test_validate_mapping_reports_bad_day_part_and_date(session, edition):
|
||||
from app.models import DayPart
|
||||
|
||||
day_parts = list(
|
||||
session.exec(select(DayPart).where(DayPart.edition_id == edition.id))
|
||||
)
|
||||
def test_validate_mapping_reports_missing_and_unknown_columns():
|
||||
mapping = ImportMapping(
|
||||
columns=ColumnMapping(first_name="Prénom", last_name="Nom"),
|
||||
availability_columns=[
|
||||
AvailabilityColumnMapping(
|
||||
header="Ven matin", date=dt.date(2026, 7, 10),
|
||||
day_part_code="NIGHT",
|
||||
),
|
||||
AvailabilityColumnMapping(
|
||||
header="Ven aprem", date=dt.date(2030, 1, 1),
|
||||
day_part_code="MORNING",
|
||||
),
|
||||
],
|
||||
columns=ColumnMapping(first_name="Prénom", last_name="", email="Absente")
|
||||
)
|
||||
errors = validate_mapping(_table([]), mapping, edition, day_parts)
|
||||
errors = validate_mapping(_table([]), mapping)
|
||||
codes = {e.code for e in errors}
|
||||
assert "import.mapping.unknown_day_part" in codes
|
||||
assert "import.mapping.date_outside_edition" in codes
|
||||
assert "import.mapping.required_field_missing" in codes
|
||||
assert "import.mapping.column_not_found" in codes
|
||||
|
||||
Reference in New Issue
Block a user