Édition éditable (dates) + robustesse de l'écran Postes & créneaux
Back-end :
- GET /editions/{id} (tout compte de l'édition) et PATCH /editions/{id}
(COORDINATION) : name, year, start_date, end_date, retention_days.
Refus si end_date < start_date (edition.dates_inconsistent). Tests.
Front-end :
- api : EditionRead/Update, editions.get/update (http + mock), hooks
useEdition / useUpdateEdition.
- ConfigGrid : le bouton « ajouter un poste » sélectionne le poste créé et
affiche l'état (pending) + les erreurs (ErrorNote) — avant, un échec ou
même un succès passaient inaperçus. Renommage du poste + choix de la
catégorie + suppression dans l'en-tête. Une colonne par jour de l'édition
(même vide) → on peut ajouter un créneau n'importe quel jour.
- ConfigRules : formulaire d'édition (nom + dates) → c'est là qu'on ajoute
ou retire des jours ; compteur de jours ; retour d'enregistrement.
- i18n mis à jour.
144 tests back OK ; typecheck + lint + build + smoke front OK.
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
9521034207
commit
e0a0c56307
@@ -0,0 +1,39 @@
|
||||
"""Lecture et mise à jour d'une édition (dates, nom, rétention)."""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlmodel import Session
|
||||
|
||||
from app.api.deps import get_edition
|
||||
from app.api.schemas import EditionRead, EditionUpdate
|
||||
from app.auth.deps import current_account_in_edition, require_coordinator
|
||||
from app.db import get_session
|
||||
from app.models import Account, Edition
|
||||
|
||||
router = APIRouter(prefix="/editions", tags=["editions"])
|
||||
|
||||
|
||||
@router.get("/{edition_id}", response_model=EditionRead)
|
||||
def read_edition(
|
||||
edition: Edition = Depends(get_edition),
|
||||
_account: Account = Depends(current_account_in_edition),
|
||||
) -> Edition:
|
||||
return edition
|
||||
|
||||
|
||||
@router.patch("/{edition_id}", response_model=EditionRead)
|
||||
def update_edition(
|
||||
payload: EditionUpdate,
|
||||
edition: Edition = Depends(get_edition),
|
||||
_guard: Account = Depends(require_coordinator),
|
||||
session: Session = Depends(get_session),
|
||||
) -> Edition:
|
||||
for field, value in payload.model_dump(exclude_unset=True).items():
|
||||
setattr(edition, field, value)
|
||||
if edition.end_date < edition.start_date:
|
||||
raise HTTPException(
|
||||
status_code=422, detail={"code": "edition.dates_inconsistent"}
|
||||
)
|
||||
session.add(edition)
|
||||
session.commit()
|
||||
session.refresh(edition)
|
||||
return edition
|
||||
@@ -19,6 +19,28 @@ class ApiModel(BaseModel):
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Edition #
|
||||
# --------------------------------------------------------------------------- #
|
||||
class EditionRead(ApiModel):
|
||||
id: uuid.UUID
|
||||
year: int
|
||||
name: str | None
|
||||
start_date: dt.date
|
||||
end_date: dt.date
|
||||
retention_days: int | None
|
||||
created_at: dt.datetime
|
||||
updated_at: dt.datetime
|
||||
|
||||
|
||||
class EditionUpdate(ApiModel):
|
||||
name: str | None = None
|
||||
year: int | None = None
|
||||
start_date: dt.date | None = None
|
||||
end_date: dt.date | None = None
|
||||
retention_days: int | None = None
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Volunteer #
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"error.unauthorized": "Authentification requise",
|
||||
"error.validation_failed": "La validation a échoué",
|
||||
"error.conflict": "Conflit avec une ressource existante.",
|
||||
"edition.dates_inconsistent": "La date de fin doit être postérieure ou égale à la date de début.",
|
||||
"shift.ends_not_after_starts": "La fin du créneau doit être postérieure à son début.",
|
||||
"shift.position_wrong_edition": "Le poste n'appartient pas à cette édition.",
|
||||
"volunteer.duplicate_email": "Un bénévole avec cet email existe déjà dans l'édition.",
|
||||
|
||||
@@ -4,6 +4,7 @@ from fastapi.middleware.cors import CORSMiddleware
|
||||
from app.api.accounts import router as account_router
|
||||
from app.api.assignments import router as assignment_router
|
||||
from app.api.auth import router as auth_router
|
||||
from app.api.editions import router as edition_router
|
||||
from app.api.health import router as health_router
|
||||
from app.api.imports import router as import_router
|
||||
from app.api.matrix import router as matrix_router
|
||||
@@ -30,6 +31,7 @@ app.add_middleware(
|
||||
|
||||
app.include_router(health_router)
|
||||
app.include_router(auth_router)
|
||||
app.include_router(edition_router)
|
||||
app.include_router(account_router)
|
||||
app.include_router(import_router)
|
||||
app.include_router(volunteer_router)
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import datetime as dt
|
||||
import uuid
|
||||
|
||||
from app.models import Role
|
||||
|
||||
|
||||
def _url(edition):
|
||||
return f"/editions/{edition.id}"
|
||||
|
||||
|
||||
def test_read_edition_exposes_the_date_range(client, edition):
|
||||
body = client.get(_url(edition)).json()
|
||||
assert body["start_date"] == "2026-07-09"
|
||||
assert body["end_date"] == "2026-07-12"
|
||||
assert body["year"] == 2026
|
||||
|
||||
|
||||
def test_coordinator_can_extend_the_edition(client, edition):
|
||||
resp = client.patch(_url(edition), json={"end_date": "2026-07-15"})
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["end_date"] == "2026-07-15"
|
||||
assert client.get(_url(edition)).json()["end_date"] == "2026-07-15"
|
||||
|
||||
|
||||
def test_end_before_start_is_rejected(client, edition):
|
||||
resp = client.patch(_url(edition), json={"end_date": "2026-07-01"})
|
||||
assert resp.status_code == 422
|
||||
assert resp.json()["detail"]["code"] == "edition.dates_inconsistent"
|
||||
|
||||
|
||||
def test_lead_cannot_update_the_edition(edition, make_account, as_account):
|
||||
lead = make_account(Role.LEAD, email="lead@e.fr")
|
||||
resp = as_account(lead).patch(_url(edition), json={"name": "Renommée"})
|
||||
assert resp.status_code == 403
|
||||
|
||||
|
||||
def test_volunteer_can_read_but_not_update(edition, session, make_account, as_account):
|
||||
from tests.factories import make_volunteer
|
||||
|
||||
volunteer = make_volunteer(session, edition, email="v@e.fr")
|
||||
account = make_account(Role.VOLUNTEER, volunteer_id=volunteer.id, email="v@e.fr")
|
||||
client = as_account(account)
|
||||
assert client.get(_url(edition)).status_code == 200
|
||||
assert client.patch(_url(edition), json={"name": "X"}).status_code == 403
|
||||
|
||||
|
||||
def test_unknown_edition_is_404(client):
|
||||
assert client.get(f"/editions/{uuid.uuid4()}").status_code == 404
|
||||
|
||||
|
||||
def test_update_keeps_unchanged_fields(client, edition):
|
||||
resp = client.patch(_url(edition), json={"name": "Festival 2026"})
|
||||
assert resp.status_code == 200
|
||||
body = resp.json()
|
||||
assert body["name"] == "Festival 2026"
|
||||
assert body["start_date"] == "2026-07-09"
|
||||
assert dt.date.fromisoformat(body["end_date"]) == dt.date(2026, 7, 12)
|
||||
Reference in New Issue
Block a user