"""Feuilles de poste (PDF) — API.""" import base64 import datetime as dt from app.models import Assignment, Role from tests.factories import dt_at, make_artist, make_position, make_shift, make_volunteer # PNG 1×1 valide (transparent), le plus petit possible. _PNG = base64.b64decode( "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY" "42YAAAAASUVORK5CYII=" ) def _url(edition): return f"/editions/{edition.id}/position-sheets/generate" def test_is_coordinator_only(edition, make_account, as_account): lead = make_account(Role.LEAD, email="lead@e.fr") client = as_account(lead) resp = client.post(_url(edition), json={}) assert resp.status_code == 403 def test_rejects_when_no_position_matches(client, edition): resp = client.post( _url(edition), json={"position_ids": ["00000000-0000-0000-0000-000000000000"]}, ) assert resp.status_code == 422 assert resp.json()["detail"]["code"] == "position_sheets.no_positions" def test_rejects_when_no_real_need_to_print(client, edition, session): # Créneaux générés à 0/0, jamais remplis : rien à imprimer. make_position(session, edition, name="Bar central") resp = client.post(_url(edition), json={}) assert resp.status_code == 422 assert resp.json()["detail"]["code"] == "position_sheets.no_shifts" def test_generates_a_pdf_with_assigned_volunteers(client, edition, session): position = make_position(session, edition, name="Bar central") shift = make_shift( session, edition, position, start=dt_at(9, 19), end=dt_at(9, 22), min_staff=1, ideal_staff=2, ) volunteer = make_volunteer(session, edition, first_name="Alex", last_name="Martin") session.add( Assignment(edition_id=edition.id, volunteer_id=volunteer.id, shift_id=shift.id) ) session.commit() resp = client.post(_url(edition), json={}) assert resp.status_code == 200 assert resp.headers["content-type"] == "application/pdf" assert resp.content.startswith(b"%PDF") def test_generates_a_pdf_with_a_referent(client, edition, session, make_account): position = make_position(session, edition, name="Bar central") make_shift( session, edition, position, start=dt_at(9, 19), end=dt_at(9, 22), min_staff=1, ideal_staff=2, ) referent_volunteer = make_volunteer(session, edition, first_name="Sam", last_name="Nuit") referent_account = make_account( Role.LEAD, volunteer_id=referent_volunteer.id, email="referent@e.fr" ) client.patch( f"/editions/{edition.id}/positions/{position.id}", json={"lead_account_id": str(referent_account.id)}, ) resp = client.post(_url(edition), json={}) assert resp.status_code == 200 assert resp.content.startswith(b"%PDF") def test_includes_a_volunteer_photo_when_present(client, edition, session): position = make_position(session, edition, name="Bar central") shift = make_shift( session, edition, position, start=dt_at(9, 19), end=dt_at(9, 22), min_staff=1, ideal_staff=2, ) with_photo = make_volunteer(session, edition, first_name="Alex", last_name="Martin") with_photo.photo = _PNG with_photo.photo_content_type = "image/png" without_photo = make_volunteer(session, edition, first_name="Sam", last_name="Nuit") session.add_all( [ Assignment(edition_id=edition.id, volunteer_id=with_photo.id, shift_id=shift.id), Assignment(edition_id=edition.id, volunteer_id=without_photo.id, shift_id=shift.id), ] ) session.commit() resp = client.post(_url(edition), json={}) assert resp.status_code == 200 assert resp.content.startswith(b"%PDF") def test_includes_the_day_lineup_across_all_positions(client, edition, session): bar = make_position(session, edition, name="Bar central") welcome = make_position(session, edition, name="Accueil") make_shift( session, edition, bar, start=dt_at(9, 19), end=dt_at(9, 22), min_staff=1, ideal_staff=2, ) make_shift( session, edition, welcome, start=dt_at(9, 9), end=dt_at(9, 13), min_staff=1, ideal_staff=2, ) # Passage le 9 (jour des deux créneaux) : doit apparaître sur les deux # feuilles, pas seulement celle du poste « Régie ». make_artist( session, edition, name="Chilla", performance_date=dt.date(2026, 7, 9), start_time=dt.time(20, 0), end_time=dt.time(21, 0), ) # Passage un autre jour : ne doit apparaître sur aucune des deux. make_artist( session, edition, name="Hors sujet", performance_date=dt.date(2026, 7, 11), start_time=dt.time(20, 0), end_time=dt.time(21, 0), ) resp = client.post(_url(edition), json={}) assert resp.status_code == 200 assert resp.content.startswith(b"%PDF") def test_position_ids_filter_scopes_the_sheets(client, edition, session): bar = make_position(session, edition, name="Bar central") welcome = make_position(session, edition, name="Accueil") make_shift( session, edition, bar, start=dt_at(9, 19), end=dt_at(9, 22), min_staff=1, ideal_staff=2, ) make_shift( session, edition, welcome, start=dt_at(9, 9), end=dt_at(9, 13), min_staff=1, ideal_staff=2, ) resp = client.post(_url(edition), json={"position_ids": [str(bar.id)]}) assert resp.status_code == 200 resp = client.post(_url(edition), json={"position_ids": [str(welcome.id)]}) assert resp.status_code == 200 def test_a_night_that_passes_midnight_stays_on_its_festival_day(client, edition, session): # Début de journée = 06:00 (premier créneau type de la fixture) : 00h–02h # le 10 appartient encore à la soirée du 9 — une seule page, pas deux. from io import BytesIO from pypdf import PdfReader position = make_position(session, edition, name="Bar de nuit") for start, end in [ (dt_at(9, 22), dt_at(9, 23)), (dt_at(9, 23), dt_at(10, 0)), (dt_at(10, 0), dt_at(10, 1)), (dt_at(10, 1), dt_at(10, 2)), ]: make_shift(session, edition, position, start=start, end=end) resp = client.post(_url(edition), json={}) assert resp.status_code == 200 reader = PdfReader(BytesIO(resp.content)) assert len(reader.pages) == 1 text = reader.pages[0].extract_text() assert "jeudi 9 juillet" in text.lower() assert "01:00" in text