"""Configuration et génération des planches de badges (API).""" import base64 import pytest from app.models import Role from tests.factories import dt_at, make_position, make_shift, make_volunteer # PNG 1×1 valide (transparent), le plus petit possible. _PNG = base64.b64decode( "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY" "42YAAAAASUVORK5CYII=" ) def _badges_url(edition): return f"/editions/{edition.id}/badges" def _set_full_name_field(client, edition): return client.put( f"{_badges_url(edition)}/template/fields", json={ "fields": [ { "field_type": "FULL_NAME", "x_mm": 5, "y_mm": 5, "width_mm": 60, "height_mm": 10, "font_size": 12, } ] }, ) def test_template_is_coordinator_only(edition, make_account, as_account): lead = make_account(Role.LEAD, email="lead@e.fr") client = as_account(lead) assert client.get(f"{_badges_url(edition)}/template").status_code == 403 resp = client.put(f"{_badges_url(edition)}/template/fields", json={"fields": []}) assert resp.status_code == 403 def test_template_starts_empty(client, edition): resp = client.get(f"{_badges_url(edition)}/template") assert resp.status_code == 200 body = resp.json() assert body["has_front_image"] is False assert body["has_back_image"] is False assert body["fields"] == [] # Format CR80 par défaut tant qu'aucune image n'a été importée. assert body["badge_width_mm"] == 85.6 assert body["badge_height_mm"] == 54.0 def test_uploading_front_image_derives_the_badge_format_from_its_pixels(client, edition): resp = client.put( f"{_badges_url(edition)}/template/front-image", files={"file": ("front.png", _PNG, "image/png")}, ) body = resp.json() # PNG 1×1 px, résolution supposée 300 dpi ⇒ 25,4/300 mm de côté. assert body["badge_width_mm"] == pytest.approx(25.4 / 300) assert body["badge_height_mm"] == pytest.approx(25.4 / 300) def test_uploading_front_and_back_images(client, edition): front = client.put( f"{_badges_url(edition)}/template/front-image", files={"file": ("front.png", _PNG, "image/png")}, ) assert front.status_code == 200 assert front.json()["has_front_image"] is True back = client.put( f"{_badges_url(edition)}/template/back-image", files={"file": ("back.png", _PNG, "image/png")}, ) assert back.status_code == 200 assert back.json()["has_back_image"] is True served = client.get(f"{_badges_url(edition)}/template/front-image") assert served.status_code == 200 assert served.headers["content-type"] == "image/png" assert served.content == _PNG def test_uploading_an_unsupported_image_type_is_rejected(client, edition): resp = client.put( f"{_badges_url(edition)}/template/front-image", files={"file": ("front.gif", b"not-a-real-gif", "image/gif")}, ) assert resp.status_code == 422 assert resp.json()["detail"]["code"] == "badge.image_type_not_supported" def test_resizing_the_badge_scales_it_independently_of_the_image(client, edition): client.put( f"{_badges_url(edition)}/template/front-image", files={"file": ("front.png", _PNG, "image/png")}, ) # Image 1×1 px à 300 dpi supposés ⇒ badge minuscule (~0,08 mm) : on # l'agrandit à un format exploitable, en conservant le ratio (ici 1:1). resp = client.put( f"{_badges_url(edition)}/template/size", json={"badge_width_mm": 90, "badge_height_mm": 90}, ) assert resp.status_code == 200 body = resp.json() assert body["badge_width_mm"] == 90 assert body["badge_height_mm"] == 90 # Persisté : une relecture du gabarit renvoie toujours la taille agrandie. again = client.get(f"{_badges_url(edition)}/template").json() assert again["badge_width_mm"] == 90 assert again["badge_height_mm"] == 90 def test_resizing_the_badge_to_a_non_positive_size_is_rejected(client, edition): resp = client.put( f"{_badges_url(edition)}/template/size", json={"badge_width_mm": 0, "badge_height_mm": 54}, ) assert resp.status_code == 422 assert resp.json()["detail"]["code"] == "badge.invalid_size" def test_setting_the_same_field_type_twice_is_rejected(client, edition): resp = client.put( f"{_badges_url(edition)}/template/fields", json={ "fields": [ { "field_type": "FULL_NAME", "x_mm": 0, "y_mm": 0, "width_mm": 10, "height_mm": 10, }, { "field_type": "FULL_NAME", "x_mm": 5, "y_mm": 5, "width_mm": 10, "height_mm": 10, }, ] }, ) assert resp.status_code == 422 assert resp.json()["detail"]["code"] == "badge.duplicate_field_type" def test_generating_without_any_field_configured_is_rejected(client, edition, session): make_volunteer(session, edition, first_name="Alex", last_name="Martin") resp = client.post(f"{_badges_url(edition)}/generate", json={}) assert resp.status_code == 422 assert resp.json()["detail"]["code"] == "badge.no_fields" def test_generating_with_no_matching_volunteers_is_rejected(client, edition, session): _set_full_name_field(client, edition) make_volunteer(session, edition, first_name="Alex", last_name="Martin") resp = client.post( f"{_badges_url(edition)}/generate", json={"volunteer_ids": ["00000000-0000-0000-0000-000000000000"]}, ) assert resp.status_code == 422 assert resp.json()["detail"]["code"] == "badge.no_volunteers" def test_generating_produces_a_pdf(client, edition, session): from app.models import Assignment resp = client.put( f"{_badges_url(edition)}/template/fields", json={ "fields": [ { "field_type": "FULL_NAME", "x_mm": 5, "y_mm": 5, "width_mm": 60, "height_mm": 10, "font_size": 12, }, { "field_type": "POSITIONS", "x_mm": 5, "y_mm": 20, "width_mm": 60, "height_mm": 10, "font_size": 9, }, ] }, ) assert resp.status_code == 200 client.put( f"{_badges_url(edition)}/template/front-image", files={"file": ("front.png", _PNG, "image/png")}, ) client.put( f"{_badges_url(edition)}/template/back-image", files={"file": ("back.png", _PNG, "image/png")}, ) volunteer = make_volunteer(session, edition, first_name="Alex", last_name="Martin") position = make_position(session, edition, name="Bar central") shift = make_shift(session, edition, position, start=dt_at(9, 18), end=dt_at(9, 22)) session.add(Assignment(edition_id=edition.id, volunteer_id=volunteer.id, shift_id=shift.id)) session.commit() resp = client.post( f"{_badges_url(edition)}/generate", json={"volunteer_ids": [str(volunteer.id)]}, ) assert resp.status_code == 200 assert resp.headers["content-type"] == "application/pdf" assert resp.content.startswith(b"%PDF") def test_generating_with_photo_and_free_text_fields(client, edition, session): resp = client.put( f"{_badges_url(edition)}/template/fields", json={ "fields": [ { "field_type": "PHOTO", "x_mm": 60, "y_mm": 5, "width_mm": 20, "height_mm": 25, "font_size": 8, }, { "field_type": "FREE_TEXT", "x_mm": 5, "y_mm": 40, "width_mm": 50, "height_mm": 8, "font_size": 9, }, ] }, ) assert resp.status_code == 200 assert {f["field_type"] for f in resp.json()} == {"PHOTO", "FREE_TEXT"} 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(with_photo) session.commit() resp = client.patch( f"/editions/{edition.id}/volunteers/{without_photo.id}", json={"badge_text": "Accès backstage"}, ) assert resp.status_code == 200 assert resp.json()["badge_text"] == "Accès backstage" resp = client.post(f"{_badges_url(edition)}/generate", json={}) assert resp.status_code == 200 assert resp.content.startswith(b"%PDF")