Referee PDF: the info-card background used CSS color-mix(), which WeasyPrint doesn't support -- the rule was silently dropped, leaving the card with no background at all. Computed in Python instead (management/pdf.py) and baked into the template as a plain hex value; the tint is based on the club's primary_color, falling back to secondary_color when primary is itself (near) black or white, where a straight tint would be invisible or too harsh. Event forms: the location picker now shows "Name — City" (plus the country when it isn't Belgium) and is searchable by name or city, reusing the existing single-select searchable-select.js widget. Games API: GameOut now carries `end` (explicit, or start + 2h when a GAME was saved without one -- Event.save() sets this, never overwriting an explicit end; other event kinds are untouched). /games/upcoming/ now includes anything not yet finished rather than only things that haven't started, so a game already in progress keeps showing up until its window closes; `status` was adjusted to match so a game returned there never calls itself "finished". Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
79 lines
3.6 KiB
Python
79 lines
3.6 KiB
Python
"""HTML-to-PDF rendering for exports (currently just the memberships list).
|
|
|
|
Same lazy-import shape as billing.services.invoices.render_pdf -- WeasyPrint binds
|
|
to native pango/cairo libraries, and a machine without them must still be able to
|
|
run the app, the tests and every other page, so the import happens here, not at
|
|
module scope, and only fails when someone actually asks for a PDF. Kept separate
|
|
from billing's version rather than shared: the two exports have nothing else in
|
|
common, and sharing would make one app depend on the other's error type for no
|
|
real benefit.
|
|
"""
|
|
|
|
from django.template.loader import render_to_string
|
|
|
|
#: Referee form defaults, used whenever a club hasn't set its own colours (see
|
|
#: club.models.Club.primary_color/secondary_color) -- picked to match the
|
|
#: app's own daisyUI theme accent/secondary so an unbranded club's form still
|
|
#: looks intentional rather than grey.
|
|
DEFAULT_ACCENT_COLOR = "#3730a3"
|
|
DEFAULT_SECONDARY_COLOR = "#be185d"
|
|
|
|
|
|
class PDFExportError(Exception):
|
|
"""Raised when WeasyPrint's native libraries aren't available."""
|
|
|
|
|
|
def _is_near_black_or_white(hex_color: str, threshold: float = 0.12) -> bool:
|
|
"""Whether a #rrggbb colour reads as (near-)black or (near-)white -- a rough
|
|
perceived-lightness check (plain channel average, not WCAG luminance: this
|
|
doesn't need to be contrast-accurate, just good enough to say "too close to
|
|
black or white to make a nice pale card background")."""
|
|
red, green, blue = (int(hex_color[index : index + 2], 16) for index in (1, 3, 5))
|
|
lightness = (red + green + blue) / (3 * 255)
|
|
return lightness < threshold or lightness > 1 - threshold
|
|
|
|
|
|
def _tint_with_white(hex_color: str, strength: float = 0.14) -> str:
|
|
"""A pale, card-background-friendly tint of `hex_color` -- mixed `strength`
|
|
of the colour into white. Computed here rather than left to CSS
|
|
`color-mix()`: WeasyPrint doesn't support that function, so the rule was
|
|
silently dropped and the card rendered with no background at all."""
|
|
channels = (int(hex_color[index : index + 2], 16) for index in (1, 3, 5))
|
|
mixed = (round(channel * strength + 255 * (1 - strength)) for channel in channels)
|
|
return "#{:02x}{:02x}{:02x}".format(*mixed)
|
|
|
|
|
|
def referee_form_colors(club) -> dict:
|
|
"""Accent colour (the club's own primary_color, or the app default) and a
|
|
pale info-card background tint for the referee payment form.
|
|
|
|
The card is tinted off the *secondary* colour instead when the primary
|
|
colour is itself (near) black or white -- using it straight would make an
|
|
all-but-invisible near-white-on-white or a harsh near-black card, so the
|
|
secondary colour (meant for exactly this kind of highlight -- see
|
|
Club.secondary_color's help text) stands in instead.
|
|
"""
|
|
accent = club.primary_color or DEFAULT_ACCENT_COLOR
|
|
secondary = club.secondary_color or DEFAULT_SECONDARY_COLOR
|
|
tint_source = secondary if _is_near_black_or_white(accent) else accent
|
|
return {"accent_color": accent, "info_card_color": _tint_with_white(tint_source)}
|
|
|
|
|
|
def render_pdf(html: str) -> bytes:
|
|
try:
|
|
from weasyprint import HTML
|
|
except (ImportError, OSError) as error:
|
|
raise PDFExportError("PDF rendering needs the native pango/cairo libraries (on macOS: brew install pango).") from error
|
|
|
|
return HTML(string=html).write_pdf()
|
|
|
|
|
|
def membership_list_pdf(context: dict) -> bytes:
|
|
html = render_to_string("management/membership_list_pdf.html", context)
|
|
return render_pdf(html)
|
|
|
|
|
|
def event_referee_form_pdf(context: dict) -> bytes:
|
|
html = render_to_string("management/event_referee_form_pdf.html", context)
|
|
return render_pdf(html)
|