Refonte du créateur de badges et nombre de badges par bénévole
ci-cd / build-backend (push) Successful in 30s
ci-cd / deploy (push) Successful in 13s
ci-cd / build-frontend (push) Successful in 22s

Créateur de badges refait d'après la maquette : champs sur le recto et le
verso (BadgeField.face, verso placé en miroir dans le PDF pour le
recto-verso), fond de zone, réduction automatique du texte, photo ovale,
initiales sans photo, nom sur deux lignes (migration 0026) ; éditeur en
trois colonnes (champs, aperçu recto/verso avec de vrais bénévoles,
inspecteur), formats prédéfinis, annuler, flèches, zoom, repères de
découpe, dépôt d'image sur l'aperçu, contrôle de tous les badges avant
génération.

Nombre de badges par bénévole (Volunteer.badge_count, 1 par défaut,
migration 0027) : champ sur la fiche, autant d'exemplaires sur les
planches, total affiché à la génération, colonne dans l'export Excel.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
AdamThomas
2026-10-27 19:20:42 +01:00
co-authored by Claude Opus 5.5
parent 1a23881e81
commit ced0855d47
28 changed files with 3738 additions and 803 deletions
+151
View File
@@ -247,3 +247,154 @@ def test_generating_with_photo_and_free_text_fields(client, edition, session):
resp = client.post(f"{_badges_url(edition)}/generate", json={})
assert resp.status_code == 200
assert resp.content.startswith(b"%PDF")
def _field(**kw):
return {"x_mm": 0, "y_mm": 0, "width_mm": 10, "height_mm": 10, **kw}
def test_new_field_attributes_default_and_round_trip(client, edition):
resp = client.put(
f"{_badges_url(edition)}/template/fields",
json={
"fields": [
_field(field_type="FULL_NAME"),
_field(
field_type="PHOTO", face="BACK", photo_shape="OVAL",
bg_color="#FFD23F", auto_fit=True,
),
]
},
)
assert resp.status_code == 200
by_face = {f["face"]: f for f in resp.json()}
front, back = by_face["FRONT"], by_face["BACK"]
assert front["field_type"] == "FULL_NAME"
assert front["bg_color"] is None
assert front["auto_fit"] is False
assert front["photo_shape"] == "RECT"
assert back["field_type"] == "PHOTO"
assert back["photo_shape"] == "OVAL"
assert back["bg_color"] == "#FFD23F"
assert back["auto_fit"] is True
again = client.get(f"{_badges_url(edition)}/template").json()["fields"]
assert {(f["face"], f["field_type"]) for f in again} == {
("FRONT", "FULL_NAME"), ("BACK", "PHOTO"),
}
def test_same_field_type_on_front_and_back_is_allowed(client, edition):
resp = client.put(
f"{_badges_url(edition)}/template/fields",
json={
"fields": [
_field(field_type="FREE_TEXT", face="FRONT", text="Recto"),
_field(field_type="FREE_TEXT", face="BACK", text="Verso"),
]
},
)
assert resp.status_code == 200
assert {(f["face"], f["text"]) for f in resp.json()} == {
("FRONT", "Recto"), ("BACK", "Verso"),
}
def test_same_field_type_twice_on_the_back_is_rejected(client, edition):
resp = client.put(
f"{_badges_url(edition)}/template/fields",
json={
"fields": [
_field(field_type="ROLE", face="BACK"),
_field(field_type="ROLE", face="BACK"),
]
},
)
assert resp.status_code == 422
assert resp.json()["detail"]["code"] == "badge.duplicate_field_type"
@pytest.mark.parametrize("color", ["red", "#FFF", "#GGGGGG", "FFD23F", "#FFD23F0"])
def test_invalid_background_color_is_rejected(client, edition, color):
resp = client.put(
f"{_badges_url(edition)}/template/fields",
json={"fields": [_field(field_type="ROLE", bg_color=color)]},
)
assert resp.status_code == 422
assert resp.json()["detail"]["code"] == "badge.invalid_color"
def test_generating_with_back_fields(client, edition, session, monkeypatch):
import app.api.badges as badges_api
captured = {}
original = badges_api.generate_badge_sheets_pdf
def spy(badges, *args, **kwargs):
captured["badges"] = badges
return original(badges, *args, **kwargs)
monkeypatch.setattr(badges_api, "generate_badge_sheets_pdf", spy)
resp = client.put(
f"{_badges_url(edition)}/template/fields",
json={
"fields": [
_field(field_type="FULL_NAME", width_mm=60, font_size=12),
_field(field_type="ROLE", face="BACK", bg_color="#FFD23F", auto_fit=True),
_field(field_type="PHOTO", face="BACK", photo_shape="OVAL"),
]
},
)
assert resp.status_code == 200
make_volunteer(session, edition, first_name="Alex", last_name="Martin")
resp = client.post(f"{_badges_url(edition)}/generate", json={})
assert resp.status_code == 200
assert resp.content.startswith(b"%PDF")
(badge,) = captured["badges"]
assert [f.text for f in badge.fields] == ["Alex\nMartin"]
back = {f.is_photo: f for f in badge.back_fields}
assert back[False].text == "Bénévole"
assert back[False].valign == "MIDDLE"
assert back[False].bg_color == "#FFD23F"
assert back[False].auto_fit is True
assert back[True].photo_shape == "OVAL"
assert back[True].text == "AM" # initiales, faute de photo
def test_generating_prints_as_many_badges_as_requested(client, edition, session, monkeypatch):
import app.api.badges as badges_api
captured = {}
original = badges_api.generate_badge_sheets_pdf
def spy(badges, *args, **kwargs):
captured["badges"] = badges
return original(badges, *args, **kwargs)
monkeypatch.setattr(badges_api, "generate_badge_sheets_pdf", spy)
client.put(
f"{_badges_url(edition)}/template/fields",
json={"fields": [_field(field_type="FULL_NAME", width_mm=60, font_size=12)]},
)
many = make_volunteer(session, edition, first_name="Alex", last_name="Martin")
make_volunteer(session, edition, first_name="Sam", last_name="Nuit")
resp = client.patch(f"/editions/{edition.id}/volunteers/{many.id}", json={"badge_count": 3})
assert resp.status_code == 200
assert resp.json()["badge_count"] == 3
resp = client.post(f"{_badges_url(edition)}/generate", json={})
assert resp.status_code == 200
names = [badge.fields[0].text for badge in captured["badges"]]
assert names == ["Alex\nMartin"] * 3 + ["Sam\nNuit"]
@pytest.mark.parametrize("count", [0, -1, 101])
def test_badge_count_out_of_range_is_rejected(client, edition, session, count):
volunteer = make_volunteer(session, edition)
resp = client.patch(
f"/editions/{edition.id}/volunteers/{volunteer.id}", json={"badge_count": count}
)
assert resp.status_code == 422