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
+21 -15
View File
@@ -17,11 +17,13 @@ from market.schemas import (
router = Router(tags=["market"])
PREFIX = "/events/{event_path}/market"
@router.get("/", response=List[MarketListSchema])
def list_markets(request):
@router.get(f"{PREFIX}/", response=List[MarketListSchema])
def list_markets(request, event_path: str):
"""List all markets (excludes draft markets)."""
markets = Market.objects.exclude(status=Market.Status.DRAFT)
markets = Market.for_event.exclude(status=Market.Status.DRAFT)
# Prefetch options with total_bets annotation sorted by total_bets desc, then text asc
options_queryset = MarketOption.objects.annotate(
total_bets=Coalesce(Sum("user_bets__amount"), 0)
@@ -30,38 +32,40 @@ def list_markets(request):
return markets.prefetch_related(Prefetch("options", queryset=options_queryset))
@router.get("/user/bets", response=List[UserBetSchema])
def list_user_bets(request):
@router.get(f"{PREFIX}/user/bets", response=List[UserBetSchema])
def list_user_bets(request, event_path: str):
"""List all bets placed by the current user."""
if not request.user.is_authenticated:
raise HttpError(401, "Authentication required")
return (
UserBet.objects.filter(user=request.user)
UserBet.objects.filter(user=request.user, option__market__event=request.event)
.select_related("option__market")
.prefetch_related("option__market__options")
)
@router.post("/{market_uuid}/actions/close")
def close_market(request, market_uuid: str):
@router.post(f"{PREFIX}/{{market_uuid}}/actions/close")
def close_market(request, event_path: str, market_uuid: str):
"""Close a market. Admin only."""
if not request.user.is_staff:
raise HttpError(403, "Permission denied")
market = get_object_or_404(Market, uuid=market_uuid)
market = get_object_or_404(Market, uuid=market_uuid, event=request.event)
market.status = Market.Status.CLOSED
market.save(update_fields=["status", "updated_at"])
return {"status": Market.Status.CLOSED}
@router.post("/{market_uuid}/actions/resolve", response=MarketListSchema)
def resolve_market(request, market_uuid: str, payload: ResolveMarketSchema):
@router.post(f"{PREFIX}/{{market_uuid}}/actions/resolve", response=MarketListSchema)
def resolve_market(
request, event_path: str, market_uuid: str, payload: ResolveMarketSchema
):
"""Resolve a market with a winning option. Admin only."""
if not request.user.is_staff:
raise HttpError(403, "Permission denied")
market = get_object_or_404(Market, uuid=market_uuid)
market = get_object_or_404(Market, uuid=market_uuid, event=request.event)
winning_option = get_object_or_404(MarketOption, uuid=payload.winning_option_uuid)
if winning_option.market_id != market.id:
@@ -129,13 +133,15 @@ def resolve_market(request, market_uuid: str, payload: ResolveMarketSchema):
return market
@router.post("/{market_uuid}/bets", response=UserBetSchema)
def create_bet(request, market_uuid: str, payload: UserBetCreateSchema):
@router.post(f"{PREFIX}/{{market_uuid}}/bets", response=UserBetSchema)
def create_bet(
request, event_path: str, market_uuid: str, payload: UserBetCreateSchema
):
"""Place a bet on a market option."""
if not request.user.is_authenticated:
raise HttpError(401, "Authentication required")
market = get_object_or_404(Market, uuid=market_uuid)
market = get_object_or_404(Market, uuid=market_uuid, event=request.event)
option = get_object_or_404(MarketOption, uuid=payload.option_uuid)
if option.market_id != market.id:
@@ -0,0 +1,24 @@
# Generated by Django 6.0.5 on 2026-09-15 21:10
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("events", "0002_seed_polylan_43"),
("market", "0006_alter_market_status"),
]
operations = [
migrations.AddField(
model_name="market",
name="event",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="markets",
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")
Market = apps.get_model("market", "Market")
event = Event.objects.get(path=EVENT_PATH)
Market.objects.filter(event__isnull=True).update(event=event)
def noop(apps, schema_editor):
pass
class Migration(migrations.Migration):
dependencies = [
("market", "0007_market_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 = [
("market", "0008_backfill_event"),
]
operations = [
migrations.AlterField(
model_name="market",
name="event",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="markets",
to="events.event",
),
),
]
+12
View File
@@ -1,6 +1,9 @@
import uuid
from django.db import models
from events.models import Event
from events.scoping import EventScopedManager
class BaseModel(models.Model):
uuid = models.UUIDField(default=uuid.uuid4, unique=True, editable=False)
@@ -18,6 +21,12 @@ class Market(BaseModel):
CLOSED = "closed", "Closed"
RESOLVED = "resolved", "Resolved"
event = models.ForeignKey(
Event,
on_delete=models.CASCADE,
related_name="markets",
)
title = models.CharField(max_length=255)
description = models.TextField(blank=True)
status = models.CharField(
@@ -34,6 +43,9 @@ class Market(BaseModel):
related_name="market_won",
)
objects = models.Manager()
for_event = EventScopedManager()
class Meta:
ordering = ["-created_at"]
indexes = [
+6 -3
View File
@@ -1,8 +1,11 @@
from django.contrib.auth.decorators import login_required
from django.shortcuts import render
from django.shortcuts import get_object_or_404, render
from django.http import HttpRequest
from events.models import Event
@login_required
def market_home(request: HttpRequest):
return render(request, "market.html", {})
def market_home(request: HttpRequest, event_path: str):
event = get_object_or_404(Event, path=event_path)
return render(request, "market.html", {"event": event})