É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,192 @@
|
||||
"""Schémas d'entrée/sortie de l'API CRUD et de la matrice."""
|
||||
|
||||
import datetime as dt
|
||||
import uuid
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from app.models import (
|
||||
AssignmentSource,
|
||||
AssignmentStatus,
|
||||
PositionCategory,
|
||||
TshirtSize,
|
||||
)
|
||||
from app.validation import Violation
|
||||
|
||||
|
||||
class ApiModel(BaseModel):
|
||||
"""Base commune : autorise la construction depuis un objet ORM."""
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Volunteer #
|
||||
# --------------------------------------------------------------------------- #
|
||||
class VolunteerCreate(ApiModel):
|
||||
first_name: str
|
||||
last_name: str
|
||||
email: str | None = None
|
||||
phone: str | None = None
|
||||
tshirt_size: TshirtSize | None = None
|
||||
diet: str | None = None
|
||||
comment: str | None = None
|
||||
skill_codes: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class VolunteerUpdate(ApiModel):
|
||||
first_name: str | None = None
|
||||
last_name: str | None = None
|
||||
email: str | None = None
|
||||
phone: str | None = None
|
||||
tshirt_size: TshirtSize | None = None
|
||||
diet: str | None = None
|
||||
comment: str | None = None
|
||||
# None = inchangé ; [] = vider les compétences.
|
||||
skill_codes: list[str] | None = None
|
||||
|
||||
|
||||
class VolunteerRead(ApiModel):
|
||||
id: uuid.UUID
|
||||
edition_id: uuid.UUID
|
||||
first_name: str
|
||||
last_name: str
|
||||
email: str | None
|
||||
phone: str | None
|
||||
tshirt_size: TshirtSize | None
|
||||
diet: str | None
|
||||
comment: str | None
|
||||
skill_codes: list[str]
|
||||
created_at: dt.datetime
|
||||
updated_at: dt.datetime
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Position #
|
||||
# --------------------------------------------------------------------------- #
|
||||
class PositionCreate(ApiModel):
|
||||
name: str
|
||||
location: str | None = None
|
||||
category: PositionCategory
|
||||
lead_account_id: uuid.UUID | None = None
|
||||
|
||||
|
||||
class PositionUpdate(ApiModel):
|
||||
name: str | None = None
|
||||
location: str | None = None
|
||||
category: PositionCategory | None = None
|
||||
lead_account_id: uuid.UUID | None = None
|
||||
|
||||
|
||||
class PositionRead(ApiModel):
|
||||
id: uuid.UUID
|
||||
edition_id: uuid.UUID
|
||||
name: str
|
||||
location: str | None
|
||||
category: PositionCategory
|
||||
lead_account_id: uuid.UUID | None
|
||||
created_at: dt.datetime
|
||||
updated_at: dt.datetime
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Shift #
|
||||
# --------------------------------------------------------------------------- #
|
||||
class ShiftCreate(ApiModel):
|
||||
position_id: uuid.UUID
|
||||
starts_at: dt.datetime
|
||||
ends_at: dt.datetime
|
||||
min_staff: int = Field(default=0, ge=0)
|
||||
ideal_staff: int = Field(default=0, ge=0)
|
||||
required_skill_codes: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class ShiftUpdate(ApiModel):
|
||||
position_id: uuid.UUID | None = None
|
||||
starts_at: dt.datetime | None = None
|
||||
ends_at: dt.datetime | None = None
|
||||
min_staff: int | None = Field(default=None, ge=0)
|
||||
ideal_staff: int | None = Field(default=None, ge=0)
|
||||
required_skill_codes: list[str] | None = None
|
||||
|
||||
|
||||
class ShiftRead(ApiModel):
|
||||
id: uuid.UUID
|
||||
edition_id: uuid.UUID
|
||||
position_id: uuid.UUID
|
||||
starts_at: dt.datetime
|
||||
ends_at: dt.datetime
|
||||
min_staff: int
|
||||
ideal_staff: int
|
||||
required_skill_codes: list[str]
|
||||
created_at: dt.datetime
|
||||
updated_at: dt.datetime
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Assignment #
|
||||
# --------------------------------------------------------------------------- #
|
||||
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
|
||||
|
||||
|
||||
class AssignmentRead(ApiModel):
|
||||
id: uuid.UUID
|
||||
edition_id: uuid.UUID
|
||||
volunteer_id: uuid.UUID
|
||||
shift_id: uuid.UUID
|
||||
status: AssignmentStatus
|
||||
source: AssignmentSource
|
||||
locked: bool
|
||||
created_at: dt.datetime
|
||||
updated_at: dt.datetime
|
||||
|
||||
|
||||
class AssignmentWriteResult(ApiModel):
|
||||
assignment: AssignmentRead
|
||||
warnings: list[Violation] = Field(default_factory=list)
|
||||
|
||||
|
||||
class AssignmentValidateRequest(ApiModel):
|
||||
volunteer_id: uuid.UUID
|
||||
shift_id: uuid.UUID
|
||||
|
||||
|
||||
class AssignmentValidateResult(ApiModel):
|
||||
blocking: bool
|
||||
violations: list[Violation] = Field(default_factory=list)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Matrice poste × créneau #
|
||||
# --------------------------------------------------------------------------- #
|
||||
class MatrixShift(ApiModel):
|
||||
shift_id: uuid.UUID
|
||||
starts_at: dt.datetime
|
||||
ends_at: dt.datetime
|
||||
min_staff: int
|
||||
ideal_staff: int
|
||||
confirmed: int
|
||||
proposed: int
|
||||
|
||||
|
||||
class MatrixPosition(ApiModel):
|
||||
id: uuid.UUID
|
||||
name: str
|
||||
category: PositionCategory
|
||||
location: str | None
|
||||
shifts: list[MatrixShift]
|
||||
|
||||
|
||||
class MatrixResponse(ApiModel):
|
||||
edition_id: uuid.UUID
|
||||
positions: list[MatrixPosition]
|
||||
Reference in New Issue
Block a user