feat: version for scores

This commit is contained in:
2026-06-12 01:14:02 +02:00
parent f3c11cbe28
commit 9d9ee53a14
12 changed files with 363 additions and 38 deletions
+2 -2
View File
@@ -2,6 +2,8 @@ from django.conf import settings
from ninja import NinjaAPI
from ninja.security import APIKeyHeader
from scores.api import router as scores_router
class SecretKeyAuth(APIKeyHeader):
param_name = "X-Secret-Key"
@@ -12,6 +14,4 @@ class SecretKeyAuth(APIKeyHeader):
api = NinjaAPI(auth=SecretKeyAuth())
from scores.api import router as scores_router # noqa: E402
api.add_router("/scores", scores_router)
+15
View File
@@ -0,0 +1,15 @@
from ninja import Router
from pydantic import ConfigDict
from pydantic.alias_generators import to_camel
class BaseSchema:
"""Mixin: camelCase aliases, accept snake_case on input too."""
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
class CamelCaseRouter(Router):
def add_api_operation(self, *args, **kwargs):
kwargs["by_alias"] = True
return super().add_api_operation(*args, **kwargs)
+1
View File
@@ -11,6 +11,7 @@ class Settings(BaseSettings):
django_secret_key: str = ""
debug: bool = True
allowed_hosts: list[str] = []
installed_apps: list[str] = []
api_secret: str = "selecta-secret"
db_engine: str = "django.db.backends.postgresql"
+1
View File
@@ -31,6 +31,7 @@ INSTALLED_APPS = [
"django.contrib.messages",
"django.contrib.staticfiles",
"scores",
*env.installed_apps,
]
API_SECRET = env.api_secret
-34
View File
@@ -1,34 +0,0 @@
from django.http import HttpRequest
from ninja import Router, Schema
from ninja.pagination import PageNumberPagination, paginate
from .models import Score
router = Router()
class ScoreIn(Schema):
username: str
points: int
class ScoreOut(Schema):
id: int
username: str
points: int
created_at: str
@staticmethod
def resolve_created_at(obj: Score) -> str:
return obj.created_at.isoformat()
@router.post("", response=ScoreOut)
def submit_score(request: HttpRequest, payload: ScoreIn) -> Score:
return Score.objects.create(username=payload.username, points=payload.points)
@router.get("", response=list[ScoreOut])
@paginate(PageNumberPagination, page_size=20)
def list_scores(request: HttpRequest) -> list[Score]:
return Score.objects.all()
+3
View File
@@ -0,0 +1,3 @@
from .views import router
__all__ = ["router"]
+25
View File
@@ -0,0 +1,25 @@
from ninja import Schema
from app.api_base import BaseSchema
from scores.models import Score
class ScoreIn(Schema, BaseSchema):
username: str
points: int
class ScoreOut(Schema, BaseSchema):
id: int
username: str
points: int
version: str | None
created_at: str
@staticmethod
def resolve_version(obj: Score) -> str | None:
return obj.version.name if obj.version_id else None
@staticmethod
def resolve_created_at(obj: Score) -> str:
return obj.created_at.isoformat()
+42
View File
@@ -0,0 +1,42 @@
from typing import Annotated
from django.http import HttpRequest
from ninja import Header
from ninja.pagination import PageNumberPagination, paginate
from app.api_base import CamelCaseRouter
from scores.models import Score, Version
from .schemas import ScoreIn, ScoreOut
router = CamelCaseRouter()
def _resolve_version(name: str | None) -> Version:
if name:
version, _ = Version.objects.get_or_create(name=name)
return version
version = Version.objects.order_by("-created_at").first()
if version is None:
version = Version.objects.create(name="default")
return version
@router.post("", response=ScoreOut)
def submit_score(
request: HttpRequest,
payload: ScoreIn,
x_game_version: Annotated[str | None, Header(alias="X-Game-Version")] = None,
) -> Score:
version = _resolve_version(x_game_version)
return Score.objects.create(username=payload.username, points=payload.points, version=version)
@router.get("", response=list[ScoreOut])
@paginate(PageNumberPagination, page_size=20)
def list_scores(
request: HttpRequest,
x_game_version: Annotated[str | None, Header(alias="X-Game-Version")] = None,
) -> list[Score]:
version = _resolve_version(x_game_version)
return Score.objects.filter(version=version).select_related("version")
+28 -1
View File
@@ -1,5 +1,6 @@
# Generated by Django 6.0.6 on 2026-06-11 22:22
# Generated by Django 6.0.6 on 2026-06-11 22:58
import django.db.models.deletion
from django.db import migrations, models
@@ -9,6 +10,22 @@ class Migration(migrations.Migration):
dependencies = []
operations = [
migrations.CreateModel(
name="Version",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=50, unique=True)),
("created_at", models.DateTimeField(auto_now_add=True)),
],
),
migrations.CreateModel(
name="Score",
fields=[
@@ -24,6 +41,16 @@ class Migration(migrations.Migration):
("username", models.CharField(max_length=100)),
("points", models.IntegerField()),
("created_at", models.DateTimeField(auto_now_add=True)),
(
"version",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="scores",
to="scores.version",
),
),
],
options={
"ordering": ["-points"],
+11
View File
@@ -1,9 +1,20 @@
from django.db import models
class Version(models.Model):
name = models.CharField(max_length=50, unique=True)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self) -> str:
return self.name
class Score(models.Model):
username = models.CharField(max_length=100)
points = models.IntegerField()
version = models.ForeignKey(
Version, on_delete=models.PROTECT, null=True, blank=True, related_name="scores"
)
created_at = models.DateTimeField(auto_now_add=True)
class Meta: