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 <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,17 @@
|
|||||||
{% endblock actions %}
|
{% endblock actions %}
|
||||||
|
|
||||||
{% block panel %}
|
{% block panel %}
|
||||||
|
<form method="get" class="mb-2 flex flex-wrap items-center gap-2">
|
||||||
|
<label class="input">
|
||||||
|
<span class="opacity-50">{% lucide "search" size=16 %}</span>
|
||||||
|
<input type="search" name="q" value="{{ search }}" placeholder="{% trans 'Search by parent or child name ...' %}" class="input input-bordered w-full max-w-xs">
|
||||||
|
</label>
|
||||||
|
<button class="btn btn-outline gap-2" type="submit">{% lucide "search" size=16 %} {% trans "Search" %}</button>
|
||||||
|
{% if search %}
|
||||||
|
<a class="btn gap-2" href="{% url "management:family_list" %}">{% lucide "x" size=16 %} {% trans "Clear filter" %}</a>
|
||||||
|
{% endif %}
|
||||||
|
</form>
|
||||||
|
|
||||||
<div class="card bg-base-100 shadow">
|
<div class="card bg-base-100 shadow">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
|
|||||||
@@ -5896,6 +5896,45 @@ class FamilyListViewTests(ManagementTestBase):
|
|||||||
|
|
||||||
self.assertNotContains(response, "The Smiths")
|
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):
|
class SidebarCounterTests(ManagementTestBase):
|
||||||
"""The nav's two admin-only badges -- pending parent claims, and upcoming
|
"""The nav's two admin-only badges -- pending parent claims, and upcoming
|
||||||
|
|||||||
@@ -1376,10 +1376,20 @@ class FamilyListView(ClubStaffRequiredMixin, ListView):
|
|||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
visible = members_visible_to(self.request.user, self.request.club, include_guardians=True)
|
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()
|
return families_of_club(self.request.club).filter(memberships__member__in=visible).distinct()
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
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"])
|
families = list(context["families"])
|
||||||
|
|
||||||
visible = members_visible_to(self.request.user, self.request.club, include_guardians=True)
|
visible = members_visible_to(self.request.user, self.request.club, include_guardians=True)
|
||||||
|
|||||||
Reference in New Issue
Block a user