Improve search quality, ORM efficiency, and filter consistency
This commit is contained in:
@ -43,6 +43,8 @@ class PlayerListSerializer(serializers.ModelSerializer):
|
||||
nationality = serializers.CharField(source="nationality.name", allow_null=True)
|
||||
nominal_position = serializers.CharField(source="nominal_position.code", allow_null=True)
|
||||
inferred_role = serializers.CharField(source="inferred_role.name", allow_null=True)
|
||||
origin_competition = serializers.CharField(source="origin_competition.name", allow_null=True)
|
||||
origin_team = serializers.CharField(source="origin_team.name", allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = Player
|
||||
@ -53,6 +55,8 @@ class PlayerListSerializer(serializers.ModelSerializer):
|
||||
"nationality",
|
||||
"nominal_position",
|
||||
"inferred_role",
|
||||
"origin_competition",
|
||||
"origin_team",
|
||||
"height_cm",
|
||||
"weight_kg",
|
||||
"dominant_hand",
|
||||
@ -88,6 +92,8 @@ class PlayerDetailSerializer(serializers.ModelSerializer):
|
||||
nationality = serializers.CharField(source="nationality.name", allow_null=True)
|
||||
nominal_position = serializers.CharField(source="nominal_position.name", allow_null=True)
|
||||
inferred_role = serializers.CharField(source="inferred_role.name", allow_null=True)
|
||||
origin_competition = serializers.CharField(source="origin_competition.name", allow_null=True)
|
||||
origin_team = serializers.CharField(source="origin_team.name", allow_null=True)
|
||||
age = serializers.SerializerMethodField()
|
||||
aliases = serializers.SerializerMethodField()
|
||||
season_stats = serializers.SerializerMethodField()
|
||||
@ -102,6 +108,8 @@ class PlayerDetailSerializer(serializers.ModelSerializer):
|
||||
"nationality",
|
||||
"nominal_position",
|
||||
"inferred_role",
|
||||
"origin_competition",
|
||||
"origin_team",
|
||||
"height_cm",
|
||||
"weight_kg",
|
||||
"dominant_hand",
|
||||
|
||||
@ -5,7 +5,13 @@ from rest_framework.throttling import AnonRateThrottle, UserRateThrottle
|
||||
from apps.competitions.models import Competition, Season
|
||||
from apps.players.forms import PlayerSearchForm
|
||||
from apps.players.models import Player
|
||||
from apps.players.services.search import apply_sorting, base_player_queryset, filter_players
|
||||
from apps.players.services.search import (
|
||||
METRIC_SORT_KEYS,
|
||||
annotate_player_metrics,
|
||||
apply_sorting,
|
||||
base_player_queryset,
|
||||
filter_players,
|
||||
)
|
||||
from apps.teams.models import Team
|
||||
|
||||
from .permissions import ReadOnlyOrDeny
|
||||
@ -38,7 +44,10 @@ class PlayerSearchApiView(ReadOnlyBaseAPIView, generics.ListAPIView):
|
||||
queryset = base_player_queryset()
|
||||
if form.is_valid():
|
||||
queryset = filter_players(queryset, form.cleaned_data)
|
||||
queryset = apply_sorting(queryset, form.cleaned_data.get("sort", "name_asc"))
|
||||
sort_key = form.cleaned_data.get("sort", "name_asc")
|
||||
if sort_key in METRIC_SORT_KEYS:
|
||||
queryset = annotate_player_metrics(queryset)
|
||||
queryset = apply_sorting(queryset, sort_key)
|
||||
else:
|
||||
queryset = queryset.order_by("full_name", "id")
|
||||
return queryset
|
||||
@ -50,6 +59,8 @@ class PlayerDetailApiView(ReadOnlyBaseAPIView, generics.RetrieveAPIView):
|
||||
"nationality",
|
||||
"nominal_position",
|
||||
"inferred_role",
|
||||
"origin_competition",
|
||||
"origin_team",
|
||||
).prefetch_related("aliases")
|
||||
|
||||
|
||||
|
||||
17
apps/players/migrations/0005_player_weight_index.py
Normal file
17
apps/players/migrations/0005_player_weight_index.py
Normal file
@ -0,0 +1,17 @@
|
||||
# Generated by Django 5.2.12 on 2026-03-10 17:05
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("players", "0004_backfill_player_origins"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddIndex(
|
||||
model_name="player",
|
||||
index=models.Index(fields=["weight_kg"], name="players_pla_weight__fb76a4_idx"),
|
||||
),
|
||||
]
|
||||
@ -123,6 +123,7 @@ class Player(TimeStampedModel):
|
||||
models.Index(fields=["origin_team"]),
|
||||
models.Index(fields=["is_active"]),
|
||||
models.Index(fields=["height_cm"]),
|
||||
models.Index(fields=["weight_kg"]),
|
||||
]
|
||||
|
||||
def __str__(self) -> str:
|
||||
|
||||
@ -16,6 +16,8 @@ from django.db.models.functions import Coalesce
|
||||
|
||||
from apps.players.models import Player
|
||||
|
||||
METRIC_SORT_KEYS = {"ppg_desc", "ppg_asc", "mpg_desc", "mpg_asc"}
|
||||
|
||||
|
||||
def _years_ago_today(years: int) -> date:
|
||||
today = date.today()
|
||||
@ -36,6 +38,40 @@ def _apply_min_max_filter(queryset, min_key: str, max_key: str, field_name: str,
|
||||
return queryset
|
||||
|
||||
|
||||
def _needs_distinct(data: dict) -> bool:
|
||||
join_filter_keys = (
|
||||
"q",
|
||||
"team",
|
||||
"competition",
|
||||
"season",
|
||||
"games_played_min",
|
||||
"games_played_max",
|
||||
"minutes_per_game_min",
|
||||
"minutes_per_game_max",
|
||||
"points_per_game_min",
|
||||
"points_per_game_max",
|
||||
"rebounds_per_game_min",
|
||||
"rebounds_per_game_max",
|
||||
"assists_per_game_min",
|
||||
"assists_per_game_max",
|
||||
"steals_per_game_min",
|
||||
"steals_per_game_max",
|
||||
"blocks_per_game_min",
|
||||
"blocks_per_game_max",
|
||||
"turnovers_per_game_min",
|
||||
"turnovers_per_game_max",
|
||||
"fg_pct_min",
|
||||
"fg_pct_max",
|
||||
"three_pct_min",
|
||||
"three_pct_max",
|
||||
"ft_pct_min",
|
||||
"ft_pct_max",
|
||||
"efficiency_metric_min",
|
||||
"efficiency_metric_max",
|
||||
)
|
||||
return any(data.get(key) not in (None, "") for key in join_filter_keys)
|
||||
|
||||
|
||||
def filter_players(queryset, data: dict):
|
||||
query = data.get("q")
|
||||
if query:
|
||||
@ -108,6 +144,12 @@ def filter_players(queryset, data: dict):
|
||||
for min_key, max_key, field_name in stat_pairs:
|
||||
queryset = _apply_min_max_filter(queryset, min_key, max_key, field_name, data)
|
||||
|
||||
if _needs_distinct(data):
|
||||
return queryset.distinct()
|
||||
return queryset
|
||||
|
||||
|
||||
def annotate_player_metrics(queryset):
|
||||
mpg_expression = Case(
|
||||
When(
|
||||
player_seasons__games_played__gt=0,
|
||||
@ -120,7 +162,7 @@ def filter_players(queryset, data: dict):
|
||||
output_field=FloatField(),
|
||||
)
|
||||
|
||||
queryset = queryset.annotate(
|
||||
return queryset.annotate(
|
||||
games_played_value=Coalesce(
|
||||
Max("player_seasons__games_played"),
|
||||
Value(0, output_field=IntegerField()),
|
||||
@ -159,8 +201,6 @@ def filter_players(queryset, data: dict):
|
||||
),
|
||||
)
|
||||
|
||||
return queryset.distinct()
|
||||
|
||||
|
||||
def apply_sorting(queryset, sort_key: str):
|
||||
if sort_key == "name_desc":
|
||||
@ -191,4 +231,4 @@ def base_player_queryset():
|
||||
"inferred_role",
|
||||
"origin_competition",
|
||||
"origin_team",
|
||||
).prefetch_related("aliases")
|
||||
)
|
||||
|
||||
@ -7,8 +7,8 @@ from apps.scouting.models import FavoritePlayer
|
||||
from apps.stats.models import PlayerSeason
|
||||
|
||||
from .forms import PlayerSearchForm
|
||||
from .models import Player
|
||||
from .services.search import apply_sorting, base_player_queryset, filter_players
|
||||
from .models import Player, PlayerCareerEntry
|
||||
from .services.search import annotate_player_metrics, apply_sorting, base_player_queryset, filter_players
|
||||
|
||||
|
||||
def calculate_age(birth_date):
|
||||
@ -48,9 +48,10 @@ class PlayerSearchView(ListView):
|
||||
|
||||
if form.is_valid():
|
||||
queryset = filter_players(queryset, form.cleaned_data)
|
||||
queryset = annotate_player_metrics(queryset)
|
||||
queryset = apply_sorting(queryset, form.cleaned_data.get("sort", "name_asc"))
|
||||
else:
|
||||
queryset = queryset.order_by("full_name", "id")
|
||||
queryset = annotate_player_metrics(queryset).order_by("full_name", "id")
|
||||
|
||||
return queryset
|
||||
|
||||
@ -81,6 +82,12 @@ 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(
|
||||
@ -93,10 +100,7 @@ class PlayerDetailView(DetailView):
|
||||
.prefetch_related(
|
||||
"aliases",
|
||||
Prefetch("player_seasons", queryset=season_queryset),
|
||||
"career_entries__team",
|
||||
"career_entries__competition",
|
||||
"career_entries__season",
|
||||
"career_entries__role_snapshot",
|
||||
Prefetch("career_entries", queryset=career_queryset),
|
||||
)
|
||||
)
|
||||
|
||||
@ -132,7 +136,7 @@ class PlayerDetailView(DetailView):
|
||||
|
||||
context["age"] = calculate_age(player.birth_date)
|
||||
context["current_assignment"] = current_assignment
|
||||
context["career_entries"] = player.career_entries.all().order_by("-start_date", "-id")
|
||||
context["career_entries"] = player.career_entries.all()
|
||||
context["season_rows"] = season_rows
|
||||
context["is_favorite"] = False
|
||||
if self.request.user.is_authenticated:
|
||||
|
||||
@ -0,0 +1,41 @@
|
||||
# Generated by Django 5.2.12 on 2026-03-10 17:05
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("stats", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddIndex(
|
||||
model_name="playerseasonstats",
|
||||
index=models.Index(fields=["steals"], name="stats_playe_steals_59b0f3_idx"),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="playerseasonstats",
|
||||
index=models.Index(fields=["blocks"], name="stats_playe_blocks_b2d4de_idx"),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="playerseasonstats",
|
||||
index=models.Index(fields=["turnovers"], name="stats_playe_turnove_aa4e87_idx"),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="playerseasonstats",
|
||||
index=models.Index(fields=["fg_pct"], name="stats_playe_fg_pct_bf2ff1_idx"),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="playerseasonstats",
|
||||
index=models.Index(fields=["three_pct"], name="stats_playe_three_p_c67201_idx"),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="playerseasonstats",
|
||||
index=models.Index(fields=["ft_pct"], name="stats_playe_ft_pct_da7421_idx"),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="playerseasonstats",
|
||||
index=models.Index(fields=["player_efficiency_rating"], name="stats_playe_player__641815_idx"),
|
||||
),
|
||||
]
|
||||
@ -63,8 +63,15 @@ class PlayerSeasonStats(models.Model):
|
||||
models.Index(fields=["points"]),
|
||||
models.Index(fields=["rebounds"]),
|
||||
models.Index(fields=["assists"]),
|
||||
models.Index(fields=["steals"]),
|
||||
models.Index(fields=["blocks"]),
|
||||
models.Index(fields=["turnovers"]),
|
||||
models.Index(fields=["fg_pct"]),
|
||||
models.Index(fields=["three_pct"]),
|
||||
models.Index(fields=["ft_pct"]),
|
||||
models.Index(fields=["usage_rate"]),
|
||||
models.Index(fields=["true_shooting_pct"]),
|
||||
models.Index(fields=["player_efficiency_rating"]),
|
||||
]
|
||||
|
||||
def __str__(self) -> str:
|
||||
|
||||
Reference in New Issue
Block a user