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

@@ -16,7 +16,7 @@ from api.errors import require_club
from club.services.access import current_season
from teams.models import Team
from .models import Event
from .models import ASSUMED_EVENT_DURATION, Event
router = Router(tags=["games"])
@@ -49,6 +49,7 @@ class TeamRefOut(Schema):
class GameOut(Schema):
id: uuid.UUID
start: datetime
end: datetime
location: LocationOut | None
home_team: TeamRefOut | None
away_team: TeamRefOut | None
@@ -82,10 +83,19 @@ def _to_game_out(event, request, club, team=None) -> GameOut:
home_team, away_team = opponent_ref, team_ref
home_score, away_score = event.score_against, event.score_for
now = timezone.now()
effective_end = event.end or (event.start + ASSUMED_EVENT_DURATION)
if event.is_live:
status = "live"
elif event.start > timezone.now():
elif event.start > now:
status = "upcoming"
elif effective_end > now:
# Started, not manually flagged live, but our own assumed/explicit
# window says it isn't over yet -- matches list_upcoming_games'
# "not finished" inclusion below, so a game returned there never
# turns around and calls itself "finished".
status = "live"
else:
status = "finished"
@@ -96,6 +106,7 @@ def _to_game_out(event, request, club, team=None) -> GameOut:
return GameOut(
id=event.pk,
start=event.start,
end=effective_end,
location=location,
home_team=home_team,
away_team=away_team,
@@ -109,11 +120,20 @@ def _to_game_out(event, request, club, team=None) -> GameOut:
@router.get("/games/upcoming/", response=list[GameOut], summary="Upcoming games")
def list_upcoming_games(request, count: int = DEFAULT_UPCOMING_COUNT):
"""The next `count` non-cancelled upcoming games and tournaments, club-wide."""
"""The next `count` non-cancelled games and tournaments that aren't finished
yet, club-wide -- a game already in progress (started, not yet past its
explicit or assumed end) still counts, not just ones that haven't started."""
club = require_club(request)
count = max(1, min(count, MAX_UPCOMING_COUNT))
now = timezone.now()
events = Event.objects.filter(club=club, kind__in=UPCOMING_KINDS, cancelled=False, start__gte=timezone.now()).select_related("opponent", "location").prefetch_related("teams").order_by("start")[:count]
events = (
Event.objects.filter(club=club, kind__in=UPCOMING_KINDS, cancelled=False)
.filter(Q(end__gte=now) | Q(end__isnull=True, start__gte=now - ASSUMED_EVENT_DURATION))
.select_related("opponent", "location")
.prefetch_related("teams")
.order_by("start")[:count]
)
return [_to_game_out(event, request, club) for event in events]