feat: add events app + "multi-tenants" aka multi-events
This commit is contained in:
@@ -12,17 +12,23 @@ from .tasks import process_submission_file, process_crop
|
||||
|
||||
router = Router()
|
||||
|
||||
PREFIX = "/events/{event_path}/submissions"
|
||||
|
||||
@router.post("/submission/create", response=SubmissionOut)
|
||||
|
||||
@router.post(f"{PREFIX}/submission/create", response=SubmissionOut)
|
||||
def create_submission(
|
||||
request: HttpRequest,
|
||||
event_path: str,
|
||||
game_path: str,
|
||||
files: List[UploadedFile] = File(...),
|
||||
notes: Optional[str] = Form(None),
|
||||
):
|
||||
game = get_object_or_404(Game, path=game_path, enabled=True)
|
||||
game = get_object_or_404(
|
||||
Game.objects.enabled_for_event(request.event), path=game_path
|
||||
)
|
||||
|
||||
submission = Submission.objects.create(
|
||||
event=request.event,
|
||||
game=game,
|
||||
user=request.user if request.user.is_authenticated else None,
|
||||
notes=notes or None,
|
||||
@@ -39,32 +45,32 @@ def create_submission(
|
||||
return submission
|
||||
|
||||
|
||||
@router.get("/submission/by/user", response=list[SubmissionOut])
|
||||
def list_user_submissions(request: HttpRequest, game_path: str):
|
||||
@router.get(f"{PREFIX}/submission/by/user", response=list[SubmissionOut])
|
||||
def list_user_submissions(request: HttpRequest, event_path: str, game_path: str):
|
||||
game = get_object_or_404(Game, path=game_path)
|
||||
|
||||
if not request.user.is_authenticated:
|
||||
return 403, {"detail": "Authentication required"}
|
||||
|
||||
submissions = Submission.objects.filter(game=game, user=request.user).order_by(
|
||||
submissions = Submission.for_event.filter(game=game, user=request.user).order_by(
|
||||
"-created_at"
|
||||
)
|
||||
return submissions
|
||||
|
||||
|
||||
@router.get("/submission/by/game", response=list[SubmissionOut])
|
||||
def list_submissions_by_game(request: HttpRequest, game_path: str):
|
||||
@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.objects.filter(game=game)
|
||||
return Submission.for_event.filter(game=game)
|
||||
|
||||
|
||||
@router.get("/submission/{submission_id}", response=SubmissionOut)
|
||||
def get_submission(request: HttpRequest, submission_id: UUID):
|
||||
submission = get_object_or_404(Submission, id=submission_id)
|
||||
@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"}
|
||||
@@ -72,16 +78,19 @@ def get_submission(request: HttpRequest, submission_id: UUID):
|
||||
return submission
|
||||
|
||||
|
||||
@router.post("/crops/s/{submission_file_id}", response=ImageCropOut)
|
||||
@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_file = get_object_or_404(
|
||||
SubmissionFile, id=submission_file_id, submission__event=request.event
|
||||
)
|
||||
|
||||
crop = ImageCrop.objects.create(
|
||||
submission_file=submission_file,
|
||||
@@ -96,23 +105,27 @@ def create_image_crop(
|
||||
return crop
|
||||
|
||||
|
||||
@router.delete("/crops/{crop_id}", response=dict)
|
||||
def delete_image_crop(request: HttpRequest, crop_id: UUID):
|
||||
@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)
|
||||
crop = get_object_or_404(
|
||||
ImageCrop, id=crop_id, submission_file__submission__event=request.event
|
||||
)
|
||||
crop.delete()
|
||||
|
||||
return {"status": "deleted"}
|
||||
|
||||
|
||||
@router.post("/files/{file_id}/process-ocr", response=dict)
|
||||
def process_file_ocr(request: HttpRequest, file_id: UUID):
|
||||
@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_file = get_object_or_404(
|
||||
SubmissionFile, id=file_id, submission__event=request.event
|
||||
)
|
||||
submission_file.ocr_status = OCRStatus.TO_PROCESS
|
||||
submission_file.save()
|
||||
|
||||
@@ -122,12 +135,14 @@ def process_file_ocr(request: HttpRequest, file_id: UUID):
|
||||
return {"status": "queued"}
|
||||
|
||||
|
||||
@router.post("/crops/{crop_id}/process-ocr", response=dict)
|
||||
def process_crop_ocr(request: HttpRequest, crop_id: UUID):
|
||||
@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)
|
||||
crop = get_object_or_404(
|
||||
ImageCrop, id=crop_id, submission_file__submission__event=request.event
|
||||
)
|
||||
crop.ocr_status = OCRStatus.TO_PROCESS
|
||||
crop.save()
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
# Generated by Django 6.0.5 on 2026-09-15 21:08
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("events", "0002_seed_polylan_43"),
|
||||
("submissions", "0002_submission_game_alter_submission_user"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="submission",
|
||||
name="event",
|
||||
field=models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="submissions",
|
||||
to="events.event",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,25 @@
|
||||
from django.db import migrations
|
||||
|
||||
EVENT_PATH = "polylan-43"
|
||||
|
||||
|
||||
def backfill_event(apps, schema_editor):
|
||||
Event = apps.get_model("events", "Event")
|
||||
Submission = apps.get_model("submissions", "Submission")
|
||||
event = Event.objects.get(path=EVENT_PATH)
|
||||
Submission.objects.filter(event__isnull=True).update(event=event)
|
||||
|
||||
|
||||
def noop(apps, schema_editor):
|
||||
pass
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("submissions", "0003_submission_event"),
|
||||
("events", "0002_seed_polylan_43"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(backfill_event, noop),
|
||||
]
|
||||
@@ -0,0 +1,20 @@
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("submissions", "0004_backfill_submission_event"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="submission",
|
||||
name="event",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="submissions",
|
||||
to="events.event",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -4,6 +4,8 @@ from django.utils.translation import gettext_lazy as _
|
||||
import uuid
|
||||
|
||||
from accounts.models import CustomUser
|
||||
from events.models import Event
|
||||
from events.scoping import EventScopedManager
|
||||
from games.models import Game
|
||||
|
||||
|
||||
@@ -36,6 +38,12 @@ class BaseModel(models.Model):
|
||||
|
||||
|
||||
class Submission(BaseModel):
|
||||
event = models.ForeignKey(
|
||||
Event,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="submissions",
|
||||
)
|
||||
|
||||
game = models.ForeignKey(
|
||||
Game,
|
||||
on_delete=models.CASCADE,
|
||||
@@ -60,6 +68,9 @@ class Submission(BaseModel):
|
||||
help_text="Optional notes about the submission",
|
||||
)
|
||||
|
||||
objects = models.Manager()
|
||||
for_event = EventScopedManager()
|
||||
|
||||
|
||||
class SubmissionFile(BaseModel):
|
||||
submission = models.ForeignKey(
|
||||
|
||||
@@ -2,16 +2,21 @@ from django.http import HttpRequest
|
||||
from django.shortcuts import render, get_object_or_404
|
||||
from django.contrib.auth.decorators import login_required
|
||||
|
||||
from events.models import Event
|
||||
from games.models import Game
|
||||
|
||||
|
||||
@login_required
|
||||
def submission_home(request: HttpRequest, game: str):
|
||||
game_obj = get_object_or_404(Game, path=f"/submissions/{game}", enabled=True)
|
||||
def submission_home(request: HttpRequest, event_path: str, game: str):
|
||||
event = get_object_or_404(Event, path=event_path)
|
||||
game_obj = get_object_or_404(
|
||||
Game.objects.enabled_for_event(event), path=f"/submissions/{game}"
|
||||
)
|
||||
return render(
|
||||
request,
|
||||
"submissions.html",
|
||||
{
|
||||
"event": event,
|
||||
"game": game_obj,
|
||||
},
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user