Regroup the management event list to match the member app's agenda look
The desktop "List" view was a flat, paginated <table>. It's now grouped the same way mobile.views.CalendarView already groups the member app's own calendar: "This week" / "Next week", then everything further out under its own month divider -- same algorithm (itertools.groupby over an already- sorted, already-paginated page of events), applied after pagination so the two don't fight each other. "Show past events" mode skips the this/next-week labels (they only make sense for what's ahead) and just groups straight into months, most recent first. Each row keeps the same information and admin actions the table had (kind badge, series icon, teams, location, Edit/Delete) -- just laid out as a list row instead of table cells, in its own reusable partial. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
25
management/templates/management/_event_list_row.html
Normal file
25
management/templates/management/_event_list_row.html
Normal file
@@ -0,0 +1,25 @@
|
||||
{% 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 %}">
|
||||
{{ event.get_kind_display }}
|
||||
</span>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<a class="link link-hover truncate font-semibold text-ink" href="{% url 'management:event_detail' event.pk %}">{{ event.title }}</a>
|
||||
{% if event.series_id %}
|
||||
<span class="shrink-0 text-dim" title="{% trans 'Part of a series' %}" aria-label="{% trans 'Part of a series' %}">{% lucide "repeat" size=12 %}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="mt-0.5 flex flex-wrap items-center gap-x-3 gap-y-0.5 font-mono text-xs text-muted">
|
||||
<span>{{ event.start|date:"D j M, H:i" }}</span>
|
||||
{% if event.teams.all %}<span>{% for team in event.teams.all %}{{ team.short_name }}{% if not forloop.last %}, {% endif %}{% endfor %}</span>{% endif %}
|
||||
{% if event.location %}<span>{{ event.location.name }}</span>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% if event.can_manage %}
|
||||
<div class="flex shrink-0 gap-1">
|
||||
<a class="btn btn-square btn-outline btn-sm" href="{% url 'management:event_detail' event.pk %}" aria-label="{% trans 'Edit' %}">{% lucide "pencil" size=14 %}</a>
|
||||
<button class="btn btn-square btn-sm btn-outline btn-error" type="button" onclick="document.getElementById('{{ event.pk|dom_id:"event_delete_modal" }}').showModal()" aria-label="{% trans 'Delete' %}">{% lucide "trash-2" size=14 %}</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -67,54 +67,30 @@
|
||||
|
||||
{% block panel %}
|
||||
{% if view_mode == "list" %}
|
||||
<div class="card overflow-hidden">
|
||||
<div class="overflow-x-auto overflow-y-visible">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Title" %}</th>
|
||||
<th>{% trans "Kind" %}</th>
|
||||
<th>{% trans "When" %}</th>
|
||||
<th>{% trans "Teams" %}</th>
|
||||
<th>{% trans "Location" %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for event in events %}
|
||||
<tr>
|
||||
<td>
|
||||
<a class="link link-hover font-semibold text-ink" href="{% url 'management:event_detail' event.pk %}">{{ event.title }}</a>
|
||||
{% if event.series_id %}
|
||||
<span class="ml-1 inline-flex align-middle text-dim" title="{% trans 'Part of a series' %}" aria-label="{% trans 'Part of a series' %}">{% lucide "repeat" size=12 %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge badge-sm {% 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>
|
||||
</td>
|
||||
<td class="font-mono text-xs text-ink">{{ event.start|date:"D j M Y H:i" }}</td>
|
||||
<td>{% for team in event.teams.all %}{{ team.short_name }}{% if not forloop.last %}, {% endif %}{% empty %}<span class="text-dim">—</span>{% endfor %}</td>
|
||||
<td>{{ event.location.name|default:"—" }}</td>
|
||||
<td class="text-right">
|
||||
{% if event.can_manage %}
|
||||
<div class="flex justify-end gap-1">
|
||||
<a class="btn btn-outline btn-sm" href="{% url 'management:event_detail' event.pk %}" aria-label="{% trans 'Edit' %}">{% lucide "pencil" size=14 %} {% trans "Edit" %}</a>
|
||||
<button class="btn btn-sm btn-outline btn-error" type="button" onclick="document.getElementById('{{ event.pk|dom_id:"event_delete_modal" }}').showModal()" aria-label="{% trans 'Delete' %}">{% lucide "trash-2" size=14 %} {% trans "Delete" %}</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="6" class="py-8 text-center text-muted">{% trans "No events." %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% if not events %}
|
||||
<div class="card p-8 text-center text-muted">{% trans "No events." %}</div>
|
||||
{% else %}
|
||||
<div class="flex flex-col gap-4">
|
||||
{% if list_this_week %}
|
||||
<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">{% trans "This week" %}</div>
|
||||
{% for event in list_this_week %}{% include "management/_event_list_row.html" %}{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if list_next_week %}
|
||||
<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">{% trans "Next week" %}</div>
|
||||
{% for event in list_next_week %}{% include "management/_event_list_row.html" %}{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% 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 %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% include "management/_pagination.html" %}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ from club.models import Club, ClubMembership, ClubRole, DuesInvoice, FeePayment,
|
||||
from club.services.invoicing import DuesInvoicePDFError
|
||||
from club.services.onboarding import mark_complete
|
||||
from events.models import Attendance, Competition, Event, EventReferee, EventSeries, Location, Opponent, RefereeSignup
|
||||
from events.services.calendar import week_bounds
|
||||
from events.services.rbihf_import import RBIHFImportError
|
||||
from events.services.recurrence import detach_occurrence, generate_occurrences
|
||||
from events.services.referees import add_external_referee
|
||||
@@ -5606,6 +5607,42 @@ class EventManagementTests(ManagementTestBase):
|
||||
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
def test_the_list_groups_into_this_week_next_week_and_month_dividers(self):
|
||||
# Mirrors mobile.views.CalendarView's own This week/Next week/by-month
|
||||
# agenda grouping -- see EventListView._list_groups.
|
||||
_this_week_start, this_week_end = week_bounds(timezone.localdate())
|
||||
this_week_event = Event.objects.create(club=self.club, title="This week event", start=timezone.make_aware(datetime.datetime.combine(this_week_end, datetime.time(18, 0))))
|
||||
this_week_event.teams.add(self.own_team)
|
||||
next_week_event = Event.objects.create(club=self.club, title="Next week event", start=timezone.make_aware(datetime.datetime.combine(this_week_end + datetime.timedelta(days=3), datetime.time(18, 0))))
|
||||
next_week_event.teams.add(self.own_team)
|
||||
later_event = Event.objects.create(club=self.club, title="Later event", start=timezone.make_aware(datetime.datetime.combine(this_week_end + datetime.timedelta(days=60), datetime.time(18, 0))))
|
||||
later_event.teams.add(self.own_team)
|
||||
self.client.force_login(self.own_team_coach)
|
||||
|
||||
response = self.club_get("event_list", params={"view": "list"})
|
||||
|
||||
self.assertEqual([event.title for event in response.context["list_this_week"]], ["This week event"])
|
||||
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.assertContains(response, "This week")
|
||||
self.assertContains(response, "Next week")
|
||||
self.assertContains(response, later_event.start.strftime("%B %Y"))
|
||||
|
||||
def test_show_past_groups_by_month_without_this_next_week_labels(self):
|
||||
past_event = Event.objects.create(club=self.club, title="Past event", start=timezone.now() - datetime.timedelta(days=10))
|
||||
past_event.teams.add(self.own_team)
|
||||
self.client.force_login(self.own_team_coach)
|
||||
|
||||
response = self.club_get("event_list", params={"view": "list", "show_past": "1"})
|
||||
|
||||
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.assertNotContains(response, "This week")
|
||||
|
||||
def test_the_dashboard_only_shows_upcoming_events_for_managed_teams(self):
|
||||
own_event = Event.objects.create(club=self.club, title="My event", start=timezone.now() + datetime.timedelta(days=1))
|
||||
own_event.teams.add(self.own_team)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import itertools
|
||||
from datetime import date, timedelta
|
||||
from decimal import Decimal
|
||||
|
||||
@@ -2471,22 +2472,55 @@ class EventListView(ClubStaffRequiredMixin, ListView):
|
||||
calendar_nav = {"prev": add_months(anchor, -1), "next": add_months(anchor, 1)}
|
||||
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
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
club, user = self.request.club, self.request.user
|
||||
view_mode = self.request.GET.get("view", "calendar")
|
||||
range_kind = self.request.GET.get("range", "week")
|
||||
anchor = self._anchor_date()
|
||||
selected_season = selected_season_from_request(self.request, club)
|
||||
show_past = self.request.GET.get("show_past") == "1"
|
||||
|
||||
calendar, calendar_nav = (None, None)
|
||||
if view_mode == "calendar":
|
||||
calendar, calendar_nav = self._calendar_context(range_kind, anchor, selected_season)
|
||||
|
||||
return super().get_context_data(
|
||||
context = super().get_context_data(
|
||||
seasons=Season.objects.filter(club=club).order_by("-start_date"),
|
||||
selected_season=selected_season,
|
||||
selected_kind=self.request.GET.get("kind", ""),
|
||||
show_past=self.request.GET.get("show_past") == "1",
|
||||
show_past=show_past,
|
||||
event_kinds=Event.EventKind.choices,
|
||||
can_create=is_club_admin(user, club) or teams_managed_by(user, club).exists(),
|
||||
view_mode=view_mode,
|
||||
@@ -2503,6 +2537,11 @@ class EventListView(ClubStaffRequiredMixin, ListView):
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
if view_mode == "list":
|
||||
context["list_this_week"], context["list_next_week"], context["list_months"] = self._list_groups(context["events"], show_past)
|
||||
|
||||
return context
|
||||
|
||||
|
||||
class EventDetailView(ClubStaffRequiredMixin, DetailView):
|
||||
template_name = "management/event_detail.html"
|
||||
|
||||
Reference in New Issue
Block a user