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:
@@ -16,6 +16,7 @@ Three grids, one per granularity:
|
||||
|
||||
import calendar as _calendar
|
||||
import datetime
|
||||
import itertools
|
||||
|
||||
from django.utils import timezone
|
||||
|
||||
@@ -55,6 +56,58 @@ def add_months(anchor: datetime.date, months: int) -> datetime.date:
|
||||
return datetime.date(year, month, 1)
|
||||
|
||||
|
||||
def agenda_groups(items, *, start_of=lambda item: item.start, show_past=False, today=None):
|
||||
"""Group already start-sorted ``items`` the way every agenda-style
|
||||
schedule in this app presents itself: "This week"/"Next week", then
|
||||
everything further out under its own month divider -- shared by
|
||||
mobile.views.CalendarView (the member app's own Calendar), management.
|
||||
views.EventListView (the desktop's "List" view), and mobile.coach_views.
|
||||
CoachScheduleView (the coach app's "Schedule"), so the three stay
|
||||
behaviourally identical rather than three hand-rolled copies of the same
|
||||
grouping drifting apart over time.
|
||||
|
||||
``start_of`` extracts an item's start datetime -- the default assumes a
|
||||
bare ``Event``-like object; pass a lambda for anything else (e.g.
|
||||
CalendarView's own rows, each a dict wrapping one).
|
||||
|
||||
``show_past`` is for a caller already querying in descending order (most
|
||||
recent first) -- "This week"/"Next week" only make sense for what's
|
||||
ahead, so past mode skips that split and groups straight into months
|
||||
instead, newest month first, oldest last.
|
||||
|
||||
Returns ``(this_week, next_week, later_months)`` -- ``later_months`` is
|
||||
``[{"month_start": date, "items": [...]}, ...]``; ``this_week``/
|
||||
``next_week`` are always ``[]`` in show_past mode, with everything
|
||||
carried in ``later_months`` instead.
|
||||
"""
|
||||
if not items:
|
||||
return [], [], []
|
||||
|
||||
def _month_start(item):
|
||||
return timezone.localtime(start_of(item)).date().replace(day=1)
|
||||
|
||||
if show_past:
|
||||
months = [{"month_start": month_start, "items": list(month_items)} for month_start, month_items in itertools.groupby(items, key=_month_start)]
|
||||
return [], [], months
|
||||
|
||||
today = today or timezone.localdate()
|
||||
_this_week_start, this_week_end = week_bounds(today)
|
||||
next_week_end = this_week_end + datetime.timedelta(days=7)
|
||||
|
||||
this_week, next_week, later = [], [], []
|
||||
for item in items:
|
||||
item_date = timezone.localtime(start_of(item)).date()
|
||||
if item_date <= this_week_end:
|
||||
this_week.append(item)
|
||||
elif item_date <= next_week_end:
|
||||
next_week.append(item)
|
||||
else:
|
||||
later.append(item)
|
||||
|
||||
later_months = [{"month_start": month_start, "items": list(month_items)} for month_start, month_items in itertools.groupby(later, key=_month_start)]
|
||||
return this_week, next_week, later_months
|
||||
|
||||
|
||||
def _local_span(event) -> tuple[datetime.datetime, datetime.datetime]:
|
||||
"""An event's start/end in local time, end defaulting to +1h when unset
|
||||
(mirrors the "assumed duration" read-time fallback events.models.Event
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from datetime import date, datetime, time, timedelta
|
||||
from decimal import Decimal
|
||||
from io import StringIO
|
||||
from types import SimpleNamespace
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.cache import cache
|
||||
@@ -35,7 +36,7 @@ from .services import (
|
||||
team_no_shows,
|
||||
)
|
||||
from .services.attendance import member_attendance_counts
|
||||
from .services.calendar import add_months, month_bounds, month_grid, season_grid, week_bounds, week_grid
|
||||
from .services.calendar import add_months, agenda_groups, month_bounds, month_grid, season_grid, week_bounds, week_grid
|
||||
from .services.lineup import cancel_scheduled_publish, notify_dropout, publish_lineup, schedule_lineup_publish, selected_members_by_position, toggle_selection
|
||||
from .services.rbihf_import import RBIHFImportError, apply_plan, build_plan, extract_team_id, parse_fixtures, suggested_location, suggested_opponent
|
||||
from .services.referees import (
|
||||
@@ -1807,6 +1808,53 @@ class CalendarGridTests(EventsTestBase):
|
||||
def test_add_months_rolls_over_the_year(self):
|
||||
self.assertEqual(add_months(date(2026, 11, 15), 2), date(2027, 1, 1))
|
||||
|
||||
def _item(self, day):
|
||||
return SimpleNamespace(start=self.at(day, 12))
|
||||
|
||||
def test_agenda_groups_splits_into_this_week_next_week_and_later(self):
|
||||
# 2026-08-19 is a Wednesday -- this week runs Mon 17..Sun 23, next
|
||||
# week Mon 24..Sun 30, matching test_week_bounds_returns_monday_to_
|
||||
# sunday above.
|
||||
this_week_item = self._item(date(2026, 8, 21))
|
||||
next_week_item = self._item(date(2026, 8, 27))
|
||||
later_item = self._item(date(2026, 9, 15))
|
||||
items = [this_week_item, next_week_item, later_item]
|
||||
|
||||
this_week, next_week, later_months = agenda_groups(items, today=date(2026, 8, 19))
|
||||
|
||||
self.assertEqual(this_week, [this_week_item])
|
||||
self.assertEqual(next_week, [next_week_item])
|
||||
self.assertEqual(len(later_months), 1)
|
||||
self.assertEqual(later_months[0]["month_start"], date(2026, 9, 1))
|
||||
self.assertEqual(later_months[0]["items"], [later_item])
|
||||
|
||||
def test_agenda_groups_later_items_split_across_separate_months(self):
|
||||
september_item = self._item(date(2026, 9, 15))
|
||||
october_item = self._item(date(2026, 10, 15))
|
||||
|
||||
_this_week, _next_week, later_months = agenda_groups([september_item, october_item], today=date(2026, 8, 19))
|
||||
|
||||
self.assertEqual([month["month_start"] for month in later_months], [date(2026, 9, 1), date(2026, 10, 1)])
|
||||
|
||||
def test_agenda_groups_show_past_skips_week_labels(self):
|
||||
past_item = self._item(date(2026, 8, 1))
|
||||
|
||||
this_week, next_week, months = agenda_groups([past_item], show_past=True)
|
||||
|
||||
self.assertEqual(this_week, [])
|
||||
self.assertEqual(next_week, [])
|
||||
self.assertEqual(months, [{"month_start": date(2026, 8, 1), "items": [past_item]}])
|
||||
|
||||
def test_agenda_groups_empty_list(self):
|
||||
self.assertEqual(agenda_groups([]), ([], [], []))
|
||||
|
||||
def test_agenda_groups_start_of_accessor_for_non_event_items(self):
|
||||
row = {"event": self._item(date(2026, 8, 21))}
|
||||
|
||||
this_week, _next_week, _later = agenda_groups([row], start_of=lambda row: row["event"].start, today=date(2026, 8, 19))
|
||||
|
||||
self.assertEqual(this_week, [row])
|
||||
|
||||
def test_week_grid_always_covers_the_full_day(self):
|
||||
monday = date(2026, 8, 17)
|
||||
event = self.make_event(start=self.at(monday, 10), end=self.at(monday, 11, 30))
|
||||
|
||||
Reference in New Issue
Block a user