"""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, PositionCategory, Role, TshirtSize, ) from app.validation import Violation class ApiModel(BaseModel): """Base commune : autorise la construction depuis un objet ORM.""" 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 # # --------------------------------------------------------------------------- # 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 day_part_id: uuid.UUID | None 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 class GenerateShiftsResult(ApiModel): created: int # --------------------------------------------------------------------------- # # DayPart (créneau type) # # --------------------------------------------------------------------------- # class DayPartRead(ApiModel): id: uuid.UUID edition_id: uuid.UUID code: str label: str start_time: dt.time end_time: dt.time order: int created_at: dt.datetime updated_at: dt.datetime class DayPartCreate(ApiModel): label: str start_time: dt.time end_time: dt.time # Déduit du label si absent. Immuable ensuite (référencé par l'import). code: str | None = None order: int = 0 class DayPartUpdate(ApiModel): label: str | None = None start_time: dt.time | None = None end_time: dt.time | None = None order: int | None = None # --------------------------------------------------------------------------- # # Assignment # # --------------------------------------------------------------------------- # class AssignmentCreate(ApiModel): volunteer_id: uuid.UUID shift_id: uuid.UUID source: AssignmentSource = AssignmentSource.MANUAL locked: bool = False class AssignmentUpdate(ApiModel): 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 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 assigned: 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] # --------------------------------------------------------------------------- # # Authentification et comptes # # --------------------------------------------------------------------------- # class MagicLinkRequest(ApiModel): email: str class VerifyRequest(ApiModel): token: str class LoginRequest(ApiModel): email: str password: str class ChangePasswordRequest(ApiModel): current_password: str new_password: str class AccountRead(ApiModel): id: uuid.UUID edition_id: uuid.UUID email: str role: Role volunteer_id: uuid.UUID | None # Mode de connexion : "password" (admins) ou "magic_link". auth_method: str created_at: dt.datetime updated_at: dt.datetime class TokenResponse(ApiModel): access_token: str token_type: str account: AccountRead class AccountCreate(ApiModel): email: str role: Role volunteer_id: uuid.UUID | None = None # Requis pour un compte COORDINATION, interdit sinon. password: str | None = None # --------------------------------------------------------------------------- # # Notifications par email # # --------------------------------------------------------------------------- # class PlanningNotificationRequest(ApiModel): # Modèle saisi par la coordination. Variables : {prenom}, {nom}, # {nb_creneaux}, {nb_heures}, {creneaux}, {lien_connexion}. subject: str | None = None body: str | None = None # None = tous les bénévoles de l'édition qui ont un email. volunteer_ids: list[uuid.UUID] | None = None class PlanningNotificationResult(ApiModel): queued: int skipped_no_email: int recipients: list[uuid.UUID] = Field(default_factory=list)