feat(v2): implement scoped player search and detail flows

This commit is contained in:
Alfredo Di Stasio
2026-03-13 14:10:39 +01:00
parent eacff3d25e
commit 6fc583c79f
10 changed files with 137 additions and 129 deletions

View File

@ -14,7 +14,6 @@ from django.db.models import (
Value,
When,
)
from django.db.models.functions import Coalesce
from apps.players.models import Player
from apps.stats.models import PlayerSeason
@ -22,7 +21,8 @@ from apps.stats.models import PlayerSeason
METRIC_SORT_KEYS = {"ppg_desc", "ppg_asc", "mpg_desc", "mpg_asc"}
SEARCH_METRIC_SEMANTICS_TEXT = (
"Search metrics are best eligible values per player (max per metric across eligible player-season rows). "
"With season/team/competition/stat filters, eligibility is scoped by those filters."
"With season/team/competition/stat filters, eligibility is scoped by those filters. "
"When no eligible stat exists in the current filter context, metric cells show '-'."
)
@ -73,8 +73,6 @@ def _season_scope_filter_keys() -> tuple[str, ...]:
"three_pct_max",
"ft_pct_min",
"ft_pct_max",
"efficiency_metric_min",
"efficiency_metric_max",
)
@ -121,7 +119,6 @@ def _apply_player_season_scope_filters(queryset, data: dict):
("fg_pct_min", "fg_pct_max", "stats__fg_pct"),
("three_pct_min", "three_pct_max", "stats__three_pct"),
("ft_pct_min", "ft_pct_max", "stats__ft_pct"),
("efficiency_metric_min", "efficiency_metric_max", "stats__player_efficiency_rating"),
)
for min_key, max_key, field_name in stat_pairs:
queryset = _apply_min_max_filter(queryset, min_key, max_key, field_name, data)
@ -149,11 +146,6 @@ def _build_metric_context_filter(data: dict) -> Q:
("fg_pct_min", "fg_pct_max", "player_seasons__stats__fg_pct"),
("three_pct_min", "three_pct_max", "player_seasons__stats__three_pct"),
("ft_pct_min", "ft_pct_max", "player_seasons__stats__ft_pct"),
(
"efficiency_metric_min",
"efficiency_metric_max",
"player_seasons__stats__player_efficiency_rating",
),
)
for min_key, max_key, field_name in minmax_pairs:
min_value = data.get(min_key)
@ -188,10 +180,6 @@ def filter_players(queryset, data: dict):
queryset = queryset.filter(inferred_role=data["inferred_role"])
if data.get("nationality"):
queryset = queryset.filter(nationality=data["nationality"])
if data.get("origin_competition"):
queryset = queryset.filter(origin_competition=data["origin_competition"])
if data.get("origin_team"):
queryset = queryset.filter(origin_team=data["origin_team"])
queryset = _apply_min_max_filter(queryset, "height_min", "height_max", "height_cm", data)
queryset = _apply_min_max_filter(queryset, "weight_min", "weight_max", "weight_kg", data)
@ -235,47 +223,62 @@ def annotate_player_metrics(queryset, data: dict | None = None):
output_field=FloatField(),
),
),
default=Value(0.0),
default=Value(None),
output_field=FloatField(),
)
return queryset.annotate(
games_played_value=Coalesce(
Max("player_seasons__games_played", filter=context_filter),
Value(0, output_field=IntegerField()),
games_played_value=Max(
"player_seasons__games_played",
filter=context_filter,
output_field=IntegerField(),
),
mpg_value=Coalesce(Max(mpg_expression, filter=context_filter), Value(0.0)),
ppg_value=Coalesce(
Max("player_seasons__stats__points", filter=context_filter),
Value(0, output_field=DecimalField(max_digits=6, decimal_places=2)),
mpg_value=Max(mpg_expression, filter=context_filter),
ppg_value=Max(
"player_seasons__stats__points",
filter=context_filter,
output_field=DecimalField(max_digits=6, decimal_places=2),
),
rpg_value=Coalesce(
Max("player_seasons__stats__rebounds", filter=context_filter),
Value(0, output_field=DecimalField(max_digits=6, decimal_places=2)),
rpg_value=Max(
"player_seasons__stats__rebounds",
filter=context_filter,
output_field=DecimalField(max_digits=6, decimal_places=2),
),
apg_value=Coalesce(
Max("player_seasons__stats__assists", filter=context_filter),
Value(0, output_field=DecimalField(max_digits=6, decimal_places=2)),
apg_value=Max(
"player_seasons__stats__assists",
filter=context_filter,
output_field=DecimalField(max_digits=6, decimal_places=2),
),
spg_value=Coalesce(
Max("player_seasons__stats__steals", filter=context_filter),
Value(0, output_field=DecimalField(max_digits=6, decimal_places=2)),
spg_value=Max(
"player_seasons__stats__steals",
filter=context_filter,
output_field=DecimalField(max_digits=6, decimal_places=2),
),
bpg_value=Coalesce(
Max("player_seasons__stats__blocks", filter=context_filter),
Value(0, output_field=DecimalField(max_digits=6, decimal_places=2)),
bpg_value=Max(
"player_seasons__stats__blocks",
filter=context_filter,
output_field=DecimalField(max_digits=6, decimal_places=2),
),
top_efficiency=Coalesce(
Max("player_seasons__stats__player_efficiency_rating", filter=context_filter),
Value(0, output_field=DecimalField(max_digits=6, decimal_places=2)),
tov_value=Max(
"player_seasons__stats__turnovers",
filter=context_filter,
output_field=DecimalField(max_digits=6, decimal_places=2),
),
fg_pct_value=Max(
"player_seasons__stats__fg_pct",
filter=context_filter,
output_field=DecimalField(max_digits=5, decimal_places=2),
),
three_pct_value=Max(
"player_seasons__stats__three_pct",
filter=context_filter,
output_field=DecimalField(max_digits=5, decimal_places=2),
),
ft_pct_value=Max(
"player_seasons__stats__ft_pct",
filter=context_filter,
output_field=DecimalField(max_digits=5, decimal_places=2),
),
)