feat(scouting): add wingspan filters and saved searches mvp
This commit is contained in:
@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from decimal import Decimal
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from django.core.paginator import Paginator
|
||||
from django.contrib.auth.decorators import login_required
|
||||
@ -10,8 +11,8 @@ from django.shortcuts import get_object_or_404, render
|
||||
from django.urls import reverse
|
||||
from django.views.decorators.http import require_POST
|
||||
|
||||
from .forms import PlayerSearchForm
|
||||
from .models import FavoritePlayer, Player, PlayerNote, PlayerSeason
|
||||
from .forms import PlayerSearchForm, SavedSearchForm
|
||||
from .models import FavoritePlayer, Player, PlayerNote, PlayerSeason, SavedSearch
|
||||
|
||||
PAGE_SIZE = 20
|
||||
PLAYER_SORTS = {
|
||||
@ -27,6 +28,100 @@ CONTEXT_SORTS = {
|
||||
"ts_pct_desc": "ts_pct",
|
||||
"blocks_desc": "blocks",
|
||||
}
|
||||
SEARCH_PARAM_FIELDS = [
|
||||
"name",
|
||||
"sort",
|
||||
"position",
|
||||
"role",
|
||||
"specialty",
|
||||
"min_age",
|
||||
"max_age",
|
||||
"min_height_cm",
|
||||
"max_height_cm",
|
||||
"min_weight_kg",
|
||||
"max_weight_kg",
|
||||
"min_wingspan_cm",
|
||||
"max_wingspan_cm",
|
||||
"competition",
|
||||
"season",
|
||||
"team",
|
||||
"min_points",
|
||||
"min_assists",
|
||||
"min_steals",
|
||||
"max_turnovers",
|
||||
"min_blocks",
|
||||
"min_efg_pct",
|
||||
"min_ts_pct",
|
||||
"min_plus_minus",
|
||||
"min_offensive_rating",
|
||||
"max_defensive_rating",
|
||||
]
|
||||
FILTER_LABELS = {
|
||||
"name": "Name",
|
||||
"position": "Position",
|
||||
"role": "Role",
|
||||
"specialty": "Specialty",
|
||||
"min_age": "Min age",
|
||||
"max_age": "Max age",
|
||||
"min_height_cm": "Min height (cm)",
|
||||
"max_height_cm": "Max height (cm)",
|
||||
"min_weight_kg": "Min weight (kg)",
|
||||
"max_weight_kg": "Max weight (kg)",
|
||||
"min_wingspan_cm": "Min wingspan (cm)",
|
||||
"max_wingspan_cm": "Max wingspan (cm)",
|
||||
"competition": "Competition",
|
||||
"season": "Season",
|
||||
"team": "Team",
|
||||
"min_points": "Min points",
|
||||
"min_assists": "Min assists",
|
||||
"min_steals": "Min steals",
|
||||
"max_turnovers": "Max turnovers",
|
||||
"min_blocks": "Min blocks",
|
||||
"min_efg_pct": "Min eFG%",
|
||||
"min_ts_pct": "Min TS%",
|
||||
"min_plus_minus": "Min +/-",
|
||||
"min_offensive_rating": "Min ORtg",
|
||||
"max_defensive_rating": "Max DRtg",
|
||||
}
|
||||
|
||||
|
||||
def serialize_search_params(cleaned_data: dict) -> dict[str, str]:
|
||||
params = {}
|
||||
for field_name in SEARCH_PARAM_FIELDS:
|
||||
value = cleaned_data.get(field_name)
|
||||
if value in (None, ""):
|
||||
continue
|
||||
if field_name == "sort" and value == "name_asc":
|
||||
continue
|
||||
params[field_name] = str(getattr(value, "pk", value))
|
||||
return params
|
||||
|
||||
|
||||
def read_search_params_from_payload(payload) -> dict[str, str]:
|
||||
params = {}
|
||||
for field_name in SEARCH_PARAM_FIELDS:
|
||||
value = payload.get(field_name)
|
||||
if value in (None, ""):
|
||||
continue
|
||||
params[field_name] = str(value)
|
||||
return params
|
||||
|
||||
|
||||
def build_active_filters(form: PlayerSearchForm, cleaned_data: dict) -> list[dict[str, str]]:
|
||||
active_filters = []
|
||||
for field_name, label in FILTER_LABELS.items():
|
||||
value = cleaned_data.get(field_name)
|
||||
if value in (None, ""):
|
||||
continue
|
||||
field = form.fields[field_name]
|
||||
if getattr(field, "choices", None):
|
||||
value_label = str(dict(field.choices).get(str(value), value))
|
||||
elif hasattr(value, "name"):
|
||||
value_label = value.name
|
||||
else:
|
||||
value_label = str(value)
|
||||
active_filters.append({"label": label, "value": value_label})
|
||||
return active_filters
|
||||
|
||||
|
||||
def apply_favorite_state(players, user):
|
||||
@ -103,6 +198,7 @@ def sort_players(players, sort_key: str, context_filters_used: bool):
|
||||
|
||||
def player_list(request):
|
||||
form = PlayerSearchForm(request.GET or None)
|
||||
saved_search_form = SavedSearchForm()
|
||||
queryset = (
|
||||
Player.objects.all()
|
||||
.prefetch_related("roles", "specialties")
|
||||
@ -110,10 +206,14 @@ def player_list(request):
|
||||
)
|
||||
context_filters_used = False
|
||||
requested_sort = request.GET.get("sort") or "name_asc"
|
||||
current_search_params = read_search_params_from_payload(request.GET)
|
||||
active_filters = []
|
||||
|
||||
if form.is_valid():
|
||||
data = form.cleaned_data
|
||||
requested_sort = data["sort"] or "name_asc"
|
||||
current_search_params = serialize_search_params(data)
|
||||
active_filters = build_active_filters(form, data)
|
||||
|
||||
if data["name"]:
|
||||
queryset = queryset.filter(full_name__icontains=data["name"])
|
||||
@ -132,6 +232,10 @@ def player_list(request):
|
||||
queryset = queryset.filter(weight_kg__gte=data["min_weight_kg"])
|
||||
if data["max_weight_kg"] is not None:
|
||||
queryset = queryset.filter(weight_kg__lte=data["max_weight_kg"])
|
||||
if data["min_wingspan_cm"] is not None:
|
||||
queryset = queryset.filter(wingspan_cm__gte=data["min_wingspan_cm"])
|
||||
if data["max_wingspan_cm"] is not None:
|
||||
queryset = queryset.filter(wingspan_cm__lte=data["max_wingspan_cm"])
|
||||
|
||||
if data["min_age"] is not None:
|
||||
cutoff = form.birth_date_upper_bound_for_age(data["min_age"])
|
||||
@ -253,8 +357,19 @@ def player_list(request):
|
||||
paginator = Paginator(players, PAGE_SIZE)
|
||||
page_obj = paginator.get_page(request.GET.get("page"))
|
||||
apply_favorite_state(page_obj.object_list, request.user)
|
||||
query_without_page = request.GET.copy()
|
||||
query_without_page.pop("page", None)
|
||||
|
||||
saved_searches = []
|
||||
if request.user.is_authenticated:
|
||||
for saved_search in SavedSearch.objects.filter(user=request.user):
|
||||
querystring = urlencode(saved_search.params)
|
||||
saved_searches.append(
|
||||
{
|
||||
"id": saved_search.id,
|
||||
"name": saved_search.name,
|
||||
"querystring": querystring,
|
||||
"is_active": saved_search.params == current_search_params,
|
||||
}
|
||||
)
|
||||
|
||||
return render(
|
||||
request,
|
||||
@ -265,8 +380,13 @@ def player_list(request):
|
||||
"page_obj": page_obj,
|
||||
"active_sort": active_sort,
|
||||
"total_results": paginator.count,
|
||||
"query_without_page": query_without_page.urlencode(),
|
||||
"query_without_page": urlencode(current_search_params),
|
||||
"current_search_params": current_search_params,
|
||||
"has_submitted_search": bool(current_search_params),
|
||||
"active_filters": active_filters,
|
||||
"context_sorting_enabled": context_filters_used,
|
||||
"saved_search_form": saved_search_form,
|
||||
"saved_searches": saved_searches,
|
||||
},
|
||||
)
|
||||
|
||||
@ -355,3 +475,38 @@ def favorites_list(request):
|
||||
"favorites": favorites,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
@require_POST
|
||||
def save_search(request):
|
||||
name_form = SavedSearchForm(request.POST)
|
||||
player_search_form = PlayerSearchForm(request.POST)
|
||||
if not name_form.is_valid():
|
||||
return HttpResponseRedirect(reverse("scouting:player_list"))
|
||||
|
||||
if player_search_form.is_valid():
|
||||
params = serialize_search_params(player_search_form.cleaned_data)
|
||||
else:
|
||||
params = read_search_params_from_payload(request.POST)
|
||||
|
||||
SavedSearch.objects.update_or_create(
|
||||
user=request.user,
|
||||
name=name_form.cleaned_data["saved_search_name"].strip(),
|
||||
defaults={"params": params},
|
||||
)
|
||||
target = reverse("scouting:player_list")
|
||||
if not params:
|
||||
return HttpResponseRedirect(target)
|
||||
return HttpResponseRedirect(f"{target}?{urlencode(params)}")
|
||||
|
||||
|
||||
@login_required
|
||||
@require_POST
|
||||
def delete_saved_search(request, saved_search_id: int):
|
||||
SavedSearch.objects.filter(user=request.user, pk=saved_search_id).delete()
|
||||
query = read_search_params_from_payload(request.POST)
|
||||
target = reverse("scouting:player_list")
|
||||
if not query:
|
||||
return HttpResponseRedirect(target)
|
||||
return HttpResponseRedirect(f"{target}?{urlencode(query)}")
|
||||
|
||||
Reference in New Issue
Block a user