Étape 20 : planches de badges bénévoles (PDF)
Recto personnalisable (fond image + champs positionnés : nom, poste(s)
affectés, catégorie, rôle) via un éditeur visuel glisser-déposer ; verso
fixe, la même image pour tous les bénévoles. Nouvel écran « Badges »
(COORDINATION uniquement, /badges).
Backend : modèle BadgeTemplate/BadgeField (migration 0011), génération
PDF isolée (app/badges/pdf.py, ReportLab — planches recto suivies des
planches verso correspondantes pour une impression recto-verso), route
POST /editions/{id}/badges/generate, tests (module pur + API).
Frontend : client HTTP (upload multipart, réponses binaires images/PDF),
client mock (mode démonstration), éditeur de positionnement en
millimètres (coordonnées partagées avec le backend), écran de génération
avec sélection des bénévoles et téléchargement.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
4bcdafca96
commit
b7fc80a7dc
@@ -9,13 +9,15 @@ au niveau base (`ondelete="CASCADE"`) ; les relations ORM utilisent
|
||||
import datetime as dt
|
||||
import uuid
|
||||
|
||||
from sqlalchemy import Boolean, CheckConstraint, Column, Integer, Time
|
||||
from sqlalchemy import Boolean, CheckConstraint, Column, Integer, LargeBinary, Time
|
||||
from sqlalchemy import Enum as SAEnum
|
||||
from sqlmodel import Field, Relationship, SQLModel, UniqueConstraint
|
||||
|
||||
from app.models.base import TimestampMixin
|
||||
from app.models.enums import (
|
||||
AssignmentSource,
|
||||
BadgeAlign,
|
||||
BadgeFieldType,
|
||||
PositionCategory,
|
||||
Role,
|
||||
)
|
||||
@@ -364,3 +366,65 @@ class Assignment(TimestampMixin, table=True):
|
||||
|
||||
volunteer: Volunteer = Relationship(back_populates="assignments")
|
||||
shift: Shift = Relationship(back_populates="assignments")
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Badges (planches recto/verso, voir app/badges/) #
|
||||
# --------------------------------------------------------------------------- #
|
||||
class BadgeTemplate(TimestampMixin, table=True):
|
||||
"""Recto/verso du badge d'une édition. Une ligne par édition (créée à la
|
||||
demande). Le verso (`back_image`) est le même pour tous les bénévoles ;
|
||||
le recto combine `front_image` (fond) et les `BadgeField` positionnés
|
||||
dessus. Images stockées en base (pas de volume de fichiers à gérer)."""
|
||||
|
||||
__tablename__ = "badge_template"
|
||||
|
||||
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
|
||||
edition_id: uuid.UUID = Field(
|
||||
foreign_key="edition.id", unique=True, ondelete="CASCADE"
|
||||
)
|
||||
front_image: bytes | None = Field(default=None, sa_column=Column(LargeBinary))
|
||||
front_image_content_type: str | None = None
|
||||
back_image: bytes | None = Field(default=None, sa_column=Column(LargeBinary))
|
||||
back_image_content_type: str | None = None
|
||||
|
||||
|
||||
class BadgeField(TimestampMixin, table=True):
|
||||
"""Un champ positionné sur le recto du badge (nom, poste(s), catégorie,
|
||||
rôle — voir `BadgeFieldType`). Coordonnées en millimètres depuis le coin
|
||||
haut-gauche du badge (format CR80, 85,6 × 54 mm — voir `app/badges/pdf.py`).
|
||||
Au plus un champ par `field_type` et par édition : décocher un champ dans
|
||||
l'éditeur retire sa ligne (remplacement complet, `PUT .../fields`)."""
|
||||
|
||||
__tablename__ = "badge_field"
|
||||
__table_args__ = (
|
||||
UniqueConstraint(
|
||||
"edition_id", "field_type", name="uq_badge_field_edition_type"
|
||||
),
|
||||
)
|
||||
|
||||
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
|
||||
edition_id: uuid.UUID = Field(
|
||||
foreign_key="edition.id", index=True, ondelete="CASCADE"
|
||||
)
|
||||
field_type: BadgeFieldType = Field(
|
||||
sa_column=Column(SAEnum(BadgeFieldType, native_enum=False), nullable=False)
|
||||
)
|
||||
x_mm: float
|
||||
y_mm: float
|
||||
width_mm: float
|
||||
height_mm: float
|
||||
font_size: float = Field(default=10.0)
|
||||
align: BadgeAlign = Field(
|
||||
default=BadgeAlign.LEFT,
|
||||
sa_column=Column(
|
||||
SAEnum(BadgeAlign, native_enum=False),
|
||||
nullable=False,
|
||||
server_default=BadgeAlign.LEFT.value,
|
||||
),
|
||||
)
|
||||
bold: bool = Field(
|
||||
default=False,
|
||||
sa_column=Column(Boolean, nullable=False, server_default="false"),
|
||||
)
|
||||
color: str = Field(default="#000000")
|
||||
|
||||
Reference in New Issue
Block a user