Compare commits

..
6 Commits
182 changed files with 9358 additions and 674 deletions
+12
View File
@@ -12,6 +12,18 @@ dist
dist-ssr dist-ssr
*.local *.local
# Vite build output (generated locally via `pnpm build`, served by whitenoise)
static_source/vite/assets
static_source/vite/manifest.json
static_source/vite/vite.svg
tsconfig.tsbuildinfo
# vue-tsc emits a compiled copy of vite.config.ts here (tsconfig.node.json,
# composite project ref) — Vite's config lookup prefers .js over .ts, so a
# committed copy silently shadows every edit to the real config.
/vite.config.js
/vite.config.d.ts
# Editor directories and files # Editor directories and files
.vscode/* .vscode/*
!.vscode/extensions.json !.vscode/extensions.json
+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.
+39 -19
View File
@@ -1,28 +1,48 @@
from django.contrib import admin from django.contrib import admin, messages
from .models import Game from submissions.tasks import sync_event_game_scopes_task
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", "has_apibot_token"]
list_filter = ["event", "enabled"]
search_fields = ["game__name", "event__name"]
actions = ["sync_scopes"]
@admin.display(description="AAndT token", boolean=True)
def has_apibot_token(self, obj):
return bool(obj.apibot_token)
@admin.action(description="Sync scopes from AAndT for selected EventGames")
def sync_scopes(self, request, queryset):
tokened = queryset.exclude(apibot_token__isnull=True).exclude(apibot_token="")
skipped = queryset.count() - tokened.count()
for event_game in tokened:
sync_event_game_scopes_task.enqueue(event_game.id)
self.message_user(
request, f"Queued scope sync for {tokened.count()} EventGame(s)."
)
if skipped:
self.message_user(
request,
f"Skipped {skipped} EventGame(s) with no AAndT token configured.",
level=messages.WARNING,
)
+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",
),
]
@@ -0,0 +1,23 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("games", "0006_remove_game_enabled"),
]
operations = [
migrations.AddField(
model_name="eventgame",
name="apibot_token",
field=models.CharField(
blank=True,
help_text=(
"Bearer token for the legacy PolyLAN AAndT API (from the AAndT's "
"'apibot' admin page), used to auto-sync Scopes and their assigned users."
),
max_length=512,
null=True,
),
),
]
@@ -0,0 +1,17 @@
# Generated by Django 6.0.5 on 2026-09-16 00:09
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("games", "0007_eventgame_apibot_token"),
]
operations = [
migrations.AddField(
model_name="game",
name="custom_path",
field=models.BooleanField(default=False),
),
]
+45 -1
View File
@@ -1,16 +1,25 @@
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)
custom_path = models.BooleanField(default=False)
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 +27,38 @@ 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)
apibot_token = models.CharField(
max_length=512,
blank=True,
null=True,
help_text=(
"Bearer token for the legacy PolyLAN AAndT API (from the AAndT's "
"'apibot' admin page), used to auto-sync Scopes and their assigned users."
),
)
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'})"
+1
View File
@@ -8,4 +8,5 @@ class GameOut(Schema):
steam_app_id: Optional[int] = None steam_app_id: Optional[int] = None
name: str name: str
path: str path: str
custom_path: bool
image: Optional[str] = None image: Optional[str] = None
+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",
] ]
@@ -147,6 +149,14 @@ CAS_SERVER_URL = "https://polylan.ch/cas/"
# Steam API Configuration # Steam API Configuration
STEAM_API_KEY = os.environ.get("STEAM_API_KEY", None) # Set via environment variable STEAM_API_KEY = os.environ.get("STEAM_API_KEY", None) # Set via environment variable
# Base URL of the legacy PolyLAN site, used to sync AAndT matches/members
# (see submissions.services.aandt_sync)
AANDT_API_BASE_URL = os.environ.get("AANDT_API_BASE_URL", "https://polylan.ch")
# Base URL of this site, used to build submission links pushed back to the
# legacy AAndT match messages (see submissions.services.aandt_sync)
SUBMITTER_BASE_URL = os.environ.get("SUBMITTER_BASE_URL", "http://127.0.0.1:7777")
# File Upload Settings # File Upload Settings
MEDIA_URL = "/media/" MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "media" MEDIA_ROOT = BASE_DIR / "media"
@@ -240,6 +250,7 @@ STORAGES = {
} }
SENTRY_ENVIRONMENT = "production"
from polylan_submitter.settingsLocal import * # noqa from polylan_submitter.settingsLocal import * # noqa
@@ -250,4 +261,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,
) )
+38 -12
View File
@@ -16,16 +16,17 @@ 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, submission_view
from .api import api from .api import api
NOITA_APP_ID = 881100 NOITA_APP_ID = 881100
@@ -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,20 @@ 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/<slug:game_path>",
submission_home,
name="submissions.home",
),
path(
"<slug:event_path>/submissions/<slug:game_path>/<uuid:scope_id>",
submission_view,
name="submissions.view",
),
path("<slug:event_path>/", event_home, name="event.home"),
path("", home, name="home"), path("", home, name="home"),
] ]
+2 -4
View File
@@ -7,6 +7,7 @@ import requests
from django.conf import settings from django.conf import settings
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.auth.backends import BaseBackend from django.contrib.auth.backends import BaseBackend
import sentry_sdk
class SimpleCASBackend(BaseBackend): class SimpleCASBackend(BaseBackend):
@@ -23,9 +24,6 @@ class SimpleCASBackend(BaseBackend):
if not cas_user_id: if not cas_user_id:
return None return None
print(f"CAS User ID: {cas_user_id}")
print(f"CAS Attributes: {attributes}")
User = get_user_model() User = get_user_model()
# Try to find user by CAS user ID first, then by username # Try to find user by CAS user ID first, then by username
@@ -80,7 +78,7 @@ class SimpleCASBackend(BaseBackend):
return None, None return None, None
except Exception as e: except Exception as e:
print(f"CAS validation error: {e}") sentry_sdk.capture_exception(e)
return None, None return None, None
def get_user(self, user_id): def get_user(self, user_id):
+36
View File
@@ -0,0 +1,36 @@
<script setup lang="ts">
interface Props {
code?: string;
title?: string;
message?: string;
}
withDefaults(defineProps<Props>(), {
code: "500",
title: "Something went wrong",
message: "An unexpected error occurred.",
});
const goBack = () => window.history.back();
</script>
<template>
<div class="min-h-screen bg-base-300 flex items-center justify-center px-4">
<div class="w-full max-w-xl text-center">
<i class="mdi mdi-alert-circle-outline text-7xl text-primary"></i>
<h1 class="text-7xl font-bold mt-4">{{ code }}</h1>
<h2 class="text-2xl font-semibold mt-2">{{ title }}</h2>
<p class="text-base-content/70 mt-4">{{ message }}</p>
<div class="mt-8 flex items-center justify-center gap-4">
<a href="/" class="btn btn-primary">
<i class="mdi mdi-home mr-2"></i>
Go home
</a>
<button @click="goBack" class="btn btn-ghost">
<i class="mdi mdi-arrow-left mr-2"></i>
Go back
</button>
</div>
</div>
</div>
</template>
+12 -8
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;
} }
@@ -88,7 +91,8 @@ onMounted(async () => {
</div> </div>
<!-- Game Cards --> <!-- Game Cards -->
<div v-for="game in games" :key="game.id" @click="navigate(game.path)" <div v-for="game in games" :key="game.id"
@click="navigate(game.custom_path ? `/${game.path}` : `/submissions/${game.path}`)"
class="card card-xl bg-base-200 shadow-xl hover:shadow-2xl transition-all cursor-pointer transform hover:-translate-y-2 hover:scale-[1.05] hover:bg-base-100 overflow-hidden"> class="card card-xl bg-base-200 shadow-xl hover:shadow-2xl transition-all cursor-pointer transform hover:-translate-y-2 hover:scale-[1.05] hover:bg-base-100 overflow-hidden">
<figure class="relative h-60 bg-base-300 overflow-hidden"> <figure class="relative h-60 bg-base-300 overflow-hidden">
<img v-if="getHeaderImage(game) && !imageErrors.has(game.id)" :src="getHeaderImage(game)!" :alt="game.name" <img v-if="getHeaderImage(game) && !imageErrors.has(game.id)" :src="getHeaderImage(game)!" :alt="game.name"
+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>
+10 -4
View File
@@ -14,6 +14,12 @@ import {
type SortingState, type SortingState,
} from "@tanstack/vue-table"; } from "@tanstack/vue-table";
interface Props {
eventPath: string;
}
const props = defineProps<Props>();
const noitaStore = useNoitaStore(); const noitaStore = useNoitaStore();
const { userInfo, objectives, leaderboard, isLoadingLeaderboard, isUploading } = storeToRefs(noitaStore); const { userInfo, objectives, leaderboard, isLoadingLeaderboard, isUploading } = storeToRefs(noitaStore);
@@ -135,7 +141,7 @@ const submitRun = async () => {
if (uploadedFiles.value.length === 0) return; if (uploadedFiles.value.length === 0) return;
try { try {
await noitaStore.submitRun(uploadedFiles.value); await noitaStore.submitRun(props.eventPath, uploadedFiles.value);
uploadedFiles.value = []; uploadedFiles.value = [];
alert("Run submitted successfully!"); alert("Run submitted successfully!");
} catch (error) { } catch (error) {
@@ -145,12 +151,12 @@ const submitRun = async () => {
}; };
const goHome = () => { const goHome = () => {
window.location.href = "/"; window.location.href = `/${props.eventPath}/`;
}; };
const clearCache = async () => { const clearCache = async () => {
try { try {
await noitaStore.clearCache(); await noitaStore.clearCache(props.eventPath);
alert("Cache cleared successfully!"); alert("Cache cleared successfully!");
} catch (error) { } catch (error) {
console.error("Error clearing cache:", error); console.error("Error clearing cache:", error);
@@ -159,7 +165,7 @@ const clearCache = async () => {
}; };
onMounted(() => { onMounted(() => {
noitaStore.loadUserData(); noitaStore.loadUserData(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>
+33 -33
View File
@@ -4,21 +4,28 @@ 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 {
gamePath?: string; eventPath: string;
scopeId: string;
scopeName?: string;
gameName?: string; gameName?: string;
gamePath?: string;
gameImage?: string; gameImage?: string;
gameSteamId?: string; gameSteamId?: string;
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
gamePath: "", scopeName: "Match",
gameName: "Game", gameName: "Game",
gamePath: "",
gameImage: "", gameImage: "",
gameSteamId: "", gameSteamId: "",
}); });
provideEventPath(props.eventPath);
const userStore = useUserStore(); const userStore = useUserStore();
const { userInfo } = storeToRefs(userStore); const { userInfo } = storeToRefs(userStore);
@@ -238,7 +245,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 +277,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 +300,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 +312,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,7 +405,8 @@ const submitSubmission = async () => {
isUploading.value = true; isUploading.value = true;
try { try {
await submissionsApiCreateSubmission({ await submissionsApiCreateSubmission({
query: { game_path: props.gamePath }, path: { event_path: props.eventPath },
query: { scope_id: props.scopeId },
body: { body: {
files: uploadedFiles.value, files: uploadedFiles.value,
notes: notes.value || null, notes: notes.value || null,
@@ -418,7 +426,7 @@ const submitSubmission = async () => {
}; };
const goHome = () => { const goHome = () => {
window.location.href = "/"; window.location.href = `/${props.eventPath}/submissions/${props.gamePath}`;
}; };
const formatFileSize = (bytes: number) => { const formatFileSize = (bytes: number) => {
@@ -449,12 +457,11 @@ const getOCRStatusColor = (status: string) => {
}; };
const loadUserSubmissions = async () => { const loadUserSubmissions = async () => {
if (!props.gamePath) return;
isLoadingSubmissions.value = true; isLoadingSubmissions.value = true;
try { try {
const { data } = await submissionsApiListUserSubmissions({ const { data } = await submissionsApiListUserSubmissions({
query: { game_path: props.gamePath }, path: { event_path: props.eventPath },
query: { scope_id: props.scopeId },
}); });
if (data && Array.isArray(data)) { if (data && Array.isArray(data)) {
userSubmissions.value = data; userSubmissions.value = data;
@@ -516,7 +523,7 @@ onUnmounted(() => {
<i class="mdi mdi-arrow-left"></i> <i class="mdi mdi-arrow-left"></i>
Back Back
</button> </button>
<h1 class="text-xl font-bold">{{ gameName }} Submissions</h1> <h1 class="text-xl font-bold">{{ gameName }} Submissions &mdash; {{ scopeName }}</h1>
<div class="flex-1"></div> <div class="flex-1"></div>
<!-- WebSocket Status Indicator --> <!-- WebSocket Status Indicator -->
@@ -549,7 +556,8 @@ onUnmounted(() => {
</figure> </figure>
<div class="card-body"> <div class="card-body">
<h2 class="card-title text-2xl">{{ gameName }}</h2> <h2 class="card-title text-2xl">{{ gameName }}</h2>
<p class="text-base-content/70">Submit your best results for this game</p> <p class="text-base-content/70">Submitting for match: <span class="font-semibold">{{ scopeName }}</span>
</p>
</div> </div>
</div> </div>
@@ -562,10 +570,11 @@ onUnmounted(() => {
</h2> </h2>
<!-- Upload Area --> <!-- Upload Area -->
<div @dragover="handleDragover" @dragleave="handleDragleave" @drop="handleDrop" @mouseenter="isUploadAreaHovered = true" @mouseleave="isUploadAreaHovered = false" :class="[ <div @dragover="handleDragover" @dragleave="handleDragleave" @drop="handleDrop"
'border-2 border-dashed rounded-lg p-8 text-center transition-colors cursor-pointer bg-base-200/50 mb-6', @mouseenter="isUploadAreaHovered = true" @mouseleave="isUploadAreaHovered = false" :class="[
isDragover ? 'border-primary bg-primary/10' : 'border-base-300 hover:border-primary' 'border-2 border-dashed rounded-lg p-8 text-center transition-colors cursor-pointer bg-base-200/50 mb-6',
]"> isDragover ? 'border-primary bg-primary/10' : 'border-base-300 hover:border-primary'
]">
<input type="file" multiple @change="handleFileUpload" class="hidden" id="file-upload" /> <input type="file" multiple @change="handleFileUpload" class="hidden" id="file-upload" />
<label for="file-upload" class="cursor-pointer flex flex-col items-center gap-3"> <label for="file-upload" class="cursor-pointer flex flex-col items-center gap-3">
<i :class="[ <i :class="[
@@ -619,7 +628,7 @@ onUnmounted(() => {
<span class="label-text-alt">0/500</span> <span class="label-text-alt">0/500</span>
</label> </label>
<textarea class="flex textarea textarea-bordered h-24 w-full resize-none" <textarea v-model="notes" class="flex textarea textarea-bordered h-24 w-full resize-none"
placeholder="Add any notes special for this upload" maxlength="500"></textarea> placeholder="Add any notes special for this upload" maxlength="500"></textarea>
</div> </div>
</div> </div>
@@ -693,15 +702,6 @@ onUnmounted(() => {
processed. processed.
</p> </p>
</div> </div>
<div class="divider"></div>
<div class="bg-base-200 p-4 rounded-lg">
<p class="text-base-content/70 text-sm">
<i class="mdi mdi-lightbulb-on mr-2 text-warning"></i>
Pro tip: Include clear screenshots or documentation with your submission for faster verification.
</p>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -919,8 +919,7 @@ onUnmounted(() => {
<p class="text-base-content/70">OCR processing failed</p> <p class="text-base-content/70">OCR processing failed</p>
</div> </div>
</div> </div>
<div v-else-if="!selectedSubmissionFile?.ocr_raw_data" <div v-else-if="!selectedSubmissionFile?.ocr_raw_data" class="flex items-center justify-center py-8">
class="flex items-center justify-center py-8">
<div class="text-center"> <div class="text-center">
<i class="mdi mdi-clock-outline text-4xl text-base-content/30 mb-2"></i> <i class="mdi mdi-clock-outline text-4xl text-base-content/30 mb-2"></i>
<p class="text-base-content/70">Waiting for OCR processing...</p> <p class="text-base-content/70">Waiting for OCR processing...</p>
@@ -928,9 +927,9 @@ onUnmounted(() => {
</div> </div>
<!-- Crops OCR Data --> <!-- Crops OCR Data -->
<div v-if="selectedSubmissionFile?.crops?.length" <div v-if="selectedSubmissionFile?.crops?.length" class="space-y-3 pt-4 border-t border-base-300">
class="space-y-3 pt-4 border-t border-base-300"> <p class="text-xs font-semibold text-base-content/70">Crop OCR Results ({{
<p class="text-xs font-semibold text-base-content/70">Crop OCR Results ({{ selectedSubmissionFile.crops.length }}):</p> selectedSubmissionFile.crops.length }}):</p>
<div v-for="crop in selectedSubmissionFile.crops" :key="crop.id" class="space-y-2"> <div v-for="crop in selectedSubmissionFile.crops" :key="crop.id" class="space-y-2">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<p class="text-xs font-medium">{{ crop.label || `Crop (${crop.x1}, ${crop.y1})` }}</p> <p class="text-xs font-medium">{{ crop.label || `Crop (${crop.x1}, ${crop.y1})` }}</p>
@@ -940,7 +939,8 @@ onUnmounted(() => {
class="bg-base-100 p-2 rounded text-xs whitespace-pre-wrap break-words font-mono max-h-32 overflow-y-auto"> class="bg-base-100 p-2 rounded text-xs whitespace-pre-wrap break-words font-mono max-h-32 overflow-y-auto">
{{ crop.ocr_raw_data }} {{ crop.ocr_raw_data }}
</div> </div>
<div v-else-if="crop.ocr_status === 'processing'" class="text-xs text-base-content/70 text-center py-2"> <div v-else-if="crop.ocr_status === 'processing'"
class="text-xs text-base-content/70 text-center py-2">
<span class="loading loading-spinner loading-sm inline-block"></span> <span class="loading loading-spinner loading-sm inline-block"></span>
Processing... Processing...
</div> </div>
@@ -0,0 +1,54 @@
<script setup lang="ts">
interface ScopeOption {
id: string;
name: string;
}
interface Props {
eventPath: string;
gamePath: string;
gameName?: string;
scopes?: ScopeOption[];
}
const props = withDefaults(defineProps<Props>(), {
gameName: "Game",
scopes: () => [],
});
const goToScope = (scopeId: string) => {
window.location.href = `/${props.eventPath}/submissions/${props.gamePath}/${scopeId}`;
};
const goHome = () => {
window.location.href = `/${props.eventPath}`;
};
</script>
<template>
<div class="min-h-screen bg-base-200 flex items-center justify-center p-4">
<div class="card bg-base-100 shadow-lg w-full max-w-md">
<div class="card-body">
<h2 class="card-title">Select your match</h2>
<p class="text-base-content/70 mb-2">
You're assigned to multiple matches for {{ gameName }}. Pick which one you want to submit for.
</p>
<div class="flex flex-col gap-2 mt-2">
<button v-for="scope in scopes" :key="scope.id" @click="goToScope(scope.id)"
class="btn btn-outline justify-start">
<i class="mdi mdi-sword-cross mr-2"></i>
{{ scope.name }}
</button>
</div>
<div class="card-actions mt-4">
<button @click="goHome" class="btn btn-ghost btn-sm">
<i class="mdi mdi-arrow-left"></i>
Back
</button>
</div>
</div>
</div>
</div>
</template>
+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, submissionsApiListMyScopes, 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, ScopeOut, SteamCollectionItemOut, SteamCollectionOut, SubmissionFileOut, SubmissionIn, SubmissionOut, SubmissionsApiCreateImageCropData, SubmissionsApiCreateImageCropResponse, SubmissionsApiCreateImageCropResponses, SubmissionsApiCreateSubmissionData, SubmissionsApiCreateSubmissionError, SubmissionsApiCreateSubmissionErrors, SubmissionsApiCreateSubmissionResponse, SubmissionsApiCreateSubmissionResponses, SubmissionsApiDeleteImageCropData, SubmissionsApiDeleteImageCropResponse, SubmissionsApiDeleteImageCropResponses, SubmissionsApiGetSubmissionData, SubmissionsApiGetSubmissionResponse, SubmissionsApiGetSubmissionResponses, SubmissionsApiListMyScopesData, SubmissionsApiListMyScopesError, SubmissionsApiListMyScopesErrors, SubmissionsApiListMyScopesResponse, SubmissionsApiListMyScopesResponses, SubmissionsApiListSubmissionsByGameData, SubmissionsApiListSubmissionsByGameResponse, SubmissionsApiListSubmissionsByGameResponses, SubmissionsApiListUserSubmissionsData, SubmissionsApiListUserSubmissionsError, SubmissionsApiListUserSubmissionsErrors, SubmissionsApiListUserSubmissionsResponse, SubmissionsApiListUserSubmissionsResponses, SubmissionsApiProcessCropOcrData, SubmissionsApiProcessCropOcrResponse, SubmissionsApiProcessCropOcrResponses, SubmissionsApiProcessFileOcrData, SubmissionsApiProcessFileOcrResponse, SubmissionsApiProcessFileOcrResponses, TournamentPuzzleResultsOut, TournamentSubmissionsOut, UserBetCreateSchema, UserBetSchema, UserDisplayOut, UserInfoOut, ValidationIn, WinnerFileOut, WinnerResponseOut } from './types.gen';
+45 -35
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, SubmissionsApiCreateSubmissionErrors, SubmissionsApiCreateSubmissionResponses, SubmissionsApiDeleteImageCropData, SubmissionsApiDeleteImageCropResponses, SubmissionsApiGetSubmissionData, SubmissionsApiGetSubmissionResponses, SubmissionsApiListMyScopesData, SubmissionsApiListMyScopesErrors, SubmissionsApiListMyScopesResponses, SubmissionsApiListSubmissionsByGameData, SubmissionsApiListSubmissionsByGameResponses, SubmissionsApiListUserSubmissionsData, SubmissionsApiListUserSubmissionsErrors, 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,21 @@ 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 });
/**
* List My Scopes
*/
export const submissionsApiListMyScopes = <ThrowOnError extends boolean = false>(options: Options<SubmissionsApiListMyScopesData, ThrowOnError>) => (options.client ?? client).get<SubmissionsApiListMyScopesResponses, SubmissionsApiListMyScopesErrors, ThrowOnError>({ url: '/api/events/{event_path}/submissions/scopes/mine', ...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, SubmissionsApiCreateSubmissionErrors, 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 +217,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, SubmissionsApiListUserSubmissionsErrors, 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 +244,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 +283,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 +297,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',
+328 -70
View File
@@ -52,6 +52,36 @@ 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;
/**
* Custom Path
*/
custom_path: boolean;
/**
* Image
*/
image?: string | null;
};
/** /**
* PuzzlePointsFactorOut * PuzzlePointsFactorOut
* *
@@ -386,9 +416,9 @@ export type SubmissionOut = {
*/ */
id: string; id: string;
/** /**
* Game Id * Scope Id
*/ */
game_id?: number | null; scope_id: string;
/** /**
* Notes * Notes
*/ */
@@ -891,29 +921,25 @@ export type NoitaSubmissionOut = {
}; };
/** /**
* GameOut * ScopeOut
*/ */
export type GameOut = { export type ScopeOut = {
/** /**
* Id * Id
*/ */
id: number; id: string;
/**
* Steam App Id
*/
steam_app_id?: number | null;
/** /**
* Name * Name
*/ */
name: string; name: string;
/** /**
* Path * Is Active
*/ */
path: string; is_active: boolean;
/** /**
* Image * Created At
*/ */
image?: string | null; created_at: string;
}; };
/** /**
@@ -1102,7 +1128,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 +1160,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 +1203,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 +1224,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 +1240,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 +1263,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 +1285,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 +1308,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 +1333,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 +1358,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 +1382,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 +1406,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 +1430,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 +1449,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 +1470,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 +1496,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 +1522,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 +1543,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 +1572,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 +1604,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 +1625,48 @@ export type GamesApiListGamesResponses = {
200: Array<GameOut>; 200: Array<GameOut>;
}; };
export type GamesApiListGamesResponse = GamesApiListGamesResponses[keyof GamesApiListGamesResponses]; export type GamesApiListEventGamesResponse = GamesApiListEventGamesResponses[keyof GamesApiListEventGamesResponses];
export type SubmissionsApiListMyScopesData = {
body?: never;
path: {
/**
* Event Path
*/
event_path: string;
};
query: {
/**
* Game Path
*/
game_path: string;
};
url: '/api/events/{event_path}/submissions/scopes/mine';
};
export type SubmissionsApiListMyScopesErrors = {
/**
* Response
*
* Forbidden
*/
403: {
[key: string]: unknown;
};
};
export type SubmissionsApiListMyScopesError = SubmissionsApiListMyScopesErrors[keyof SubmissionsApiListMyScopesErrors];
export type SubmissionsApiListMyScopesResponses = {
/**
* Response
*
* OK
*/
200: Array<ScopeOut>;
};
export type SubmissionsApiListMyScopesResponse = SubmissionsApiListMyScopesResponses[keyof SubmissionsApiListMyScopesResponses];
export type SubmissionsApiCreateSubmissionData = { export type SubmissionsApiCreateSubmissionData = {
/** /**
@@ -1507,16 +1682,34 @@ export type SubmissionsApiCreateSubmissionData = {
*/ */
notes?: string | null; notes?: string | null;
}; };
path?: never; path: {
/**
* Event Path
*/
event_path: string;
};
query: { query: {
/** /**
* Game Path * Scope Id
*/ */
game_path: string; scope_id: string;
}; };
url: '/api/submissions/submission/create'; url: '/api/events/{event_path}/submissions/submission/create';
}; };
export type SubmissionsApiCreateSubmissionErrors = {
/**
* Response
*
* Forbidden
*/
403: {
[key: string]: unknown;
};
};
export type SubmissionsApiCreateSubmissionError = SubmissionsApiCreateSubmissionErrors[keyof SubmissionsApiCreateSubmissionErrors];
export type SubmissionsApiCreateSubmissionResponses = { export type SubmissionsApiCreateSubmissionResponses = {
/** /**
* OK * OK
@@ -1528,16 +1721,34 @@ 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 * Scope Id
*/ */
game_path: string; scope_id: string;
}; };
url: '/api/submissions/submission/by/user'; url: '/api/events/{event_path}/submissions/submission/by/user';
}; };
export type SubmissionsApiListUserSubmissionsErrors = {
/**
* Response
*
* Forbidden
*/
403: {
[key: string]: unknown;
};
};
export type SubmissionsApiListUserSubmissionsError = SubmissionsApiListUserSubmissionsErrors[keyof SubmissionsApiListUserSubmissionsErrors];
export type SubmissionsApiListUserSubmissionsResponses = { export type SubmissionsApiListUserSubmissionsResponses = {
/** /**
* Response * Response
@@ -1551,14 +1762,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 +1791,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 +1816,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 +1841,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 +1870,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 +1899,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 +1927,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 +1950,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 +1974,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 +1997,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 +2022,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 {
+25 -23
View File
@@ -1,6 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from "vue"; import { ref, onMounted } from "vue";
import RankBadge from "./RankBadge.vue"; import RankBadge from "./RankBadge.vue";
import { animationsApiResults, polylanSubmitterApiClearCache, polylanSubmitterApiGetUserInfo } from "@/api";
import { useEventPath } from "@/composables/useEventPath";
const eventPath = useEventPath();
interface User { interface User {
id: number; id: number;
@@ -65,10 +69,10 @@ const userInfo = ref({
const fetchResults = async () => { const fetchResults = async () => {
isLoading.value = true; isLoading.value = true;
try { try {
const response = await fetch("/api/results/results"); const response = await animationsApiResults({ path: { event_path: eventPath } });
if (!response.ok) throw new Error("Failed to fetch results"); if (response.error || !response.data) throw new Error("Failed to fetch results");
resultsData.value = await response.json(); resultsData.value = response.data as unknown as ResultsData;
} catch (error) { } catch (error) {
console.error("Error fetching results:", error); console.error("Error fetching results:", error);
} finally { } finally {
@@ -117,16 +121,16 @@ const togglePuzzleExpanded = (puzzleId: number) => {
const clearCache = async () => { const clearCache = async () => {
try { try {
const response = await fetch("/api/cache/clear", { const response = await polylanSubmitterApiClearCache({
method: "POST", query: { event_path: eventPath },
}); });
if (response.ok) { if (!response.error) {
alert("Cache cleared successfully!"); alert("Cache cleared successfully!");
await fetchResults(); await fetchResults();
} else { } else {
const error = await response.json(); const detail = (response.error as { detail?: string })?.detail;
alert(`Error clearing cache: ${error.detail || "Unknown error"}`); alert(`Error clearing cache: ${detail || "Unknown error"}`);
} }
} catch (error) { } catch (error) {
console.error("Error clearing cache:", error); console.error("Error clearing cache:", error);
@@ -136,24 +140,22 @@ const clearCache = async () => {
const loadUserData = async () => { const loadUserData = async () => {
try { try {
const response = await fetch("/api/user"); const response = await polylanSubmitterApiGetUserInfo();
if (response.ok) { const user = response.data;
const user = await response.json(); if (user?.is_authenticated) {
if (user.is_authenticated) { userInfo.value.username = user.username ?? "";
userInfo.value.username = user.username; userInfo.value.isStaff = user.is_staff || false;
userInfo.value.isStaff = user.is_staff || false;
await fetchResults(); await fetchResults();
// Calculate user's rank and stats // Calculate user's rank and stats
const ranking = getOverallRanking(); const ranking = getOverallRanking();
const userRankIndex = ranking.findIndex((u) => u.user.id === user.id); const userRankIndex = ranking.findIndex((u) => u.user.id === user.id);
if (userRankIndex !== -1) { if (userRankIndex !== -1) {
userInfo.value.rank = userRankIndex + 1; userInfo.value.rank = userRankIndex + 1;
userInfo.value.totalPoints = ranking[userRankIndex].totalPoints; userInfo.value.totalPoints = ranking[userRankIndex].totalPoints;
userInfo.value.puzzlesSolved = ranking[userRankIndex].puzzlesSolved; userInfo.value.puzzlesSolved = ranking[userRankIndex].puzzlesSolved;
}
} }
} }
} catch (error) { } catch (error) {
@@ -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
}
@@ -3,7 +3,7 @@ import { useWebSocket } from '@vueuse/core'
interface SubmissionUpdate { interface SubmissionUpdate {
id: string id: string
game_id?: number scope_id?: string
notes?: string notes?: string
files?: SubmissionFileUpdate[] files?: SubmissionFileUpdate[]
created_at: string created_at: string
+8
View File
@@ -0,0 +1,8 @@
import { createApp } from 'vue'
import ErrorPage from '@/ErrorPage.vue'
import '@/style.css'
const selector = "#app"
const mountData = document.querySelector<HTMLElement>(selector)
const app = createApp(ErrorPage, { ...mountData?.dataset })
app.mount(selector)
+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(),
]) ])
} }
+39 -40
View File
@@ -1,5 +1,12 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { ref } from 'vue' import { ref } from 'vue'
import {
noitaApiGetResults,
noitaApiGetLeaderboard,
noitaApiSubmitLogFile,
polylanSubmitterApiGetUserInfo,
polylanSubmitterApiClearCache,
} from '@/api'
import type { ObjectivResultOut, LeaderboardEntryOut } from '@/api/types.gen' import type { ObjectivResultOut, LeaderboardEntryOut } from '@/api/types.gen'
export type Objective = ObjectivResultOut export type Objective = ObjectivResultOut
@@ -30,12 +37,12 @@ export const useNoitaStore = defineStore('noita', () => {
const error = ref<string>('') const error = ref<string>('')
// Actions // Actions
const fetchUserResults = async () => { const fetchUserResults = async (eventPath: string) => {
try { try {
const response = await fetch('/api/noita/results') const response = await noitaApiGetResults({ path: { event_path: eventPath } })
if (!response.ok) throw new Error('Failed to fetch results') if (response.error || !response.data) throw new Error('Failed to fetch results')
const results = await response.json() const results = response.data
userInfo.value.score = results.total_score userInfo.value.score = results.total_score
userInfo.value.deathsCount = results.deaths_count userInfo.value.deathsCount = results.deaths_count
userInfo.value.runsSubmitted = results.objectives.length userInfo.value.runsSubmitted = results.objectives.length
@@ -46,14 +53,13 @@ export const useNoitaStore = defineStore('noita', () => {
} }
} }
const fetchLeaderboard = async () => { const fetchLeaderboard = async (eventPath: string) => {
isLoadingLeaderboard.value = true isLoadingLeaderboard.value = true
try { try {
const response = await fetch('/api/noita/leaderboard') const response = await noitaApiGetLeaderboard({ path: { event_path: eventPath } })
if (!response.ok) throw new Error('Failed to fetch leaderboard') if (response.error || !response.data) throw new Error('Failed to fetch leaderboard')
const data = await response.json() leaderboard.value = response.data.leaderboard
leaderboard.value = data.leaderboard
// Find current user's rank // Find current user's rank
const userRank = leaderboard.value.find( const userRank = leaderboard.value.find(
@@ -73,48 +79,41 @@ export const useNoitaStore = defineStore('noita', () => {
} }
} }
const loadUserData = async () => { const loadUserData = async (eventPath: string) => {
try { try {
const response = await fetch('/api/user') const response = await polylanSubmitterApiGetUserInfo()
if (response.ok) { if (response.data?.is_authenticated) {
const user = await response.json() userInfo.value.username = response.data.username ?? ''
if (user.is_authenticated) { userInfo.value.isStaff = response.data.is_staff || false
userInfo.value.username = user.username
userInfo.value.isStaff = user.is_staff || false
}
} }
} catch (err) { } catch (err) {
console.error('Error fetching user info:', err) console.error('Error fetching user info:', err)
} }
await Promise.all([fetchUserResults(), fetchLeaderboard()]) await Promise.all([fetchUserResults(eventPath), fetchLeaderboard(eventPath)])
} }
const submitRun = async (files: File[]) => { const submitRun = async (eventPath: string, files: File[]) => {
if (files.length === 0) return if (files.length === 0) return
isUploading.value = true isUploading.value = true
try { try {
for (const file of files) { for (const file of files) {
const formData = new FormData() const response = await noitaApiSubmitLogFile({
formData.append('file', file) path: { event_path: eventPath },
body: { file },
const response = await fetch('/api/noita/submit', {
method: 'POST',
body: formData,
}) })
if (!response.ok) { if (response.error) {
const errorData = await response.json() const detail = (response.error as { detail?: string })?.detail
throw new Error(errorData.detail || 'Unknown error') throw new Error(detail || 'Unknown error')
} }
const result = await response.json() console.log('Submission successful:', response.data)
console.log('Submission successful:', result)
} }
// Refresh objectives, score, and rank after successful submission // Refresh objectives, score, and rank after successful submission
await Promise.all([fetchUserResults(), fetchLeaderboard()]) await Promise.all([fetchUserResults(eventPath), fetchLeaderboard(eventPath)])
} catch (err) { } catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Unknown error' const errorMessage = err instanceof Error ? err.message : 'Unknown error'
error.value = `Error submitting run: ${errorMessage}` error.value = `Error submitting run: ${errorMessage}`
@@ -124,18 +123,18 @@ export const useNoitaStore = defineStore('noita', () => {
} }
} }
const clearCache = async () => { const clearCache = async (eventPath: string) => {
try { try {
const response = await fetch('/api/cache/clear', { const response = await polylanSubmitterApiClearCache({
method: 'POST', query: { event_path: eventPath },
}) })
if (!response.ok) { if (response.error) {
const errorData = await response.json() const detail = (response.error as { detail?: string })?.detail
throw new Error(errorData.detail || 'Unknown error') throw new Error(detail || 'Unknown error')
} }
await Promise.all([fetchUserResults(), fetchLeaderboard()]) await Promise.all([fetchUserResults(eventPath), fetchLeaderboard(eventPath)])
} catch (err) { } catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Unknown error' const errorMessage = err instanceof Error ? err.message : 'Unknown error'
error.value = `Error clearing cache: ${errorMessage}` error.value = `Error clearing cache: ${errorMessage}`
@@ -143,8 +142,8 @@ export const useNoitaStore = defineStore('noita', () => {
} }
} }
const refreshData = async () => { const refreshData = async (eventPath: string) => {
await Promise.all([fetchUserResults(), fetchLeaderboard()]) await Promise.all([fetchUserResults(eventPath), fetchLeaderboard(eventPath)])
} }
return { return {
+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 {
+4 -3
View File
@@ -1,5 +1,6 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { ref } from 'vue' import { ref } from 'vue'
import { polylanSubmitterApiGetUserInfo } from '@/api'
interface UserInfo { interface UserInfo {
username: string username: string
@@ -19,9 +20,9 @@ export const useUserStore = defineStore('user', () => {
const loadUserData = async () => { const loadUserData = async () => {
isLoading.value = true isLoading.value = true
try { try {
const response = await fetch('/api/user') const response = await polylanSubmitterApiGetUserInfo()
if (response.ok) { if (response.data) {
const user = await response.json() const user = response.data
userInfo.value.isAuthenticated = user.is_authenticated || false userInfo.value.isAuthenticated = user.is_authenticated || false
userInfo.value.username = user.username || '' userInfo.value.username = user.username || ''
userInfo.value.isStaff = user.is_staff || false userInfo.value.isStaff = user.is_staff || false
@@ -0,0 +1,10 @@
import { createApp } from 'vue'
import SubmissionsSelect from '@/SubmissionsSelect.vue'
import '@/style.css'
const selector = "#app"
const mountData = document.querySelector<HTMLElement>(selector)
const scopesEl = document.getElementById('scopes-data')
const scopes = scopesEl ? JSON.parse(scopesEl.textContent || '[]') : []
const app = createApp(SubmissionsSelect, { ...mountData?.dataset, scopes })
app.mount(selector)
@@ -1 +0,0 @@
import{d as s,a as e,o as n,t}from"./style-CWE_e4zZ.js";const c={key:0,class:"flex justify-center"},d={key:0,class:"badge badge-warning badge-lg"},k={key:1,class:"badge badge-lg"},o={key:2,class:"badge badge-lg"},l={key:3,class:"badge badge-lg"},g={key:1,class:"text-2xl text-base-content/50"},y=s({__name:"RankBadge",props:{rank:{}},setup(a){return(i,r)=>a.rank!==null?(n(),e("div",c,[a.rank===1?(n(),e("span",d," 🏆 #"+t(a.rank),1)):a.rank===2?(n(),e("span",k," 🥈 #"+t(a.rank),1)):a.rank===3?(n(),e("span",o," 🥉 #"+t(a.rank),1)):(n(),e("span",l," #"+t(a.rank),1))])):(n(),e("div",g," No rank yet "))}});export{y as _};
@@ -1 +0,0 @@
import{d as x,r as l,q as h,a as o,o as r,b as e,e as y,j as k,F as w,g as _,t as E,f as j,E as S}from"./style-CWE_e4zZ.js";import{g as C}from"./sdk.gen-yuJ1arsq.js";const B={class:"min-h-screen bg-base-300 flex items-center justify-center px-4"},N={class:"w-full max-w-6xl"},V={key:0,class:"flex justify-center py-20"},$={key:1,class:"grid grid-cols-1 md:grid-cols-2 gap-8"},A=["onClick"],L={class:"relative h-60 bg-base-300 overflow-hidden"},M=["src","alt","onError"],P={key:1,class:"w-full h-full bg-gradient-to-br from-blue-600 to-blue-400 flex items-center justify-center text-white"},q={class:"card-body"},D={class:"card-title text-2xl"},F=x({__name:"Home",props:{marketEnabled:{type:[String,Boolean],default:!0}},setup(b){const n=b,i=l(),d=l(!0),c=l(new Set),p=s=>`https://cdn.akamai.steamstatic.com/steam/apps/${s}/header.jpg`,v=s=>{c.value.add(s)},m=s=>{window.location.href=s},g=()=>typeof n.marketEnabled=="string"?n.marketEnabled==="true":!!n.marketEnabled;return h(async()=>{const s=await C();s.data&&(i.value=s.data),d.value=!1}),(s,t)=>(r(),o("div",B,[e("div",N,[t[6]||(t[6]=e("div",{class:"text-center mb-12"},[e("h1",{class:"text-5xl font-bold mb-4"},"PolyLAN Submitter"),e("p",{class:"text-xl text-base-content/70"}," Choose a game and submit your best solutions ")],-1)),d.value?(r(),o("div",V,[...t[1]||(t[1]=[e("span",{class:"loading loading-spinner loading-lg"},null,-1)])])):(r(),o("div",$,[g()?(r(),o("div",{key:0,onClick:t[0]||(t[0]=a=>m("/market")),class:"card card-xl bg-base-200 shadow-xl hover:shadow-2xl transition-all cursor-pointer transform hover:-translate-y-2 hover:scale-[1.05] hover:bg-base-100 overflow-hidden"},[...t[2]||(t[2]=[k('<figure class="relative h-60 bg-gradient-to-br from-purple-600 to-blue-600 flex items-center justify-center"><i class="mdi mdi-chart-box text-6xl text-white opacity-80"></i><div class="absolute inset-0 bg-black/30 group-hover:bg-black/20 transition-colors"></div></figure><div class="card-body"><h2 class="card-title text-2xl">Market</h2><p class="text-base-content/70">Place your bets and compete</p><div class="card-actions justify-end mt-4"><button class="btn btn-primary"><i class="mdi mdi-arrow-right mr-2"></i> Place bets </button></div></div>',2)])])):y("",!0),(r(!0),o(w,null,_(i.value,a=>(r(),o("div",{key:a.steam_app_id,onClick:f=>m(a.path),class:"card card-xl bg-base-200 shadow-xl hover:shadow-2xl transition-all cursor-pointer transform hover:-translate-y-2 hover:scale-[1.05] hover:bg-base-100 overflow-hidden"},[e("figure",L,[c.value.has(a.steam_app_id)?(r(),o("div",P,[...t[3]||(t[3]=[e("i",{class:"mdi mdi-gamepad-variant text-5xl"},null,-1)])])):(r(),o("img",{key:0,src:p(a.steam_app_id),alt:a.name,onError:f=>v(a.steam_app_id),class:"w-full h-full object-cover"},null,40,M)),t[4]||(t[4]=e("div",{class:"absolute inset-0 bg-black/30 group-hover:bg-black/20 transition-colors"},null,-1))]),e("div",q,[e("h2",D,E(a.name),1),t[5]||(t[5]=e("div",{class:"card-actions justify-end mt-4"},[e("button",{class:"btn btn-primary"},[e("i",{class:"mdi mdi-arrow-right mr-2"}),j(" Submit results ")])],-1))])],8,A))),128))])),t[7]||(t[7]=e("div",{class:"text-center mt-12 text-base-content/50"},[e("p",null,"Select a game above to begin submitting")],-1))])]))}}),u="#app",H=document.querySelector(u),I=S(F,{...H?.dataset});I.mount(u);
@@ -1,5 +0,0 @@
import{I as H,r as J,J as K,K as V,c as D,y as R,L as N,M as T,N as tt,O as et,P as st,B as nt,C as ot,w as ct,h as rt,Q as ut}from"./style-CWE_e4zZ.js";/*!
* pinia v3.0.3
* (c) 2025 Eduardo San Martin Morote
* @license MIT
*/let Q;const x=t=>Q=t,q=Symbol();function k(t){return t&&typeof t=="object"&&Object.prototype.toString.call(t)==="[object Object]"&&typeof t.toJSON!="function"}var g;(function(t){t.direct="direct",t.patchObject="patch object",t.patchFunction="patch function"})(g||(g={}));function at(){const t=H(!0),o=t.run(()=>J({}));let s=[],e=[];const r=K({install(u){x(r),r._a=u,u.provide(q,r),u.config.globalProperties.$pinia=r,e.forEach(a=>s.push(a)),e=[]},use(u){return this._a?s.push(u):e.push(u),this},_p:s,_a:null,_e:t,_s:new Map,state:o});return r}const z=()=>{};function A(t,o,s,e=z){t.push(o);const r=()=>{const u=t.indexOf(o);u>-1&&(t.splice(u,1),e())};return!s&&nt()&&ot(r),r}function d(t,...o){t.slice().forEach(s=>{s(...o)})}const ft=t=>t(),B=Symbol(),I=Symbol();function L(t,o){t instanceof Map&&o instanceof Map?o.forEach((s,e)=>t.set(e,s)):t instanceof Set&&o instanceof Set&&o.forEach(t.add,t);for(const s in o){if(!o.hasOwnProperty(s))continue;const e=o[s],r=t[s];k(r)&&k(e)&&t.hasOwnProperty(s)&&!R(e)&&!N(e)?t[s]=L(r,e):t[s]=e}return t}const it=Symbol();function lt(t){return!k(t)||!Object.prototype.hasOwnProperty.call(t,it)}const{assign:h}=Object;function ht(t){return!!(R(t)&&t.effect)}function bt(t,o,s,e){const{state:r,actions:u,getters:a}=o,O=s.state.value[t];let y;function b(){O||(s.state.value[t]=r?r():{});const S=ut(s.state.value[t]);return h(S,u,Object.keys(a||{}).reduce((v,_)=>(v[_]=K(D(()=>{x(s);const m=s._s.get(t);return a[_].call(m,m)})),v),{}))}return y=U(t,b,o,s,e,!0),y}function U(t,o,s={},e,r,u){let a;const O=h({actions:{}},s),y={deep:!0};let b,S,v=[],_=[],m;const j=e.state.value[t];!u&&!j&&(e.state.value[t]={}),J({});let W;function F(c){let n;b=S=!1,typeof c=="function"?(c(e.state.value[t]),n={type:g.patchFunction,storeId:t,events:m}):(L(e.state.value[t],c),n={type:g.patchObject,payload:c,storeId:t,events:m});const f=W=Symbol();rt().then(()=>{W===f&&(b=!0)}),S=!0,d(v,n,e.state.value[t])}const X=u?function(){const{state:n}=s,f=n?n():{};this.$patch(p=>{h(p,f)})}:z;function Y(){a.stop(),v=[],_=[],e._s.delete(t)}const M=(c,n="")=>{if(B in c)return c[I]=n,c;const f=function(){x(e);const p=Array.from(arguments),C=[],E=[];function G(i){C.push(i)}function $(i){E.push(i)}d(_,{args:p,name:f[I],store:l,after:G,onError:$});let w;try{w=c.apply(this&&this.$id===t?this:l,p)}catch(i){throw d(E,i),i}return w instanceof Promise?w.then(i=>(d(C,i),i)).catch(i=>(d(E,i),Promise.reject(i))):(d(C,w),w)};return f[B]=!0,f[I]=n,f},Z={_p:e,$id:t,$onAction:A.bind(null,_),$patch:F,$reset:X,$subscribe(c,n={}){const f=A(v,c,n.detached,()=>p()),p=a.run(()=>ct(()=>e.state.value[t],C=>{(n.flush==="sync"?S:b)&&c({storeId:t,type:g.direct,events:m},C)},h({},y,n)));return f},$dispose:Y},l=st(Z);e._s.set(t,l);const P=(e._a&&e._a.runWithContext||ft)(()=>e._e.run(()=>(a=H()).run(()=>o({action:M}))));for(const c in P){const n=P[c];if(R(n)&&!ht(n)||N(n))u||(j&&lt(n)&&(R(n)?n.value=j[c]:L(n,j[c])),e.state.value[t][c]=n);else if(typeof n=="function"){const f=M(n,c);P[c]=f,O.actions[c]=n}}return h(l,P),h(V(l),P),Object.defineProperty(l,"$state",{get:()=>e.state.value[t],set:c=>{F(n=>{h(n,c)})}}),e._p.forEach(c=>{h(l,a.run(()=>c({store:l,app:e._a,pinia:e,options:O})))}),j&&u&&s.hydrate&&s.hydrate(l.$state,j),b=!0,S=!0,l}/*! #__NO_SIDE_EFFECTS__ */function vt(t,o,s){let e;const r=typeof o=="function";e=r?s:o;function u(a,O){const y=et();return a=a||(y?tt(q,null):null),a&&x(a),a=Q,a._s.has(t)||(r?U(t,o,e,a):bt(t,e,a)),a._s.get(t)}return u.$id=t,u}function _t(t){const o=V(t),s={};for(const e in o){const r=o[e];r.effect?s[e]=D({get:()=>t[e],set(u){t[e]=u}}):(R(r)||N(r))&&(s[e]=T(t,e))}return s}const mt=at();export{vt as d,mt as p,_t as s};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,97 +0,0 @@
{
"_RankBadge.vue_vue_type_script_setup_true_lang-CzWD2ic-.js": {
"file": "assets/RankBadge.vue_vue_type_script_setup_true_lang-CzWD2ic-.js",
"name": "RankBadge.vue_vue_type_script_setup_true_lang",
"imports": [
"_style-CWE_e4zZ.js"
]
},
"_index-2B-edoO9.js": {
"file": "assets/index-2B-edoO9.js",
"name": "index",
"imports": [
"_style-CWE_e4zZ.js"
]
},
"_sdk.gen-yuJ1arsq.js": {
"file": "assets/sdk.gen-yuJ1arsq.js",
"name": "sdk.gen"
},
"_style-B5FWYd7r.css": {
"file": "assets/style-B5FWYd7r.css",
"src": "_style-B5FWYd7r.css"
},
"_style-CWE_e4zZ.js": {
"file": "assets/style-CWE_e4zZ.js",
"name": "style",
"css": [
"assets/style-B5FWYd7r.css"
],
"assets": [
"assets/materialdesignicons-webfont-CSr8KVlo.eot",
"assets/materialdesignicons-webfont-Dp5v-WZN.woff2",
"assets/materialdesignicons-webfont-PXm3-2wK.woff",
"assets/materialdesignicons-webfont-B7mPwVP_.ttf"
]
},
"node_modules/.pnpm/@mdi+font@7.4.47/node_modules/@mdi/font/fonts/materialdesignicons-webfont.eot": {
"file": "assets/materialdesignicons-webfont-CSr8KVlo.eot",
"src": "node_modules/.pnpm/@mdi+font@7.4.47/node_modules/@mdi/font/fonts/materialdesignicons-webfont.eot"
},
"node_modules/.pnpm/@mdi+font@7.4.47/node_modules/@mdi/font/fonts/materialdesignicons-webfont.ttf": {
"file": "assets/materialdesignicons-webfont-B7mPwVP_.ttf",
"src": "node_modules/.pnpm/@mdi+font@7.4.47/node_modules/@mdi/font/fonts/materialdesignicons-webfont.ttf"
},
"node_modules/.pnpm/@mdi+font@7.4.47/node_modules/@mdi/font/fonts/materialdesignicons-webfont.woff": {
"file": "assets/materialdesignicons-webfont-PXm3-2wK.woff",
"src": "node_modules/.pnpm/@mdi+font@7.4.47/node_modules/@mdi/font/fonts/materialdesignicons-webfont.woff"
},
"node_modules/.pnpm/@mdi+font@7.4.47/node_modules/@mdi/font/fonts/materialdesignicons-webfont.woff2": {
"file": "assets/materialdesignicons-webfont-Dp5v-WZN.woff2",
"src": "node_modules/.pnpm/@mdi+font@7.4.47/node_modules/@mdi/font/fonts/materialdesignicons-webfont.woff2"
},
"src/home.ts": {
"file": "assets/home-CykeG-Mq.js",
"name": "home",
"src": "src/home.ts",
"isEntry": true,
"imports": [
"_style-CWE_e4zZ.js",
"_sdk.gen-yuJ1arsq.js"
]
},
"src/market.ts": {
"file": "assets/market-CMZT_MuA.js",
"name": "market",
"src": "src/market.ts",
"isEntry": true,
"imports": [
"_style-CWE_e4zZ.js",
"_index-2B-edoO9.js",
"_sdk.gen-yuJ1arsq.js"
]
},
"src/noita.ts": {
"file": "assets/noita-D_MM7R73.js",
"name": "noita",
"src": "src/noita.ts",
"isEntry": true,
"imports": [
"_style-CWE_e4zZ.js",
"_index-2B-edoO9.js",
"_RankBadge.vue_vue_type_script_setup_true_lang-CzWD2ic-.js"
]
},
"src/opus-magnum.ts": {
"file": "assets/opus_magnum-BJYv3njz.js",
"name": "opus_magnum",
"src": "src/opus-magnum.ts",
"isEntry": true,
"imports": [
"_style-CWE_e4zZ.js",
"_index-2B-edoO9.js",
"_sdk.gen-yuJ1arsq.js",
"_RankBadge.vue_vue_type_script_setup_true_lang-CzWD2ic-.js"
]
}
}
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

+66 -5
View File
@@ -1,5 +1,6 @@
from django.contrib import admin from django.contrib import admin
from .models import Submission, SubmissionFile, ImageCrop from django.urls import reverse
from .models import Submission, SubmissionFile, ImageCrop, Scope
class ImageCropInline(admin.TabularInline): class ImageCropInline(admin.TabularInline):
@@ -37,16 +38,76 @@ class SubmissionFileInline(admin.TabularInline):
return super().get_queryset(request) return super().get_queryset(request)
@admin.register(Scope)
class ScopeAdmin(admin.ModelAdmin):
list_display = [
"name",
"event_game",
"external_id",
"is_active",
"assigned_user_count",
"synced_at",
"created_at",
]
list_filter = ["is_active", "event_game__event", "event_game__game"]
search_fields = ["name", "external_id", "event_game__game__name"]
readonly_fields = ["id", "created_at", "updated_at", "synced_at"]
filter_horizontal = ["assigned_users"]
autocomplete_fields = ["event_game"]
def get_queryset(self, request):
return (
super()
.get_queryset(request)
.select_related("event_game", "event_game__event", "event_game__game")
.prefetch_related("assigned_users")
)
@admin.display(description="Assigned users")
def assigned_user_count(self, obj):
return obj.assigned_users.count()
def view_on_site(self, obj):
return reverse(
"submissions.view",
kwargs={
"event_path": obj.event_game.event.path,
"game_path": obj.event_game.game.path,
"scope_id": obj.id,
},
)
@admin.register(Submission) @admin.register(Submission)
class SubmissionAdmin(admin.ModelAdmin): class SubmissionAdmin(admin.ModelAdmin):
list_display = ["id_short", "game", "user", "notes_preview", "created_at"] list_display = ["id_short", "scope", "game", "user", "notes_preview", "created_at"]
list_filter = ["game", "created_at", "updated_at"] list_filter = [
search_fields = ["id", "user__email", "notes", "game__name"] "scope__event_game__game",
"scope__event_game__event",
"created_at",
"updated_at",
]
search_fields = [
"id",
"user__email",
"notes",
"scope__event_game__game__name",
"scope__name",
]
readonly_fields = ["id", "created_at", "updated_at"] readonly_fields = ["id", "created_at", "updated_at"]
autocomplete_fields = ["scope"]
inlines = [SubmissionFileInline] inlines = [SubmissionFileInline]
def get_queryset(self, request): def get_queryset(self, request):
return super().get_queryset(request).select_related("user", "game") return (
super()
.get_queryset(request)
.select_related("user", "scope", "scope__event_game__game")
)
@admin.display(description="Game", ordering="scope__event_game__game")
def game(self, obj):
return obj.scope.event_game.game
@admin.display(description="ID") @admin.display(description="ID")
def id_short(self, obj): def id_short(self, obj):
+71 -31
View File
@@ -6,25 +6,56 @@ from ninja.files import UploadedFile
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from games.models import Game from games.models import Game
from .models import Submission, SubmissionFile, ImageCrop, OCRStatus from .models import Submission, SubmissionFile, ImageCrop, OCRStatus, Scope
from .schemas import SubmissionOut, ImageCropIn, ImageCropOut from .schemas import SubmissionOut, ImageCropIn, ImageCropOut, ScopeOut
from .tasks import process_submission_file, process_crop from .tasks import process_submission_file, process_crop
router = Router() router = Router()
PREFIX = "/events/{event_path}/submissions"
@router.post("/submission/create", response=SubmissionOut)
@router.get(f"{PREFIX}/scopes/mine", response={200: list[ScopeOut], 403: dict})
def list_my_scopes(request: HttpRequest, event_path: str, game_path: str):
if not request.user.is_authenticated:
return 403, {"detail": "Authentication required"}
game = get_object_or_404(
Game.objects.enabled_for_event(request.event), path=game_path
)
return Scope.objects.filter(
event_game__event=request.event,
event_game__game=game,
assigned_users=request.user,
)
@router.post(f"{PREFIX}/submission/create", response={200: SubmissionOut, 403: dict})
def create_submission( def create_submission(
request: HttpRequest, request: HttpRequest,
game_path: str, event_path: str,
scope_id: UUID,
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) if not request.user.is_authenticated:
return 403, {"detail": "Authentication required"}
scope = get_object_or_404(
Scope, id=scope_id, event_game__event=request.event, is_active=True
)
if (
not request.user.is_staff
and not scope.assigned_users.filter(pk=request.user.pk).exists()
):
return 403, {"detail": "You are not assigned to this scope"}
submission = Submission.objects.create( submission = Submission.objects.create(
game=game, event=request.event,
user=request.user if request.user.is_authenticated else None, scope=scope,
user=request.user,
notes=notes or None, notes=notes or None,
) )
@@ -39,32 +70,32 @@ def create_submission(
return submission return submission
@router.get("/submission/by/user", response=list[SubmissionOut]) @router.get(
def list_user_submissions(request: HttpRequest, game_path: str): f"{PREFIX}/submission/by/user", response={200: list[SubmissionOut], 403: dict}
game = get_object_or_404(Game, path=game_path) )
def list_user_submissions(request: HttpRequest, event_path: str, scope_id: UUID):
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(
"-created_at" scope_id=scope_id, user=request.user
) ).order_by("-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(scope__event_game__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 +103,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 +130,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 +160,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,43 @@
from django.core.management.base import BaseCommand, CommandError
from accounts.models import CustomUser
from games.models import EventGame
from submissions.models import Scope
class Command(BaseCommand):
help = "Manually create a Scope (match) and assign users to it."
def add_arguments(self, parser):
parser.add_argument("event_game_id", type=int)
parser.add_argument("name", type=str)
parser.add_argument(
"--user",
dest="usernames",
action="append",
default=[],
help="Username to assign to this scope. Repeatable.",
)
def handle(self, *args, **options):
try:
event_game = EventGame.objects.get(id=options["event_game_id"])
except EventGame.DoesNotExist:
raise CommandError(f"No EventGame with id={options['event_game_id']}")
usernames = options["usernames"]
users = list(CustomUser.objects.filter(username__in=usernames))
missing = set(usernames) - {u.username for u in users}
if missing:
raise CommandError(f"Unknown usernames: {', '.join(sorted(missing))}")
scope = Scope.objects.create(event_game=event_game, name=options["name"])
scope.assigned_users.set(users)
self.stdout.write(
self.style.SUCCESS(
f"Created scope '{scope.name}' (id={scope.id}) "
f"with {len(users)} assigned user(s)."
)
)
@@ -0,0 +1,47 @@
from django.core.management.base import BaseCommand, CommandError
from games.models import EventGame
from submissions.tasks import sync_all_event_games_task, sync_event_game_scopes_task
class Command(BaseCommand):
help = "Queue a sync of Scopes (matches) and their assigned users from the legacy AAndT API."
def add_arguments(self, parser):
parser.add_argument(
"--event-game",
type=int,
default=None,
help=(
"Only queue a sync for the EventGame with this id. "
"Default: every EventGame with an AAndT token configured."
),
)
def handle(self, *args, **options):
event_game_id = options["event_game"]
if event_game_id is not None:
if not EventGame.objects.filter(id=event_game_id).exists():
raise CommandError(f"No EventGame with id={event_game_id}")
sync_event_game_scopes_task.enqueue(event_game_id)
self.stdout.write(
self.style.SUCCESS(f"Queued sync for EventGame {event_game_id}")
)
return
if (
not EventGame.objects.exclude(apibot_token__isnull=True)
.exclude(apibot_token="")
.exists()
):
self.stdout.write(
self.style.WARNING("No EventGame has an AAndT token configured.")
)
return
sync_all_event_games_task.enqueue()
self.stdout.write(
self.style.SUCCESS("Queued sync for every EventGame with an AAndT token.")
)
@@ -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",
),
),
]
@@ -0,0 +1,84 @@
import uuid
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("submissions", "0005_submission_event_required"),
("games", "0007_eventgame_apibot_token"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="Scope",
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)),
(
"external_id",
models.PositiveIntegerField(
blank=True,
help_text="Legacy AAndT match id, set when synced automatically.",
null=True,
),
),
("name", models.CharField(max_length=255)),
(
"is_active",
models.BooleanField(
default=True,
help_text="Whether submissions are currently accepted for this scope.",
),
),
(
"synced_at",
models.DateTimeField(
blank=True,
help_text="Last time this scope was synced from the AAndT API.",
null=True,
),
),
(
"assigned_users",
models.ManyToManyField(
blank=True,
help_text="Users allowed to submit scores for this scope.",
related_name="scopes",
to=settings.AUTH_USER_MODEL,
),
),
(
"event_game",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="scopes",
to="games.eventgame",
),
),
],
options={
"ordering": ["-created_at"],
},
),
migrations.AddConstraint(
model_name="scope",
constraint=models.UniqueConstraint(
condition=models.Q(("external_id__isnull", False)),
fields=("event_game", "external_id"),
name="unique_scope_external_id_per_event_game",
),
),
]
@@ -0,0 +1,37 @@
import uuid
import django.db.models.deletion
from django.db import migrations, models
def delete_existing_submissions(apps, schema_editor):
"""Submissions predate Scope and can't be backfilled with one; wipe them."""
Submission = apps.get_model("submissions", "Submission")
Submission.objects.all().delete()
class Migration(migrations.Migration):
# Deleting existing submissions (which cascades to their files/crops) fires
# deferred FK triggers on postgres; altering the same table right after in
# the same transaction hits "pending trigger events", so run non-atomically.
atomic = False
dependencies = [
("submissions", "0006_scope"),
]
operations = [
migrations.RunPython(delete_existing_submissions, migrations.RunPython.noop),
migrations.AddField(
model_name="submission",
name="scope",
field=models.ForeignKey(
default=uuid.uuid4,
help_text="The match this submission's score is for.",
on_delete=django.db.models.deletion.CASCADE,
related_name="submissions",
to="submissions.scope",
),
preserve_default=False,
),
]
@@ -0,0 +1,17 @@
# Generated by Django 6.0.5 on 2026-09-16 09:27
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("submissions", "0007_submission_scope"),
]
operations = [
migrations.RemoveField(
model_name="submission",
name="game",
),
]
+77 -7
View File
@@ -4,7 +4,9 @@ from django.utils.translation import gettext_lazy as _
import uuid import uuid
from accounts.models import CustomUser from accounts.models import CustomUser
from games.models import Game from events.models import Event
from events.scoping import EventScopedManager
from games.models import EventGame
class OCRStatus(models.TextChoices): class OCRStatus(models.TextChoices):
@@ -14,14 +16,20 @@ class OCRStatus(models.TextChoices):
ERROR = "error", _("Error processing") ERROR = "error", _("Error processing")
def submission_file_upload_path(instance, filename): def submission_file_upload_path(instance: "SubmissionFile", filename: str):
ext = filename.split(".")[-1] if "." in filename else "" ext = filename.split(".")[-1] if "." in filename else ""
new_filename = f"{instance.id}_{filename}" if ext else str(instance.id) new_filename = f"{instance.id}_{filename}" if ext else str(instance.id)
date = instance.created_at.strftime("%Y/%m/%d")
user = instance.submission.user.username user = instance.submission.user.username
game_path = instance.submission.scope.event_game.game.path
event_path = instance.submission.scope.event_game.event.path
mid = (
instance.submission.scope.external_id
if instance.submission.scope.external_id
else instance.submission.scope.name
)
return f"{instance.submission.game.path.lstrip('/')}/{date}/{user}/{instance.submission.id}/{new_filename}" return f"{event_path.lstrip('/')}/{game_path.lstrip('/')}/{mid}/{user}/{instance.submission.id}/{new_filename}"
class BaseModel(models.Model): class BaseModel(models.Model):
@@ -35,15 +43,74 @@ class BaseModel(models.Model):
abstract = True abstract = True
class Submission(BaseModel): class Scope(BaseModel):
game = models.ForeignKey( """A "match" that submissions can be scored against.
Game,
Scopes are either created manually (admin/CLI) or auto-synced from the
legacy PolyLAN AAndT API (see submissions.services.aandt_sync), in which
case `external_id` holds the remote match id for idempotent upserts.
"""
event_game = models.ForeignKey(
EventGame,
on_delete=models.CASCADE, on_delete=models.CASCADE,
related_name="scopes",
)
external_id = models.PositiveIntegerField(
null=True, null=True,
blank=True, blank=True,
help_text="Legacy AAndT match id, set when synced automatically.",
)
name = models.CharField(max_length=255)
assigned_users = models.ManyToManyField(
CustomUser,
blank=True,
related_name="scopes",
help_text="Users allowed to submit scores for this scope.",
)
is_active = models.BooleanField(
default=True,
help_text="Whether submissions are currently accepted for this scope.",
)
synced_at = models.DateTimeField(
null=True,
blank=True,
help_text="Last time this scope was synced from the AAndT API.",
)
class Meta:
ordering = ["-created_at"]
constraints = [
models.UniqueConstraint(
fields=["event_game", "external_id"],
condition=models.Q(external_id__isnull=False),
name="unique_scope_external_id_per_event_game",
),
]
def __str__(self) -> str:
return f"{self.name} ({self.event_game})"
class Submission(BaseModel):
event = models.ForeignKey(
Event,
on_delete=models.CASCADE,
related_name="submissions", related_name="submissions",
) )
scope = models.ForeignKey(
Scope,
on_delete=models.CASCADE,
related_name="submissions",
help_text="The match this submission's score is for.",
)
user = models.ForeignKey( user = models.ForeignKey(
CustomUser, CustomUser,
on_delete=models.CASCADE, on_delete=models.CASCADE,
@@ -60,6 +127,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(
+8 -1
View File
@@ -3,6 +3,13 @@ from uuid import UUID
from ninja import Schema from ninja import Schema
class ScopeOut(Schema):
id: UUID
name: str
is_active: bool
created_at: datetime
class ImageCropIn(Schema): class ImageCropIn(Schema):
x1: int x1: int
y1: int y1: int
@@ -36,7 +43,7 @@ class SubmissionFileOut(Schema):
class SubmissionOut(Schema): class SubmissionOut(Schema):
id: UUID id: UUID
game_id: int | None = None scope_id: UUID
notes: str | None = None notes: str | None = None
files: list[SubmissionFileOut] = [] files: list[SubmissionFileOut] = []
created_at: datetime created_at: datetime
@@ -0,0 +1,8 @@
"""A client library for accessing PolyLAN AAndT Bot API"""
from .client import AuthenticatedClient, Client
__all__ = (
"AuthenticatedClient",
"Client",
)
@@ -0,0 +1 @@
"""Contains methods for accessing the API"""
@@ -0,0 +1 @@
"""Contains endpoint functions for accessing the API"""
@@ -0,0 +1,134 @@
from http import HTTPStatus
from typing import Any
import httpx
from ... import errors
from ...client import AuthenticatedClient, Client
from ...models.aandt_api_apibot_get_response_200 import AandtApiApibotGetResponse200
from ...models.aandt_api_apibot_get_response_401 import AandtApiApibotGetResponse401
from ...types import Response
def _get_kwargs() -> dict[str, Any]:
_kwargs: dict[str, Any] = {
"method": "get",
"url": "/apibot",
}
return _kwargs
def _parse_response(
*, client: AuthenticatedClient | Client, response: httpx.Response
) -> AandtApiApibotGetResponse200 | AandtApiApibotGetResponse401 | None:
if response.status_code == 200:
response_200 = AandtApiApibotGetResponse200.from_dict(response.json())
return response_200
if response.status_code == 401:
response_401 = AandtApiApibotGetResponse401.from_dict(response.text)
return response_401
if client.raise_on_unexpected_status:
raise errors.UnexpectedStatus(response.status_code, response.content)
else:
return None
def _build_response(
*, client: AuthenticatedClient | Client, response: httpx.Response
) -> Response[AandtApiApibotGetResponse200 | AandtApiApibotGetResponse401]:
return Response(
status_code=HTTPStatus(response.status_code),
content=response.content,
headers=response.headers,
parsed=_parse_response(client=client, response=response),
)
def sync_detailed(
*,
client: AuthenticatedClient | Client,
) -> Response[AandtApiApibotGetResponse200 | AandtApiApibotGetResponse401]:
"""Get the AAndT's apibot activation state and display name
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Response[AandtApiApibotGetResponse200 | AandtApiApibotGetResponse401]
"""
kwargs = _get_kwargs()
response = client.get_httpx_client().request(
**kwargs,
)
return _build_response(client=client, response=response)
def sync(
*,
client: AuthenticatedClient | Client,
) -> AandtApiApibotGetResponse200 | AandtApiApibotGetResponse401 | None:
"""Get the AAndT's apibot activation state and display name
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
AandtApiApibotGetResponse200 | AandtApiApibotGetResponse401
"""
return sync_detailed(
client=client,
).parsed
async def asyncio_detailed(
*,
client: AuthenticatedClient | Client,
) -> Response[AandtApiApibotGetResponse200 | AandtApiApibotGetResponse401]:
"""Get the AAndT's apibot activation state and display name
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Response[AandtApiApibotGetResponse200 | AandtApiApibotGetResponse401]
"""
kwargs = _get_kwargs()
response = await client.get_async_httpx_client().request(**kwargs)
return _build_response(client=client, response=response)
async def asyncio(
*,
client: AuthenticatedClient | Client,
) -> AandtApiApibotGetResponse200 | AandtApiApibotGetResponse401 | None:
"""Get the AAndT's apibot activation state and display name
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
AandtApiApibotGetResponse200 | AandtApiApibotGetResponse401
"""
return (
await asyncio_detailed(
client=client,
)
).parsed
@@ -0,0 +1,174 @@
from http import HTTPStatus
from typing import Any
import httpx
from ... import errors
from ...client import AuthenticatedClient, Client
from ...models.aandt_api_apibot_set_body import AandtApiApibotSetBody
from ...models.aandt_api_apibot_set_response_401 import AandtApiApibotSetResponse401
from ...models.success_response import SuccessResponse
from ...types import Response
def _get_kwargs(
*,
body: AandtApiApibotSetBody,
) -> dict[str, Any]:
headers: dict[str, Any] = {}
_kwargs: dict[str, Any] = {
"method": "post",
"url": "/apibot",
}
_kwargs["json"] = body.to_dict()
headers["Content-Type"] = "application/json"
_kwargs["headers"] = headers
return _kwargs
def _parse_response(
*, client: AuthenticatedClient | Client, response: httpx.Response
) -> AandtApiApibotSetResponse401 | SuccessResponse | None:
if response.status_code == 200:
response_200 = SuccessResponse.from_dict(response.json())
return response_200
if response.status_code == 401:
response_401 = AandtApiApibotSetResponse401.from_dict(response.text)
return response_401
if client.raise_on_unexpected_status:
raise errors.UnexpectedStatus(response.status_code, response.content)
else:
return None
def _build_response(
*, client: AuthenticatedClient | Client, response: httpx.Response
) -> Response[AandtApiApibotSetResponse401 | SuccessResponse]:
return Response(
status_code=HTTPStatus(response.status_code),
content=response.content,
headers=response.headers,
parsed=_parse_response(client=client, response=response),
)
def sync_detailed(
*,
client: AuthenticatedClient | Client,
body: AandtApiApibotSetBody,
) -> Response[AandtApiApibotSetResponse401 | SuccessResponse]:
"""Set the AAndT's apibot activation state and/or display name
Broadcasts the update over Zuperpush after saving.
Args:
body (AandtApiApibotSetBody):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Response[AandtApiApibotSetResponse401 | SuccessResponse]
"""
kwargs = _get_kwargs(
body=body,
)
response = client.get_httpx_client().request(
**kwargs,
)
return _build_response(client=client, response=response)
def sync(
*,
client: AuthenticatedClient | Client,
body: AandtApiApibotSetBody,
) -> AandtApiApibotSetResponse401 | SuccessResponse | None:
"""Set the AAndT's apibot activation state and/or display name
Broadcasts the update over Zuperpush after saving.
Args:
body (AandtApiApibotSetBody):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
AandtApiApibotSetResponse401 | SuccessResponse
"""
return sync_detailed(
client=client,
body=body,
).parsed
async def asyncio_detailed(
*,
client: AuthenticatedClient | Client,
body: AandtApiApibotSetBody,
) -> Response[AandtApiApibotSetResponse401 | SuccessResponse]:
"""Set the AAndT's apibot activation state and/or display name
Broadcasts the update over Zuperpush after saving.
Args:
body (AandtApiApibotSetBody):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Response[AandtApiApibotSetResponse401 | SuccessResponse]
"""
kwargs = _get_kwargs(
body=body,
)
response = await client.get_async_httpx_client().request(**kwargs)
return _build_response(client=client, response=response)
async def asyncio(
*,
client: AuthenticatedClient | Client,
body: AandtApiApibotSetBody,
) -> AandtApiApibotSetResponse401 | SuccessResponse | None:
"""Set the AAndT's apibot activation state and/or display name
Broadcasts the update over Zuperpush after saving.
Args:
body (AandtApiApibotSetBody):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
AandtApiApibotSetResponse401 | SuccessResponse
"""
return (
await asyncio_detailed(
client=client,
body=body,
)
).parsed

Some files were not shown because too many files have changed in this diff Show More