feat(v2): implement scoped player search and detail flows
This commit is contained in:
@ -25,10 +25,8 @@ class PlayerSearchForm(forms.Form):
|
||||
nominal_position = forms.ModelChoiceField(queryset=Position.objects.none(), required=False)
|
||||
inferred_role = forms.ModelChoiceField(queryset=Role.objects.none(), required=False)
|
||||
competition = forms.ModelChoiceField(queryset=Competition.objects.none(), required=False)
|
||||
origin_competition = forms.ModelChoiceField(queryset=Competition.objects.none(), required=False)
|
||||
nationality = forms.ModelChoiceField(queryset=Nationality.objects.none(), required=False)
|
||||
team = forms.ModelChoiceField(queryset=Team.objects.none(), required=False)
|
||||
origin_team = forms.ModelChoiceField(queryset=Team.objects.none(), required=False)
|
||||
season = forms.ModelChoiceField(queryset=Season.objects.none(), required=False)
|
||||
|
||||
age_min = forms.IntegerField(required=False, min_value=0, max_value=60, label="Min age")
|
||||
@ -60,20 +58,6 @@ class PlayerSearchForm(forms.Form):
|
||||
three_pct_max = forms.DecimalField(required=False, min_value=0, decimal_places=2, max_digits=5, label="3P% max")
|
||||
ft_pct_min = forms.DecimalField(required=False, min_value=0, decimal_places=2, max_digits=5, label="FT% min")
|
||||
ft_pct_max = forms.DecimalField(required=False, min_value=0, decimal_places=2, max_digits=5, label="FT% max")
|
||||
efficiency_metric_min = forms.DecimalField(
|
||||
required=False,
|
||||
min_value=0,
|
||||
decimal_places=2,
|
||||
max_digits=6,
|
||||
label="Impact metric min",
|
||||
)
|
||||
efficiency_metric_max = forms.DecimalField(
|
||||
required=False,
|
||||
min_value=0,
|
||||
decimal_places=2,
|
||||
max_digits=6,
|
||||
label="Impact metric max",
|
||||
)
|
||||
|
||||
sort = forms.ChoiceField(choices=SORT_CHOICES, required=False, initial="name_asc")
|
||||
page_size = forms.TypedChoiceField(
|
||||
@ -88,10 +72,8 @@ class PlayerSearchForm(forms.Form):
|
||||
self.fields["nominal_position"].queryset = Position.objects.order_by("code")
|
||||
self.fields["inferred_role"].queryset = Role.objects.order_by("name")
|
||||
self.fields["competition"].queryset = Competition.objects.order_by("name")
|
||||
self.fields["origin_competition"].queryset = Competition.objects.order_by("name")
|
||||
self.fields["nationality"].queryset = Nationality.objects.order_by("name")
|
||||
self.fields["team"].queryset = Team.objects.order_by("name")
|
||||
self.fields["origin_team"].queryset = Team.objects.order_by("name")
|
||||
self.fields["season"].queryset = Season.objects.order_by("-start_date")
|
||||
|
||||
def clean(self):
|
||||
@ -110,7 +92,6 @@ class PlayerSearchForm(forms.Form):
|
||||
self._validate_min_max(cleaned_data, "fg_pct_min", "fg_pct_max")
|
||||
self._validate_min_max(cleaned_data, "three_pct_min", "three_pct_max")
|
||||
self._validate_min_max(cleaned_data, "ft_pct_min", "ft_pct_max")
|
||||
self._validate_min_max(cleaned_data, "efficiency_metric_min", "efficiency_metric_max")
|
||||
|
||||
if not cleaned_data.get("sort"):
|
||||
cleaned_data["sort"] = "name_asc"
|
||||
|
||||
@ -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),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@ -7,7 +7,7 @@ from apps.scouting.models import FavoritePlayer
|
||||
from apps.stats.models import PlayerSeason
|
||||
|
||||
from .forms import PlayerSearchForm
|
||||
from .models import Player, PlayerCareerEntry
|
||||
from .models import Player
|
||||
from .services.search import (
|
||||
SEARCH_METRIC_SEMANTICS_TEXT,
|
||||
annotate_player_metrics,
|
||||
@ -92,12 +92,6 @@ class PlayerDetailView(DetailView):
|
||||
"competition",
|
||||
"stats",
|
||||
).order_by("-season__start_date", "-id")
|
||||
career_queryset = PlayerCareerEntry.objects.select_related(
|
||||
"team",
|
||||
"competition",
|
||||
"season",
|
||||
"role_snapshot",
|
||||
).order_by("-start_date", "-id")
|
||||
|
||||
return (
|
||||
Player.objects.select_related(
|
||||
@ -108,9 +102,7 @@ class PlayerDetailView(DetailView):
|
||||
"origin_team",
|
||||
)
|
||||
.prefetch_related(
|
||||
"aliases",
|
||||
Prefetch("player_seasons", queryset=season_queryset),
|
||||
Prefetch("career_entries", queryset=career_queryset),
|
||||
)
|
||||
)
|
||||
|
||||
@ -146,7 +138,6 @@ class PlayerDetailView(DetailView):
|
||||
|
||||
context["age"] = calculate_age(player.birth_date)
|
||||
context["current_assignment"] = current_assignment
|
||||
context["career_entries"] = player.career_entries.all()
|
||||
context["season_rows"] = season_rows
|
||||
context["is_favorite"] = False
|
||||
if self.request.user.is_authenticated:
|
||||
|
||||
Reference in New Issue
Block a user