Add a name search box to Add players, explain what "Suggested" means
"Suggested" is whoever was on this team's roster last season -- now spelled out in a caption under the chips (same for "No team"), not just left for a coach to guess at. The search box (?q=, ANDed with whichever filter chip is active) narrows the eligible pool by first/last name, useful once a club's pool of eligible members outgrows a single screenful. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -9,7 +9,7 @@ import datetime
|
||||
|
||||
from django import forms
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.db.models import Case, F, When
|
||||
from django.db.models import Case, F, Q, When
|
||||
from django.http import Http404, HttpResponseForbidden, HttpResponseRedirect
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.urls import reverse
|
||||
@@ -438,10 +438,10 @@ class CoachAddPlayerView(CoachScopeMixin, LoginRequiredMixin, TemplateView):
|
||||
candidate rather than management.forms.TeamMembershipForm's one-member-
|
||||
at-a-time shape, which doesn't fit a "tap a few names, add them" flow
|
||||
anyway (the mock itself shows plain checkboxes, no inline position
|
||||
picker). A coach sets jersey number/position afterward on the desktop --
|
||||
same as any roster spot added blank via the Sign-up page today
|
||||
(TeamMembership.position's own help_text already documents this as a
|
||||
normal, expected state, not a shortcut this screen invents).
|
||||
picker). A player added here starts with no jersey number/position set --
|
||||
the coach picks those afterward from the player's own row on the Squad
|
||||
screen (CoachRosterMemberView), same "blank is a normal, expected state"
|
||||
TeamMembership.position's own help_text already documents.
|
||||
|
||||
The pool is teams.services.eligible_roster_members(club) minus whoever's
|
||||
already on this team+season -- the same two rules TeamMembershipForm
|
||||
@@ -449,7 +449,10 @@ class CoachAddPlayerView(CoachScopeMixin, LoginRequiredMixin, TemplateView):
|
||||
"Suggested" (on this team last season) is real, computed data. "Age
|
||||
eligible" from the mock isn't built -- neither Club nor Team carries an
|
||||
age-group field to compare a birth date against, so faking that filter
|
||||
would just mean it silently matched nothing.
|
||||
would just mean it silently matched nothing. A plain first/last-name
|
||||
search (?q=) narrows the pool further, ANDed with whichever filter chip
|
||||
is active -- useful once a club's eligible-member pool outgrows a single
|
||||
screenful.
|
||||
"""
|
||||
|
||||
template_name = "mobile/coach/add_player.html"
|
||||
@@ -477,6 +480,7 @@ class CoachAddPlayerView(CoachScopeMixin, LoginRequiredMixin, TemplateView):
|
||||
filter_param = self.request.GET.get("filter")
|
||||
if filter_param not in self.FILTERS:
|
||||
filter_param = ""
|
||||
search_query = self.request.GET.get("q", "").strip()
|
||||
|
||||
if self.active_team is not None and season is not None:
|
||||
squad_count = TeamMembership.objects.filter(team=self.active_team, season=season).count()
|
||||
@@ -488,12 +492,16 @@ class CoachAddPlayerView(CoachScopeMixin, LoginRequiredMixin, TemplateView):
|
||||
previous_season = Season.before(self.request.club, season)
|
||||
pool = pool.filter(team_memberships__team=self.active_team, team_memberships__season=previous_season) if previous_season is not None else pool.none()
|
||||
|
||||
if search_query:
|
||||
pool = pool.filter(Q(first_name__icontains=search_query) | Q(last_name__icontains=search_query))
|
||||
|
||||
candidates = list(pool.distinct().order_by("last_name", "first_name"))
|
||||
|
||||
return super().get_context_data(
|
||||
candidates=candidates,
|
||||
squad_count=squad_count,
|
||||
filter_param=filter_param,
|
||||
search_query=search_query,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{% extends "mobile/coach/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load i18n lucide %}
|
||||
|
||||
{% comment %}
|
||||
C6 -- design_handoff_rosterchief_platform/README.md's C6 section: a
|
||||
@@ -16,18 +16,30 @@
|
||||
{% endblock header_extra %}
|
||||
|
||||
{% block content %}
|
||||
<form method="get" class="relative">
|
||||
{% if filter_param %}<input type="hidden" name="filter" value="{{ filter_param }}">{% endif %}
|
||||
<span class="pointer-events-none absolute top-1/2 left-3 -translate-y-1/2 text-dim">{% lucide "search" size=16 %}</span>
|
||||
<input type="search" name="q" value="{{ search_query }}" placeholder="{% trans "Search by name" %}" class="h-11 w-full rounded-lg border border-stroke bg-paper pr-3 pl-9 text-[15px] text-ink placeholder:text-dim focus:border-ink focus:outline-none">
|
||||
</form>
|
||||
|
||||
<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 filter_param %}bg-ink text-white{% else %}border border-line bg-white text-muted{% endif %}" href="?">
|
||||
<a class="flex h-9 flex-1 items-center justify-center rounded-full font-display text-xs font-extrabold tracking-wide uppercase {% if not filter_param %}bg-ink text-white{% else %}border border-line bg-white text-muted{% endif %}" href="?{% if search_query %}q={{ search_query|urlencode }}{% endif %}">
|
||||
{% 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 filter_param == "suggested" %}bg-ink text-white{% else %}border border-line bg-white text-muted{% endif %}" href="?filter=suggested">
|
||||
<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 == "suggested" %}bg-ink text-white{% else %}border border-line bg-white text-muted{% endif %}" href="?filter=suggested{% if search_query %}&q={{ search_query|urlencode }}{% endif %}">
|
||||
{% trans "Suggested" %}
|
||||
</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 == "no_team" %}bg-ink text-white{% else %}border border-line bg-white text-muted{% endif %}" href="?filter=no_team">
|
||||
<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 == "no_team" %}bg-ink text-white{% else %}border border-line bg-white text-muted{% endif %}" href="?filter=no_team{% if search_query %}&q={{ search_query|urlencode }}{% endif %}">
|
||||
{% trans "No team" %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{% if filter_param == "suggested" %}
|
||||
<p class="text-xs text-muted">{% trans "Was on this team's roster last season." %}</p>
|
||||
{% elif filter_param == "no_team" %}
|
||||
<p class="text-xs text-muted">{% trans "Not on any team's roster yet this season." %}</p>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" action="{% url "mobile:coach_add_player" %}" x-data="{ count: 0 }" hx-boost="false">
|
||||
{% csrf_token %}
|
||||
<div class="m-card flex flex-col overflow-hidden">
|
||||
@@ -38,7 +50,7 @@
|
||||
<input class="h-5 w-5 shrink-0 accent-ink" type="checkbox" name="member" value="{{ candidate.pk }}" @change="count = $el.form.querySelectorAll('input[name=member]:checked').length">
|
||||
</label>
|
||||
{% empty %}
|
||||
<div class="px-4 py-6 text-center text-sm text-muted">{% trans "No one matches this filter." %}</div>
|
||||
<div class="px-4 py-6 text-center text-sm text-muted">{% trans "No one matches." %}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -3536,6 +3536,30 @@ class CoachAddPlayerViewTests(TestCase):
|
||||
self.assertEqual(list(candidates), [returning])
|
||||
self.assertNotIn(new_signup, candidates)
|
||||
|
||||
def test_search_matches_first_or_last_name(self):
|
||||
match = self.make_eligible_member(first_name="Zara", last_name="Zenith")
|
||||
other = self.make_eligible_member(first_name="Not", last_name="Matching")
|
||||
self.client.force_login(self.user)
|
||||
|
||||
response = self.client.get(reverse("mobile:coach_add_player") + "?q=zar", HTTP_HOST="ajax-united.rosterchief.app")
|
||||
|
||||
candidates = response.context["candidates"]
|
||||
self.assertIn(match, candidates)
|
||||
self.assertNotIn(other, candidates)
|
||||
|
||||
def test_search_combines_with_the_active_filter(self):
|
||||
previous_season = Season.objects.create(club=self.club, start_date=self.season.start_date - datetime.timedelta(days=365), end_date=self.season.start_date - datetime.timedelta(days=1))
|
||||
returning_match = self.make_eligible_member(first_name="Zara", last_name="Returning")
|
||||
TeamMembership.objects.create(team=self.team, member=returning_match, season=previous_season)
|
||||
returning_no_match = self.make_eligible_member(first_name="Other", last_name="Returning")
|
||||
TeamMembership.objects.create(team=self.team, member=returning_no_match, season=previous_season)
|
||||
self.client.force_login(self.user)
|
||||
|
||||
response = self.client.get(reverse("mobile:coach_add_player") + "?filter=suggested&q=zar", HTTP_HOST="ajax-united.rosterchief.app")
|
||||
|
||||
candidates = response.context["candidates"]
|
||||
self.assertEqual(list(candidates), [returning_match])
|
||||
|
||||
def test_post_adds_selected_members_to_the_roster(self):
|
||||
first = self.make_eligible_member(first_name="First", last_name="Pick")
|
||||
second = self.make_eligible_member(first_name="Second", last_name="Pick")
|
||||
|
||||
Reference in New Issue
Block a user