diff --git a/controlpanel/templates/controlpanel/_club_health_table.html b/controlpanel/templates/controlpanel/_club_health_table.html new file mode 100644 index 0000000..b8795bd --- /dev/null +++ b/controlpanel/templates/controlpanel/_club_health_table.html @@ -0,0 +1,62 @@ +{% load lucide %} + +{% comment %} + The club table, shared by the dashboard and the clubs list so the two cannot drift apart. + Health, not vanity: a member total says nothing you can act on, while "no coach", + "nothing scheduled", "€ owed" and "no admins" each name something somebody has to go and + fix. Every column is annotated by clubs_with_health() in a single query. + + Expects: clubs (from clubs_with_health), and optionally empty_message. +{% endcomment %} +
+ + + + + + + + + + + + + + {% for club in clubs %} + + + + + + + + + + {% empty %} + + + + {% endfor %} + +
ClubMembersUnpaidOwedTeamsUpcomingAdmins
+ {{ club.name }} +
+ {{ club.slug }} + {% if club.is_archived %} + {% comment %} + An archived club's subdomain does not resolve, so "dormant" and + "no season" would be noise: of course nothing is scheduled. + {% endcomment %} + {% lucide "archive" size=10 %} Archived + {% else %} + {% if not club.has_season %}{% lucide "calendar-x" size=10 %} No season{% endif %} + {% if not club.upcoming_events %}{% lucide "moon-star" size=10 %} Dormant{% endif %} + {% endif %} +
+
{{ club.active_members }}{{ club.unpaid_members }}€{{ club.outstanding|floatformat:2 }} + {{ club.team_count }} + {% if club.teams_without_coach %} + {{ club.teams_without_coach }} no coach + {% endif %} + {{ club.upcoming_events }}{{ club.admin_count }}
{{ empty_message|default:"No clubs yet." }}
+
diff --git a/controlpanel/tests.py b/controlpanel/tests.py index b52ba57..c40a4df 100644 --- a/controlpanel/tests.py +++ b/controlpanel/tests.py @@ -1,8 +1,10 @@ import datetime +import pathlib from decimal import Decimal from allauth.mfa.models import Authenticator from django import forms +from django.conf import settings from django.contrib import messages from django.contrib.auth import get_user_model from django.contrib.messages.storage.base import Message @@ -1008,3 +1010,48 @@ class ClubHealthTableTests(TestCase): self.assertContains(response, "Owed") self.assertContains(response, "Upcoming") self.assertContains(response, "Unpaid") + + +class ClubListHealthTests(ControlPanelTestBase): + def test_the_list_shows_the_same_health_columns_as_the_dashboard(self): + response = self.client.get(reverse("controlpanel:club_list")) + + self.assertContains(response, "Owed") + self.assertContains(response, "Upcoming") + self.assertContains(response, "Unpaid") + self.assertTemplateUsed(response, "controlpanel/_club_health_table.html") + + def test_an_archived_club_is_badged_archived_rather_than_dormant(self): + # Its subdomain does not resolve, so "nothing scheduled" is not news. + self.club.archive() + + response = self.client.get(reverse("controlpanel:club_list"), {"archived": "1"}) + + self.assertContains(response, "Archived") + self.assertNotContains(response, "Dormant") + + def test_searching_keeps_the_health_annotations(self): + response = self.client.get(reverse("controlpanel:club_list"), {"q": "Ajax"}) + + club = response.context["clubs"][0] + + self.assertEqual(club.active_members, 0) + self.assertEqual(club.teams_without_coach, 0) + + def test_the_list_does_not_fan_out_per_club(self): + for name in ("Feyenoord", "PSV", "Twente"): + Club.objects.create(name=name) + + with self.assertNumQueries(1): + [(club.outstanding, club.upcoming_events) for club in clubs_with_health()] + + +class TemplateCommentTests(TestCase): + def test_no_template_uses_a_multiline_hash_comment(self): + """Django's {# #} is single-line only — its lexer regex is not DOTALL, so a + multi-line one is not a comment at all: it renders to the page as text.""" + templates = [path for path in pathlib.Path(settings.BASE_DIR).glob("**/templates/**/*.html") if ".venv" not in path.parts and "node_modules" not in path.parts] + offenders = [f"{path.relative_to(settings.BASE_DIR)}:{number}" for path in templates for number, line in enumerate(path.read_text().splitlines(), start=1) if "{#" in line and "#}" not in line] + + self.assertTrue(templates) # the glob must actually be finding our templates + self.assertEqual(offenders, [], "use {% comment %} for multi-line comments")