Improve search quality, ORM efficiency, and filter consistency

This commit is contained in:
Alfredo Di Stasio
2026-03-10 14:37:01 +01:00
parent ceff4bc42c
commit a1ae380fd5
12 changed files with 375 additions and 14 deletions

View File

@ -5,6 +5,7 @@ from django.urls import reverse
from apps.competitions.models import Competition, Season
from apps.players.models import Nationality, Player, Position, Role
from apps.stats.models import PlayerSeason, PlayerSeasonStats
from apps.teams.models import Team
@ -57,3 +58,99 @@ def test_lookup_list_endpoints(client):
def test_api_is_read_only(client):
response = client.post(reverse("api:players"), data={"q": "x"})
assert response.status_code == 403
@pytest.mark.django_db
def test_players_api_search_consistent_with_ui_filters(client):
nationality = Nationality.objects.create(name="Portugal", iso2_code="PT", iso3_code="PRT")
position = Position.objects.create(code="SF", name="Small Forward")
role = Role.objects.create(code="wing", name="Wing")
competition = Competition.objects.create(
name="Liga Betclic",
slug="liga-betclic",
competition_type=Competition.CompetitionType.LEAGUE,
gender=Competition.Gender.MEN,
country=nationality,
)
team = Team.objects.create(name="Porto Hoops", slug="porto-hoops", country=nationality)
season = Season.objects.create(label="2025-2026", start_date=date(2025, 9, 1), end_date=date(2026, 6, 30))
matching = Player.objects.create(
first_name="Tiago",
last_name="Silva",
full_name="Tiago Silva",
birth_date=date(2001, 3, 1),
nationality=nationality,
nominal_position=position,
inferred_role=role,
origin_competition=competition,
origin_team=team,
)
ps = PlayerSeason.objects.create(
player=matching,
season=season,
team=team,
competition=competition,
games_played=10,
minutes_played=320,
)
PlayerSeasonStats.objects.create(
player_season=ps,
points=16.5,
rebounds=5,
assists=3,
steals=1,
blocks=0.5,
turnovers=2,
)
Player.objects.create(
first_name="Pedro",
last_name="Costa",
full_name="Pedro Costa",
birth_date=date(2001, 4, 2),
nationality=nationality,
)
params = {
"origin_competition": competition.id,
"nominal_position": position.id,
"points_per_game_min": "10",
"sort": "ppg_desc",
}
ui_response = client.get(reverse("players:index"), data=params)
api_response = client.get(reverse("api:players"), data=params)
assert ui_response.status_code == 200
assert api_response.status_code == 200
assert list(ui_response.context["players"])[0].id == matching.id
assert api_response.json()["count"] == 1
assert api_response.json()["results"][0]["id"] == matching.id
@pytest.mark.django_db
def test_player_detail_api_includes_origin_fields(client):
nationality = Nationality.objects.create(name="Greece", iso2_code="GR", iso3_code="GRC")
competition = Competition.objects.create(
name="HEBA A1",
slug="heba-a1",
competition_type=Competition.CompetitionType.LEAGUE,
gender=Competition.Gender.MEN,
country=nationality,
)
team = Team.objects.create(name="Athens BC", slug="athens-bc", country=nationality)
player = Player.objects.create(
first_name="Alex",
last_name="Dimitriou",
full_name="Alex Dimitriou",
birth_date=date(2000, 2, 2),
nationality=nationality,
origin_competition=competition,
origin_team=team,
)
response = client.get(reverse("api:player_detail", kwargs={"pk": player.pk}))
assert response.status_code == 200
payload = response.json()
assert payload["origin_competition"] == competition.name
assert payload["origin_team"] == team.name

View File

@ -4,6 +4,8 @@ import pytest
from django.contrib.auth.models import User
from django.urls import reverse
from apps.ingestion.models import IngestionRun
from apps.ingestion.services.sync import run_sync_job
from apps.players.models import Nationality, Player, Position, Role
from apps.scouting.models import SavedSearch
@ -47,3 +49,25 @@ def test_saved_search_run_filters_player_results(client):
assert response.status_code == 200
assert "Marco Rossi" in response.content.decode()
assert "Luca Bianchi" not in response.content.decode()
@pytest.mark.django_db
def test_ingestion_output_is_searchable_in_ui_and_api(settings, client):
settings.PROVIDER_DEFAULT_NAMESPACE = "mvp_demo"
run = run_sync_job(provider_namespace="mvp_demo", job_type=IngestionRun.JobType.FULL_SYNC)
assert run.status == IngestionRun.RunStatus.SUCCESS
player = Player.objects.filter(origin_competition__isnull=False).order_by("id").first()
assert player is not None
assert player.origin_competition_id is not None
params = {"origin_competition": player.origin_competition_id}
ui_response = client.get(reverse("players:index"), data=params)
api_response = client.get(reverse("api:players"), data=params)
assert ui_response.status_code == 200
assert api_response.status_code == 200
ui_ids = {item.id for item in ui_response.context["players"]}
api_ids = {item["id"] for item in api_response.json()["results"]}
assert player.id in ui_ids
assert player.id in api_ids

View File

@ -1,10 +1,12 @@
from datetime import date
import pytest
from django.contrib.auth.models import User
from django.urls import reverse
from apps.competitions.models import Competition, Season
from apps.players.models import Nationality, Player, Position, Role
from apps.scouting.models import FavoritePlayer
from apps.stats.models import PlayerSeason, PlayerSeasonStats
from apps.teams.models import Team
@ -81,3 +83,106 @@ def test_player_search_pagination_preserves_querystring(client):
assert response.status_code == 200
assert response.context["page_obj"].number == 2
@pytest.mark.django_db
def test_player_search_combined_filters_sorting_and_pagination(client):
nationality = Nationality.objects.create(name="Serbia", iso2_code="RS", iso3_code="SRB")
position = Position.objects.create(code="SG", name="Shooting Guard")
role = Role.objects.create(code="scorer", name="Scorer")
season = Season.objects.create(label="2024-2025", start_date=date(2024, 9, 1), end_date=date(2025, 6, 30))
competition = Competition.objects.create(
name="ABA League",
slug="aba-league",
competition_type=Competition.CompetitionType.LEAGUE,
gender=Competition.Gender.MEN,
country=nationality,
)
team = Team.objects.create(name="Belgrade BC", slug="belgrade-bc", country=nationality)
players = []
for idx, ppg in enumerate(range(40, 19, -1), start=1):
player = Player.objects.create(
first_name=f"S{idx}",
last_name=f"Guard{idx}",
full_name=f"Serbian Guard {idx}",
birth_date=date(2001, 1, idx),
nationality=nationality,
nominal_position=position,
inferred_role=role,
origin_competition=competition,
origin_team=team,
)
player_season = PlayerSeason.objects.create(
player=player,
season=season,
team=team,
competition=competition,
games_played=20,
minutes_played=600,
)
PlayerSeasonStats.objects.create(
player_season=player_season,
points=ppg,
rebounds=4.0,
assists=3.0,
steals=1.0,
blocks=0.3,
turnovers=2.0,
)
players.append(player)
response = client.get(
reverse("players:index"),
data={
"origin_competition": competition.id,
"nominal_position": position.id,
"sort": "ppg_desc",
"page_size": 20,
"page": 1,
},
)
assert response.status_code == 200
page_items = list(response.context["players"])
assert len(page_items) == 20
assert page_items[0].full_name == players[0].full_name
assert response.context["page_obj"].has_next()
page2 = client.get(
reverse("players:index"),
data={
"origin_competition": competition.id,
"nominal_position": position.id,
"sort": "ppg_desc",
"page_size": 20,
"page": 2,
},
)
assert page2.status_code == 200
page2_items = list(page2.context["players"])
assert [item.full_name for item in page2_items] == [players[20].full_name]
@pytest.mark.django_db
def test_player_search_results_include_favorite_ids(client):
user = User.objects.create_user(username="fav-check", password="pass12345")
client.force_login(user)
nationality = Nationality.objects.create(name="Croatia", iso2_code="HR", iso3_code="HRV")
position = Position.objects.create(code="PG", name="Point Guard")
role = Role.objects.create(code="playmaker", name="Playmaker")
player = Player.objects.create(
first_name="Niko",
last_name="Play",
full_name="Niko Play",
birth_date=date(2002, 5, 5),
nationality=nationality,
nominal_position=position,
inferred_role=role,
)
FavoritePlayer.objects.create(user=user, player=player)
response = client.get(reverse("players:index"))
assert response.status_code == 200
assert player.id in response.context["favorite_player_ids"]