Étape 62 : les fiches de poste intégraient la photo d'origine de chaque bénévole pour chaque créneau (CPU à 300 %, puis erreur) — photos réduites à la vignette une fois par PDF ; badges : photo recadrée une fois par génération et décodée à taille réduite. Étape 61 : sans photo, le cadre de la photo reste vide sur le badge. Étape 60 : vue par bénévole en tranches de 30 min, affectations étirées sur toute leur durée, postes simultanés empilés, ligne de l'heure actuelle. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
177 lines
5.7 KiB
Python
177 lines
5.7 KiB
Python
"""Génération des feuilles de poste (module pur, sans base)."""
|
||
|
||
import base64
|
||
import datetime as dt
|
||
import io
|
||
|
||
from app.sheets.pdf import (
|
||
PositionDaySheet,
|
||
SheetArtist,
|
||
SheetShift,
|
||
SheetVolunteer,
|
||
generate_position_sheets_pdf,
|
||
)
|
||
|
||
# PNG 1×1 valide (transparent), le plus petit possible.
|
||
_PNG = base64.b64decode(
|
||
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY"
|
||
"42YAAAAASUVORK5CYII="
|
||
)
|
||
|
||
|
||
def _sheet(volunteers=(), min_staff=1, ideal_staff=2) -> PositionDaySheet:
|
||
return PositionDaySheet(
|
||
position_name="Bar central",
|
||
position_location="Chapiteau",
|
||
date=dt.date(2026, 7, 10),
|
||
shifts=(
|
||
SheetShift(
|
||
starts_at=dt.datetime(2026, 7, 10, 19, 0),
|
||
ends_at=dt.datetime(2026, 7, 10, 22, 0),
|
||
min_staff=min_staff,
|
||
ideal_staff=ideal_staff,
|
||
volunteers=volunteers,
|
||
),
|
||
),
|
||
)
|
||
|
||
|
||
def test_shift_is_understaffed_when_below_min_staff():
|
||
fully_staffed = _sheet(
|
||
volunteers=(SheetVolunteer(name="Alex Martin", phone="0600000000"),),
|
||
min_staff=1,
|
||
)
|
||
assert fully_staffed.shifts[0].understaffed is False
|
||
|
||
understaffed = _sheet(volunteers=(), min_staff=1)
|
||
assert understaffed.shifts[0].understaffed is True
|
||
|
||
|
||
def test_generates_a_valid_pdf_with_volunteers():
|
||
sheet = _sheet(
|
||
volunteers=(
|
||
SheetVolunteer(name="Alex Martin", phone="0600000000"),
|
||
SheetVolunteer(name="Sam Nuit", phone=None),
|
||
)
|
||
)
|
||
pdf = generate_position_sheets_pdf([sheet])
|
||
assert pdf.startswith(b"%PDF")
|
||
assert pdf.rstrip().endswith(b"%%EOF")
|
||
|
||
|
||
def test_generates_a_valid_pdf_with_no_volunteers_assigned():
|
||
pdf = generate_position_sheets_pdf([_sheet(volunteers=())])
|
||
assert pdf.startswith(b"%PDF")
|
||
|
||
|
||
def test_generates_a_valid_pdf_with_no_shifts_that_day():
|
||
sheet = PositionDaySheet(
|
||
position_name="Bar central", position_location=None,
|
||
date=dt.date(2026, 7, 10), shifts=(),
|
||
)
|
||
pdf = generate_position_sheets_pdf([sheet])
|
||
assert pdf.startswith(b"%PDF")
|
||
|
||
|
||
def test_multiple_sheets_produce_a_larger_pdf():
|
||
one = generate_position_sheets_pdf([_sheet()])
|
||
two = generate_position_sheets_pdf([_sheet(), _sheet()])
|
||
assert len(two) > len(one)
|
||
|
||
|
||
def test_generates_a_valid_pdf_with_a_referent():
|
||
sheet = PositionDaySheet(
|
||
position_name="Bar central", position_location="Chapiteau",
|
||
date=dt.date(2026, 7, 10), shifts=(),
|
||
referent_name="Sam Nuit", referent_phone="0600000000",
|
||
)
|
||
pdf = generate_position_sheets_pdf([sheet])
|
||
assert pdf.startswith(b"%PDF")
|
||
|
||
|
||
def test_generates_a_valid_pdf_without_a_referent():
|
||
sheet = PositionDaySheet(
|
||
position_name="Bar central", position_location="Chapiteau",
|
||
date=dt.date(2026, 7, 10), shifts=(),
|
||
)
|
||
assert sheet.referent_name is None
|
||
pdf = generate_position_sheets_pdf([sheet])
|
||
assert pdf.startswith(b"%PDF")
|
||
|
||
|
||
def test_generates_a_valid_pdf_with_volunteer_photos():
|
||
sheet = _sheet(
|
||
volunteers=(
|
||
SheetVolunteer(name="Alex Martin", phone="0600000000", photo=_PNG),
|
||
SheetVolunteer(name="Sam Nuit", phone=None, photo=None),
|
||
)
|
||
)
|
||
assert sheet.shifts[0].volunteers[0].photo == _PNG
|
||
assert sheet.shifts[0].volunteers[1].photo is None
|
||
pdf = generate_position_sheets_pdf([sheet])
|
||
assert pdf.startswith(b"%PDF")
|
||
|
||
|
||
def test_a_volunteer_photo_makes_the_pdf_larger():
|
||
without_photo = generate_position_sheets_pdf(
|
||
[_sheet(volunteers=(SheetVolunteer(name="Alex Martin", phone=None, photo=None),))]
|
||
)
|
||
with_photo = generate_position_sheets_pdf(
|
||
[_sheet(volunteers=(SheetVolunteer(name="Alex Martin", phone=None, photo=_PNG),))]
|
||
)
|
||
assert len(with_photo) > len(without_photo)
|
||
|
||
|
||
def test_generates_a_valid_pdf_with_the_day_lineup():
|
||
sheet = PositionDaySheet(
|
||
position_name="Bar central", position_location="Chapiteau",
|
||
date=dt.date(2026, 7, 10), shifts=(),
|
||
artists=(
|
||
SheetArtist(name="Chilla", start_time=dt.time(20, 0), end_time=dt.time(21, 0)),
|
||
SheetArtist(name="XYZ", start_time=dt.time(21, 30), end_time=dt.time(23, 0)),
|
||
),
|
||
)
|
||
pdf = generate_position_sheets_pdf([sheet])
|
||
assert pdf.startswith(b"%PDF")
|
||
|
||
|
||
def test_generates_a_valid_pdf_without_a_lineup():
|
||
sheet = PositionDaySheet(
|
||
position_name="Bar central", position_location="Chapiteau",
|
||
date=dt.date(2026, 7, 10), shifts=(),
|
||
)
|
||
assert sheet.artists == ()
|
||
pdf = generate_position_sheets_pdf([sheet])
|
||
assert pdf.startswith(b"%PDF")
|
||
|
||
|
||
def _phone_photo(seed: int) -> bytes:
|
||
"""Photo de téléphone : 3000 × 4000 px, plusieurs Mo."""
|
||
from PIL import Image as PILImage
|
||
|
||
img = PILImage.new("RGB", (3000, 4000), (seed * 40 % 255, 120, 60))
|
||
img.paste(PILImage.effect_noise((800, 800), 80).convert("RGB"), (seed * 100, 200))
|
||
out = io.BytesIO()
|
||
img.save(out, format="JPEG", quality=92)
|
||
return out.getvalue()
|
||
|
||
|
||
def test_large_photos_are_shrunk_to_the_thumbnail():
|
||
# Régression : la photo d'origine était intégrée telle quelle pour
|
||
# chaque bénévole de chaque créneau — génération interminable, PDF
|
||
# énorme, puis erreur.
|
||
photos = [_phone_photo(i) for i in range(5)]
|
||
volunteers = tuple(
|
||
SheetVolunteer(name=f"Bénévole {i}", phone=None, photo=p) for i, p in enumerate(photos)
|
||
)
|
||
pdf = generate_position_sheets_pdf([_sheet(volunteers=volunteers)])
|
||
assert sum(len(p) for p in photos) > 2_000_000
|
||
assert len(pdf) < 200_000
|
||
|
||
|
||
def test_unreadable_photo_does_not_break_the_sheet():
|
||
pdf = generate_position_sheets_pdf(
|
||
[_sheet(volunteers=(SheetVolunteer(name="Alex", phone=None, photo=b"pas une image"),))]
|
||
)
|
||
assert pdf.startswith(b"%PDF")
|