feat: add events app + "multi-tenants" aka multi-events
This commit is contained in:
@@ -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"]
|
||||
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class EventsConfig(AppConfig):
|
||||
name = "events"
|
||||
@@ -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),
|
||||
]
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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])
|
||||
@@ -0,0 +1 @@
|
||||
# Create your views here.
|
||||
Reference in New Issue
Block a user