feat(scouting): add wingspan filters and saved searches mvp

This commit is contained in:
bisco
2026-04-11 00:07:55 +02:00
parent e44cad6167
commit c09aad2d63
8 changed files with 412 additions and 9 deletions

View File

@ -1,5 +1,6 @@
from datetime import date
from decimal import Decimal
from urllib.parse import urlencode
from django.contrib.auth import get_user_model
from django.core.management import call_command
@ -8,7 +9,19 @@ from django.db.migrations.executor import MigrationExecutor
from django.test import TestCase, TransactionTestCase
from django.urls import reverse
from .models import Competition, FavoritePlayer, Player, PlayerNote, PlayerSeason, PlayerSeasonStats, Role, Season, Specialty, Team
from .models import (
Competition,
FavoritePlayer,
Player,
PlayerNote,
PlayerSeason,
PlayerSeasonStats,
Role,
SavedSearch,
Season,
Specialty,
Team,
)
User = get_user_model()
@ -34,6 +47,7 @@ class ScoutingSearchViewsTests(TestCase):
position="PG",
height_cm=Decimal("188.00"),
weight_kg=Decimal("82.00"),
wingspan_cm=Decimal("194.00"),
nationality="IT",
)
cls.player_pg.roles.add(cls.role_playmaker)
@ -45,6 +59,7 @@ class ScoutingSearchViewsTests(TestCase):
position="SF",
height_cm=Decimal("201.00"),
weight_kg=Decimal("95.00"),
wingspan_cm=Decimal("211.00"),
)
cls.player_wing.roles.add(cls.role_3d)
cls.player_wing.specialties.add(cls.specialty_offball)
@ -127,6 +142,15 @@ class ScoutingSearchViewsTests(TestCase):
self.assertContains(response, self.player_pg.full_name)
self.assertNotContains(response, self.player_wing.full_name)
def test_filter_by_wingspan_thresholds(self):
response = self.client.get(
reverse("scouting:player_list"),
{"min_wingspan_cm": "205"},
)
self.assertContains(response, self.player_wing.full_name)
self.assertNotContains(response, self.player_pg.full_name)
def test_filter_by_context_fields_and_stats(self):
response = self.client.get(
reverse("scouting:player_list"),
@ -514,6 +538,114 @@ class FavoritePlayerViewsTests(TestCase):
self.assertRedirects(response, f"{reverse('login')}?next={reverse('scouting:favorites_list')}")
class SavedSearchViewsTests(TestCase):
@classmethod
def setUpTestData(cls):
cls.user = User.objects.create_user(username="saved_owner", password="pass12345")
cls.other_user = User.objects.create_user(username="saved_other", password="pass12345")
cls.player_guard = Player.objects.create(
full_name="Saved Guard",
birth_date=date(2002, 7, 7),
position="PG",
height_cm=Decimal("188.00"),
weight_kg=Decimal("82.00"),
wingspan_cm=Decimal("196.00"),
)
cls.player_wing = Player.objects.create(
full_name="Saved Wing",
birth_date=date(2000, 9, 9),
position="SF",
height_cm=Decimal("203.00"),
weight_kg=Decimal("95.00"),
wingspan_cm=Decimal("213.00"),
)
def test_logged_in_user_can_save_search(self):
self.client.force_login(self.user)
response = self.client.post(
reverse("scouting:save_search"),
{
"saved_search_name": "Long Wings",
"position": "SF",
"min_wingspan_cm": "210",
"sort": "height_desc",
},
)
self.assertEqual(response.status_code, 302)
self.assertIn(f"{reverse('scouting:player_list')}?", response["Location"])
self.assertIn("sort=height_desc", response["Location"])
self.assertIn("position=SF", response["Location"])
self.assertIn("min_wingspan_cm=210", response["Location"])
saved = SavedSearch.objects.get(user=self.user, name="Long Wings")
self.assertEqual(saved.params["position"], "SF")
self.assertEqual(saved.params["min_wingspan_cm"], "210")
def test_saved_searches_are_user_scoped(self):
SavedSearch.objects.create(user=self.user, name="Mine", params={"position": "PG"})
SavedSearch.objects.create(user=self.other_user, name="Other", params={"position": "SF"})
self.client.force_login(self.user)
response = self.client.get(reverse("scouting:player_list"))
self.assertContains(response, "Mine")
self.assertNotContains(response, "Other")
def test_logged_in_user_can_rerun_saved_search(self):
saved = SavedSearch.objects.create(user=self.user, name="Wingspan Hunt", params={"min_wingspan_cm": "210"})
self.client.force_login(self.user)
response = self.client.get(f"{reverse('scouting:player_list')}?{urlencode(saved.params)}")
self.assertEqual(response.status_code, 200)
self.assertContains(response, self.player_wing.full_name)
self.assertNotContains(response, self.player_guard.full_name)
def test_logged_in_user_can_delete_saved_search(self):
saved = SavedSearch.objects.create(user=self.user, name="Delete Me", params={"position": "PG"})
self.client.force_login(self.user)
response = self.client.post(
reverse("scouting:delete_saved_search", args=[saved.id]),
{"position": "PG"},
)
self.assertRedirects(response, f"{reverse('scouting:player_list')}?position=PG")
self.assertFalse(SavedSearch.objects.filter(pk=saved.id).exists())
def test_unauthenticated_user_cannot_save_or_delete_saved_searches(self):
save_response = self.client.post(reverse("scouting:save_search"), {"saved_search_name": "No Auth"})
self.assertRedirects(save_response, f"{reverse('login')}?next={reverse('scouting:save_search')}")
saved = SavedSearch.objects.create(user=self.user, name="Auth Required", params={"position": "PG"})
delete_response = self.client.post(reverse("scouting:delete_saved_search", args=[saved.id]))
self.assertRedirects(
delete_response,
f"{reverse('login')}?next={reverse('scouting:delete_saved_search', args=[saved.id])}",
)
def test_combined_filters_saved_search_and_rerun(self):
self.client.force_login(self.user)
save_response = self.client.post(
reverse("scouting:save_search"),
{
"saved_search_name": "SF Long Wings",
"position": "SF",
"min_wingspan_cm": "212",
"sort": "height_desc",
},
)
self.assertEqual(save_response.status_code, 302)
saved = SavedSearch.objects.get(user=self.user, name="SF Long Wings")
rerun_response = self.client.get(f"{reverse('scouting:player_list')}?position=SF&min_wingspan_cm=212&sort=height_desc")
self.assertEqual(rerun_response.status_code, 200)
self.assertContains(rerun_response, self.player_wing.full_name)
self.assertNotContains(rerun_response, self.player_guard.full_name)
self.assertEqual(saved.params["sort"], "height_desc")
class PlayerNoteViewsTests(TestCase):
@classmethod
def setUpTestData(cls):