Fiches de poste rapides, badges sans initiales, vue par bénévole sur l'axe des temps
É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>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
c57c70569c
commit
c6ea7378c7
@@ -360,7 +360,7 @@ def test_generating_with_back_fields(client, edition, session, monkeypatch):
|
||||
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
|
||||
assert back[True].text == "" # pas de photo : rien d'imprimé
|
||||
|
||||
|
||||
def test_generating_prints_as_many_badges_as_requested(client, edition, session, monkeypatch):
|
||||
@@ -427,7 +427,11 @@ def test_fields_are_drawn_in_stacking_order(client, edition, session, monkeypatc
|
||||
|
||||
assert client.post(f"{_badges_url(edition)}/generate", json={}).status_code == 200
|
||||
(badge,) = captured["badges"]
|
||||
assert [f.text for f in badge.fields] == ["Alex\nMartin", "AM", "Staff"]
|
||||
assert [(f.is_photo, f.text) for f in badge.fields] == [
|
||||
(False, "Alex\nMartin"),
|
||||
(True, ""),
|
||||
(False, "Staff"),
|
||||
]
|
||||
|
||||
|
||||
def test_photo_is_drawn_below_text_at_equal_stacking_order(client, edition):
|
||||
|
||||
@@ -115,10 +115,11 @@ def test_field_text_full_name_is_first_name_then_last_name_on_two_lines():
|
||||
assert _field_text(field, volunteer, {}, set()) == "Alex\nMartin"
|
||||
|
||||
|
||||
def test_field_text_photo_is_the_initials_used_without_a_photo():
|
||||
def test_field_text_photo_is_empty():
|
||||
volunteer = _volunteer(first_name="alex", last_name="martin")
|
||||
field = BadgeField(
|
||||
edition_id=uuid.uuid4(), field_type=BadgeFieldType.PHOTO,
|
||||
x_mm=0, y_mm=0, width_mm=10, height_mm=10, font_size=9,
|
||||
)
|
||||
assert _field_text(field, volunteer, {}, set()) == "AM"
|
||||
# Une image, pas un texte : rien n'est imprimé sans photo.
|
||||
assert _field_text(field, volunteer, {}, set()) == ""
|
||||
|
||||
@@ -242,7 +242,7 @@ def test_table_rows_beyond_the_frame_are_not_drawn():
|
||||
# Éditeur v2 : verso, fond de zone, réduction automatique, photo #
|
||||
# --------------------------------------------------------------------------- #
|
||||
from app.badges import pdf as pdf_module # noqa: E402
|
||||
from app.badges.pdf import fit_text, initials # noqa: E402
|
||||
from app.badges.pdf import fit_text # noqa: E402
|
||||
|
||||
|
||||
def _text_field(**kw) -> BadgeFieldSpec:
|
||||
@@ -269,9 +269,9 @@ def test_back_page_draws_each_volunteer_back_fields_at_the_mirrored_slot(monkeyp
|
||||
calls = []
|
||||
original = pdf_module._draw_fields
|
||||
|
||||
def spy(c, fields, x0, y0, badge_height_mm):
|
||||
def spy(c, fields, x0, y0, badge_height_mm, photos):
|
||||
calls.append((fields, x0, y0))
|
||||
original(c, fields, x0, y0, badge_height_mm)
|
||||
original(c, fields, x0, y0, badge_height_mm, photos)
|
||||
|
||||
monkeypatch.setattr(pdf_module, "_draw_fields", spy)
|
||||
back = (_text_field(text="Verso Alex"),)
|
||||
@@ -376,30 +376,23 @@ def test_middle_aligned_text_is_lower_than_top_aligned_text(monkeypatch):
|
||||
assert middle < top
|
||||
|
||||
|
||||
def test_initials_are_the_uppercase_first_letters():
|
||||
assert initials("alex", "martin") == "AM"
|
||||
assert initials("Élodie", "de la Tour") == "ÉD"
|
||||
|
||||
|
||||
def test_missing_photo_prints_the_initials(monkeypatch):
|
||||
drawn = []
|
||||
monkeypatch.setattr(
|
||||
pdf_module.canvas.Canvas, "drawCentredString",
|
||||
lambda self, x, y, text, *a, **k: drawn.append(text),
|
||||
)
|
||||
for image in (None, b"pas une image"):
|
||||
badge = BadgeSpec(
|
||||
fields=(BadgeFieldSpec(**{**_photo_badge(image).fields[0].__dict__, "text": "AM"}),)
|
||||
def test_missing_photo_prints_nothing(monkeypatch):
|
||||
calls = []
|
||||
for name in ("drawImage", "drawPath", "drawCentredString", "drawString"):
|
||||
monkeypatch.setattr(
|
||||
pdf_module.canvas.Canvas, name,
|
||||
lambda self, *a, _name=name, **k: calls.append(_name),
|
||||
)
|
||||
generate_badge_sheets_pdf([badge], None, None)
|
||||
assert drawn == ["AM", "AM"]
|
||||
for image in (None, b"pas une image"):
|
||||
generate_badge_sheets_pdf([_photo_badge(image)], None, None)
|
||||
assert calls == []
|
||||
|
||||
|
||||
def test_oval_photo_and_oval_initials_generate_a_valid_pdf():
|
||||
def test_oval_photo_and_missing_oval_photo_generate_a_valid_pdf():
|
||||
for image in (_jpeg(400, 500), None):
|
||||
field = _photo_badge(image).fields[0]
|
||||
badge = BadgeSpec(
|
||||
fields=(BadgeFieldSpec(**{**field.__dict__, "photo_shape": "OVAL", "text": "AM"}),)
|
||||
fields=(BadgeFieldSpec(**{**field.__dict__, "photo_shape": "OVAL"}),)
|
||||
)
|
||||
pdf = generate_badge_sheets_pdf([badge], None, None)
|
||||
assert pdf.startswith(b"%PDF")
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import base64
|
||||
import datetime as dt
|
||||
import io
|
||||
|
||||
from app.sheets.pdf import (
|
||||
PositionDaySheet,
|
||||
@@ -142,3 +143,34 @@ def test_generates_a_valid_pdf_without_a_lineup():
|
||||
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")
|
||||
|
||||
Reference in New Issue
Block a user