From 0a9ac67b21705b4ce8f2cd50b875264219591bd6 Mon Sep 17 00:00:00 2001 From: Bernard Siebens Date: Mon, 10 Aug 2026 12:34:33 +0200 Subject: [PATCH] Fix widget attrs being dropped from rendered inputs, and redesign the referee PDF field.html's "input" branch rendered type/class/name/value/placeholder only, silently dropping every other widget attr -- so a NumberInput's step="any" (added to let the per-km rate take values like 0.083) never reached the page, and the browser fell back to whole-number-only validation. Pass widget attrs through the same way the "select" branch already does. Also gives the referee payment PDF a proper visual pass (accent header, an info card for the game details, a real fee/km table with a grand-total row) instead of the plain label/dotted-line layout, and lets the dashboard tile show assigned referees with a "referee form" download button per tile. Co-Authored-By: Claude Sonnet 5 --- .../templates/templatetags/field.html | 1 + controlpanel/tests.py | 17 +- .../management/event_referee_form_pdf.html | 173 ++++++++++++------ .../management/referee_management.html | 2 +- management/views.py | 4 +- 5 files changed, 140 insertions(+), 57 deletions(-) diff --git a/controlpanel/templates/templatetags/field.html b/controlpanel/templates/templatetags/field.html index 67dc2a3..24679be 100644 --- a/controlpanel/templates/templatetags/field.html +++ b/controlpanel/templates/templatetags/field.html @@ -24,6 +24,7 @@ name="{{ field.html_name }}" value="{% if field.widget_type == "date" or field.widget_type == "datetime" %}{% if field.value|date:"c"|default:"" != "" %}{{ field.value|date:"c"|default:"" }}{% else %}{{ field.value|default:"" }}{% endif %}{% else %}{% if field.value == None %}{{ field.value|default:"" }}{% else %}{{ field.value }}{% endif %}{% endif %}" {% if show_placeholder %}placeholder="{{ field.label|capfirst }}"{% endif %} + {% for attr, value in field.field.widget.attrs.items %}{% if attr != "type" %}{{ attr }}="{{ value }}" {% endif %}{% endfor %} /> {% elif field_type == "checkbox" %} diff --git a/controlpanel/tests.py b/controlpanel/tests.py index 243f385..e27549d 100644 --- a/controlpanel/tests.py +++ b/controlpanel/tests.py @@ -11,6 +11,7 @@ from django.contrib.auth import get_user_model from django.contrib.messages.storage.base import Message from django.contrib.messages.storage.fallback import FallbackStorage from django.core.cache import cache +from django.template.loader import render_to_string from django.test import RequestFactory, TestCase, override_settings from django.urls import reverse from django.utils import timezone @@ -50,7 +51,7 @@ from .services.statistics import ( teams_without_a_manager, unrostered_members, ) -from .templatetags.ui import as_alert, daisy, excluded, field_icon +from .templatetags.ui import as_alert, daisy, excluded, field_icon, form_field User = get_user_model() Flag = get_waffle_flag_model() @@ -691,6 +692,20 @@ class FieldRenderingTests(TestCase): self.assertIn("input-error", rendered) + def test_the_form_field_tag_carries_widget_attrs_onto_the_input(self): + # Regression: the "input" branch of templatetags/field.html rendered type/ + # class/name/value/placeholder only, silently dropping every other widget + # attr (step, min, ...) -- e.g. NumberInput(attrs={"step": "any"}) never + # reached the page, so a decimal-only field like a per-km rate couldn't be + # typed at all. See form_field in controlpanel/templatetags/ui.py. + bound_field = self.field(forms.DecimalField(widget=forms.NumberInput(attrs={"step": "any", "min": "0"})), "rate") + + html = render_to_string("templatetags/field.html", form_field(bound_field)) + + self.assertIn('step="any"', html) + self.assertIn('min="0"', html) + self.assertEqual(html.count('type="number"'), 1) + class LoginFormRenderingTests(TestCase): def setUp(self): diff --git a/management/templates/management/event_referee_form_pdf.html b/management/templates/management/event_referee_form_pdf.html index 26d98fb..f321138 100644 --- a/management/templates/management/event_referee_form_pdf.html +++ b/management/templates/management/event_referee_form_pdf.html @@ -1,14 +1,13 @@ {% load i18n %} {% comment %} - Rendered by WeasyPrint -- modeled directly on the club's existing paper - referee payment form ("Leeg Document RefereesCompetitie25-26.pdf"): a - 3-line header (club name/address), match details, referee names, a - payment sentence, a fee+km breakdown per referee, and blank signature - lines. Header uses club.official_name (legal_name if set, else name); - the payment sentence uses the plain club name -- the original document - does the same, using a longer legal form up top and a shorter one in the - body text. + Rendered by WeasyPrint -- modeled on the club's existing paper referee + payment form ("Leeg Document RefereesCompetitie25-26.pdf"): club header, + match details, referee names, a payment sentence, a fee+km breakdown per + referee, and blank signature lines. Header uses club.official_name + (legal_name if set, else name); the payment sentence uses the plain club + name -- the original document does the same, using a longer legal form up + top and a shorter one in the body text. {% endcomment %} @@ -18,69 +17,135 @@
-
{{ club.official_name }}
- {% if home_location %} -
{{ home_location.address }}
-
{{ home_location.zip_code }} {{ home_location.city }}
- {% endif %} +
+
{{ club.official_name }}
+ {% if home_location %} +
{{ home_location.address }}
+
{{ home_location.zip_code }} {{ home_location.city }}
+ {% endif %} +
+
+
{% trans "Referee payment form" %}
+
{{ event.start|date:"j F Y" }}
+
-
- {% trans "Regarding: Game" %} - {% for team in event.teams.all %}{{ team.short_name }}{% if not forloop.last %}, {% endif %}{% endfor %} - {% if event.opponent %}{% trans "vs" %} {{ event.opponent }}{% endif %} - {% if event.external_game_id %}({% trans "ID" %}: {{ event.external_game_id }}){% endif %} +
+
+
+
{% trans "Game" %}
+
+ {% for team in event.teams.all %}{{ team.short_name }}{% if not forloop.last %}, {% endif %}{% endfor %} + {% if event.opponent %}{% trans "vs" %} {{ event.opponent }}{% endif %} + {% if event.external_game_id %}({% trans "ID" %}: {{ event.external_game_id }}){% endif %} +
+
+
+
{% trans "Date" %}
+
{{ event.start|date:"j F Y" }}
+
+
+
{% trans "Time" %}
+
{{ event.start|date:"H:i" }}
+
+
+
{% trans "Referee names" %}
+
+ {% for referee in referees %}{{ referee.display_name }}{% if not forloop.last %}, {% endif %}{% empty %}—{% endfor %} +
+
+
-
{% trans "Date" %} {{ event.start|date:"j F Y" }}
-
{% trans "Time" %} {{ event.start|date:"H:i" }}
-
- {% trans "Referee names" %} - {% for referee in referees %}{{ referee.display_name }}{% if not forloop.last %}, {% endif %}{% empty %}—{% endfor %} -
-
-

{% blocktrans with name=club.name %}{{ name }} pays the fee to the referee(s) for officiating the game mentioned above.{% endblocktrans %}

+

{% blocktrans with name=club.name %}{{ name }} pays the fee to the referee(s) for officiating the game mentioned above.{% endblocktrans %}

{% trans "Referees and fee, plus any kilometers" %}
- {% for referee in referees %} -
- {{ referee.display_name }} — - {% blocktrans with fee=referee.fee %}fee: €{{ fee }}{% endblocktrans %} - {% if referee.km %} - {% blocktrans with km=referee.km rate=referee.km_rate total=referee.km_total %}+ {{ km }} km × €{{ rate }}/km = €{{ total }}{% endblocktrans %} + + + + + + + + + + + + {% for referee in referees %} + + + + + + + + {% empty %} + + {% endfor %} + {% if referees %} + + + + {% endif %} - — {% blocktrans with total=referee.total_payable %}total: €{{ total }}{% endblocktrans %} - - {% empty %} -
- {% endfor %} + +
{% trans "Referee" %}{% trans "Fee" %}{% trans "Kilometers" %}{% trans "Rate" %}{% trans "Total" %}
+ {{ referee.display_name }} + {% if referee.is_external %}{% trans "External" %}{% endif %} + €{{ referee.fee|floatformat:2 }}{% if referee.km %}{{ referee.km }} km{% else %}—{% endif %}{% if referee.km %}€{{ referee.km_rate }}{% else %}—{% endif %}€{{ referee.total_payable|floatformat:2 }}
{% trans "No referees assigned yet." %}
{% trans "Total to pay" %}€{{ grand_total|floatformat:2 }}
-
-
{% trans "Referee signature" %}
-
+
+
+
{% trans "Referee signature" %}
+
+
diff --git a/management/templates/management/referee_management.html b/management/templates/management/referee_management.html index e40fe55..ec44c75 100644 --- a/management/templates/management/referee_management.html +++ b/management/templates/management/referee_management.html @@ -54,7 +54,7 @@
{{ game.start|date:"H:i" }}{% if game.location %} — {{ game.location }}{% endif %}
{% for referee in game.referee_rows %} - {{ referee.display_name }}{% if referee.is_external %} ({% trans "ext." %}){% endif %} + {{ referee.display_name }}{% if referee.is_external %} ({% trans "ext." %}){% endif %} {% empty %} {% trans "No referees assigned yet." %} {% endfor %} diff --git a/management/views.py b/management/views.py index 6c621a9..380e73c 100644 --- a/management/views.py +++ b/management/views.py @@ -1,4 +1,5 @@ from datetime import timedelta +from decimal import Decimal from django.core.exceptions import ValidationError from django.db import IntegrityError, transaction @@ -2141,7 +2142,8 @@ class EventRefereeFormPdfView(ClubAdminRequiredMixin, View): def get(self, request, pk): event = get_object_or_404(Event.objects.filter(club=request.club).prefetch_related("teams", "referees__member"), pk=pk) home_location = Location.objects.filter(club=request.club, is_home=True).first() - context = {"club": request.club, "event": event, "referees": list(event.referees.all()), "home_location": home_location} + referees = list(event.referees.all()) + context = {"club": request.club, "event": event, "referees": referees, "home_location": home_location, "grand_total": sum((referee.total_payable for referee in referees), Decimal("0"))} try: pdf = event_referee_form_pdf(context)