From 464635656afd687de07c46a014de0799b93f264f Mon Sep 17 00:00:00 2001 From: AdamThomas Date: Thu, 10 Sep 2026 07:36:06 +0200 Subject: [PATCH] =?UTF-8?q?=C3=89tape=209=20:=20suppression=20du=20statut?= =?UTF-8?q?=20d'affectation=20(plus=20de=20confirmation)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Une affectation est valide dès sa création ; la retirer = supprimer la ligne. - Modèle : suppression de AssignmentStatus, ACTIVE_ASSIGNMENT_STATUSES et de Assignment.status ; migration 0003 (drop_column, alembic check vert) - Loader de validation : toutes les affectations du bénévole comptent, plus de filtre par statut - API : AssignmentCreate/Update/Read sans status ; création → validation toujours exécutée (blocage si violation bloquante) ; PATCH ne porte plus que source/locked - Matrice : MatrixShift.{confirmed, proposed} → {assigned} (compte unique) - Seed : affectations sans statut, résumé « Affectations » (260) - Tests mis à jour (matrice, loader, modèle, API) ; 128 tests au total - CLAUDE.md : glossaire, modèle, sémantique de validation, périmètre Frontend : les références au statut (propositions, badges confirmé/proposé, écran de confirmation mobile, mock, schema.ts) restent à retirer séparément. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM --- CLAUDE.md | 16 ++-- backend/app/api/assignments.py | 82 +++++++------------ backend/app/api/matrix.py | 26 ++---- backend/app/api/schemas.py | 7 +- .../versions/0003_drop_assignment_status.py | 35 ++++++++ backend/app/models/__init__.py | 4 - backend/app/models/domain.py | 13 +-- backend/app/models/enums.py | 14 ---- backend/app/seed.py | 17 +--- backend/app/validation/loader.py | 6 +- backend/app/validation/models.py | 5 +- backend/tests/test_api_assignments.py | 41 +--------- backend/tests/test_api_matrix.py | 20 ++--- backend/tests/test_models.py | 2 - backend/tests/test_validation_loader.py | 29 +++---- 15 files changed, 109 insertions(+), 208 deletions(-) create mode 100644 backend/app/migrations/versions/0003_drop_assignment_status.py diff --git a/CLAUDE.md b/CLAUDE.md index c582a11..9f8e1cd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -31,7 +31,6 @@ colonne, route et variable utilise la colonne « Code / base ». | Métier | Code | |------------------------------------|-----------------------------------------------| -| Statut d'affectation | `PROPOSED` / `CONFIRMED` / `DECLINED` / `CANCELLED` | | Origine d'affectation | `SOLVER` / `MANUAL` | | Verrouillée | `Assignment.locked: bool` | | Rôle | `COORDINATOR` / `LEAD` / `VOLUNTEER` | @@ -74,10 +73,10 @@ les tables sauf les tables d'association. lendemain), `min_staff`, `ideal_staff`, compétences requises (N-N `Skill`). - **`Availability`** : `volunteer_id`, `date`, `day_part_id`, `status` (`AvailabilityStatus`). Unique (`volunteer_id`, `date`, `day_part_id`). -- **`Assignment`** : `volunteer_id`, `shift_id`, `status` - (`AssignmentStatus`, défaut `PROPOSED`), `source` (`AssignmentSource`, +- **`Assignment`** : `volunteer_id`, `shift_id`, `source` (`AssignmentSource`, défaut `MANUAL`), `locked` (défaut `false`). Unique (`volunteer_id`, - `shift_id`). + `shift_id`). **Pas de statut** : l'affectation est valide dès sa création + (aucune étape de confirmation) ; la retirer, c'est supprimer la ligne. - **`Account`** : `email` (unique par édition), `role` (`Role`), `edition_id`, `volunteer_id` (nullable, unique). - **`MagicLinkToken`** : `account_id`, `token_hash` (unique), `expires_at`, @@ -164,8 +163,8 @@ traduction se fait via `locales/fr.json`). Sémantique figée : -- Statuts pris en compte pour chevauchement / cumul / repos : - `PROPOSED` + `CONFIRMED`. `DECLINED` et `CANCELLED` sont ignorés. +- Toutes les affectations du bénévole comptent pour chevauchement / cumul / + repos (une affectation = une décision, toujours active). - « Journée » (cumul 8 h) : du début du premier `DayPart` du jour au début du premier `DayPart` du lendemain. - Cumul : `> 8 h 00` strict (8 h 00 pile passe). @@ -190,7 +189,7 @@ Sémantique figée : 3. API CRUD sur `Position`, `Shift`, `Volunteer`, `Assignment`. 4. Moteur de validation (section 3). 5. Endpoint matrice d'une édition : - `[{ position, shifts: [{ shift, min_staff, ideal_staff, confirmed, proposed }] }]`. + `[{ position, shifts: [{ shift, min_staff, ideal_staff, assigned }] }]`. 6. Jeu de données de démonstration : 120 bénévoles, 15 postes, 4 jours, quelques trous d'effectif, 2–3 avertissements actifs. @@ -308,7 +307,8 @@ Aucune dépendance locale requise hors Docker. | 5 | API CRUD (Position, Shift, Volunteer, Assignment) + matrice + validation branchée | validée | | 6 | Jeu de données de démonstration (`app/seed.py`) | validée | | 7 | Authentification : lien magique + JWT + application des rôles | validée | -| 8 | Second système : connexion mot de passe (bcrypt) pour la coordination | à valider | +| 8 | Second système : connexion mot de passe (bcrypt) pour la coordination | validée | +| 9 | Suppression du statut d'affectation (plus d'étape de confirmation) | à valider | Premier incrément backend + authentification terminés. **Reste hors périmètre** (modèle prêt à les accueillir) : envoi réel d'emails/SMS (le lien diff --git a/backend/app/api/assignments.py b/backend/app/api/assignments.py index d938e96..9f2a129 100644 --- a/backend/app/api/assignments.py +++ b/backend/app/api/assignments.py @@ -1,5 +1,6 @@ -"""CRUD des affectations. Toute écriture passe par le moteur de validation : -une violation bloquante interdit l'enregistrement, les avertissements sont +"""CRUD des affectations. Une affectation est valide dès sa création (pas +d'étape de confirmation). La création passe par le moteur de validation : une +violation bloquante interdit l'enregistrement, les avertissements sont renvoyés.""" import uuid @@ -23,15 +24,7 @@ from app.auth.deps import ( require_staff, ) from app.db import get_session -from app.models import ( - ACTIVE_ASSIGNMENT_STATUSES, - Account, - Assignment, - Edition, - Role, - Shift, - Volunteer, -) +from app.models import Account, Assignment, Edition, Role, Shift, Volunteer from app.validation import has_blocking, validate from app.validation.loader import build_validation_input @@ -114,32 +107,27 @@ def create_assignment( status_code=409, detail={"code": "assignment.duplicate"} ) - # La validation ne s'applique qu'aux affectations actives : créer - # directement une affectation DECLINEE ou ANNULEE n'a rien à valider. - violations: list = [] - if payload.status in ACTIVE_ASSIGNMENT_STATUSES: - violations = validate( - build_validation_input( - session=session, - edition=edition, - volunteer=volunteer, - shift=shift, - ) + violations = validate( + build_validation_input( + session=session, + edition=edition, + volunteer=volunteer, + shift=shift, + ) + ) + if has_blocking(violations): + raise HTTPException( + status_code=422, + detail={ + "code": "error.validation_failed", + "violations": [v.model_dump() for v in violations], + }, ) - if has_blocking(violations): - raise HTTPException( - status_code=422, - detail={ - "code": "error.validation_failed", - "violations": [v.model_dump() for v in violations], - }, - ) assignment = Assignment( edition_id=edition.id, volunteer_id=volunteer.id, shift_id=shift.id, - status=payload.status, source=payload.source, locked=payload.locked, ) @@ -202,29 +190,17 @@ def update_assignment( for field, value in changes.items(): setattr(assignment, field, value) - warnings: list = [] - if assignment.status in ACTIVE_ASSIGNMENT_STATUSES: - volunteer = session.get(Volunteer, assignment.volunteer_id) - shift = session.get(Shift, assignment.shift_id) - warnings = validate( - build_validation_input( - session=session, - edition=edition, - volunteer=volunteer, - shift=shift, - exclude_assignment_id=assignment.id, - ) + # `source` / `locked` ne changent pas le résultat de validation ; on le + # rejoue seulement pour renvoyer les avertissements courants. + warnings = validate( + build_validation_input( + session=session, + edition=edition, + volunteer=session.get(Volunteer, assignment.volunteer_id), + shift=session.get(Shift, assignment.shift_id), + exclude_assignment_id=assignment.id, ) - if has_blocking(warnings): - session.rollback() - raise HTTPException( - status_code=422, - detail={ - "code": "error.validation_failed", - "violations": [v.model_dump() for v in warnings], - }, - ) - + ) session.commit() session.refresh(assignment) return AssignmentWriteResult(assignment=assignment, warnings=warnings) diff --git a/backend/app/api/matrix.py b/backend/app/api/matrix.py index c5ed85b..ddfe807 100644 --- a/backend/app/api/matrix.py +++ b/backend/app/api/matrix.py @@ -8,14 +8,7 @@ from app.api.deps import get_edition from app.api.schemas import MatrixPosition, MatrixResponse, MatrixShift from app.auth.deps import require_staff from app.db import get_session -from app.models import ( - Account, - Assignment, - AssignmentStatus, - Edition, - Position, - Shift, -) +from app.models import Account, Assignment, Edition, Position, Shift router = APIRouter(prefix="/editions/{edition_id}/matrix", tags=["matrix"]) @@ -41,13 +34,11 @@ def get_matrix( ) ) counts = { - (shift_id, status): total - for shift_id, status, total in session.exec( - select( - Assignment.shift_id, Assignment.status, func.count() - ) + shift_id: total + for shift_id, total in session.exec( + select(Assignment.shift_id, func.count()) .where(Assignment.edition_id == edition.id) - .group_by(Assignment.shift_id, Assignment.status) + .group_by(Assignment.shift_id) ) } @@ -60,12 +51,7 @@ def get_matrix( ends_at=shift.ends_at, min_staff=shift.min_staff, ideal_staff=shift.ideal_staff, - confirmed=counts.get( - (shift.id, AssignmentStatus.CONFIRMED), 0 - ), - proposed=counts.get( - (shift.id, AssignmentStatus.PROPOSED), 0 - ), + assigned=counts.get(shift.id, 0), ) ) diff --git a/backend/app/api/schemas.py b/backend/app/api/schemas.py index 96f1d08..edec713 100644 --- a/backend/app/api/schemas.py +++ b/backend/app/api/schemas.py @@ -7,7 +7,6 @@ from pydantic import BaseModel, ConfigDict, Field from app.models import ( AssignmentSource, - AssignmentStatus, PositionCategory, Role, TshirtSize, @@ -129,13 +128,11 @@ class ShiftRead(ApiModel): class AssignmentCreate(ApiModel): volunteer_id: uuid.UUID shift_id: uuid.UUID - status: AssignmentStatus = AssignmentStatus.PROPOSED source: AssignmentSource = AssignmentSource.MANUAL locked: bool = False class AssignmentUpdate(ApiModel): - status: AssignmentStatus | None = None source: AssignmentSource | None = None locked: bool | None = None @@ -145,7 +142,6 @@ class AssignmentRead(ApiModel): edition_id: uuid.UUID volunteer_id: uuid.UUID shift_id: uuid.UUID - status: AssignmentStatus source: AssignmentSource locked: bool created_at: dt.datetime @@ -176,8 +172,7 @@ class MatrixShift(ApiModel): ends_at: dt.datetime min_staff: int ideal_staff: int - confirmed: int - proposed: int + assigned: int class MatrixPosition(ApiModel): diff --git a/backend/app/migrations/versions/0003_drop_assignment_status.py b/backend/app/migrations/versions/0003_drop_assignment_status.py new file mode 100644 index 0000000..7f34a83 --- /dev/null +++ b/backend/app/migrations/versions/0003_drop_assignment_status.py @@ -0,0 +1,35 @@ +"""Affectation : suppression du statut (plus d'étape de confirmation) + +Revision ID: 0003_drop_assignment_status +Revises: 0002_account_password +Create Date: 2026-09-10 + +""" + +from collections.abc import Sequence + +import sqlalchemy as sa +from alembic import op + +revision: str = "0003_drop_assignment_status" +down_revision: str | None = "0002_account_password" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None + +_STATUS = sa.Enum( + "PROPOSED", "CONFIRMED", "DECLINED", "CANCELLED", + name="assignmentstatus", native_enum=False, +) + + +def upgrade() -> None: + op.drop_column("assignment", "status") + + +def downgrade() -> None: + op.add_column( + "assignment", + sa.Column( + "status", _STATUS, server_default="PROPOSED", nullable=False + ), + ) diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index 256bedf..bdfeeae 100644 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -20,9 +20,7 @@ from app.models.domain import ( VolunteerSkill, ) from app.models.enums import ( - ACTIVE_ASSIGNMENT_STATUSES, AssignmentSource, - AssignmentStatus, AvailabilityStatus, PositionCategory, Role, @@ -30,11 +28,9 @@ from app.models.enums import ( ) __all__ = [ - "ACTIVE_ASSIGNMENT_STATUSES", "Account", "Assignment", "AssignmentSource", - "AssignmentStatus", "Availability", "AvailabilityStatus", "DayPart", diff --git a/backend/app/models/domain.py b/backend/app/models/domain.py index cf21b75..767a39c 100644 --- a/backend/app/models/domain.py +++ b/backend/app/models/domain.py @@ -16,7 +16,6 @@ from sqlmodel import Field, Relationship, SQLModel, UniqueConstraint from app.models.base import TimestampMixin from app.models.enums import ( AssignmentSource, - AssignmentStatus, AvailabilityStatus, PositionCategory, Role, @@ -293,7 +292,9 @@ class Availability(TimestampMixin, table=True): class Assignment(TimestampMixin, table=True): """Décision de l'organisation : « tu tiens tel créneau ». - À ne pas confondre avec Availability (déclaration du bénévole). + Existe ⇒ le bénévole est affecté (pas d'étape de confirmation). Retirer une + affectation, c'est supprimer la ligne. À ne pas confondre avec Availability + (déclaration du bénévole). """ __table_args__ = ( @@ -312,14 +313,6 @@ class Assignment(TimestampMixin, table=True): shift_id: uuid.UUID = Field( foreign_key="shift.id", index=True, ondelete="CASCADE" ) - status: AssignmentStatus = Field( - default=AssignmentStatus.PROPOSED, - sa_column=Column( - SAEnum(AssignmentStatus, native_enum=False), - nullable=False, - server_default=AssignmentStatus.PROPOSED.value, - ), - ) source: AssignmentSource = Field( default=AssignmentSource.MANUAL, sa_column=Column( diff --git a/backend/app/models/enums.py b/backend/app/models/enums.py index 2ecc551..1263765 100644 --- a/backend/app/models/enums.py +++ b/backend/app/models/enums.py @@ -19,13 +19,6 @@ class AvailabilityStatus(enum.StrEnum): UNAVAILABLE = "UNAVAILABLE" -class AssignmentStatus(enum.StrEnum): - PROPOSED = "PROPOSED" - CONFIRMED = "CONFIRMED" - DECLINED = "DECLINED" - CANCELLED = "CANCELLED" - - class AssignmentSource(enum.StrEnum): SOLVER = "SOLVER" MANUAL = "MANUAL" @@ -51,10 +44,3 @@ class TshirtSize(enum.StrEnum): XL = "XL" XXL = "XXL" XXXL = "XXXL" - - -# Statuts d'affectation « actifs » : seuls ceux-ci comptent pour les règles de -# chevauchement, de cumul horaire et de repos (voir app/validation). -ACTIVE_ASSIGNMENT_STATUSES: frozenset[AssignmentStatus] = frozenset( - {AssignmentStatus.PROPOSED, AssignmentStatus.CONFIRMED} -) diff --git a/backend/app/seed.py b/backend/app/seed.py index 2c964de..c1838cc 100644 --- a/backend/app/seed.py +++ b/backend/app/seed.py @@ -20,7 +20,6 @@ from app.models import ( Account, Assignment, AssignmentSource, - AssignmentStatus, Availability, AvailabilityStatus, DayPart, @@ -225,7 +224,7 @@ class Summary: f" Postes : {self.positions}", f" Créneaux : {self.shifts}", f" Créneaux sous l'effectif minimal (trous) : {self.understaffed_shifts}", - f" Affectations actives : {self.assignments}", + f" Affectations : {self.assignments}", f" Avertissements actifs : {sum(self.warnings.values())}", ] for code, count in sorted(self.warnings.items()): @@ -490,11 +489,6 @@ def build(session: Session, *, rng: random.Random | None = None) -> Summary: edition_id=edition.id, volunteer_id=volunteer.id, shift_id=shift.id, - status=( - AssignmentStatus.CONFIRMED - if rng.random() < 0.55 - else AssignmentStatus.PROPOSED - ), source=AssignmentSource.MANUAL, ) ) @@ -662,7 +656,6 @@ def _add_active(session, edition, volunteer, shift, assigned) -> None: edition_id=edition.id, volunteer_id=volunteer.id, shift_id=shift.id, - status=AssignmentStatus.CONFIRMED, source=AssignmentSource.MANUAL, ) ) @@ -671,12 +664,10 @@ def _add_active(session, edition, volunteer, shift, assigned) -> None: def _verify(session: Session, edition: Edition, summary: Summary) -> None: session.flush() - active = session.exec( - select(Assignment) - .where(Assignment.edition_id == edition.id) - .where(Assignment.status.in_((AssignmentStatus.PROPOSED, AssignmentStatus.CONFIRMED))) + assignments = session.exec( + select(Assignment).where(Assignment.edition_id == edition.id) ) - for assignment in active: + for assignment in assignments: volunteer = session.get(Volunteer, assignment.volunteer_id) shift = session.get(Shift, assignment.shift_id) for violation in validate( diff --git a/backend/app/validation/loader.py b/backend/app/validation/loader.py index c77c005..032e209 100644 --- a/backend/app/validation/loader.py +++ b/backend/app/validation/loader.py @@ -7,7 +7,6 @@ import uuid from sqlmodel import Session, select from app.models import ( - ACTIVE_ASSIGNMENT_STATUSES, Assignment, Availability, DayPart, @@ -65,13 +64,12 @@ def build_validation_input( ) code_by_day_part_id = {dp.id: dp.code for dp in day_parts} + # Toute affectation existante du bénévole compte (une affectation = une + # décision, toujours active). stmt = ( select(Shift) .join(Assignment, Assignment.shift_id == Shift.id) .where(Assignment.volunteer_id == volunteer.id) - .where( - Assignment.status.in_(tuple(ACTIVE_ASSIGNMENT_STATUSES)) - ) .where(Shift.id != shift.id) ) if exclude_assignment_id is not None: diff --git a/backend/app/validation/models.py b/backend/app/validation/models.py index 98b729b..037665e 100644 --- a/backend/app/validation/models.py +++ b/backend/app/validation/models.py @@ -76,9 +76,8 @@ class AvailabilityDeclaration: class ValidationInput: """Tout ce qu'il faut pour valider une affectation candidate. - `existing_shifts` = les créneaux des autres affectations **actives** - (`PROPOSED` / `CONFIRMED`) du même bénévole ; le filtrage par statut est à - la charge de l'appelant. + `existing_shifts` = les créneaux des autres affectations du même bénévole + (une affectation = une décision, il n'y a pas d'étape de confirmation). """ candidate_shift: ShiftInfo diff --git a/backend/tests/test_api_assignments.py b/backend/tests/test_api_assignments.py index f5232ae..4aadef3 100644 --- a/backend/tests/test_api_assignments.py +++ b/backend/tests/test_api_assignments.py @@ -35,8 +35,8 @@ def test_create_assignment_clean(client, edition, session, day_parts): assert resp.status_code == 201 body = resp.json() assert body["warnings"] == [] - assert body["assignment"]["status"] == "PROPOSED" assert body["assignment"]["source"] == "MANUAL" + assert body["assignment"]["locked"] is False def test_create_assignment_returns_warnings(client, edition, session, day_parts): @@ -135,7 +135,7 @@ def test_validate_endpoint_does_not_persist(client, edition, session, day_parts) assert client.get(_url(edition)).json() == [] -def test_update_status_revalidates_and_returns_warnings( +def test_patch_locked_keeps_assignment_and_returns_warnings( client, edition, session, day_parts ): volunteer, shift = _setup(session, edition, day_parts, available=False) @@ -144,47 +144,14 @@ def test_update_status_revalidates_and_returns_warnings( json={"volunteer_id": str(volunteer.id), "shift_id": str(shift.id)}, ).json()["assignment"]["id"] - resp = client.patch( - _url(edition, f"/{aid}"), json={"status": "CONFIRMED"} - ) + resp = client.patch(_url(edition, f"/{aid}"), json={"locked": True}) assert resp.status_code == 200 - assert resp.json()["assignment"]["status"] == "CONFIRMED" + assert resp.json()["assignment"]["locked"] is True assert any( w["code"] == "OUTSIDE_AVAILABILITY" for w in resp.json()["warnings"] ) -def test_cancelling_an_assignment_skips_validation( - client, edition, session, day_parts -): - volunteer, shift = _setup(session, edition, day_parts) - first = client.post( - _url(edition), - json={"volunteer_id": str(volunteer.id), "shift_id": str(shift.id)}, - ).json()["assignment"]["id"] - - # Un second créneau qui chevauche, mais l'affectation est directement DECLINEE. - position = make_position(session, edition, name="X") - overlapping = make_shift( - session, edition, position, start=dt_at(10, 16), end=dt_at(10, 20) - ) - created = client.post( - _url(edition), - json={ - "volunteer_id": str(volunteer.id), - "shift_id": str(overlapping.id), - "status": "DECLINED", - }, - ) - assert created.status_code == 201 - - patched = client.patch( - _url(edition, f"/{first}"), json={"locked": True} - ) - assert patched.status_code == 200 - assert patched.json()["assignment"]["locked"] is True - - def test_delete_assignment(client, edition, session, day_parts): volunteer, shift = _setup(session, edition, day_parts) aid = client.post( diff --git a/backend/tests/test_api_matrix.py b/backend/tests/test_api_matrix.py index 2d95371..4a6dc53 100644 --- a/backend/tests/test_api_matrix.py +++ b/backend/tests/test_api_matrix.py @@ -1,5 +1,4 @@ - -from app.models import Assignment, AssignmentStatus +from app.models import Assignment from tests.factories import dt_at, make_position, make_shift, make_volunteer @@ -18,14 +17,7 @@ def test_matrix_shape_and_counts(client, edition, session): session, edition, parking, start=dt_at(10, 8), end=dt_at(10, 12) ) - for i, status in enumerate( - [ - AssignmentStatus.CONFIRMED, - AssignmentStatus.CONFIRMED, - AssignmentStatus.PROPOSED, - AssignmentStatus.DECLINED, - ] - ): + for i in range(3): volunteer = make_volunteer( session, edition, first_name=f"V{i}", last_name="X", email=f"v{i}@ex.fr" ) @@ -34,7 +26,6 @@ def test_matrix_shape_and_counts(client, edition, session): edition_id=edition.id, volunteer_id=volunteer.id, shift_id=shift_a.id, - status=status, ) ) session.commit() @@ -47,13 +38,12 @@ def test_matrix_shape_and_counts(client, edition, session): assert [s["shift_id"] for s in bar_shifts] == [str(shift_a.id), str(shift_b.id)] first = bar_shifts[0] - assert first["confirmed"] == 2 - assert first["proposed"] == 1 # la DECLINEE n'est pas comptée + assert first["assigned"] == 3 assert first["min_staff"] == 2 assert first["ideal_staff"] == 5 - assert bar_shifts[1]["confirmed"] == 0 - assert body["positions"][1]["shifts"][0]["proposed"] == 0 + assert bar_shifts[1]["assigned"] == 0 + assert body["positions"][1]["shifts"][0]["assigned"] == 0 def test_matrix_unknown_edition_is_404(client): diff --git a/backend/tests/test_models.py b/backend/tests/test_models.py index 33c90a2..0d0fd9f 100644 --- a/backend/tests/test_models.py +++ b/backend/tests/test_models.py @@ -7,7 +7,6 @@ from sqlalchemy.exc import IntegrityError from app.models import ( Assignment, AssignmentSource, - AssignmentStatus, Availability, AvailabilityStatus, DayPart, @@ -98,7 +97,6 @@ def test_create_core_graph(session): session.refresh(assignment) # Valeurs par défaut appliquées. - assert assignment.status == AssignmentStatus.PROPOSED assert assignment.source == AssignmentSource.MANUAL assert assignment.locked is False # Relations. diff --git a/backend/tests/test_validation_loader.py b/backend/tests/test_validation_loader.py index 04f4e36..b2da482 100644 --- a/backend/tests/test_validation_loader.py +++ b/backend/tests/test_validation_loader.py @@ -1,6 +1,6 @@ import datetime as dt -from app.models import Assignment, AssignmentStatus, AvailabilityStatus +from app.models import Assignment, AvailabilityStatus from app.validation import AvailabilityDeclaration from app.validation.loader import build_validation_input from tests.factories import ( @@ -18,12 +18,9 @@ def test_build_input_gathers_everything_from_db(client, edition, session, day_pa session, edition, position, start=dt_at(10, 14), end=dt_at(10, 18), skills=["SST"], ) - active_shift = make_shift( + other_shift = make_shift( session, edition, position, start=dt_at(11, 9), end=dt_at(11, 12) ) - declined_shift = make_shift( - session, edition, position, start=dt_at(12, 9), end=dt_at(12, 12) - ) volunteer = make_volunteer( session, edition, email="v@ex.fr", skills=["SST", "PERMIS_B"] ) @@ -31,17 +28,11 @@ def test_build_input_gathers_everything_from_db(client, edition, session, day_pa session, edition, volunteer, day_parts["AFTERNOON"], date=dt.date(2026, 7, 10), status=AvailabilityStatus.IF_NEEDED, ) - session.add_all( - [ - Assignment( - edition_id=edition.id, volunteer_id=volunteer.id, - shift_id=active_shift.id, status=AssignmentStatus.CONFIRMED, - ), - Assignment( - edition_id=edition.id, volunteer_id=volunteer.id, - shift_id=declined_shift.id, status=AssignmentStatus.DECLINED, - ), - ] + session.add( + Assignment( + edition_id=edition.id, volunteer_id=volunteer.id, + shift_id=other_shift.id, + ) ) session.commit() @@ -52,8 +43,8 @@ def test_build_input_gathers_everything_from_db(client, edition, session, day_pa assert data.candidate_shift.id == str(candidate.id) assert data.candidate_shift.required_skill_codes == frozenset({"SST"}) assert data.volunteer_skill_codes == frozenset({"SST", "PERMIS_B"}) - # Seul le créneau de l'affectation active, ni le candidat ni la DECLINEE. - assert [s.id for s in data.existing_shifts] == [str(active_shift.id)] + # Toute affectation existante compte, sauf le créneau candidat lui-même. + assert [s.id for s in data.existing_shifts] == [str(other_shift.id)] assert len(data.day_parts) == 3 assert data.availabilities == ( AvailabilityDeclaration( @@ -73,7 +64,7 @@ def test_exclude_assignment_id_is_honoured(client, edition, session, day_parts): volunteer = make_volunteer(session, edition, email="v@ex.fr") own = Assignment( edition_id=edition.id, volunteer_id=volunteer.id, - shift_id=other_shift.id, status=AssignmentStatus.PROPOSED, + shift_id=other_shift.id, ) session.add(own) session.commit()