From fa76fbce9233cd6863284c3406baf29c1709dc70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Gremaud?= Date: Fri, 28 Nov 2025 14:05:26 +0100 Subject: [PATCH] mutiple fixes --- opus_submitter/accounts/models.py | 13 ++ opus_submitter/animations/__init__.py | 0 opus_submitter/animations/admin.py | 59 +++++ opus_submitter/animations/api.py | 36 +++ opus_submitter/animations/apps.py | 6 + .../animations/migrations/0001_initial.py | 26 +++ .../migrations/0002_puzzlepointsvalue.py | 22 ++ .../animations/migrations/__init__.py | 0 opus_submitter/animations/models.py | 24 ++ opus_submitter/animations/schemas.py | 37 +++ opus_submitter/animations/tests.py | 3 + opus_submitter/animations/views.py | 3 + opus_submitter/opus_submitter/api.py | 2 + opus_submitter/opus_submitter/settings.py | 1 + opus_submitter/package.json | 1 + opus_submitter/pnpm-lock.yaml | 20 ++ opus_submitter/src/App.vue | 9 +- opus_submitter/src/components/AdminPanel.vue | 40 ++-- opus_submitter/src/components/PuzzleCard.vue | 7 +- opus_submitter/src/components/Results.vue | 12 + opus_submitter/src/services/apiService.ts | 76 ++---- opus_submitter/src/services/ocrService.ts | 11 +- opus_submitter/src/stores/submissions.ts | 9 +- opus_submitter/src/types/index.ts | 26 +-- opus_submitter/submissions/admin.py | 3 +- opus_submitter/submissions/api.py | 10 +- .../0009_steamcollectionitem_points_factor.py | 20 ++ ..._puzzleresponse_validated_area_and_more.py | 28 +++ ...area_alter_puzzleresponse_cost_and_more.py | 28 +++ ..._puzzleresponse_validated_area_and_more.py | 28 +++ .../0013_steamcollectionitem_points_value.py | 20 ++ opus_submitter/submissions/models.py | 110 ++++++++- opus_submitter/submissions/schemas.py | 18 +- opus_submitter/submissions/utils.py | 9 +- pyproject.toml | 3 +- uv.lock | 216 +++++++++--------- 36 files changed, 694 insertions(+), 242 deletions(-) create mode 100644 opus_submitter/animations/__init__.py create mode 100644 opus_submitter/animations/admin.py create mode 100644 opus_submitter/animations/api.py create mode 100644 opus_submitter/animations/apps.py create mode 100644 opus_submitter/animations/migrations/0001_initial.py create mode 100644 opus_submitter/animations/migrations/0002_puzzlepointsvalue.py create mode 100644 opus_submitter/animations/migrations/__init__.py create mode 100644 opus_submitter/animations/models.py create mode 100644 opus_submitter/animations/schemas.py create mode 100644 opus_submitter/animations/tests.py create mode 100644 opus_submitter/animations/views.py create mode 100644 opus_submitter/src/components/Results.vue create mode 100644 opus_submitter/submissions/migrations/0009_steamcollectionitem_points_factor.py create mode 100644 opus_submitter/submissions/migrations/0010_alter_puzzleresponse_validated_area_and_more.py create mode 100644 opus_submitter/submissions/migrations/0011_alter_puzzleresponse_area_alter_puzzleresponse_cost_and_more.py create mode 100644 opus_submitter/submissions/migrations/0012_alter_puzzleresponse_validated_area_and_more.py create mode 100644 opus_submitter/submissions/migrations/0013_steamcollectionitem_points_value.py diff --git a/opus_submitter/accounts/models.py b/opus_submitter/accounts/models.py index b0abfda..ae65750 100644 --- a/opus_submitter/accounts/models.py +++ b/opus_submitter/accounts/models.py @@ -2,6 +2,14 @@ from django.contrib.auth.models import AbstractUser from django.db import models +class UserQuerySet(models.QuerySet): + pass + + +class UserManager(models.Manager.from_queryset(UserQuerySet)): + pass + + class CustomUser(AbstractUser): """Custom User model to store CAS attributes from PolyLAN.""" @@ -14,6 +22,11 @@ class CustomUser(AbstractUser): # Additional fields that might come from CAS cas_attributes = models.JSONField(default=dict, blank=True) + objects = UserManager() + + class Meta: + _base_manager_name = "objects" + def __str__(self): return f"{self.username} ({self.cas_user_id})" diff --git a/opus_submitter/animations/__init__.py b/opus_submitter/animations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/opus_submitter/animations/admin.py b/opus_submitter/animations/admin.py new file mode 100644 index 0000000..b48e135 --- /dev/null +++ b/opus_submitter/animations/admin.py @@ -0,0 +1,59 @@ +from django.contrib import admin +from animations.models import PuzzlePointsFactor, PuzzlePointsValue + + +@admin.register(PuzzlePointsFactor) +class PuzzlePointsFactorAdmin(admin.ModelAdmin): + list_display = [ + "id", + "cost", + "cycles", + "area", + "special_notes", + ] + list_filter = ["cost", "cycles", "area", "special_notes"] + search_fields = ["cost", "cycles", "area", "special_notes"] + readonly_fields = ["created_at", "updated_at"] + + fieldsets = ( + ( + "Basic Information", + {"fields": ("cost", "cycles", "area")}, + ), + ( + "Special notes", + { + "fields": ("special_notes",), + "description": "Special notes about the puzzle. May be some extra restriction, etc...", + }, + ), + ( + "Metadata", + { + "fields": ("created_at", "updated_at"), + "classes": ("collapse",), + }, + ), + ) + + +@admin.register(PuzzlePointsValue) +class PuzzlePointsValueAdmin(admin.ModelAdmin): + list_display = ["id", "points"] + list_filter = ["points"] + search_fields = ["points"] + readonly_fields = ["created_at", "updated_at"] + + fieldsets = ( + ( + "Basic Information", + {"fields": ("points",)}, + ), + ( + "Metadata", + { + "fields": ("created_at", "updated_at"), + "classes": ("collapse",), + }, + ), + ) diff --git a/opus_submitter/animations/api.py b/opus_submitter/animations/api.py new file mode 100644 index 0000000..a1bef72 --- /dev/null +++ b/opus_submitter/animations/api.py @@ -0,0 +1,36 @@ +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() + + +@router.get("results", response=RankingSchema) +def results(request: HttpRequest) -> dict: + responses_by_userid = defaultdict(list) + responses_by_puzzleid = defaultdict(list) + + for response in list( + PuzzleResponse.objects.filter(needs_manual_validation=False) + .filter_user_best_response() + .prefetch_related("submission__user") + ): + responses_by_userid[response.submission.user.id].append(response) + responses_by_puzzleid[response.puzzle.id].append(response) + + ranking = {} + + for puzzle_id, responses in responses_by_puzzleid.items(): + ranking[puzzle_id] = sorted(responses, key=lambda x: x.rank_points) + + return { + "users": CustomUser.objects.filter(pk__in=responses_by_userid.keys()), + "puzzles": SteamCollectionItem.objects.all(), + "responses_by_userid": responses_by_userid, + "ranking_by_puzzle": ranking, + } diff --git a/opus_submitter/animations/apps.py b/opus_submitter/animations/apps.py new file mode 100644 index 0000000..0c38973 --- /dev/null +++ b/opus_submitter/animations/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class AnimationsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'animations' diff --git a/opus_submitter/animations/migrations/0001_initial.py b/opus_submitter/animations/migrations/0001_initial.py new file mode 100644 index 0000000..56d383f --- /dev/null +++ b/opus_submitter/animations/migrations/0001_initial.py @@ -0,0 +1,26 @@ +# Generated by Django 5.2.7 on 2025-11-23 22:33 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='PuzzlePointsFactor', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('cost', models.IntegerField()), + ('cycles', models.IntegerField()), + ('area', models.IntegerField()), + ('special_notes', models.TextField(blank=True)), + ], + ), + ] diff --git a/opus_submitter/animations/migrations/0002_puzzlepointsvalue.py b/opus_submitter/animations/migrations/0002_puzzlepointsvalue.py new file mode 100644 index 0000000..0acebb4 --- /dev/null +++ b/opus_submitter/animations/migrations/0002_puzzlepointsvalue.py @@ -0,0 +1,22 @@ +# Generated by Django 5.2.7 on 2025-11-24 01:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('animations', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='PuzzlePointsValue', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('points', models.JSONField(default=[])), + ], + ), + ] diff --git a/opus_submitter/animations/migrations/__init__.py b/opus_submitter/animations/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/opus_submitter/animations/models.py b/opus_submitter/animations/models.py new file mode 100644 index 0000000..bd0231d --- /dev/null +++ b/opus_submitter/animations/models.py @@ -0,0 +1,24 @@ +from django.db import models + + +class PuzzlePointsFactor(models.Model): + # Timestamps + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + cost = models.IntegerField() + cycles = models.IntegerField() + area = models.IntegerField() + + special_notes = models.TextField(blank=True) + + def __str__(self) -> str: + return f"{self.cost} - {self.cycles} - {self.area}" + + +class PuzzlePointsValue(models.Model): + # Timestamps + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + points = models.JSONField(default=[]) diff --git a/opus_submitter/animations/schemas.py b/opus_submitter/animations/schemas.py new file mode 100644 index 0000000..bf04b84 --- /dev/null +++ b/opus_submitter/animations/schemas.py @@ -0,0 +1,37 @@ +from ninja import ModelSchema, Schema + +from submissions.models import PuzzleResponse +from submissions.schemas import SteamCollectionItemOut, UserInfoOut + + +class PuzzleResponseRankingOut(ModelSchema): + class Meta: + model = PuzzleResponse + fields = [ + "id", + "puzzle_name", + "created_at", + "updated_at", + ] + + points: int + rank_points: int + puzzle_user_rank: int + user_response_rank: int + + user_id: int + + final_cost: int | None + final_cycles: int | None + final_area: int | None + + @staticmethod + def resolve_user_id(obj) -> int: + return obj.submission.user.id + + +class RankingSchema(Schema): + users: list[UserInfoOut] + puzzles: list[SteamCollectionItemOut] + responses_by_userid: dict[int, list[PuzzleResponseRankingOut]] + ranking_by_puzzle: dict[int, list[PuzzleResponseRankingOut]] diff --git a/opus_submitter/animations/tests.py b/opus_submitter/animations/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/opus_submitter/animations/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/opus_submitter/animations/views.py b/opus_submitter/animations/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/opus_submitter/animations/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/opus_submitter/opus_submitter/api.py b/opus_submitter/opus_submitter/api.py index 388b623..ba9ef47 100644 --- a/opus_submitter/opus_submitter/api.py +++ b/opus_submitter/opus_submitter/api.py @@ -1,6 +1,7 @@ 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 # Create the main API instance api = NinjaAPI( @@ -26,6 +27,7 @@ It provides features for user authentication, puzzle listing, submission uploads # Include the submissions router api.add_router("/submissions/", submissions_router, tags=["submissions"]) +api.add_router("/results/", results_router, tags=["results"]) # Health check endpoint diff --git a/opus_submitter/opus_submitter/settings.py b/opus_submitter/opus_submitter/settings.py index 7f5b2dc..dbcc3ff 100644 --- a/opus_submitter/opus_submitter/settings.py +++ b/opus_submitter/opus_submitter/settings.py @@ -40,6 +40,7 @@ INSTALLED_APPS = [ "django.contrib.staticfiles", "django_vite", "accounts", + "animations", "submissions", ] diff --git a/opus_submitter/package.json b/opus_submitter/package.json index b267e08..c94375e 100644 --- a/opus_submitter/package.json +++ b/opus_submitter/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@tailwindcss/vite": "^4.1.16", + "@tanstack/vue-table": "^8.21.3", "@vueuse/core": "^14.0.0", "install": "^0.13.0", "pinia": "^3.0.3", diff --git a/opus_submitter/pnpm-lock.yaml b/opus_submitter/pnpm-lock.yaml index 7a92bc2..fe1017e 100644 --- a/opus_submitter/pnpm-lock.yaml +++ b/opus_submitter/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@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)) + '@tanstack/vue-table': + specifier: ^8.21.3 + version: 8.21.3(vue@3.5.22(typescript@5.9.3)) '@vueuse/core': specifier: ^14.0.0 version: 14.0.0(vue@3.5.22(typescript@5.9.3)) @@ -452,6 +455,16 @@ packages: peerDependencies: vite: ^5.2.0 || ^6 || ^7 + '@tanstack/table-core@8.21.3': + resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==} + engines: {node: '>=12'} + + '@tanstack/vue-table@8.21.3': + resolution: {integrity: sha512-rusRyd77c5tDPloPskctMyPLFEQUeBzxdQ+2Eow4F7gDPlPOB1UnnhzfpdvqZ8ZyX2rRNGmqNnQWm87OI2OQPw==} + engines: {node: '>=12'} + peerDependencies: + vue: '>=3.2' + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -1120,6 +1133,13 @@ snapshots: tailwindcss: 4.1.16 vite: 7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2) + '@tanstack/table-core@8.21.3': {} + + '@tanstack/vue-table@8.21.3(vue@3.5.22(typescript@5.9.3))': + dependencies: + '@tanstack/table-core': 8.21.3 + vue: 3.5.22(typescript@5.9.3) + '@types/estree@1.0.8': {} '@types/node@24.9.2': diff --git a/opus_submitter/src/App.vue b/opus_submitter/src/App.vue index 7806048..1ce6c0b 100644 --- a/opus_submitter/src/App.vue +++ b/opus_submitter/src/App.vue @@ -3,6 +3,7 @@ import { ref, onMounted, computed } from "vue"; import PuzzleCard from "@/components/PuzzleCard.vue"; import SubmissionForm from "@/components/SubmissionForm.vue"; import AdminPanel from "@/components/AdminPanel.vue"; +import Results from "@/components/Results.vue"; import { apiService, errorHelpers } from "@/services/apiService"; import { usePuzzlesStore } from "@/stores/puzzles"; import { useSubmissionsStore } from "@/stores/submissions"; @@ -39,10 +40,10 @@ const responsesByPuzzle = computed(() => { submissions.value.forEach((submission) => { submission.responses.forEach((response) => { // Handle both number and object types for puzzle field - if (!grouped[response.puzzle]) { - grouped[response.puzzle] = []; + if (!grouped[response.puzzle_id]) { + grouped[response.puzzle_id] = []; } - grouped[response.puzzle].push(response); + grouped[response.puzzle_id].push(response); }); }); return grouped; @@ -197,6 +198,8 @@ const reloadPage = () => { + +
diff --git a/opus_submitter/src/components/AdminPanel.vue b/opus_submitter/src/components/AdminPanel.vue index 2b1d894..6944d87 100644 --- a/opus_submitter/src/components/AdminPanel.vue +++ b/opus_submitter/src/components/AdminPanel.vue @@ -151,10 +151,6 @@
-
-
{{ validationModal}}
-
-
@@ -194,7 +190,9 @@ v-model="validationModal.data.validated_cost" type="text" class="input input-bordered input-sm" - :placeholder="validationModal.response.cost || 'Enter cost'" + :placeholder=" + validationModal.response.cost?.toString() || 'Enter cost' + " />
@@ -206,7 +204,9 @@ v-model="validationModal.data.validated_cycles" type="text" class="input input-bordered input-sm" - :placeholder="validationModal.response.cycles || 'Enter cycles'" + :placeholder=" + validationModal.response.cycles?.toString() || 'Enter cycles' + " />
@@ -218,7 +218,9 @@ v-model="validationModal.data.validated_area" type="text" class="input input-bordered input-sm" - :placeholder="validationModal.response.area || 'Enter area'" + :placeholder=" + validationModal.response.area?.toString() || 'Enter area' + " /> @@ -239,6 +241,10 @@ {{ isValidating ? "Validating..." : "Validate" }} + +
+
{{ validationModal}}
+
@@ -270,9 +276,9 @@ const validationModal = ref({ response: null as PuzzleResponse | null, data: { puzzle: -1, - validated_cost: "", - validated_cycles: "", - validated_area: "", + validated_cost: 0, + validated_cycles: 0, + validated_area: 0, }, }); @@ -332,10 +338,10 @@ const autoValidationResponse = async () => { const openValidationModal = (response: PuzzleResponse) => { validationModal.value.response = response; validationModal.value.data = { - puzzle: response.puzzle || -1, - validated_cost: response.cost || "", - validated_cycles: response.cycles || "", - validated_area: response.area || "", + puzzle: response.puzzle_id || -1, + validated_cost: response.cost || 0, + validated_cycles: response.cycles || 0, + validated_area: response.area || 0, }; validationModal.value.show = true; }; @@ -345,9 +351,9 @@ const closeValidationModal = () => { validationModal.value.response = null; validationModal.value.data = { puzzle: -1, - validated_cost: "", - validated_cycles: "", - validated_area: "", + validated_cost: 0, + validated_cycles: 0, + validated_area: 0, }; }; diff --git a/opus_submitter/src/components/PuzzleCard.vue b/opus_submitter/src/components/PuzzleCard.vue index ee75c30..7f593d4 100644 --- a/opus_submitter/src/components/PuzzleCard.vue +++ b/opus_submitter/src/components/PuzzleCard.vue @@ -1,6 +1,7 @@