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
+37 -22
View File
@@ -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",
),
),
]
+11
View File
@@ -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(
+7 -2
View File
@@ -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,
},
)