242 lines
8.3 KiB
Python
242 lines
8.3 KiB
Python
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,
|
|
TournamentSubmissionsOut,
|
|
TournamentPuzzleResultsOut,
|
|
PuzzleSubmissionsOut,
|
|
PuzzleResultsOut,
|
|
PuzzleSubmissionWithRankOut,
|
|
PuzzlePointsFactorOut,
|
|
WinnerResponseOut,
|
|
WinnerFileOut,
|
|
)
|
|
from submissions.models import PuzzleResponse, SteamCollectionItem, SteamCollection
|
|
|
|
|
|
router = Router()
|
|
|
|
|
|
@router.get("results", response=RankingSchema)
|
|
def results(request: HttpRequest) -> dict:
|
|
cache_key = "api:results:results"
|
|
cached_data = cache.get(cache_key)
|
|
|
|
if cached_data is not None:
|
|
return cached_data
|
|
|
|
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 is None, x.rank_points or 0)
|
|
)
|
|
|
|
data = {
|
|
"users": list(CustomUser.objects.filter(pk__in=responses_by_userid.keys())),
|
|
"puzzles": list(SteamCollectionItem.objects.all()),
|
|
"responses_by_userid": dict(responses_by_userid),
|
|
"ranking_by_puzzle": ranking,
|
|
}
|
|
|
|
cache.set("api:results:results", data, 300)
|
|
return data
|
|
|
|
|
|
@router.get("top-submissions", response=TournamentSubmissionsOut)
|
|
def top_submissions(request: HttpRequest, limit: int = 5) -> dict:
|
|
"""Get tournament top submissions for each puzzle. Only available when tournament is closed."""
|
|
cache_key = f"api:results:top_submissions:{limit}"
|
|
cached_data = cache.get(cache_key)
|
|
|
|
if cached_data is not None:
|
|
return cached_data
|
|
|
|
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, highest first)
|
|
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,
|
|
)
|
|
)
|
|
|
|
data = {"submissions": submissions_list}
|
|
cache.set(f"api:results:top_submissions:{limit}", data, 300)
|
|
return data
|
|
|
|
|
|
@router.get("puzzle-results", response=TournamentPuzzleResultsOut)
|
|
def puzzle_results(request: HttpRequest, limit: int = 5) -> dict:
|
|
"""Get tournament results organized by puzzle with coefficients. Only available when tournament is closed."""
|
|
cache_key = f"api:results:puzzle_results:{limit}"
|
|
cached_data = cache.get(cache_key)
|
|
|
|
if cached_data is not None:
|
|
return cached_data
|
|
|
|
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,
|
|
)
|
|
)
|
|
|
|
data = {"results": results_list}
|
|
cache.set(f"api:results:puzzle_results:{limit}", data, 300)
|
|
return data
|