Coach Schedule: same This week/Next week/month-divider agenda grouping

Extracted the grouping algorithm (previously duplicated between mobile.views.
CalendarView and management.views.EventListView's own "List" mode) into a
shared events.services.calendar.agenda_groups, and wired it into both --
plus mobile.coach_views.CoachScheduleView, the coach app's own upcoming-
events screen, which had no grouping at all before (one flat card). All
three now read identically and can't drift apart.

Also: give each management event-list row's kind badge a fixed width, so
titles start at the same horizontal position regardless of whether the row
says "Game" or "Tournament".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-23 17:27:56 +02:00
parent a13ab20a0f
commit 6f16737bd9
13 changed files with 211 additions and 88 deletions

View File

@@ -1,6 +1,6 @@
{% load i18n lucide ui %}
<div class="flex items-center gap-3 px-4 py-3 {% if not forloop.last %}border-b border-line{% endif %}">
<span class="badge badge-sm shrink-0 {% if event.kind == "game" %}badge-error{% elif event.kind == "training" %}badge-info{% elif event.kind == "tournament" %}badge-warning{% elif event.kind == "meeting" %}badge-neutral{% elif event.kind == "social" %}border-violet/30 bg-violet/10 text-violet{% else %}badge-outline{% endif %}">
<span class="badge badge-sm w-24 shrink-0 justify-center {% if event.kind == "game" %}badge-error{% elif event.kind == "training" %}badge-info{% elif event.kind == "tournament" %}badge-warning{% elif event.kind == "meeting" %}badge-neutral{% elif event.kind == "social" %}border-violet/30 bg-violet/10 text-violet{% else %}badge-outline{% endif %}">
{{ event.get_kind_display }}
</span>
<div class="min-w-0 flex-1">

View File

@@ -86,7 +86,7 @@
{% for month in list_months %}
<div class="card overflow-hidden">
<div class="border-b border-line bg-subhead px-4 py-2 font-display text-xs font-extrabold tracking-[.08em] text-ink uppercase">{{ month.month_start|date:"F Y" }}</div>
{% for event in month.events %}{% include "management/_event_list_row.html" %}{% endfor %}
{% for event in month.items %}{% include "management/_event_list_row.html" %}{% endfor %}
</div>
{% endfor %}
</div>

View File

@@ -5625,7 +5625,7 @@ class EventManagementTests(ManagementTestBase):
self.assertEqual([event.title for event in response.context["list_next_week"]], ["Next week event"])
later_months = response.context["list_months"]
self.assertEqual(len(later_months), 1)
self.assertEqual([event.title for event in later_months[0]["events"]], ["Later event"])
self.assertEqual([event.title for event in later_months[0]["items"]], ["Later event"])
self.assertContains(response, "This week")
self.assertContains(response, "Next week")
self.assertContains(response, later_event.start.strftime("%B %Y"))
@@ -5640,7 +5640,7 @@ class EventManagementTests(ManagementTestBase):
self.assertEqual(response.context["list_this_week"], [])
self.assertEqual(response.context["list_next_week"], [])
self.assertEqual(len(response.context["list_months"]), 1)
self.assertEqual([event.title for event in response.context["list_months"][0]["events"]], ["Past event"])
self.assertEqual([event.title for event in response.context["list_months"][0]["items"]], ["Past event"])
self.assertNotContains(response, "This week")
def test_the_dashboard_only_shows_upcoming_events_for_managed_teams(self):

View File

@@ -1,4 +1,3 @@
import itertools
from datetime import date, timedelta
from decimal import Decimal
@@ -38,7 +37,7 @@ from controlpanel.mixins import RedirectOnInvalidMixin
from controlpanel.services.statistics import club_attention, club_charts, club_statistics, unrostered_members
from events.models import Attendance, Event, EventReferee, EventSeries, Location, Opponent, RefereeSignup
from events.services.attendance import member_attendance_counts, member_attendance_sparkline, player_attendance_rankings, players_who_missed_recent_practices, team_attendance_rate, team_no_shows
from events.services.calendar import add_months, month_bounds, month_grid, season_grid, week_bounds, week_grid
from events.services.calendar import add_months, agenda_groups, month_bounds, month_grid, season_grid, week_bounds, week_grid
from events.services.competitions import CompetitionFetchError, fetch_game_info
from events.services.rbihf_import import RBIHFImportError, apply_plan, build_plan, extract_team_id, fetch_html
from events.services.recurrence import cancel_occurrence, detach_occurrence, generate_occurrences, propagate_series
@@ -2473,36 +2472,15 @@ class EventListView(ClubStaffRequiredMixin, ListView):
return grid, calendar_nav
def _list_groups(self, events, show_past):
"""The same This week/Next week/by-month agenda grouping mobile.views.
CalendarView uses (see that view's own docstring for the algorithm) --
applied to whichever page of `events` is actually being shown, so
pagination and grouping don't fight each other. Past mode (show_past=1,
already descending) skips the this/next-week special-casing -- those
labels only make sense for what's ahead -- and just groups straight
into months, most recent first."""
if not events:
return [], [], []
if show_past:
months = [{"month_start": month_start, "events": list(month_events)} for month_start, month_events in itertools.groupby(events, key=lambda event: timezone.localtime(event.start).date().replace(day=1))]
return [], [], months
today = timezone.localdate()
_this_week_start, this_week_end = week_bounds(today)
next_week_end = this_week_end + timedelta(days=7)
this_week, next_week, later = [], [], []
for event in events:
event_date = timezone.localtime(event.start).date()
if event_date <= this_week_end:
this_week.append(event)
elif event_date <= next_week_end:
next_week.append(event)
else:
later.append(event)
later_months = [{"month_start": month_start, "events": list(month_events)} for month_start, month_events in itertools.groupby(later, key=lambda event: timezone.localtime(event.start).date().replace(day=1))]
return this_week, next_week, later_months
"""The shared This week/Next week/by-month agenda grouping (events.
services.calendar.agenda_groups, also behind mobile.views.CalendarView
and mobile.coach_views.CoachScheduleView) -- applied to whichever page
of `events` is actually being shown, so pagination and grouping don't
fight each other. Past mode (show_past=1, already descending) skips
the this/next-week special-casing -- those labels only make sense for
what's ahead -- and just groups straight into months, most recent
first."""
return agenda_groups(events, show_past=show_past)
def get_context_data(self, **kwargs):
club, user = self.request.club, self.request.user