Add a working event-kind filter to Calendar; merge per-screen headers into the shared navy bar
Calendar gains a real "All/Games/Practices" filter (?kind=), a working version of the design mock's own "Games only" pill -- its List/Month toggle isn't reproduced, that's a whole second view mode, not a filter. base.html gains a header_extra block so a screen can add its own title into the *same* navy app-header instead of stacking a second, differently-coloured block underneath it. Me's avatar/name/subtitle row moved there from its own bg-ink block (a visible colour seam that didn't match the design canvas's own M5 markup, which keeps it all in one navy area), and Calendar now shows a "Calendar" title the same way. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
This commit is contained in:
@@ -70,6 +70,7 @@
|
||||
{% if unread_notification_count %}<span class="absolute top-1.5 right-1.5 h-2 w-2 rounded-full bg-club" style="border: 2px solid var(--color-navy)"></span>{% endif %}
|
||||
</a>
|
||||
</div>
|
||||
{% block header_extra %}{% endblock header_extra %}
|
||||
</header>
|
||||
|
||||
<main class="flex-1 overflow-y-auto">
|
||||
|
||||
@@ -7,13 +7,33 @@
|
||||
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. -mx-4 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 (the desktop List/Month/"Games only" controls in that mock
|
||||
aren't reproduced here -- no real functionality behind them yet).
|
||||
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 %}
|
||||
<div class="mt-3">
|
||||
<span class="font-display text-2xl font-extrabold text-white uppercase">{% trans "Calendar" %}</span>
|
||||
</div>
|
||||
{% endblock header_extra %}
|
||||
|
||||
{% block content %}
|
||||
<div class="flex gap-2">
|
||||
<a class="flex h-9 flex-1 items-center justify-center rounded-full font-display text-xs font-extrabold tracking-wide uppercase {% if not kind_filter %}bg-ink text-white{% else %}border border-line bg-white text-muted{% endif %}" href="{% url "mobile:calendar" %}">
|
||||
{% trans "All" %}
|
||||
</a>
|
||||
<a class="flex h-9 flex-1 items-center justify-center rounded-full font-display text-xs font-extrabold tracking-wide uppercase {% if kind_filter == "game" %}bg-ink text-white{% else %}border border-line bg-white text-muted{% endif %}" href="?kind=game">
|
||||
{% trans "Games" %}
|
||||
</a>
|
||||
<a class="flex h-9 flex-1 items-center justify-center rounded-full font-display text-xs font-extrabold tracking-wide uppercase {% if kind_filter == "training" %}bg-ink text-white{% else %}border border-line bg-white text-muted{% endif %}" href="?kind=training">
|
||||
{% trans "Practices" %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="-mx-4">
|
||||
{% if not managed_people %}
|
||||
<div class="mx-4">
|
||||
@@ -25,7 +45,13 @@
|
||||
{% elif not this_week and not next_week %}
|
||||
<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>
|
||||
{% elif kind_filter == "training" %}
|
||||
<p class="text-sm text-muted">{% trans "No practices in the next two weeks." %}</p>
|
||||
{% else %}
|
||||
<p class="text-sm text-muted">{% trans "Nothing scheduled in the next two weeks." %}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
|
||||
@@ -9,19 +9,17 @@
|
||||
"Payments & dues" have no screen to lead to and are omitted, same for the
|
||||
mockup's "Coach mode" promo (base.html's own precedent -- no Coach mode
|
||||
screens exist yet, so it's never rendered, not even as a dead/inert link).
|
||||
The avatar/name/subtitle row lives in header_extra -- merged into the
|
||||
shared navy app-header (base.html) rather than a separately-coloured
|
||||
block of its own, matching the design canvas's own M5 markup.
|
||||
{% endcomment %}
|
||||
|
||||
{% block content %}
|
||||
{% if not managed_people %}
|
||||
<div class="m-card p-6 text-center">
|
||||
<p class="font-display text-lg font-extrabold text-ink uppercase">{% trans "No one to show yet" %}</p>
|
||||
<p class="mt-1 text-sm text-muted">{% trans "Once you're linked to a member record, your profile will show up here." %}</p>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="-mx-4 -mt-4 flex items-center gap-3 bg-ink px-4 pt-4 pb-5 text-white">
|
||||
{% block header_extra %}
|
||||
{% if managed_people %}
|
||||
<div class="mt-3 flex items-center gap-3">
|
||||
{% include "mobile/_avatar.html" with person=me size_class="h-14 w-14" text_class="text-xl" bg_class="bg-club" %}
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="font-display text-xl leading-none font-extrabold uppercase">{{ me.get_full_name }}</div>
|
||||
<div class="font-display text-xl leading-none font-extrabold text-white uppercase">{{ me.get_full_name }}</div>
|
||||
{% if member_since or team_manager_label %}
|
||||
<div class="mt-1.5 text-[13px] text-on-dark-dim">
|
||||
{% if member_since %}{% blocktrans with year=member_since|date:"Y" %}Member since {{ year }}{% endblocktrans %}{% endif %}
|
||||
@@ -31,7 +29,16 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock header_extra %}
|
||||
|
||||
{% block content %}
|
||||
{% if not managed_people %}
|
||||
<div class="m-card p-6 text-center">
|
||||
<p class="font-display text-lg font-extrabold text-ink uppercase">{% trans "No one to show yet" %}</p>
|
||||
<p class="mt-1 text-sm text-muted">{% trans "Once you're linked to a member record, your profile will show up here." %}</p>
|
||||
</div>
|
||||
{% else %}
|
||||
<div>
|
||||
<div class="mb-2 font-display text-xs font-extrabold tracking-wide text-muted uppercase">{% trans "People I manage" %}</div>
|
||||
<div class="m-card overflow-hidden">
|
||||
|
||||
@@ -596,6 +596,13 @@ class CalendarViewTests(TestCase):
|
||||
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
def test_header_shows_a_calendar_title_merged_into_the_shared_header(self):
|
||||
self.client.force_login(self.user)
|
||||
|
||||
response = self._get()
|
||||
|
||||
self.assertContains(response, '<span class="font-display text-2xl font-extrabold text-white uppercase">Calendar</span>')
|
||||
|
||||
def test_shows_only_events_the_managed_people_are_invited_to(self):
|
||||
invited = self.make_event(title="Lars's practice")
|
||||
not_invited = self.make_event(title="Someone else's practice")
|
||||
@@ -637,6 +644,39 @@ class CalendarViewTests(TestCase):
|
||||
|
||||
self.assertNotContains(response, 'href="?as=')
|
||||
|
||||
def test_games_filter_shows_only_games(self):
|
||||
game = self.make_event(title="vs Leuven", kind=Event.EventKind.GAME)
|
||||
practice = self.make_event(title="Ice 3", kind=Event.EventKind.TRAINING)
|
||||
Attendance.objects.create(event=game, member=self.member)
|
||||
Attendance.objects.create(event=practice, member=self.member)
|
||||
self.client.force_login(self.user)
|
||||
|
||||
response = self._get(kind="game")
|
||||
|
||||
self.assertEqual(self._events_in_context(response), {game})
|
||||
self.assertEqual(response.context["kind_filter"], "game")
|
||||
|
||||
def test_practices_filter_shows_only_practices(self):
|
||||
game = self.make_event(title="vs Leuven", kind=Event.EventKind.GAME)
|
||||
practice = self.make_event(title="Ice 3", kind=Event.EventKind.TRAINING)
|
||||
Attendance.objects.create(event=game, member=self.member)
|
||||
Attendance.objects.create(event=practice, member=self.member)
|
||||
self.client.force_login(self.user)
|
||||
|
||||
response = self._get(kind="training")
|
||||
|
||||
self.assertEqual(self._events_in_context(response), {practice})
|
||||
|
||||
def test_unrecognized_kind_falls_back_to_all(self):
|
||||
event = self.make_event(title="Social night", kind=Event.EventKind.SOCIAL)
|
||||
Attendance.objects.create(event=event, member=self.member)
|
||||
self.client.force_login(self.user)
|
||||
|
||||
response = self._get(kind="bogus")
|
||||
|
||||
self.assertEqual(self._events_in_context(response), {event})
|
||||
self.assertEqual(response.context["kind_filter"], "")
|
||||
|
||||
def test_no_managed_people_shows_a_graceful_empty_state(self):
|
||||
bare_user = User.objects.create_user(email="new@example.com", password="pw-secret-123")
|
||||
self.client.force_login(bare_user)
|
||||
@@ -1092,6 +1132,13 @@ class MeViewTests(TestCase):
|
||||
self.assertContains(response, "Lars Bakker")
|
||||
self.assertContains(response, "(me)")
|
||||
|
||||
def test_header_is_merged_into_the_shared_navy_header_not_a_separate_block(self):
|
||||
self.client.force_login(self.user)
|
||||
|
||||
response = self._get()
|
||||
|
||||
self.assertNotContains(response, "bg-ink")
|
||||
|
||||
def test_every_managed_child_appears_in_the_list(self):
|
||||
self.client.force_login(self.user)
|
||||
|
||||
|
||||
@@ -282,16 +282,22 @@ class CalendarView(PersonScopeMixin, LoginRequiredMixin, TemplateView):
|
||||
|
||||
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
|
||||
just "what is my family invited to", full stop. (The design mock's own
|
||||
"All members"/list-vs-month/games-only controls aren't built -- they'd
|
||||
need real functionality behind them, not just markup; flagged rather than
|
||||
faked.)
|
||||
just "what is my family invited to", full stop. A ``?kind=`` filter (All/
|
||||
Games/Practices, the two dominant event kinds) narrows that down -- a
|
||||
real, working version of the design mock's own "Games only" pill, not
|
||||
reproducing its List/Month toggle (that needs a whole second view mode,
|
||||
not a filter) or its per-person "All members" (removed on purpose, see
|
||||
above).
|
||||
"""
|
||||
|
||||
template_name = "mobile/calendar.html"
|
||||
screen_title = _("Calendar")
|
||||
active_tab = "calendar"
|
||||
|
||||
#: ?kind= values this screen actually understands, mapped to the Event.EventKind
|
||||
#: they filter on -- anything else (including no param at all) means "All".
|
||||
KIND_FILTERS = {"game": Event.EventKind.GAME, "training": Event.EventKind.TRAINING}
|
||||
|
||||
#: Pill styling for a per-person RSVP status (assets/mobile.css's .pill-*).
|
||||
STATUS_PILL_CLASSES = {
|
||||
Attendance.AttendanceStatus.PRESENT: "pill-ok",
|
||||
@@ -308,6 +314,7 @@ class CalendarView(PersonScopeMixin, LoginRequiredMixin, TemplateView):
|
||||
_this_week_start, this_week_end = week_bounds(timezone.localdate())
|
||||
next_week_end = this_week_end + datetime.timedelta(days=7)
|
||||
window_end = timezone.make_aware(datetime.datetime.combine(next_week_end, datetime.time.max))
|
||||
kind_filter = self.request.GET.get("kind")
|
||||
|
||||
rows = []
|
||||
if self.managed_people:
|
||||
@@ -323,6 +330,9 @@ class CalendarView(PersonScopeMixin, LoginRequiredMixin, TemplateView):
|
||||
.prefetch_related("event__teams")
|
||||
.order_by("event__start")
|
||||
)
|
||||
if kind_filter in self.KIND_FILTERS:
|
||||
attendances = attendances.filter(event__kind=self.KIND_FILTERS[kind_filter])
|
||||
|
||||
# Only worth naming whose row it is once there's more than one managed
|
||||
# person to tell apart -- a lone member's own agenda doesn't need it.
|
||||
show_member = len(self.managed_people) > 1
|
||||
@@ -336,7 +346,7 @@ class CalendarView(PersonScopeMixin, LoginRequiredMixin, TemplateView):
|
||||
bucket = this_week if timezone.localtime(row["event"].start).date() <= this_week_end else next_week
|
||||
bucket.append(row)
|
||||
|
||||
return super().get_context_data(this_week=this_week, next_week=next_week, **kwargs)
|
||||
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)
|
||||
|
||||
|
||||
class EventDetailView(PersonScopeMixin, LoginRequiredMixin, TemplateView):
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user