tournament outputs

This commit is contained in:
2026-05-22 06:18:11 +02:00
parent bf21a5eae6
commit 92dddca964
21 changed files with 925 additions and 69 deletions
+231 -2
View File
@@ -1,11 +1,25 @@
from collections import defaultdict
from django.http import HttpRequest
from ninja import Router
from ninja.errors import HttpError
from django.core.cache import cache
from django.shortcuts import get_object_or_404
from accounts.models import CustomUser
from animations.schemas import RankingSchema
from submissions.models import PuzzleResponse, SteamCollectionItem
from animations.schemas import (
RankingSchema,
TournamentWinnersOut,
TournamentSubmissionsOut,
TournamentPuzzleResultsOut,
PuzzleWinnerOut,
PuzzleSubmissionsOut,
PuzzleResultsOut,
PuzzleSubmissionWithRankOut,
PuzzlePointsFactorOut,
WinnerResponseOut,
WinnerFileOut,
)
from submissions.models import PuzzleResponse, SteamCollectionItem, SteamCollection
router = Router()
@@ -46,3 +60,218 @@ def results(request: HttpRequest) -> dict:
cache.set("api:results:results", data, 300)
return data
@router.get("winners", response=TournamentWinnersOut)
def winners(request: HttpRequest) -> TournamentWinnersOut:
"""Get tournament winners with submission file URLs. Only available when tournament is closed."""
collection = get_object_or_404(SteamCollection, is_active=True)
# Only allow access when tournament is closed
if collection.accepting_submissions:
raise HttpError(403, "Tournament is still accepting submissions")
# Get all puzzles
puzzles = SteamCollectionItem.objects.filter(collection=collection).order_by("order_index")
# Get best response for each puzzle
winners_by_puzzle = {}
for puzzle in puzzles:
# Get the best response for this puzzle (ranked by points)
best_response = (
PuzzleResponse.objects
.filter(puzzle=puzzle, needs_manual_validation=False)
.filter_user_best_response()
.annotate_rank_points()
.order_by("rank_points")
.first()
)
if best_response:
winners_by_puzzle[puzzle.id] = best_response
# Build response
winners_list = []
for puzzle in puzzles:
best_response = winners_by_puzzle.get(puzzle.id)
winner_data = None
if best_response:
# Get submission files
files = best_response.files.all()
winner_files = [
WinnerFileOut(
file_url=file.file_url or "",
original_filename=file.original_filename
)
for file in files
]
winner_data = WinnerResponseOut(
user_id=best_response.submission.user.id if best_response.submission.user else 0,
username=best_response.submission.user.username if best_response.submission.user else "Anonymous",
final_cost=best_response.final_cost,
final_cycles=best_response.final_cycles,
final_area=best_response.final_area,
rank_points=best_response.rank_points,
files=winner_files,
)
winners_list.append(
PuzzleWinnerOut(
puzzle_id=puzzle.id,
puzzle_title=puzzle.title,
winner=winner_data,
)
)
return TournamentWinnersOut(winners=winners_list)
@router.get("top-submissions", response=TournamentSubmissionsOut)
def top_submissions(request: HttpRequest, limit: int = 5) -> TournamentSubmissionsOut:
"""Get tournament top submissions for each puzzle. Only available when tournament is closed."""
collection = get_object_or_404(SteamCollection, is_active=True)
# Only allow access when tournament is closed
if collection.accepting_submissions:
raise HttpError(403, "Tournament is still accepting submissions")
# Get all puzzles
puzzles = SteamCollectionItem.objects.filter(collection=collection).order_by("order_index")
# Build response
submissions_list = []
for puzzle in puzzles:
# Get the top N responses for this puzzle (ranked by points)
top_responses = (
PuzzleResponse.objects
.filter(puzzle=puzzle, needs_manual_validation=False)
.filter_user_best_response()
.annotate_rank_points()
.order_by("rank_points")[:limit]
)
# Build submission list for this puzzle
puzzle_submissions = []
for response in top_responses:
# Get submission files
files = response.files.all()
response_files = [
WinnerFileOut(
file_url=file.file_url or "",
original_filename=file.original_filename
)
for file in files
]
# Calculate total coefficient
total_coef = None
if puzzle.points_factor and response.final_cost is not None and response.final_cycles is not None and response.final_area is not None:
total_coef = (
puzzle.points_factor.cost * response.final_cost +
puzzle.points_factor.cycles * response.final_cycles +
puzzle.points_factor.area * response.final_area
)
submission_data = WinnerResponseOut(
user_id=response.submission.user.id if response.submission.user else 0,
username=response.submission.user.username if response.submission.user else "Anonymous",
final_cost=response.final_cost,
final_cycles=response.final_cycles,
final_area=response.final_area,
rank_points=response.rank_points,
total_coef=total_coef,
files=response_files,
)
puzzle_submissions.append(submission_data)
submissions_list.append(
PuzzleSubmissionsOut(
puzzle_id=puzzle.id,
puzzle_title=puzzle.title,
submissions=puzzle_submissions,
)
)
return TournamentSubmissionsOut(submissions=submissions_list)
@router.get("puzzle-results", response=TournamentPuzzleResultsOut)
def puzzle_results(request: HttpRequest, limit: int = 5) -> TournamentPuzzleResultsOut:
"""Get tournament results organized by puzzle with coefficients. Only available when tournament is closed."""
collection = get_object_or_404(SteamCollection, is_active=True)
# Only allow access when tournament is closed
if collection.accepting_submissions:
raise HttpError(403, "Tournament is still accepting submissions")
# Get all puzzles
puzzles = SteamCollectionItem.objects.filter(collection=collection).order_by("order_index")
# Build response
results_list = []
for puzzle in puzzles:
# Get the top N responses for this puzzle (ranked by points)
top_responses = (
PuzzleResponse.objects
.filter(puzzle=puzzle, needs_manual_validation=False)
.filter_user_best_response()
.annotate_rank_points()
.order_by("rank_points")[:limit]
)
# Build submission list for this puzzle with rank
puzzle_submissions = []
for rank, response in enumerate(top_responses, 1):
# Get submission files
files = response.files.all()
response_files = [
WinnerFileOut(
file_url=file.file_url or "",
original_filename=file.original_filename
)
for file in files
]
# Calculate total coefficient
total_coef = None
if puzzle.points_factor and response.final_cost is not None and response.final_cycles is not None and response.final_area is not None:
total_coef = (
puzzle.points_factor.cost * response.final_cost +
puzzle.points_factor.cycles * response.final_cycles +
puzzle.points_factor.area * response.final_area
)
submission_data = PuzzleSubmissionWithRankOut(
rank=rank,
user_id=response.submission.user.id if response.submission.user else 0,
username=response.submission.user.username if response.submission.user else "Anonymous",
final_cost=response.final_cost,
final_cycles=response.final_cycles,
final_area=response.final_area,
rank_points=response.rank_points,
total_coef=total_coef,
files=response_files,
)
puzzle_submissions.append(submission_data)
# Get points factor if available
points_factor = None
if puzzle.points_factor:
points_factor = PuzzlePointsFactorOut(
cost=puzzle.points_factor.cost,
cycles=puzzle.points_factor.cycles,
area=puzzle.points_factor.area,
)
results_list.append(
PuzzleResultsOut(
puzzle_id=puzzle.id,
puzzle_title=puzzle.title,
points_factor=points_factor,
submissions=puzzle_submissions,
)
)
return TournamentPuzzleResultsOut(results=results_list)
+86
View File
@@ -1,4 +1,5 @@
from ninja import ModelSchema, Schema
from typing import List, Optional
from submissions.models import PuzzleResponse
from submissions.schemas import SteamCollectionItemOut
@@ -41,3 +42,88 @@ class RankingSchema(Schema):
puzzles: list[SteamCollectionItemOut]
responses_by_userid: dict[int, list[PuzzleResponseRankingOut]]
ranking_by_puzzle: dict[int, list[PuzzleResponseRankingOut]]
class WinnerFileOut(Schema):
"""Schema for winner submission file"""
file_url: str
original_filename: str
class WinnerResponseOut(Schema):
"""Schema for winner response with files"""
user_id: int
username: str
final_cost: Optional[int]
final_cycles: Optional[int]
final_area: Optional[int]
rank_points: Optional[int]
total_coef: Optional[int]
files: List[WinnerFileOut]
class PuzzleWinnerOut(Schema):
"""Schema for puzzle with winner info"""
puzzle_id: int
puzzle_title: str
winner: Optional[WinnerResponseOut]
class PuzzleSubmissionsOut(Schema):
"""Schema for puzzle with all top submissions"""
puzzle_id: int
puzzle_title: str
submissions: List[WinnerResponseOut]
class TournamentWinnersOut(Schema):
"""Schema for tournament winners results"""
winners: List[PuzzleWinnerOut]
class TournamentSubmissionsOut(Schema):
"""Schema for tournament top submissions results"""
submissions: List[PuzzleSubmissionsOut]
class PuzzlePointsFactorOut(Schema):
"""Schema for puzzle points factor"""
cost: int
cycles: int
area: int
class PuzzleSubmissionWithRankOut(Schema):
"""Schema for puzzle submission with rank"""
rank: int
user_id: int
username: str
final_cost: Optional[int]
final_cycles: Optional[int]
final_area: Optional[int]
rank_points: Optional[int]
total_coef: Optional[int]
files: List[WinnerFileOut]
class PuzzleResultsOut(Schema):
"""Schema for puzzle-specific results with coefficients"""
puzzle_id: int
puzzle_title: str
points_factor: Optional[PuzzlePointsFactorOut]
submissions: List[PuzzleSubmissionWithRankOut]
class TournamentPuzzleResultsOut(Schema):
"""Schema for tournament puzzle-specific results"""
results: List[PuzzleResultsOut]