Feuilles de poste compactées : une page par poste et par jour
ci-cd / build-backend (push) Failing after 1m2s
ci-cd / build-frontend (push) Skipped
ci-cd / deploy (push) Skipped

Bénévoles deux par ligne (vignette 10 mm, nom et téléphone superposés),
titres, en-têtes de créneau et marges resserrés — 6 feuilles réalistes :
11 pages ⇒ 6.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
AdamThomas
2026-10-28 19:22:46 +01:00
co-authored by Claude Opus 5.5
parent 749c7e0917
commit 5c2cc0a6c3
3 changed files with 97 additions and 33 deletions
+66 -30
View File
@@ -30,9 +30,12 @@ from reportlab.platypus import (
TableStyle,
)
_PHOTO_BOX_MM = 14 * mm
# Côté de la vignette en pixels : 14 mm à 300 dpi.
_PHOTO_BOX_PX = round(14 / 25.4 * 300)
# Mise en page compacte (une feuille = une page autant que possible) :
# vignette de 10 mm, deux bénévoles par ligne.
_PHOTO_BOX_MM = 10 * mm
# Côté de la vignette en pixels : 10 mm à 300 dpi.
_PHOTO_BOX_PX = round(10 / 25.4 * 300)
_PER_ROW = 2
_WEEKDAYS_FR = ["Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi", "Dimanche"]
_MONTHS_FR = [
@@ -94,26 +97,36 @@ def _styles() -> dict[str, ParagraphStyle]:
base = getSampleStyleSheet()
return {
"title": ParagraphStyle(
"SheetTitle", parent=base["Title"], alignment=0, fontSize=22, spaceAfter=2
"SheetTitle", parent=base["Title"], alignment=0,
fontSize=18, leading=21, spaceBefore=0, spaceAfter=1,
),
"subtitle": ParagraphStyle(
"SheetSubtitle", parent=base["Normal"],
fontSize=12, textColor=colors.grey, spaceAfter=4,
fontSize=10.5, leading=13, textColor=colors.grey, spaceAfter=2,
),
"referent": ParagraphStyle(
"SheetReferent", parent=base["Normal"],
fontSize=11.5, fontName="Helvetica-Bold", spaceAfter=6,
fontSize=10.5, leading=13, fontName="Helvetica-Bold", spaceAfter=3,
),
"artists": ParagraphStyle(
"SheetArtists", parent=base["Normal"],
fontSize=9.5, textColor=colors.HexColor("#4a4a4a"), spaceAfter=16,
fontSize=8.5, leading=10.5, textColor=colors.HexColor("#4a4a4a"), spaceAfter=4,
),
"shiftHeader": ParagraphStyle(
"ShiftHeader", parent=base["Heading3"], fontSize=13, spaceBefore=12, spaceAfter=4
"ShiftHeader", parent=base["Heading3"],
fontSize=11, leading=13, spaceBefore=7, spaceAfter=3,
),
"shiftHeaderWarning": ParagraphStyle(
"ShiftHeaderWarning", parent=base["Heading3"],
fontSize=13, spaceBefore=12, spaceAfter=4, textColor=colors.HexColor("#b23a1f"),
fontSize=11, leading=13, spaceBefore=7, spaceAfter=3,
textColor=colors.HexColor("#b23a1f"),
),
"volunteerName": ParagraphStyle(
"SheetVolunteerName", parent=base["Normal"],
fontName="Helvetica-Bold", fontSize=9.5, leading=11.5,
),
"volunteerPhone": ParagraphStyle(
"SheetVolunteerPhone", parent=base["Normal"], fontSize=9, leading=11,
),
"footerTitle": ParagraphStyle(
"SheetFooterTitle", parent=base["Normal"],
@@ -136,8 +149,9 @@ class SheetFooter:
message: str
_MARGIN_X = 18 * mm
_MARGIN_BOTTOM = 16 * mm
_MARGIN_X = 15 * mm
_MARGIN_TOP = 12 * mm
_MARGIN_BOTTOM = 12 * mm
# Écart entre le bas du contenu et le filet du pied de page.
_FOOTER_GAP = 5 * mm
@@ -186,25 +200,47 @@ def _photo_cell(photo: bytes | None, cache: dict[bytes, tuple[bytes, int, int] |
return Image(io.BytesIO(data), width=width_px * scale, height=height_px * scale)
def _shift_table(shift: SheetShift, cache: dict[bytes, tuple[bytes, int, int] | None]) -> Table:
rows = [["", "Bénévole", "Téléphone"]] + [
[_photo_cell(v.photo, cache), v.name, v.phone or "—"]
def _shift_table(
shift: SheetShift,
cache: dict[bytes, tuple[bytes, int, int] | None],
styles: dict[str, ParagraphStyle],
width: float,
) -> Table:
"""Bénévoles du créneau, deux par ligne : vignette, puis nom (gras) et
téléphone l'un sous l'autre. Deux fois moins de lignes qu'une liste à
une personne par ligne (avant l'étape 65, souvent deux pages par
feuille)."""
photo_w = _PHOTO_BOX_MM + 3 * mm
text_w = width / _PER_ROW - photo_w
cells = [
[
_photo_cell(v.photo, cache),
[
Paragraph(escape(v.name), styles["volunteerName"]),
Paragraph(escape(v.phone or "—"), styles["volunteerPhone"]),
],
]
for v in shift.volunteers
]
table = Table(rows, colWidths=[18 * mm, 92 * mm, 55 * mm])
table.setStyle(
TableStyle(
[
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
("FONTSIZE", (0, 0), (-1, -1), 10.5),
("TOPPADDING", (0, 0), (-1, -1), 5),
("BOTTOMPADDING", (0, 0), (-1, -1), 5),
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
("LINEBELOW", (0, 0), (-1, 0), 0.75, colors.black),
("GRID", (0, 1), (-1, -1), 0.25, colors.lightgrey),
]
)
)
rows = []
for i in range(0, len(cells), _PER_ROW):
chunk = cells[i : i + _PER_ROW]
chunk += [["", ""]] * (_PER_ROW - len(chunk))
rows.append([cell for pair in chunk for cell in pair])
table = Table(rows, colWidths=[photo_w, text_w] * _PER_ROW)
style = [
("TOPPADDING", (0, 0), (-1, -1), 2),
("BOTTOMPADDING", (0, 0), (-1, -1), 2),
("LEFTPADDING", (0, 0), (-1, -1), 3),
("RIGHTPADDING", (0, 0), (-1, -1), 3),
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
("LINEBELOW", (0, 0), (-1, -1), 0.25, colors.lightgrey),
("LINEABOVE", (0, 0), (-1, 0), 0.25, colors.lightgrey),
]
# Filet vertical entre les deux bénévoles d'une même ligne.
for col in range(2, 2 * _PER_ROW, 2):
style.append(("LINEBEFORE", (col, 0), (col, -1), 0.25, colors.lightgrey))
table.setStyle(TableStyle(style))
return table
@@ -221,7 +257,7 @@ def generate_position_sheets_pdf(
footer_height = sum(p.wrap(width, A4[1])[1] for p in paragraphs)
doc = SimpleDocTemplate(
buffer, pagesize=A4,
leftMargin=_MARGIN_X, rightMargin=_MARGIN_X, topMargin=16 * mm,
leftMargin=_MARGIN_X, rightMargin=_MARGIN_X, topMargin=_MARGIN_TOP,
bottomMargin=_MARGIN_BOTTOM + (footer_height + _FOOTER_GAP if paragraphs else 0),
)
@@ -279,7 +315,7 @@ def generate_position_sheets_pdf(
style = styles["shiftHeaderWarning"] if shift.understaffed else styles["shiftHeader"]
story.append(Paragraph(header, style))
if shift.volunteers:
story.append(_shift_table(shift, thumbnails))
story.append(_shift_table(shift, thumbnails, styles, width))
else:
story.append(Paragraph("Personne affecté pour l'instant.", styles["normal"]))
story.append(Spacer(1, 0))