Show everything past next week on Calendar, grouped by month
The agenda used to hard-cut at a ~14-day lookahead with no way to see anything past it. Drop the cutoff and bucket whatever falls beyond "Next week" into per-month groups (each its own sticky header), so the screen stays a full upcoming agenda instead of losing events that were still worth seeing. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
This commit is contained in:
@@ -3,16 +3,16 @@
|
||||
|
||||
{% comment %}
|
||||
M3 -- design_handoff_rosterchief_platform/README.md's M3 section: a full-
|
||||
width chronological agenda, grouped under "This week"/"Next week" (see
|
||||
CalendarView's own docstring for the browsing-window judgment call --
|
||||
current + next calendar week, no further paging). No person switcher and
|
||||
no club-wide toggle here -- always every event self.managed_people is
|
||||
invited to, full stop. A real, working ?kind= filter (All/Games/
|
||||
Practices) stands in for the design mock's "Games only" pill; its List/
|
||||
Month toggle isn't reproduced -- that's a second view mode, not a filter.
|
||||
-mx-4 (on the row groups only, not this filter row) breaks the rows out
|
||||
of base.html's shared page padding so they run edge-to-edge, matching
|
||||
the design canvas's own M3 markup.
|
||||
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
|
||||
switcher and no club-wide toggle here -- always every event
|
||||
self.managed_people is invited to, full stop. A real, working ?kind=
|
||||
filter (All/Games/Practices) stands in for the design mock's "Games only"
|
||||
pill; its List/Month toggle isn't reproduced -- that's a second view
|
||||
mode, not a filter. -mx-4 (on the row groups only, not this filter row)
|
||||
breaks the rows out of base.html's shared page padding so they run
|
||||
edge-to-edge, matching the design canvas's own M3 markup.
|
||||
{% endcomment %}
|
||||
|
||||
{% block header_extra %}
|
||||
@@ -42,15 +42,15 @@
|
||||
<p class="mt-1 text-sm text-muted">{% trans "Once you're linked to a member record, their schedule will show up here." %}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% elif not this_week and not next_week %}
|
||||
{% elif not this_week and not next_week and not later_months %}
|
||||
<div class="mx-4">
|
||||
<div class="m-card p-6 text-center">
|
||||
{% if kind_filter == "game" %}
|
||||
<p class="text-sm text-muted">{% trans "No games in the next two weeks." %}</p>
|
||||
<p class="text-sm text-muted">{% trans "No games scheduled." %}</p>
|
||||
{% elif kind_filter == "training" %}
|
||||
<p class="text-sm text-muted">{% trans "No practices in the next two weeks." %}</p>
|
||||
<p class="text-sm text-muted">{% trans "No practices scheduled." %}</p>
|
||||
{% else %}
|
||||
<p class="text-sm text-muted">{% trans "Nothing scheduled in the next two weeks." %}</p>
|
||||
<p class="text-sm text-muted">{% trans "Nothing scheduled." %}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -76,6 +76,17 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% for month in later_months %}
|
||||
<div>
|
||||
<div class="sticky top-0 z-10 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 %}
|
||||
{% include "mobile/_calendar_row.html" %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
||||
@@ -619,7 +619,10 @@ class CalendarViewTests(TestCase):
|
||||
return Event.objects.create(**kwargs)
|
||||
|
||||
def _events_in_context(self, response):
|
||||
return {row["event"] for row in response.context["this_week"] + response.context["next_week"]}
|
||||
rows = response.context["this_week"] + response.context["next_week"]
|
||||
for month in response.context["later_months"]:
|
||||
rows += month["rows"]
|
||||
return {row["event"] for row in rows}
|
||||
|
||||
def add_child(self, first_name="Noor"):
|
||||
family = Family.objects.create(name="Bakker")
|
||||
@@ -724,10 +727,8 @@ class CalendarViewTests(TestCase):
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "No one to show yet")
|
||||
|
||||
def test_events_outside_the_two_week_window_are_excluded(self):
|
||||
far_future = self.make_event(title="Far future game", start=timezone.now() + datetime.timedelta(days=30))
|
||||
def test_past_events_are_excluded(self):
|
||||
past = self.make_event(title="Past practice", start=timezone.now() - datetime.timedelta(days=1))
|
||||
Attendance.objects.create(event=far_future, member=self.member)
|
||||
Attendance.objects.create(event=past, member=self.member)
|
||||
self.client.force_login(self.user)
|
||||
|
||||
@@ -735,12 +736,42 @@ class CalendarViewTests(TestCase):
|
||||
|
||||
self.assertEqual(self._events_in_context(response), set())
|
||||
|
||||
def test_events_beyond_next_week_are_grouped_by_month(self):
|
||||
far_future = self.make_event(title="Far future game", start=timezone.now() + datetime.timedelta(days=45))
|
||||
Attendance.objects.create(event=far_future, member=self.member)
|
||||
self.client.force_login(self.user)
|
||||
|
||||
response = self._get()
|
||||
|
||||
self.assertIn(far_future, self._events_in_context(response))
|
||||
later_months = response.context["later_months"]
|
||||
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.assertContains(response, month_start.strftime("%B %Y"))
|
||||
|
||||
def test_further_out_events_are_split_across_separate_month_groups(self):
|
||||
# >=32 days apart guarantees two different calendar months regardless
|
||||
# of which day-of-month "today" happens to be (the longest month is
|
||||
# 31 days), so this can't flake depending on when the suite runs.
|
||||
this_month = self.make_event(title="This month game", start=timezone.now() + datetime.timedelta(days=45))
|
||||
next_month = self.make_event(title="Next month game", start=timezone.now() + datetime.timedelta(days=80))
|
||||
Attendance.objects.create(event=this_month, member=self.member)
|
||||
Attendance.objects.create(event=next_month, member=self.member)
|
||||
self.client.force_login(self.user)
|
||||
|
||||
response = self._get()
|
||||
|
||||
later_months = response.context["later_months"]
|
||||
self.assertEqual(len(later_months), 2)
|
||||
|
||||
def test_window_covers_at_least_fourteen_days_regardless_of_which_weekday_today_is(self):
|
||||
# Regression: the window used to be pinned to "through next calendar
|
||||
# week's Sunday", which shrank to as little as 8-9 days whenever today
|
||||
# fell late in the week -- an event 13 days out (a noon start, so
|
||||
# today's own time-of-day can't push it across a date boundary) must
|
||||
# always still show, whatever day the test happens to run on.
|
||||
# An event 13 days out (a noon start, so today's own time-of-day can't
|
||||
# push it across a date boundary) must always still show up somewhere
|
||||
# on the agenda, whatever day the test happens to run on -- whether
|
||||
# that's "Next week" or (on weekdays where the calendar week ends
|
||||
# sooner) the first month group.
|
||||
thirteen_days_out = timezone.make_aware(datetime.datetime.combine(timezone.localdate() + datetime.timedelta(days=13), datetime.time(12, 0)))
|
||||
event = self.make_event(title="Two weeks out", start=thirteen_days_out)
|
||||
Attendance.objects.create(event=event, member=self.member)
|
||||
|
||||
@@ -5,6 +5,7 @@ routes here yet.
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import itertools
|
||||
import json
|
||||
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
@@ -274,15 +275,12 @@ class HomeView(PersonScopeMixin, LoginRequiredMixin, TemplateView):
|
||||
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". Browsing-window judgment call: the design doc
|
||||
doesn't specify month navigation for the mobile screen, so this only ever
|
||||
shows *upcoming* events within the next 14 days (no "Later"/past bucket,
|
||||
no ?month= paging) -- a simple, bounded agenda rather than a full season
|
||||
browser. The window is always >= 14 days from today, not just "through
|
||||
next calendar week's Sunday" -- pinning it to the calendar week alone
|
||||
would shrink the effective lookahead to as little as 8-9 days whenever
|
||||
today falls late in the week, silently dropping events a member would
|
||||
expect to still see (see get_context_data's window_end_date).
|
||||
"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
|
||||
browser here, just "everything upcoming, readably grouped".
|
||||
|
||||
Always scoped to every one of ``self.managed_people`` -- unlike Home,
|
||||
this screen has no person switcher and no "every club event" toggle: it's
|
||||
@@ -317,12 +315,7 @@ class CalendarView(PersonScopeMixin, LoginRequiredMixin, TemplateView):
|
||||
now = timezone.now()
|
||||
today = timezone.localdate()
|
||||
_this_week_start, this_week_end = week_bounds(today)
|
||||
# At least 14 days out from today, not just "through next calendar week's
|
||||
# Sunday" -- that alone shrinks to as little as 8-9 days when today falls
|
||||
# late in the week (e.g. today=Friday puts next_week_end only 9 days out),
|
||||
# silently dropping events a member would reasonably expect to still see.
|
||||
window_end_date = max(this_week_end + datetime.timedelta(days=7), today + datetime.timedelta(days=13))
|
||||
window_end = timezone.make_aware(datetime.datetime.combine(window_end_date, datetime.time.max))
|
||||
next_week_end = this_week_end + datetime.timedelta(days=7)
|
||||
kind_filter = self.request.GET.get("kind")
|
||||
|
||||
rows = []
|
||||
@@ -333,7 +326,6 @@ class CalendarView(PersonScopeMixin, LoginRequiredMixin, TemplateView):
|
||||
event__club=self.request.club,
|
||||
event__cancelled=False,
|
||||
event__start__gte=now,
|
||||
event__start__lte=window_end,
|
||||
)
|
||||
.select_related("event", "event__location", "event__opponent", "member")
|
||||
.prefetch_related("event__teams")
|
||||
@@ -350,12 +342,24 @@ class CalendarView(PersonScopeMixin, LoginRequiredMixin, TemplateView):
|
||||
for attendance in attendances
|
||||
]
|
||||
|
||||
this_week, next_week = [], []
|
||||
this_week, next_week, later_rows = [], [], []
|
||||
for row in rows:
|
||||
bucket = this_week if timezone.localtime(row["event"].start).date() <= this_week_end else next_week
|
||||
bucket.append(row)
|
||||
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)
|
||||
|
||||
return super().get_context_data(this_week=this_week, next_week=next_week, kind_filter=kind_filter if kind_filter in self.KIND_FILTERS else "", **kwargs)
|
||||
# ``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))
|
||||
]
|
||||
|
||||
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)
|
||||
|
||||
|
||||
class EventDetailView(PersonScopeMixin, LoginRequiredMixin, TemplateView):
|
||||
|
||||
Reference in New Issue
Block a user