Files
polylan-submitter/polylan_submitter/submissions/api.py
T
2026-09-17 00:56:17 +02:00

210 lines
6.5 KiB
Python

from typing import List, Optional
from uuid import UUID
from django.http import HttpRequest
from ninja import Router, File, Form
from ninja.files import UploadedFile
from django.shortcuts import get_object_or_404
from games.models import Game
from .models import Submission, SubmissionFile, ImageCrop, OCRStatus, Scope
from .schemas import SubmissionOut, ImageCropIn, ImageCropOut, ScopeOut
from .tasks import process_submission_file, process_crop
router = Router()
PREFIX = "/events/{event_path}/submissions"
@router.get(f"{PREFIX}/scopes/mine", response={200: list[ScopeOut], 403: dict})
def list_my_scopes(request: HttpRequest, event_path: str, game_path: str):
if not request.user.is_authenticated:
return 403, {"detail": "Authentication required"}
game = get_object_or_404(
Game.objects.enabled_for_event(request.event), path=game_path
)
return Scope.objects.filter(
event_game__event=request.event,
event_game__game=game,
assigned_users=request.user,
)
@router.get(f"{PREFIX}/{{game_path}}/available-scopes", response={200: list[ScopeOut], 403: dict})
def list_available_scopes(request: HttpRequest, event_path: str, game_path: str):
"""Scopes the current user may submit to for this game - superusers see
every active scope, everyone else only the ones assigned to them. Used
by the submissions-select and submissions-detail pages, both to list
scopes and (for the detail page) to confirm the requested scope_id is
one the current user is actually allowed to see."""
if not request.user.is_authenticated:
return 403, {"detail": "Authentication required"}
game = get_object_or_404(
Game.objects.enabled_for_event(request.event), path=game_path
)
scopes = (
Scope.objects.filter(
event_game__event=request.event,
event_game__game=game,
is_active=True,
)
if request.user.is_superuser
else Scope.objects.filter(
event_game__event=request.event,
event_game__game=game,
assigned_users=request.user,
is_active=True,
)
)
return list(scopes.order_by("name"))
@router.post(f"{PREFIX}/submission/create", response={200: SubmissionOut, 403: dict})
def create_submission(
request: HttpRequest,
event_path: str,
scope_id: UUID,
files: List[UploadedFile] = File(...),
notes: Optional[str] = Form(None),
):
if not request.user.is_authenticated:
return 403, {"detail": "Authentication required"}
scope = get_object_or_404(
Scope, id=scope_id, event_game__event=request.event, is_active=True
)
if (
not request.user.is_staff
and not scope.assigned_users.filter(pk=request.user.pk).exists()
):
return 403, {"detail": "You are not assigned to this scope"}
submission = Submission.objects.create(
event=request.event,
scope=scope,
user=request.user,
notes=notes or None,
)
for file in files:
SubmissionFile.objects.create(
submission=submission,
file=file,
original_filename=file.name,
content_type=file.content_type or "application/octet-stream",
)
return submission
@router.get(
f"{PREFIX}/submission/by/user", response={200: list[SubmissionOut], 403: dict}
)
def list_user_submissions(request: HttpRequest, event_path: str, scope_id: UUID):
if not request.user.is_authenticated:
return 403, {"detail": "Authentication required"}
submissions = Submission.for_event.filter(
scope_id=scope_id, user=request.user
).order_by("-created_at")
return submissions
@router.get(f"{PREFIX}/submission/by/game", response=list[SubmissionOut])
def list_submissions_by_game(request: HttpRequest, event_path: str, game_path: str):
game = get_object_or_404(Game, path=game_path)
if not request.user.is_staff:
return 403, {"detail": "Admin access required"}
return Submission.for_event.filter(scope__event_game__game=game)
@router.get(f"{PREFIX}/submission/{{submission_id}}", response=SubmissionOut)
def get_submission(request: HttpRequest, event_path: str, submission_id: UUID):
submission = get_object_or_404(Submission.for_event, id=submission_id)
if submission.user != request.user and not request.user.is_staff:
return 403, {"detail": "Not authorized"}
return submission
@router.post(f"{PREFIX}/crops/s/{{submission_file_id}}", response=ImageCropOut)
def create_image_crop(
request: HttpRequest,
event_path: str,
submission_file_id: UUID,
data: ImageCropIn,
):
if not request.user.is_staff:
return 403, {"detail": "Admin access required"}
submission_file = get_object_or_404(
SubmissionFile, id=submission_file_id, submission__event=request.event
)
crop = ImageCrop.objects.create(
submission_file=submission_file,
x1=data.x1,
y1=data.y1,
x2=data.x2,
y2=data.y2,
label=data.label or "",
ocr_status=OCRStatus.TO_PROCESS,
)
return crop
@router.delete(f"{PREFIX}/crops/{{crop_id}}", response=dict)
def delete_image_crop(request: HttpRequest, event_path: str, crop_id: UUID):
if not request.user.is_staff:
return 403, {"detail": "Admin access required"}
crop = get_object_or_404(
ImageCrop, id=crop_id, submission_file__submission__event=request.event
)
crop.delete()
return {"status": "deleted"}
@router.post(f"{PREFIX}/files/{{file_id}}/process-ocr", response=dict)
def process_file_ocr(request: HttpRequest, event_path: str, file_id: UUID):
if not request.user.is_staff:
return 403, {"detail": "Admin access required"}
submission_file = get_object_or_404(
SubmissionFile, id=file_id, submission__event=request.event
)
submission_file.ocr_status = OCRStatus.TO_PROCESS
submission_file.save()
# Enqueue the OCR processing task
process_submission_file.enqueue(str(file_id))
return {"status": "queued"}
@router.post(f"{PREFIX}/crops/{{crop_id}}/process-ocr", response=dict)
def process_crop_ocr(request: HttpRequest, event_path: str, crop_id: UUID):
if not request.user.is_staff:
return 403, {"detail": "Admin access required"}
crop = get_object_or_404(
ImageCrop, id=crop_id, submission_file__submission__event=request.event
)
crop.ocr_status = OCRStatus.TO_PROCESS
crop.save()
# Enqueue the OCR processing task
process_crop.enqueue(str(crop_id))
return {"status": "queued"}