277 lines
9.8 KiB
Python
277 lines
9.8 KiB
Python
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():
|
|
# 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.5
|
|
)
|
|
or (
|
|
response_data.ocr_confidence_cycles is not None
|
|
and response_data.ocr_confidence_cycles < 0.5
|
|
)
|
|
or (
|
|
response_data.ocr_confidence_area is not None
|
|
and response_data.ocr_confidence_area < 0.5
|
|
)
|
|
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
|
|
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=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
|
|
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,
|
|
}
|