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 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, SavedSearch, Season, Specialty, Team, ) User = get_user_model() 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"), wingspan_cm=Decimal("194.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"), wingspan_cm=Decimal("211.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_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"), { "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"]) 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), position="PG", height_cm=Decimal("187.00"), weight_kg=Decimal("81.00"), ) cls.other_player = Player.objects.create( full_name="Other Prospect", birth_date=date(2000, 6, 6), position="SF", height_cm=Decimal("202.00"), weight_kg=Decimal("94.00"), ) 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(user=self.user, player=self.player).exists()) 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]), {"next": reverse("scouting:favorites_list")}, ) self.assertRedirects(response, reverse("scouting:favorites_list")) self.assertFalse(FavoritePlayer.objects.filter(user=self.user, player=self.player).exists()) 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_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 your shortlist.") self.assertContains(detail_response, "Remove from shortlist") self.assertContains(list_response, "Shortlisted") 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 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): 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), position="PF", height_cm=Decimal("205.00"), weight_kg=Decimal("97.00"), ) 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]), { "body": "Shows good weak-side help instincts.", "next": reverse("scouting:player_detail", args=[self.player.id]), }, ) self.assertRedirects(response, reverse("scouting:player_detail", args=[self.player.id])) note = PlayerNote.objects.get(player=self.player, user=self.user) self.assertEqual(note.body, "Shows good weak-side help instincts.") 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]), {"next": reverse("scouting:player_detail", args=[self.player.id])}, ) self.assertRedirects(response, reverse("scouting:player_detail", args=[self.player.id])) self.assertFalse(PlayerNote.objects.filter(pk=note.id).exists()) 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(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")) detail_response = self.client.get(reverse("scouting:player_detail", args=[self.player.id])) self.assertEqual(search_response.status_code, 200) self.assertEqual(favorites_response.status_code, 200) self.assertEqual(detail_response.status_code, 200) def test_favorites_page_shows_note_count(self): 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)