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
+22 -16
View File
@@ -22,10 +22,12 @@ from opus_magnum.models import PuzzleResponse, SteamCollectionItem, SteamCollect
router = Router() router = Router()
PREFIX = "/events/{event_path}/results"
@router.get("results", response=RankingSchema)
def results(request: HttpRequest) -> dict: @router.get(f"{PREFIX}/results", response=RankingSchema)
cache_key = "api:results:results" def results(request: HttpRequest, event_path: str) -> dict:
cache_key = f"api:{event_path}:results:results"
cached_data = cache.get(cache_key) cached_data = cache.get(cache_key)
if cached_data is not None: if cached_data is not None:
@@ -35,7 +37,9 @@ def results(request: HttpRequest) -> dict:
responses_by_puzzleid = defaultdict(list) responses_by_puzzleid = defaultdict(list)
for response in list( for response in list(
PuzzleResponse.objects.filter(needs_manual_validation=False) PuzzleResponse.objects.filter(
needs_manual_validation=False, submission__event=request.event
)
.filter_user_best_response() .filter_user_best_response()
.prefetch_related("submission__user") .prefetch_related("submission__user")
): ):
@@ -51,25 +55,27 @@ def results(request: HttpRequest) -> dict:
data = { data = {
"users": list(CustomUser.objects.filter(pk__in=responses_by_userid.keys())), "users": list(CustomUser.objects.filter(pk__in=responses_by_userid.keys())),
"puzzles": list(SteamCollectionItem.objects.all()), "puzzles": list(
SteamCollectionItem.objects.filter(collection__event=request.event)
),
"responses_by_userid": dict(responses_by_userid), "responses_by_userid": dict(responses_by_userid),
"ranking_by_puzzle": ranking, "ranking_by_puzzle": ranking,
} }
cache.set("api:results:results", data, 300) cache.set(cache_key, data, 300)
return data return data
@router.get("top-submissions", response=TournamentSubmissionsOut) @router.get(f"{PREFIX}/top-submissions", response=TournamentSubmissionsOut)
def top_submissions(request: HttpRequest, limit: int = 5) -> dict: def top_submissions(request: HttpRequest, event_path: str, limit: int = 5) -> dict:
"""Get tournament top submissions for each puzzle. Only available when tournament is closed.""" """Get tournament top submissions for each puzzle. Only available when tournament is closed."""
cache_key = f"api:results:top_submissions:{limit}" cache_key = f"api:{event_path}:results:top_submissions:{limit}"
cached_data = cache.get(cache_key) cached_data = cache.get(cache_key)
if cached_data is not None: if cached_data is not None:
return cached_data return cached_data
collection = get_object_or_404(SteamCollection, is_active=True) collection = get_object_or_404(SteamCollection, is_active=True, event=request.event)
# Only allow access when tournament is closed # Only allow access when tournament is closed
if collection.accepting_submissions: if collection.accepting_submissions:
@@ -141,20 +147,20 @@ def top_submissions(request: HttpRequest, limit: int = 5) -> dict:
) )
data = {"submissions": submissions_list} data = {"submissions": submissions_list}
cache.set(f"api:results:top_submissions:{limit}", data, 300) cache.set(cache_key, data, 300)
return data return data
@router.get("puzzle-results", response=TournamentPuzzleResultsOut) @router.get(f"{PREFIX}/puzzle-results", response=TournamentPuzzleResultsOut)
def puzzle_results(request: HttpRequest, limit: int = 5) -> dict: def puzzle_results(request: HttpRequest, event_path: str, limit: int = 5) -> dict:
"""Get tournament results organized by puzzle with coefficients. Only available when tournament is closed.""" """Get tournament results organized by puzzle with coefficients. Only available when tournament is closed."""
cache_key = f"api:results:puzzle_results:{limit}" cache_key = f"api:{event_path}:results:puzzle_results:{limit}"
cached_data = cache.get(cache_key) cached_data = cache.get(cache_key)
if cached_data is not None: if cached_data is not None:
return cached_data return cached_data
collection = get_object_or_404(SteamCollection, is_active=True) collection = get_object_or_404(SteamCollection, is_active=True, event=request.event)
# Only allow access when tournament is closed # Only allow access when tournament is closed
if collection.accepting_submissions: if collection.accepting_submissions:
@@ -237,5 +243,5 @@ def puzzle_results(request: HttpRequest, limit: int = 5) -> dict:
) )
data = {"results": results_list} data = {"results": results_list}
cache.set(f"api:results:puzzle_results:{limit}", data, 300) cache.set(cache_key, data, 300)
return data return data
+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.
+13 -18
View File
@@ -1,28 +1,23 @@
from django.contrib import admin from django.contrib import admin
from .models import Game from .models import EventGame, Game
def enable_games(modeladmin, request, queryset): class EventGameInline(admin.TabularInline):
updated = queryset.update(enabled=True) model = EventGame
modeladmin.message_user(request, f"{updated} game(s) enabled successfully.") extra = 0
enable_games.short_description = "Enable selected games"
def disable_games(modeladmin, request, queryset):
updated = queryset.update(enabled=False)
modeladmin.message_user(request, f"{updated} game(s) disabled successfully.")
disable_games.short_description = "Disable selected games"
@admin.register(Game) @admin.register(Game)
class GameAdmin(admin.ModelAdmin): class GameAdmin(admin.ModelAdmin):
list_display = ["name", "steam_app_id", "enabled", "updated_at"] list_display = ["name", "steam_app_id", "updated_at"]
list_filter = ["enabled"]
search_fields = ["name", "steam_app_id"] search_fields = ["name", "steam_app_id"]
readonly_fields = ["created_at", "updated_at"] readonly_fields = ["created_at", "updated_at"]
actions = [enable_games, disable_games] inlines = [EventGameInline]
@admin.register(EventGame)
class EventGameAdmin(admin.ModelAdmin):
list_display = ["game", "event", "enabled"]
list_filter = ["event", "enabled"]
search_fields = ["game__name", "event__name"]
+7 -1
View File
@@ -6,8 +6,14 @@ from .models import Game
from .schemas import GameOut from .schemas import GameOut
router = Router() router = Router()
event_router = Router()
@router.get("", response=List[GameOut]) @router.get("", response=List[GameOut])
def list_games(request): def list_games(request):
return Game.objects.filter(enabled=True) return Game.objects.all()
@event_router.get("/events/{event_path}/games", response=List[GameOut])
def list_event_games(request, event_path: str):
return Game.objects.enabled_for_event(request.event)
+10 -5
View File
@@ -1,7 +1,9 @@
from functools import wraps from functools import wraps
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.shortcuts import get_object_or_404
from events.models import Event
from .models import Game from .models import Game
@@ -9,12 +11,15 @@ def require_game_enabled(steam_app_id: int):
def decorator(view_func): def decorator(view_func):
@wraps(view_func) @wraps(view_func)
def wrapper(request, *args, **kwargs): def wrapper(request, *args, **kwargs):
try: event = get_object_or_404(Event, path=kwargs.get("event_path"))
game = Game.objects.get(steam_app_id=steam_app_id)
except Game.DoesNotExist: if (
raise PermissionDenied not Game.objects.enabled_for_event(event)
if not game.enabled: .filter(steam_app_id=steam_app_id)
.exists()
):
raise PermissionDenied raise PermissionDenied
return view_func(request, *args, **kwargs) return view_func(request, *args, **kwargs)
return wrapper return wrapper
@@ -0,0 +1,52 @@
# Generated by Django 6.0.5 on 2026-09-15 21:11
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("events", "0002_seed_polylan_43"),
("games", "0004_game_image_alter_game_steam_app_id"),
]
operations = [
migrations.CreateModel(
name="EventGame",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("enabled", models.BooleanField(default=True)),
(
"event",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="event_games",
to="events.event",
),
),
(
"game",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="event_games",
to="games.game",
),
),
],
options={
"constraints": [
models.UniqueConstraint(
fields=("event", "game"), name="unique_event_game"
)
],
},
),
]
@@ -0,0 +1,16 @@
# Generated by Django 6.0.5 on 2026-09-15 21:51
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("games", "0005_eventgame"),
]
operations = [
migrations.RemoveField(
model_name="game",
name="enabled",
),
]
+34 -1
View File
@@ -1,16 +1,24 @@
from django.db import models from django.db import models
from events.models import Event
class GameQuerySet(models.QuerySet):
def enabled_for_event(self, event):
return self.exclude(event_games__event=event, event_games__enabled=False)
class Game(models.Model): class Game(models.Model):
steam_app_id = models.PositiveIntegerField(null=True, blank=True) steam_app_id = models.PositiveIntegerField(null=True, blank=True)
name = models.CharField(max_length=255) name = models.CharField(max_length=255)
path = models.CharField(max_length=100) path = models.CharField(max_length=100)
image = models.URLField(null=True, blank=True) image = models.URLField(null=True, blank=True)
enabled = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True) updated_at = models.DateTimeField(auto_now=True)
objects = models.Manager.from_queryset(GameQuerySet)()
class Meta: class Meta:
ordering = ["name"] ordering = ["name"]
@@ -18,3 +26,28 @@ class Game(models.Model):
if self.steam_app_id: if self.steam_app_id:
return f"{self.name} ({self.steam_app_id})" return f"{self.name} ({self.steam_app_id})"
return self.name return self.name
class EventGame(models.Model):
event = models.ForeignKey(
Event,
on_delete=models.CASCADE,
related_name="event_games",
)
game = models.ForeignKey(
Game,
on_delete=models.CASCADE,
related_name="event_games",
)
enabled = models.BooleanField(default=True)
class Meta:
constraints = [
models.UniqueConstraint(
fields=["event", "game"],
name="unique_event_game",
),
]
def __str__(self) -> str:
return f"{self.game.name} @ {self.event.name} ({'enabled' if self.enabled else 'disabled'})"
+21 -15
View File
@@ -17,11 +17,13 @@ from market.schemas import (
router = Router(tags=["market"]) 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).""" """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 # Prefetch options with total_bets annotation sorted by total_bets desc, then text asc
options_queryset = MarketOption.objects.annotate( options_queryset = MarketOption.objects.annotate(
total_bets=Coalesce(Sum("user_bets__amount"), 0) 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)) return markets.prefetch_related(Prefetch("options", queryset=options_queryset))
@router.get("/user/bets", response=List[UserBetSchema]) @router.get(f"{PREFIX}/user/bets", response=List[UserBetSchema])
def list_user_bets(request): def list_user_bets(request, event_path: str):
"""List all bets placed by the current user.""" """List all bets placed by the current user."""
if not request.user.is_authenticated: if not request.user.is_authenticated:
raise HttpError(401, "Authentication required") raise HttpError(401, "Authentication required")
return ( return (
UserBet.objects.filter(user=request.user) UserBet.objects.filter(user=request.user, option__market__event=request.event)
.select_related("option__market") .select_related("option__market")
.prefetch_related("option__market__options") .prefetch_related("option__market__options")
) )
@router.post("/{market_uuid}/actions/close") @router.post(f"{PREFIX}/{{market_uuid}}/actions/close")
def close_market(request, market_uuid: str): def close_market(request, event_path: str, market_uuid: str):
"""Close a market. Admin only.""" """Close a market. Admin only."""
if not request.user.is_staff: if not request.user.is_staff:
raise HttpError(403, "Permission denied") 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.status = Market.Status.CLOSED
market.save(update_fields=["status", "updated_at"]) market.save(update_fields=["status", "updated_at"])
return {"status": Market.Status.CLOSED} return {"status": Market.Status.CLOSED}
@router.post("/{market_uuid}/actions/resolve", response=MarketListSchema) @router.post(f"{PREFIX}/{{market_uuid}}/actions/resolve", response=MarketListSchema)
def resolve_market(request, market_uuid: str, payload: ResolveMarketSchema): def resolve_market(
request, event_path: str, market_uuid: str, payload: ResolveMarketSchema
):
"""Resolve a market with a winning option. Admin only.""" """Resolve a market with a winning option. Admin only."""
if not request.user.is_staff: if not request.user.is_staff:
raise HttpError(403, "Permission denied") 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) winning_option = get_object_or_404(MarketOption, uuid=payload.winning_option_uuid)
if winning_option.market_id != market.id: if winning_option.market_id != market.id:
@@ -129,13 +133,15 @@ def resolve_market(request, market_uuid: str, payload: ResolveMarketSchema):
return market return market
@router.post("/{market_uuid}/bets", response=UserBetSchema) @router.post(f"{PREFIX}/{{market_uuid}}/bets", response=UserBetSchema)
def create_bet(request, market_uuid: str, payload: UserBetCreateSchema): def create_bet(
request, event_path: str, market_uuid: str, payload: UserBetCreateSchema
):
"""Place a bet on a market option.""" """Place a bet on a market option."""
if not request.user.is_authenticated: if not request.user.is_authenticated:
raise HttpError(401, "Authentication required") 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) option = get_object_or_404(MarketOption, uuid=payload.option_uuid)
if option.market_id != market.id: 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 import uuid
from django.db import models from django.db import models
from events.models import Event
from events.scoping import EventScopedManager
class BaseModel(models.Model): class BaseModel(models.Model):
uuid = models.UUIDField(default=uuid.uuid4, unique=True, editable=False) uuid = models.UUIDField(default=uuid.uuid4, unique=True, editable=False)
@@ -18,6 +21,12 @@ class Market(BaseModel):
CLOSED = "closed", "Closed" CLOSED = "closed", "Closed"
RESOLVED = "resolved", "Resolved" RESOLVED = "resolved", "Resolved"
event = models.ForeignKey(
Event,
on_delete=models.CASCADE,
related_name="markets",
)
title = models.CharField(max_length=255) title = models.CharField(max_length=255)
description = models.TextField(blank=True) description = models.TextField(blank=True)
status = models.CharField( status = models.CharField(
@@ -34,6 +43,9 @@ class Market(BaseModel):
related_name="market_won", related_name="market_won",
) )
objects = models.Manager()
for_event = EventScopedManager()
class Meta: class Meta:
ordering = ["-created_at"] ordering = ["-created_at"]
indexes = [ indexes = [
+6 -3
View File
@@ -1,8 +1,11 @@
from django.contrib.auth.decorators import login_required 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 django.http import HttpRequest
from events.models import Event
@login_required @login_required
def market_home(request: HttpRequest): def market_home(request: HttpRequest, event_path: str):
return render(request, "market.html", {}) event = get_object_or_404(Event, path=event_path)
return render(request, "market.html", {"event": event})
+23 -17
View File
@@ -25,11 +25,13 @@ from .schemas import NoitaSubmissionOut
router = Router() router = Router()
NOITA_APP_ID = 881100 NOITA_APP_ID = 881100
PREFIX = "/events/{event_path}/noita"
@router.get("results", response=ResultsOut)
@router.get(f"{PREFIX}/results", response=ResultsOut)
@decorate_view(require_game_enabled(NOITA_APP_ID)) @decorate_view(require_game_enabled(NOITA_APP_ID))
def get_results(request: HttpRequest): def get_results(request: HttpRequest, event_path: str):
cache_key = f"api:noita:results:{request.user.id}" cache_key = f"api:{event_path}:noita:results:{request.user.id}"
cached_data = cache.get(cache_key) cached_data = cache.get(cache_key)
if cached_data is not None: if cached_data is not None:
@@ -42,7 +44,7 @@ def get_results(request: HttpRequest):
""" """
# Group objectives by objectiv_id and count occurrences # Group objectives by objectiv_id and count occurrences
user_objectives = ( user_objectives = (
Objectiv.objects.filter(user=request.user) Objectiv.objects.filter(user=request.user, submission__event=request.event)
.values("objectiv_id") .values("objectiv_id")
.annotate(count=Count("id")) .annotate(count=Count("id"))
) )
@@ -118,7 +120,7 @@ def get_results(request: HttpRequest):
total_score += points total_score += points
# Count deaths for the user # Count deaths for the user
deaths_count = DeathCounter.objects.filter(user=request.user).count() deaths_count = DeathCounter.for_event.filter(user=request.user).count()
data = { data = {
"total_score": total_score, "total_score": total_score,
@@ -126,19 +128,19 @@ def get_results(request: HttpRequest):
"objectives": list(with_points.values()), "objectives": list(with_points.values()),
} }
cache.set(f"api:noita:results:{request.user.id}", data, 300) cache.set(cache_key, data, 300)
return data return data
@router.get("leaderboard", response=LeaderboardOut) @router.get(f"{PREFIX}/leaderboard", response=LeaderboardOut)
@decorate_view(require_game_enabled(NOITA_APP_ID)) @decorate_view(require_game_enabled(NOITA_APP_ID))
def get_leaderboard(request: HttpRequest): def get_leaderboard(request: HttpRequest, event_path: str):
""" """
Get the global leaderboard for all users ranked by total score. Get the global leaderboard for all users ranked by total score.
Uses Window functions to rank users by their total score in descending order. Uses Window functions to rank users by their total score in descending order.
""" """
cache_key = "api:noita:leaderboard" cache_key = f"api:{event_path}:noita:leaderboard"
cached_data = cache.get(cache_key) cached_data = cache.get(cache_key)
if cached_data is not None: if cached_data is not None:
@@ -150,7 +152,8 @@ def get_leaderboard(request: HttpRequest):
# Get all objectives with calculated points (grouped by objectiv_id and user) # Get all objectives with calculated points (grouped by objectiv_id and user)
all_objectives = ( all_objectives = (
Objectiv.objects.values("user", "objectiv_id") Objectiv.objects.filter(submission__event=request.event)
.values("user", "objectiv_id")
.annotate(count=Count("id")) .annotate(count=Count("id"))
.annotate( .annotate(
# Fetch points from ObjectivPoint using Subquery # Fetch points from ObjectivPoint using Subquery
@@ -201,12 +204,12 @@ def get_leaderboard(request: HttpRequest):
for user_id, total_score in user_totals_dict.items(): for user_id, total_score in user_totals_dict.items():
user = User.objects.get(id=user_id) user = User.objects.get(id=user_id)
objectives_count = ( objectives_count = (
Objectiv.objects.filter(user_id=user_id) Objectiv.objects.filter(user_id=user_id, submission__event=request.event)
.values("objectiv_id") .values("objectiv_id")
.distinct() .distinct()
.count() .count()
) )
deaths_count = DeathCounter.objects.filter(user_id=user_id).count() deaths_count = DeathCounter.for_event.filter(user_id=user_id).count()
users_with_scores.append( users_with_scores.append(
{ {
"user_id": user_id, "user_id": user_id,
@@ -232,13 +235,15 @@ def get_leaderboard(request: HttpRequest):
] ]
data = {"leaderboard": leaderboard} data = {"leaderboard": leaderboard}
cache.set("api:noita:leaderboard", data, 300) cache.set(cache_key, data, 300)
return data return data
@router.post("submit", response={200: NoitaSubmissionOut, 400: dict}) @router.post(f"{PREFIX}/submit", response={200: NoitaSubmissionOut, 400: dict})
@decorate_view(require_game_enabled(NOITA_APP_ID)) @decorate_view(require_game_enabled(NOITA_APP_ID))
def submit_log_file(request: HttpRequest, file: UploadedFile = File(...)): def submit_log_file(
request: HttpRequest, event_path: str, file: UploadedFile = File(...)
):
""" """
Submit a Noita run file (log file, screenshot, or video). Submit a Noita run file (log file, screenshot, or video).
@@ -267,6 +272,7 @@ def submit_log_file(request: HttpRequest, file: UploadedFile = File(...)):
try: try:
# Create submission # Create submission
submission = LogfileSubmission.objects.create( submission = LogfileSubmission.objects.create(
event=request.event,
user=request.user if request.user.is_authenticated else None, user=request.user if request.user.is_authenticated else None,
content_type=file.content_type, content_type=file.content_type,
file_size=file.size, file_size=file.size,
@@ -284,8 +290,8 @@ def submit_log_file(request: HttpRequest, file: UploadedFile = File(...)):
# Invalidate caches on successful submission # Invalidate caches on successful submission
if submission.user: if submission.user:
cache.delete(f"api:noita:results:{submission.user.id}") cache.delete(f"api:{event_path}:noita:results:{submission.user.id}")
cache.delete("api:noita:leaderboard") cache.delete(f"api:{event_path}:noita:leaderboard")
return { return {
"id": str(submission.id), "id": str(submission.id),
@@ -0,0 +1,34 @@
# 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"),
("noita", "0012_deathcounter_unique_death_per_seen_at"),
]
operations = [
migrations.AddField(
model_name="deathcounter",
name="event",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="death_counters",
to="events.event",
),
),
migrations.AddField(
model_name="logfilesubmission",
name="event",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="logfile_submissions",
to="events.event",
),
),
]
@@ -0,0 +1,27 @@
from django.db import migrations
EVENT_PATH = "polylan-43"
def backfill_event(apps, schema_editor):
Event = apps.get_model("events", "Event")
LogfileSubmission = apps.get_model("noita", "LogfileSubmission")
DeathCounter = apps.get_model("noita", "DeathCounter")
event = Event.objects.get(path=EVENT_PATH)
LogfileSubmission.objects.filter(event__isnull=True).update(event=event)
DeathCounter.objects.filter(event__isnull=True).update(event=event)
def noop(apps, schema_editor):
pass
class Migration(migrations.Migration):
dependencies = [
("noita", "0013_deathcounter_event_logfilesubmission_event"),
("events", "0002_seed_polylan_43"),
]
operations = [
migrations.RunPython(backfill_event, noop),
]
@@ -0,0 +1,29 @@
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("noita", "0014_backfill_event"),
]
operations = [
migrations.AlterField(
model_name="deathcounter",
name="event",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="death_counters",
to="events.event",
),
),
migrations.AlterField(
model_name="logfilesubmission",
name="event",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="logfile_submissions",
to="events.event",
),
),
]
+20
View File
@@ -4,6 +4,9 @@ from django.utils import timezone
import uuid import uuid
from events.models import Event
from events.scoping import EventScopedManager
User = get_user_model() User = get_user_model()
@@ -21,6 +24,12 @@ class LogfileSubmission(models.Model):
# Identification # Identification
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
event = models.ForeignKey(
Event,
on_delete=models.CASCADE,
related_name="logfile_submissions",
)
content_type = models.CharField(max_length=100, help_text="MIME type of the file") content_type = models.CharField(max_length=100, help_text="MIME type of the file")
file_size = models.PositiveIntegerField(help_text="File size in bytes") file_size = models.PositiveIntegerField(help_text="File size in bytes")
file = models.FileField( file = models.FileField(
@@ -44,6 +53,9 @@ class LogfileSubmission(models.Model):
processed = models.BooleanField(default=False) processed = models.BooleanField(default=False)
objects = models.Manager()
for_event = EventScopedManager()
class Objectiv(models.Model): class Objectiv(models.Model):
objectiv_id = models.CharField(max_length=64) objectiv_id = models.CharField(max_length=64)
@@ -71,10 +83,18 @@ class ObjectivPoint(models.Model):
class DeathCounter(models.Model): class DeathCounter(models.Model):
event = models.ForeignKey(
Event,
on_delete=models.CASCADE,
related_name="death_counters",
)
user = models.ForeignKey(User, on_delete=models.CASCADE) user = models.ForeignKey(User, on_delete=models.CASCADE)
seed = models.CharField(max_length=32) seed = models.CharField(max_length=32)
seen_at = models.DateTimeField() seen_at = models.DateTimeField()
objects = models.Manager()
for_event = EventScopedManager()
class Meta: class Meta:
constraints = [ constraints = [
models.UniqueConstraint( models.UniqueConstraint(
@@ -31,7 +31,11 @@ def parse_objectives_and_store(logfile: LogfileSubmission) -> None:
continue continue
if idx == "DEATH": if idx == "DEATH":
deaths.append(DeathCounter(user=logfile.user, seed=seed, seen_at=ts)) deaths.append(
DeathCounter(
event=logfile.event, user=logfile.user, seed=seed, seen_at=ts
)
)
continue continue
objectives.append( objectives.append(
+64 -46
View File
@@ -32,50 +32,51 @@ from .schemas import (
router = Router() router = Router()
OPUS_MAGNUM_APP_ID = 558990 OPUS_MAGNUM_APP_ID = 558990
PREFIX = "/events/{event_path}/opus-magnum"
@router.get("/puzzles", response=List[SteamCollectionItemOut])
@router.get(f"{PREFIX}/puzzles", response=List[SteamCollectionItemOut])
@decorate_view(require_game_enabled(OPUS_MAGNUM_APP_ID)) @decorate_view(require_game_enabled(OPUS_MAGNUM_APP_ID))
def list_puzzles(request): def list_puzzles(request, event_path: str):
"""Get list of available puzzles""" """Get list of available puzzles"""
return SteamCollectionItem.objects.select_related("collection").filter( return SteamCollectionItem.objects.select_related("collection").filter(
collection__is_active=True collection__is_active=True, collection__event=request.event
) )
@router.get("/collection", response=SteamCollectionOut) @router.get(f"{PREFIX}/collection", response=SteamCollectionOut)
@decorate_view(require_game_enabled(OPUS_MAGNUM_APP_ID)) @decorate_view(require_game_enabled(OPUS_MAGNUM_APP_ID))
def get_collection(request): def get_collection(request, event_path: str):
"""Get the active collection details""" """Get the active collection details"""
collection = get_object_or_404(SteamCollection, is_active=True) return get_object_or_404(SteamCollection.for_event, is_active=True)
return collection
@router.get("/submissions", response=List[SubmissionOut]) @router.get(f"{PREFIX}/submissions", response=List[SubmissionOut])
@decorate_view(require_game_enabled(OPUS_MAGNUM_APP_ID)) @decorate_view(require_game_enabled(OPUS_MAGNUM_APP_ID))
@paginate @paginate
def list_submissions(request): def list_submissions(request, event_path: str):
"""Get paginated list of submissions""" """Get paginated list of submissions"""
return Submission.objects.prefetch_related( return Submission.for_event.prefetch_related(
"responses__files", "responses__puzzle" "responses__files", "responses__puzzle"
).filter(user=request.user) ).filter(user=request.user)
@router.get("/submissions/{submission_id}", response=SubmissionOut) @router.get(f"{PREFIX}/submissions/{{submission_id}}", response=SubmissionOut)
@decorate_view(require_game_enabled(OPUS_MAGNUM_APP_ID)) @decorate_view(require_game_enabled(OPUS_MAGNUM_APP_ID))
def get_submission(request, submission_id: str): def get_submission(request, event_path: str, submission_id: str):
"""Get detailed submission by ID""" """Get detailed submission by ID"""
return get_object_or_404( return get_object_or_404(
Submission.objects.prefetch_related( Submission.for_event.prefetch_related(
"responses__files", "responses__puzzle" "responses__files", "responses__puzzle"
).filter(user=request.user), ).filter(user=request.user),
id=submission_id, id=submission_id,
) )
@router.post("/submissions", response=SubmissionOut) @router.post(f"{PREFIX}/submissions", response=SubmissionOut)
@decorate_view(require_game_enabled(OPUS_MAGNUM_APP_ID)) @decorate_view(require_game_enabled(OPUS_MAGNUM_APP_ID))
def create_submission( def create_submission(
request, data: SubmissionIn, files: List[UploadedFile] = File(...) request, event_path: str, data: SubmissionIn, files: List[UploadedFile] = File(...)
): ):
"""Create a new submission with multiple puzzle responses""" """Create a new submission with multiple puzzle responses"""
@@ -92,7 +93,9 @@ def create_submission(
if data.responses: if data.responses:
for puzzle in data.responses: for puzzle in data.responses:
if not get_object_or_404( if not get_object_or_404(
SteamCollectionItem, id=puzzle.puzzle_id SteamCollectionItem,
id=puzzle.puzzle_id,
collection__event=request.event,
).collection.accepting_submissions: ).collection.accepting_submissions:
raise HttpError( raise HttpError(
403, "This tournament is no longer accepting submissions" 403, "This tournament is no longer accepting submissions"
@@ -119,6 +122,7 @@ def create_submission(
# Create the submission # Create the submission
submission = Submission.objects.create( submission = Submission.objects.create(
event=request.event,
user=request.user if request.user.is_authenticated else None, user=request.user if request.user.is_authenticated else None,
notes=data.notes, notes=data.notes,
manual_validation_requested=data.manual_validation_requested manual_validation_requested=data.manual_validation_requested
@@ -129,7 +133,9 @@ def create_submission(
for response_data in data.responses: for response_data in data.responses:
# Get the puzzle # Get the puzzle
puzzle = get_object_or_404( puzzle = get_object_or_404(
SteamCollectionItem, id=response_data.puzzle_id SteamCollectionItem,
id=response_data.puzzle_id,
collection__event=request.event,
) )
# Create the puzzle response # Create the puzzle response
@@ -192,12 +198,12 @@ def create_submission(
response.mark_for_validation("Incomplete OCR data") response.mark_for_validation("Incomplete OCR data")
# Reload with relations for response # Reload with relations for response
submission = Submission.objects.prefetch_related( submission = Submission.for_event.prefetch_related(
"responses__files", "responses__puzzle" "responses__files", "responses__puzzle"
).get(id=submission.id) ).get(id=submission.id)
# Invalidate results cache on successful submission # Invalidate results cache on successful submission
cache.delete("api:results:results") cache.delete(f"api:{event_path}:results:results")
return submission return submission
@@ -205,18 +211,22 @@ def create_submission(
return 500, {"detail": f"Error creating submission: {str(e)}"} return 500, {"detail": f"Error creating submission: {str(e)}"}
@router.put("/responses/{response_id}/validate", response=PuzzleResponseOut) @router.put(f"{PREFIX}/responses/{{response_id}}/validate", response=PuzzleResponseOut)
@decorate_view(require_game_enabled(OPUS_MAGNUM_APP_ID)) @decorate_view(require_game_enabled(OPUS_MAGNUM_APP_ID))
def validate_response(request, response_id: int, data: ValidationIn): def validate_response(request, event_path: str, response_id: int, data: ValidationIn):
"""Manually validate a puzzle response""" """Manually validate a puzzle response"""
if not request.user.is_authenticated or not request.user.is_staff: if not request.user.is_authenticated or not request.user.is_staff:
return 403, {"detail": "Admin access required"} return 403, {"detail": "Admin access required"}
response = get_object_or_404(PuzzleResponse, id=response_id) response = get_object_or_404(
PuzzleResponse, id=response_id, submission__event=request.event
)
if data.puzzle is not None: if data.puzzle is not None:
puzzle = get_object_or_404(SteamCollectionItem, id=data.puzzle) puzzle = get_object_or_404(
SteamCollectionItem, id=data.puzzle, collection__event=request.event
)
response.puzzle = puzzle response.puzzle = puzzle
# Update validated values # Update validated values
@@ -236,20 +246,24 @@ def validate_response(request, response_id: int, data: ValidationIn):
response.save() response.save()
# Invalidate results cache when a response is validated # Invalidate results cache when a response is validated
cache.delete("api:results:results") cache.delete(f"api:{event_path}:results:results")
return response return response
@router.put("/responses/{response_id}/validate/auto", response=PuzzleResponseOut) @router.put(
f"{PREFIX}/responses/{{response_id}}/validate/auto", response=PuzzleResponseOut
)
@decorate_view(require_game_enabled(OPUS_MAGNUM_APP_ID)) @decorate_view(require_game_enabled(OPUS_MAGNUM_APP_ID))
def validate_auto(request, response_id: int): def validate_auto(request, event_path: str, response_id: int):
"""Try to auto validate a puzzle response""" """Try to auto validate a puzzle response"""
if not request.user.is_authenticated or not request.user.is_staff: if not request.user.is_authenticated or not request.user.is_staff:
return 403, {"detail": "Admin access required"} return 403, {"detail": "Admin access required"}
response = get_object_or_404(PuzzleResponse, id=response_id) response = get_object_or_404(
PuzzleResponse, id=response_id, submission__event=request.event
)
for file in response.files.all(): for file in response.files.all():
verify_and_validate_ocr_date_for_submission(file) verify_and_validate_ocr_date_for_submission(file)
@@ -257,9 +271,9 @@ def validate_auto(request, response_id: int):
return response return response
@router.get("/responses/needs-validation", response=List[PuzzleResponseOut]) @router.get(f"{PREFIX}/responses/needs-validation", response=List[PuzzleResponseOut])
@decorate_view(require_game_enabled(OPUS_MAGNUM_APP_ID)) @decorate_view(require_game_enabled(OPUS_MAGNUM_APP_ID))
def list_responses_needing_validation(request): def list_responses_needing_validation(request, event_path: str):
"""Get all responses that need manual validation""" """Get all responses that need manual validation"""
if not request.user.is_authenticated or not request.user.is_staff: if not request.user.is_authenticated or not request.user.is_staff:
@@ -267,21 +281,23 @@ def list_responses_needing_validation(request):
return ( return (
PuzzleResponse.objects.filter(needs_manual_validation=True) PuzzleResponse.objects.filter(needs_manual_validation=True)
.filter(puzzle__collection__is_active=True) .filter(
puzzle__collection__is_active=True, puzzle__collection__event=request.event
)
.select_related("puzzle", "submission") .select_related("puzzle", "submission")
.prefetch_related("files") .prefetch_related("files")
) )
@router.post("/submissions/{submission_id}/validate", response=SubmissionOut) @router.post(f"{PREFIX}/submissions/{{submission_id}}/validate", response=SubmissionOut)
@decorate_view(require_game_enabled(OPUS_MAGNUM_APP_ID)) @decorate_view(require_game_enabled(OPUS_MAGNUM_APP_ID))
def validate_submission(request, submission_id: str): def validate_submission(request, event_path: str, submission_id: str):
"""Mark entire submission as validated""" """Mark entire submission as validated"""
if not request.user.is_authenticated or not request.user.is_staff: if not request.user.is_authenticated or not request.user.is_staff:
return 403, {"detail": "Admin access required"} return 403, {"detail": "Admin access required"}
submission = get_object_or_404(Submission, id=submission_id) submission = get_object_or_404(Submission.for_event, id=submission_id)
submission.is_validated = True submission.is_validated = True
submission.validated_by = request.user submission.validated_by = request.user
@@ -292,44 +308,46 @@ def validate_submission(request, submission_id: str):
submission.responses.update(needs_manual_validation=False) submission.responses.update(needs_manual_validation=False)
# Reload with relations # Reload with relations
submission = Submission.objects.prefetch_related( submission = Submission.for_event.prefetch_related(
"responses__files", "responses__puzzle" "responses__files", "responses__puzzle"
).get(id=submission.id) ).get(id=submission.id)
# Invalidate results cache when submission is validated # Invalidate results cache when submission is validated
cache.delete("api:results:results") cache.delete(f"api:{event_path}:results:results")
return submission return submission
@router.delete("/submissions/{submission_id}") @router.delete(f"{PREFIX}/submissions/{{submission_id}}")
@decorate_view(require_game_enabled(OPUS_MAGNUM_APP_ID)) @decorate_view(require_game_enabled(OPUS_MAGNUM_APP_ID))
def delete_submission(request, submission_id: str): def delete_submission(request, event_path: str, submission_id: str):
"""Delete a submission (admin only)""" """Delete a submission (admin only)"""
if not request.user.is_authenticated or not request.user.is_staff: if not request.user.is_authenticated or not request.user.is_staff:
return 403, {"detail": "Admin access required"} return 403, {"detail": "Admin access required"}
submission = get_object_or_404(Submission, id=submission_id) submission = get_object_or_404(Submission.for_event, id=submission_id)
submission.delete() submission.delete()
# Invalidate results cache when submission is deleted # Invalidate results cache when submission is deleted
cache.delete("api:results:results") cache.delete(f"api:{event_path}:results:results")
return {"detail": "Submission deleted successfully"} return {"detail": "Submission deleted successfully"}
@router.get("/stats") @router.get(f"{PREFIX}/stats")
@decorate_view(require_game_enabled(OPUS_MAGNUM_APP_ID)) @decorate_view(require_game_enabled(OPUS_MAGNUM_APP_ID))
def get_stats(request): def get_stats(request, event_path: str):
"""Get submission statistics""" """Get submission statistics"""
total_submissions = Submission.objects.count() total_submissions = Submission.for_event.count()
total_responses = PuzzleResponse.objects.count() total_responses = PuzzleResponse.objects.filter(
needs_validation = PuzzleResponse.objects.filter( submission__event=request.event
needs_manual_validation=True
).count() ).count()
validated_submissions = Submission.objects.filter(is_validated=True).count() needs_validation = PuzzleResponse.objects.filter(
needs_manual_validation=True, submission__event=request.event
).count()
validated_submissions = Submission.for_event.filter(is_validated=True).count()
return { return {
"total_submissions": total_submissions, "total_submissions": total_submissions,
@@ -3,45 +3,31 @@
from django.db import migrations from django.db import migrations
TABLE_PAIRS = [
("submissions_steamapikey", "opus_magnum_steamapikey"),
("submissions_steamcollection", "opus_magnum_steamcollection"),
("submissions_steamcollectionitem", "opus_magnum_steamcollectionitem"),
("submissions_submission", "opus_magnum_submission"),
("submissions_puzzleresponse", "opus_magnum_puzzleresponse"),
("submissions_submissionfile", "opus_magnum_submissionfile"),
]
def migrate_data_forward(apps, schema_editor): def migrate_data_forward(apps, schema_editor):
"""Copy data from submissions_* tables to opus_magnum_* tables""" """Copy data from submissions_* tables to opus_magnum_* tables.
# Use raw SQL to copy data while preserving all fields
The source tables only exist on databases that predate the submissions ->
opus_magnum rename; a database built fresh from this migration history
never has them, so skip pairs whose source table is absent instead of
failing.
"""
existing_tables = set(schema_editor.connection.introspection.table_names())
with schema_editor.connection.cursor() as cursor: with schema_editor.connection.cursor() as cursor:
# Copy SteamAPIKey for source, dest in TABLE_PAIRS:
cursor.execute(""" if source not in existing_tables:
INSERT INTO opus_magnum_steamapikey continue
SELECT * FROM submissions_steamapikey cursor.execute(f"INSERT INTO {dest} SELECT * FROM {source}")
""")
# Copy SteamCollection
cursor.execute("""
INSERT INTO opus_magnum_steamcollection
SELECT * FROM submissions_steamcollection
""")
# Copy SteamCollectionItem
cursor.execute("""
INSERT INTO opus_magnum_steamcollectionitem
SELECT * FROM submissions_steamcollectionitem
""")
# Copy Submission
cursor.execute("""
INSERT INTO opus_magnum_submission
SELECT * FROM submissions_submission
""")
# Copy PuzzleResponse
cursor.execute("""
INSERT INTO opus_magnum_puzzleresponse
SELECT * FROM submissions_puzzleresponse
""")
# Copy SubmissionFile
cursor.execute("""
INSERT INTO opus_magnum_submissionfile
SELECT * FROM submissions_submissionfile
""")
def migrate_data_backward(apps, schema_editor): def migrate_data_backward(apps, schema_editor):
@@ -0,0 +1,34 @@
# Generated by Django 6.0.5 on 2026-09-15 21:09
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("events", "0002_seed_polylan_43"),
("opus_magnum", "0015_migrate_data_from_submissions"),
]
operations = [
migrations.AddField(
model_name="steamcollection",
name="event",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="steam_collections",
to="events.event",
),
),
migrations.AddField(
model_name="submission",
name="event",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="opus_magnum_submissions",
to="events.event",
),
),
]
@@ -0,0 +1,27 @@
from django.db import migrations
EVENT_PATH = "polylan-43"
def backfill_event(apps, schema_editor):
Event = apps.get_model("events", "Event")
SteamCollection = apps.get_model("opus_magnum", "SteamCollection")
Submission = apps.get_model("opus_magnum", "Submission")
event = Event.objects.get(path=EVENT_PATH)
SteamCollection.objects.filter(event__isnull=True).update(event=event)
Submission.objects.filter(event__isnull=True).update(event=event)
def noop(apps, schema_editor):
pass
class Migration(migrations.Migration):
dependencies = [
("opus_magnum", "0016_steamcollection_event_submission_event"),
("events", "0002_seed_polylan_43"),
]
operations = [
migrations.RunPython(backfill_event, noop),
]
@@ -0,0 +1,29 @@
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("opus_magnum", "0017_backfill_event"),
]
operations = [
migrations.AlterField(
model_name="steamcollection",
name="event",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="steam_collections",
to="events.event",
),
),
migrations.AlterField(
model_name="submission",
name="event",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="opus_magnum_submissions",
to="events.event",
),
),
]
+21
View File
@@ -8,6 +8,9 @@ from django.db.models.expressions import Window
from django.db.models.functions import Cast, Rank, RowNumber from django.db.models.functions import Cast, Rank, RowNumber
from django.db.models.query import F from django.db.models.query import F
from events.models import Event
from events.scoping import EventScopedManager
User = get_user_model() User = get_user_model()
@@ -108,6 +111,12 @@ class SteamAPIKey(models.Model):
class SteamCollection(models.Model): class SteamCollection(models.Model):
"""Model representing a Steam Workshop collection""" """Model representing a Steam Workshop collection"""
event = models.ForeignKey(
Event,
on_delete=models.CASCADE,
related_name="steam_collections",
)
# Basic collection info # Basic collection info
steam_id = models.CharField( steam_id = models.CharField(
max_length=50, unique=True, help_text="Steam collection ID from URL" max_length=50, unique=True, help_text="Steam collection ID from URL"
@@ -164,6 +173,9 @@ class SteamCollection(models.Model):
blank=True, help_text="Last error encountered when fetching data" blank=True, help_text="Last error encountered when fetching data"
) )
objects = models.Manager()
for_event = EventScopedManager()
class Meta: class Meta:
ordering = ["-created_at"] ordering = ["-created_at"]
verbose_name = "Steam Collection" verbose_name = "Steam Collection"
@@ -257,6 +269,12 @@ class Submission(models.Model):
# Identification # Identification
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
event = models.ForeignKey(
Event,
on_delete=models.CASCADE,
related_name="opus_magnum_submissions",
)
# User information (optional for anonymous submissions) # User information (optional for anonymous submissions)
user = models.ForeignKey( user = models.ForeignKey(
User, User,
@@ -299,6 +317,9 @@ class Submission(models.Model):
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True) updated_at = models.DateTimeField(auto_now=True)
objects = models.Manager()
for_event = EventScopedManager()
class Meta: class Meta:
ordering = ["-created_at"] ordering = ["-created_at"]
verbose_name = "Submission" verbose_name = "Submission"
+20 -9
View File
@@ -6,7 +6,7 @@ from opus_magnum.api import router as opus_magnum_router
from opus_magnum.schemas import UserInfoOut from opus_magnum.schemas import UserInfoOut
from animations.api import router as results_router from animations.api import router as results_router
from noita.api import router as noita_router from noita.api import router as noita_router
from games.api import router as games_router from games.api import router as games_router, event_router as games_event_router
from market.api import router as market_router from market.api import router as market_router
from submissions.api import router as submissions_router from submissions.api import router as submissions_router
@@ -35,14 +35,24 @@ The Noita Submission API allows clients to upload the result of the log file of
# api.auth = django_auth # Uncomment if you want global auth # api.auth = django_auth # Uncomment if you want global auth
# Include the routers # Include the routers
api.add_router("/opus-magnum/", opus_magnum_router, tags=["opus-magnum"])
api.add_router("/results/", results_router, tags=["results"])
api.add_router("/noita/", noita_router, tags=["noita"])
api.add_router("/games/", games_router, tags=["games"]) api.add_router("/games/", games_router, tags=["games"])
api.add_router("/submissions/", submissions_router, tags=["submissions"])
# Event-scoped routers: all tournament/competition activity belongs to one event.
# Each operation's own path already starts with "/events/{event_path}/<resource>/..."
# (see the PREFIX constant in each app's api.py) - django-ninja resolves a path
# parameter's source (path vs query) from the operation's own registered path
# string at decoration time, not from whatever prefix add_router() attaches later,
# so {event_path} has to live there to show up as a real templated path parameter
# in the generated OpenAPI schema (which the frontend's generated API client needs).
# These routers are therefore mounted at the root, with no extra prefix here.
api.add_router("", opus_magnum_router, tags=["opus-magnum"])
api.add_router("", results_router, tags=["results"])
api.add_router("", noita_router, tags=["noita"])
api.add_router("", games_event_router, tags=["games"])
api.add_router("", submissions_router, tags=["submissions"])
if settings.MARKET_ENABLED: if settings.MARKET_ENABLED:
api.add_router("/market/", market_router) api.add_router("", market_router, tags=["market"])
# Health check endpoint # Health check endpoint
@@ -54,12 +64,13 @@ def health_check(request):
# Cache management endpoint # Cache management endpoint
@api.post("/cache/clear") @api.post("/cache/clear")
def clear_cache(request: HttpRequest): def clear_cache(request: HttpRequest, event_path: str | None = None):
"""Clear all API caches (admin only)""" """Clear API caches (admin only). Pass event_path to clear just that event's keys."""
if not request.user.is_authenticated or not request.user.is_staff: if not request.user.is_authenticated or not request.user.is_staff:
return 403, {"detail": "Admin access required"} return 403, {"detail": "Admin access required"}
keys = cache.keys("api:*") pattern = f"api:{event_path}:*" if event_path else "api:*"
keys = cache.keys(pattern)
cache.delete_many(keys) cache.delete_many(keys)
return {"detail": f"Cleared {len(keys)} cache entries"} return {"detail": f"Cleared {len(keys)} cache entries"}
@@ -46,6 +46,7 @@ INSTALLED_APPS = [
"django_rq", "django_rq",
"django_tasks_rq", "django_tasks_rq",
"accounts", "accounts",
"events",
"animations", "animations",
"opus_magnum", "opus_magnum",
"noita", "noita",
@@ -62,6 +63,7 @@ MIDDLEWARE = [
"django.middleware.csrf.CsrfViewMiddleware", "django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware",
# Removed CASMiddleware to avoid conflicts with our custom backend # Removed CASMiddleware to avoid conflicts with our custom backend
"events.middleware.EventContextMiddleware",
"django.contrib.messages.middleware.MessageMiddleware", "django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware",
] ]
@@ -240,6 +242,7 @@ STORAGES = {
} }
SENTRY_ENVIRONMENT = "production"
from polylan_submitter.settingsLocal import * # noqa from polylan_submitter.settingsLocal import * # noqa
@@ -250,4 +253,5 @@ sentry_sdk.init(
# Set traces_sample_rate to 1.0 to capture 100% # Set traces_sample_rate to 1.0 to capture 100%
# of transactions for tracing. # of transactions for tracing.
traces_sample_rate=1.0, traces_sample_rate=1.0,
environment=SENTRY_ENVIRONMENT,
) )
+32 -11
View File
@@ -16,13 +16,14 @@ Including another URLconf
""" """
from django.contrib import admin from django.contrib import admin
from django.http import HttpRequest from django.http import Http404, HttpRequest
from django.shortcuts import render from django.shortcuts import get_object_or_404, redirect, render
from django.urls import path from django.urls import path
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.conf import settings from django.conf import settings
from django.conf.urls.static import static from django.conf.urls.static import static
from simple_cas_views import SimpleCASLoginView, SimpleCASLogoutView from simple_cas_views import SimpleCASLoginView, SimpleCASLogoutView
from events.models import Event
from games.decorators import require_game_enabled from games.decorators import require_game_enabled
from market.views import market_home from market.views import market_home
from submissions.views import submission_home from submissions.views import submission_home
@@ -34,12 +35,21 @@ OPUS_MAGNUM_APP_ID = 558990
@login_required @login_required
def home(request: HttpRequest): def home(request: HttpRequest):
from django.conf import settings event = Event.objects.order_by("-created_at").first()
if event is None:
raise Http404("No event configured")
return redirect("event.home", event_path=event.path)
@login_required
def event_home(request: HttpRequest, event_path: str):
event = get_object_or_404(Event, path=event_path)
return render( return render(
request, request,
"home.html", "home.html",
{ {
"event": event,
"market_enabled": settings.MARKET_ENABLED, "market_enabled": settings.MARKET_ENABLED,
}, },
) )
@@ -47,22 +57,28 @@ def home(request: HttpRequest):
@login_required @login_required
@require_game_enabled(OPUS_MAGNUM_APP_ID) @require_game_enabled(OPUS_MAGNUM_APP_ID)
def opus_magnum_home(request: HttpRequest): def opus_magnum_home(request: HttpRequest, event_path: str):
from opus_magnum.models import SteamCollection from opus_magnum.models import SteamCollection
event = get_object_or_404(Event, path=event_path)
return render( return render(
request, request,
"opus-magnum.html", "opus-magnum.html",
{ {
"collection": SteamCollection.objects.filter(is_active=True).last(), "event": event,
"collection": SteamCollection.objects.filter(
is_active=True, event=event
).last(),
}, },
) )
@login_required @login_required
@require_game_enabled(NOITA_APP_ID) @require_game_enabled(NOITA_APP_ID)
def noita_home(request: HttpRequest): def noita_home(request: HttpRequest, event_path: str):
return render(request, "noita.html", {}) event = get_object_or_404(Event, path=event_path)
return render(request, "noita.html", {"event": event})
urlpatterns = [ urlpatterns = [
@@ -70,10 +86,15 @@ urlpatterns = [
path("cas/login/", SimpleCASLoginView.as_view(), name="cas_ng_login"), path("cas/login/", SimpleCASLoginView.as_view(), name="cas_ng_login"),
path("cas/logout/", SimpleCASLogoutView.as_view(), name="cas_ng_logout"), path("cas/logout/", SimpleCASLogoutView.as_view(), name="cas_ng_logout"),
path("api/", api.urls), path("api/", api.urls),
path("market", market_home, name="market.home"), path("<slug:event_path>/market", market_home, name="market.home"),
path("opus-magnum", opus_magnum_home, name="opus-magnum.home"), path("<slug:event_path>/opus-magnum", opus_magnum_home, name="opus-magnum.home"),
path("noita", noita_home, name="noita.home"), path("<slug:event_path>/noita", noita_home, name="noita.home"),
path("submissions/<str:game>", submission_home, name="submissions.home"), path(
"<slug:event_path>/submissions/<str:game>",
submission_home,
name="submissions.home",
),
path("<slug:event_path>/", event_home, name="event.home"),
path("", home, name="home"), path("", home, name="home"),
] ]
+10 -7
View File
@@ -1,9 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from "vue"; import { ref, onMounted } from "vue";
import { gamesApiListGames } from "./api"; import { gamesApiListEventGames } from "./api";
import type { GamesApiListGamesResponse } from "./api/types.gen"; import type { GamesApiListEventGamesResponse } from "./api/types.gen";
interface Props { interface Props {
eventPath: string;
marketEnabled?: string | boolean; marketEnabled?: string | boolean;
} }
@@ -11,11 +12,11 @@ const props = withDefaults(defineProps<Props>(), {
marketEnabled: true, marketEnabled: true,
}); });
const games = ref<GamesApiListGamesResponse | undefined>(); const games = ref<GamesApiListEventGamesResponse | undefined>();
const loading = ref(true); const loading = ref(true);
const imageErrors = ref<Set<number>>(new Set()); const imageErrors = ref<Set<number>>(new Set());
const getHeaderImage = (game: GamesApiListGamesResponse[number]) => { const getHeaderImage = (game: GamesApiListEventGamesResponse[number]) => {
if (game.image) { if (game.image) {
return game.image; return game.image;
} }
@@ -30,7 +31,7 @@ const onImageError = (gameId: number) => {
}; };
const navigate = (path: string) => { const navigate = (path: string) => {
window.location.href = path; window.location.href = `/${props.eventPath}${path}`;
}; };
const isMarketEnabled = () => { const isMarketEnabled = () => {
@@ -41,8 +42,10 @@ const isMarketEnabled = () => {
}; };
onMounted(async () => { onMounted(async () => {
// Fetch games list // Fetch games enabled for this event
const response = await gamesApiListGames(); const response = await gamesApiListEventGames({
path: { event_path: props.eventPath },
});
if (response.data) { if (response.data) {
games.value = response.data; games.value = response.data;
} }
+11 -3
View File
@@ -4,20 +4,28 @@ import { storeToRefs } from "pinia";
import { useMarketStore } from "./stores/market"; import { useMarketStore } from "./stores/market";
import MarketCard from "./components/MarketCard.vue"; import MarketCard from "./components/MarketCard.vue";
import UserBets from "./components/UserBets.vue"; import UserBets from "./components/UserBets.vue";
import { provideEventPath } from "./composables/useEventPath";
interface Props {
eventPath: string;
}
const props = defineProps<Props>();
provideEventPath(props.eventPath);
const marketStore = useMarketStore(); const marketStore = useMarketStore();
const { markets, userInfo, isLoading } = storeToRefs(marketStore); const { markets, userInfo, isLoading } = storeToRefs(marketStore);
const goHome = () => { const goHome = () => {
window.location.href = "/"; window.location.href = `/${props.eventPath}/`;
}; };
const reloadPage = async () => { const reloadPage = async () => {
await marketStore.refreshPage(); await marketStore.refreshPage(props.eventPath);
}; };
onMounted(() => { onMounted(() => {
marketStore.initializeMarketPage(); marketStore.initializeMarketPage(props.eventPath);
}); });
</script> </script>
+11 -4
View File
@@ -8,19 +8,24 @@ import Winners from "@/components/Winners.vue";
import PuzzleResults from "@/components/PuzzleResults.vue"; import PuzzleResults from "@/components/PuzzleResults.vue";
import TopUsersLeaderboard from "@/components/TopUsersLeaderboard.vue"; import TopUsersLeaderboard from "@/components/TopUsersLeaderboard.vue";
import { polylanSubmitterApiGetUserInfo, opusMagnumApiGetCollection } from "@/api"; import { polylanSubmitterApiGetUserInfo, opusMagnumApiGetCollection } from "@/api";
import { errorHelpers } from "@/services/apiService"; import { apiService, errorHelpers } from "@/services/apiService";
import { usePuzzlesStore } from "@/stores/puzzles"; import { usePuzzlesStore } from "@/stores/puzzles";
import { useSubmissionsStore } from "@/stores/submissions"; import { useSubmissionsStore } from "@/stores/submissions";
import { provideEventPath } from "@/composables/useEventPath";
import type { PuzzleResponse, UserInfo, SteamCollection } from "@/types"; import type { PuzzleResponse, UserInfo, SteamCollection } from "@/types";
import { useCountdown } from "@vueuse/core"; import { useCountdown } from "@vueuse/core";
import { storeToRefs } from "pinia"; import { storeToRefs } from "pinia";
const props = defineProps<{ const props = defineProps<{
eventPath: string;
collectionTitle: string; collectionTitle: string;
collectionUrl: string; collectionUrl: string;
collectionDescription: string; collectionDescription: string;
}>(); }>();
provideEventPath(props.eventPath);
apiService.setEventPath(props.eventPath);
const puzzlesStore = usePuzzlesStore(); const puzzlesStore = usePuzzlesStore();
const submissionsStore = useSubmissionsStore(); const submissionsStore = useSubmissionsStore();
@@ -77,7 +82,9 @@ async function initialize() {
// Load collection data // Load collection data
console.log("Loading collection..."); console.log("Loading collection...");
const collectionResponse = await opusMagnumApiGetCollection(); const collectionResponse = await opusMagnumApiGetCollection({
path: { event_path: props.eventPath },
});
if (collectionResponse.data) { if (collectionResponse.data) {
collection.value = collectionResponse.data as SteamCollection; collection.value = collectionResponse.data as SteamCollection;
console.log("Collection loaded:", collectionResponse.data); console.log("Collection loaded:", collectionResponse.data);
@@ -87,7 +94,7 @@ async function initialize() {
// Load puzzles from API using store // Load puzzles from API using store
console.log("Loading puzzles..."); console.log("Loading puzzles...");
await puzzlesStore.loadPuzzles(); await puzzlesStore.loadPuzzles(props.eventPath);
console.log("Puzzles loaded:", puzzlesStore.puzzles.length); console.log("Puzzles loaded:", puzzlesStore.puzzles.length);
// Load existing submissions using store // Load existing submissions using store
@@ -129,7 +136,7 @@ const reloadPage = () => {
}; };
const goHome = () => { const goHome = () => {
window.location.href = "/"; window.location.href = `/${props.eventPath}/`;
}; };
</script> </script>
+11 -5
View File
@@ -4,8 +4,10 @@ import { storeToRefs } from "pinia";
import { useUserStore } from "@/stores/user"; import { useUserStore } from "@/stores/user";
import { submissionsApiCreateSubmission, submissionsApiListUserSubmissions, submissionsApiCreateImageCrop, submissionsApiDeleteImageCrop, submissionsApiProcessFileOcr, submissionsApiProcessCropOcr } from "@/api"; import { submissionsApiCreateSubmission, submissionsApiListUserSubmissions, submissionsApiCreateImageCrop, submissionsApiDeleteImageCrop, submissionsApiProcessFileOcr, submissionsApiProcessCropOcr } from "@/api";
import { useSubmissionsWebSocket } from "@/composables/useSubmissionsWebSocket"; import { useSubmissionsWebSocket } from "@/composables/useSubmissionsWebSocket";
import { provideEventPath } from "@/composables/useEventPath";
interface Props { interface Props {
eventPath: string;
gamePath?: string; gamePath?: string;
gameName?: string; gameName?: string;
gameImage?: string; gameImage?: string;
@@ -19,6 +21,8 @@ const props = withDefaults(defineProps<Props>(), {
gameSteamId: "", gameSteamId: "",
}); });
provideEventPath(props.eventPath);
const userStore = useUserStore(); const userStore = useUserStore();
const { userInfo } = storeToRefs(userStore); const { userInfo } = storeToRefs(userStore);
@@ -238,7 +242,7 @@ const submitCrop = async () => {
isCreatingCrop.value = true; isCreatingCrop.value = true;
try { try {
const { data: newCrop } = await submissionsApiCreateImageCrop({ const { data: newCrop } = await submissionsApiCreateImageCrop({
path: { submission_file_id: selectedSubmissionFile.value.id }, path: { event_path: props.eventPath, submission_file_id: selectedSubmissionFile.value.id },
body: { body: {
x1: Math.round(currentCrop.value.x1), x1: Math.round(currentCrop.value.x1),
y1: Math.round(currentCrop.value.y1), y1: Math.round(currentCrop.value.y1),
@@ -270,7 +274,7 @@ const deleteCrop = async (cropId: string) => {
try { try {
await submissionsApiDeleteImageCrop({ await submissionsApiDeleteImageCrop({
path: { crop_id: cropId }, path: { event_path: props.eventPath, crop_id: cropId },
}); });
selectedSubmissionFile.value.crops = selectedSubmissionFile.value.crops.filter( selectedSubmissionFile.value.crops = selectedSubmissionFile.value.crops.filter(
(c: any) => c.id !== cropId (c: any) => c.id !== cropId
@@ -293,7 +297,7 @@ const processFileOcr = async () => {
try { try {
await submissionsApiProcessFileOcr({ await submissionsApiProcessFileOcr({
path: { file_id: selectedSubmissionFile.value.id }, path: { event_path: props.eventPath, file_id: selectedSubmissionFile.value.id },
}); });
selectedSubmissionFile.value.ocr_status = "processing"; selectedSubmissionFile.value.ocr_status = "processing";
} catch (error) { } catch (error) {
@@ -305,7 +309,7 @@ const processFileOcr = async () => {
const processCropOcr = async (cropId: string) => { const processCropOcr = async (cropId: string) => {
try { try {
await submissionsApiProcessCropOcr({ await submissionsApiProcessCropOcr({
path: { crop_id: cropId }, path: { event_path: props.eventPath, crop_id: cropId },
}); });
// Update the crop status in the list // Update the crop status in the list
if (selectedSubmissionFile.value?.crops) { if (selectedSubmissionFile.value?.crops) {
@@ -398,6 +402,7 @@ const submitSubmission = async () => {
isUploading.value = true; isUploading.value = true;
try { try {
await submissionsApiCreateSubmission({ await submissionsApiCreateSubmission({
path: { event_path: props.eventPath },
query: { game_path: props.gamePath }, query: { game_path: props.gamePath },
body: { body: {
files: uploadedFiles.value, files: uploadedFiles.value,
@@ -418,7 +423,7 @@ const submitSubmission = async () => {
}; };
const goHome = () => { const goHome = () => {
window.location.href = "/"; window.location.href = `/${props.eventPath}/`;
}; };
const formatFileSize = (bytes: number) => { const formatFileSize = (bytes: number) => {
@@ -454,6 +459,7 @@ const loadUserSubmissions = async () => {
isLoadingSubmissions.value = true; isLoadingSubmissions.value = true;
try { try {
const { data } = await submissionsApiListUserSubmissions({ const { data } = await submissionsApiListUserSubmissions({
path: { event_path: props.eventPath },
query: { game_path: props.gamePath }, query: { game_path: props.gamePath },
}); });
if (data && Array.isArray(data)) { if (data && Array.isArray(data)) {
+2 -2
View File
@@ -1,4 +1,4 @@
// This file is auto-generated by @hey-api/openapi-ts // This file is auto-generated by @hey-api/openapi-ts
export { animationsApiPuzzleResults, animationsApiResults, animationsApiTopSubmissions, gamesApiListGames, marketApiCloseMarket, marketApiCreateBet, marketApiListMarkets, marketApiListUserBets, marketApiResolveMarket, noitaApiGetLeaderboard, noitaApiGetResults, noitaApiSubmitLogFile, type Options, opusMagnumApiCreateSubmission, opusMagnumApiDeleteSubmission, opusMagnumApiGetCollection, opusMagnumApiGetStats, opusMagnumApiGetSubmission, opusMagnumApiListPuzzles, opusMagnumApiListResponsesNeedingValidation, opusMagnumApiListSubmissions, opusMagnumApiValidateAuto, opusMagnumApiValidateResponse, opusMagnumApiValidateSubmission, polylanSubmitterApiClearCache, polylanSubmitterApiGetUserInfo, polylanSubmitterApiHealthCheck, submissionsApiCreateImageCrop, submissionsApiCreateSubmission, submissionsApiDeleteImageCrop, submissionsApiGetSubmission, submissionsApiListSubmissionsByGame, submissionsApiListUserSubmissions, submissionsApiProcessCropOcr, submissionsApiProcessFileOcr } from './sdk.gen'; export { animationsApiPuzzleResults, animationsApiResults, animationsApiTopSubmissions, gamesApiListEventGames, gamesApiListGames, marketApiCloseMarket, marketApiCreateBet, marketApiListMarkets, marketApiListUserBets, marketApiResolveMarket, noitaApiGetLeaderboard, noitaApiGetResults, noitaApiSubmitLogFile, type Options, opusMagnumApiCreateSubmission, opusMagnumApiDeleteSubmission, opusMagnumApiGetCollection, opusMagnumApiGetStats, opusMagnumApiGetSubmission, opusMagnumApiListPuzzles, opusMagnumApiListResponsesNeedingValidation, opusMagnumApiListSubmissions, opusMagnumApiValidateAuto, opusMagnumApiValidateResponse, opusMagnumApiValidateSubmission, polylanSubmitterApiClearCache, polylanSubmitterApiGetUserInfo, polylanSubmitterApiHealthCheck, submissionsApiCreateImageCrop, submissionsApiCreateSubmission, submissionsApiDeleteImageCrop, submissionsApiGetSubmission, submissionsApiListSubmissionsByGame, submissionsApiListUserSubmissions, submissionsApiProcessCropOcr, submissionsApiProcessFileOcr } from './sdk.gen';
export type { AnimationsApiPuzzleResultsData, AnimationsApiPuzzleResultsResponse, AnimationsApiPuzzleResultsResponses, AnimationsApiResultsData, AnimationsApiResultsResponse, AnimationsApiResultsResponses, AnimationsApiTopSubmissionsData, AnimationsApiTopSubmissionsResponse, AnimationsApiTopSubmissionsResponses, ClientOptions, GameOut, GamesApiListGamesData, GamesApiListGamesResponse, GamesApiListGamesResponses, ImageCropIn, ImageCropOut, Input, LeaderboardEntryOut, LeaderboardOut, MarketApiCloseMarketData, MarketApiCloseMarketResponses, MarketApiCreateBetData, MarketApiCreateBetResponse, MarketApiCreateBetResponses, MarketApiListMarketsData, MarketApiListMarketsResponse, MarketApiListMarketsResponses, MarketApiListUserBetsData, MarketApiListUserBetsResponse, MarketApiListUserBetsResponses, MarketApiResolveMarketData, MarketApiResolveMarketResponse, MarketApiResolveMarketResponses, MarketListSchema, MarketOptionSchema, NoitaApiGetLeaderboardData, NoitaApiGetLeaderboardResponse, NoitaApiGetLeaderboardResponses, NoitaApiGetResultsData, NoitaApiGetResultsResponse, NoitaApiGetResultsResponses, NoitaApiSubmitLogFileData, NoitaApiSubmitLogFileError, NoitaApiSubmitLogFileErrors, NoitaApiSubmitLogFileResponse, NoitaApiSubmitLogFileResponses, NoitaSubmissionOut, ObjectivResultOut, OpusMagnumApiCreateSubmissionData, OpusMagnumApiCreateSubmissionResponse, OpusMagnumApiCreateSubmissionResponses, OpusMagnumApiDeleteSubmissionData, OpusMagnumApiDeleteSubmissionResponses, OpusMagnumApiGetCollectionData, OpusMagnumApiGetCollectionResponse, OpusMagnumApiGetCollectionResponses, OpusMagnumApiGetStatsData, OpusMagnumApiGetStatsResponses, OpusMagnumApiGetSubmissionData, OpusMagnumApiGetSubmissionResponse, OpusMagnumApiGetSubmissionResponses, OpusMagnumApiListPuzzlesData, OpusMagnumApiListPuzzlesResponse, OpusMagnumApiListPuzzlesResponses, OpusMagnumApiListResponsesNeedingValidationData, OpusMagnumApiListResponsesNeedingValidationResponse, OpusMagnumApiListResponsesNeedingValidationResponses, OpusMagnumApiListSubmissionsData, OpusMagnumApiListSubmissionsResponse, OpusMagnumApiListSubmissionsResponses, OpusMagnumApiValidateAutoData, OpusMagnumApiValidateAutoResponse, OpusMagnumApiValidateAutoResponses, OpusMagnumApiValidateResponseData, OpusMagnumApiValidateResponseResponse, OpusMagnumApiValidateResponseResponses, OpusMagnumApiValidateSubmissionData, OpusMagnumApiValidateSubmissionResponse, OpusMagnumApiValidateSubmissionResponses, PagedSubmissionOut, PolylanSubmitterApiClearCacheData, PolylanSubmitterApiClearCacheResponses, PolylanSubmitterApiGetUserInfoData, PolylanSubmitterApiGetUserInfoResponse, PolylanSubmitterApiGetUserInfoResponses, PolylanSubmitterApiHealthCheckData, PolylanSubmitterApiHealthCheckResponses, PuzzlePointsFactorOut, PuzzleResponseIn, PuzzleResponseOut, PuzzleResponseRankingOut, PuzzleResultsOut, PuzzleSubmissionsOut, PuzzleSubmissionWithRankOut, RankingSchema, ResolveMarketSchema, ResultsOut, SteamCollectionItemOut, SteamCollectionOut, SubmissionFileOut, SubmissionIn, SubmissionOut, SubmissionsApiCreateImageCropData, SubmissionsApiCreateImageCropResponse, SubmissionsApiCreateImageCropResponses, SubmissionsApiCreateSubmissionData, SubmissionsApiCreateSubmissionResponse, SubmissionsApiCreateSubmissionResponses, SubmissionsApiDeleteImageCropData, SubmissionsApiDeleteImageCropResponse, SubmissionsApiDeleteImageCropResponses, SubmissionsApiGetSubmissionData, SubmissionsApiGetSubmissionResponse, SubmissionsApiGetSubmissionResponses, SubmissionsApiListSubmissionsByGameData, SubmissionsApiListSubmissionsByGameResponse, SubmissionsApiListSubmissionsByGameResponses, SubmissionsApiListUserSubmissionsData, SubmissionsApiListUserSubmissionsResponse, SubmissionsApiListUserSubmissionsResponses, SubmissionsApiProcessCropOcrData, SubmissionsApiProcessCropOcrResponse, SubmissionsApiProcessCropOcrResponses, SubmissionsApiProcessFileOcrData, SubmissionsApiProcessFileOcrResponse, SubmissionsApiProcessFileOcrResponses, TournamentPuzzleResultsOut, TournamentSubmissionsOut, UserBetCreateSchema, UserBetSchema, UserDisplayOut, UserInfoOut, ValidationIn, WinnerFileOut, WinnerResponseOut } from './types.gen'; export type { AnimationsApiPuzzleResultsData, AnimationsApiPuzzleResultsResponse, AnimationsApiPuzzleResultsResponses, AnimationsApiResultsData, AnimationsApiResultsResponse, AnimationsApiResultsResponses, AnimationsApiTopSubmissionsData, AnimationsApiTopSubmissionsResponse, AnimationsApiTopSubmissionsResponses, ClientOptions, GameOut, GamesApiListEventGamesData, GamesApiListEventGamesResponse, GamesApiListEventGamesResponses, GamesApiListGamesData, GamesApiListGamesResponse, GamesApiListGamesResponses, ImageCropIn, ImageCropOut, Input, LeaderboardEntryOut, LeaderboardOut, MarketApiCloseMarketData, MarketApiCloseMarketResponses, MarketApiCreateBetData, MarketApiCreateBetResponse, MarketApiCreateBetResponses, MarketApiListMarketsData, MarketApiListMarketsResponse, MarketApiListMarketsResponses, MarketApiListUserBetsData, MarketApiListUserBetsResponse, MarketApiListUserBetsResponses, MarketApiResolveMarketData, MarketApiResolveMarketResponse, MarketApiResolveMarketResponses, MarketListSchema, MarketOptionSchema, NoitaApiGetLeaderboardData, NoitaApiGetLeaderboardResponse, NoitaApiGetLeaderboardResponses, NoitaApiGetResultsData, NoitaApiGetResultsResponse, NoitaApiGetResultsResponses, NoitaApiSubmitLogFileData, NoitaApiSubmitLogFileError, NoitaApiSubmitLogFileErrors, NoitaApiSubmitLogFileResponse, NoitaApiSubmitLogFileResponses, NoitaSubmissionOut, ObjectivResultOut, OpusMagnumApiCreateSubmissionData, OpusMagnumApiCreateSubmissionResponse, OpusMagnumApiCreateSubmissionResponses, OpusMagnumApiDeleteSubmissionData, OpusMagnumApiDeleteSubmissionResponses, OpusMagnumApiGetCollectionData, OpusMagnumApiGetCollectionResponse, OpusMagnumApiGetCollectionResponses, OpusMagnumApiGetStatsData, OpusMagnumApiGetStatsResponses, OpusMagnumApiGetSubmissionData, OpusMagnumApiGetSubmissionResponse, OpusMagnumApiGetSubmissionResponses, OpusMagnumApiListPuzzlesData, OpusMagnumApiListPuzzlesResponse, OpusMagnumApiListPuzzlesResponses, OpusMagnumApiListResponsesNeedingValidationData, OpusMagnumApiListResponsesNeedingValidationResponse, OpusMagnumApiListResponsesNeedingValidationResponses, OpusMagnumApiListSubmissionsData, OpusMagnumApiListSubmissionsResponse, OpusMagnumApiListSubmissionsResponses, OpusMagnumApiValidateAutoData, OpusMagnumApiValidateAutoResponse, OpusMagnumApiValidateAutoResponses, OpusMagnumApiValidateResponseData, OpusMagnumApiValidateResponseResponse, OpusMagnumApiValidateResponseResponses, OpusMagnumApiValidateSubmissionData, OpusMagnumApiValidateSubmissionResponse, OpusMagnumApiValidateSubmissionResponses, PagedSubmissionOut, PolylanSubmitterApiClearCacheData, PolylanSubmitterApiClearCacheResponses, PolylanSubmitterApiGetUserInfoData, PolylanSubmitterApiGetUserInfoResponse, PolylanSubmitterApiGetUserInfoResponses, PolylanSubmitterApiHealthCheckData, PolylanSubmitterApiHealthCheckResponses, PuzzlePointsFactorOut, PuzzleResponseIn, PuzzleResponseOut, PuzzleResponseRankingOut, PuzzleResultsOut, PuzzleSubmissionsOut, PuzzleSubmissionWithRankOut, RankingSchema, ResolveMarketSchema, ResultsOut, SteamCollectionItemOut, SteamCollectionOut, SubmissionFileOut, SubmissionIn, SubmissionOut, SubmissionsApiCreateImageCropData, SubmissionsApiCreateImageCropResponse, SubmissionsApiCreateImageCropResponses, SubmissionsApiCreateSubmissionData, SubmissionsApiCreateSubmissionResponse, SubmissionsApiCreateSubmissionResponses, SubmissionsApiDeleteImageCropData, SubmissionsApiDeleteImageCropResponse, SubmissionsApiDeleteImageCropResponses, SubmissionsApiGetSubmissionData, SubmissionsApiGetSubmissionResponse, SubmissionsApiGetSubmissionResponses, SubmissionsApiListSubmissionsByGameData, SubmissionsApiListSubmissionsByGameResponse, SubmissionsApiListSubmissionsByGameResponses, SubmissionsApiListUserSubmissionsData, SubmissionsApiListUserSubmissionsResponse, SubmissionsApiListUserSubmissionsResponses, SubmissionsApiProcessCropOcrData, SubmissionsApiProcessCropOcrResponse, SubmissionsApiProcessCropOcrResponses, SubmissionsApiProcessFileOcrData, SubmissionsApiProcessFileOcrResponse, SubmissionsApiProcessFileOcrResponses, TournamentPuzzleResultsOut, TournamentSubmissionsOut, UserBetCreateSchema, UserBetSchema, UserDisplayOut, UserInfoOut, ValidationIn, WinnerFileOut, WinnerResponseOut } from './types.gen';
+39 -34
View File
@@ -2,7 +2,7 @@
import { type Client, formDataBodySerializer, type Options as Options2, type TDataShape } from './client'; import { type Client, formDataBodySerializer, type Options as Options2, type TDataShape } from './client';
import { client } from './client.gen'; import { client } from './client.gen';
import type { AnimationsApiPuzzleResultsData, AnimationsApiPuzzleResultsResponses, AnimationsApiResultsData, AnimationsApiResultsResponses, AnimationsApiTopSubmissionsData, AnimationsApiTopSubmissionsResponses, GamesApiListGamesData, GamesApiListGamesResponses, MarketApiCloseMarketData, MarketApiCloseMarketResponses, MarketApiCreateBetData, MarketApiCreateBetResponses, MarketApiListMarketsData, MarketApiListMarketsResponses, MarketApiListUserBetsData, MarketApiListUserBetsResponses, MarketApiResolveMarketData, MarketApiResolveMarketResponses, NoitaApiGetLeaderboardData, NoitaApiGetLeaderboardResponses, NoitaApiGetResultsData, NoitaApiGetResultsResponses, NoitaApiSubmitLogFileData, NoitaApiSubmitLogFileErrors, NoitaApiSubmitLogFileResponses, OpusMagnumApiCreateSubmissionData, OpusMagnumApiCreateSubmissionResponses, OpusMagnumApiDeleteSubmissionData, OpusMagnumApiDeleteSubmissionResponses, OpusMagnumApiGetCollectionData, OpusMagnumApiGetCollectionResponses, OpusMagnumApiGetStatsData, OpusMagnumApiGetStatsResponses, OpusMagnumApiGetSubmissionData, OpusMagnumApiGetSubmissionResponses, OpusMagnumApiListPuzzlesData, OpusMagnumApiListPuzzlesResponses, OpusMagnumApiListResponsesNeedingValidationData, OpusMagnumApiListResponsesNeedingValidationResponses, OpusMagnumApiListSubmissionsData, OpusMagnumApiListSubmissionsResponses, OpusMagnumApiValidateAutoData, OpusMagnumApiValidateAutoResponses, OpusMagnumApiValidateResponseData, OpusMagnumApiValidateResponseResponses, OpusMagnumApiValidateSubmissionData, OpusMagnumApiValidateSubmissionResponses, PolylanSubmitterApiClearCacheData, PolylanSubmitterApiClearCacheResponses, PolylanSubmitterApiGetUserInfoData, PolylanSubmitterApiGetUserInfoResponses, PolylanSubmitterApiHealthCheckData, PolylanSubmitterApiHealthCheckResponses, SubmissionsApiCreateImageCropData, SubmissionsApiCreateImageCropResponses, SubmissionsApiCreateSubmissionData, SubmissionsApiCreateSubmissionResponses, SubmissionsApiDeleteImageCropData, SubmissionsApiDeleteImageCropResponses, SubmissionsApiGetSubmissionData, SubmissionsApiGetSubmissionResponses, SubmissionsApiListSubmissionsByGameData, SubmissionsApiListSubmissionsByGameResponses, SubmissionsApiListUserSubmissionsData, SubmissionsApiListUserSubmissionsResponses, SubmissionsApiProcessCropOcrData, SubmissionsApiProcessCropOcrResponses, SubmissionsApiProcessFileOcrData, SubmissionsApiProcessFileOcrResponses } from './types.gen'; import type { AnimationsApiPuzzleResultsData, AnimationsApiPuzzleResultsResponses, AnimationsApiResultsData, AnimationsApiResultsResponses, AnimationsApiTopSubmissionsData, AnimationsApiTopSubmissionsResponses, GamesApiListEventGamesData, GamesApiListEventGamesResponses, GamesApiListGamesData, GamesApiListGamesResponses, MarketApiCloseMarketData, MarketApiCloseMarketResponses, MarketApiCreateBetData, MarketApiCreateBetResponses, MarketApiListMarketsData, MarketApiListMarketsResponses, MarketApiListUserBetsData, MarketApiListUserBetsResponses, MarketApiResolveMarketData, MarketApiResolveMarketResponses, NoitaApiGetLeaderboardData, NoitaApiGetLeaderboardResponses, NoitaApiGetResultsData, NoitaApiGetResultsResponses, NoitaApiSubmitLogFileData, NoitaApiSubmitLogFileErrors, NoitaApiSubmitLogFileResponses, OpusMagnumApiCreateSubmissionData, OpusMagnumApiCreateSubmissionResponses, OpusMagnumApiDeleteSubmissionData, OpusMagnumApiDeleteSubmissionResponses, OpusMagnumApiGetCollectionData, OpusMagnumApiGetCollectionResponses, OpusMagnumApiGetStatsData, OpusMagnumApiGetStatsResponses, OpusMagnumApiGetSubmissionData, OpusMagnumApiGetSubmissionResponses, OpusMagnumApiListPuzzlesData, OpusMagnumApiListPuzzlesResponses, OpusMagnumApiListResponsesNeedingValidationData, OpusMagnumApiListResponsesNeedingValidationResponses, OpusMagnumApiListSubmissionsData, OpusMagnumApiListSubmissionsResponses, OpusMagnumApiValidateAutoData, OpusMagnumApiValidateAutoResponses, OpusMagnumApiValidateResponseData, OpusMagnumApiValidateResponseResponses, OpusMagnumApiValidateSubmissionData, OpusMagnumApiValidateSubmissionResponses, PolylanSubmitterApiClearCacheData, PolylanSubmitterApiClearCacheResponses, PolylanSubmitterApiGetUserInfoData, PolylanSubmitterApiGetUserInfoResponses, PolylanSubmitterApiHealthCheckData, PolylanSubmitterApiHealthCheckResponses, SubmissionsApiCreateImageCropData, SubmissionsApiCreateImageCropResponses, SubmissionsApiCreateSubmissionData, SubmissionsApiCreateSubmissionResponses, SubmissionsApiDeleteImageCropData, SubmissionsApiDeleteImageCropResponses, SubmissionsApiGetSubmissionData, SubmissionsApiGetSubmissionResponses, SubmissionsApiListSubmissionsByGameData, SubmissionsApiListSubmissionsByGameResponses, SubmissionsApiListUserSubmissionsData, SubmissionsApiListUserSubmissionsResponses, SubmissionsApiProcessCropOcrData, SubmissionsApiProcessCropOcrResponses, SubmissionsApiProcessFileOcrData, SubmissionsApiProcessFileOcrResponses } from './types.gen';
export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean, TResponse = unknown> = Options2<TData, ThrowOnError, TResponse> & { export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean, TResponse = unknown> = Options2<TData, ThrowOnError, TResponse> & {
/** /**
@@ -28,7 +28,7 @@ export const polylanSubmitterApiHealthCheck = <ThrowOnError extends boolean = fa
/** /**
* Clear Cache * Clear Cache
* *
* Clear all API caches (admin only) * Clear API caches (admin only). Pass event_path to clear just that event's keys.
*/ */
export const polylanSubmitterApiClearCache = <ThrowOnError extends boolean = false>(options?: Options<PolylanSubmitterApiClearCacheData, ThrowOnError>) => (options?.client ?? client).post<PolylanSubmitterApiClearCacheResponses, unknown, ThrowOnError>({ url: '/api/cache/clear', ...options }); export const polylanSubmitterApiClearCache = <ThrowOnError extends boolean = false>(options?: Options<PolylanSubmitterApiClearCacheData, ThrowOnError>) => (options?.client ?? client).post<PolylanSubmitterApiClearCacheResponses, unknown, ThrowOnError>({ url: '/api/cache/clear', ...options });
@@ -39,26 +39,31 @@ export const polylanSubmitterApiClearCache = <ThrowOnError extends boolean = fal
*/ */
export const polylanSubmitterApiGetUserInfo = <ThrowOnError extends boolean = false>(options?: Options<PolylanSubmitterApiGetUserInfoData, ThrowOnError>) => (options?.client ?? client).get<PolylanSubmitterApiGetUserInfoResponses, unknown, ThrowOnError>({ url: '/api/user', ...options }); export const polylanSubmitterApiGetUserInfo = <ThrowOnError extends boolean = false>(options?: Options<PolylanSubmitterApiGetUserInfoData, ThrowOnError>) => (options?.client ?? client).get<PolylanSubmitterApiGetUserInfoResponses, unknown, ThrowOnError>({ url: '/api/user', ...options });
/**
* List Games
*/
export const gamesApiListGames = <ThrowOnError extends boolean = false>(options?: Options<GamesApiListGamesData, ThrowOnError>) => (options?.client ?? client).get<GamesApiListGamesResponses, unknown, ThrowOnError>({ url: '/api/games/', ...options });
/** /**
* List Puzzles * List Puzzles
* *
* Get list of available puzzles * Get list of available puzzles
*/ */
export const opusMagnumApiListPuzzles = <ThrowOnError extends boolean = false>(options?: Options<OpusMagnumApiListPuzzlesData, ThrowOnError>) => (options?.client ?? client).get<OpusMagnumApiListPuzzlesResponses, unknown, ThrowOnError>({ url: '/api/opus-magnum/puzzles', ...options }); export const opusMagnumApiListPuzzles = <ThrowOnError extends boolean = false>(options: Options<OpusMagnumApiListPuzzlesData, ThrowOnError>) => (options.client ?? client).get<OpusMagnumApiListPuzzlesResponses, unknown, ThrowOnError>({ url: '/api/events/{event_path}/opus-magnum/puzzles', ...options });
/** /**
* Get Collection * Get Collection
* *
* Get the active collection details * Get the active collection details
*/ */
export const opusMagnumApiGetCollection = <ThrowOnError extends boolean = false>(options?: Options<OpusMagnumApiGetCollectionData, ThrowOnError>) => (options?.client ?? client).get<OpusMagnumApiGetCollectionResponses, unknown, ThrowOnError>({ url: '/api/opus-magnum/collection', ...options }); export const opusMagnumApiGetCollection = <ThrowOnError extends boolean = false>(options: Options<OpusMagnumApiGetCollectionData, ThrowOnError>) => (options.client ?? client).get<OpusMagnumApiGetCollectionResponses, unknown, ThrowOnError>({ url: '/api/events/{event_path}/opus-magnum/collection', ...options });
/** /**
* List Submissions * List Submissions
* *
* Get paginated list of submissions * Get paginated list of submissions
*/ */
export const opusMagnumApiListSubmissions = <ThrowOnError extends boolean = false>(options?: Options<OpusMagnumApiListSubmissionsData, ThrowOnError>) => (options?.client ?? client).get<OpusMagnumApiListSubmissionsResponses, unknown, ThrowOnError>({ url: '/api/opus-magnum/submissions', ...options }); export const opusMagnumApiListSubmissions = <ThrowOnError extends boolean = false>(options: Options<OpusMagnumApiListSubmissionsData, ThrowOnError>) => (options.client ?? client).get<OpusMagnumApiListSubmissionsResponses, unknown, ThrowOnError>({ url: '/api/events/{event_path}/opus-magnum/submissions', ...options });
/** /**
* Create Submission * Create Submission
@@ -67,7 +72,7 @@ export const opusMagnumApiListSubmissions = <ThrowOnError extends boolean = fals
*/ */
export const opusMagnumApiCreateSubmission = <ThrowOnError extends boolean = false>(options: Options<OpusMagnumApiCreateSubmissionData, ThrowOnError>) => (options.client ?? client).post<OpusMagnumApiCreateSubmissionResponses, unknown, ThrowOnError>({ export const opusMagnumApiCreateSubmission = <ThrowOnError extends boolean = false>(options: Options<OpusMagnumApiCreateSubmissionData, ThrowOnError>) => (options.client ?? client).post<OpusMagnumApiCreateSubmissionResponses, unknown, ThrowOnError>({
...formDataBodySerializer, ...formDataBodySerializer,
url: '/api/opus-magnum/submissions', url: '/api/events/{event_path}/opus-magnum/submissions',
...options, ...options,
headers: { headers: {
'Content-Type': null, 'Content-Type': null,
@@ -80,14 +85,14 @@ export const opusMagnumApiCreateSubmission = <ThrowOnError extends boolean = fal
* *
* Delete a submission (admin only) * Delete a submission (admin only)
*/ */
export const opusMagnumApiDeleteSubmission = <ThrowOnError extends boolean = false>(options: Options<OpusMagnumApiDeleteSubmissionData, ThrowOnError>) => (options.client ?? client).delete<OpusMagnumApiDeleteSubmissionResponses, unknown, ThrowOnError>({ url: '/api/opus-magnum/submissions/{submission_id}', ...options }); export const opusMagnumApiDeleteSubmission = <ThrowOnError extends boolean = false>(options: Options<OpusMagnumApiDeleteSubmissionData, ThrowOnError>) => (options.client ?? client).delete<OpusMagnumApiDeleteSubmissionResponses, unknown, ThrowOnError>({ url: '/api/events/{event_path}/opus-magnum/submissions/{submission_id}', ...options });
/** /**
* Get Submission * Get Submission
* *
* Get detailed submission by ID * Get detailed submission by ID
*/ */
export const opusMagnumApiGetSubmission = <ThrowOnError extends boolean = false>(options: Options<OpusMagnumApiGetSubmissionData, ThrowOnError>) => (options.client ?? client).get<OpusMagnumApiGetSubmissionResponses, unknown, ThrowOnError>({ url: '/api/opus-magnum/submissions/{submission_id}', ...options }); export const opusMagnumApiGetSubmission = <ThrowOnError extends boolean = false>(options: Options<OpusMagnumApiGetSubmissionData, ThrowOnError>) => (options.client ?? client).get<OpusMagnumApiGetSubmissionResponses, unknown, ThrowOnError>({ url: '/api/events/{event_path}/opus-magnum/submissions/{submission_id}', ...options });
/** /**
* Validate Response * Validate Response
@@ -95,7 +100,7 @@ export const opusMagnumApiGetSubmission = <ThrowOnError extends boolean = false>
* Manually validate a puzzle response * Manually validate a puzzle response
*/ */
export const opusMagnumApiValidateResponse = <ThrowOnError extends boolean = false>(options: Options<OpusMagnumApiValidateResponseData, ThrowOnError>) => (options.client ?? client).put<OpusMagnumApiValidateResponseResponses, unknown, ThrowOnError>({ export const opusMagnumApiValidateResponse = <ThrowOnError extends boolean = false>(options: Options<OpusMagnumApiValidateResponseData, ThrowOnError>) => (options.client ?? client).put<OpusMagnumApiValidateResponseResponses, unknown, ThrowOnError>({
url: '/api/opus-magnum/responses/{response_id}/validate', url: '/api/events/{event_path}/opus-magnum/responses/{response_id}/validate',
...options, ...options,
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@@ -108,52 +113,52 @@ export const opusMagnumApiValidateResponse = <ThrowOnError extends boolean = fal
* *
* Try to auto validate a puzzle response * Try to auto validate a puzzle response
*/ */
export const opusMagnumApiValidateAuto = <ThrowOnError extends boolean = false>(options: Options<OpusMagnumApiValidateAutoData, ThrowOnError>) => (options.client ?? client).put<OpusMagnumApiValidateAutoResponses, unknown, ThrowOnError>({ url: '/api/opus-magnum/responses/{response_id}/validate/auto', ...options }); export const opusMagnumApiValidateAuto = <ThrowOnError extends boolean = false>(options: Options<OpusMagnumApiValidateAutoData, ThrowOnError>) => (options.client ?? client).put<OpusMagnumApiValidateAutoResponses, unknown, ThrowOnError>({ url: '/api/events/{event_path}/opus-magnum/responses/{response_id}/validate/auto', ...options });
/** /**
* List Responses Needing Validation * List Responses Needing Validation
* *
* Get all responses that need manual validation * Get all responses that need manual validation
*/ */
export const opusMagnumApiListResponsesNeedingValidation = <ThrowOnError extends boolean = false>(options?: Options<OpusMagnumApiListResponsesNeedingValidationData, ThrowOnError>) => (options?.client ?? client).get<OpusMagnumApiListResponsesNeedingValidationResponses, unknown, ThrowOnError>({ url: '/api/opus-magnum/responses/needs-validation', ...options }); export const opusMagnumApiListResponsesNeedingValidation = <ThrowOnError extends boolean = false>(options: Options<OpusMagnumApiListResponsesNeedingValidationData, ThrowOnError>) => (options.client ?? client).get<OpusMagnumApiListResponsesNeedingValidationResponses, unknown, ThrowOnError>({ url: '/api/events/{event_path}/opus-magnum/responses/needs-validation', ...options });
/** /**
* Validate Submission * Validate Submission
* *
* Mark entire submission as validated * Mark entire submission as validated
*/ */
export const opusMagnumApiValidateSubmission = <ThrowOnError extends boolean = false>(options: Options<OpusMagnumApiValidateSubmissionData, ThrowOnError>) => (options.client ?? client).post<OpusMagnumApiValidateSubmissionResponses, unknown, ThrowOnError>({ url: '/api/opus-magnum/submissions/{submission_id}/validate', ...options }); export const opusMagnumApiValidateSubmission = <ThrowOnError extends boolean = false>(options: Options<OpusMagnumApiValidateSubmissionData, ThrowOnError>) => (options.client ?? client).post<OpusMagnumApiValidateSubmissionResponses, unknown, ThrowOnError>({ url: '/api/events/{event_path}/opus-magnum/submissions/{submission_id}/validate', ...options });
/** /**
* Get Stats * Get Stats
* *
* Get submission statistics * Get submission statistics
*/ */
export const opusMagnumApiGetStats = <ThrowOnError extends boolean = false>(options?: Options<OpusMagnumApiGetStatsData, ThrowOnError>) => (options?.client ?? client).get<OpusMagnumApiGetStatsResponses, unknown, ThrowOnError>({ url: '/api/opus-magnum/stats', ...options }); export const opusMagnumApiGetStats = <ThrowOnError extends boolean = false>(options: Options<OpusMagnumApiGetStatsData, ThrowOnError>) => (options.client ?? client).get<OpusMagnumApiGetStatsResponses, unknown, ThrowOnError>({ url: '/api/events/{event_path}/opus-magnum/stats', ...options });
/** /**
* Results * Results
*/ */
export const animationsApiResults = <ThrowOnError extends boolean = false>(options?: Options<AnimationsApiResultsData, ThrowOnError>) => (options?.client ?? client).get<AnimationsApiResultsResponses, unknown, ThrowOnError>({ url: '/api/results/results', ...options }); export const animationsApiResults = <ThrowOnError extends boolean = false>(options: Options<AnimationsApiResultsData, ThrowOnError>) => (options.client ?? client).get<AnimationsApiResultsResponses, unknown, ThrowOnError>({ url: '/api/events/{event_path}/results/results', ...options });
/** /**
* Top Submissions * Top Submissions
* *
* Get tournament top submissions for each puzzle. Only available when tournament is closed. * Get tournament top submissions for each puzzle. Only available when tournament is closed.
*/ */
export const animationsApiTopSubmissions = <ThrowOnError extends boolean = false>(options?: Options<AnimationsApiTopSubmissionsData, ThrowOnError>) => (options?.client ?? client).get<AnimationsApiTopSubmissionsResponses, unknown, ThrowOnError>({ url: '/api/results/top-submissions', ...options }); export const animationsApiTopSubmissions = <ThrowOnError extends boolean = false>(options: Options<AnimationsApiTopSubmissionsData, ThrowOnError>) => (options.client ?? client).get<AnimationsApiTopSubmissionsResponses, unknown, ThrowOnError>({ url: '/api/events/{event_path}/results/top-submissions', ...options });
/** /**
* Puzzle Results * Puzzle Results
* *
* Get tournament results organized by puzzle with coefficients. Only available when tournament is closed. * Get tournament results organized by puzzle with coefficients. Only available when tournament is closed.
*/ */
export const animationsApiPuzzleResults = <ThrowOnError extends boolean = false>(options?: Options<AnimationsApiPuzzleResultsData, ThrowOnError>) => (options?.client ?? client).get<AnimationsApiPuzzleResultsResponses, unknown, ThrowOnError>({ url: '/api/results/puzzle-results', ...options }); export const animationsApiPuzzleResults = <ThrowOnError extends boolean = false>(options: Options<AnimationsApiPuzzleResultsData, ThrowOnError>) => (options.client ?? client).get<AnimationsApiPuzzleResultsResponses, unknown, ThrowOnError>({ url: '/api/events/{event_path}/results/puzzle-results', ...options });
/** /**
* Get Results * Get Results
*/ */
export const noitaApiGetResults = <ThrowOnError extends boolean = false>(options?: Options<NoitaApiGetResultsData, ThrowOnError>) => (options?.client ?? client).get<NoitaApiGetResultsResponses, unknown, ThrowOnError>({ url: '/api/noita/results', ...options }); export const noitaApiGetResults = <ThrowOnError extends boolean = false>(options: Options<NoitaApiGetResultsData, ThrowOnError>) => (options.client ?? client).get<NoitaApiGetResultsResponses, unknown, ThrowOnError>({ url: '/api/events/{event_path}/noita/results', ...options });
/** /**
* Get Leaderboard * Get Leaderboard
@@ -162,7 +167,7 @@ export const noitaApiGetResults = <ThrowOnError extends boolean = false>(options
* *
* Uses Window functions to rank users by their total score in descending order. * Uses Window functions to rank users by their total score in descending order.
*/ */
export const noitaApiGetLeaderboard = <ThrowOnError extends boolean = false>(options?: Options<NoitaApiGetLeaderboardData, ThrowOnError>) => (options?.client ?? client).get<NoitaApiGetLeaderboardResponses, unknown, ThrowOnError>({ url: '/api/noita/leaderboard', ...options }); export const noitaApiGetLeaderboard = <ThrowOnError extends boolean = false>(options: Options<NoitaApiGetLeaderboardData, ThrowOnError>) => (options.client ?? client).get<NoitaApiGetLeaderboardResponses, unknown, ThrowOnError>({ url: '/api/events/{event_path}/noita/leaderboard', ...options });
/** /**
* Submit Log File * Submit Log File
@@ -178,7 +183,7 @@ export const noitaApiGetLeaderboard = <ThrowOnError extends boolean = false>(opt
*/ */
export const noitaApiSubmitLogFile = <ThrowOnError extends boolean = false>(options: Options<NoitaApiSubmitLogFileData, ThrowOnError>) => (options.client ?? client).post<NoitaApiSubmitLogFileResponses, NoitaApiSubmitLogFileErrors, ThrowOnError>({ export const noitaApiSubmitLogFile = <ThrowOnError extends boolean = false>(options: Options<NoitaApiSubmitLogFileData, ThrowOnError>) => (options.client ?? client).post<NoitaApiSubmitLogFileResponses, NoitaApiSubmitLogFileErrors, ThrowOnError>({
...formDataBodySerializer, ...formDataBodySerializer,
url: '/api/noita/submit', url: '/api/events/{event_path}/noita/submit',
...options, ...options,
headers: { headers: {
'Content-Type': null, 'Content-Type': null,
@@ -187,16 +192,16 @@ export const noitaApiSubmitLogFile = <ThrowOnError extends boolean = false>(opti
}); });
/** /**
* List Games * List Event Games
*/ */
export const gamesApiListGames = <ThrowOnError extends boolean = false>(options?: Options<GamesApiListGamesData, ThrowOnError>) => (options?.client ?? client).get<GamesApiListGamesResponses, unknown, ThrowOnError>({ url: '/api/games/', ...options }); export const gamesApiListEventGames = <ThrowOnError extends boolean = false>(options: Options<GamesApiListEventGamesData, ThrowOnError>) => (options.client ?? client).get<GamesApiListEventGamesResponses, unknown, ThrowOnError>({ url: '/api/events/{event_path}/games', ...options });
/** /**
* Create Submission * Create Submission
*/ */
export const submissionsApiCreateSubmission = <ThrowOnError extends boolean = false>(options: Options<SubmissionsApiCreateSubmissionData, ThrowOnError>) => (options.client ?? client).post<SubmissionsApiCreateSubmissionResponses, unknown, ThrowOnError>({ export const submissionsApiCreateSubmission = <ThrowOnError extends boolean = false>(options: Options<SubmissionsApiCreateSubmissionData, ThrowOnError>) => (options.client ?? client).post<SubmissionsApiCreateSubmissionResponses, unknown, ThrowOnError>({
...formDataBodySerializer, ...formDataBodySerializer,
url: '/api/submissions/submission/create', url: '/api/events/{event_path}/submissions/submission/create',
...options, ...options,
headers: { headers: {
'Content-Type': null, 'Content-Type': null,
@@ -207,23 +212,23 @@ export const submissionsApiCreateSubmission = <ThrowOnError extends boolean = fa
/** /**
* List User Submissions * List User Submissions
*/ */
export const submissionsApiListUserSubmissions = <ThrowOnError extends boolean = false>(options: Options<SubmissionsApiListUserSubmissionsData, ThrowOnError>) => (options.client ?? client).get<SubmissionsApiListUserSubmissionsResponses, unknown, ThrowOnError>({ url: '/api/submissions/submission/by/user', ...options }); export const submissionsApiListUserSubmissions = <ThrowOnError extends boolean = false>(options: Options<SubmissionsApiListUserSubmissionsData, ThrowOnError>) => (options.client ?? client).get<SubmissionsApiListUserSubmissionsResponses, unknown, ThrowOnError>({ url: '/api/events/{event_path}/submissions/submission/by/user', ...options });
/** /**
* List Submissions By Game * List Submissions By Game
*/ */
export const submissionsApiListSubmissionsByGame = <ThrowOnError extends boolean = false>(options: Options<SubmissionsApiListSubmissionsByGameData, ThrowOnError>) => (options.client ?? client).get<SubmissionsApiListSubmissionsByGameResponses, unknown, ThrowOnError>({ url: '/api/submissions/submission/by/game', ...options }); export const submissionsApiListSubmissionsByGame = <ThrowOnError extends boolean = false>(options: Options<SubmissionsApiListSubmissionsByGameData, ThrowOnError>) => (options.client ?? client).get<SubmissionsApiListSubmissionsByGameResponses, unknown, ThrowOnError>({ url: '/api/events/{event_path}/submissions/submission/by/game', ...options });
/** /**
* Get Submission * Get Submission
*/ */
export const submissionsApiGetSubmission = <ThrowOnError extends boolean = false>(options: Options<SubmissionsApiGetSubmissionData, ThrowOnError>) => (options.client ?? client).get<SubmissionsApiGetSubmissionResponses, unknown, ThrowOnError>({ url: '/api/submissions/submission/{submission_id}', ...options }); export const submissionsApiGetSubmission = <ThrowOnError extends boolean = false>(options: Options<SubmissionsApiGetSubmissionData, ThrowOnError>) => (options.client ?? client).get<SubmissionsApiGetSubmissionResponses, unknown, ThrowOnError>({ url: '/api/events/{event_path}/submissions/submission/{submission_id}', ...options });
/** /**
* Create Image Crop * Create Image Crop
*/ */
export const submissionsApiCreateImageCrop = <ThrowOnError extends boolean = false>(options: Options<SubmissionsApiCreateImageCropData, ThrowOnError>) => (options.client ?? client).post<SubmissionsApiCreateImageCropResponses, unknown, ThrowOnError>({ export const submissionsApiCreateImageCrop = <ThrowOnError extends boolean = false>(options: Options<SubmissionsApiCreateImageCropData, ThrowOnError>) => (options.client ?? client).post<SubmissionsApiCreateImageCropResponses, unknown, ThrowOnError>({
url: '/api/submissions/crops/s/{submission_file_id}', url: '/api/events/{event_path}/submissions/crops/s/{submission_file_id}',
...options, ...options,
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@@ -234,38 +239,38 @@ export const submissionsApiCreateImageCrop = <ThrowOnError extends boolean = fal
/** /**
* Delete Image Crop * Delete Image Crop
*/ */
export const submissionsApiDeleteImageCrop = <ThrowOnError extends boolean = false>(options: Options<SubmissionsApiDeleteImageCropData, ThrowOnError>) => (options.client ?? client).delete<SubmissionsApiDeleteImageCropResponses, unknown, ThrowOnError>({ url: '/api/submissions/crops/{crop_id}', ...options }); export const submissionsApiDeleteImageCrop = <ThrowOnError extends boolean = false>(options: Options<SubmissionsApiDeleteImageCropData, ThrowOnError>) => (options.client ?? client).delete<SubmissionsApiDeleteImageCropResponses, unknown, ThrowOnError>({ url: '/api/events/{event_path}/submissions/crops/{crop_id}', ...options });
/** /**
* Process File Ocr * Process File Ocr
*/ */
export const submissionsApiProcessFileOcr = <ThrowOnError extends boolean = false>(options: Options<SubmissionsApiProcessFileOcrData, ThrowOnError>) => (options.client ?? client).post<SubmissionsApiProcessFileOcrResponses, unknown, ThrowOnError>({ url: '/api/submissions/files/{file_id}/process-ocr', ...options }); export const submissionsApiProcessFileOcr = <ThrowOnError extends boolean = false>(options: Options<SubmissionsApiProcessFileOcrData, ThrowOnError>) => (options.client ?? client).post<SubmissionsApiProcessFileOcrResponses, unknown, ThrowOnError>({ url: '/api/events/{event_path}/submissions/files/{file_id}/process-ocr', ...options });
/** /**
* Process Crop Ocr * Process Crop Ocr
*/ */
export const submissionsApiProcessCropOcr = <ThrowOnError extends boolean = false>(options: Options<SubmissionsApiProcessCropOcrData, ThrowOnError>) => (options.client ?? client).post<SubmissionsApiProcessCropOcrResponses, unknown, ThrowOnError>({ url: '/api/submissions/crops/{crop_id}/process-ocr', ...options }); export const submissionsApiProcessCropOcr = <ThrowOnError extends boolean = false>(options: Options<SubmissionsApiProcessCropOcrData, ThrowOnError>) => (options.client ?? client).post<SubmissionsApiProcessCropOcrResponses, unknown, ThrowOnError>({ url: '/api/events/{event_path}/submissions/crops/{crop_id}/process-ocr', ...options });
/** /**
* List Markets * List Markets
* *
* List all markets (excludes draft markets). * List all markets (excludes draft markets).
*/ */
export const marketApiListMarkets = <ThrowOnError extends boolean = false>(options?: Options<MarketApiListMarketsData, ThrowOnError>) => (options?.client ?? client).get<MarketApiListMarketsResponses, unknown, ThrowOnError>({ url: '/api/market/', ...options }); export const marketApiListMarkets = <ThrowOnError extends boolean = false>(options: Options<MarketApiListMarketsData, ThrowOnError>) => (options.client ?? client).get<MarketApiListMarketsResponses, unknown, ThrowOnError>({ url: '/api/events/{event_path}/market/', ...options });
/** /**
* List User Bets * List User Bets
* *
* List all bets placed by the current user. * List all bets placed by the current user.
*/ */
export const marketApiListUserBets = <ThrowOnError extends boolean = false>(options?: Options<MarketApiListUserBetsData, ThrowOnError>) => (options?.client ?? client).get<MarketApiListUserBetsResponses, unknown, ThrowOnError>({ url: '/api/market/user/bets', ...options }); export const marketApiListUserBets = <ThrowOnError extends boolean = false>(options: Options<MarketApiListUserBetsData, ThrowOnError>) => (options.client ?? client).get<MarketApiListUserBetsResponses, unknown, ThrowOnError>({ url: '/api/events/{event_path}/market/user/bets', ...options });
/** /**
* Close Market * Close Market
* *
* Close a market. Admin only. * Close a market. Admin only.
*/ */
export const marketApiCloseMarket = <ThrowOnError extends boolean = false>(options: Options<MarketApiCloseMarketData, ThrowOnError>) => (options.client ?? client).post<MarketApiCloseMarketResponses, unknown, ThrowOnError>({ url: '/api/market/{market_uuid}/actions/close', ...options }); export const marketApiCloseMarket = <ThrowOnError extends boolean = false>(options: Options<MarketApiCloseMarketData, ThrowOnError>) => (options.client ?? client).post<MarketApiCloseMarketResponses, unknown, ThrowOnError>({ url: '/api/events/{event_path}/market/{market_uuid}/actions/close', ...options });
/** /**
* Resolve Market * Resolve Market
@@ -273,7 +278,7 @@ export const marketApiCloseMarket = <ThrowOnError extends boolean = false>(optio
* Resolve a market with a winning option. Admin only. * Resolve a market with a winning option. Admin only.
*/ */
export const marketApiResolveMarket = <ThrowOnError extends boolean = false>(options: Options<MarketApiResolveMarketData, ThrowOnError>) => (options.client ?? client).post<MarketApiResolveMarketResponses, unknown, ThrowOnError>({ export const marketApiResolveMarket = <ThrowOnError extends boolean = false>(options: Options<MarketApiResolveMarketData, ThrowOnError>) => (options.client ?? client).post<MarketApiResolveMarketResponses, unknown, ThrowOnError>({
url: '/api/market/{market_uuid}/actions/resolve', url: '/api/events/{event_path}/market/{market_uuid}/actions/resolve',
...options, ...options,
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@@ -287,7 +292,7 @@ export const marketApiResolveMarket = <ThrowOnError extends boolean = false>(opt
* Place a bet on a market option. * Place a bet on a market option.
*/ */
export const marketApiCreateBet = <ThrowOnError extends boolean = false>(options: Options<MarketApiCreateBetData, ThrowOnError>) => (options.client ?? client).post<MarketApiCreateBetResponses, unknown, ThrowOnError>({ export const marketApiCreateBet = <ThrowOnError extends boolean = false>(options: Options<MarketApiCreateBetData, ThrowOnError>) => (options.client ?? client).post<MarketApiCreateBetResponses, unknown, ThrowOnError>({
url: '/api/market/{market_uuid}/bets', url: '/api/events/{event_path}/market/{market_uuid}/bets',
...options, ...options,
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
+244 -79
View File
@@ -52,6 +52,32 @@ export type UserInfoOut = {
cas_groups?: Array<string> | null; cas_groups?: Array<string> | null;
}; };
/**
* GameOut
*/
export type GameOut = {
/**
* Id
*/
id: number;
/**
* Steam App Id
*/
steam_app_id?: number | null;
/**
* Name
*/
name: string;
/**
* Path
*/
path: string;
/**
* Image
*/
image?: string | null;
};
/** /**
* PuzzlePointsFactorOut * PuzzlePointsFactorOut
* *
@@ -890,32 +916,6 @@ export type NoitaSubmissionOut = {
processed: boolean; processed: boolean;
}; };
/**
* GameOut
*/
export type GameOut = {
/**
* Id
*/
id: number;
/**
* Steam App Id
*/
steam_app_id?: number | null;
/**
* Name
*/
name: string;
/**
* Path
*/
path: string;
/**
* Image
*/
image?: string | null;
};
/** /**
* ImageCropOut * ImageCropOut
*/ */
@@ -1102,7 +1102,12 @@ export type PolylanSubmitterApiHealthCheckResponses = {
export type PolylanSubmitterApiClearCacheData = { export type PolylanSubmitterApiClearCacheData = {
body?: never; body?: never;
path?: never; path?: never;
query?: never; query?: {
/**
* Event Path
*/
event_path?: string | null;
};
url: '/api/cache/clear'; url: '/api/cache/clear';
}; };
@@ -1129,11 +1134,34 @@ export type PolylanSubmitterApiGetUserInfoResponses = {
export type PolylanSubmitterApiGetUserInfoResponse = PolylanSubmitterApiGetUserInfoResponses[keyof PolylanSubmitterApiGetUserInfoResponses]; export type PolylanSubmitterApiGetUserInfoResponse = PolylanSubmitterApiGetUserInfoResponses[keyof PolylanSubmitterApiGetUserInfoResponses];
export type OpusMagnumApiListPuzzlesData = { export type GamesApiListGamesData = {
body?: never; body?: never;
path?: never; path?: never;
query?: never; query?: never;
url: '/api/opus-magnum/puzzles'; url: '/api/games/';
};
export type GamesApiListGamesResponses = {
/**
* Response
*
* OK
*/
200: Array<GameOut>;
};
export type GamesApiListGamesResponse = GamesApiListGamesResponses[keyof GamesApiListGamesResponses];
export type OpusMagnumApiListPuzzlesData = {
body?: never;
path: {
/**
* Event Path
*/
event_path: string;
};
query?: never;
url: '/api/events/{event_path}/opus-magnum/puzzles';
}; };
export type OpusMagnumApiListPuzzlesResponses = { export type OpusMagnumApiListPuzzlesResponses = {
@@ -1149,9 +1177,14 @@ export type OpusMagnumApiListPuzzlesResponse = OpusMagnumApiListPuzzlesResponses
export type OpusMagnumApiGetCollectionData = { export type OpusMagnumApiGetCollectionData = {
body?: never; body?: never;
path?: never; path: {
/**
* Event Path
*/
event_path: string;
};
query?: never; query?: never;
url: '/api/opus-magnum/collection'; url: '/api/events/{event_path}/opus-magnum/collection';
}; };
export type OpusMagnumApiGetCollectionResponses = { export type OpusMagnumApiGetCollectionResponses = {
@@ -1165,7 +1198,12 @@ export type OpusMagnumApiGetCollectionResponse = OpusMagnumApiGetCollectionRespo
export type OpusMagnumApiListSubmissionsData = { export type OpusMagnumApiListSubmissionsData = {
body?: never; body?: never;
path?: never; path: {
/**
* Event Path
*/
event_path: string;
};
query?: { query?: {
/** /**
* Limit * Limit
@@ -1176,7 +1214,7 @@ export type OpusMagnumApiListSubmissionsData = {
*/ */
offset?: number; offset?: number;
}; };
url: '/api/opus-magnum/submissions'; url: '/api/events/{event_path}/opus-magnum/submissions';
}; };
export type OpusMagnumApiListSubmissionsResponses = { export type OpusMagnumApiListSubmissionsResponses = {
@@ -1199,9 +1237,14 @@ export type OpusMagnumApiCreateSubmissionData = {
files: Array<Blob | File>; files: Array<Blob | File>;
data: SubmissionIn; data: SubmissionIn;
}; };
path?: never; path: {
/**
* Event Path
*/
event_path: string;
};
query?: never; query?: never;
url: '/api/opus-magnum/submissions'; url: '/api/events/{event_path}/opus-magnum/submissions';
}; };
export type OpusMagnumApiCreateSubmissionResponses = { export type OpusMagnumApiCreateSubmissionResponses = {
@@ -1216,13 +1259,17 @@ export type OpusMagnumApiCreateSubmissionResponse = OpusMagnumApiCreateSubmissio
export type OpusMagnumApiDeleteSubmissionData = { export type OpusMagnumApiDeleteSubmissionData = {
body?: never; body?: never;
path: { path: {
/**
* Event Path
*/
event_path: string;
/** /**
* Submission Id * Submission Id
*/ */
submission_id: string; submission_id: string;
}; };
query?: never; query?: never;
url: '/api/opus-magnum/submissions/{submission_id}'; url: '/api/events/{event_path}/opus-magnum/submissions/{submission_id}';
}; };
export type OpusMagnumApiDeleteSubmissionResponses = { export type OpusMagnumApiDeleteSubmissionResponses = {
@@ -1235,13 +1282,17 @@ export type OpusMagnumApiDeleteSubmissionResponses = {
export type OpusMagnumApiGetSubmissionData = { export type OpusMagnumApiGetSubmissionData = {
body?: never; body?: never;
path: { path: {
/**
* Event Path
*/
event_path: string;
/** /**
* Submission Id * Submission Id
*/ */
submission_id: string; submission_id: string;
}; };
query?: never; query?: never;
url: '/api/opus-magnum/submissions/{submission_id}'; url: '/api/events/{event_path}/opus-magnum/submissions/{submission_id}';
}; };
export type OpusMagnumApiGetSubmissionResponses = { export type OpusMagnumApiGetSubmissionResponses = {
@@ -1256,13 +1307,17 @@ export type OpusMagnumApiGetSubmissionResponse = OpusMagnumApiGetSubmissionRespo
export type OpusMagnumApiValidateResponseData = { export type OpusMagnumApiValidateResponseData = {
body: ValidationIn; body: ValidationIn;
path: { path: {
/**
* Event Path
*/
event_path: string;
/** /**
* Response Id * Response Id
*/ */
response_id: number; response_id: number;
}; };
query?: never; query?: never;
url: '/api/opus-magnum/responses/{response_id}/validate'; url: '/api/events/{event_path}/opus-magnum/responses/{response_id}/validate';
}; };
export type OpusMagnumApiValidateResponseResponses = { export type OpusMagnumApiValidateResponseResponses = {
@@ -1277,13 +1332,17 @@ export type OpusMagnumApiValidateResponseResponse = OpusMagnumApiValidateRespons
export type OpusMagnumApiValidateAutoData = { export type OpusMagnumApiValidateAutoData = {
body?: never; body?: never;
path: { path: {
/**
* Event Path
*/
event_path: string;
/** /**
* Response Id * Response Id
*/ */
response_id: number; response_id: number;
}; };
query?: never; query?: never;
url: '/api/opus-magnum/responses/{response_id}/validate/auto'; url: '/api/events/{event_path}/opus-magnum/responses/{response_id}/validate/auto';
}; };
export type OpusMagnumApiValidateAutoResponses = { export type OpusMagnumApiValidateAutoResponses = {
@@ -1297,9 +1356,14 @@ export type OpusMagnumApiValidateAutoResponse = OpusMagnumApiValidateAutoRespons
export type OpusMagnumApiListResponsesNeedingValidationData = { export type OpusMagnumApiListResponsesNeedingValidationData = {
body?: never; body?: never;
path?: never; path: {
/**
* Event Path
*/
event_path: string;
};
query?: never; query?: never;
url: '/api/opus-magnum/responses/needs-validation'; url: '/api/events/{event_path}/opus-magnum/responses/needs-validation';
}; };
export type OpusMagnumApiListResponsesNeedingValidationResponses = { export type OpusMagnumApiListResponsesNeedingValidationResponses = {
@@ -1316,13 +1380,17 @@ export type OpusMagnumApiListResponsesNeedingValidationResponse = OpusMagnumApiL
export type OpusMagnumApiValidateSubmissionData = { export type OpusMagnumApiValidateSubmissionData = {
body?: never; body?: never;
path: { path: {
/**
* Event Path
*/
event_path: string;
/** /**
* Submission Id * Submission Id
*/ */
submission_id: string; submission_id: string;
}; };
query?: never; query?: never;
url: '/api/opus-magnum/submissions/{submission_id}/validate'; url: '/api/events/{event_path}/opus-magnum/submissions/{submission_id}/validate';
}; };
export type OpusMagnumApiValidateSubmissionResponses = { export type OpusMagnumApiValidateSubmissionResponses = {
@@ -1336,9 +1404,14 @@ export type OpusMagnumApiValidateSubmissionResponse = OpusMagnumApiValidateSubmi
export type OpusMagnumApiGetStatsData = { export type OpusMagnumApiGetStatsData = {
body?: never; body?: never;
path?: never; path: {
/**
* Event Path
*/
event_path: string;
};
query?: never; query?: never;
url: '/api/opus-magnum/stats'; url: '/api/events/{event_path}/opus-magnum/stats';
}; };
export type OpusMagnumApiGetStatsResponses = { export type OpusMagnumApiGetStatsResponses = {
@@ -1350,9 +1423,14 @@ export type OpusMagnumApiGetStatsResponses = {
export type AnimationsApiResultsData = { export type AnimationsApiResultsData = {
body?: never; body?: never;
path?: never; path: {
/**
* Event Path
*/
event_path: string;
};
query?: never; query?: never;
url: '/api/results/results'; url: '/api/events/{event_path}/results/results';
}; };
export type AnimationsApiResultsResponses = { export type AnimationsApiResultsResponses = {
@@ -1366,14 +1444,19 @@ export type AnimationsApiResultsResponse = AnimationsApiResultsResponses[keyof A
export type AnimationsApiTopSubmissionsData = { export type AnimationsApiTopSubmissionsData = {
body?: never; body?: never;
path?: never; path: {
/**
* Event Path
*/
event_path: string;
};
query?: { query?: {
/** /**
* Limit * Limit
*/ */
limit?: number; limit?: number;
}; };
url: '/api/results/top-submissions'; url: '/api/events/{event_path}/results/top-submissions';
}; };
export type AnimationsApiTopSubmissionsResponses = { export type AnimationsApiTopSubmissionsResponses = {
@@ -1387,14 +1470,19 @@ export type AnimationsApiTopSubmissionsResponse = AnimationsApiTopSubmissionsRes
export type AnimationsApiPuzzleResultsData = { export type AnimationsApiPuzzleResultsData = {
body?: never; body?: never;
path?: never; path: {
/**
* Event Path
*/
event_path: string;
};
query?: { query?: {
/** /**
* Limit * Limit
*/ */
limit?: number; limit?: number;
}; };
url: '/api/results/puzzle-results'; url: '/api/events/{event_path}/results/puzzle-results';
}; };
export type AnimationsApiPuzzleResultsResponses = { export type AnimationsApiPuzzleResultsResponses = {
@@ -1408,9 +1496,14 @@ export type AnimationsApiPuzzleResultsResponse = AnimationsApiPuzzleResultsRespo
export type NoitaApiGetResultsData = { export type NoitaApiGetResultsData = {
body?: never; body?: never;
path?: never; path: {
/**
* Event Path
*/
event_path: string;
};
query?: never; query?: never;
url: '/api/noita/results'; url: '/api/events/{event_path}/noita/results';
}; };
export type NoitaApiGetResultsResponses = { export type NoitaApiGetResultsResponses = {
@@ -1424,9 +1517,14 @@ export type NoitaApiGetResultsResponse = NoitaApiGetResultsResponses[keyof Noita
export type NoitaApiGetLeaderboardData = { export type NoitaApiGetLeaderboardData = {
body?: never; body?: never;
path?: never; path: {
/**
* Event Path
*/
event_path: string;
};
query?: never; query?: never;
url: '/api/noita/leaderboard'; url: '/api/events/{event_path}/noita/leaderboard';
}; };
export type NoitaApiGetLeaderboardResponses = { export type NoitaApiGetLeaderboardResponses = {
@@ -1448,9 +1546,14 @@ export type NoitaApiSubmitLogFileData = {
*/ */
file: Blob | File; file: Blob | File;
}; };
path?: never; path: {
/**
* Event Path
*/
event_path: string;
};
query?: never; query?: never;
url: '/api/noita/submit'; url: '/api/events/{event_path}/noita/submit';
}; };
export type NoitaApiSubmitLogFileErrors = { export type NoitaApiSubmitLogFileErrors = {
@@ -1475,14 +1578,19 @@ export type NoitaApiSubmitLogFileResponses = {
export type NoitaApiSubmitLogFileResponse = NoitaApiSubmitLogFileResponses[keyof NoitaApiSubmitLogFileResponses]; export type NoitaApiSubmitLogFileResponse = NoitaApiSubmitLogFileResponses[keyof NoitaApiSubmitLogFileResponses];
export type GamesApiListGamesData = { export type GamesApiListEventGamesData = {
body?: never; body?: never;
path?: never; path: {
/**
* Event Path
*/
event_path: string;
};
query?: never; query?: never;
url: '/api/games/'; url: '/api/events/{event_path}/games';
}; };
export type GamesApiListGamesResponses = { export type GamesApiListEventGamesResponses = {
/** /**
* Response * Response
* *
@@ -1491,7 +1599,7 @@ export type GamesApiListGamesResponses = {
200: Array<GameOut>; 200: Array<GameOut>;
}; };
export type GamesApiListGamesResponse = GamesApiListGamesResponses[keyof GamesApiListGamesResponses]; export type GamesApiListEventGamesResponse = GamesApiListEventGamesResponses[keyof GamesApiListEventGamesResponses];
export type SubmissionsApiCreateSubmissionData = { export type SubmissionsApiCreateSubmissionData = {
/** /**
@@ -1507,14 +1615,19 @@ export type SubmissionsApiCreateSubmissionData = {
*/ */
notes?: string | null; notes?: string | null;
}; };
path?: never; path: {
/**
* Event Path
*/
event_path: string;
};
query: { query: {
/** /**
* Game Path * Game Path
*/ */
game_path: string; game_path: string;
}; };
url: '/api/submissions/submission/create'; url: '/api/events/{event_path}/submissions/submission/create';
}; };
export type SubmissionsApiCreateSubmissionResponses = { export type SubmissionsApiCreateSubmissionResponses = {
@@ -1528,14 +1641,19 @@ export type SubmissionsApiCreateSubmissionResponse = SubmissionsApiCreateSubmiss
export type SubmissionsApiListUserSubmissionsData = { export type SubmissionsApiListUserSubmissionsData = {
body?: never; body?: never;
path?: never; path: {
/**
* Event Path
*/
event_path: string;
};
query: { query: {
/** /**
* Game Path * Game Path
*/ */
game_path: string; game_path: string;
}; };
url: '/api/submissions/submission/by/user'; url: '/api/events/{event_path}/submissions/submission/by/user';
}; };
export type SubmissionsApiListUserSubmissionsResponses = { export type SubmissionsApiListUserSubmissionsResponses = {
@@ -1551,14 +1669,19 @@ export type SubmissionsApiListUserSubmissionsResponse = SubmissionsApiListUserSu
export type SubmissionsApiListSubmissionsByGameData = { export type SubmissionsApiListSubmissionsByGameData = {
body?: never; body?: never;
path?: never; path: {
/**
* Event Path
*/
event_path: string;
};
query: { query: {
/** /**
* Game Path * Game Path
*/ */
game_path: string; game_path: string;
}; };
url: '/api/submissions/submission/by/game'; url: '/api/events/{event_path}/submissions/submission/by/game';
}; };
export type SubmissionsApiListSubmissionsByGameResponses = { export type SubmissionsApiListSubmissionsByGameResponses = {
@@ -1575,13 +1698,17 @@ export type SubmissionsApiListSubmissionsByGameResponse = SubmissionsApiListSubm
export type SubmissionsApiGetSubmissionData = { export type SubmissionsApiGetSubmissionData = {
body?: never; body?: never;
path: { path: {
/**
* Event Path
*/
event_path: string;
/** /**
* Submission Id * Submission Id
*/ */
submission_id: string; submission_id: string;
}; };
query?: never; query?: never;
url: '/api/submissions/submission/{submission_id}'; url: '/api/events/{event_path}/submissions/submission/{submission_id}';
}; };
export type SubmissionsApiGetSubmissionResponses = { export type SubmissionsApiGetSubmissionResponses = {
@@ -1596,13 +1723,17 @@ export type SubmissionsApiGetSubmissionResponse = SubmissionsApiGetSubmissionRes
export type SubmissionsApiCreateImageCropData = { export type SubmissionsApiCreateImageCropData = {
body: ImageCropIn; body: ImageCropIn;
path: { path: {
/**
* Event Path
*/
event_path: string;
/** /**
* Submission File Id * Submission File Id
*/ */
submission_file_id: string; submission_file_id: string;
}; };
query?: never; query?: never;
url: '/api/submissions/crops/s/{submission_file_id}'; url: '/api/events/{event_path}/submissions/crops/s/{submission_file_id}';
}; };
export type SubmissionsApiCreateImageCropResponses = { export type SubmissionsApiCreateImageCropResponses = {
@@ -1617,13 +1748,17 @@ export type SubmissionsApiCreateImageCropResponse = SubmissionsApiCreateImageCro
export type SubmissionsApiDeleteImageCropData = { export type SubmissionsApiDeleteImageCropData = {
body?: never; body?: never;
path: { path: {
/**
* Event Path
*/
event_path: string;
/** /**
* Crop Id * Crop Id
*/ */
crop_id: string; crop_id: string;
}; };
query?: never; query?: never;
url: '/api/submissions/crops/{crop_id}'; url: '/api/events/{event_path}/submissions/crops/{crop_id}';
}; };
export type SubmissionsApiDeleteImageCropResponses = { export type SubmissionsApiDeleteImageCropResponses = {
@@ -1642,13 +1777,17 @@ export type SubmissionsApiDeleteImageCropResponse = SubmissionsApiDeleteImageCro
export type SubmissionsApiProcessFileOcrData = { export type SubmissionsApiProcessFileOcrData = {
body?: never; body?: never;
path: { path: {
/**
* Event Path
*/
event_path: string;
/** /**
* File Id * File Id
*/ */
file_id: string; file_id: string;
}; };
query?: never; query?: never;
url: '/api/submissions/files/{file_id}/process-ocr'; url: '/api/events/{event_path}/submissions/files/{file_id}/process-ocr';
}; };
export type SubmissionsApiProcessFileOcrResponses = { export type SubmissionsApiProcessFileOcrResponses = {
@@ -1667,13 +1806,17 @@ export type SubmissionsApiProcessFileOcrResponse = SubmissionsApiProcessFileOcrR
export type SubmissionsApiProcessCropOcrData = { export type SubmissionsApiProcessCropOcrData = {
body?: never; body?: never;
path: { path: {
/**
* Event Path
*/
event_path: string;
/** /**
* Crop Id * Crop Id
*/ */
crop_id: string; crop_id: string;
}; };
query?: never; query?: never;
url: '/api/submissions/crops/{crop_id}/process-ocr'; url: '/api/events/{event_path}/submissions/crops/{crop_id}/process-ocr';
}; };
export type SubmissionsApiProcessCropOcrResponses = { export type SubmissionsApiProcessCropOcrResponses = {
@@ -1691,9 +1834,14 @@ export type SubmissionsApiProcessCropOcrResponse = SubmissionsApiProcessCropOcrR
export type MarketApiListMarketsData = { export type MarketApiListMarketsData = {
body?: never; body?: never;
path?: never; path: {
/**
* Event Path
*/
event_path: string;
};
query?: never; query?: never;
url: '/api/market/'; url: '/api/events/{event_path}/market/';
}; };
export type MarketApiListMarketsResponses = { export type MarketApiListMarketsResponses = {
@@ -1709,9 +1857,14 @@ export type MarketApiListMarketsResponse = MarketApiListMarketsResponses[keyof M
export type MarketApiListUserBetsData = { export type MarketApiListUserBetsData = {
body?: never; body?: never;
path?: never; path: {
/**
* Event Path
*/
event_path: string;
};
query?: never; query?: never;
url: '/api/market/user/bets'; url: '/api/events/{event_path}/market/user/bets';
}; };
export type MarketApiListUserBetsResponses = { export type MarketApiListUserBetsResponses = {
@@ -1728,13 +1881,17 @@ export type MarketApiListUserBetsResponse = MarketApiListUserBetsResponses[keyof
export type MarketApiCloseMarketData = { export type MarketApiCloseMarketData = {
body?: never; body?: never;
path: { path: {
/**
* Event Path
*/
event_path: string;
/** /**
* Market Uuid * Market Uuid
*/ */
market_uuid: string; market_uuid: string;
}; };
query?: never; query?: never;
url: '/api/market/{market_uuid}/actions/close'; url: '/api/events/{event_path}/market/{market_uuid}/actions/close';
}; };
export type MarketApiCloseMarketResponses = { export type MarketApiCloseMarketResponses = {
@@ -1747,13 +1904,17 @@ export type MarketApiCloseMarketResponses = {
export type MarketApiResolveMarketData = { export type MarketApiResolveMarketData = {
body: ResolveMarketSchema; body: ResolveMarketSchema;
path: { path: {
/**
* Event Path
*/
event_path: string;
/** /**
* Market Uuid * Market Uuid
*/ */
market_uuid: string; market_uuid: string;
}; };
query?: never; query?: never;
url: '/api/market/{market_uuid}/actions/resolve'; url: '/api/events/{event_path}/market/{market_uuid}/actions/resolve';
}; };
export type MarketApiResolveMarketResponses = { export type MarketApiResolveMarketResponses = {
@@ -1768,13 +1929,17 @@ export type MarketApiResolveMarketResponse = MarketApiResolveMarketResponses[key
export type MarketApiCreateBetData = { export type MarketApiCreateBetData = {
body: UserBetCreateSchema; body: UserBetCreateSchema;
path: { path: {
/**
* Event Path
*/
event_path: string;
/** /**
* Market Uuid * Market Uuid
*/ */
market_uuid: string; market_uuid: string;
}; };
query?: never; query?: never;
url: '/api/market/{market_uuid}/bets'; url: '/api/events/{event_path}/market/{market_uuid}/bets';
}; };
export type MarketApiCreateBetResponses = { export type MarketApiCreateBetResponses = {
@@ -3,6 +3,7 @@ import { ref, computed, onMounted } from "vue";
import { storeToRefs } from "pinia"; import { storeToRefs } from "pinia";
import { marketApiCreateBet, marketApiCloseMarket, marketApiResolveMarket } from "../api"; import { marketApiCreateBet, marketApiCloseMarket, marketApiResolveMarket } from "../api";
import { useMarketStore } from "../stores/market"; import { useMarketStore } from "../stores/market";
import { useEventPath } from "../composables/useEventPath";
import type { Market, MarketOption } from "../types"; import type { Market, MarketOption } from "../types";
import type { UserBetSchema } from "../api/types.gen"; import type { UserBetSchema } from "../api/types.gen";
@@ -14,6 +15,7 @@ const emit = defineEmits<{
refresh: []; refresh: [];
}>(); }>();
const eventPath = useEventPath();
const marketStore = useMarketStore(); const marketStore = useMarketStore();
const { userInfo, userBets } = storeToRefs(marketStore); const { userInfo, userBets } = storeToRefs(marketStore);
@@ -82,7 +84,7 @@ const closeMarket = async () => {
try { try {
await marketApiCloseMarket({ await marketApiCloseMarket({
path: { market_uuid: props.market.uuid }, path: { event_path: eventPath, market_uuid: props.market.uuid },
}); });
emit("refresh"); emit("refresh");
} catch (e) { } catch (e) {
@@ -103,7 +105,7 @@ const resolveMarket = async () => {
try { try {
await marketApiResolveMarket({ await marketApiResolveMarket({
path: { market_uuid: props.market.uuid }, path: { event_path: eventPath, market_uuid: props.market.uuid },
body: { winning_option_uuid: selectedWinningOption.value }, body: { winning_option_uuid: selectedWinningOption.value },
}); });
emit("refresh"); emit("refresh");
@@ -132,6 +134,7 @@ const placeBet = async () => {
try { try {
const response = await marketApiCreateBet({ const response = await marketApiCreateBet({
path: { path: {
event_path: eventPath,
market_uuid: props.market.uuid market_uuid: props.market.uuid
}, },
body: { body: {
@@ -142,7 +145,7 @@ const placeBet = async () => {
if (!response.error) { if (!response.error) {
// Reload user bets to reflect the new bet // Reload user bets to reflect the new bet
await marketStore.loadUserBets(); await marketStore.loadUserBets(eventPath);
updateExistingBet(); updateExistingBet();
betAmount.value = 0; betAmount.value = 0;
} else { } else {
@@ -0,0 +1,15 @@
import { inject, provide, type InjectionKey } from 'vue'
export const EventPathKey: InjectionKey<string> = Symbol('eventPath')
export function provideEventPath(eventPath: string) {
provide(EventPathKey, eventPath)
}
export function useEventPath(): string {
const eventPath = inject(EventPathKey)
if (!eventPath) {
throw new Error('useEventPath() called without a provided eventPath - is this component mounted under a page that calls provideEventPath()?')
}
return eventPath
}
+18 -2
View File
@@ -37,12 +37,28 @@ interface SubmissionStats {
// API Service Class // API Service Class
export class ApiService { export class ApiService {
private eventPath: string | null = null
// Every endpoint this service calls is event-scoped (/api/events/{event_path}/...),
// so the owning page sets this once (via provideEventPath()'s value) before any
// request is made, instead of threading eventPath through every method signature.
setEventPath(eventPath: string) {
this.eventPath = eventPath
}
private buildUrl(endpoint: string): string {
if (!this.eventPath) {
throw new Error('ApiService.setEventPath() must be called before making requests')
}
return `${API_BASE_URL}/events/${this.eventPath}${endpoint}`
}
private async request<T>( private async request<T>(
endpoint: string, endpoint: string,
options: RequestInit = {} options: RequestInit = {}
): Promise<ApiResponse<T>> { ): Promise<ApiResponse<T>> {
try { try {
const response = await fetch(`${API_BASE_URL}${endpoint}`, { const response = await fetch(this.buildUrl(endpoint), {
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
...options.headers, ...options.headers,
@@ -76,7 +92,7 @@ export class ApiService {
formData: FormData formData: FormData
): Promise<ApiResponse<T>> { ): Promise<ApiResponse<T>> {
try { try {
const response = await fetch(`${API_BASE_URL}${endpoint}`, { const response = await fetch(this.buildUrl(endpoint), {
method: 'POST', method: 'POST',
body: formData, body: formData,
}) })
+10 -10
View File
@@ -25,9 +25,9 @@ export const useMarketStore = defineStore('market', () => {
} }
} }
const loadMarkets = async () => { const loadMarkets = async (eventPath: string) => {
try { try {
const response = await marketApiListMarkets() const response = await marketApiListMarkets({ path: { event_path: eventPath } })
if (response.data) { if (response.data) {
markets.value = response.data as unknown as Market[] markets.value = response.data as unknown as Market[]
} }
@@ -37,9 +37,9 @@ export const useMarketStore = defineStore('market', () => {
} }
} }
const loadUserBets = async () => { const loadUserBets = async (eventPath: string) => {
try { try {
const response = await marketApiListUserBets() const response = await marketApiListUserBets({ path: { event_path: eventPath } })
if (response.data) { if (response.data) {
userBets.value = response.data userBets.value = response.data
} }
@@ -49,30 +49,30 @@ export const useMarketStore = defineStore('market', () => {
} }
} }
const initializeMarketPage = async () => { const initializeMarketPage = async (eventPath: string) => {
isLoading.value = true isLoading.value = true
error.value = '' error.value = ''
try { try {
await Promise.all([ await Promise.all([
loadUserInfo(), loadUserInfo(),
loadMarkets(), loadMarkets(eventPath),
]) ])
// Load user bets if authenticated // Load user bets if authenticated
if (userInfo.value?.is_authenticated) { if (userInfo.value?.is_authenticated) {
await loadUserBets() await loadUserBets(eventPath)
} }
} finally { } finally {
isLoading.value = false isLoading.value = false
} }
} }
const refreshPage = async () => { const refreshPage = async (eventPath: string) => {
await Promise.all([ await Promise.all([
loadUserInfo(), loadUserInfo(),
loadMarkets(), loadMarkets(eventPath),
userInfo.value?.is_authenticated ? loadUserBets() : Promise.resolve(), userInfo.value?.is_authenticated ? loadUserBets(eventPath) : Promise.resolve(),
]) ])
} }
+4 -4
View File
@@ -31,14 +31,14 @@ export const usePuzzlesStore = defineStore('puzzles', () => {
}) })
// Actions // Actions
const loadPuzzles = async () => { const loadPuzzles = async (eventPath: string) => {
if (puzzles.value.length > 0) return // Already loaded if (puzzles.value.length > 0) return // Already loaded
try { try {
isLoading.value = true isLoading.value = true
error.value = '' error.value = ''
const response = await opusMagnumApiListPuzzles() const response = await opusMagnumApiListPuzzles({ path: { event_path: eventPath } })
if (response.error) { if (response.error) {
error.value = String(response.error) error.value = String(response.error)
console.error('Failed to load puzzles:', response.error) console.error('Failed to load puzzles:', response.error)
@@ -56,9 +56,9 @@ export const usePuzzlesStore = defineStore('puzzles', () => {
} }
} }
const refreshPuzzles = async () => { const refreshPuzzles = async (eventPath: string) => {
puzzles.value = [] puzzles.value = []
await loadPuzzles() await loadPuzzles(eventPath)
} }
return { return {
+37 -22
View File
@@ -12,17 +12,23 @@ from .tasks import process_submission_file, process_crop
router = Router() router = Router()
PREFIX = "/events/{event_path}/submissions"
@router.post("/submission/create", response=SubmissionOut)
@router.post(f"{PREFIX}/submission/create", response=SubmissionOut)
def create_submission( def create_submission(
request: HttpRequest, request: HttpRequest,
event_path: str,
game_path: str, game_path: str,
files: List[UploadedFile] = File(...), files: List[UploadedFile] = File(...),
notes: Optional[str] = Form(None), 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( submission = Submission.objects.create(
event=request.event,
game=game, game=game,
user=request.user if request.user.is_authenticated else None, user=request.user if request.user.is_authenticated else None,
notes=notes or None, notes=notes or None,
@@ -39,32 +45,32 @@ def create_submission(
return submission return submission
@router.get("/submission/by/user", response=list[SubmissionOut]) @router.get(f"{PREFIX}/submission/by/user", response=list[SubmissionOut])
def list_user_submissions(request: HttpRequest, game_path: str): def list_user_submissions(request: HttpRequest, event_path: str, game_path: str):
game = get_object_or_404(Game, path=game_path) game = get_object_or_404(Game, path=game_path)
if not request.user.is_authenticated: if not request.user.is_authenticated:
return 403, {"detail": "Authentication required"} 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" "-created_at"
) )
return submissions return submissions
@router.get("/submission/by/game", response=list[SubmissionOut]) @router.get(f"{PREFIX}/submission/by/game", response=list[SubmissionOut])
def list_submissions_by_game(request: HttpRequest, game_path: str): def list_submissions_by_game(request: HttpRequest, event_path: str, game_path: str):
game = get_object_or_404(Game, path=game_path) game = get_object_or_404(Game, path=game_path)
if not request.user.is_staff: if not request.user.is_staff:
return 403, {"detail": "Admin access required"} 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) @router.get(f"{PREFIX}/submission/{{submission_id}}", response=SubmissionOut)
def get_submission(request: HttpRequest, submission_id: UUID): def get_submission(request: HttpRequest, event_path: str, submission_id: UUID):
submission = get_object_or_404(Submission, id=submission_id) submission = get_object_or_404(Submission.for_event, id=submission_id)
if submission.user != request.user and not request.user.is_staff: if submission.user != request.user and not request.user.is_staff:
return 403, {"detail": "Not authorized"} return 403, {"detail": "Not authorized"}
@@ -72,16 +78,19 @@ def get_submission(request: HttpRequest, submission_id: UUID):
return submission 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( def create_image_crop(
request: HttpRequest, request: HttpRequest,
event_path: str,
submission_file_id: UUID, submission_file_id: UUID,
data: ImageCropIn, data: ImageCropIn,
): ):
if not request.user.is_staff: if not request.user.is_staff:
return 403, {"detail": "Admin access required"} 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( crop = ImageCrop.objects.create(
submission_file=submission_file, submission_file=submission_file,
@@ -96,23 +105,27 @@ def create_image_crop(
return crop return crop
@router.delete("/crops/{crop_id}", response=dict) @router.delete(f"{PREFIX}/crops/{{crop_id}}", response=dict)
def delete_image_crop(request: HttpRequest, crop_id: UUID): def delete_image_crop(request: HttpRequest, event_path: str, crop_id: UUID):
if not request.user.is_staff: if not request.user.is_staff:
return 403, {"detail": "Admin access required"} 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() crop.delete()
return {"status": "deleted"} return {"status": "deleted"}
@router.post("/files/{file_id}/process-ocr", response=dict) @router.post(f"{PREFIX}/files/{{file_id}}/process-ocr", response=dict)
def process_file_ocr(request: HttpRequest, file_id: UUID): def process_file_ocr(request: HttpRequest, event_path: str, file_id: UUID):
if not request.user.is_staff: if not request.user.is_staff:
return 403, {"detail": "Admin access required"} 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.ocr_status = OCRStatus.TO_PROCESS
submission_file.save() submission_file.save()
@@ -122,12 +135,14 @@ def process_file_ocr(request: HttpRequest, file_id: UUID):
return {"status": "queued"} return {"status": "queued"}
@router.post("/crops/{crop_id}/process-ocr", response=dict) @router.post(f"{PREFIX}/crops/{{crop_id}}/process-ocr", response=dict)
def process_crop_ocr(request: HttpRequest, crop_id: UUID): def process_crop_ocr(request: HttpRequest, event_path: str, crop_id: UUID):
if not request.user.is_staff: if not request.user.is_staff:
return 403, {"detail": "Admin access required"} 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.ocr_status = OCRStatus.TO_PROCESS
crop.save() 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 import uuid
from accounts.models import CustomUser from accounts.models import CustomUser
from events.models import Event
from events.scoping import EventScopedManager
from games.models import Game from games.models import Game
@@ -36,6 +38,12 @@ class BaseModel(models.Model):
class Submission(BaseModel): class Submission(BaseModel):
event = models.ForeignKey(
Event,
on_delete=models.CASCADE,
related_name="submissions",
)
game = models.ForeignKey( game = models.ForeignKey(
Game, Game,
on_delete=models.CASCADE, on_delete=models.CASCADE,
@@ -60,6 +68,9 @@ class Submission(BaseModel):
help_text="Optional notes about the submission", help_text="Optional notes about the submission",
) )
objects = models.Manager()
for_event = EventScopedManager()
class SubmissionFile(BaseModel): class SubmissionFile(BaseModel):
submission = models.ForeignKey( 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.shortcuts import render, get_object_or_404
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from events.models import Event
from games.models import Game from games.models import Game
@login_required @login_required
def submission_home(request: HttpRequest, game: str): def submission_home(request: HttpRequest, event_path: str, game: str):
game_obj = get_object_or_404(Game, path=f"/submissions/{game}", enabled=True) 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( return render(
request, request,
"submissions.html", "submissions.html",
{ {
"event": event,
"game": game_obj, "game": game_obj,
}, },
) )
+1 -1
View File
@@ -9,5 +9,5 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div id="app" data-market-enabled="{{ market_enabled|lower }}"></div> <div id="app" data-event-path="{{ event.path }}" data-market-enabled="{{ market_enabled|lower }}"></div>
{% endblock %} {% endblock %}
+1 -1
View File
@@ -9,5 +9,5 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div id="app"></div> <div id="app" data-event-path="{{ event.path }}"></div>
{% endblock %} {% endblock %}
+1 -1
View File
@@ -9,5 +9,5 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div id="app"></div> <div id="app" data-event-path="{{ event.path }}"></div>
{% endblock %} {% endblock %}
+1 -1
View File
@@ -9,5 +9,5 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div id="app" data-collection-title="{{ collection.title }}" data-collection-url="{{ collection.url }}" data-collection-description="{{ collection.description }}"></div> <div id="app" data-event-path="{{ event.path }}" data-collection-title="{{ collection.title }}" data-collection-url="{{ collection.url }}" data-collection-description="{{ collection.description }}"></div>
{% endblock %} {% endblock %}
+1 -1
View File
@@ -9,5 +9,5 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div id="app" data-game-path="{{ game.path }}" data-game-name="{{ game.name }}" data-game-image="{{ game.image |default:'' }}" data-game-steam-id="{{ game.steam_app_id|default:'' }}"></div> <div id="app" data-event-path="{{ event.path }}" data-game-path="{{ game.path }}" data-game-name="{{ game.name }}" data-game-image="{{ game.image |default:'' }}" data-game-steam-id="{{ game.steam_app_id|default:'' }}"></div>
{% endblock %} {% endblock %}