Étape 5 : API CRUD, matrice et validation branchée
- CRUD complet, scopé par édition, sur Position, Shift, Volunteer, Assignment
(routers app/api/*, schémas Pydantic, dépendance get_edition partagée)
- Volunteer : gestion des compétences par codes (création à la volée),
suppression = effacement complet en cascade (RGPD)
- Shift : contrôle fin > début, poste de la même édition, compétences requises
- Assignment : toute écriture active passe par le moteur de validation ;
violation bloquante -> 422 error.validation_failed, avertissements renvoyés
dans la réponse. Endpoint POST .../assignments/validate en simulation.
app/validation/loader.py construit la ValidationInput depuis la base.
- GET /editions/{id}/matrix : postes -> créneaux -> {min, ideal, confirmed,
proposed}, source de l'écran principal
- 30 tests d'API (CRUD, 404/409/422, cascade, blocage, avertissements, matrice,
loader) ; 104 tests au total ; alembic check toujours vert
- Clés de traduction shift.* / assignment.* / volunteer.* ajoutées
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
c96e85af37
commit
74e35f0195
@@ -0,0 +1,118 @@
|
||||
"""Fabriques d'objets du domaine pour les tests (écriture directe en base)."""
|
||||
|
||||
import datetime as dt
|
||||
|
||||
from app.models import (
|
||||
Availability,
|
||||
AvailabilityStatus,
|
||||
Position,
|
||||
PositionCategory,
|
||||
Shift,
|
||||
Skill,
|
||||
Volunteer,
|
||||
VolunteerSkill,
|
||||
)
|
||||
|
||||
|
||||
def make_position(session, edition, *, name="Bar", category=PositionCategory.BAR):
|
||||
position = Position(edition_id=edition.id, name=name, category=category)
|
||||
session.add(position)
|
||||
session.commit()
|
||||
session.refresh(position)
|
||||
return position
|
||||
|
||||
|
||||
def make_shift(
|
||||
session, edition, position, *, start, end, min_staff=1, ideal_staff=2, skills=()
|
||||
):
|
||||
shift = Shift(
|
||||
edition_id=edition.id,
|
||||
position_id=position.id,
|
||||
starts_at=start,
|
||||
ends_at=end,
|
||||
min_staff=min_staff,
|
||||
ideal_staff=ideal_staff,
|
||||
)
|
||||
session.add(shift)
|
||||
session.flush()
|
||||
for code in skills:
|
||||
_link_skill(session, edition, code=code, shift=shift)
|
||||
session.commit()
|
||||
session.refresh(shift)
|
||||
return shift
|
||||
|
||||
|
||||
def make_volunteer(
|
||||
session, edition, *, first_name="Alex", last_name="Martin", email=None, skills=()
|
||||
):
|
||||
volunteer = Volunteer(
|
||||
edition_id=edition.id,
|
||||
first_name=first_name,
|
||||
last_name=last_name,
|
||||
email=email,
|
||||
)
|
||||
session.add(volunteer)
|
||||
session.flush()
|
||||
for code in skills:
|
||||
_link_skill(session, edition, volunteer=volunteer, code=code)
|
||||
session.commit()
|
||||
session.refresh(volunteer)
|
||||
return volunteer
|
||||
|
||||
|
||||
def make_availability(session, edition, volunteer, day_part, *, date, status):
|
||||
row = Availability(
|
||||
edition_id=edition.id,
|
||||
volunteer_id=volunteer.id,
|
||||
day_part_id=day_part.id,
|
||||
date=date,
|
||||
status=status,
|
||||
)
|
||||
session.add(row)
|
||||
session.commit()
|
||||
return row
|
||||
|
||||
|
||||
def declare_available_all_day(session, edition, volunteer, day_parts, date):
|
||||
for part in day_parts.values():
|
||||
make_availability(
|
||||
session,
|
||||
edition,
|
||||
volunteer,
|
||||
part,
|
||||
date=date,
|
||||
status=AvailabilityStatus.AVAILABLE,
|
||||
)
|
||||
|
||||
|
||||
def _get_or_create_skill(session, edition, code) -> Skill:
|
||||
from sqlmodel import select
|
||||
|
||||
skill = session.exec(
|
||||
select(Skill)
|
||||
.where(Skill.edition_id == edition.id)
|
||||
.where(Skill.code == code)
|
||||
).first()
|
||||
if skill is None:
|
||||
skill = Skill(edition_id=edition.id, code=code, label=code)
|
||||
session.add(skill)
|
||||
session.flush()
|
||||
return skill
|
||||
|
||||
|
||||
def _link_skill(session, edition, *, code, volunteer=None, shift=None):
|
||||
from app.models import ShiftRequiredSkill
|
||||
|
||||
skill = _get_or_create_skill(session, edition, code)
|
||||
if volunteer is not None:
|
||||
session.add(
|
||||
VolunteerSkill(volunteer_id=volunteer.id, skill_id=skill.id)
|
||||
)
|
||||
if shift is not None:
|
||||
session.add(
|
||||
ShiftRequiredSkill(shift_id=shift.id, skill_id=skill.id)
|
||||
)
|
||||
|
||||
|
||||
def dt_at(day: int, hour: int, minute: int = 0) -> dt.datetime:
|
||||
return dt.datetime(2026, 7, day, hour, minute)
|
||||
Reference in New Issue
Block a user