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

@@ -23,7 +23,7 @@ from events.models import Attendance, Competition, Event, EventReferee, EventSer
from events.services.rbihf_import import RBIHFImportError
from events.services.recurrence import detach_occurrence, generate_occurrences
from management.bulk_import import TEMPLATE_COLUMNS
from management.pdf import PDFExportError, render_pdf
from management.pdf import PDFExportError, _tint_with_white, referee_form_colors, render_pdf
from management.recurrence_ui import build_rrule, describe_rrule, parse_rrule
from members.models import Family, FamilyMembership, Group, GroupMembership, Member
from news.models import News, NewsPhoto
@@ -3526,6 +3526,30 @@ class EventManagementTests(ManagementTestBase):
self.assertRedirects(response, reverse("management:event_detail", args=[event.pk]))
self.assertEqual(event.location, location)
def test_the_location_dropdown_shows_the_city(self):
Location.objects.create(club=self.club, name="Sportcentrum", address="Straat 1", city="Mechelen", zip_code="2800", country="BE")
self.client.force_login(self.admin_user)
response = self.club_get("event_create")
self.assertContains(response, "Sportcentrum — Mechelen")
def test_the_location_dropdown_adds_the_country_when_not_belgium(self):
Location.objects.create(club=self.club, name="Rival Hall", address="Rue 2", city="Lille", zip_code="59000", country="FR")
self.client.force_login(self.admin_user)
response = self.club_get("event_create")
self.assertContains(response, "Rival Hall — Lille, France")
def test_the_location_field_is_searchable(self):
self.client.force_login(self.admin_user)
response = self.club_get("event_create")
self.assertContains(response, 'name="location"')
self.assertContains(response, "data-searchable")
def test_the_new_event_forms_competition_dropdown_shows_every_competition_regardless_of_flag(self):
# Unlike the Django-admin form, this dropdown isn't filtered by whether the
# competition's flag is active for the club -- see management.forms.EventForm
@@ -4322,18 +4346,45 @@ class EventRefereeFormPdfTests(ManagementTestBase):
self.club.save(update_fields=["primary_color", "secondary_color"])
game = self.make_game()
html = render_to_string("management/event_referee_form_pdf.html", {"club": self.club, "event": game, "referees": [], "home_location": self.home_ground, "grand_total": Decimal("0")})
html = render_to_string("management/event_referee_form_pdf.html", {"club": self.club, "event": game, "referees": [], "home_location": self.home_ground, "grand_total": Decimal("0"), **referee_form_colors(self.club)})
self.assertIn("--accent: #0f766e", html)
self.assertIn("--accent-secondary: #f59e0b", html)
def test_the_pdf_falls_back_to_default_colours_when_unset(self):
game = self.make_game()
html = render_to_string("management/event_referee_form_pdf.html", {"club": self.club, "event": game, "referees": [], "home_location": self.home_ground, "grand_total": Decimal("0")})
html = render_to_string("management/event_referee_form_pdf.html", {"club": self.club, "event": game, "referees": [], "home_location": self.home_ground, "grand_total": Decimal("0"), **referee_form_colors(self.club)})
self.assertIn("--accent: #3730a3", html)
self.assertIn("--accent-secondary: #be185d", html)
def test_the_info_card_background_is_not_left_to_unsupported_css(self):
# WeasyPrint doesn't support color-mix() -- the background must be a
# plain computed hex value baked into the template, or the card
# silently renders with no background at all.
game = self.make_game()
html = render_to_string("management/event_referee_form_pdf.html", {"club": self.club, "event": game, "referees": [], "home_location": self.home_ground, "grand_total": Decimal("0"), **referee_form_colors(self.club)})
self.assertNotIn("color-mix(", html)
self.assertIn("--info-card-bg: #", html)
def test_the_info_card_falls_back_to_secondary_when_primary_is_near_white(self):
self.club.primary_color = "#ffffff"
self.club.secondary_color = "#f59e0b"
self.club.save(update_fields=["primary_color", "secondary_color"])
colors = referee_form_colors(self.club)
self.assertEqual(colors["accent_color"], "#ffffff")
self.assertNotEqual(colors["info_card_color"], "#ffffff")
def test_the_info_card_uses_primary_when_it_is_not_near_black_or_white(self):
self.club.primary_color = "#0f766e"
self.club.save(update_fields=["primary_color"])
colors = referee_form_colors(self.club)
self.assertEqual(colors["info_card_color"], _tint_with_white("#0f766e"))
def test_a_missing_pdf_library_is_reported_rather_than_a_500(self):
game = self.make_game()