Badges : ordre de superposition des champs
La photo recouvrait le nom et le texte libre (champs dessinés dans l'ordre alphabétique de leur type). BadgeField.z_order (migration 0028, champs existants renumérotés photo dessous) fixe l'ordre de dessin du PDF et de l'aperçu ; boutons « Premier plan / Avancer / Reculer / Arrière-plan » dans l'inspecteur. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
4f86398ccb
commit
1026d9b9e1
@@ -86,7 +86,14 @@ def _fields(session: Session, edition_id: uuid.UUID) -> list[BadgeField]:
|
||||
session.exec(
|
||||
select(BadgeField)
|
||||
.where(BadgeField.edition_id == edition_id)
|
||||
.order_by(BadgeField.face, BadgeField.field_type)
|
||||
# Ordre de dessin (voir `BadgeField.z_order`) ; à égalité, la
|
||||
# photo dessous, pour ne jamais masquer un texte.
|
||||
.order_by(
|
||||
BadgeField.face,
|
||||
BadgeField.z_order,
|
||||
(BadgeField.field_type == BadgeFieldType.PHOTO).desc(),
|
||||
BadgeField.field_type,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -369,6 +369,8 @@ class BadgeFieldIn(ApiModel):
|
||||
auto_fit: bool = False
|
||||
# `PHOTO` seulement, ignoré sinon.
|
||||
photo_shape: BadgePhotoShape = BadgePhotoShape.RECT
|
||||
# Ordre d'empilement sur la face (voir `BadgeField.z_order`).
|
||||
z_order: int = 0
|
||||
|
||||
|
||||
class BadgeFieldsSet(ApiModel):
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
"""Ordre d'empilement des champs du badge
|
||||
|
||||
Revision ID: 0028_badge_field_z_order
|
||||
Revises: 0027_volunteer_badge_count
|
||||
Create Date: 2026-10-27
|
||||
|
||||
`badge_field.z_order` : les champs d'une face sont dessinés du plus petit au
|
||||
plus grand. Avant, ils l'étaient dans l'ordre alphabétique de leur type, et
|
||||
la photo recouvrait le nom et le texte libre. Les champs existants sont
|
||||
numérotés photo d'abord (dessous), puis les autres par type.
|
||||
"""
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision: str = "0028_badge_field_z_order"
|
||||
down_revision: str | None = "0027_volunteer_badge_count"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column(
|
||||
"badge_field",
|
||||
sa.Column("z_order", sa.Integer(), nullable=False, server_default="0"),
|
||||
)
|
||||
op.execute(
|
||||
"""
|
||||
UPDATE badge_field AS b SET z_order = ranked.rank
|
||||
FROM (
|
||||
SELECT id, ROW_NUMBER() OVER (
|
||||
PARTITION BY edition_id, face
|
||||
ORDER BY (field_type = 'PHOTO') DESC, field_type
|
||||
) - 1 AS rank
|
||||
FROM badge_field
|
||||
) AS ranked
|
||||
WHERE b.id = ranked.id
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column("badge_field", "z_order")
|
||||
@@ -509,6 +509,11 @@ class BadgeField(TimestampMixin, table=True):
|
||||
server_default=BadgePhotoShape.RECT.value,
|
||||
),
|
||||
)
|
||||
# Ordre d'empilement sur sa face : les champs sont dessinés du plus petit
|
||||
# au plus grand, un champ recouvre donc ceux d'un `z_order` inférieur.
|
||||
z_order: int = Field(
|
||||
default=0, sa_column=Column(Integer, nullable=False, server_default="0")
|
||||
)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
@@ -398,3 +398,41 @@ def test_badge_count_out_of_range_is_rejected(client, edition, session, count):
|
||||
f"/editions/{edition.id}/volunteers/{volunteer.id}", json={"badge_count": count}
|
||||
)
|
||||
assert resp.status_code == 422
|
||||
|
||||
|
||||
def test_fields_are_drawn_in_stacking_order(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="FREE_TEXT", text="Staff", z_order=2),
|
||||
_field(field_type="FULL_NAME", z_order=0),
|
||||
_field(field_type="PHOTO", z_order=1),
|
||||
]
|
||||
},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert [f["field_type"] for f in resp.json()] == ["FULL_NAME", "PHOTO", "FREE_TEXT"]
|
||||
make_volunteer(session, edition, first_name="Alex", last_name="Martin")
|
||||
|
||||
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"]
|
||||
|
||||
|
||||
def test_photo_is_drawn_below_text_at_equal_stacking_order(client, edition):
|
||||
resp = client.put(
|
||||
f"{_badges_url(edition)}/template/fields",
|
||||
json={"fields": [_field(field_type="FULL_NAME"), _field(field_type="PHOTO")]},
|
||||
)
|
||||
assert [f["field_type"] for f in resp.json()] == ["PHOTO", "FULL_NAME"]
|
||||
|
||||
Reference in New Issue
Block a user