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.utils import timezone from django.shortcuts import get_object_or_404 from typing import List from opus_submitter.submissions.utils import verify_and_validate_ocr_date_for_submission 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(): # Check if any confidence score is below 50% to auto-request validation auto_request_validation = any( ( response_data.ocr_confidence_cost is not None and response_data.ocr_confidence_cost < 0.8 ) or ( response_data.ocr_confidence_cycles is not None and response_data.ocr_confidence_cycles < 0.8 ) or ( response_data.ocr_confidence_area is not None and response_data.ocr_confidence_area < 0.8 ) for response_data in data.responses ) # Create the submission submission = Submission.objects.create( user=request.user if request.user.is_authenticated else None, notes=data.notes, manual_validation_requested=data.manual_validation_requested or auto_request_validation, ) file_index = 0 for response_data in data.responses: # Get the puzzle puzzle = get_object_or_404( SteamCollectionItem, id=response_data.puzzle_id ) # 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=data.manual_validation_requested, ocr_confidence_cost=response_data.ocr_confidence_cost, ocr_confidence_cycles=response_data.ocr_confidence_cycles, ocr_confidence_area=response_data.ocr_confidence_area, ) # 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 print("FI", file_index, files) 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"} response = get_object_or_404(PuzzleResponse, id=response_id) if data.puzzle is not None: puzzle = get_object_or_404(SteamCollectionItem, id=data.puzzle) response.puzzle = puzzle # 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 @router.put("/responses/{response_id}/validate/auto", response=PuzzleResponseOut) def validate_response(request, response_id: int): """Try to auto validate a puzzle response""" if not request.user.is_authenticated or not request.user.is_staff: return 403, {"detail": "Admin access required"} response = get_object_or_404(PuzzleResponse, id=response_id) for file in response.files.all(): verify_and_validate_ocr_date_for_submission(file) return response @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) .filter(puzzle__collection__is_active=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"} submission = get_object_or_404(Submission, 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 @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"} submission = get_object_or_404(Submission, id=submission_id) submission.delete() return {"detail": "Submission deleted successfully"} @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": (total_responses - needs_validation) / total_responses if total_responses else 0, }