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
+11
View File
@@ -0,0 +1,11 @@
from django.contrib import admin
from .models import Event
@admin.register(Event)
class EventAdmin(admin.ModelAdmin):
list_display = ["name", "path", "created_at"]
search_fields = ["name", "path"]
prepopulated_fields = {"path": ("name",)}
readonly_fields = ["created_at", "updated_at"]
+5
View File
@@ -0,0 +1,5 @@
from django.apps import AppConfig
class EventsConfig(AppConfig):
name = "events"
+27
View File
@@ -0,0 +1,27 @@
from django.shortcuts import get_object_or_404
from .models import Event
from .scoping import current_event
class EventContextMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
try:
return self.get_response(request)
finally:
# Not current_event.reset(token): under ASGI, process_view and this
# finally block can run in different contextvars.Context copies
# (sync_to_async(thread_sensitive=True) copies the context), and
# Token.reset() requires the exact Context it was created in.
# set(None) has no such constraint.
current_event.set(None)
def process_view(self, request, view_func, view_args, view_kwargs):
event_path = view_kwargs.get("event_path")
if event_path:
request.event = get_object_or_404(Event, path=event_path)
current_event.set(request.event)
return None
@@ -0,0 +1,34 @@
# Generated by Django 6.0.5 on 2026-09-15 21:08
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name="Event",
fields=[
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
("name", models.CharField(max_length=255)),
("path", models.SlugField(max_length=100, unique=True)),
],
options={
"abstract": False,
},
),
]
@@ -0,0 +1,24 @@
from django.db import migrations
EVENT_NAME = "PolyLAN 43"
EVENT_PATH = "polylan-43"
def seed_event(apps, schema_editor):
Event = apps.get_model("events", "Event")
Event.objects.get_or_create(path=EVENT_PATH, defaults={"name": EVENT_NAME})
def unseed_event(apps, schema_editor):
Event = apps.get_model("events", "Event")
Event.objects.filter(path=EVENT_PATH).delete()
class Migration(migrations.Migration):
dependencies = [
("events", "0001_initial"),
]
operations = [
migrations.RunPython(seed_event, unseed_event),
]
+22
View File
@@ -0,0 +1,22 @@
from django.db import models
import uuid
class BaseModel(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
# Timestamps
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
abstract = True
class Event(BaseModel):
name = models.CharField(max_length=255)
path = models.SlugField(max_length=100, unique=True)
def __str__(self) -> str:
return self.name
+21
View File
@@ -0,0 +1,21 @@
import contextvars
from django.core.exceptions import ImproperlyConfigured
from django.db import models
current_event = contextvars.ContextVar("current_event", default=None)
class EventScopedQuerySet(models.QuerySet):
def for_current_event(self):
event = current_event.get()
if event is None:
raise ImproperlyConfigured(
f"{self.model.__name__}.for_event was used outside an event-scoped request"
)
return self.filter(event=event)
class EventScopedManager(models.Manager.from_queryset(EventScopedQuerySet)):
def get_queryset(self):
return super().get_queryset().for_current_event()
+34
View File
@@ -0,0 +1,34 @@
from django.core.exceptions import ImproperlyConfigured
from django.test import TestCase
from games.models import Game
from submissions.models import Submission
from .models import Event
from .scoping import current_event
class EventScopedManagerTests(TestCase):
def setUp(self):
self.event_a = Event.objects.create(name="Event A", path="event-a")
self.event_b = Event.objects.create(name="Event B", path="event-b")
self.game = Game.objects.create(name="Test Game", path="test-game")
self.submission_a = Submission.objects.create(
event=self.event_a, game=self.game
)
self.submission_b = Submission.objects.create(
event=self.event_b, game=self.game
)
def test_for_event_raises_outside_event_context(self):
with self.assertRaises(ImproperlyConfigured):
list(Submission.for_event.all())
def test_for_event_filters_to_current_event(self):
token = current_event.set(self.event_a)
try:
results = list(Submission.for_event.all())
finally:
current_event.reset(token)
self.assertEqual(results, [self.submission_a])
+1
View File
@@ -0,0 +1 @@
# Create your views here.