diff --git a/project/thscoreboard/replays/migrations/0043_replay_legacy_rank.py b/project/thscoreboard/replays/migrations/0043_replay_legacy_rank.py new file mode 100644 index 00000000..b2c3d0a7 --- /dev/null +++ b/project/thscoreboard/replays/migrations/0043_replay_legacy_rank.py @@ -0,0 +1,223 @@ +# Generated by Django 4.2.4 on 2025-01-13 19:10 + +from django.db import migrations + + +class Migration(migrations.Migration): + """Fix rank view to include RF replays.""" + + dependencies = [ + ("replays", "0042_unique_rank_view"), + ] + + operations = [ + migrations.RunSQL( + sql=""" +CREATE OR REPLACE VIEW replays_rank +AS +SELECT + row_number() over () as id, + replay, + score, + shot_id, + difficulty, + route_id, + category, + place +FROM + ( + SELECT + replay, + score, + shot_id, + difficulty, + route_id, + category, + created, + rank() OVER ( + PARTITION BY shot_id, + difficulty, + route_id, + category + ORDER BY + score DESC, + created, + replay + ) as place + FROM + ( + SELECT + replay, + score, + shot_id, + difficulty, + route_id, + category, + created, + user_id, + imported_username + FROM + ( + -- Per-user bests + ( + SELECT + id as replay, + score, + shot_id, + difficulty, + route_id, + category, + created, + user_id, + NULL as imported_username, + rank() OVER ( + PARTITION BY shot_id, + difficulty, + route_id, + category, + user_id + ORDER BY + score DESC, + created, + id + ) as per_user_place + FROM + replays_replay + WHERE + replay_type = 1 -- FULL_GAME + AND category = 1 -- STANDARD + AND user_id IS NOT NULL + ) + UNION ALL + -- Per-username bests for imported, unowned Royalflare replays + ( + SELECT + id as replay, + score, + shot_id, + difficulty, + route_id, + category, + created, + NULL as user_id, + imported_username, + rank() OVER ( + PARTITION BY shot_id, + difficulty, + route_id, + category, + imported_username + ORDER BY + score DESC, + created, + id + ) as per_user_place + FROM + replays_replay + WHERE + replay_type = 1 -- FULL_GAME + AND category = 1 -- STANDARD + AND user_id IS NULL + AND imported_username IS NOT NULL + ) + ) + AS replays_ranked_per_user + WHERE + per_user_place = 1 + ) AS top_replays_per_user + ) AS top_replays +WHERE + place <= 3 +ORDER BY + shot_id, + difficulty, + route_id, + category, + place DESC; +""", + reverse_sql=""" +CREATE OR REPLACE VIEW replays_rank +AS +SELECT + row_number() over () as id, + replay, + score, + shot_id, + difficulty, + route_id, + category, + place +FROM + ( + SELECT + replay, + score, + shot_id, + difficulty, + route_id, + category, + created, + rank() OVER ( + PARTITION BY shot_id, + difficulty, + route_id, + category + ORDER BY + score DESC, + created, + replay + ) as place + FROM + ( + SELECT + replay, + score, + shot_id, + difficulty, + route_id, + category, + created, + user_id + FROM + ( + SELECT + id as replay, + score, + shot_id, + difficulty, + route_id, + category, + created, + user_id, + rank() OVER ( + PARTITION BY shot_id, + difficulty, + route_id, + category, + user_id + ORDER BY + score DESC, + created, + id + ) as per_user_place + FROM + replays_replay + WHERE + replay_type = 1 -- FULL_GAME + AND category = 1 -- STANDARD + ) AS replays_ranked_per_user + WHERE + per_user_place = 1 + ) AS top_replays_per_user + ) AS top_replays +WHERE + place <= 3 +ORDER BY + shot_id, + difficulty, + route_id, + category, + place DESC; +""", + ) + ] diff --git a/project/thscoreboard/replays/test_models.py b/project/thscoreboard/replays/test_models.py index d15b5ae6..636fa8fe 100644 --- a/project/thscoreboard/replays/test_models.py +++ b/project/thscoreboard/replays/test_models.py @@ -231,6 +231,62 @@ def testOnlyBestRecordFromAPlayerIsRanked(self): self.assertIsNone(replays[1].GetRank()) self.assertIsNone(replays[3].GetRank()) + def testCorrectlyRanksMixOfOwnedAndLegacyReplays(self): + test_replays.CreateAsPublishedReplay( + filename="th6_extra", + user=self.user1, + imported_username=None, + score=1_000_000_000, + ) + test_replays.CreateAsPublishedReplay( + filename="th6_extra", + user=None, + imported_username="user2", + score=900_000_000, + ) + test_replays.CreateAsPublishedReplay( + filename="th6_extra", + user=None, + imported_username="user2", + score=800_000_000, + ) + test_replays.CreateAsPublishedReplay( + filename="th6_extra", + user=None, + imported_username="user3", + score=700_000_000, + ) + + replays = ( + models.Replay.objects.order_by("-score").select_related("rank_view").all() + ) + + self.assertEqual(replays[0].GetRank(), 1) + self.assertEqual(replays[1].GetRank(), 2) + self.assertIsNone(replays[2].GetRank()) + self.assertEqual(replays[3].GetRank(), 3) + + def testOwnedLegacyReplayRankedTogetherWithOwnedNewReplay(self): + test_replays.CreateAsPublishedReplay( + filename="th6_extra", + user=self.user1, + imported_username=None, + score=1_000_000_000, + ) + test_replays.CreateAsPublishedReplay( + filename="th6_extra", + user=self.user1, + imported_username="user1", + score=900_000_000, + ) + + replays = ( + models.Replay.objects.order_by("-score").select_related("rank_view").all() + ) + + self.assertEqual(replays[0].GetRank(), 1) + self.assertIsNone(replays[1].GetRank()) + class ReplayTest(test_case.ReplayTestCase): def setUp(self):