From 407cc22911c6ac3d4bea3d762f032a71397bbf05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Gremaud?= Date: Sun, 25 Oct 2026 14:20:49 +0100 Subject: [PATCH] opus-stats-score --- polylan_submitter/opus_magnum/api.py | 42 +++++++++++++++ polylan_submitter/opus_magnum/tests.py | 75 +++++++++++++++++++++++++- 2 files changed, 116 insertions(+), 1 deletion(-) diff --git a/polylan_submitter/opus_magnum/api.py b/polylan_submitter/opus_magnum/api.py index 4f62be5..dbbe906 100644 --- a/polylan_submitter/opus_magnum/api.py +++ b/polylan_submitter/opus_magnum/api.py @@ -1,3 +1,5 @@ +import csv + from ninja import Router, File from ninja.files import UploadedFile from ninja.pagination import paginate @@ -5,6 +7,8 @@ from ninja.errors import HttpError from django.db import transaction from django.core.files.base import ContentFile from django.core.cache import cache +from django.http import HttpRequest, HttpResponse +from ninja.security import django_auth_is_staff from django.utils import timezone from django.shortcuts import get_object_or_404 from typing import List @@ -395,3 +399,41 @@ def get_stats(request, event_path: str): if total_responses else 0, } + + +@router.get(f"{PREFIX}/points.csv", auth=django_auth_is_staff) +@decorate_view(require_game_enabled(OPUS_MAGNUM_APP_ID)) +def points_csv(request: HttpRequest, event_path: str): + """Admin-only: points per player for each puzzle of the active collection, + one column per puzzle (1 puzzle = 1 round), `;` separated.""" + puzzles = list( + SteamCollectionItem.objects.filter( + collection__is_active=True, collection__event=request.event + ).order_by("order_index") + ) + + points = {} + for response in ( + PuzzleResponse.objects.filter( + needs_manual_validation=False, + submission__event=request.event, + submission__user__isnull=False, + puzzle__in=puzzles, + ) + .filter_user_best_response() + .select_related("submission__user") + ): + user_points = points.setdefault(response.submission.user.username, {}) + user_points[response.puzzle_id] = response.rank_points or 0 + + # text/plain so browsers render it inline - most force-download text/csv + response = HttpResponse(content_type="text/plain; charset=utf-8") + writer = csv.writer(response, delimiter=";") + writer.writerow(["username", *(puzzle.title for puzzle in puzzles)]) + for username, user_points in sorted( + points.items(), key=lambda item: (-sum(item[1].values()), item[0]) + ): + writer.writerow( + [username, *(user_points.get(puzzle.id, 0) for puzzle in puzzles)] + ) + return response diff --git a/polylan_submitter/opus_magnum/tests.py b/polylan_submitter/opus_magnum/tests.py index a39b155..9f1b6be 100644 --- a/polylan_submitter/opus_magnum/tests.py +++ b/polylan_submitter/opus_magnum/tests.py @@ -1 +1,74 @@ -# Create your tests here. +from django.test import TestCase + +from accounts.models import CustomUser +from animations.models import PuzzlePointsFactor, PuzzlePointsValue +from events.models import Event +from games.models import EventGame, Game + +from .api import OPUS_MAGNUM_APP_ID +from .models import PuzzleResponse, SteamCollection, SteamCollectionItem, Submission + +URL = "/api/events/event/opus-magnum/points.csv" + + +class PointsCsvTests(TestCase): + def setUp(self): + self.event = Event.objects.create(name="Event", path="event") + game = Game.objects.create(name="Opus Magnum", steam_app_id=OPUS_MAGNUM_APP_ID) + EventGame.objects.create(event=self.event, game=game) + collection = SteamCollection.objects.create( + event=self.event, steam_id="1", url="https://example.com", is_active=True + ) + factor = PuzzlePointsFactor.objects.create(cost=1, cycles=1, area=1) + value = PuzzlePointsValue.objects.create(points=[10, 5, 1]) + self.p1, self.p2 = ( + SteamCollectionItem.objects.create( + collection=collection, + steam_item_id=str(i), + title=f"Puzzle {i}", + order_index=i, + points_factor=factor, + points_value=value, + ) + for i in (1, 2) + ) + self.alice = CustomUser.objects.create_user(username="alice", password="x") + self.bob = CustomUser.objects.create_user(username="bob", password="x") + self.admin = CustomUser.objects.create_user( + username="admin", password="x", is_staff=True + ) + + def respond(self, user, puzzle, score): + submission = Submission.objects.create(event=self.event, user=user) + PuzzleResponse.objects.create( + submission=submission, + puzzle=puzzle, + puzzle_name=puzzle.title, + validated_cost=score, + validated_cycles=0, + validated_area=0, + ) + + def test_requires_staff(self): + self.client.force_login(self.alice) + + response = self.client.get(URL) + + self.assertEqual(response.status_code, 401) + + def test_points_per_puzzle(self): + self.respond(self.alice, self.p1, 10) + self.respond(self.bob, self.p1, 20) + self.respond(self.bob, self.p2, 30) + self.client.force_login(self.admin) + + response = self.client.get(URL) + + self.assertEqual( + response.content.decode().splitlines(), + [ + "username;Puzzle 1;Puzzle 2", + "bob;5;10", + "alice;10;0", + ], + )