From 5d54f1cdccbace009258064f53583605194215c9 Mon Sep 17 00:00:00 2001 From: Bernard Siebens Date: Sun, 23 Aug 2026 14:23:19 +0200 Subject: [PATCH] New event: space out the stacked cards, rework the kind picker to 4 tiles - The kind-tile grid, title/date/location card, Who card, and Answers-close card were direct children of the form with no gap between them at all -- the wrapping flex/gap-4 on coach-sheet's own content div only ever applied between the header bar and the form, not what's inside it. The form itself is now flex flex-col gap-4. - The picker offered Practice/Game/Other, none of which actually changed which fields render. Reworked to Practice/Game/Tournament/Meeting (2x2) -- the four kinds worth adding from the app; social/other stay desktop-only. Also narrowed the form's own kind choices to match server-side, not just cosmetically in the template. Co-Authored-By: Claude Sonnet 5 --- mobile/coach_views.py | 9 +++++ mobile/templates/mobile/coach/event_form.html | 13 ++++--- mobile/tests.py | 34 +++++++++++++++++++ 3 files changed, 52 insertions(+), 4 deletions(-) diff --git a/mobile/coach_views.py b/mobile/coach_views.py index d288fdc..9406809 100644 --- a/mobile/coach_views.py +++ b/mobile/coach_views.py @@ -45,6 +45,11 @@ IN_STATUSES = [Attendance.AttendanceStatus.PRESENT, Attendance.AttendanceStatus. #: Distinct from NO_RESPONSE ("silent"), which is a non-answer rather than a no. OUT_STATUSES = [Attendance.AttendanceStatus.ABSENT, Attendance.AttendanceStatus.EXCUSED, Attendance.AttendanceStatus.NOT_SELECTED] +#: The event kinds CoachCreateEventView's tile picker offers -- social/other +#: stay desktop-only (management.forms.EventForm keeps the full list), since +#: neither has a tile here. +COACH_EVENT_KINDS = [Event.EventKind.TRAINING, Event.EventKind.GAME, Event.EventKind.TOURNAMENT, Event.EventKind.MEETING] + #: How long an event stays "current" (CoachTodayView's session card, and the #: missing-line-up nudge) past the moment it starts -- events.start__gte=now #: alone would flip to the next session the instant this one begins, while @@ -321,6 +326,10 @@ class CoachCreateEventView(CoachScopeMixin, LoginRequiredMixin, TemplateView): # referee-count control this screen has no use for; construct_instance # skips deleted fields entirely, leaving the instance's own default. del form.fields["max_referees"] + # Narrowed to the four kinds the tile picker actually offers -- social/ + # other don't get their own tile, and this keeps a tampered request from + # setting one anyway (the desktop form still offers the full list). + form.fields["kind"].choices = [choice for choice in form.fields["kind"].choices if choice[0] in COACH_EVENT_KINDS] # The desktop searchable multi-select relies on management's own JS # widget, not loaded here -- plain checkboxes work without it and # read better on a phone regardless. diff --git a/mobile/templates/mobile/coach/event_form.html b/mobile/templates/mobile/coach/event_form.html index e8e5d4e..b5cae04 100644 --- a/mobile/templates/mobile/coach/event_form.html +++ b/mobile/templates/mobile/coach/event_form.html @@ -24,10 +24,10 @@ {% endif %} -
+ {% csrf_token %} -
+
+
+ {% for error in form.kind.errors %}

{{ error }}

{% endfor %}
diff --git a/mobile/tests.py b/mobile/tests.py index d4d6fd6..c8be1aa 100644 --- a/mobile/tests.py +++ b/mobile/tests.py @@ -3464,6 +3464,40 @@ class CoachCreateEventViewTests(TestCase): self.assertEqual(response.status_code, 403) self.assertFalse(Event.objects.filter(title="Blocked practice").exists()) + def test_kind_choices_are_limited_to_the_tile_picker(self): + self.client.force_login(self.user) + + response = self.client.get(reverse("mobile:coach_create_event"), HTTP_HOST="ajax-united.rosterchief.app") + + kind_choices = {value for value, _label in response.context["form"].fields["kind"].choices} + self.assertEqual(kind_choices, {"training", "game", "tournament", "meeting"}) + + def test_can_create_a_tournament(self): + self.client.force_login(self.user) + + self._post(kind="tournament", title="Regional tournament") + + event = Event.objects.get(title="Regional tournament") + self.assertEqual(event.kind, Event.EventKind.TOURNAMENT) + + def test_can_create_a_meeting(self): + self.client.force_login(self.user) + + self._post(kind="meeting", title="Team meeting") + + event = Event.objects.get(title="Team meeting") + self.assertEqual(event.kind, Event.EventKind.MEETING) + + def test_other_and_social_are_rejected(self): + self.client.force_login(self.user) + + for kind in ("other", "social"): + with self.subTest(kind=kind): + response = self._post(kind=kind, title=f"Not a {kind} event") + + self.assertEqual(response.status_code, 200) + self.assertFalse(Event.objects.filter(title=f"Not a {kind} event").exists()) + def test_missing_title_reshows_the_form_with_errors(self): self.client.force_login(self.user)