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) -> 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, 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, ) ) 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)