feat: add user-scoped favorites and notes
This commit is contained in:
@ -1,12 +1,17 @@
|
||||
from datetime import date
|
||||
from decimal import Decimal
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.management import call_command
|
||||
from django.test import TestCase
|
||||
from django.db import connection
|
||||
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
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class ScoutingSearchViewsTests(TestCase):
|
||||
@classmethod
|
||||
@ -416,6 +421,8 @@ class SeedScoutingDataCommandTests(TestCase):
|
||||
class FavoritePlayerViewsTests(TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.user = User.objects.create_user(username="scout_a", password="pass12345")
|
||||
cls.other_user = User.objects.create_user(username="scout_b", password="pass12345")
|
||||
cls.player = Player.objects.create(
|
||||
full_name="Favorite Prospect",
|
||||
birth_date=date(2001, 4, 4),
|
||||
@ -431,17 +438,31 @@ class FavoritePlayerViewsTests(TestCase):
|
||||
weight_kg=Decimal("94.00"),
|
||||
)
|
||||
|
||||
def test_adding_player_to_favorites(self):
|
||||
def test_login_page_loads(self):
|
||||
response = self.client.get(reverse("login"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "Log In")
|
||||
|
||||
def test_login_works_for_existing_user(self):
|
||||
response = self.client.post(
|
||||
reverse("login"),
|
||||
{"username": "scout_a", "password": "pass12345"},
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
def test_authenticated_user_can_add_player_to_favorites(self):
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.post(
|
||||
reverse("scouting:add_favorite", args=[self.player.id]),
|
||||
{"next": reverse("scouting:player_detail", args=[self.player.id])},
|
||||
)
|
||||
|
||||
self.assertRedirects(response, reverse("scouting:player_detail", args=[self.player.id]))
|
||||
self.assertTrue(FavoritePlayer.objects.filter(player=self.player).exists())
|
||||
self.assertTrue(FavoritePlayer.objects.filter(user=self.user, player=self.player).exists())
|
||||
|
||||
def test_removing_player_from_favorites(self):
|
||||
FavoritePlayer.objects.create(player=self.player)
|
||||
def test_authenticated_user_can_remove_player_from_favorites(self):
|
||||
FavoritePlayer.objects.create(user=self.user, player=self.player)
|
||||
self.client.force_login(self.user)
|
||||
|
||||
response = self.client.post(
|
||||
reverse("scouting:remove_favorite", args=[self.player.id]),
|
||||
@ -449,37 +470,55 @@ class FavoritePlayerViewsTests(TestCase):
|
||||
)
|
||||
|
||||
self.assertRedirects(response, reverse("scouting:favorites_list"))
|
||||
self.assertFalse(FavoritePlayer.objects.filter(player=self.player).exists())
|
||||
self.assertFalse(FavoritePlayer.objects.filter(user=self.user, player=self.player).exists())
|
||||
|
||||
def test_favorites_list_page_loads(self):
|
||||
FavoritePlayer.objects.create(player=self.player)
|
||||
def test_unauthenticated_user_cannot_add_or_remove_favorites(self):
|
||||
add_response = self.client.post(reverse("scouting:add_favorite", args=[self.player.id]))
|
||||
self.assertRedirects(add_response, f"{reverse('login')}?next={reverse('scouting:add_favorite', args=[self.player.id])}")
|
||||
|
||||
FavoritePlayer.objects.create(user=self.user, player=self.player)
|
||||
remove_response = self.client.post(reverse("scouting:remove_favorite", args=[self.player.id]))
|
||||
self.assertRedirects(remove_response, f"{reverse('login')}?next={reverse('scouting:remove_favorite', args=[self.player.id])}")
|
||||
|
||||
def test_favorites_page_is_user_scoped(self):
|
||||
FavoritePlayer.objects.create(user=self.user, player=self.player)
|
||||
FavoritePlayer.objects.create(user=self.other_user, player=self.other_player)
|
||||
self.client.force_login(self.user)
|
||||
|
||||
response = self.client.get(reverse("scouting:favorites_list"))
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "Favorite Prospect")
|
||||
self.assertNotContains(response, "Other Prospect")
|
||||
|
||||
def test_favorite_state_is_visible_on_detail_and_search_pages(self):
|
||||
FavoritePlayer.objects.create(player=self.player)
|
||||
def test_favorite_state_is_visible_on_detail_and_search_pages_for_logged_in_user(self):
|
||||
FavoritePlayer.objects.create(user=self.user, player=self.player)
|
||||
self.client.force_login(self.user)
|
||||
|
||||
detail_response = self.client.get(reverse("scouting:player_detail", args=[self.player.id]))
|
||||
list_response = self.client.get(reverse("scouting:player_list"))
|
||||
|
||||
self.assertContains(detail_response, "On the shared development shortlist.")
|
||||
self.assertContains(detail_response, "On your shortlist.")
|
||||
self.assertContains(detail_response, "Remove from shortlist")
|
||||
self.assertContains(list_response, "Shortlisted")
|
||||
|
||||
def test_search_and_detail_pages_still_load_after_favorites_integration(self):
|
||||
def test_search_and_detail_pages_still_load_after_user_scoping(self):
|
||||
search_response = self.client.get(reverse("scouting:player_list"))
|
||||
detail_response = self.client.get(reverse("scouting:player_detail", args=[self.player.id]))
|
||||
|
||||
self.assertEqual(search_response.status_code, 200)
|
||||
self.assertEqual(detail_response.status_code, 200)
|
||||
|
||||
def test_favorites_page_requires_login(self):
|
||||
response = self.client.get(reverse("scouting:favorites_list"))
|
||||
self.assertRedirects(response, f"{reverse('login')}?next={reverse('scouting:favorites_list')}")
|
||||
|
||||
|
||||
class PlayerNoteViewsTests(TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.user = User.objects.create_user(username="note_owner", password="pass12345")
|
||||
cls.other_user = User.objects.create_user(username="note_other", password="pass12345")
|
||||
cls.player = Player.objects.create(
|
||||
full_name="Notes Prospect",
|
||||
birth_date=date(2001, 8, 8),
|
||||
@ -488,7 +527,8 @@ class PlayerNoteViewsTests(TestCase):
|
||||
weight_kg=Decimal("97.00"),
|
||||
)
|
||||
|
||||
def test_adding_note_to_player(self):
|
||||
def test_authenticated_user_can_add_note_to_player(self):
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.post(
|
||||
reverse("scouting:add_note", args=[self.player.id]),
|
||||
{
|
||||
@ -498,11 +538,16 @@ class PlayerNoteViewsTests(TestCase):
|
||||
)
|
||||
|
||||
self.assertRedirects(response, reverse("scouting:player_detail", args=[self.player.id]))
|
||||
note = PlayerNote.objects.get(player=self.player)
|
||||
note = PlayerNote.objects.get(player=self.player, user=self.user)
|
||||
self.assertEqual(note.body, "Shows good weak-side help instincts.")
|
||||
|
||||
def test_deleting_note(self):
|
||||
note = PlayerNote.objects.create(player=self.player, body="Needs tighter handle under pressure.")
|
||||
def test_authenticated_user_can_delete_note(self):
|
||||
note = PlayerNote.objects.create(
|
||||
user=self.user,
|
||||
player=self.player,
|
||||
body="Needs tighter handle under pressure.",
|
||||
)
|
||||
self.client.force_login(self.user)
|
||||
|
||||
response = self.client.post(
|
||||
reverse("scouting:delete_note", args=[self.player.id, note.id]),
|
||||
@ -512,17 +557,35 @@ class PlayerNoteViewsTests(TestCase):
|
||||
self.assertRedirects(response, reverse("scouting:player_detail", args=[self.player.id]))
|
||||
self.assertFalse(PlayerNote.objects.filter(pk=note.id).exists())
|
||||
|
||||
def test_player_detail_page_shows_notes(self):
|
||||
PlayerNote.objects.create(player=self.player, body="Reliable closeout discipline.")
|
||||
def test_unauthenticated_user_cannot_add_or_delete_notes(self):
|
||||
add_response = self.client.post(
|
||||
reverse("scouting:add_note", args=[self.player.id]),
|
||||
{"body": "Test note"},
|
||||
)
|
||||
self.assertRedirects(add_response, f"{reverse('login')}?next={reverse('scouting:add_note', args=[self.player.id])}")
|
||||
|
||||
note = PlayerNote.objects.create(user=self.user, player=self.player, body="Existing note")
|
||||
delete_response = self.client.post(reverse("scouting:delete_note", args=[self.player.id, note.id]))
|
||||
self.assertRedirects(
|
||||
delete_response,
|
||||
f"{reverse('login')}?next={reverse('scouting:delete_note', args=[self.player.id, note.id])}",
|
||||
)
|
||||
|
||||
def test_player_detail_page_shows_only_current_users_notes(self):
|
||||
PlayerNote.objects.create(user=self.user, player=self.player, body="Reliable closeout discipline.")
|
||||
PlayerNote.objects.create(user=self.other_user, player=self.player, body="Other scout private note.")
|
||||
self.client.force_login(self.user)
|
||||
|
||||
response = self.client.get(reverse("scouting:player_detail", args=[self.player.id]))
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "Scouting Notes")
|
||||
self.assertContains(response, "Reliable closeout discipline.")
|
||||
self.assertNotContains(response, "Other scout private note.")
|
||||
|
||||
def test_existing_search_shortlist_and_detail_flows_still_load(self):
|
||||
FavoritePlayer.objects.create(player=self.player)
|
||||
FavoritePlayer.objects.create(user=self.user, player=self.player)
|
||||
self.client.force_login(self.user)
|
||||
|
||||
search_response = self.client.get(reverse("scouting:player_list"))
|
||||
favorites_response = self.client.get(reverse("scouting:favorites_list"))
|
||||
@ -533,10 +596,44 @@ class PlayerNoteViewsTests(TestCase):
|
||||
self.assertEqual(detail_response.status_code, 200)
|
||||
|
||||
def test_favorites_page_shows_note_count(self):
|
||||
FavoritePlayer.objects.create(player=self.player)
|
||||
PlayerNote.objects.create(player=self.player, body="Can defend up a position in small lineups.")
|
||||
PlayerNote.objects.create(player=self.player, body="Late-clock decision making still inconsistent.")
|
||||
FavoritePlayer.objects.create(user=self.user, player=self.player)
|
||||
PlayerNote.objects.create(user=self.user, player=self.player, body="Can defend up a position in small lineups.")
|
||||
PlayerNote.objects.create(user=self.user, player=self.player, body="Late-clock decision making still inconsistent.")
|
||||
PlayerNote.objects.create(user=self.other_user, player=self.player, body="Other user's note should not count.")
|
||||
self.client.force_login(self.user)
|
||||
|
||||
response = self.client.get(reverse("scouting:favorites_list"))
|
||||
|
||||
self.assertContains(response, "Notes: 2")
|
||||
|
||||
|
||||
class UserScopedMigrationTests(TransactionTestCase):
|
||||
reset_sequences = True
|
||||
|
||||
migrate_from = [("scouting", "0006_playernote")]
|
||||
migrate_to = [("scouting", "0007_user_scoped_favorites_and_notes")]
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.executor = MigrationExecutor(connection)
|
||||
self.executor.migrate(self.migrate_from)
|
||||
old_apps = self.executor.loader.project_state(self.migrate_from).apps
|
||||
|
||||
player_model = old_apps.get_model("scouting", "Player")
|
||||
favorite_model = old_apps.get_model("scouting", "FavoritePlayer")
|
||||
note_model = old_apps.get_model("scouting", "PlayerNote")
|
||||
|
||||
player = player_model.objects.create(full_name="Legacy Shared Player", position="PG")
|
||||
favorite_model.objects.create(player=player)
|
||||
note_model.objects.create(player=player, body="Legacy shared note")
|
||||
|
||||
self.executor = MigrationExecutor(connection)
|
||||
self.executor.migrate(self.migrate_to)
|
||||
|
||||
def test_legacy_shared_rows_are_cleared_by_user_scope_migration(self):
|
||||
apps = self.executor.loader.project_state(self.migrate_to).apps
|
||||
favorite_model = apps.get_model("scouting", "FavoritePlayer")
|
||||
note_model = apps.get_model("scouting", "PlayerNote")
|
||||
|
||||
self.assertEqual(favorite_model.objects.count(), 0)
|
||||
self.assertEqual(note_model.objects.count(), 0)
|
||||
|
||||
Reference in New Issue
Block a user