from __future__ import annotations from decimal import Decimal from django.core.paginator import Paginator from django.db.models import Exists, OuterRef, Prefetch from django.shortcuts import get_object_or_404, render from .forms import PlayerSearchForm from .models import Player, PlayerSeason PAGE_SIZE = 20 PLAYER_SORTS = { "name_asc", "name_desc", "age_youngest", "height_desc", "weight_desc", } CONTEXT_SORTS = { "points_desc": "points", "assists_desc": "assists", "ts_pct_desc": "ts_pct", "blocks_desc": "blocks", } def sort_players(players, sort_key: str, context_filters_used: bool): if sort_key not in PLAYER_SORTS | set(CONTEXT_SORTS): sort_key = "name_asc" if sort_key == "name_asc": players.sort(key=lambda player: player.full_name.casefold()) return sort_key if sort_key == "name_desc": players.sort(key=lambda player: player.full_name.casefold(), reverse=True) return sort_key if sort_key == "age_youngest": players.sort( key=lambda player: ( player.birth_date is None, -(player.birth_date.toordinal()) if player.birth_date else 0, player.full_name.casefold(), ) ) return sort_key if sort_key == "height_desc": players.sort( key=lambda player: ( player.height_cm is None, -(player.height_cm or Decimal("0")), player.full_name.casefold(), ) ) return sort_key if sort_key == "weight_desc": players.sort( key=lambda player: ( player.weight_kg is None, -(player.weight_kg or Decimal("0")), player.full_name.casefold(), ) ) return sort_key if not context_filters_used: players.sort(key=lambda player: player.full_name.casefold()) return "name_asc" stat_name = CONTEXT_SORTS[sort_key] players.sort( key=lambda player: ( getattr(getattr(getattr(player, "matching_context", None), "stats", None), stat_name, None) is None, -( getattr(getattr(getattr(player, "matching_context", None), "stats", None), stat_name) or Decimal("0") ), player.full_name.casefold(), ) ) return sort_key def player_list(request): form = PlayerSearchForm(request.GET or None) queryset = ( Player.objects.all() .prefetch_related("roles", "specialties") .order_by("full_name") ) context_filters_used = False requested_sort = request.GET.get("sort") or "name_asc" if form.is_valid(): data = form.cleaned_data requested_sort = data["sort"] or "name_asc" if data["name"]: queryset = queryset.filter(full_name__icontains=data["name"]) if data["position"]: queryset = queryset.filter(position=data["position"]) if data["role"]: queryset = queryset.filter(roles=data["role"]) if data["specialty"]: queryset = queryset.filter(specialties=data["specialty"]) if data["min_height_cm"] is not None: queryset = queryset.filter(height_cm__gte=data["min_height_cm"]) if data["max_height_cm"] is not None: queryset = queryset.filter(height_cm__lte=data["max_height_cm"]) if data["min_weight_kg"] is not None: 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_age"] is not None: cutoff = form.birth_date_upper_bound_for_age(data["min_age"]) queryset = queryset.filter(birth_date__lte=cutoff) if data["max_age"] is not None: cutoff = form.birth_date_lower_bound_for_age(data["max_age"]) queryset = queryset.filter(birth_date__gte=cutoff) context_filters_used = any( data[field] is not None and data[field] != "" for field in [ "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", ] ) if context_filters_used: context_qs = PlayerSeason.objects.filter(player=OuterRef("pk")) matching_contexts = PlayerSeason.objects.all() if data["competition"]: context_qs = context_qs.filter(competition=data["competition"]) matching_contexts = matching_contexts.filter(competition=data["competition"]) if data["season"]: context_qs = context_qs.filter(season=data["season"]) matching_contexts = matching_contexts.filter(season=data["season"]) if data["team"]: context_qs = context_qs.filter(team=data["team"]) matching_contexts = matching_contexts.filter(team=data["team"]) stats_filters_used = any( data[field] is not None for field in [ "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", ] ) if stats_filters_used: context_qs = context_qs.filter(stats__isnull=False) matching_contexts = matching_contexts.filter(stats__isnull=False) if data["min_points"] is not None: context_qs = context_qs.filter(stats__points__gte=data["min_points"]) matching_contexts = matching_contexts.filter(stats__points__gte=data["min_points"]) if data["min_assists"] is not None: context_qs = context_qs.filter(stats__assists__gte=data["min_assists"]) matching_contexts = matching_contexts.filter(stats__assists__gte=data["min_assists"]) if data["min_steals"] is not None: context_qs = context_qs.filter(stats__steals__gte=data["min_steals"]) matching_contexts = matching_contexts.filter(stats__steals__gte=data["min_steals"]) if data["max_turnovers"] is not None: context_qs = context_qs.filter(stats__turnovers__lte=data["max_turnovers"]) matching_contexts = matching_contexts.filter(stats__turnovers__lte=data["max_turnovers"]) if data["min_blocks"] is not None: context_qs = context_qs.filter(stats__blocks__gte=data["min_blocks"]) matching_contexts = matching_contexts.filter(stats__blocks__gte=data["min_blocks"]) if data["min_efg_pct"] is not None: context_qs = context_qs.filter(stats__efg_pct__gte=data["min_efg_pct"]) matching_contexts = matching_contexts.filter(stats__efg_pct__gte=data["min_efg_pct"]) if data["min_ts_pct"] is not None: context_qs = context_qs.filter(stats__ts_pct__gte=data["min_ts_pct"]) matching_contexts = matching_contexts.filter(stats__ts_pct__gte=data["min_ts_pct"]) if data["min_plus_minus"] is not None: context_qs = context_qs.filter(stats__plus_minus__gte=data["min_plus_minus"]) matching_contexts = matching_contexts.filter(stats__plus_minus__gte=data["min_plus_minus"]) if data["min_offensive_rating"] is not None: context_qs = context_qs.filter(stats__offensive_rating__gte=data["min_offensive_rating"]) matching_contexts = matching_contexts.filter( stats__offensive_rating__gte=data["min_offensive_rating"] ) if data["max_defensive_rating"] is not None: context_qs = context_qs.filter(stats__defensive_rating__lte=data["max_defensive_rating"]) matching_contexts = matching_contexts.filter( stats__defensive_rating__lte=data["max_defensive_rating"] ) queryset = queryset.annotate(has_matching_context=Exists(context_qs)).filter(has_matching_context=True) # Reuse the same filtered PlayerSeason scope and take the first ordered row # so the displayed context is deterministic and tied to the actual match. matching_contexts = ( matching_contexts.select_related("season", "team", "competition", "stats") .order_by("-season__start_year", "team__name", "competition__name", "pk") ) queryset = queryset.prefetch_related( Prefetch( "player_seasons", queryset=matching_contexts, to_attr="matching_contexts", ) ) queryset = queryset.distinct() players = list(queryset) if context_filters_used: for player in players: player.matching_context = next(iter(player.matching_contexts), None) active_sort = sort_players(players, requested_sort, context_filters_used) paginator = Paginator(players, PAGE_SIZE) page_obj = paginator.get_page(request.GET.get("page")) query_without_page = request.GET.copy() query_without_page.pop("page", None) return render( request, "scouting/player_list.html", { "form": form, "players": page_obj.object_list, "page_obj": page_obj, "active_sort": active_sort, "total_results": paginator.count, "query_without_page": query_without_page.urlencode(), "context_sorting_enabled": context_filters_used, }, ) def player_detail(request, player_id: int): player = get_object_or_404( Player.objects.prefetch_related("roles", "specialties"), pk=player_id, ) contexts = ( PlayerSeason.objects.filter(player=player) .select_related("season", "team", "competition", "stats") .order_by("-season__start_year", "team__name", "competition__name") ) return render( request, "scouting/player_detail.html", { "player": player, "contexts": contexts, }, )