feat: add events app + "multi-tenants" aka multi-events
This commit is contained in:
@@ -32,50 +32,51 @@ from .schemas import (
|
||||
router = Router()
|
||||
OPUS_MAGNUM_APP_ID = 558990
|
||||
|
||||
PREFIX = "/events/{event_path}/opus-magnum"
|
||||
|
||||
@router.get("/puzzles", response=List[SteamCollectionItemOut])
|
||||
|
||||
@router.get(f"{PREFIX}/puzzles", response=List[SteamCollectionItemOut])
|
||||
@decorate_view(require_game_enabled(OPUS_MAGNUM_APP_ID))
|
||||
def list_puzzles(request):
|
||||
def list_puzzles(request, event_path: str):
|
||||
"""Get list of available puzzles"""
|
||||
return SteamCollectionItem.objects.select_related("collection").filter(
|
||||
collection__is_active=True
|
||||
collection__is_active=True, collection__event=request.event
|
||||
)
|
||||
|
||||
|
||||
@router.get("/collection", response=SteamCollectionOut)
|
||||
@router.get(f"{PREFIX}/collection", response=SteamCollectionOut)
|
||||
@decorate_view(require_game_enabled(OPUS_MAGNUM_APP_ID))
|
||||
def get_collection(request):
|
||||
def get_collection(request, event_path: str):
|
||||
"""Get the active collection details"""
|
||||
collection = get_object_or_404(SteamCollection, is_active=True)
|
||||
return collection
|
||||
return get_object_or_404(SteamCollection.for_event, is_active=True)
|
||||
|
||||
|
||||
@router.get("/submissions", response=List[SubmissionOut])
|
||||
@router.get(f"{PREFIX}/submissions", response=List[SubmissionOut])
|
||||
@decorate_view(require_game_enabled(OPUS_MAGNUM_APP_ID))
|
||||
@paginate
|
||||
def list_submissions(request):
|
||||
def list_submissions(request, event_path: str):
|
||||
"""Get paginated list of submissions"""
|
||||
return Submission.objects.prefetch_related(
|
||||
return Submission.for_event.prefetch_related(
|
||||
"responses__files", "responses__puzzle"
|
||||
).filter(user=request.user)
|
||||
|
||||
|
||||
@router.get("/submissions/{submission_id}", response=SubmissionOut)
|
||||
@router.get(f"{PREFIX}/submissions/{{submission_id}}", response=SubmissionOut)
|
||||
@decorate_view(require_game_enabled(OPUS_MAGNUM_APP_ID))
|
||||
def get_submission(request, submission_id: str):
|
||||
def get_submission(request, event_path: str, submission_id: str):
|
||||
"""Get detailed submission by ID"""
|
||||
return get_object_or_404(
|
||||
Submission.objects.prefetch_related(
|
||||
Submission.for_event.prefetch_related(
|
||||
"responses__files", "responses__puzzle"
|
||||
).filter(user=request.user),
|
||||
id=submission_id,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/submissions", response=SubmissionOut)
|
||||
@router.post(f"{PREFIX}/submissions", response=SubmissionOut)
|
||||
@decorate_view(require_game_enabled(OPUS_MAGNUM_APP_ID))
|
||||
def create_submission(
|
||||
request, data: SubmissionIn, files: List[UploadedFile] = File(...)
|
||||
request, event_path: str, data: SubmissionIn, files: List[UploadedFile] = File(...)
|
||||
):
|
||||
"""Create a new submission with multiple puzzle responses"""
|
||||
|
||||
@@ -92,7 +93,9 @@ def create_submission(
|
||||
if data.responses:
|
||||
for puzzle in data.responses:
|
||||
if not get_object_or_404(
|
||||
SteamCollectionItem, id=puzzle.puzzle_id
|
||||
SteamCollectionItem,
|
||||
id=puzzle.puzzle_id,
|
||||
collection__event=request.event,
|
||||
).collection.accepting_submissions:
|
||||
raise HttpError(
|
||||
403, "This tournament is no longer accepting submissions"
|
||||
@@ -119,6 +122,7 @@ def create_submission(
|
||||
|
||||
# Create the submission
|
||||
submission = Submission.objects.create(
|
||||
event=request.event,
|
||||
user=request.user if request.user.is_authenticated else None,
|
||||
notes=data.notes,
|
||||
manual_validation_requested=data.manual_validation_requested
|
||||
@@ -129,7 +133,9 @@ def create_submission(
|
||||
for response_data in data.responses:
|
||||
# Get the puzzle
|
||||
puzzle = get_object_or_404(
|
||||
SteamCollectionItem, id=response_data.puzzle_id
|
||||
SteamCollectionItem,
|
||||
id=response_data.puzzle_id,
|
||||
collection__event=request.event,
|
||||
)
|
||||
|
||||
# Create the puzzle response
|
||||
@@ -192,12 +198,12 @@ def create_submission(
|
||||
response.mark_for_validation("Incomplete OCR data")
|
||||
|
||||
# Reload with relations for response
|
||||
submission = Submission.objects.prefetch_related(
|
||||
submission = Submission.for_event.prefetch_related(
|
||||
"responses__files", "responses__puzzle"
|
||||
).get(id=submission.id)
|
||||
|
||||
# Invalidate results cache on successful submission
|
||||
cache.delete("api:results:results")
|
||||
cache.delete(f"api:{event_path}:results:results")
|
||||
|
||||
return submission
|
||||
|
||||
@@ -205,18 +211,22 @@ def create_submission(
|
||||
return 500, {"detail": f"Error creating submission: {str(e)}"}
|
||||
|
||||
|
||||
@router.put("/responses/{response_id}/validate", response=PuzzleResponseOut)
|
||||
@router.put(f"{PREFIX}/responses/{{response_id}}/validate", response=PuzzleResponseOut)
|
||||
@decorate_view(require_game_enabled(OPUS_MAGNUM_APP_ID))
|
||||
def validate_response(request, response_id: int, data: ValidationIn):
|
||||
def validate_response(request, event_path: str, 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)
|
||||
response = get_object_or_404(
|
||||
PuzzleResponse, id=response_id, submission__event=request.event
|
||||
)
|
||||
|
||||
if data.puzzle is not None:
|
||||
puzzle = get_object_or_404(SteamCollectionItem, id=data.puzzle)
|
||||
puzzle = get_object_or_404(
|
||||
SteamCollectionItem, id=data.puzzle, collection__event=request.event
|
||||
)
|
||||
response.puzzle = puzzle
|
||||
|
||||
# Update validated values
|
||||
@@ -236,20 +246,24 @@ def validate_response(request, response_id: int, data: ValidationIn):
|
||||
response.save()
|
||||
|
||||
# Invalidate results cache when a response is validated
|
||||
cache.delete("api:results:results")
|
||||
cache.delete(f"api:{event_path}:results:results")
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@router.put("/responses/{response_id}/validate/auto", response=PuzzleResponseOut)
|
||||
@router.put(
|
||||
f"{PREFIX}/responses/{{response_id}}/validate/auto", response=PuzzleResponseOut
|
||||
)
|
||||
@decorate_view(require_game_enabled(OPUS_MAGNUM_APP_ID))
|
||||
def validate_auto(request, response_id: int):
|
||||
def validate_auto(request, event_path: str, 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)
|
||||
response = get_object_or_404(
|
||||
PuzzleResponse, id=response_id, submission__event=request.event
|
||||
)
|
||||
|
||||
for file in response.files.all():
|
||||
verify_and_validate_ocr_date_for_submission(file)
|
||||
@@ -257,9 +271,9 @@ def validate_auto(request, response_id: int):
|
||||
return response
|
||||
|
||||
|
||||
@router.get("/responses/needs-validation", response=List[PuzzleResponseOut])
|
||||
@router.get(f"{PREFIX}/responses/needs-validation", response=List[PuzzleResponseOut])
|
||||
@decorate_view(require_game_enabled(OPUS_MAGNUM_APP_ID))
|
||||
def list_responses_needing_validation(request):
|
||||
def list_responses_needing_validation(request, event_path: str):
|
||||
"""Get all responses that need manual validation"""
|
||||
|
||||
if not request.user.is_authenticated or not request.user.is_staff:
|
||||
@@ -267,21 +281,23 @@ def list_responses_needing_validation(request):
|
||||
|
||||
return (
|
||||
PuzzleResponse.objects.filter(needs_manual_validation=True)
|
||||
.filter(puzzle__collection__is_active=True)
|
||||
.filter(
|
||||
puzzle__collection__is_active=True, puzzle__collection__event=request.event
|
||||
)
|
||||
.select_related("puzzle", "submission")
|
||||
.prefetch_related("files")
|
||||
)
|
||||
|
||||
|
||||
@router.post("/submissions/{submission_id}/validate", response=SubmissionOut)
|
||||
@router.post(f"{PREFIX}/submissions/{{submission_id}}/validate", response=SubmissionOut)
|
||||
@decorate_view(require_game_enabled(OPUS_MAGNUM_APP_ID))
|
||||
def validate_submission(request, submission_id: str):
|
||||
def validate_submission(request, event_path: str, 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 = get_object_or_404(Submission.for_event, id=submission_id)
|
||||
|
||||
submission.is_validated = True
|
||||
submission.validated_by = request.user
|
||||
@@ -292,44 +308,46 @@ def validate_submission(request, submission_id: str):
|
||||
submission.responses.update(needs_manual_validation=False)
|
||||
|
||||
# Reload with relations
|
||||
submission = Submission.objects.prefetch_related(
|
||||
submission = Submission.for_event.prefetch_related(
|
||||
"responses__files", "responses__puzzle"
|
||||
).get(id=submission.id)
|
||||
|
||||
# Invalidate results cache when submission is validated
|
||||
cache.delete("api:results:results")
|
||||
cache.delete(f"api:{event_path}:results:results")
|
||||
|
||||
return submission
|
||||
|
||||
|
||||
@router.delete("/submissions/{submission_id}")
|
||||
@router.delete(f"{PREFIX}/submissions/{{submission_id}}")
|
||||
@decorate_view(require_game_enabled(OPUS_MAGNUM_APP_ID))
|
||||
def delete_submission(request, submission_id: str):
|
||||
def delete_submission(request, event_path: str, 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 = get_object_or_404(Submission.for_event, id=submission_id)
|
||||
submission.delete()
|
||||
|
||||
# Invalidate results cache when submission is deleted
|
||||
cache.delete("api:results:results")
|
||||
cache.delete(f"api:{event_path}:results:results")
|
||||
|
||||
return {"detail": "Submission deleted successfully"}
|
||||
|
||||
|
||||
@router.get("/stats")
|
||||
@router.get(f"{PREFIX}/stats")
|
||||
@decorate_view(require_game_enabled(OPUS_MAGNUM_APP_ID))
|
||||
def get_stats(request):
|
||||
def get_stats(request, event_path: str):
|
||||
"""Get submission statistics"""
|
||||
|
||||
total_submissions = Submission.objects.count()
|
||||
total_responses = PuzzleResponse.objects.count()
|
||||
needs_validation = PuzzleResponse.objects.filter(
|
||||
needs_manual_validation=True
|
||||
total_submissions = Submission.for_event.count()
|
||||
total_responses = PuzzleResponse.objects.filter(
|
||||
submission__event=request.event
|
||||
).count()
|
||||
validated_submissions = Submission.objects.filter(is_validated=True).count()
|
||||
needs_validation = PuzzleResponse.objects.filter(
|
||||
needs_manual_validation=True, submission__event=request.event
|
||||
).count()
|
||||
validated_submissions = Submission.for_event.filter(is_validated=True).count()
|
||||
|
||||
return {
|
||||
"total_submissions": total_submissions,
|
||||
|
||||
Reference in New Issue
Block a user