feat: add events app + "multi-tenants" aka multi-events

This commit is contained in:
2026-09-16 00:03:26 +02:00
parent b5c2cdc037
commit 8b9c254fd2
62 changed files with 1343 additions and 362 deletions
+64 -46
View File
@@ -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,