Files

311 lines
9.4 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 EventGame, Game
from .models import Submission, SubmissionFile, ImageCrop, OCRStatus, Scope
from .schemas import (
SubmissionOut,
ImageCropIn,
ImageCropOut,
ScopeOut,
SyncScopesIn,
SyncScopesOut,
SyncScopesStatusOut,
SyncScopesStopOut,
)
from .tasks import (
get_scope_sync_state,
process_submission_file,
process_crop,
schedule_scope_sync,
stop_scope_sync,
)
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"))
def _syncable_event_game(request: HttpRequest, game_path: str) -> EventGame:
return get_object_or_404(
EventGame,
event=request.event,
game__path=game_path,
game__custom_path=False,
enabled=True,
)
@router.get(
f"{PREFIX}/{{game_path}}/sync-scopes",
response={200: SyncScopesStatusOut, 403: dict},
)
def sync_scopes_status(request: HttpRequest, event_path: str, game_path: str):
"""State of the AAndT scope sync loop of a (non custom path) game,
superusers only."""
if not request.user.is_superuser:
return 403, {"detail": "Superuser required"}
event_game = _syncable_event_game(request, game_path)
scopes = Scope.objects.filter(event_game=event_game)
return {
**vars(get_scope_sync_state(event_game.id)),
"has_token": bool(event_game.apibot_token),
"scope_count": scopes.count(),
"active_scope_count": scopes.filter(is_active=True).count(),
}
@router.post(
f"{PREFIX}/{{game_path}}/sync-scopes",
response={200: SyncScopesOut, 400: dict, 403: dict},
)
def sync_scopes(
request: HttpRequest, event_path: str, game_path: str, payload: SyncScopesIn
):
"""Start the AAndT scope sync loop for a (non custom path) game, superusers
only. Stores `apibot_token` first when given; without one and none stored
yet, answers 400 with `token_required` so the frontend asks for it."""
if not request.user.is_superuser:
return 403, {"detail": "Superuser required"}
event_game = _syncable_event_game(request, game_path)
token = (payload.apibot_token or "").strip()
if token:
event_game.apibot_token = token
event_game.save(update_fields=["apibot_token"])
if not event_game.apibot_token:
return 400, {
"detail": "No AAndT apibot token configured",
"token_required": True,
}
return {"started": schedule_scope_sync(event_game.id)}
@router.delete(
f"{PREFIX}/{{game_path}}/sync-scopes",
response={200: SyncScopesStopOut, 403: dict},
)
def stop_sync_scopes(request: HttpRequest, event_path: str, game_path: str):
"""Stop the AAndT scope sync loop of a (non custom path) game, superusers
only. Cancels its scheduled job, scopes are left as they are."""
if not request.user.is_superuser:
return 403, {"detail": "Superuser required"}
event_game = _syncable_event_game(request, game_path)
return {"stopped": stop_scope_sync(event_game.id)}
@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)
# Admins see every player's submissions for the scope
if not request.user.is_staff:
submissions = submissions.filter(user=request.user)
return submissions.select_related("user").order_by("-created_at")
@router.get(
f"{PREFIX}/submission/by/game", response={200: list[SubmissionOut], 403: dict}
)
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={200: SubmissionOut, 403: dict}
)
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={200: ImageCropOut, 403: dict}
)
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={200: dict, 403: 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={200: dict, 403: 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={200: dict, 403: 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"}