Files
hoopscout-v2/app/scouting/tests.py

414 lines
16 KiB
Python

from datetime import date
from decimal import Decimal
from django.core.management import call_command
from django.test import TestCase
from django.urls import reverse
from .models import Competition, Player, PlayerSeason, PlayerSeasonStats, Role, Season, Specialty, Team
class ScoutingSearchViewsTests(TestCase):
@classmethod
def setUpTestData(cls):
cls.role_playmaker = Role.objects.create(name="playmaker", slug="playmaker")
cls.role_3d = Role.objects.create(name="3-and-D", slug="3-and-d")
cls.specialty_defense = Specialty.objects.create(name="defense", slug="defense")
cls.specialty_offball = Specialty.objects.create(name="off ball", slug="off-ball")
cls.comp_a = Competition.objects.create(name="League A")
cls.comp_b = Competition.objects.create(name="League B")
cls.team_a = Team.objects.create(name="Team A", country="IT")
cls.team_b = Team.objects.create(name="Team B", country="IT")
cls.season_2025 = Season.objects.create(name="2025-2026", start_year=2025, end_year=2026)
cls.season_2024 = Season.objects.create(name="2024-2025", start_year=2024, end_year=2025)
cls.player_pg = Player.objects.create(
full_name="Marco Guard",
birth_date=date(2002, 1, 1),
position="PG",
height_cm=Decimal("188.00"),
weight_kg=Decimal("82.00"),
nationality="IT",
)
cls.player_pg.roles.add(cls.role_playmaker)
cls.player_pg.specialties.add(cls.specialty_defense)
cls.player_wing = Player.objects.create(
full_name="Luca Wing",
birth_date=date(1998, 2, 2),
position="SF",
height_cm=Decimal("201.00"),
weight_kg=Decimal("95.00"),
)
cls.player_wing.roles.add(cls.role_3d)
cls.player_wing.specialties.add(cls.specialty_offball)
cls.ctx_pg_good = PlayerSeason.objects.create(
player=cls.player_pg,
season=cls.season_2025,
team=cls.team_a,
competition=cls.comp_a,
)
PlayerSeasonStats.objects.create(
player_season=cls.ctx_pg_good,
points=Decimal("16.00"),
assists=Decimal("7.50"),
steals=Decimal("1.80"),
turnovers=Decimal("2.10"),
blocks=Decimal("0.30"),
efg_pct=Decimal("53.20"),
ts_pct=Decimal("58.10"),
plus_minus=Decimal("4.20"),
offensive_rating=Decimal("112.00"),
defensive_rating=Decimal("104.00"),
)
cls.ctx_pg_other = PlayerSeason.objects.create(
player=cls.player_pg,
season=cls.season_2024,
team=cls.team_b,
competition=cls.comp_b,
)
PlayerSeasonStats.objects.create(
player_season=cls.ctx_pg_other,
points=Decimal("10.00"),
assists=Decimal("4.00"),
steals=Decimal("1.00"),
turnovers=Decimal("3.50"),
blocks=Decimal("0.20"),
efg_pct=Decimal("48.00"),
ts_pct=Decimal("50.00"),
plus_minus=Decimal("-2.00"),
offensive_rating=Decimal("101.00"),
defensive_rating=Decimal("112.00"),
)
cls.ctx_wing = PlayerSeason.objects.create(
player=cls.player_wing,
season=cls.season_2025,
team=cls.team_b,
competition=cls.comp_a,
)
PlayerSeasonStats.objects.create(
player_season=cls.ctx_wing,
points=Decimal("14.00"),
assists=Decimal("2.00"),
steals=Decimal("1.20"),
turnovers=Decimal("1.90"),
blocks=Decimal("0.70"),
efg_pct=Decimal("55.00"),
ts_pct=Decimal("60.00"),
plus_minus=Decimal("1.50"),
offensive_rating=Decimal("108.00"),
defensive_rating=Decimal("106.00"),
)
def test_player_list_page_loads(self):
response = self.client.get(reverse("scouting:player_list"))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Scout Search")
def test_player_detail_page_loads(self):
response = self.client.get(reverse("scouting:player_detail", args=[self.player_pg.id]))
self.assertEqual(response.status_code, 200)
self.assertContains(response, self.player_pg.full_name)
def test_filter_by_player_level_fields(self):
response = self.client.get(
reverse("scouting:player_list"),
{"position": "PG", "role": self.role_playmaker.id, "specialty": self.specialty_defense.id},
)
self.assertContains(response, self.player_pg.full_name)
self.assertNotContains(response, self.player_wing.full_name)
def test_filter_by_context_fields_and_stats(self):
response = self.client.get(
reverse("scouting:player_list"),
{
"competition": self.comp_a.id,
"season": self.season_2025.id,
"team": self.team_a.id,
"min_ts_pct": "57",
},
)
self.assertContains(response, self.player_pg.full_name)
self.assertNotContains(response, self.player_wing.full_name)
def test_result_row_shows_matching_context_for_filtered_search(self):
response = self.client.get(
reverse("scouting:player_list"),
{
"competition": self.comp_a.id,
"season": self.season_2025.id,
"team": self.team_a.id,
"min_ts_pct": "57",
},
)
player = next(player for player in response.context["players"] if player.id == self.player_pg.id)
self.assertEqual(player.matching_context.id, self.ctx_pg_good.id)
self.assertContains(response, "Match context:")
self.assertContains(response, self.season_2025.name)
self.assertContains(response, "PTS 16.00")
def test_result_row_does_not_show_non_matching_context(self):
response = self.client.get(
reverse("scouting:player_list"),
{
"competition": self.comp_a.id,
"season": self.season_2025.id,
"team": self.team_a.id,
"min_ts_pct": "57",
},
)
player = next(player for player in response.context["players"] if player.id == self.player_pg.id)
self.assertEqual(player.matching_context.id, self.ctx_pg_good.id)
self.assertNotContains(response, "PTS 10.00")
self.assertNotContains(response, "AST 4.00")
def test_no_false_positive_from_different_context_rows(self):
response = self.client.get(
reverse("scouting:player_list"),
{
"team": self.team_b.id,
"season": self.season_2024.id,
"min_ts_pct": "57",
},
)
self.assertNotContains(response, self.player_pg.full_name)
def test_combined_pg_under_age_with_assists(self):
response = self.client.get(
reverse("scouting:player_list"),
{
"position": "PG",
"max_age": "25",
"min_assists": "7",
},
)
self.assertContains(response, self.player_pg.full_name)
self.assertNotContains(response, self.player_wing.full_name)
def test_matching_context_selection_is_deterministic_when_multiple_contexts_match(self):
second_matching_context = PlayerSeason.objects.create(
player=self.player_pg,
season=self.season_2024,
team=self.team_a,
competition=self.comp_a,
)
PlayerSeasonStats.objects.create(
player_season=second_matching_context,
points=Decimal("18.00"),
assists=Decimal("6.20"),
steals=Decimal("1.50"),
turnovers=Decimal("2.00"),
blocks=Decimal("0.20"),
efg_pct=Decimal("52.00"),
ts_pct=Decimal("57.50"),
plus_minus=Decimal("3.50"),
offensive_rating=Decimal("110.00"),
defensive_rating=Decimal("105.00"),
)
response = self.client.get(
reverse("scouting:player_list"),
{
"competition": self.comp_a.id,
"team": self.team_a.id,
},
)
player = next(player for player in response.context["players"] if player.id == self.player_pg.id)
self.assertEqual(player.matching_context.id, self.ctx_pg_good.id)
self.assertContains(response, self.season_2025.name)
self.assertNotContains(response, "PTS 18.00")
def test_player_detail_page_still_loads_after_related_loading_cleanup(self):
response = self.client.get(reverse("scouting:player_detail", args=[self.player_pg.id]))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "PTS 16.00")
def test_combined_team_and_defensive_rating_quality_filter(self):
response = self.client.get(
reverse("scouting:player_list"),
{
"team": self.team_a.id,
"max_defensive_rating": "105",
},
)
self.assertContains(response, self.player_pg.full_name)
self.assertNotContains(response, self.player_wing.full_name)
def test_sorting_by_player_level_field(self):
taller_player = Player.objects.create(
full_name="Big Wing",
birth_date=date(2001, 5, 5),
position="SF",
height_cm=Decimal("208.00"),
weight_kg=Decimal("101.00"),
)
response = self.client.get(reverse("scouting:player_list"), {"sort": "height_desc"})
player_names = [player.full_name for player in response.context["players"]]
self.assertEqual(player_names[:3], [taller_player.full_name, self.player_wing.full_name, self.player_pg.full_name])
def test_sorting_by_matching_context_stat_field(self):
response = self.client.get(
reverse("scouting:player_list"),
{"competition": self.comp_a.id, "sort": "assists_desc"},
)
player_names = [player.full_name for player in response.context["players"]]
self.assertEqual(player_names[:2], [self.player_pg.full_name, self.player_wing.full_name])
def test_context_sorting_preserves_matching_context_semantics(self):
second_context = PlayerSeason.objects.create(
player=self.player_pg,
season=self.season_2024,
team=self.team_b,
competition=self.comp_a,
)
PlayerSeasonStats.objects.create(
player_season=second_context,
points=Decimal("12.00"),
assists=Decimal("9.00"),
steals=Decimal("1.00"),
turnovers=Decimal("2.80"),
blocks=Decimal("0.20"),
efg_pct=Decimal("49.00"),
ts_pct=Decimal("52.00"),
plus_minus=Decimal("1.00"),
offensive_rating=Decimal("107.00"),
defensive_rating=Decimal("109.00"),
)
response = self.client.get(
reverse("scouting:player_list"),
{"competition": self.comp_a.id, "team": self.team_a.id, "sort": "assists_desc"},
)
player = next(player for player in response.context["players"] if player.id == self.player_pg.id)
self.assertEqual(player.matching_context.id, self.ctx_pg_good.id)
self.assertContains(response, "AST 7.50")
self.assertNotContains(response, "AST 9.00")
def test_pagination_works_on_player_list(self):
for index in range(25):
Player.objects.create(
full_name=f"Depth Player {index:02d}",
birth_date=date(2000, 1, 1),
position="SG",
height_cm=Decimal("190.00"),
weight_kg=Decimal("84.00"),
)
response = self.client.get(reverse("scouting:player_list"), {"page": 2})
self.assertEqual(response.status_code, 200)
self.assertEqual(response.context["page_obj"].number, 2)
self.assertContains(response, "Page 2 of 2")
self.assertContains(response, "Depth Player 20")
self.assertContains(response, "Depth Player 24")
self.assertNotContains(response, "Depth Player 00")
def test_pagination_preserves_filters_and_sort_order(self):
for index in range(25):
Player.objects.create(
full_name=f"Guard Prospect {index:02d}",
birth_date=date(2001, 1, 1),
position="PG",
height_cm=Decimal("185.00"),
weight_kg=Decimal("80.00"),
)
response = self.client.get(
reverse("scouting:player_list"),
{"position": "PG", "sort": "name_desc"},
)
self.assertContains(response, "position=PG")
self.assertContains(response, "sort=name_desc")
self.assertContains(response, "page=2")
def test_combined_filters_sort_and_pagination(self):
for index in range(25):
player = Player.objects.create(
full_name=f"Playmaker Prospect {index:02d}",
birth_date=date(2003, 1, 1),
position="PG",
height_cm=Decimal("186.00"),
weight_kg=Decimal("79.00"),
)
context = PlayerSeason.objects.create(
player=player,
season=self.season_2025,
team=self.team_a,
competition=self.comp_a,
)
PlayerSeasonStats.objects.create(
player_season=context,
points=Decimal("10.00"),
assists=Decimal(str(30 - index)),
steals=Decimal("1.00"),
turnovers=Decimal("2.00"),
blocks=Decimal("0.10"),
efg_pct=Decimal("50.00"),
ts_pct=Decimal("56.00"),
plus_minus=Decimal("1.00"),
offensive_rating=Decimal("109.00"),
defensive_rating=Decimal("108.00"),
)
response = self.client.get(
reverse("scouting:player_list"),
{
"position": "PG",
"competition": self.comp_a.id,
"sort": "assists_desc",
"page": 2,
},
)
self.assertEqual(response.context["page_obj"].number, 2)
player_names = [player.full_name for player in response.context["players"]]
self.assertNotIn(self.player_wing.full_name, player_names)
self.assertEqual(player_names[0], "Playmaker Prospect 20")
self.assertIn("Marco Guard", player_names)
self.assertContains(response, "sort=assists_desc")
self.assertContains(response, "competition=%s" % self.comp_a.id)
class SeedScoutingDataCommandTests(TestCase):
def test_seed_command_creates_expected_core_objects(self):
call_command("seed_scouting_data")
self.assertGreaterEqual(Competition.objects.count(), 3)
self.assertGreaterEqual(Team.objects.count(), 4)
self.assertGreaterEqual(Season.objects.count(), 3)
self.assertGreaterEqual(Player.objects.count(), 5)
self.assertGreaterEqual(PlayerSeason.objects.count(), 6)
self.assertEqual(PlayerSeason.objects.count(), PlayerSeasonStats.objects.count())
player = Player.objects.get(full_name="Marco Guard")
self.assertEqual(player.position, Player.Position.PG)
self.assertTrue(player.roles.filter(slug="playmaker").exists())
self.assertTrue(player.specialties.filter(slug="ball-handling").exists())
def test_seed_command_is_idempotent_for_repeat_runs(self):
call_command("seed_scouting_data")
first_counts = {
"players": Player.objects.count(),
"contexts": PlayerSeason.objects.count(),
"stats": PlayerSeasonStats.objects.count(),
"roles": Role.objects.count(),
"specialties": Specialty.objects.count(),
}
call_command("seed_scouting_data")
self.assertEqual(Player.objects.count(), first_counts["players"])
self.assertEqual(PlayerSeason.objects.count(), first_counts["contexts"])
self.assertEqual(PlayerSeasonStats.objects.count(), first_counts["stats"])
self.assertEqual(Role.objects.count(), first_counts["roles"])
self.assertEqual(Specialty.objects.count(), first_counts["specialties"])