Badges : retour à la ligne et créneaux en tableau par jour
Le texte des champs passe à la ligne pour tenir dans la largeur de son cadre (wrap_text) au lieu de déborder. Les créneaux affectés s'affichent en tableau : le jour de festival en en-tête, puis une colonne horaire et une colonne poste (plus de référent sur le badge) ; même tableau dans l'aperçu de l'éditeur. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
cfed76c239
commit
1a23881e81
+43
-24
@@ -8,6 +8,7 @@ pour la composition du PDF et `app/models/domain.py` (`BadgeTemplate`,
|
||||
`BadgeField`) pour le modèle.
|
||||
"""
|
||||
|
||||
import datetime as dt
|
||||
import io
|
||||
import uuid
|
||||
from collections import defaultdict
|
||||
@@ -15,7 +16,6 @@ from collections import defaultdict
|
||||
from fastapi import APIRouter, Depends, File, HTTPException, Response, UploadFile
|
||||
from PIL import Image, UnidentifiedImageError
|
||||
from sqlalchemy import delete
|
||||
from sqlalchemy.orm import aliased
|
||||
from sqlmodel import Session, select
|
||||
|
||||
from app.api.deps import get_edition
|
||||
@@ -32,6 +32,8 @@ from app.badges.pdf import (
|
||||
DEFAULT_BADGE_WIDTH_MM,
|
||||
BadgeFieldSpec,
|
||||
BadgeSpec,
|
||||
BadgeTable,
|
||||
BadgeTableSection,
|
||||
generate_badge_sheets_pdf,
|
||||
image_size_mm,
|
||||
)
|
||||
@@ -43,12 +45,14 @@ from app.models import (
|
||||
BadgeField,
|
||||
BadgeFieldType,
|
||||
BadgeTemplate,
|
||||
DayPart,
|
||||
Edition,
|
||||
Position,
|
||||
Role,
|
||||
Shift,
|
||||
Volunteer,
|
||||
)
|
||||
from app.validation.engine import day_boundary, festival_day
|
||||
|
||||
router = APIRouter(prefix="/editions/{edition_id}/badges", tags=["badges"])
|
||||
|
||||
@@ -246,7 +250,7 @@ def set_size(
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Génération du PDF #
|
||||
# --------------------------------------------------------------------------- #
|
||||
AssignmentRow = tuple[Position, Shift, Volunteer | None] # (poste, créneau, référent)
|
||||
AssignmentRow = tuple[Position, Shift]
|
||||
|
||||
|
||||
def _role_label(volunteer_id: uuid.UUID, lead_volunteer_ids: set[uuid.UUID]) -> str:
|
||||
@@ -259,16 +263,29 @@ def _category_label(category) -> str:
|
||||
return t(f"position.category.{category.value.lower()}")
|
||||
|
||||
|
||||
def _position_line(
|
||||
position: Position, shift: Shift, referent: Volunteer | None, volunteer_id: uuid.UUID
|
||||
) -> str:
|
||||
"""Une ligne par créneau affecté : poste + horaire, et le référent du
|
||||
poste s'il y en a un — sauf si le bénévole badgé est lui-même ce
|
||||
référent (pas besoin de se l'indiquer à soi-même)."""
|
||||
line = f"{position.name} {shift.starts_at:%H:%M}–{shift.ends_at:%H:%M}"
|
||||
if referent is not None and referent.id != volunteer_id:
|
||||
line += f" ({t('badge.role.referent')} : {referent.first_name} {referent.last_name})"
|
||||
return line
|
||||
def _day_label(day: dt.date) -> str:
|
||||
"""En-tête d'un jour dans le tableau des créneaux : « Samedi 11 »."""
|
||||
return t("badge.table.day_label", weekday=t(f"badge.weekday.{day.weekday()}"), day=day.day)
|
||||
|
||||
|
||||
def _positions_table(assignments: list[AssignmentRow], boundary: dt.time) -> BadgeTable | None:
|
||||
"""Créneaux affectés regroupés par jour de festival (une soirée qui
|
||||
passe minuit reste sur le jour où elle a commencé — `festival_day`) :
|
||||
le jour en en-tête, puis une ligne horaire / poste par créneau, dans
|
||||
l'ordre chronologique. `None` sans aucun créneau (rien à dessiner)."""
|
||||
by_day: dict[dt.date, list[tuple[str, str]]] = defaultdict(list)
|
||||
for position, shift in assignments:
|
||||
by_day[festival_day(shift.starts_at, boundary)].append(
|
||||
(f"{shift.starts_at:%H:%M}–{shift.ends_at:%H:%M}", position.name)
|
||||
)
|
||||
if not by_day:
|
||||
return None
|
||||
return BadgeTable(
|
||||
sections=tuple(
|
||||
BadgeTableSection(title=_day_label(day), rows=tuple(rows))
|
||||
for day, rows in sorted(by_day.items())
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _field_text(
|
||||
@@ -277,6 +294,8 @@ def _field_text(
|
||||
assignments_by_volunteer: dict[uuid.UUID, list[AssignmentRow]],
|
||||
lead_volunteer_ids: set[uuid.UUID],
|
||||
) -> str:
|
||||
"""Texte d'un champ — `POSITIONS` n'en a pas : c'est un tableau
|
||||
(`_positions_table`)."""
|
||||
if field.field_type == BadgeFieldType.FULL_NAME:
|
||||
return f"{volunteer.first_name} {volunteer.last_name}"
|
||||
if field.field_type == BadgeFieldType.FREE_TEXT:
|
||||
@@ -284,14 +303,9 @@ def _field_text(
|
||||
# le « Texte du badge » de sa fiche s'il en a un.
|
||||
return volunteer.badge_text or field.text or ""
|
||||
assignments = assignments_by_volunteer.get(volunteer.id, [])
|
||||
if field.field_type == BadgeFieldType.POSITIONS:
|
||||
return "\n".join(
|
||||
_position_line(position, shift, referent, volunteer.id)
|
||||
for position, shift, referent in assignments
|
||||
)
|
||||
if field.field_type == BadgeFieldType.CATEGORY:
|
||||
categories = sorted(
|
||||
{_category_label(position.category) for position, _shift, _r in assignments}
|
||||
{_category_label(position.category) for position, _shift in assignments}
|
||||
)
|
||||
return ", ".join(categories)
|
||||
if field.field_type == BadgeFieldType.ROLE:
|
||||
@@ -319,20 +333,20 @@ def generate_badges(
|
||||
raise HTTPException(status_code=422, detail={"code": "badge.no_volunteers"})
|
||||
|
||||
volunteer_ids = [v.id for v in volunteers]
|
||||
Referent = aliased(Volunteer)
|
||||
rows = session.exec(
|
||||
select(Assignment.volunteer_id, Position, Shift, Referent)
|
||||
select(Assignment.volunteer_id, Position, Shift)
|
||||
.join(Shift, Shift.id == Assignment.shift_id)
|
||||
.join(Position, Position.id == Shift.position_id)
|
||||
.join(Account, Account.id == Position.lead_account_id, isouter=True)
|
||||
.join(Referent, Referent.id == Account.volunteer_id, isouter=True)
|
||||
.where(Assignment.volunteer_id.in_(volunteer_ids))
|
||||
.order_by(Shift.starts_at)
|
||||
).all()
|
||||
assignments_by_volunteer: dict[uuid.UUID, list[AssignmentRow]] = defaultdict(list)
|
||||
for volunteer_id, position, shift, referent in rows:
|
||||
assignments_by_volunteer[volunteer_id].append((position, shift, referent))
|
||||
for volunteer_id, position, shift in rows:
|
||||
assignments_by_volunteer[volunteer_id].append((position, shift))
|
||||
|
||||
boundary = day_boundary(
|
||||
session.exec(select(DayPart).where(DayPart.edition_id == edition.id))
|
||||
)
|
||||
lead_volunteer_ids = {
|
||||
account.volunteer_id
|
||||
for account in session.exec(
|
||||
@@ -364,6 +378,11 @@ def generate_badges(
|
||||
image=volunteer.photo
|
||||
if field.field_type == BadgeFieldType.PHOTO
|
||||
else None,
|
||||
table=_positions_table(
|
||||
assignments_by_volunteer.get(volunteer.id, []), boundary
|
||||
)
|
||||
if field.field_type == BadgeFieldType.POSITIONS
|
||||
else None,
|
||||
)
|
||||
for field in fields
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user