tournament outputs
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user