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

@@ -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

View File

@@ -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))

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

View File

@@ -26,6 +26,7 @@ from controlpanel.messages import notify
from events.models import Attendance, Event, EventSeries, Lineup, LineupSelection, Location, Opponent
from events.services import generate_occurrences
from events.services.attendance import member_attendance_counts, record_check_in
from events.services.calendar import agenda_groups
from events.services.lineup import UNAVAILABLE_STATUSES, cancel_scheduled_publish, publish_lineup, schedule_lineup_publish, toggle_selection
from events.tasks import notify_new_event
from management.forms import EventForm, EventSeriesForm, LocationForm, NewsForm, OpponentForm
@@ -1104,15 +1105,19 @@ class CoachScheduleView(CoachScopeMixin, LoginRequiredMixin, TemplateView):
straight into the coach-relevant action -- Bench attendance for a
practice, the Line-up for a game -- rather than mobile:event_detail (the
Member-shell RSVP page a coach browsing their own team's schedule has no
use for)."""
use for). Grouped into This week/Next week/by-month dividers via events.
services.calendar.agenda_groups -- the same shared grouping mobile.views.
CalendarView (the member app's own Calendar) and management.views.
EventListView's "List" mode use, so all three read the same way."""
template_name = "mobile/coach/schedule.html"
screen_title = _("Schedule")
active_tab = "coach_schedule"
def get_context_data(self, **kwargs):
events = []
this_week, next_week, later_months = [], [], []
if self.active_team is not None:
events = list(Event.objects.filter(teams=self.active_team, cancelled=False, start__gte=timezone.now()).order_by("start"))
this_week, next_week, later_months = agenda_groups(events)
return super().get_context_data(events=events, **kwargs)
return super().get_context_data(this_week=this_week, next_week=next_week, later_months=later_months, **kwargs)

View File

@@ -5,7 +5,7 @@
M3 -- design_handoff_rosterchief_platform/README.md's M3 section: a full-
width chronological agenda, grouped under "This week"/"Next week", then
everything further out grouped by calendar month (see CalendarView's own
docstring -- later_months is a list of {month_start, rows}). No person
docstring -- later_months is a list of {month_start, items}). No person
switcher and no club-wide toggle here -- always every event
self.managed_people is invited to, plus (merged into the same
chronological list, each its own accent colour/look):
@@ -90,7 +90,7 @@
<div>
<div class="bg-paper px-4 py-2 font-display text-xs font-extrabold tracking-wide text-muted uppercase">{{ month.month_start|date:"F Y" }}</div>
<div class="flex flex-col gap-px bg-line">
{% for row in month.rows %}
{% for row in month.items %}
{% if row.blocked_requirements %}{% include "mobile/_calendar_blocked_row.html" %}{% elif row.referee_signup %}{% include "mobile/_calendar_referee_row.html" %}{% else %}{% include "mobile/_calendar_row.html" %}{% endif %}
{% endfor %}
</div>

View File

@@ -0,0 +1,13 @@
{% load i18n %}
<a class="flex items-center gap-3 p-3.5" href="{% if event.kind == "game" %}{% url "mobile:coach_lineup" event.pk %}{% else %}{% url "mobile:coach_attendance" event.pk %}{% endif %}">
<div class="w-9.5 shrink-0 text-center">
<div class="font-mono text-[10px] tracking-wide text-muted uppercase">{{ event.start|date:"D" }}</div>
<div class="font-display text-2xl leading-none font-extrabold text-ink">{{ event.start|date:"d" }}</div>
</div>
<div class="w-[3px] shrink-0 self-stretch rounded-full {% if event.kind == "game" %}bg-club{% elif event.kind == "training" %}bg-info{% else %}bg-warn{% endif %}"></div>
<div class="min-w-0 flex-1">
<div class="text-sm font-semibold text-ink">{{ event.title }}</div>
<div class="truncate text-xs text-muted">{{ event.start|date:"H:i" }}{% if event.location %} &middot; {{ event.location.name }}{% endif %}</div>
</div>
<span class="pill {% if event.kind == "game" %}pill-danger{% elif event.kind == "training" %}pill-info{% else %}pill-neutral{% endif %} shrink-0">{{ event.get_kind_display }}</span>
</a>

View File

@@ -3,9 +3,12 @@
{% comment %}
Bottom-tab "Schedule" -- every upcoming event for the active team (not
just the next one, that's Today's job). Each row links straight into the
coach-relevant action for its kind rather than mobile:event_detail (the
Member-shell RSVP page) -- see CoachScheduleView's own docstring.
just the next one, that's Today's job), grouped into This week/Next
week/by-month dividers -- see CoachScheduleView's own docstring for the
shared agenda_groups grouping behind this (also mobile.views.CalendarView
and management.views.EventListView's "List" mode). Each row links
straight into the coach-relevant action for its kind rather than
mobile:event_detail (the Member-shell RSVP page).
{% endcomment %}
{% block content %}
@@ -13,26 +16,46 @@
<div class="m-card p-6 text-center">
<p class="font-display text-lg font-extrabold text-ink uppercase">{% trans "Not staffing a team yet" %}</p>
</div>
{% elif not events %}
{% elif not this_week and not next_week and not later_months %}
<div class="m-card p-6 text-center">
<p class="text-sm text-muted">{% trans "Nothing scheduled." %}</p>
</div>
{% else %}
<div class="m-card flex flex-col overflow-hidden">
{% for event in events %}
{% if not forloop.first %}<div class="h-px bg-rule"></div>{% endif %}
<a class="flex items-center gap-3 p-3.5" href="{% if event.kind == "game" %}{% url "mobile:coach_lineup" event.pk %}{% else %}{% url "mobile:coach_attendance" event.pk %}{% endif %}">
<div class="w-9.5 shrink-0 text-center">
<div class="font-mono text-[10px] tracking-wide text-muted uppercase">{{ event.start|date:"D" }}</div>
<div class="font-display text-2xl leading-none font-extrabold text-ink">{{ event.start|date:"d" }}</div>
<div class="flex flex-col gap-4">
{% if this_week %}
<div>
<div class="mb-2 font-display text-xs font-extrabold tracking-wide text-muted uppercase">{% trans "This week" %}</div>
<div class="m-card flex flex-col overflow-hidden">
{% for event in this_week %}
{% if not forloop.first %}<div class="h-px bg-rule"></div>{% endif %}
{% include "mobile/coach/_schedule_row.html" %}
{% endfor %}
</div>
<div class="w-[3px] shrink-0 self-stretch rounded-full {% if event.kind == "game" %}bg-club{% elif event.kind == "training" %}bg-info{% else %}bg-warn{% endif %}"></div>
<div class="min-w-0 flex-1">
<div class="text-sm font-semibold text-ink">{{ event.title }}</div>
<div class="truncate text-xs text-muted">{{ event.start|date:"H:i" }}{% if event.location %} &middot; {{ event.location.name }}{% endif %}</div>
</div>
{% endif %}
{% if next_week %}
<div>
<div class="mb-2 font-display text-xs font-extrabold tracking-wide text-muted uppercase">{% trans "Next week" %}</div>
<div class="m-card flex flex-col overflow-hidden">
{% for event in next_week %}
{% if not forloop.first %}<div class="h-px bg-rule"></div>{% endif %}
{% include "mobile/coach/_schedule_row.html" %}
{% endfor %}
</div>
<span class="pill {% if event.kind == "game" %}pill-danger{% elif event.kind == "training" %}pill-info{% else %}pill-neutral{% endif %} shrink-0">{{ event.get_kind_display }}</span>
</a>
</div>
{% endif %}
{% for month in later_months %}
<div>
<div class="mb-2 font-display text-xs font-extrabold tracking-wide text-muted uppercase">{{ month.month_start|date:"F Y" }}</div>
<div class="m-card flex flex-col overflow-hidden">
{% for event in month.items %}
{% if not forloop.first %}<div class="h-px bg-rule"></div>{% endif %}
{% include "mobile/coach/_schedule_row.html" %}
{% endfor %}
</div>
</div>
{% endfor %}
</div>
{% endif %}

View File

@@ -12,6 +12,7 @@ from icalendar import Calendar as ICalCalendar
from club.models import Club, ClubMembership, DuesInvoice, MemberRequirementStatus, OnboardingRequirement, Season, Sponsor
from events.models import Attendance, Competition, Event, EventReferee, EventSeries, Lineup, LineupSelection, Location, Opponent, RefereeSignup
from events.services.attendance import record_check_in
from events.services.calendar import week_bounds
from members.models import Family, FamilyMembership, Member
from news.models import News
from notifications.models import Notification
@@ -850,7 +851,7 @@ class CalendarViewTests(TestCase):
def _events_in_context(self, response):
rows = response.context["this_week"] + response.context["next_week"]
for month in response.context["later_months"]:
rows += month["rows"]
rows += month["items"]
return {row["event"] for row in rows}
def add_child(self, first_name="Noor"):
@@ -977,7 +978,7 @@ class CalendarViewTests(TestCase):
self.assertEqual(len(later_months), 1)
month_start = timezone.localtime(far_future.start).date().replace(day=1)
self.assertEqual(later_months[0]["month_start"], month_start)
self.assertEqual([row["event"] for row in later_months[0]["rows"]], [far_future])
self.assertEqual([row["event"] for row in later_months[0]["items"]], [far_future])
self.assertContains(response, month_start.strftime("%B %Y"))
def test_further_out_events_are_split_across_separate_month_groups(self):
@@ -3127,7 +3128,9 @@ class CoachScheduleViewTests(TestCase):
response = self._get()
self.assertEqual(response.context["events"], [])
self.assertEqual(response.context["this_week"], [])
self.assertEqual(response.context["next_week"], [])
self.assertEqual(response.context["later_months"], [])
def test_no_staff_assignment_shows_a_graceful_empty_state(self):
bare_user = User.objects.create_user(email="new@example.com", password="pw-secret-123")
@@ -3138,6 +3141,23 @@ class CoachScheduleViewTests(TestCase):
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Not staffing a team yet")
def test_events_are_grouped_into_this_week_next_week_and_month_dividers(self):
_this_week_start, this_week_end = week_bounds(timezone.localdate())
this_week_event = Event.objects.create(club=self.club, title="This week practice", kind=Event.EventKind.TRAINING, start=timezone.make_aware(datetime.datetime.combine(this_week_end, datetime.time(18, 0))))
this_week_event.teams.add(self.team)
far_future = Event.objects.create(club=self.club, title="Far future practice", kind=Event.EventKind.TRAINING, start=timezone.now() + datetime.timedelta(days=60))
far_future.teams.add(self.team)
self.client.force_login(self.user)
response = self._get()
self.assertEqual([event.title for event in response.context["this_week"]], ["This week practice"])
later_months = response.context["later_months"]
self.assertEqual(len(later_months), 1)
self.assertEqual([event.title for event in later_months[0]["items"]], ["Far future practice"])
self.assertContains(response, "This week")
self.assertContains(response, far_future.start.strftime("%B %Y"))
@override_settings(ROSTERCHIEF_BASE_DOMAIN="rosterchief.app", ALLOWED_HOSTS=["rosterchief.app", "ajax-united.rosterchief.app", "testserver"])
class CoachAttendanceViewTests(TestCase):

View File

@@ -4,8 +4,6 @@ phase -- see design_handoff_rosterchief_platform/README.md -- and has no
routes here yet.
"""
import datetime
import itertools
import json
from django.contrib.auth.mixins import LoginRequiredMixin
@@ -29,7 +27,7 @@ from club.services.sponsors import active_sponsors
from controlpanel.messages import notify
from events.models import Attendance, Event, Lineup, RefereeSignup
from events.services.attendance import blocked_upcoming_events_for_member
from events.services.calendar import week_bounds
from events.services.calendar import agenda_groups, week_bounds
from events.services.lineup import notify_dropout, selected_members_by_position
from events.services.referees import RefereeAssignmentError, accept_referee_signup, decline_referee_signup
from members.models import FamilyMembership, Member
@@ -284,10 +282,12 @@ class CalendarView(PersonScopeMixin, LoginRequiredMixin, TemplateView):
"""M3 -- README's M3 section: a chronological agenda list (not the desktop
week/month grid events.services.calendar was built for) grouped under
"This week"/"Next week", then everything further out grouped by calendar
month (a "later_months" list of {month_start, rows}, each its own sticky
header) -- an unbounded agenda rather than a fixed lookahead window, since
a hard cutoff just hid events a member would reasonably expect to still
find here. No ?month= paging beyond that grouping -- there's no season
month via events.services.calendar.agenda_groups (also behind management.
views.EventListView's own "List" mode and mobile.coach_views.
CoachScheduleView, so all three stay behaviourally identical) -- an
unbounded agenda rather than a fixed lookahead window, since a hard
cutoff just hid events a member would reasonably expect to still find
here. No ?month= paging beyond that grouping -- there's no season
browser here, just "everything upcoming, readably grouped".
Always scoped to every one of ``self.managed_people`` -- unlike Home,
@@ -322,8 +322,6 @@ class CalendarView(PersonScopeMixin, LoginRequiredMixin, TemplateView):
def get_context_data(self, **kwargs):
now = timezone.now()
today = timezone.localdate()
_this_week_start, this_week_end = week_bounds(today)
next_week_end = this_week_end + datetime.timedelta(days=7)
kind_filter = self.request.GET.get("kind")
rows = []
@@ -386,22 +384,7 @@ class CalendarView(PersonScopeMixin, LoginRequiredMixin, TemplateView):
rows.sort(key=lambda row: row["event"].start)
this_week, next_week, later_rows = [], [], []
for row in rows:
event_date = timezone.localtime(row["event"].start).date()
if event_date <= this_week_end:
this_week.append(row)
elif event_date <= next_week_end:
next_week.append(row)
else:
later_rows.append(row)
# ``rows`` is already start-ordered, so a plain groupby (no sorting) is
# enough to split later_rows into one chronological run per month.
later_months = [
{"month_start": month_start, "rows": list(month_rows)}
for month_start, month_rows in itertools.groupby(later_rows, key=lambda row: timezone.localtime(row["event"].start).date().replace(day=1))
]
this_week, next_week, later_months = agenda_groups(rows, start_of=lambda row: row["event"].start, today=today)
return super().get_context_data(this_week=this_week, next_week=next_week, later_months=later_months, kind_filter=kind_filter if kind_filter in self.KIND_FILTERS else "", **kwargs)

File diff suppressed because one or more lines are too long