from ninja import Router, File from ninja.files import UploadedFile from ninja.pagination import paginate from django.db import transaction from django.core.files.base import ContentFile from django.http import Http404 from django.utils import timezone from django.shortcuts import get_object_or_404 from typing import List from .models import Submission, PuzzleResponse, SubmissionFile, SteamCollectionItem from .schemas import ( SubmissionIn, SubmissionOut, PuzzleResponseOut, ValidationIn, SteamCollectionItemOut, ) router = Router() @router.get("/puzzles", response=List[SteamCollectionItemOut]) def list_puzzles(request): """Get list of available puzzles""" return SteamCollectionItem.objects.select_related("collection").filter( collection__is_active=True ) @router.get("/submissions", response=List[SubmissionOut]) @paginate def list_submissions(request): """Get paginated list of submissions""" return Submission.objects.prefetch_related( "responses__files", "responses__puzzle" ).filter(user=request.user) @router.get("/submissions/{submission_id}", response=SubmissionOut) def get_submission(request, submission_id: str): """Get detailed submission by ID""" return get_object_or_404( Submission.objects.prefetch_related( "responses__files", "responses__puzzle" ).filter(user=request.user), id=submission_id, ) @router.post("/submissions", response=SubmissionOut) def create_submission( request, data: SubmissionIn, files: List[UploadedFile] = File(...) ): """Create a new submission with multiple puzzle responses""" # Validate that we have files if not files: return 400, {"detail": "At least one file is required"} # Group files by puzzle (based on filename or order) # For now, we'll assume files are provided in the same order as responses if len(files) < len(data.responses): return 400, {"detail": "Not enough files for all responses"} print(data, files) try: with transaction.atomic(): # Create the submission submission = Submission.objects.create( user=request.user if request.user.is_authenticated else None, notes=data.notes, ) file_index = 0 for response_data in data.responses: # Get the puzzle try: puzzle = SteamCollectionItem.objects.get(id=response_data.puzzle_id) except SteamCollectionItem.DoesNotExist: return 400, { "detail": f"Puzzle with id {response_data.puzzle_id} not found" } # Create the puzzle response response = PuzzleResponse.objects.create( submission=submission, puzzle=puzzle, puzzle_name=response_data.puzzle_name, cost=response_data.cost, cycles=response_data.cycles, area=response_data.area, needs_manual_validation=response_data.needs_manual_validation, ocr_confidence_score=response_data.ocr_confidence_score, ) # Process files for this response # For simplicity, we'll take one file per response # In a real implementation, you'd need better file-to-response mapping if file_index < len(files): uploaded_file = files[file_index] # Validate file type if not uploaded_file.content_type.startswith(("image/", "video/")): return 400, { "detail": f"Invalid file type: {uploaded_file.content_type}" } # Validate file size (256MB limit) if uploaded_file.size > 256 * 1024 * 1024: return 400, {"detail": "File too large (max 256MB)"} # Create submission file submission_file = SubmissionFile.objects.create( response=response, original_filename=uploaded_file.name, file_size=uploaded_file.size, content_type=uploaded_file.content_type, ) # Save the file submission_file.file.save( uploaded_file.name, ContentFile(uploaded_file.read()), save=True ) file_index += 1 # Check if OCR validation is needed if not all( [response_data.cost, response_data.cycles, response_data.area] ): response.mark_for_validation("Incomplete OCR data") # Reload with relations for response submission = Submission.objects.prefetch_related( "responses__files", "responses__puzzle" ).get(id=submission.id) return submission except Exception as e: print(e) return 500, {"detail": f"Error creating submission: {str(e)}"} @router.put("/responses/{response_id}/validate", response=PuzzleResponseOut) def validate_response(request, response_id: int, data: ValidationIn): """Manually validate a puzzle response""" if not request.user.is_authenticated or not request.user.is_staff: return 403, {"detail": "Admin access required"} try: response = PuzzleResponse.objects.select_related("puzzle").get(id=response_id) # Update validated values if data.validated_cost is not None: response.validated_cost = data.validated_cost if data.validated_cycles is not None: response.validated_cycles = data.validated_cycles if data.validated_area is not None: response.validated_area = data.validated_area # Mark as no longer needing validation if we have all values if all([response.final_cost, response.final_cycles, response.final_area]): response.needs_manual_validation = False response.save() return response except PuzzleResponse.DoesNotExist: raise Http404("Response not found") @router.get("/responses/needs-validation", response=List[PuzzleResponseOut]) def list_responses_needing_validation(request): """Get all responses that need manual validation""" if not request.user.is_authenticated or not request.user.is_staff: return 403, {"detail": "Admin access required"} return ( PuzzleResponse.objects.filter(needs_manual_validation=True) .select_related("puzzle", "submission") .prefetch_related("files") ) @router.post("/submissions/{submission_id}/validate", response=SubmissionOut) def validate_submission(request, submission_id: str): """Mark entire submission as validated""" if not request.user.is_authenticated or not request.user.is_staff: return 403, {"detail": "Admin access required"} try: submission = Submission.objects.get(id=submission_id) submission.is_validated = True submission.validated_by = request.user submission.validated_at = timezone.now() submission.save() # Also mark all responses as not needing validation submission.responses.update(needs_manual_validation=False) # Reload with relations submission = Submission.objects.prefetch_related( "responses__files", "responses__puzzle" ).get(id=submission.id) return submission except Submission.DoesNotExist: raise Http404("Submission not found") @router.delete("/submissions/{submission_id}") def delete_submission(request, submission_id: str): """Delete a submission (admin only)""" if not request.user.is_authenticated or not request.user.is_staff: return 403, {"detail": "Admin access required"} try: submission = Submission.objects.get(id=submission_id) submission.delete() return {"detail": "Submission deleted successfully"} except Submission.DoesNotExist: raise Http404("Submission not found") @router.get("/stats") def get_stats(request): """Get submission statistics""" total_submissions = Submission.objects.count() total_responses = PuzzleResponse.objects.count() needs_validation = PuzzleResponse.objects.filter( needs_manual_validation=True ).count() validated_submissions = Submission.objects.filter(is_validated=True).count() return { "total_submissions": total_submissions, "total_responses": total_responses, "needs_validation": needs_validation, "validated_submissions": validated_submissions, "validation_rate": validated_submissions / total_submissions if total_submissions > 0 else 0, }