From 544112b204b47a9a346f1571cc7d283daee5f6e9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Lo=C3=AFc=20Gremaud?=
Date: Sat, 23 May 2026 14:18:44 +0200
Subject: [PATCH] feat(games): add games to disable + path
---
polylan_submitter/games/__init__.py | 0
polylan_submitter/games/admin.py | 11 +++++
polylan_submitter/games/api.py | 13 +++++
polylan_submitter/games/apps.py | 6 +++
polylan_submitter/games/decorators.py | 22 +++++++++
.../games/migrations/0001_initial.py | 32 +++++++++++++
.../0002_seed_noita_and_opus_magnum.py | 31 ++++++++++++
.../games/migrations/0003_game_path.py | 31 ++++++++++++
.../games/migrations/__init__.py | 0
polylan_submitter/games/models.py | 17 +++++++
polylan_submitter/games/schemas.py | 7 +++
polylan_submitter/noita/api.py | 6 +++
polylan_submitter/noita/services/decode.py | 1 -
polylan_submitter/polylan_submitter/api.py | 2 +
.../polylan_submitter/settings.py | 3 +-
polylan_submitter/polylan_submitter/urls.py | 6 +++
polylan_submitter/src/Home.vue | 48 +++++++++----------
polylan_submitter/src/services/apiService.ts | 6 +++
polylan_submitter/src/types/index.ts | 6 +++
polylan_submitter/submissions/api.py | 14 ++++++
20 files changed, 235 insertions(+), 27 deletions(-)
create mode 100644 polylan_submitter/games/__init__.py
create mode 100644 polylan_submitter/games/admin.py
create mode 100644 polylan_submitter/games/api.py
create mode 100644 polylan_submitter/games/apps.py
create mode 100644 polylan_submitter/games/decorators.py
create mode 100644 polylan_submitter/games/migrations/0001_initial.py
create mode 100644 polylan_submitter/games/migrations/0002_seed_noita_and_opus_magnum.py
create mode 100644 polylan_submitter/games/migrations/0003_game_path.py
create mode 100644 polylan_submitter/games/migrations/__init__.py
create mode 100644 polylan_submitter/games/models.py
create mode 100644 polylan_submitter/games/schemas.py
diff --git a/polylan_submitter/games/__init__.py b/polylan_submitter/games/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/polylan_submitter/games/admin.py b/polylan_submitter/games/admin.py
new file mode 100644
index 0000000..5e84eb2
--- /dev/null
+++ b/polylan_submitter/games/admin.py
@@ -0,0 +1,11 @@
+from django.contrib import admin
+
+from .models import Game
+
+
+@admin.register(Game)
+class GameAdmin(admin.ModelAdmin):
+ list_display = ["name", "steam_app_id", "enabled", "updated_at"]
+ list_filter = ["enabled"]
+ search_fields = ["name", "steam_app_id"]
+ readonly_fields = ["created_at", "updated_at"]
diff --git a/polylan_submitter/games/api.py b/polylan_submitter/games/api.py
new file mode 100644
index 0000000..e863b87
--- /dev/null
+++ b/polylan_submitter/games/api.py
@@ -0,0 +1,13 @@
+from typing import List
+
+from ninja import Router
+
+from .models import Game
+from .schemas import GameOut
+
+router = Router()
+
+
+@router.get("", response=List[GameOut])
+def list_games(request):
+ return Game.objects.filter(enabled=True)
diff --git a/polylan_submitter/games/apps.py b/polylan_submitter/games/apps.py
new file mode 100644
index 0000000..4c4fb1d
--- /dev/null
+++ b/polylan_submitter/games/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class GamesConfig(AppConfig):
+ default_auto_field = "django.db.models.BigAutoField"
+ name = "games"
diff --git a/polylan_submitter/games/decorators.py b/polylan_submitter/games/decorators.py
new file mode 100644
index 0000000..339ebad
--- /dev/null
+++ b/polylan_submitter/games/decorators.py
@@ -0,0 +1,22 @@
+from functools import wraps
+
+from django.core.exceptions import PermissionDenied
+
+from .models import Game
+
+
+def require_game_enabled(steam_app_id: int):
+ def decorator(view_func):
+ @wraps(view_func)
+ def wrapper(request, *args, **kwargs):
+ try:
+ game = Game.objects.get(steam_app_id=steam_app_id)
+ except Game.DoesNotExist:
+ raise PermissionDenied
+ if not game.enabled:
+ raise PermissionDenied
+ return view_func(request, *args, **kwargs)
+
+ return wrapper
+
+ return decorator
diff --git a/polylan_submitter/games/migrations/0001_initial.py b/polylan_submitter/games/migrations/0001_initial.py
new file mode 100644
index 0000000..c8f365a
--- /dev/null
+++ b/polylan_submitter/games/migrations/0001_initial.py
@@ -0,0 +1,32 @@
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ initial = True
+
+ dependencies = []
+
+ operations = [
+ migrations.CreateModel(
+ name="Game",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("steam_app_id", models.PositiveIntegerField(unique=True)),
+ ("name", models.CharField(max_length=255)),
+ ("enabled", models.BooleanField(default=True)),
+ ("created_at", models.DateTimeField(auto_now_add=True)),
+ ("updated_at", models.DateTimeField(auto_now=True)),
+ ],
+ options={
+ "ordering": ["name"],
+ },
+ ),
+ ]
diff --git a/polylan_submitter/games/migrations/0002_seed_noita_and_opus_magnum.py b/polylan_submitter/games/migrations/0002_seed_noita_and_opus_magnum.py
new file mode 100644
index 0000000..2b05614
--- /dev/null
+++ b/polylan_submitter/games/migrations/0002_seed_noita_and_opus_magnum.py
@@ -0,0 +1,31 @@
+from django.db import migrations
+
+NOITA_APP_ID = 881100
+OPUS_MAGNUM_APP_ID = 558990
+
+
+def seed_games(apps, schema_editor):
+ Game = apps.get_model("games", "Game")
+ Game.objects.get_or_create(
+ steam_app_id=NOITA_APP_ID,
+ defaults={"name": "Noita", "enabled": True},
+ )
+ Game.objects.get_or_create(
+ steam_app_id=OPUS_MAGNUM_APP_ID,
+ defaults={"name": "Opus Magnum", "enabled": True},
+ )
+
+
+def unseed_games(apps, schema_editor):
+ Game = apps.get_model("games", "Game")
+ Game.objects.filter(steam_app_id__in=[NOITA_APP_ID, OPUS_MAGNUM_APP_ID]).delete()
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("games", "0001_initial"),
+ ]
+
+ operations = [
+ migrations.RunPython(seed_games, reverse_code=unseed_games),
+ ]
diff --git a/polylan_submitter/games/migrations/0003_game_path.py b/polylan_submitter/games/migrations/0003_game_path.py
new file mode 100644
index 0000000..dcc30f0
--- /dev/null
+++ b/polylan_submitter/games/migrations/0003_game_path.py
@@ -0,0 +1,31 @@
+from django.db import migrations, models
+
+NOITA_APP_ID = 881100
+OPUS_MAGNUM_APP_ID = 558990
+
+PATHS = {
+ NOITA_APP_ID: "/noita",
+ OPUS_MAGNUM_APP_ID: "/opus-magnum",
+}
+
+
+def set_paths(apps, schema_editor):
+ Game = apps.get_model("games", "Game")
+ for app_id, path in PATHS.items():
+ Game.objects.filter(steam_app_id=app_id).update(path=path)
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("games", "0002_seed_noita_and_opus_magnum"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="game",
+ name="path",
+ field=models.CharField(default="", max_length=100),
+ preserve_default=False,
+ ),
+ migrations.RunPython(set_paths, reverse_code=migrations.RunPython.noop),
+ ]
diff --git a/polylan_submitter/games/migrations/__init__.py b/polylan_submitter/games/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/polylan_submitter/games/models.py b/polylan_submitter/games/models.py
new file mode 100644
index 0000000..42f1284
--- /dev/null
+++ b/polylan_submitter/games/models.py
@@ -0,0 +1,17 @@
+from django.db import models
+
+
+class Game(models.Model):
+ steam_app_id = models.PositiveIntegerField(unique=True)
+ name = models.CharField(max_length=255)
+ path = models.CharField(max_length=100)
+ enabled = models.BooleanField(default=True)
+
+ created_at = models.DateTimeField(auto_now_add=True)
+ updated_at = models.DateTimeField(auto_now=True)
+
+ class Meta:
+ ordering = ["name"]
+
+ def __str__(self) -> str:
+ return f"{self.name} ({self.steam_app_id})"
diff --git a/polylan_submitter/games/schemas.py b/polylan_submitter/games/schemas.py
new file mode 100644
index 0000000..8da0f75
--- /dev/null
+++ b/polylan_submitter/games/schemas.py
@@ -0,0 +1,7 @@
+from ninja import Schema
+
+
+class GameOut(Schema):
+ steam_app_id: int
+ name: str
+ path: str
diff --git a/polylan_submitter/noita/api.py b/polylan_submitter/noita/api.py
index 26b2943..0c53982 100644
--- a/polylan_submitter/noita/api.py
+++ b/polylan_submitter/noita/api.py
@@ -12,18 +12,22 @@ from django.db.models import (
)
from ninja import Router, File
from ninja.files import UploadedFile
+from ninja.decorators import decorate_view
from noita.schemas import ResultsOut, LeaderboardOut
from noita.services.objectives import parse_objectives_and_store
+from games.decorators import require_game_enabled
from .models import LogfileSubmission, Objectiv, ObjectivPoint, DeathCounter
from .schemas import NoitaSubmissionOut
router = Router()
+NOITA_APP_ID = 881100
@router.get("results", response=ResultsOut)
+@decorate_view(require_game_enabled(NOITA_APP_ID))
def get_results(request: HttpRequest):
cache_key = f"api:noita:results:{request.user.id}"
cached_data = cache.get(cache_key)
@@ -127,6 +131,7 @@ def get_results(request: HttpRequest):
@router.get("leaderboard", response=LeaderboardOut)
+@decorate_view(require_game_enabled(NOITA_APP_ID))
def get_leaderboard(request: HttpRequest):
"""
Get the global leaderboard for all users ranked by total score.
@@ -232,6 +237,7 @@ def get_leaderboard(request: HttpRequest):
@router.post("submit", response={200: NoitaSubmissionOut, 400: dict})
+@decorate_view(require_game_enabled(NOITA_APP_ID))
def submit_log_file(request: HttpRequest, file: UploadedFile = File(...)):
"""
Submit a Noita run file (log file, screenshot, or video).
diff --git a/polylan_submitter/noita/services/decode.py b/polylan_submitter/noita/services/decode.py
index a6e24f9..71b7c5b 100644
--- a/polylan_submitter/noita/services/decode.py
+++ b/polylan_submitter/noita/services/decode.py
@@ -72,7 +72,6 @@ POINTS = {
"NOLLA": 10,
"CHAOTIC_TRANSMUTATION": 10,
"DUPLICATE": 5,
- "OMEGA": 10,
"BURST_2": 10,
"BURST_3": 15,
"BURST_4": 20,
diff --git a/polylan_submitter/polylan_submitter/api.py b/polylan_submitter/polylan_submitter/api.py
index 747e093..2a18131 100644
--- a/polylan_submitter/polylan_submitter/api.py
+++ b/polylan_submitter/polylan_submitter/api.py
@@ -5,6 +5,7 @@ from submissions.api import router as submissions_router
from submissions.schemas import UserInfoOut
from animations.api import router as results_router
from noita.api import router as noita_router
+from games.api import router as games_router
# Create the main API instance
api = NinjaAPI(
@@ -34,6 +35,7 @@ The Noita Submission API allows clients to upload the result of the log file of
api.add_router("/submissions/", submissions_router, tags=["submissions"])
api.add_router("/results/", results_router, tags=["results"])
api.add_router("/noita/", noita_router, tags=["noita"])
+api.add_router("/games/", games_router, tags=["games"])
# Health check endpoint
diff --git a/polylan_submitter/polylan_submitter/settings.py b/polylan_submitter/polylan_submitter/settings.py
index 3642c36..b6f02f1 100644
--- a/polylan_submitter/polylan_submitter/settings.py
+++ b/polylan_submitter/polylan_submitter/settings.py
@@ -43,6 +43,7 @@ INSTALLED_APPS = [
"animations",
"submissions",
"noita",
+ "games",
]
MIDDLEWARE = [
@@ -193,7 +194,7 @@ STATICFILES_DIRS = [
from polylan_submitter.settingsLocal import * # noqa
-import sentry_sdk
+import sentry_sdk # noqa
sentry_sdk.init(
dsn="https://cc62a4ce3f3470890b43accf02cc6d8c@sentry2.polylan.ch/12",
diff --git a/polylan_submitter/polylan_submitter/urls.py b/polylan_submitter/polylan_submitter/urls.py
index 9c220a5..28626a6 100644
--- a/polylan_submitter/polylan_submitter/urls.py
+++ b/polylan_submitter/polylan_submitter/urls.py
@@ -23,8 +23,12 @@ from django.contrib.auth.decorators import login_required
from django.conf import settings
from django.conf.urls.static import static
from simple_cas_views import SimpleCASLoginView, SimpleCASLogoutView
+from games.decorators import require_game_enabled
from .api import api
+NOITA_APP_ID = 881100
+OPUS_MAGNUM_APP_ID = 558990
+
@login_required
def home(request: HttpRequest):
@@ -32,6 +36,7 @@ def home(request: HttpRequest):
@login_required
+@require_game_enabled(OPUS_MAGNUM_APP_ID)
def opus_magnum_home(request: HttpRequest):
from submissions.models import SteamCollection
@@ -45,6 +50,7 @@ def opus_magnum_home(request: HttpRequest):
@login_required
+@require_game_enabled(NOITA_APP_ID)
def noita_home(request: HttpRequest):
return render(request, "noita.html", {})
diff --git a/polylan_submitter/src/Home.vue b/polylan_submitter/src/Home.vue
index aec533c..488e1c9 100644
--- a/polylan_submitter/src/Home.vue
+++ b/polylan_submitter/src/Home.vue
@@ -1,22 +1,10 @@
@@ -44,13 +40,18 @@ const navigate = (path: string) => {
+
+
+
+
+
-