Le chevauchement de créneaux (SHIFT_OVERLAP) n'est plus bloquant : il remonte en avertissement et l'affectation est enregistrée. Le solveur ne l'exclut plus mais applique un fort malus au bénévole déjà affecté ailleurs au même moment (proposé seulement sous l'effectif minimal, faute de mieux) ; il n'est jamais reproposé sur un créneau où il est déjà. Même malus dans le classement par défaut du panneau et dans le mock. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
72 lines
1.9 KiB
Python
72 lines
1.9 KiB
Python
"""Types d'entrée et de sortie du moteur de validation.
|
|
|
|
Découplé de la base : le moteur ne connaît que ces structures simples, jamais
|
|
les modèles SQLModel. La couche appelante construit l'entrée depuis la base.
|
|
"""
|
|
|
|
import datetime as dt
|
|
from dataclasses import dataclass
|
|
from enum import StrEnum
|
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
|
|
class Severity(StrEnum):
|
|
BLOCKING = "BLOCKING"
|
|
WARNING = "WARNING"
|
|
|
|
|
|
class ViolationCode(StrEnum):
|
|
SHIFT_OVERLAP = "SHIFT_OVERLAP"
|
|
|
|
|
|
SEVERITY_BY_CODE: dict[ViolationCode, Severity] = {
|
|
# Avertissement seulement : un bénévole peut tenir deux postes à la fois
|
|
# (le solveur l'évite par un malus, cf. `app.solver.engine`).
|
|
ViolationCode.SHIFT_OVERLAP: Severity.WARNING,
|
|
}
|
|
|
|
|
|
class Violation(BaseModel):
|
|
"""Une violation : un **code** + des paramètres structurés, jamais de phrase
|
|
(traduction via `locales/fr.json`, clés `validation.<CODE>`)."""
|
|
|
|
code: ViolationCode
|
|
severity: Severity
|
|
params: dict[str, str | int] = Field(default_factory=dict)
|
|
|
|
@classmethod
|
|
def of(cls, code: ViolationCode, **params: str | int) -> "Violation":
|
|
return cls(code=code, severity=SEVERITY_BY_CODE[code], params=params)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ShiftInfo:
|
|
id: str
|
|
starts_at: dt.datetime
|
|
ends_at: dt.datetime
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class DayPartWindow:
|
|
"""Créneau type. Ne sert plus qu'à fixer la borne de « journée de
|
|
festival » (`engine.day_boundary`)."""
|
|
|
|
code: str
|
|
start_time: dt.time
|
|
# end_time <= start_time ⇒ la demi-journée passe minuit.
|
|
end_time: dt.time
|
|
order: int
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ValidationInput:
|
|
"""Tout ce qu'il faut pour valider une affectation candidate.
|
|
|
|
`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
|
|
existing_shifts: tuple[ShiftInfo, ...] = ()
|