From 9901a90266c1e7d64481525d97ec38f69ff8a5e5 Mon Sep 17 00:00:00 2001 From: Bernard Siebens Date: Wed, 12 Aug 2026 08:57:30 +0200 Subject: [PATCH] Add a name search box to the Families list Matches a first or last name against any visible member on the family -- parent/guardian or child -- narrowing the same members_visible_to() set the unfiltered list already uses, so a match still respects who the requester is allowed to see. Pagination was already wired up; the shared pager already preserves ?q= on page links. Co-Authored-By: Claude Sonnet 5 --- .../templates/management/family_list.html | 11 ++++++ management/tests.py | 39 +++++++++++++++++++ management/views.py | 12 +++++- 3 files changed, 61 insertions(+), 1 deletion(-) diff --git a/management/templates/management/family_list.html b/management/templates/management/family_list.html index 9e74301..38f848b 100644 --- a/management/templates/management/family_list.html +++ b/management/templates/management/family_list.html @@ -11,6 +11,17 @@ {% endblock actions %} {% block panel %} +
+ + + {% if search %} + {% lucide "x" size=16 %} {% trans "Clear filter" %} + {% endif %} +
+
diff --git a/management/tests.py b/management/tests.py index 2b5d0db..9355dcf 100644 --- a/management/tests.py +++ b/management/tests.py @@ -5896,6 +5896,45 @@ class FamilyListViewTests(ManagementTestBase): self.assertNotContains(response, "The Smiths") + def test_search_matches_a_parents_name(self): + response = self.client.get(reverse("management:family_list") + "?q=Pat", HTTP_HOST="ajax-united.rosterchief.app") + + self.assertContains(response, "The Smiths") + + def test_search_matches_a_childs_name(self): + response = self.client.get(reverse("management:family_list") + "?q=Cody", HTTP_HOST="ajax-united.rosterchief.app") + + self.assertContains(response, "The Smiths") + + def test_search_matches_a_last_name(self): + response = self.client.get(reverse("management:family_list") + "?q=Smith", HTTP_HOST="ajax-united.rosterchief.app") + + self.assertContains(response, "The Smiths") + + def test_search_with_no_match_excludes_the_family(self): + response = self.client.get(reverse("management:family_list") + "?q=Nobody", HTTP_HOST="ajax-united.rosterchief.app") + + self.assertNotContains(response, "The Smiths") + + def test_search_term_is_kept_in_the_input(self): + response = self.client.get(reverse("management:family_list") + "?q=Pat", HTTP_HOST="ajax-united.rosterchief.app") + + self.assertContains(response, 'value="Pat"') + + def test_search_still_respects_visibility(self): + # A search term matching someone real must not surface a family the + # requester otherwise has no reason to see. + coach_user = User.objects.create_user(email="coach-fam2@example.com", password="pw-secret-123") + coach_member = Member.objects.create(user=coach_user, first_name="Cara", last_name="Coach") + team = Team.objects.create(club=self.club, name="U12", short_name="U12") + position = Position.objects.create(club=self.club, name="Coach-fam2", short_name="CF2", staff_position=True, management_position=True) + StaffAssignment.objects.create(team=team, member=coach_member, season=self.season, position=position) + self.client.force_login(coach_user) + + response = self.client.get(reverse("management:family_list") + "?q=Pat", HTTP_HOST="ajax-united.rosterchief.app") + + self.assertNotContains(response, "The Smiths") + class SidebarCounterTests(ManagementTestBase): """The nav's two admin-only badges -- pending parent claims, and upcoming diff --git a/management/views.py b/management/views.py index f05da2b..ba4018f 100644 --- a/management/views.py +++ b/management/views.py @@ -1376,10 +1376,20 @@ class FamilyListView(ClubStaffRequiredMixin, ListView): def get_queryset(self): visible = members_visible_to(self.request.user, self.request.club, include_guardians=True) + + # ?q= matches a first or last name of any member on the family -- + # parent/guardian or child alike, since the person being searched for + # could be either. Narrowing the already-visible set first (rather than + # filtering families on a second, independent membership join) means a + # match still respects the same visibility rule as the unfiltered list. + search = self.request.GET.get("q", "").strip() + if search: + visible = visible.filter(Q(first_name__icontains=search) | Q(last_name__icontains=search)) + return families_of_club(self.request.club).filter(memberships__member__in=visible).distinct() def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) + context = super().get_context_data(search=self.request.GET.get("q", ""), **kwargs) families = list(context["families"]) visible = members_visible_to(self.request.user, self.request.club, include_guardians=True)