Compare commits

..

No commits in common. "6ad86af56815f47a7005b9cb474905d210b258f1" and "54259d64e2caf049de3d6fb4b090fa2e28e27cbc" have entirely different histories.

8 changed files with 14 additions and 139 deletions

View File

@ -1,8 +0,0 @@
from ninja.pagination import PageNumberPagination as _PageNumberPagination
from app.api_base import BaseSchema
class PageNumberPagination(_PageNumberPagination):
class Input(_PageNumberPagination.Input, BaseSchema):
pass

View File

@ -3,12 +3,11 @@ from typing import Annotated
from django.db.models import QuerySet
from django.http import HttpRequest
from ninja import Header
from ninja.pagination import paginate
from ninja.pagination import PageNumberPagination, paginate
from app.api_base import CamelCaseRouter
from scores.models import Score, Version
from .pagination import PageNumberPagination
from .schemas import ScoreIn, ScoreOut
router = CamelCaseRouter()
@ -42,4 +41,4 @@ def list_scores(
x_game_version: GameVersionHeader = None,
) -> QuerySet[Score]:
version = resolve_version(x_game_version)
return Score.objects.filter(version=version).best_per_username().with_rank()
return Score.objects.filter(version=version)

View File

@ -1,7 +1,7 @@
from django.db import models
from django.db.models import F
from django.db.models.expressions import Window
from django.db.models.functions import Lower, Rank, RowNumber
from django.db.models.functions import Rank
class Version(models.Model):
@ -12,37 +12,19 @@ class Version(models.Model):
return self.name
class ScoreQuerySet(models.QuerySet):
def best_per_username(self) -> "ScoreQuerySet":
"""Keep only each user's highest score, matching usernames case-insensitively.
Lower scores are excluded from the result but remain in the database.
"""
best_ids = (
self.alias(
_row=Window(
expression=RowNumber(),
partition_by=Lower("username"),
order_by=[F("points").desc(), F("created_at").asc()],
)
)
.filter(_row=1)
.values_list("pk", flat=True)
)
return self.filter(pk__in=best_ids)
def with_rank(self) -> "ScoreQuerySet":
return self.annotate(
class ScoreManager(models.Manager):
def get_queryset(self) -> models.QuerySet["Score"]:
return (
super()
.get_queryset()
.select_related("version")
.annotate(
rank=Window(
expression=Rank(),
order_by=F("points").desc(),
)
)
class ScoreManager(models.Manager.from_queryset(ScoreQuerySet)):
def get_queryset(self) -> ScoreQuerySet:
return super().get_queryset().select_related("version")
)
class Score(models.Model):

View File

@ -1,42 +0,0 @@
import pytest
from scores.models import Score, Version
@pytest.fixture
def version(db) -> Version:
version = Version.objects.create(name="default")
other = Version.objects.create(name="other")
Score.objects.create(username="legrems", points=120, version=version)
Score.objects.create(username="Legrems", points=119, version=version)
Score.objects.create(username="legrems", points=50, version=version)
Score.objects.create(username="alice", points=200, version=version)
# Same user, different version: must not collide across versions.
Score.objects.create(username="legrems", points=999, version=other)
return version
def best(version: Version) -> list[Score]:
return list(Score.objects.filter(version=version).best_per_username().with_rank())
def test_keeps_only_highest_per_case_insensitive_username(version: Version) -> None:
results = best(version)
assert len(results) == 2
by_name = {s.username.lower(): s for s in results}
assert by_name["legrems"].points == 120
assert by_name["alice"].points == 200
def test_ranks_recomputed_over_deduplicated_set(version: Version) -> None:
ranks = {s.username.lower(): s.rank for s in best(version)}
assert ranks["alice"] == 1
assert ranks["legrems"] == 2
def test_lower_scores_remain_in_db(version: Version) -> None:
assert Score.objects.filter(version=version).count() == 4

View File

@ -0,0 +1 @@
# Create your tests here.

View File

@ -28,8 +28,6 @@ dev = [
"django-extensions>=4.1",
"django-stubs>=6.0.5",
"ipython>=9.14.1",
"pytest>=9.1.1",
"pytest-django>=4.12.0",
"ruff>=0.15.17",
]

View File

@ -1,5 +0,0 @@
[pytest]
DJANGO_SETTINGS_MODULE = app.settings
pythonpath = highscore
python_files = test_*.py tests.py
testpaths = highscore

50
uv.lock
View File

@ -365,15 +365,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/1d/55/0f4df2a44053867ea9cbea73fc588b03c55605cd695cee0a3d86f0029cb2/incremental-24.11.0-py3-none-any.whl", hash = "sha256:a34450716b1c4341fe6676a0598e88a39e04189f4dce5dc96f656e040baa10b3", size = 21109, upload-time = "2025-11-28T02:30:16.442Z" },
]
[[package]]
name = "iniconfig"
version = "2.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
]
[[package]]
name = "ipython"
version = "9.14.1"
@ -519,15 +510,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" },
]
[[package]]
name = "pluggy"
version = "1.6.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
]
[[package]]
name = "prompt-toolkit"
version = "3.0.52"
@ -719,34 +701,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/73/b8/a0e2790ae249d6f38c9f66de7a211621a7ab2650217bcd04e1262f578a56/pyopenssl-26.2.0-py3-none-any.whl", hash = "sha256:4f9d971bc5298b8bc1fab282803da04bf000c755d4ad9d99b52de2569ca19a70", size = 55823, upload-time = "2026-05-04T23:06:08.395Z" },
]
[[package]]
name = "pytest"
version = "9.1.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "iniconfig" },
{ name = "packaging" },
{ name = "pluggy" },
{ name = "pygments" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
]
[[package]]
name = "pytest-django"
version = "4.12.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pytest" },
]
sdist = { url = "https://files.pythonhosted.org/packages/13/2b/db9a193df89e5660137f5428063bcc2ced7ad790003b26974adf5c5ceb3b/pytest_django-4.12.0.tar.gz", hash = "sha256:df94ec819a83c8979c8f6de13d9cdfbe76e8c21d39473cfe2b40c9fc9be3c758", size = 91156, upload-time = "2026-02-14T18:40:49.235Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/83/a5/41d091f697c09609e7ef1d5d61925494e0454ebf51de7de05f0f0a728f1d/pytest_django-4.12.0-py3-none-any.whl", hash = "sha256:3ff300c49f8350ba2953b90297d23bf5f589db69545f56f1ec5f8cff5da83e85", size = 26123, upload-time = "2026-02-14T18:40:47.381Z" },
]
[[package]]
name = "python-dotenv"
version = "1.2.2"
@ -799,8 +753,6 @@ dev = [
{ name = "django-extensions" },
{ name = "django-stubs" },
{ name = "ipython" },
{ name = "pytest" },
{ name = "pytest-django" },
{ name = "ruff" },
]
@ -819,8 +771,6 @@ dev = [
{ name = "django-extensions", specifier = ">=4.1" },
{ name = "django-stubs", specifier = ">=6.0.5" },
{ name = "ipython", specifier = ">=9.14.1" },
{ name = "pytest", specifier = ">=9.1.1" },
{ name = "pytest-django", specifier = ">=4.12.0" },
{ name = "ruff", specifier = ">=0.15.17" },
]