feat(games): add games to disable + path
This commit is contained in:
parent
9f94fb3974
commit
544112b204
0
polylan_submitter/games/__init__.py
Normal file
0
polylan_submitter/games/__init__.py
Normal file
11
polylan_submitter/games/admin.py
Normal file
11
polylan_submitter/games/admin.py
Normal file
@ -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"]
|
||||
13
polylan_submitter/games/api.py
Normal file
13
polylan_submitter/games/api.py
Normal file
@ -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)
|
||||
6
polylan_submitter/games/apps.py
Normal file
6
polylan_submitter/games/apps.py
Normal file
@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class GamesConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "games"
|
||||
22
polylan_submitter/games/decorators.py
Normal file
22
polylan_submitter/games/decorators.py
Normal file
@ -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
|
||||
32
polylan_submitter/games/migrations/0001_initial.py
Normal file
32
polylan_submitter/games/migrations/0001_initial.py
Normal file
@ -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"],
|
||||
},
|
||||
),
|
||||
]
|
||||
@ -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),
|
||||
]
|
||||
31
polylan_submitter/games/migrations/0003_game_path.py
Normal file
31
polylan_submitter/games/migrations/0003_game_path.py
Normal file
@ -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),
|
||||
]
|
||||
0
polylan_submitter/games/migrations/__init__.py
Normal file
0
polylan_submitter/games/migrations/__init__.py
Normal file
17
polylan_submitter/games/models.py
Normal file
17
polylan_submitter/games/models.py
Normal file
@ -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})"
|
||||
7
polylan_submitter/games/schemas.py
Normal file
7
polylan_submitter/games/schemas.py
Normal file
@ -0,0 +1,7 @@
|
||||
from ninja import Schema
|
||||
|
||||
|
||||
class GameOut(Schema):
|
||||
steam_app_id: int
|
||||
name: str
|
||||
path: str
|
||||
@ -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).
|
||||
|
||||
@ -72,7 +72,6 @@ POINTS = {
|
||||
"NOLLA": 10,
|
||||
"CHAOTIC_TRANSMUTATION": 10,
|
||||
"DUPLICATE": 5,
|
||||
"OMEGA": 10,
|
||||
"BURST_2": 10,
|
||||
"BURST_3": 15,
|
||||
"BURST_4": 20,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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", {})
|
||||
|
||||
|
||||
@ -1,22 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from "vue";
|
||||
import { ref, onMounted } from "vue";
|
||||
import { apiService } from "./services/apiService";
|
||||
import type { Game } from "./types";
|
||||
|
||||
const games = computed(() => [
|
||||
{
|
||||
id: "opus-magnum",
|
||||
title: "Opus Magnum",
|
||||
description: "Submit your best Opus Magnum puzzle solutions",
|
||||
appId: 558990,
|
||||
path: "/opus-magnum",
|
||||
},
|
||||
{
|
||||
id: "noita",
|
||||
title: "Noita",
|
||||
description: "Submit your greatest Noita achievements",
|
||||
appId: 881100,
|
||||
path: "/noita",
|
||||
},
|
||||
]);
|
||||
const games = ref<Game[]>([]);
|
||||
const loading = ref(true);
|
||||
|
||||
const imageErrors = ref<Set<number>>(new Set());
|
||||
|
||||
@ -31,6 +19,14 @@ const onImageError = (appId: number) => {
|
||||
const navigate = (path: string) => {
|
||||
window.location.href = path;
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
const response = await apiService.getGames();
|
||||
if (response.data) {
|
||||
games.value = response.data;
|
||||
}
|
||||
loading.value = false;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -44,13 +40,18 @@ const navigate = (path: string) => {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Loading -->
|
||||
<div v-if="loading" class="flex justify-center py-20">
|
||||
<span class="loading loading-spinner loading-lg"></span>
|
||||
</div>
|
||||
|
||||
<!-- Cards Grid -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
<div v-for="game in games" :key="game.id" @click="navigate(game.path)"
|
||||
<div v-else class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
<div v-for="game in games" :key="game.steam_app_id" @click="navigate(game.path)"
|
||||
class="card card-xl bg-base-200 shadow-xl hover:shadow-2xl transition-all cursor-pointer transform hover:-translate-y-2 hover:scale-[1.05] hover:bg-base-100 overflow-hidden">
|
||||
<figure class="relative h-60 bg-base-300 overflow-hidden">
|
||||
<img v-if="!imageErrors.has(game.appId)" :src="getHeaderImage(game.appId)" :alt="game.title"
|
||||
@error="onImageError(game.appId)" class="w-full h-full object-cover" />
|
||||
<img v-if="!imageErrors.has(game.steam_app_id)" :src="getHeaderImage(game.steam_app_id)" :alt="game.name"
|
||||
@error="onImageError(game.steam_app_id)" class="w-full h-full object-cover" />
|
||||
<div v-else
|
||||
class="w-full h-full bg-gradient-to-br from-blue-600 to-blue-400 flex items-center justify-center text-white">
|
||||
<i class="mdi mdi-gamepad-variant text-5xl"></i>
|
||||
@ -58,8 +59,7 @@ const navigate = (path: string) => {
|
||||
<div class="absolute inset-0 bg-black/30 group-hover:bg-black/20 transition-colors"></div>
|
||||
</figure>
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-2xl">{{ game.title }}</h2>
|
||||
<p class="text-base-content/70">{{ game.description }}</p>
|
||||
<h2 class="card-title text-2xl">{{ game.name }}</h2>
|
||||
<div class="card-actions justify-end mt-4">
|
||||
<button class="btn btn-primary">
|
||||
<i class="mdi mdi-arrow-right mr-2"></i>
|
||||
@ -75,7 +75,5 @@ const navigate = (path: string) => {
|
||||
<p>Select a game above to begin submitting</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import type {
|
||||
Game,
|
||||
SteamCollection,
|
||||
SteamCollectionItem,
|
||||
Submission,
|
||||
@ -99,6 +100,11 @@ export class ApiService {
|
||||
}
|
||||
}
|
||||
|
||||
// Games endpoint
|
||||
async getGames(): Promise<ApiResponse<Game[]>> {
|
||||
return this.request<Game[]>('/games/')
|
||||
}
|
||||
|
||||
// Puzzle endpoints
|
||||
async getPuzzles(): Promise<ApiResponse<SteamCollectionItem[]>> {
|
||||
return this.request<SteamCollectionItem[]>('/submissions/puzzles')
|
||||
|
||||
@ -1,3 +1,9 @@
|
||||
export interface Game {
|
||||
steam_app_id: number
|
||||
name: string
|
||||
path: string
|
||||
}
|
||||
|
||||
export interface SteamCollection {
|
||||
id: number
|
||||
steam_id: string
|
||||
|
||||
@ -9,7 +9,9 @@ from django.utils import timezone
|
||||
from django.shortcuts import get_object_or_404
|
||||
from typing import List
|
||||
|
||||
from games.decorators import require_game_enabled
|
||||
from submissions.utils import verify_and_validate_ocr_date_for_submission
|
||||
from ninja.decorators import decorate_view
|
||||
|
||||
from .models import (
|
||||
Submission,
|
||||
@ -28,9 +30,11 @@ from .schemas import (
|
||||
)
|
||||
|
||||
router = Router()
|
||||
OPUS_MAGNUM_APP_ID = 558990
|
||||
|
||||
|
||||
@router.get("/puzzles", response=List[SteamCollectionItemOut])
|
||||
@decorate_view(require_game_enabled(OPUS_MAGNUM_APP_ID))
|
||||
def list_puzzles(request):
|
||||
"""Get list of available puzzles"""
|
||||
return SteamCollectionItem.objects.select_related("collection").filter(
|
||||
@ -39,6 +43,7 @@ def list_puzzles(request):
|
||||
|
||||
|
||||
@router.get("/collection", response=SteamCollectionOut)
|
||||
@decorate_view(require_game_enabled(OPUS_MAGNUM_APP_ID))
|
||||
def get_collection(request):
|
||||
"""Get the active collection details"""
|
||||
collection = get_object_or_404(SteamCollection, is_active=True)
|
||||
@ -46,6 +51,7 @@ def get_collection(request):
|
||||
|
||||
|
||||
@router.get("/submissions", response=List[SubmissionOut])
|
||||
@decorate_view(require_game_enabled(OPUS_MAGNUM_APP_ID))
|
||||
@paginate
|
||||
def list_submissions(request):
|
||||
"""Get paginated list of submissions"""
|
||||
@ -55,6 +61,7 @@ def list_submissions(request):
|
||||
|
||||
|
||||
@router.get("/submissions/{submission_id}", response=SubmissionOut)
|
||||
@decorate_view(require_game_enabled(OPUS_MAGNUM_APP_ID))
|
||||
def get_submission(request, submission_id: str):
|
||||
"""Get detailed submission by ID"""
|
||||
return get_object_or_404(
|
||||
@ -66,6 +73,7 @@ def get_submission(request, submission_id: str):
|
||||
|
||||
|
||||
@router.post("/submissions", response=SubmissionOut)
|
||||
@decorate_view(require_game_enabled(OPUS_MAGNUM_APP_ID))
|
||||
def create_submission(
|
||||
request, data: SubmissionIn, files: List[UploadedFile] = File(...)
|
||||
):
|
||||
@ -198,6 +206,7 @@ def create_submission(
|
||||
|
||||
|
||||
@router.put("/responses/{response_id}/validate", response=PuzzleResponseOut)
|
||||
@decorate_view(require_game_enabled(OPUS_MAGNUM_APP_ID))
|
||||
def validate_response(request, response_id: int, data: ValidationIn):
|
||||
"""Manually validate a puzzle response"""
|
||||
|
||||
@ -233,6 +242,7 @@ def validate_response(request, response_id: int, data: ValidationIn):
|
||||
|
||||
|
||||
@router.put("/responses/{response_id}/validate/auto", response=PuzzleResponseOut)
|
||||
@decorate_view(require_game_enabled(OPUS_MAGNUM_APP_ID))
|
||||
def validate_auto(request, response_id: int):
|
||||
"""Try to auto validate a puzzle response"""
|
||||
|
||||
@ -248,6 +258,7 @@ def validate_auto(request, response_id: int):
|
||||
|
||||
|
||||
@router.get("/responses/needs-validation", response=List[PuzzleResponseOut])
|
||||
@decorate_view(require_game_enabled(OPUS_MAGNUM_APP_ID))
|
||||
def list_responses_needing_validation(request):
|
||||
"""Get all responses that need manual validation"""
|
||||
|
||||
@ -263,6 +274,7 @@ def list_responses_needing_validation(request):
|
||||
|
||||
|
||||
@router.post("/submissions/{submission_id}/validate", response=SubmissionOut)
|
||||
@decorate_view(require_game_enabled(OPUS_MAGNUM_APP_ID))
|
||||
def validate_submission(request, submission_id: str):
|
||||
"""Mark entire submission as validated"""
|
||||
|
||||
@ -291,6 +303,7 @@ def validate_submission(request, submission_id: str):
|
||||
|
||||
|
||||
@router.delete("/submissions/{submission_id}")
|
||||
@decorate_view(require_game_enabled(OPUS_MAGNUM_APP_ID))
|
||||
def delete_submission(request, submission_id: str):
|
||||
"""Delete a submission (admin only)"""
|
||||
|
||||
@ -307,6 +320,7 @@ def delete_submission(request, submission_id: str):
|
||||
|
||||
|
||||
@router.get("/stats")
|
||||
@decorate_view(require_game_enabled(OPUS_MAGNUM_APP_ID))
|
||||
def get_stats(request):
|
||||
"""Get submission statistics"""
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user