Add a Declined chip to the bench attendance sheet, drop silent's yellow tint

Silent and declined are both left out of the default "Responded" view, but
only silent had its own chip to review -- declined now gets one too. The
silent-row background tint is gone; nothing else on this screen tints rows,
and the chip itself already says which bucket you're looking at.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-23 12:34:38 +02:00
parent 15d890a24b
commit d0e9dde8ed
3 changed files with 33 additions and 3 deletions

View File

@@ -169,8 +169,9 @@ class CoachAttendanceView(CoachScopeMixin, LoginRequiredMixin, TemplateView):
#: param) means "Responded" (IN_STATUSES: present/selected/maybe), the
#: default view. A coach doesn't need to check in someone silent or
#: declined -- neither is expected to show up -- so those are left out of
#: the default rather than needing to be filtered away each time.
FILTERS = {"silent"}
#: the default rather than needing to be filtered away each time; each
#: still gets its own chip to review who's in either bucket.
FILTERS = {"silent", "declined"}
def get_event(self):
if self.active_team is None:
@@ -195,6 +196,8 @@ class CoachAttendanceView(CoachScopeMixin, LoginRequiredMixin, TemplateView):
filter_param = ""
if filter_param == "silent":
rows = [row for row in attendances if row.is_silent]
elif filter_param == "declined":
rows = [row for row in attendances if row.status in OUT_STATUSES]
else:
rows = [row for row in attendances if row.status in IN_STATUSES]
@@ -204,6 +207,7 @@ class CoachAttendanceView(CoachScopeMixin, LoginRequiredMixin, TemplateView):
total_count=len(attendances),
responded_count=sum(1 for row in attendances if row.status in IN_STATUSES),
silent_count=sum(1 for row in attendances if row.is_silent),
declined_count=sum(1 for row in attendances if row.status in OUT_STATUSES),
checked_in_count=sum(1 for row in attendances if row.showed_up is not None),
filter_param=filter_param,
**kwargs,

View File

@@ -31,13 +31,16 @@
<a class="flex h-9 flex-1 items-center justify-center rounded-full font-display text-xs font-extrabold tracking-wide uppercase {% if filter_param == "silent" %}bg-ink text-white{% else %}border border-line bg-white text-muted{% endif %}" href="?filter=silent">
{% trans "Silent" %} {{ silent_count }}
</a>
<a class="flex h-9 flex-1 items-center justify-center rounded-full font-display text-xs font-extrabold tracking-wide uppercase {% if filter_param == "declined" %}bg-ink text-white{% else %}border border-line bg-white text-muted{% endif %}" href="?filter=declined">
{% trans "Declined" %} {{ declined_count }}
</a>
</div>
<form method="post" action="{% url "mobile:coach_attendance" event.pk %}" hx-boost="false">
{% csrf_token %}
<div class="m-card flex flex-col overflow-hidden">
{% for row in rows %}
<div class="flex items-center gap-3 px-4 py-2.5 {% if not forloop.last %}border-b border-rule{% endif %} {% if row.is_silent %}bg-warn-bg{% endif %}" {% if can_manage_active_team %}x-data="{ state: '{{ row.showed_up|yesno:'true,false,' }}' }"{% endif %}>
<div class="flex items-center gap-3 px-4 py-2.5 {% if not forloop.last %}border-b border-rule{% endif %}" {% if can_manage_active_team %}x-data="{ state: '{{ row.showed_up|yesno:'true,false,' }}' }"{% endif %}>
<div class="w-8 shrink-0 text-center font-display text-lg font-extrabold text-ink tabular-nums">{{ row.membership.jersey_number|default:"—" }}</div>
<div class="min-w-0 flex-1">
<div class="text-sm font-semibold text-ink">{{ row.member.get_full_name }}</div>

View File

@@ -3113,6 +3113,18 @@ class CoachAttendanceViewTests(TestCase):
self.assertEqual([row.member for row in rows], [self.player])
self.assertEqual(response.context["responded_count"], 1)
def test_declined_filter_narrows_to_declined_rows(self):
declined_member = Member.objects.create(first_name="Cara", last_name="Declined")
TeamMembership.objects.create(team=self.team, member=declined_member, season=self.season)
Attendance.objects.update_or_create(event=self.event, member=declined_member, defaults={"status": Attendance.AttendanceStatus.ABSENT})
self.client.force_login(self.user)
response = self._get(filter="declined")
rows = response.context["rows"]
self.assertEqual([row.member for row in rows], [declined_member])
self.assertEqual(response.context["declined_count"], 1)
def test_the_goalies_chip_is_gone(self):
self.client.force_login(self.user)
@@ -3120,6 +3132,17 @@ class CoachAttendanceViewTests(TestCase):
self.assertNotContains(response, "Goalies")
self.assertContains(response, "Responded")
self.assertContains(response, "Declined")
def test_silent_rows_have_no_special_background(self):
silent_member = Member.objects.create(first_name="Ben", last_name="Silent")
TeamMembership.objects.create(team=self.team, member=silent_member, season=self.season)
Attendance.objects.update_or_create(event=self.event, member=silent_member, defaults={"status": Attendance.AttendanceStatus.NO_RESPONSE})
self.client.force_login(self.user)
response = self._get(filter="silent")
self.assertNotContains(response, "bg-warn-bg")
def test_event_from_another_team_is_not_reachable(self):
other_team = Team.objects.create(club=self.club, name="U14", short_name="U14")