Compare commits

..

No commits in common. "d2a9dbe4a423f7e22809cf173383968804d32d55" and "eb1eed852bf1d9eb5471246df272b719862cf0e7" have entirely different histories.

131 changed files with 114 additions and 2439 deletions

View File

@ -1,12 +1,12 @@
from collections import defaultdict
from django.http import HttpRequest
from django.http.request import HttpRequest
from ninja import Router
from collections import defaultdict
from accounts.models import CustomUser
from animations.schemas import RankingSchema
from submissions.models import PuzzleResponse, SteamCollectionItem
router = Router()
@ -26,9 +26,7 @@ def results(request: HttpRequest) -> dict:
ranking = {}
for puzzle_id, responses in responses_by_puzzleid.items():
ranking[puzzle_id] = sorted(
responses, key=lambda x: (x.rank_points is None, x.rank_points or 0)
)
ranking[puzzle_id] = sorted(responses, key=lambda x: x.rank_points)
return {
"users": CustomUser.objects.filter(pk__in=responses_by_userid.keys()),

View File

@ -14,8 +14,8 @@ class PuzzleResponseRankingOut(ModelSchema):
"updated_at",
]
points: int | None = None
rank_points: int | None = None
points: int
rank_points: int
puzzle_user_rank: int
user_response_rank: int
@ -30,13 +30,8 @@ class PuzzleResponseRankingOut(ModelSchema):
return obj.submission.user.id
class UserDisplayOut(Schema):
id: int
username: str
class RankingSchema(Schema):
users: list[UserDisplayOut]
users: list[UserInfoOut]
puzzles: list[SteamCollectionItemOut]
responses_by_userid: dict[int, list[PuzzleResponseRankingOut]]
ranking_by_puzzle: dict[int, list[PuzzleResponseRankingOut]]

View File

@ -7,7 +7,7 @@ import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "polylan_submitter.settings")
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "opus_submitter.settings")
try:
from django.core.management import execute_from_command_line
except ImportError as exc:

View File

@ -2,18 +2,15 @@ from ninja import NinjaAPI
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
# Create the main API instance
api = NinjaAPI(
title="PolyLAN Submission API",
title="Opus Magnum Submission API",
version="1.0.0",
description="""API for managing Opus Magnum puzzle submissions, and Noita runs.
description="""API for managing Opus Magnum puzzle submissions.
The Opus Magnum Submission API allows clients to upload, manage, validate, and review puzzle solution submissions for the Opus Magnum puzzle game community.
It provides features for user authentication, puzzle listing, submission uploads, automated and manual OCR validation, and administrative workflows.
The Noita Submission API allows clients to upload the result of the log file of the PolyLAN noita mod. It parses the output, and store each objectiv made by the user.
""",
openapi_extra={
"info": {
@ -31,14 +28,30 @@ The Noita Submission API allows clients to upload the result of the log file of
# Include the submissions router
api.add_router("/submissions/", submissions_router, tags=["submissions"])
api.add_router("/results/", results_router, tags=["results"])
api.add_router("/noita/", noita_router, tags=["noita"])
# Health check endpoint
@api.get("/health")
def health_check(request):
"""Health check endpoint"""
return {"status": "healthy", "service": "polylan-submitter-api"}
return {"status": "healthy", "service": "opus-magnum-api"}
# API info endpoint
@api.get("/info")
def api_info(request):
"""Get API information"""
return {
"name": "Opus Magnum Submission API",
"version": "1.0.0",
"description": "API for managing puzzle submissions with OCR validation",
"features": [
"Multi-puzzle submissions",
"OCR validation",
"Manual validation workflow",
"Admin validation tools",
],
}
# User info endpoint

View File

@ -1,5 +1,5 @@
"""
ASGI config for polylan_submitter project.
ASGI config for opus_submitter project.
It exposes the ASGI callable as a module-level variable named ``application``.
@ -11,6 +11,6 @@ import os
from django.core.asgi import get_asgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "polylan_submitter.settings")
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "opus_submitter.settings")
application = get_asgi_application()

View File

@ -1,5 +1,5 @@
"""
Django settings for polylan_submitter project.
Django settings for opus_submitter project.
Generated by 'django-admin startproject' using Django 5.2.7.
@ -42,7 +42,6 @@ INSTALLED_APPS = [
"accounts",
"animations",
"submissions",
"noita",
]
MIDDLEWARE = [
@ -56,7 +55,7 @@ MIDDLEWARE = [
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
ROOT_URLCONF = "polylan_submitter.urls"
ROOT_URLCONF = "opus_submitter.urls"
TEMPLATES = [
{
@ -73,7 +72,7 @@ TEMPLATES = [
},
]
WSGI_APPLICATION = "polylan_submitter.wsgi.application"
WSGI_APPLICATION = "opus_submitter.wsgi.application"
# Database
@ -178,4 +177,4 @@ STATICFILES_DIRS = [
os.path.join(BASE_DIR, "static_source/vite"),
]
from polylan_submitter.settingsLocal import * # noqa
from opus_submitter.settingsLocal import * # noqa

View File

@ -1,5 +1,5 @@
"""
URL configuration for polylan_submitter project.
URL configuration for opus_submitter project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/5.2/topics/http/urls/
@ -28,34 +28,22 @@ from .api import api
@login_required
def home(request: HttpRequest):
return render(request, "home.html", {})
@login_required
def opus_magnum_home(request: HttpRequest):
from submissions.models import SteamCollection
return render(
request,
"opus-magnum.html",
"index.html",
{
"collection": SteamCollection.objects.filter(is_active=True).last(),
},
)
@login_required
def noita_home(request: HttpRequest):
return render(request, "noita.html", {})
urlpatterns = [
path("admin/", admin.site.urls),
path("cas/login/", SimpleCASLoginView.as_view(), name="cas_ng_login"),
path("cas/logout/", SimpleCASLogoutView.as_view(), name="cas_ng_logout"),
path("api/", api.urls),
path("opus-magnum", opus_magnum_home, name="opus-magnum.home"),
path("noita", noita_home, name="noita.home"),
path("", home, name="home"),
]

View File

@ -1,5 +1,5 @@
"""
WSGI config for polylan_submitter project.
WSGI config for opus_submitter project.
It exposes the WSGI callable as a module-level variable named ``application``.
@ -11,6 +11,6 @@ import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "polylan_submitter.settings")
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "opus_submitter.settings")
application = get_wsgi_application()

View File

@ -1,5 +1,5 @@
{
"name": "polylan_submitter",
"name": "opus_submitter",
"private": true,
"version": "0.0.0",
"type": "module",

View File

@ -10,7 +10,7 @@ importers:
dependencies:
'@tailwindcss/vite':
specifier: ^4.1.16
version: 4.1.16(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.4))
version: 4.1.16(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2))
'@tanstack/vue-table':
specifier: ^8.21.3
version: 8.21.3(vue@3.5.22(typescript@5.9.3))
@ -41,7 +41,7 @@ importers:
version: 24.9.2
'@vitejs/plugin-vue':
specifier: ^6.0.1
version: 6.0.1(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.4))(vue@3.5.22(typescript@5.9.3))
version: 6.0.1(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2))(vue@3.5.22(typescript@5.9.3))
'@vue/tsconfig':
specifier: ^0.8.1
version: 0.8.1(typescript@5.9.3)(vue@3.5.22(typescript@5.9.3))
@ -53,7 +53,7 @@ importers:
version: 5.9.3
vite:
specifier: ^7.1.7
version: 7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.4)
version: 7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)
vue-tsc:
specifier: ^3.1.0
version: 3.1.2(typescript@5.9.3)
@ -880,11 +880,6 @@ packages:
whatwg-url@5.0.0:
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
yaml@2.8.4:
resolution: {integrity: sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog==}
engines: {node: '>= 14.6'}
hasBin: true
zlibjs@0.3.1:
resolution: {integrity: sha512-+J9RrgTKOmlxFSDHo0pI1xM6BLVUv+o0ZT9ANtCxGkjIVCCUdx9alUF8Gm+dGLKbkkkidWIHFDZHDMpfITt4+w==}
@ -1131,12 +1126,12 @@ snapshots:
'@tailwindcss/oxide-win32-arm64-msvc': 4.1.16
'@tailwindcss/oxide-win32-x64-msvc': 4.1.16
'@tailwindcss/vite@4.1.16(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.4))':
'@tailwindcss/vite@4.1.16(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2))':
dependencies:
'@tailwindcss/node': 4.1.16
'@tailwindcss/oxide': 4.1.16
tailwindcss: 4.1.16
vite: 7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.4)
vite: 7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)
'@tanstack/table-core@8.21.3': {}
@ -1153,10 +1148,10 @@ snapshots:
'@types/web-bluetooth@0.0.21': {}
'@vitejs/plugin-vue@6.0.1(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.4))(vue@3.5.22(typescript@5.9.3))':
'@vitejs/plugin-vue@6.0.1(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2))(vue@3.5.22(typescript@5.9.3))':
dependencies:
'@rolldown/pluginutils': 1.0.0-beta.29
vite: 7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.4)
vite: 7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)
vue: 3.5.22(typescript@5.9.3)
'@volar/language-core@2.4.23':
@ -1508,7 +1503,7 @@ snapshots:
undici-types@7.16.0: {}
vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.4):
vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2):
dependencies:
esbuild: 0.25.11
fdir: 6.5.0(picomatch@4.0.3)
@ -1521,7 +1516,6 @@ snapshots:
fsevents: 2.3.3
jiti: 2.6.1
lightningcss: 1.30.2
yaml: 2.8.4
vscode-uri@3.1.0: {}
@ -1550,7 +1544,4 @@ snapshots:
tr46: 0.0.3
webidl-conversions: 3.0.1
yaml@2.8.4:
optional: true
zlibjs@0.3.1: {}

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -108,24 +108,17 @@ const findPuzzleByName = (ocrPuzzleName: string) => {
const reloadPage = () => {
window.location.reload();
};
const goHome = () => {
window.location.href = "/";
};
</script>
<template>
<div class="min-h-screen bg-base-200">
<!-- Header -->
<div class="navbar bg-base-100 shadow-lg">
<div class="container mx-auto w-full flex items-center gap-4">
<button @click="goHome" class="btn btn-primary btn-sm">
<i class="mdi mdi-arrow-left"></i>
Back
</button>
<div class="container mx-auto">
<div class="flex-1">
<h1 class="text-xl font-bold">Opus Magnum Puzzle Submitter</h1>
<div class="flex-1"></div>
<div class="flex items-center gap-4">
</div>
<div class="flex items-start justify-between">
<div
v-if="userInfo?.is_authenticated"
class="flex items-center gap-2"
@ -140,11 +133,15 @@ const goHome = () => {
</div>
</div>
<div v-else class="text-sm text-base-content/70">Not logged in</div>
<div class="flex flex-col items-end gap-2">
<a href="/api/docs" class="btn btn-xs">API docs</a>
</div>
<div class="flex flex-col items-end gap-2">
<a href="/admin" class="btn btn-xs btn-warning">Admin panel</a>
</div>
</div>
</div>
</div>
<!-- Main Content -->
<div class="container mx-auto px-4 py-8">

View File

@ -0,0 +1,12 @@
<script setup lang="ts"></script>
<template>
<div class="mb-8">
<div class="card bg-base-100 shadow-lg">
<div class="card-body">
<h2 class="card-title text-2xl">General Results</h2>
<div class="flex flex-wrap gap-4 mt-4">TODO :)</div>
</div>
</div>
</div>
</template>

View File

@ -1,11 +1,11 @@
import { createApp } from 'vue'
import OpusMagnum from '@/OpusMagnum.vue'
import App from '@/App.vue'
import { pinia } from '@/stores'
import '@/style.css'
// const app = createApp(App)
const selector = "#app"
const mountData = document.querySelector<HTMLElement>(selector)
const app = createApp(OpusMagnum, { ...mountData?.dataset })
const app = createApp(App, { ...mountData?.dataset })
app.use(pinia)
app.mount(selector)

View File

@ -0,0 +1,33 @@
{
"node_modules/.pnpm/@mdi+font@7.4.47/node_modules/@mdi/font/fonts/materialdesignicons-webfont.eot": {
"file": "assets/materialdesignicons-webfont-CSr8KVlo.eot",
"src": "node_modules/.pnpm/@mdi+font@7.4.47/node_modules/@mdi/font/fonts/materialdesignicons-webfont.eot"
},
"node_modules/.pnpm/@mdi+font@7.4.47/node_modules/@mdi/font/fonts/materialdesignicons-webfont.ttf": {
"file": "assets/materialdesignicons-webfont-B7mPwVP_.ttf",
"src": "node_modules/.pnpm/@mdi+font@7.4.47/node_modules/@mdi/font/fonts/materialdesignicons-webfont.ttf"
},
"node_modules/.pnpm/@mdi+font@7.4.47/node_modules/@mdi/font/fonts/materialdesignicons-webfont.woff": {
"file": "assets/materialdesignicons-webfont-PXm3-2wK.woff",
"src": "node_modules/.pnpm/@mdi+font@7.4.47/node_modules/@mdi/font/fonts/materialdesignicons-webfont.woff"
},
"node_modules/.pnpm/@mdi+font@7.4.47/node_modules/@mdi/font/fonts/materialdesignicons-webfont.woff2": {
"file": "assets/materialdesignicons-webfont-Dp5v-WZN.woff2",
"src": "node_modules/.pnpm/@mdi+font@7.4.47/node_modules/@mdi/font/fonts/materialdesignicons-webfont.woff2"
},
"src/main.ts": {
"file": "assets/main-CNlI4PW6.js",
"name": "main",
"src": "src/main.ts",
"isEntry": true,
"css": [
"assets/main-HDjkw-xK.css"
],
"assets": [
"assets/materialdesignicons-webfont-CSr8KVlo.eot",
"assets/materialdesignicons-webfont-Dp5v-WZN.woff2",
"assets/materialdesignicons-webfont-PXm3-2wK.woff",
"assets/materialdesignicons-webfont-B7mPwVP_.ttf"
]
}
}

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -7,7 +7,7 @@ from django.utils import timezone
from django.shortcuts import get_object_or_404
from typing import List
from submissions.utils import verify_and_validate_ocr_date_for_submission
from opus_submitter.submissions.utils import verify_and_validate_ocr_date_for_submission
from .models import Submission, PuzzleResponse, SubmissionFile, SteamCollectionItem
from .schemas import (

View File

@ -5,13 +5,11 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{% block title %}{% endblock %}</title>
{% if debug %}{% vite_hmr_client %}{% endif %}
{% block head %}
{% endblock %}
<title>Opus Magnum Puzzle Submitter</title>
{% vite_hmr_client %}
{% vite_asset 'src/main.ts' %}
</head>
<body>
{% block content %}
{% endblock %}
<div id="app" data-collection-title="{{ collection.title }}" data-collection-url="{{ collection.url }}" data-collection-description="{{ collection.description }}"></div>
</body>
</html>

View File

@ -21,7 +21,7 @@ export default defineConfig({
outDir: resolve("./static_source/vite"),
rollupOptions: {
input:
{ opus_magnum: resolve('./src/opus-magnum.ts') }
{ main: resolve('./src/main.ts') }
}
},
})

Some files were not shown because too many files have changed in this diff Show More