Fix the referee PDF's card background, make locations searchable, and add game end times to the API

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>
This commit is contained in:
2026-08-10 23:44:11 +02:00
parent 5dcffefb28
commit e737de9140
11 changed files with 222 additions and 24 deletions

View File

@@ -11,11 +11,54 @@ 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