"""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")