Retrait des compétences ; disponibilités en fenêtres horaires libres
Compétences supprimées de bout en bout :
- tables `skill`, `volunteer_skill`, `shift_required_skill` ;
- `Volunteer.skill_codes`, `Shift.required_skill_codes`, colonne `skills`
de l'import, règle de validation `MISSING_REQUIRED_SKILL`, jeu de démo.
Disponibilités repensées : `Availability` passe de (`day_part_id`,
`status` tri-état) à une **fenêtre horaire libre** (`date`, `start_time`,
`end_time` ; `end <= start` ⇒ passe minuit), plusieurs par jour.
- Nouveaux endpoints `GET`/`PUT /editions/{id}/volunteers/{vid}/availability`
(PUT = remplacement complet, COORDINATION) et
`GET /editions/{id}/availability` (toute l'édition, pour la grille).
- `OUTSIDE_AVAILABILITY` (toujours WARNING) : compare le créneau à l'union
des fenêtres du bénévole ce jour-là ; renvoie `date` + `uncovered_minutes`.
`DayPart` ne sert plus qu'à la borne de « journée » du cumul 8 h.
- L'import ne gère plus les disponibilités (fenêtres saisies dans l'app) :
`values.py` supprimé, mapping réduit à nom/email/téléphone/commentaire.
Migration 0007 (recrée `availability`). `AvailabilityStatus` supprimé.
158 tests, `alembic check` et `ruff` au vert. Front à suivre.
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
81609eecc5
commit
be51770b51
@@ -16,7 +16,6 @@ from sqlmodel import Field, Relationship, SQLModel, UniqueConstraint
|
||||
from app.models.base import TimestampMixin
|
||||
from app.models.enums import (
|
||||
AssignmentSource,
|
||||
AvailabilityStatus,
|
||||
PositionCategory,
|
||||
Role,
|
||||
)
|
||||
@@ -27,28 +26,6 @@ _PASSIVE = {"passive_deletes": True}
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Tables d'association (sans champs d'audit) #
|
||||
# --------------------------------------------------------------------------- #
|
||||
class VolunteerSkill(SQLModel, table=True):
|
||||
__tablename__ = "volunteer_skill"
|
||||
|
||||
volunteer_id: uuid.UUID = Field(
|
||||
foreign_key="volunteer.id", primary_key=True, ondelete="CASCADE"
|
||||
)
|
||||
skill_id: uuid.UUID = Field(
|
||||
foreign_key="skill.id", primary_key=True, ondelete="CASCADE"
|
||||
)
|
||||
|
||||
|
||||
class ShiftRequiredSkill(SQLModel, table=True):
|
||||
__tablename__ = "shift_required_skill"
|
||||
|
||||
shift_id: uuid.UUID = Field(
|
||||
foreign_key="shift.id", primary_key=True, ondelete="CASCADE"
|
||||
)
|
||||
skill_id: uuid.UUID = Field(
|
||||
foreign_key="skill.id", primary_key=True, ondelete="CASCADE"
|
||||
)
|
||||
|
||||
|
||||
class VolunteerArtist(SQLModel, table=True):
|
||||
"""Vœu d'un bénévole : « je veux voir cet artiste »."""
|
||||
|
||||
@@ -114,38 +91,11 @@ class DayPart(TimestampMixin, table=True):
|
||||
)
|
||||
|
||||
edition: Edition = Relationship(back_populates="day_parts")
|
||||
availabilities: list["Availability"] = Relationship(
|
||||
back_populates="day_part", sa_relationship_kwargs=_PASSIVE
|
||||
)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Personnes : bénévoles, compétences, comptes #
|
||||
# Personnes : bénévoles, comptes #
|
||||
# --------------------------------------------------------------------------- #
|
||||
class Skill(TimestampMixin, table=True):
|
||||
__table_args__ = (
|
||||
UniqueConstraint("edition_id", "code", name="uq_skill_edition_code"),
|
||||
)
|
||||
|
||||
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
|
||||
edition_id: uuid.UUID = Field(
|
||||
foreign_key="edition.id", index=True, ondelete="CASCADE"
|
||||
)
|
||||
code: str
|
||||
label: str
|
||||
|
||||
volunteers: list["Volunteer"] = Relationship(
|
||||
back_populates="skills",
|
||||
link_model=VolunteerSkill,
|
||||
sa_relationship_kwargs=_PASSIVE,
|
||||
)
|
||||
shifts_requiring: list["Shift"] = Relationship(
|
||||
back_populates="required_skills",
|
||||
link_model=ShiftRequiredSkill,
|
||||
sa_relationship_kwargs=_PASSIVE,
|
||||
)
|
||||
|
||||
|
||||
class Artist(TimestampMixin, table=True):
|
||||
"""Artiste programmé sur l'édition.
|
||||
|
||||
@@ -194,11 +144,6 @@ class Volunteer(TimestampMixin, table=True):
|
||||
phone: str | None = None
|
||||
comment: str | None = None
|
||||
|
||||
skills: list[Skill] = Relationship(
|
||||
back_populates="volunteers",
|
||||
link_model=VolunteerSkill,
|
||||
sa_relationship_kwargs=_PASSIVE,
|
||||
)
|
||||
wished_artists: list[Artist] = Relationship(
|
||||
back_populates="fans",
|
||||
link_model=VolunteerArtist,
|
||||
@@ -319,25 +264,25 @@ class Shift(TimestampMixin, table=True):
|
||||
)
|
||||
|
||||
position: Position = Relationship(back_populates="shifts")
|
||||
required_skills: list[Skill] = Relationship(
|
||||
back_populates="shifts_requiring",
|
||||
link_model=ShiftRequiredSkill,
|
||||
sa_relationship_kwargs=_PASSIVE,
|
||||
)
|
||||
assignments: list["Assignment"] = Relationship(
|
||||
back_populates="shift", sa_relationship_kwargs=_PASSIVE
|
||||
)
|
||||
|
||||
|
||||
class Availability(TimestampMixin, table=True):
|
||||
"""Déclaration du bénévole : « je peux venir tel jour, telle demi-journée ».
|
||||
"""Déclaration du bénévole : « tel jour, je peux venir de telle à telle
|
||||
heure ». Fenêtre horaire libre (pas liée aux créneaux types). Plusieurs
|
||||
fenêtres par jour possibles.
|
||||
|
||||
À ne pas confondre avec Assignment (décision de l'organisation).
|
||||
Heures naïves, locales Europe/Paris. `end_time <= start_time` ⇒ la fenêtre
|
||||
passe minuit (se termine le lendemain). À ne pas confondre avec Assignment
|
||||
(décision de l'organisation).
|
||||
"""
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint(
|
||||
"volunteer_id", "date", "day_part_id", name="uq_availability_slot"
|
||||
"volunteer_id", "date", "start_time", "end_time",
|
||||
name="uq_availability_window",
|
||||
),
|
||||
)
|
||||
|
||||
@@ -349,17 +294,10 @@ class Availability(TimestampMixin, table=True):
|
||||
foreign_key="volunteer.id", index=True, ondelete="CASCADE"
|
||||
)
|
||||
date: dt.date
|
||||
day_part_id: uuid.UUID = Field(
|
||||
foreign_key="day_part.id", index=True, ondelete="CASCADE"
|
||||
)
|
||||
status: AvailabilityStatus = Field(
|
||||
sa_column=Column(
|
||||
SAEnum(AvailabilityStatus, native_enum=False), nullable=False
|
||||
)
|
||||
)
|
||||
start_time: dt.time
|
||||
end_time: dt.time
|
||||
|
||||
volunteer: Volunteer = Relationship(back_populates="availabilities")
|
||||
day_part: DayPart = Relationship(back_populates="availabilities")
|
||||
|
||||
|
||||
class Assignment(TimestampMixin, table=True):
|
||||
|
||||
Reference in New Issue
Block a user