Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ec9a54649
|
||
|
|
fabf06e6d4
|
||
|
|
85e67599af
|
||
|
|
68de07645a
|
||
|
|
8b9c254fd2
|
||
|
|
b5c2cdc037
|
@@ -12,6 +12,18 @@ dist
|
||||
dist-ssr
|
||||
*.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
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
|
||||
@@ -22,10 +22,12 @@ from opus_magnum.models import PuzzleResponse, SteamCollectionItem, SteamCollect
|
||||
|
||||
router = Router()
|
||||
|
||||
PREFIX = "/events/{event_path}/results"
|
||||
|
||||
@router.get("results", response=RankingSchema)
|
||||
def results(request: HttpRequest) -> dict:
|
||||
cache_key = "api:results:results"
|
||||
|
||||
@router.get(f"{PREFIX}/results", response=RankingSchema)
|
||||
def results(request: HttpRequest, event_path: str) -> dict:
|
||||
cache_key = f"api:{event_path}:results:results"
|
||||
cached_data = cache.get(cache_key)
|
||||
|
||||
if cached_data is not None:
|
||||
@@ -35,7 +37,9 @@ def results(request: HttpRequest) -> dict:
|
||||
responses_by_puzzleid = defaultdict(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()
|
||||
.prefetch_related("submission__user")
|
||||
):
|
||||
@@ -51,25 +55,27 @@ def results(request: HttpRequest) -> dict:
|
||||
|
||||
data = {
|
||||
"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),
|
||||
"ranking_by_puzzle": ranking,
|
||||
}
|
||||
|
||||
cache.set("api:results:results", data, 300)
|
||||
cache.set(cache_key, data, 300)
|
||||
return data
|
||||
|
||||
|
||||
@router.get("top-submissions", response=TournamentSubmissionsOut)
|
||||
def top_submissions(request: HttpRequest, limit: int = 5) -> dict:
|
||||
@router.get(f"{PREFIX}/top-submissions", response=TournamentSubmissionsOut)
|
||||
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."""
|
||||
cache_key = f"api:results:top_submissions:{limit}"
|
||||
cache_key = f"api:{event_path}:results:top_submissions:{limit}"
|
||||
cached_data = cache.get(cache_key)
|
||||
|
||||
if cached_data is not None:
|
||||
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
|
||||
if collection.accepting_submissions:
|
||||
@@ -141,20 +147,20 @@ def top_submissions(request: HttpRequest, limit: int = 5) -> dict:
|
||||
)
|
||||
|
||||
data = {"submissions": submissions_list}
|
||||
cache.set(f"api:results:top_submissions:{limit}", data, 300)
|
||||
cache.set(cache_key, data, 300)
|
||||
return data
|
||||
|
||||
|
||||
@router.get("puzzle-results", response=TournamentPuzzleResultsOut)
|
||||
def puzzle_results(request: HttpRequest, limit: int = 5) -> dict:
|
||||
@router.get(f"{PREFIX}/puzzle-results", response=TournamentPuzzleResultsOut)
|
||||
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."""
|
||||
cache_key = f"api:results:puzzle_results:{limit}"
|
||||
cache_key = f"api:{event_path}:results:puzzle_results:{limit}"
|
||||
cached_data = cache.get(cache_key)
|
||||
|
||||
if cached_data is not None:
|
||||
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
|
||||
if collection.accepting_submissions:
|
||||
@@ -237,5 +243,5 @@ def puzzle_results(request: HttpRequest, limit: int = 5) -> dict:
|
||||
)
|
||||
|
||||
data = {"results": results_list}
|
||||
cache.set(f"api:results:puzzle_results:{limit}", data, 300)
|
||||
cache.set(cache_key, data, 300)
|
||||
return data
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import Event
|
||||
|
||||
|
||||
@admin.register(Event)
|
||||
class EventAdmin(admin.ModelAdmin):
|
||||
list_display = ["name", "path", "created_at"]
|
||||
search_fields = ["name", "path"]
|
||||
prepopulated_fields = {"path": ("name",)}
|
||||
readonly_fields = ["created_at", "updated_at"]
|
||||
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class EventsConfig(AppConfig):
|
||||
name = "events"
|
||||
@@ -0,0 +1,27 @@
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
||||
from .models import Event
|
||||
from .scoping import current_event
|
||||
|
||||
|
||||
class EventContextMiddleware:
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request):
|
||||
try:
|
||||
return self.get_response(request)
|
||||
finally:
|
||||
# Not current_event.reset(token): under ASGI, process_view and this
|
||||
# finally block can run in different contextvars.Context copies
|
||||
# (sync_to_async(thread_sensitive=True) copies the context), and
|
||||
# Token.reset() requires the exact Context it was created in.
|
||||
# set(None) has no such constraint.
|
||||
current_event.set(None)
|
||||
|
||||
def process_view(self, request, view_func, view_args, view_kwargs):
|
||||
event_path = view_kwargs.get("event_path")
|
||||
if event_path:
|
||||
request.event = get_object_or_404(Event, path=event_path)
|
||||
current_event.set(request.event)
|
||||
return None
|
||||
@@ -0,0 +1,34 @@
|
||||
# Generated by Django 6.0.5 on 2026-09-15 21:08
|
||||
|
||||
import uuid
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
initial = True
|
||||
|
||||
dependencies = []
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="Event",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.UUIDField(
|
||||
default=uuid.uuid4,
|
||||
editable=False,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
),
|
||||
),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
("updated_at", models.DateTimeField(auto_now=True)),
|
||||
("name", models.CharField(max_length=255)),
|
||||
("path", models.SlugField(max_length=100, unique=True)),
|
||||
],
|
||||
options={
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,24 @@
|
||||
from django.db import migrations
|
||||
|
||||
EVENT_NAME = "PolyLAN 43"
|
||||
EVENT_PATH = "polylan-43"
|
||||
|
||||
|
||||
def seed_event(apps, schema_editor):
|
||||
Event = apps.get_model("events", "Event")
|
||||
Event.objects.get_or_create(path=EVENT_PATH, defaults={"name": EVENT_NAME})
|
||||
|
||||
|
||||
def unseed_event(apps, schema_editor):
|
||||
Event = apps.get_model("events", "Event")
|
||||
Event.objects.filter(path=EVENT_PATH).delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("events", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(seed_event, unseed_event),
|
||||
]
|
||||
@@ -0,0 +1,22 @@
|
||||
from django.db import models
|
||||
|
||||
import uuid
|
||||
|
||||
|
||||
class BaseModel(models.Model):
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
|
||||
# Timestamps
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class Event(BaseModel):
|
||||
name = models.CharField(max_length=255)
|
||||
path = models.SlugField(max_length=100, unique=True)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
@@ -0,0 +1,21 @@
|
||||
import contextvars
|
||||
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.db import models
|
||||
|
||||
current_event = contextvars.ContextVar("current_event", default=None)
|
||||
|
||||
|
||||
class EventScopedQuerySet(models.QuerySet):
|
||||
def for_current_event(self):
|
||||
event = current_event.get()
|
||||
if event is None:
|
||||
raise ImproperlyConfigured(
|
||||
f"{self.model.__name__}.for_event was used outside an event-scoped request"
|
||||
)
|
||||
return self.filter(event=event)
|
||||
|
||||
|
||||
class EventScopedManager(models.Manager.from_queryset(EventScopedQuerySet)):
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().for_current_event()
|
||||
@@ -0,0 +1,34 @@
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.test import TestCase
|
||||
|
||||
from games.models import Game
|
||||
from submissions.models import Submission
|
||||
|
||||
from .models import Event
|
||||
from .scoping import current_event
|
||||
|
||||
|
||||
class EventScopedManagerTests(TestCase):
|
||||
def setUp(self):
|
||||
self.event_a = Event.objects.create(name="Event A", path="event-a")
|
||||
self.event_b = Event.objects.create(name="Event B", path="event-b")
|
||||
self.game = Game.objects.create(name="Test Game", path="test-game")
|
||||
self.submission_a = Submission.objects.create(
|
||||
event=self.event_a, game=self.game
|
||||
)
|
||||
self.submission_b = Submission.objects.create(
|
||||
event=self.event_b, game=self.game
|
||||
)
|
||||
|
||||
def test_for_event_raises_outside_event_context(self):
|
||||
with self.assertRaises(ImproperlyConfigured):
|
||||
list(Submission.for_event.all())
|
||||
|
||||
def test_for_event_filters_to_current_event(self):
|
||||
token = current_event.set(self.event_a)
|
||||
try:
|
||||
results = list(Submission.for_event.all())
|
||||
finally:
|
||||
current_event.reset(token)
|
||||
|
||||
self.assertEqual(results, [self.submission_a])
|
||||
@@ -0,0 +1 @@
|
||||
# Create your views here.
|
||||
@@ -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):
|
||||
updated = queryset.update(enabled=True)
|
||||
modeladmin.message_user(request, f"{updated} game(s) enabled successfully.")
|
||||
|
||||
|
||||
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"
|
||||
class EventGameInline(admin.TabularInline):
|
||||
model = EventGame
|
||||
extra = 0
|
||||
|
||||
|
||||
@admin.register(Game)
|
||||
class GameAdmin(admin.ModelAdmin):
|
||||
list_display = ["name", "steam_app_id", "enabled", "updated_at"]
|
||||
list_filter = ["enabled"]
|
||||
list_display = ["name", "steam_app_id", "updated_at"]
|
||||
search_fields = ["name", "steam_app_id"]
|
||||
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,
|
||||
)
|
||||
|
||||
@@ -6,8 +6,14 @@ from .models import Game
|
||||
from .schemas import GameOut
|
||||
|
||||
router = Router()
|
||||
event_router = Router()
|
||||
|
||||
|
||||
@router.get("", response=List[GameOut])
|
||||
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)
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
from functools import wraps
|
||||
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
||||
from events.models import Event
|
||||
from .models import Game
|
||||
|
||||
|
||||
@@ -9,12 +11,15 @@ def require_game_enabled(steam_app_id: int):
|
||||
def decorator(view_func):
|
||||
@wraps(view_func)
|
||||
def wrapper(request, *args, **kwargs):
|
||||
try:
|
||||
game = Game.objects.get(steam_app_id=steam_app_id)
|
||||
except Game.DoesNotExist:
|
||||
raise PermissionDenied
|
||||
if not game.enabled:
|
||||
event = get_object_or_404(Event, path=kwargs.get("event_path"))
|
||||
|
||||
if (
|
||||
not Game.objects.enabled_for_event(event)
|
||||
.filter(steam_app_id=steam_app_id)
|
||||
.exists()
|
||||
):
|
||||
raise PermissionDenied
|
||||
|
||||
return view_func(request, *args, **kwargs)
|
||||
|
||||
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),
|
||||
),
|
||||
]
|
||||
@@ -1,16 +1,25 @@
|
||||
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):
|
||||
steam_app_id = models.PositiveIntegerField(null=True, blank=True)
|
||||
name = models.CharField(max_length=255)
|
||||
path = models.CharField(max_length=100)
|
||||
custom_path = models.BooleanField(default=False)
|
||||
image = models.URLField(null=True, blank=True)
|
||||
enabled = models.BooleanField(default=True)
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
objects = models.Manager.from_queryset(GameQuerySet)()
|
||||
|
||||
class Meta:
|
||||
ordering = ["name"]
|
||||
|
||||
@@ -18,3 +27,38 @@ class Game(models.Model):
|
||||
if self.steam_app_id:
|
||||
return f"{self.name} ({self.steam_app_id})"
|
||||
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'})"
|
||||
|
||||
@@ -8,4 +8,5 @@ class GameOut(Schema):
|
||||
steam_app_id: Optional[int] = None
|
||||
name: str
|
||||
path: str
|
||||
custom_path: bool
|
||||
image: Optional[str] = None
|
||||
|
||||
@@ -17,11 +17,13 @@ from market.schemas import (
|
||||
|
||||
router = Router(tags=["market"])
|
||||
|
||||
PREFIX = "/events/{event_path}/market"
|
||||
|
||||
@router.get("/", response=List[MarketListSchema])
|
||||
def list_markets(request):
|
||||
|
||||
@router.get(f"{PREFIX}/", response=List[MarketListSchema])
|
||||
def list_markets(request, event_path: str):
|
||||
"""List all markets (excludes draft markets)."""
|
||||
markets = Market.objects.exclude(status=Market.Status.DRAFT)
|
||||
markets = Market.for_event.exclude(status=Market.Status.DRAFT)
|
||||
# Prefetch options with total_bets annotation sorted by total_bets desc, then text asc
|
||||
options_queryset = MarketOption.objects.annotate(
|
||||
total_bets=Coalesce(Sum("user_bets__amount"), 0)
|
||||
@@ -30,38 +32,40 @@ def list_markets(request):
|
||||
return markets.prefetch_related(Prefetch("options", queryset=options_queryset))
|
||||
|
||||
|
||||
@router.get("/user/bets", response=List[UserBetSchema])
|
||||
def list_user_bets(request):
|
||||
@router.get(f"{PREFIX}/user/bets", response=List[UserBetSchema])
|
||||
def list_user_bets(request, event_path: str):
|
||||
"""List all bets placed by the current user."""
|
||||
if not request.user.is_authenticated:
|
||||
raise HttpError(401, "Authentication required")
|
||||
|
||||
return (
|
||||
UserBet.objects.filter(user=request.user)
|
||||
UserBet.objects.filter(user=request.user, option__market__event=request.event)
|
||||
.select_related("option__market")
|
||||
.prefetch_related("option__market__options")
|
||||
)
|
||||
|
||||
|
||||
@router.post("/{market_uuid}/actions/close")
|
||||
def close_market(request, market_uuid: str):
|
||||
@router.post(f"{PREFIX}/{{market_uuid}}/actions/close")
|
||||
def close_market(request, event_path: str, market_uuid: str):
|
||||
"""Close a market. Admin only."""
|
||||
if not request.user.is_staff:
|
||||
raise HttpError(403, "Permission denied")
|
||||
|
||||
market = get_object_or_404(Market, uuid=market_uuid)
|
||||
market = get_object_or_404(Market, uuid=market_uuid, event=request.event)
|
||||
market.status = Market.Status.CLOSED
|
||||
market.save(update_fields=["status", "updated_at"])
|
||||
return {"status": Market.Status.CLOSED}
|
||||
|
||||
|
||||
@router.post("/{market_uuid}/actions/resolve", response=MarketListSchema)
|
||||
def resolve_market(request, market_uuid: str, payload: ResolveMarketSchema):
|
||||
@router.post(f"{PREFIX}/{{market_uuid}}/actions/resolve", response=MarketListSchema)
|
||||
def resolve_market(
|
||||
request, event_path: str, market_uuid: str, payload: ResolveMarketSchema
|
||||
):
|
||||
"""Resolve a market with a winning option. Admin only."""
|
||||
if not request.user.is_staff:
|
||||
raise HttpError(403, "Permission denied")
|
||||
|
||||
market = get_object_or_404(Market, uuid=market_uuid)
|
||||
market = get_object_or_404(Market, uuid=market_uuid, event=request.event)
|
||||
winning_option = get_object_or_404(MarketOption, uuid=payload.winning_option_uuid)
|
||||
|
||||
if winning_option.market_id != market.id:
|
||||
@@ -129,13 +133,15 @@ def resolve_market(request, market_uuid: str, payload: ResolveMarketSchema):
|
||||
return market
|
||||
|
||||
|
||||
@router.post("/{market_uuid}/bets", response=UserBetSchema)
|
||||
def create_bet(request, market_uuid: str, payload: UserBetCreateSchema):
|
||||
@router.post(f"{PREFIX}/{{market_uuid}}/bets", response=UserBetSchema)
|
||||
def create_bet(
|
||||
request, event_path: str, market_uuid: str, payload: UserBetCreateSchema
|
||||
):
|
||||
"""Place a bet on a market option."""
|
||||
if not request.user.is_authenticated:
|
||||
raise HttpError(401, "Authentication required")
|
||||
|
||||
market = get_object_or_404(Market, uuid=market_uuid)
|
||||
market = get_object_or_404(Market, uuid=market_uuid, event=request.event)
|
||||
option = get_object_or_404(MarketOption, uuid=payload.option_uuid)
|
||||
|
||||
if option.market_id != market.id:
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
# Generated by Django 6.0.5 on 2026-09-15 21:10
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("events", "0002_seed_polylan_43"),
|
||||
("market", "0006_alter_market_status"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="market",
|
||||
name="event",
|
||||
field=models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="markets",
|
||||
to="events.event",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,25 @@
|
||||
from django.db import migrations
|
||||
|
||||
EVENT_PATH = "polylan-43"
|
||||
|
||||
|
||||
def backfill_event(apps, schema_editor):
|
||||
Event = apps.get_model("events", "Event")
|
||||
Market = apps.get_model("market", "Market")
|
||||
event = Event.objects.get(path=EVENT_PATH)
|
||||
Market.objects.filter(event__isnull=True).update(event=event)
|
||||
|
||||
|
||||
def noop(apps, schema_editor):
|
||||
pass
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("market", "0007_market_event"),
|
||||
("events", "0002_seed_polylan_43"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(backfill_event, noop),
|
||||
]
|
||||
@@ -0,0 +1,20 @@
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("market", "0008_backfill_event"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="market",
|
||||
name="event",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="markets",
|
||||
to="events.event",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -1,6 +1,9 @@
|
||||
import uuid
|
||||
from django.db import models
|
||||
|
||||
from events.models import Event
|
||||
from events.scoping import EventScopedManager
|
||||
|
||||
|
||||
class BaseModel(models.Model):
|
||||
uuid = models.UUIDField(default=uuid.uuid4, unique=True, editable=False)
|
||||
@@ -18,6 +21,12 @@ class Market(BaseModel):
|
||||
CLOSED = "closed", "Closed"
|
||||
RESOLVED = "resolved", "Resolved"
|
||||
|
||||
event = models.ForeignKey(
|
||||
Event,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="markets",
|
||||
)
|
||||
|
||||
title = models.CharField(max_length=255)
|
||||
description = models.TextField(blank=True)
|
||||
status = models.CharField(
|
||||
@@ -34,6 +43,9 @@ class Market(BaseModel):
|
||||
related_name="market_won",
|
||||
)
|
||||
|
||||
objects = models.Manager()
|
||||
for_event = EventScopedManager()
|
||||
|
||||
class Meta:
|
||||
ordering = ["-created_at"]
|
||||
indexes = [
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.shortcuts import render
|
||||
from django.shortcuts import get_object_or_404, render
|
||||
from django.http import HttpRequest
|
||||
|
||||
from events.models import Event
|
||||
|
||||
|
||||
@login_required
|
||||
def market_home(request: HttpRequest):
|
||||
return render(request, "market.html", {})
|
||||
def market_home(request: HttpRequest, event_path: str):
|
||||
event = get_object_or_404(Event, path=event_path)
|
||||
return render(request, "market.html", {"event": event})
|
||||
|
||||
@@ -25,11 +25,13 @@ from .schemas import NoitaSubmissionOut
|
||||
router = Router()
|
||||
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))
|
||||
def get_results(request: HttpRequest):
|
||||
cache_key = f"api:noita:results:{request.user.id}"
|
||||
def get_results(request: HttpRequest, event_path: str):
|
||||
cache_key = f"api:{event_path}:noita:results:{request.user.id}"
|
||||
cached_data = cache.get(cache_key)
|
||||
|
||||
if cached_data is not None:
|
||||
@@ -42,7 +44,7 @@ def get_results(request: HttpRequest):
|
||||
"""
|
||||
# Group objectives by objectiv_id and count occurrences
|
||||
user_objectives = (
|
||||
Objectiv.objects.filter(user=request.user)
|
||||
Objectiv.objects.filter(user=request.user, submission__event=request.event)
|
||||
.values("objectiv_id")
|
||||
.annotate(count=Count("id"))
|
||||
)
|
||||
@@ -118,7 +120,7 @@ def get_results(request: HttpRequest):
|
||||
total_score += points
|
||||
|
||||
# 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 = {
|
||||
"total_score": total_score,
|
||||
@@ -126,19 +128,19 @@ def get_results(request: HttpRequest):
|
||||
"objectives": list(with_points.values()),
|
||||
}
|
||||
|
||||
cache.set(f"api:noita:results:{request.user.id}", data, 300)
|
||||
cache.set(cache_key, data, 300)
|
||||
return data
|
||||
|
||||
|
||||
@router.get("leaderboard", response=LeaderboardOut)
|
||||
@router.get(f"{PREFIX}/leaderboard", response=LeaderboardOut)
|
||||
@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.
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
all_objectives = (
|
||||
Objectiv.objects.values("user", "objectiv_id")
|
||||
Objectiv.objects.filter(submission__event=request.event)
|
||||
.values("user", "objectiv_id")
|
||||
.annotate(count=Count("id"))
|
||||
.annotate(
|
||||
# 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():
|
||||
user = User.objects.get(id=user_id)
|
||||
objectives_count = (
|
||||
Objectiv.objects.filter(user_id=user_id)
|
||||
Objectiv.objects.filter(user_id=user_id, submission__event=request.event)
|
||||
.values("objectiv_id")
|
||||
.distinct()
|
||||
.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(
|
||||
{
|
||||
"user_id": user_id,
|
||||
@@ -232,13 +235,15 @@ def get_leaderboard(request: HttpRequest):
|
||||
]
|
||||
|
||||
data = {"leaderboard": leaderboard}
|
||||
cache.set("api:noita:leaderboard", data, 300)
|
||||
cache.set(cache_key, data, 300)
|
||||
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))
|
||||
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).
|
||||
|
||||
@@ -267,6 +272,7 @@ def submit_log_file(request: HttpRequest, file: UploadedFile = File(...)):
|
||||
try:
|
||||
# Create submission
|
||||
submission = LogfileSubmission.objects.create(
|
||||
event=request.event,
|
||||
user=request.user if request.user.is_authenticated else None,
|
||||
content_type=file.content_type,
|
||||
file_size=file.size,
|
||||
@@ -284,8 +290,8 @@ def submit_log_file(request: HttpRequest, file: UploadedFile = File(...)):
|
||||
|
||||
# Invalidate caches on successful submission
|
||||
if submission.user:
|
||||
cache.delete(f"api:noita:results:{submission.user.id}")
|
||||
cache.delete("api:noita:leaderboard")
|
||||
cache.delete(f"api:{event_path}:noita:results:{submission.user.id}")
|
||||
cache.delete(f"api:{event_path}:noita:leaderboard")
|
||||
|
||||
return {
|
||||
"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",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -4,6 +4,9 @@ from django.utils import timezone
|
||||
|
||||
import uuid
|
||||
|
||||
from events.models import Event
|
||||
from events.scoping import EventScopedManager
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
@@ -21,6 +24,12 @@ class LogfileSubmission(models.Model):
|
||||
# Identification
|
||||
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")
|
||||
file_size = models.PositiveIntegerField(help_text="File size in bytes")
|
||||
file = models.FileField(
|
||||
@@ -44,6 +53,9 @@ class LogfileSubmission(models.Model):
|
||||
|
||||
processed = models.BooleanField(default=False)
|
||||
|
||||
objects = models.Manager()
|
||||
for_event = EventScopedManager()
|
||||
|
||||
|
||||
class Objectiv(models.Model):
|
||||
objectiv_id = models.CharField(max_length=64)
|
||||
@@ -71,10 +83,18 @@ class ObjectivPoint(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)
|
||||
seed = models.CharField(max_length=32)
|
||||
seen_at = models.DateTimeField()
|
||||
|
||||
objects = models.Manager()
|
||||
for_event = EventScopedManager()
|
||||
|
||||
class Meta:
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
|
||||
@@ -31,7 +31,11 @@ def parse_objectives_and_store(logfile: LogfileSubmission) -> None:
|
||||
continue
|
||||
|
||||
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
|
||||
|
||||
objectives.append(
|
||||
|
||||
@@ -32,50 +32,51 @@ from .schemas import (
|
||||
router = Router()
|
||||
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))
|
||||
def list_puzzles(request):
|
||||
def list_puzzles(request, event_path: str):
|
||||
"""Get list of available puzzles"""
|
||||
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))
|
||||
def get_collection(request):
|
||||
def get_collection(request, event_path: str):
|
||||
"""Get the active collection details"""
|
||||
collection = get_object_or_404(SteamCollection, is_active=True)
|
||||
return collection
|
||||
return get_object_or_404(SteamCollection.for_event, is_active=True)
|
||||
|
||||
|
||||
@router.get("/submissions", response=List[SubmissionOut])
|
||||
@router.get(f"{PREFIX}/submissions", response=List[SubmissionOut])
|
||||
@decorate_view(require_game_enabled(OPUS_MAGNUM_APP_ID))
|
||||
@paginate
|
||||
def list_submissions(request):
|
||||
def list_submissions(request, event_path: str):
|
||||
"""Get paginated list of submissions"""
|
||||
return Submission.objects.prefetch_related(
|
||||
return Submission.for_event.prefetch_related(
|
||||
"responses__files", "responses__puzzle"
|
||||
).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))
|
||||
def get_submission(request, submission_id: str):
|
||||
def get_submission(request, event_path: str, submission_id: str):
|
||||
"""Get detailed submission by ID"""
|
||||
return get_object_or_404(
|
||||
Submission.objects.prefetch_related(
|
||||
Submission.for_event.prefetch_related(
|
||||
"responses__files", "responses__puzzle"
|
||||
).filter(user=request.user),
|
||||
id=submission_id,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/submissions", response=SubmissionOut)
|
||||
@router.post(f"{PREFIX}/submissions", response=SubmissionOut)
|
||||
@decorate_view(require_game_enabled(OPUS_MAGNUM_APP_ID))
|
||||
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"""
|
||||
|
||||
@@ -92,7 +93,9 @@ def create_submission(
|
||||
if data.responses:
|
||||
for puzzle in data.responses:
|
||||
if not get_object_or_404(
|
||||
SteamCollectionItem, id=puzzle.puzzle_id
|
||||
SteamCollectionItem,
|
||||
id=puzzle.puzzle_id,
|
||||
collection__event=request.event,
|
||||
).collection.accepting_submissions:
|
||||
raise HttpError(
|
||||
403, "This tournament is no longer accepting submissions"
|
||||
@@ -119,6 +122,7 @@ def create_submission(
|
||||
|
||||
# Create the submission
|
||||
submission = Submission.objects.create(
|
||||
event=request.event,
|
||||
user=request.user if request.user.is_authenticated else None,
|
||||
notes=data.notes,
|
||||
manual_validation_requested=data.manual_validation_requested
|
||||
@@ -129,7 +133,9 @@ def create_submission(
|
||||
for response_data in data.responses:
|
||||
# Get the puzzle
|
||||
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
|
||||
@@ -192,12 +198,12 @@ def create_submission(
|
||||
response.mark_for_validation("Incomplete OCR data")
|
||||
|
||||
# Reload with relations for response
|
||||
submission = Submission.objects.prefetch_related(
|
||||
submission = Submission.for_event.prefetch_related(
|
||||
"responses__files", "responses__puzzle"
|
||||
).get(id=submission.id)
|
||||
|
||||
# Invalidate results cache on successful submission
|
||||
cache.delete("api:results:results")
|
||||
cache.delete(f"api:{event_path}:results:results")
|
||||
|
||||
return submission
|
||||
|
||||
@@ -205,18 +211,22 @@ def create_submission(
|
||||
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))
|
||||
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"""
|
||||
|
||||
if not request.user.is_authenticated or not request.user.is_staff:
|
||||
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:
|
||||
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
|
||||
|
||||
# Update validated values
|
||||
@@ -236,20 +246,24 @@ def validate_response(request, response_id: int, data: ValidationIn):
|
||||
response.save()
|
||||
|
||||
# Invalidate results cache when a response is validated
|
||||
cache.delete("api:results:results")
|
||||
cache.delete(f"api:{event_path}:results:results")
|
||||
|
||||
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))
|
||||
def validate_auto(request, response_id: int):
|
||||
def validate_auto(request, event_path: str, response_id: int):
|
||||
"""Try to auto validate a puzzle response"""
|
||||
|
||||
if not request.user.is_authenticated or not request.user.is_staff:
|
||||
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():
|
||||
verify_and_validate_ocr_date_for_submission(file)
|
||||
@@ -257,9 +271,9 @@ def validate_auto(request, response_id: int):
|
||||
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))
|
||||
def list_responses_needing_validation(request):
|
||||
def list_responses_needing_validation(request, event_path: str):
|
||||
"""Get all responses that need manual validation"""
|
||||
|
||||
if not request.user.is_authenticated or not request.user.is_staff:
|
||||
@@ -267,21 +281,23 @@ def list_responses_needing_validation(request):
|
||||
|
||||
return (
|
||||
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")
|
||||
.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))
|
||||
def validate_submission(request, submission_id: str):
|
||||
def validate_submission(request, event_path: str, submission_id: str):
|
||||
"""Mark entire submission as validated"""
|
||||
|
||||
if not request.user.is_authenticated or not request.user.is_staff:
|
||||
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.validated_by = request.user
|
||||
@@ -292,44 +308,46 @@ def validate_submission(request, submission_id: str):
|
||||
submission.responses.update(needs_manual_validation=False)
|
||||
|
||||
# Reload with relations
|
||||
submission = Submission.objects.prefetch_related(
|
||||
submission = Submission.for_event.prefetch_related(
|
||||
"responses__files", "responses__puzzle"
|
||||
).get(id=submission.id)
|
||||
|
||||
# Invalidate results cache when submission is validated
|
||||
cache.delete("api:results:results")
|
||||
cache.delete(f"api:{event_path}:results:results")
|
||||
|
||||
return submission
|
||||
|
||||
|
||||
@router.delete("/submissions/{submission_id}")
|
||||
@router.delete(f"{PREFIX}/submissions/{{submission_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)"""
|
||||
|
||||
if not request.user.is_authenticated or not request.user.is_staff:
|
||||
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()
|
||||
|
||||
# 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"}
|
||||
|
||||
|
||||
@router.get("/stats")
|
||||
@router.get(f"{PREFIX}/stats")
|
||||
@decorate_view(require_game_enabled(OPUS_MAGNUM_APP_ID))
|
||||
def get_stats(request):
|
||||
def get_stats(request, event_path: str):
|
||||
"""Get submission statistics"""
|
||||
|
||||
total_submissions = Submission.objects.count()
|
||||
total_responses = PuzzleResponse.objects.count()
|
||||
needs_validation = PuzzleResponse.objects.filter(
|
||||
needs_manual_validation=True
|
||||
total_submissions = Submission.for_event.count()
|
||||
total_responses = PuzzleResponse.objects.filter(
|
||||
submission__event=request.event
|
||||
).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 {
|
||||
"total_submissions": total_submissions,
|
||||
|
||||
@@ -3,45 +3,31 @@
|
||||
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):
|
||||
"""Copy data from submissions_* tables to opus_magnum_* tables"""
|
||||
# Use raw SQL to copy data while preserving all fields
|
||||
"""Copy data from submissions_* tables to opus_magnum_* tables.
|
||||
|
||||
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:
|
||||
# Copy SteamAPIKey
|
||||
cursor.execute("""
|
||||
INSERT INTO opus_magnum_steamapikey
|
||||
SELECT * FROM submissions_steamapikey
|
||||
""")
|
||||
|
||||
# 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
|
||||
""")
|
||||
for source, dest in TABLE_PAIRS:
|
||||
if source not in existing_tables:
|
||||
continue
|
||||
cursor.execute(f"INSERT INTO {dest} SELECT * FROM {source}")
|
||||
|
||||
|
||||
def migrate_data_backward(apps, schema_editor):
|
||||
|
||||
+34
@@ -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",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -8,6 +8,9 @@ from django.db.models.expressions import Window
|
||||
from django.db.models.functions import Cast, Rank, RowNumber
|
||||
from django.db.models.query import F
|
||||
|
||||
from events.models import Event
|
||||
from events.scoping import EventScopedManager
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
@@ -108,6 +111,12 @@ class SteamAPIKey(models.Model):
|
||||
class SteamCollection(models.Model):
|
||||
"""Model representing a Steam Workshop collection"""
|
||||
|
||||
event = models.ForeignKey(
|
||||
Event,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="steam_collections",
|
||||
)
|
||||
|
||||
# Basic collection info
|
||||
steam_id = models.CharField(
|
||||
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"
|
||||
)
|
||||
|
||||
objects = models.Manager()
|
||||
for_event = EventScopedManager()
|
||||
|
||||
class Meta:
|
||||
ordering = ["-created_at"]
|
||||
verbose_name = "Steam Collection"
|
||||
@@ -257,6 +269,12 @@ class Submission(models.Model):
|
||||
# Identification
|
||||
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 = models.ForeignKey(
|
||||
User,
|
||||
@@ -299,6 +317,9 @@ class Submission(models.Model):
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
objects = models.Manager()
|
||||
for_event = EventScopedManager()
|
||||
|
||||
class Meta:
|
||||
ordering = ["-created_at"]
|
||||
verbose_name = "Submission"
|
||||
|
||||
@@ -6,7 +6,7 @@ from opus_magnum.api import router as opus_magnum_router
|
||||
from opus_magnum.schemas import UserInfoOut
|
||||
from animations.api import router as results_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 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
|
||||
|
||||
# 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("/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:
|
||||
api.add_router("/market/", market_router)
|
||||
api.add_router("", market_router, tags=["market"])
|
||||
|
||||
|
||||
# Health check endpoint
|
||||
@@ -54,12 +64,13 @@ def health_check(request):
|
||||
|
||||
# Cache management endpoint
|
||||
@api.post("/cache/clear")
|
||||
def clear_cache(request: HttpRequest):
|
||||
"""Clear all API caches (admin only)"""
|
||||
def clear_cache(request: HttpRequest, event_path: str | None = None):
|
||||
"""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:
|
||||
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)
|
||||
|
||||
return {"detail": f"Cleared {len(keys)} cache entries"}
|
||||
|
||||
@@ -46,6 +46,7 @@ INSTALLED_APPS = [
|
||||
"django_rq",
|
||||
"django_tasks_rq",
|
||||
"accounts",
|
||||
"events",
|
||||
"animations",
|
||||
"opus_magnum",
|
||||
"noita",
|
||||
@@ -62,6 +63,7 @@ MIDDLEWARE = [
|
||||
"django.middleware.csrf.CsrfViewMiddleware",
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
# Removed CASMiddleware to avoid conflicts with our custom backend
|
||||
"events.middleware.EventContextMiddleware",
|
||||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
]
|
||||
@@ -147,6 +149,14 @@ CAS_SERVER_URL = "https://polylan.ch/cas/"
|
||||
# Steam API Configuration
|
||||
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
|
||||
MEDIA_URL = "/media/"
|
||||
MEDIA_ROOT = BASE_DIR / "media"
|
||||
@@ -240,6 +250,7 @@ STORAGES = {
|
||||
}
|
||||
|
||||
|
||||
SENTRY_ENVIRONMENT = "production"
|
||||
from polylan_submitter.settingsLocal import * # noqa
|
||||
|
||||
|
||||
@@ -250,4 +261,5 @@ sentry_sdk.init(
|
||||
# Set traces_sample_rate to 1.0 to capture 100%
|
||||
# of transactions for tracing.
|
||||
traces_sample_rate=1.0,
|
||||
environment=SENTRY_ENVIRONMENT,
|
||||
)
|
||||
|
||||
@@ -16,16 +16,17 @@ Including another URLconf
|
||||
"""
|
||||
|
||||
from django.contrib import admin
|
||||
from django.http import HttpRequest
|
||||
from django.shortcuts import render
|
||||
from django.http import Http404, HttpRequest
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import path
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
from simple_cas_views import SimpleCASLoginView, SimpleCASLogoutView
|
||||
from events.models import Event
|
||||
from games.decorators import require_game_enabled
|
||||
from market.views import market_home
|
||||
from submissions.views import submission_home
|
||||
from submissions.views import submission_home, submission_view
|
||||
from .api import api
|
||||
|
||||
NOITA_APP_ID = 881100
|
||||
@@ -34,12 +35,21 @@ OPUS_MAGNUM_APP_ID = 558990
|
||||
|
||||
@login_required
|
||||
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(
|
||||
request,
|
||||
"home.html",
|
||||
{
|
||||
"event": event,
|
||||
"market_enabled": settings.MARKET_ENABLED,
|
||||
},
|
||||
)
|
||||
@@ -47,22 +57,28 @@ def home(request: HttpRequest):
|
||||
|
||||
@login_required
|
||||
@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
|
||||
|
||||
event = get_object_or_404(Event, path=event_path)
|
||||
|
||||
return render(
|
||||
request,
|
||||
"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
|
||||
@require_game_enabled(NOITA_APP_ID)
|
||||
def noita_home(request: HttpRequest):
|
||||
return render(request, "noita.html", {})
|
||||
def noita_home(request: HttpRequest, event_path: str):
|
||||
event = get_object_or_404(Event, path=event_path)
|
||||
return render(request, "noita.html", {"event": event})
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
@@ -70,10 +86,20 @@ urlpatterns = [
|
||||
path("cas/login/", SimpleCASLoginView.as_view(), name="cas_ng_login"),
|
||||
path("cas/logout/", SimpleCASLogoutView.as_view(), name="cas_ng_logout"),
|
||||
path("api/", api.urls),
|
||||
path("market", market_home, name="market.home"),
|
||||
path("opus-magnum", opus_magnum_home, name="opus-magnum.home"),
|
||||
path("noita", noita_home, name="noita.home"),
|
||||
path("submissions/<str:game>", submission_home, name="submissions.home"),
|
||||
path("<slug:event_path>/market", market_home, name="market.home"),
|
||||
path("<slug:event_path>/opus-magnum", opus_magnum_home, name="opus-magnum.home"),
|
||||
path("<slug:event_path>/noita", noita_home, name="noita.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"),
|
||||
]
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import requests
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.backends import BaseBackend
|
||||
import sentry_sdk
|
||||
|
||||
|
||||
class SimpleCASBackend(BaseBackend):
|
||||
@@ -23,9 +24,6 @@ class SimpleCASBackend(BaseBackend):
|
||||
if not cas_user_id:
|
||||
return None
|
||||
|
||||
print(f"CAS User ID: {cas_user_id}")
|
||||
print(f"CAS Attributes: {attributes}")
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
# Try to find user by CAS user ID first, then by username
|
||||
@@ -80,7 +78,7 @@ class SimpleCASBackend(BaseBackend):
|
||||
return None, None
|
||||
|
||||
except Exception as e:
|
||||
print(f"CAS validation error: {e}")
|
||||
sentry_sdk.capture_exception(e)
|
||||
return None, None
|
||||
|
||||
def get_user(self, user_id):
|
||||
|
||||
@@ -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>
|
||||
@@ -1,9 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from "vue";
|
||||
import { gamesApiListGames } from "./api";
|
||||
import type { GamesApiListGamesResponse } from "./api/types.gen";
|
||||
import { gamesApiListEventGames } from "./api";
|
||||
import type { GamesApiListEventGamesResponse } from "./api/types.gen";
|
||||
|
||||
interface Props {
|
||||
eventPath: string;
|
||||
marketEnabled?: string | boolean;
|
||||
}
|
||||
|
||||
@@ -11,11 +12,11 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
marketEnabled: true,
|
||||
});
|
||||
|
||||
const games = ref<GamesApiListGamesResponse | undefined>();
|
||||
const games = ref<GamesApiListEventGamesResponse | undefined>();
|
||||
const loading = ref(true);
|
||||
const imageErrors = ref<Set<number>>(new Set());
|
||||
|
||||
const getHeaderImage = (game: GamesApiListGamesResponse[number]) => {
|
||||
const getHeaderImage = (game: GamesApiListEventGamesResponse[number]) => {
|
||||
if (game.image) {
|
||||
return game.image;
|
||||
}
|
||||
@@ -30,7 +31,7 @@ const onImageError = (gameId: number) => {
|
||||
};
|
||||
|
||||
const navigate = (path: string) => {
|
||||
window.location.href = path;
|
||||
window.location.href = `/${props.eventPath}${path}`;
|
||||
};
|
||||
|
||||
const isMarketEnabled = () => {
|
||||
@@ -41,8 +42,10 @@ const isMarketEnabled = () => {
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
// Fetch games list
|
||||
const response = await gamesApiListGames();
|
||||
// Fetch games enabled for this event
|
||||
const response = await gamesApiListEventGames({
|
||||
path: { event_path: props.eventPath },
|
||||
});
|
||||
if (response.data) {
|
||||
games.value = response.data;
|
||||
}
|
||||
@@ -88,7 +91,8 @@ onMounted(async () => {
|
||||
</div>
|
||||
|
||||
<!-- 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">
|
||||
<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"
|
||||
|
||||
@@ -4,20 +4,28 @@ import { storeToRefs } from "pinia";
|
||||
import { useMarketStore } from "./stores/market";
|
||||
import MarketCard from "./components/MarketCard.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 { markets, userInfo, isLoading } = storeToRefs(marketStore);
|
||||
|
||||
const goHome = () => {
|
||||
window.location.href = "/";
|
||||
window.location.href = `/${props.eventPath}/`;
|
||||
};
|
||||
|
||||
const reloadPage = async () => {
|
||||
await marketStore.refreshPage();
|
||||
await marketStore.refreshPage(props.eventPath);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
marketStore.initializeMarketPage();
|
||||
marketStore.initializeMarketPage(props.eventPath);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -14,6 +14,12 @@ import {
|
||||
type SortingState,
|
||||
} from "@tanstack/vue-table";
|
||||
|
||||
interface Props {
|
||||
eventPath: string;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const noitaStore = useNoitaStore();
|
||||
const { userInfo, objectives, leaderboard, isLoadingLeaderboard, isUploading } = storeToRefs(noitaStore);
|
||||
|
||||
@@ -135,7 +141,7 @@ const submitRun = async () => {
|
||||
if (uploadedFiles.value.length === 0) return;
|
||||
|
||||
try {
|
||||
await noitaStore.submitRun(uploadedFiles.value);
|
||||
await noitaStore.submitRun(props.eventPath, uploadedFiles.value);
|
||||
uploadedFiles.value = [];
|
||||
alert("Run submitted successfully!");
|
||||
} catch (error) {
|
||||
@@ -145,12 +151,12 @@ const submitRun = async () => {
|
||||
};
|
||||
|
||||
const goHome = () => {
|
||||
window.location.href = "/";
|
||||
window.location.href = `/${props.eventPath}/`;
|
||||
};
|
||||
|
||||
const clearCache = async () => {
|
||||
try {
|
||||
await noitaStore.clearCache();
|
||||
await noitaStore.clearCache(props.eventPath);
|
||||
alert("Cache cleared successfully!");
|
||||
} catch (error) {
|
||||
console.error("Error clearing cache:", error);
|
||||
@@ -159,7 +165,7 @@ const clearCache = async () => {
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
noitaStore.loadUserData();
|
||||
noitaStore.loadUserData(props.eventPath);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -8,19 +8,24 @@ import Winners from "@/components/Winners.vue";
|
||||
import PuzzleResults from "@/components/PuzzleResults.vue";
|
||||
import TopUsersLeaderboard from "@/components/TopUsersLeaderboard.vue";
|
||||
import { polylanSubmitterApiGetUserInfo, opusMagnumApiGetCollection } from "@/api";
|
||||
import { errorHelpers } from "@/services/apiService";
|
||||
import { apiService, errorHelpers } from "@/services/apiService";
|
||||
import { usePuzzlesStore } from "@/stores/puzzles";
|
||||
import { useSubmissionsStore } from "@/stores/submissions";
|
||||
import { provideEventPath } from "@/composables/useEventPath";
|
||||
import type { PuzzleResponse, UserInfo, SteamCollection } from "@/types";
|
||||
import { useCountdown } from "@vueuse/core";
|
||||
import { storeToRefs } from "pinia";
|
||||
|
||||
const props = defineProps<{
|
||||
eventPath: string;
|
||||
collectionTitle: string;
|
||||
collectionUrl: string;
|
||||
collectionDescription: string;
|
||||
}>();
|
||||
|
||||
provideEventPath(props.eventPath);
|
||||
apiService.setEventPath(props.eventPath);
|
||||
|
||||
const puzzlesStore = usePuzzlesStore();
|
||||
const submissionsStore = useSubmissionsStore();
|
||||
|
||||
@@ -77,7 +82,9 @@ async function initialize() {
|
||||
|
||||
// Load collection data
|
||||
console.log("Loading collection...");
|
||||
const collectionResponse = await opusMagnumApiGetCollection();
|
||||
const collectionResponse = await opusMagnumApiGetCollection({
|
||||
path: { event_path: props.eventPath },
|
||||
});
|
||||
if (collectionResponse.data) {
|
||||
collection.value = collectionResponse.data as SteamCollection;
|
||||
console.log("Collection loaded:", collectionResponse.data);
|
||||
@@ -87,7 +94,7 @@ async function initialize() {
|
||||
|
||||
// Load puzzles from API using store
|
||||
console.log("Loading puzzles...");
|
||||
await puzzlesStore.loadPuzzles();
|
||||
await puzzlesStore.loadPuzzles(props.eventPath);
|
||||
console.log("Puzzles loaded:", puzzlesStore.puzzles.length);
|
||||
|
||||
// Load existing submissions using store
|
||||
@@ -129,7 +136,7 @@ const reloadPage = () => {
|
||||
};
|
||||
|
||||
const goHome = () => {
|
||||
window.location.href = "/";
|
||||
window.location.href = `/${props.eventPath}/`;
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -4,21 +4,28 @@ import { storeToRefs } from "pinia";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import { submissionsApiCreateSubmission, submissionsApiListUserSubmissions, submissionsApiCreateImageCrop, submissionsApiDeleteImageCrop, submissionsApiProcessFileOcr, submissionsApiProcessCropOcr } from "@/api";
|
||||
import { useSubmissionsWebSocket } from "@/composables/useSubmissionsWebSocket";
|
||||
import { provideEventPath } from "@/composables/useEventPath";
|
||||
|
||||
interface Props {
|
||||
gamePath?: string;
|
||||
eventPath: string;
|
||||
scopeId: string;
|
||||
scopeName?: string;
|
||||
gameName?: string;
|
||||
gamePath?: string;
|
||||
gameImage?: string;
|
||||
gameSteamId?: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
gamePath: "",
|
||||
scopeName: "Match",
|
||||
gameName: "Game",
|
||||
gamePath: "",
|
||||
gameImage: "",
|
||||
gameSteamId: "",
|
||||
});
|
||||
|
||||
provideEventPath(props.eventPath);
|
||||
|
||||
const userStore = useUserStore();
|
||||
const { userInfo } = storeToRefs(userStore);
|
||||
|
||||
@@ -238,7 +245,7 @@ const submitCrop = async () => {
|
||||
isCreatingCrop.value = true;
|
||||
try {
|
||||
const { data: newCrop } = await submissionsApiCreateImageCrop({
|
||||
path: { submission_file_id: selectedSubmissionFile.value.id },
|
||||
path: { event_path: props.eventPath, submission_file_id: selectedSubmissionFile.value.id },
|
||||
body: {
|
||||
x1: Math.round(currentCrop.value.x1),
|
||||
y1: Math.round(currentCrop.value.y1),
|
||||
@@ -270,7 +277,7 @@ const deleteCrop = async (cropId: string) => {
|
||||
|
||||
try {
|
||||
await submissionsApiDeleteImageCrop({
|
||||
path: { crop_id: cropId },
|
||||
path: { event_path: props.eventPath, crop_id: cropId },
|
||||
});
|
||||
selectedSubmissionFile.value.crops = selectedSubmissionFile.value.crops.filter(
|
||||
(c: any) => c.id !== cropId
|
||||
@@ -293,7 +300,7 @@ const processFileOcr = async () => {
|
||||
|
||||
try {
|
||||
await submissionsApiProcessFileOcr({
|
||||
path: { file_id: selectedSubmissionFile.value.id },
|
||||
path: { event_path: props.eventPath, file_id: selectedSubmissionFile.value.id },
|
||||
});
|
||||
selectedSubmissionFile.value.ocr_status = "processing";
|
||||
} catch (error) {
|
||||
@@ -305,7 +312,7 @@ const processFileOcr = async () => {
|
||||
const processCropOcr = async (cropId: string) => {
|
||||
try {
|
||||
await submissionsApiProcessCropOcr({
|
||||
path: { crop_id: cropId },
|
||||
path: { event_path: props.eventPath, crop_id: cropId },
|
||||
});
|
||||
// Update the crop status in the list
|
||||
if (selectedSubmissionFile.value?.crops) {
|
||||
@@ -398,7 +405,8 @@ const submitSubmission = async () => {
|
||||
isUploading.value = true;
|
||||
try {
|
||||
await submissionsApiCreateSubmission({
|
||||
query: { game_path: props.gamePath },
|
||||
path: { event_path: props.eventPath },
|
||||
query: { scope_id: props.scopeId },
|
||||
body: {
|
||||
files: uploadedFiles.value,
|
||||
notes: notes.value || null,
|
||||
@@ -418,7 +426,7 @@ const submitSubmission = async () => {
|
||||
};
|
||||
|
||||
const goHome = () => {
|
||||
window.location.href = "/";
|
||||
window.location.href = `/${props.eventPath}/submissions/${props.gamePath}`;
|
||||
};
|
||||
|
||||
const formatFileSize = (bytes: number) => {
|
||||
@@ -449,12 +457,11 @@ const getOCRStatusColor = (status: string) => {
|
||||
};
|
||||
|
||||
const loadUserSubmissions = async () => {
|
||||
if (!props.gamePath) return;
|
||||
|
||||
isLoadingSubmissions.value = true;
|
||||
try {
|
||||
const { data } = await submissionsApiListUserSubmissions({
|
||||
query: { game_path: props.gamePath },
|
||||
path: { event_path: props.eventPath },
|
||||
query: { scope_id: props.scopeId },
|
||||
});
|
||||
if (data && Array.isArray(data)) {
|
||||
userSubmissions.value = data;
|
||||
@@ -516,7 +523,7 @@ onUnmounted(() => {
|
||||
<i class="mdi mdi-arrow-left"></i>
|
||||
Back
|
||||
</button>
|
||||
<h1 class="text-xl font-bold">{{ gameName }} Submissions</h1>
|
||||
<h1 class="text-xl font-bold">{{ gameName }} Submissions — {{ scopeName }}</h1>
|
||||
<div class="flex-1"></div>
|
||||
|
||||
<!-- WebSocket Status Indicator -->
|
||||
@@ -549,7 +556,8 @@ onUnmounted(() => {
|
||||
</figure>
|
||||
<div class="card-body">
|
||||
<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>
|
||||
|
||||
@@ -562,7 +570,8 @@ onUnmounted(() => {
|
||||
</h2>
|
||||
|
||||
<!-- Upload Area -->
|
||||
<div @dragover="handleDragover" @dragleave="handleDragleave" @drop="handleDrop" @mouseenter="isUploadAreaHovered = true" @mouseleave="isUploadAreaHovered = false" :class="[
|
||||
<div @dragover="handleDragover" @dragleave="handleDragleave" @drop="handleDrop"
|
||||
@mouseenter="isUploadAreaHovered = true" @mouseleave="isUploadAreaHovered = false" :class="[
|
||||
'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'
|
||||
]">
|
||||
@@ -619,7 +628,7 @@ onUnmounted(() => {
|
||||
<span class="label-text-alt">0/500</span>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -693,15 +702,6 @@ onUnmounted(() => {
|
||||
processed.
|
||||
</p>
|
||||
</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>
|
||||
@@ -919,8 +919,7 @@ onUnmounted(() => {
|
||||
<p class="text-base-content/70">OCR processing failed</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="!selectedSubmissionFile?.ocr_raw_data"
|
||||
class="flex items-center justify-center py-8">
|
||||
<div v-else-if="!selectedSubmissionFile?.ocr_raw_data" class="flex items-center justify-center py-8">
|
||||
<div class="text-center">
|
||||
<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>
|
||||
@@ -928,9 +927,9 @@ onUnmounted(() => {
|
||||
</div>
|
||||
|
||||
<!-- Crops OCR Data -->
|
||||
<div v-if="selectedSubmissionFile?.crops?.length"
|
||||
class="space-y-3 pt-4 border-t border-base-300">
|
||||
<p class="text-xs font-semibold text-base-content/70">Crop OCR Results ({{ selectedSubmissionFile.crops.length }}):</p>
|
||||
<div v-if="selectedSubmissionFile?.crops?.length" class="space-y-3 pt-4 border-t border-base-300">
|
||||
<p class="text-xs font-semibold text-base-content/70">Crop OCR Results ({{
|
||||
selectedSubmissionFile.crops.length }}):</p>
|
||||
<div v-for="crop in selectedSubmissionFile.crops" :key="crop.id" class="space-y-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<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">
|
||||
{{ crop.ocr_raw_data }}
|
||||
</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>
|
||||
Processing...
|
||||
</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>
|
||||
@@ -1,4 +1,4 @@
|
||||
// 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 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 { 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, 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';
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { type Client, formDataBodySerializer, type Options as Options2, type TDataShape } from './client';
|
||||
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> & {
|
||||
/**
|
||||
@@ -28,7 +28,7 @@ export const polylanSubmitterApiHealthCheck = <ThrowOnError extends boolean = fa
|
||||
/**
|
||||
* 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 });
|
||||
|
||||
@@ -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 });
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
* 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 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
|
||||
*
|
||||
* 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
|
||||
@@ -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>({
|
||||
...formDataBodySerializer,
|
||||
url: '/api/opus-magnum/submissions',
|
||||
url: '/api/events/{event_path}/opus-magnum/submissions',
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': null,
|
||||
@@ -80,14 +85,14 @@ export const opusMagnumApiCreateSubmission = <ThrowOnError extends boolean = fal
|
||||
*
|
||||
* 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 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
|
||||
@@ -95,7 +100,7 @@ export const opusMagnumApiGetSubmission = <ThrowOnError extends boolean = false>
|
||||
* Manually validate a puzzle response
|
||||
*/
|
||||
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,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -108,52 +113,52 @@ export const opusMagnumApiValidateResponse = <ThrowOnError extends boolean = fal
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* 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 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
|
||||
*/
|
||||
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
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* 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
|
||||
*/
|
||||
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
|
||||
@@ -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.
|
||||
*/
|
||||
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
|
||||
@@ -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>({
|
||||
...formDataBodySerializer,
|
||||
url: '/api/noita/submit',
|
||||
url: '/api/events/{event_path}/noita/submit',
|
||||
...options,
|
||||
headers: {
|
||||
'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
|
||||
*/
|
||||
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,
|
||||
url: '/api/submissions/submission/create',
|
||||
url: '/api/events/{event_path}/submissions/submission/create',
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': null,
|
||||
@@ -207,23 +217,23 @@ export const submissionsApiCreateSubmission = <ThrowOnError extends boolean = fa
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -234,38 +244,38 @@ export const submissionsApiCreateImageCrop = <ThrowOnError extends boolean = fal
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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 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 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 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
|
||||
@@ -273,7 +283,7 @@ export const marketApiCloseMarket = <ThrowOnError extends boolean = false>(optio
|
||||
* 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>({
|
||||
url: '/api/market/{market_uuid}/actions/resolve',
|
||||
url: '/api/events/{event_path}/market/{market_uuid}/actions/resolve',
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -287,7 +297,7 @@ export const marketApiResolveMarket = <ThrowOnError extends boolean = false>(opt
|
||||
* 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>({
|
||||
url: '/api/market/{market_uuid}/bets',
|
||||
url: '/api/events/{event_path}/market/{market_uuid}/bets',
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
@@ -52,6 +52,36 @@ export type UserInfoOut = {
|
||||
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
|
||||
*
|
||||
@@ -386,9 +416,9 @@ export type SubmissionOut = {
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* Game Id
|
||||
* Scope Id
|
||||
*/
|
||||
game_id?: number | null;
|
||||
scope_id: string;
|
||||
/**
|
||||
* Notes
|
||||
*/
|
||||
@@ -891,29 +921,25 @@ export type NoitaSubmissionOut = {
|
||||
};
|
||||
|
||||
/**
|
||||
* GameOut
|
||||
* ScopeOut
|
||||
*/
|
||||
export type GameOut = {
|
||||
export type ScopeOut = {
|
||||
/**
|
||||
* Id
|
||||
*/
|
||||
id: number;
|
||||
/**
|
||||
* Steam App Id
|
||||
*/
|
||||
steam_app_id?: number | null;
|
||||
id: string;
|
||||
/**
|
||||
* Name
|
||||
*/
|
||||
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 = {
|
||||
body?: never;
|
||||
path?: never;
|
||||
query?: never;
|
||||
query?: {
|
||||
/**
|
||||
* Event Path
|
||||
*/
|
||||
event_path?: string | null;
|
||||
};
|
||||
url: '/api/cache/clear';
|
||||
};
|
||||
|
||||
@@ -1129,11 +1160,34 @@ export type PolylanSubmitterApiGetUserInfoResponses = {
|
||||
|
||||
export type PolylanSubmitterApiGetUserInfoResponse = PolylanSubmitterApiGetUserInfoResponses[keyof PolylanSubmitterApiGetUserInfoResponses];
|
||||
|
||||
export type OpusMagnumApiListPuzzlesData = {
|
||||
export type GamesApiListGamesData = {
|
||||
body?: never;
|
||||
path?: 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 = {
|
||||
@@ -1149,9 +1203,14 @@ export type OpusMagnumApiListPuzzlesResponse = OpusMagnumApiListPuzzlesResponses
|
||||
|
||||
export type OpusMagnumApiGetCollectionData = {
|
||||
body?: never;
|
||||
path?: never;
|
||||
path: {
|
||||
/**
|
||||
* Event Path
|
||||
*/
|
||||
event_path: string;
|
||||
};
|
||||
query?: never;
|
||||
url: '/api/opus-magnum/collection';
|
||||
url: '/api/events/{event_path}/opus-magnum/collection';
|
||||
};
|
||||
|
||||
export type OpusMagnumApiGetCollectionResponses = {
|
||||
@@ -1165,7 +1224,12 @@ export type OpusMagnumApiGetCollectionResponse = OpusMagnumApiGetCollectionRespo
|
||||
|
||||
export type OpusMagnumApiListSubmissionsData = {
|
||||
body?: never;
|
||||
path?: never;
|
||||
path: {
|
||||
/**
|
||||
* Event Path
|
||||
*/
|
||||
event_path: string;
|
||||
};
|
||||
query?: {
|
||||
/**
|
||||
* Limit
|
||||
@@ -1176,7 +1240,7 @@ export type OpusMagnumApiListSubmissionsData = {
|
||||
*/
|
||||
offset?: number;
|
||||
};
|
||||
url: '/api/opus-magnum/submissions';
|
||||
url: '/api/events/{event_path}/opus-magnum/submissions';
|
||||
};
|
||||
|
||||
export type OpusMagnumApiListSubmissionsResponses = {
|
||||
@@ -1199,9 +1263,14 @@ export type OpusMagnumApiCreateSubmissionData = {
|
||||
files: Array<Blob | File>;
|
||||
data: SubmissionIn;
|
||||
};
|
||||
path?: never;
|
||||
path: {
|
||||
/**
|
||||
* Event Path
|
||||
*/
|
||||
event_path: string;
|
||||
};
|
||||
query?: never;
|
||||
url: '/api/opus-magnum/submissions';
|
||||
url: '/api/events/{event_path}/opus-magnum/submissions';
|
||||
};
|
||||
|
||||
export type OpusMagnumApiCreateSubmissionResponses = {
|
||||
@@ -1216,13 +1285,17 @@ export type OpusMagnumApiCreateSubmissionResponse = OpusMagnumApiCreateSubmissio
|
||||
export type OpusMagnumApiDeleteSubmissionData = {
|
||||
body?: never;
|
||||
path: {
|
||||
/**
|
||||
* Event Path
|
||||
*/
|
||||
event_path: string;
|
||||
/**
|
||||
* Submission Id
|
||||
*/
|
||||
submission_id: string;
|
||||
};
|
||||
query?: never;
|
||||
url: '/api/opus-magnum/submissions/{submission_id}';
|
||||
url: '/api/events/{event_path}/opus-magnum/submissions/{submission_id}';
|
||||
};
|
||||
|
||||
export type OpusMagnumApiDeleteSubmissionResponses = {
|
||||
@@ -1235,13 +1308,17 @@ export type OpusMagnumApiDeleteSubmissionResponses = {
|
||||
export type OpusMagnumApiGetSubmissionData = {
|
||||
body?: never;
|
||||
path: {
|
||||
/**
|
||||
* Event Path
|
||||
*/
|
||||
event_path: string;
|
||||
/**
|
||||
* Submission Id
|
||||
*/
|
||||
submission_id: string;
|
||||
};
|
||||
query?: never;
|
||||
url: '/api/opus-magnum/submissions/{submission_id}';
|
||||
url: '/api/events/{event_path}/opus-magnum/submissions/{submission_id}';
|
||||
};
|
||||
|
||||
export type OpusMagnumApiGetSubmissionResponses = {
|
||||
@@ -1256,13 +1333,17 @@ export type OpusMagnumApiGetSubmissionResponse = OpusMagnumApiGetSubmissionRespo
|
||||
export type OpusMagnumApiValidateResponseData = {
|
||||
body: ValidationIn;
|
||||
path: {
|
||||
/**
|
||||
* Event Path
|
||||
*/
|
||||
event_path: string;
|
||||
/**
|
||||
* Response Id
|
||||
*/
|
||||
response_id: number;
|
||||
};
|
||||
query?: never;
|
||||
url: '/api/opus-magnum/responses/{response_id}/validate';
|
||||
url: '/api/events/{event_path}/opus-magnum/responses/{response_id}/validate';
|
||||
};
|
||||
|
||||
export type OpusMagnumApiValidateResponseResponses = {
|
||||
@@ -1277,13 +1358,17 @@ export type OpusMagnumApiValidateResponseResponse = OpusMagnumApiValidateRespons
|
||||
export type OpusMagnumApiValidateAutoData = {
|
||||
body?: never;
|
||||
path: {
|
||||
/**
|
||||
* Event Path
|
||||
*/
|
||||
event_path: string;
|
||||
/**
|
||||
* Response Id
|
||||
*/
|
||||
response_id: number;
|
||||
};
|
||||
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 = {
|
||||
@@ -1297,9 +1382,14 @@ export type OpusMagnumApiValidateAutoResponse = OpusMagnumApiValidateAutoRespons
|
||||
|
||||
export type OpusMagnumApiListResponsesNeedingValidationData = {
|
||||
body?: never;
|
||||
path?: never;
|
||||
path: {
|
||||
/**
|
||||
* Event Path
|
||||
*/
|
||||
event_path: string;
|
||||
};
|
||||
query?: never;
|
||||
url: '/api/opus-magnum/responses/needs-validation';
|
||||
url: '/api/events/{event_path}/opus-magnum/responses/needs-validation';
|
||||
};
|
||||
|
||||
export type OpusMagnumApiListResponsesNeedingValidationResponses = {
|
||||
@@ -1316,13 +1406,17 @@ export type OpusMagnumApiListResponsesNeedingValidationResponse = OpusMagnumApiL
|
||||
export type OpusMagnumApiValidateSubmissionData = {
|
||||
body?: never;
|
||||
path: {
|
||||
/**
|
||||
* Event Path
|
||||
*/
|
||||
event_path: string;
|
||||
/**
|
||||
* Submission Id
|
||||
*/
|
||||
submission_id: string;
|
||||
};
|
||||
query?: never;
|
||||
url: '/api/opus-magnum/submissions/{submission_id}/validate';
|
||||
url: '/api/events/{event_path}/opus-magnum/submissions/{submission_id}/validate';
|
||||
};
|
||||
|
||||
export type OpusMagnumApiValidateSubmissionResponses = {
|
||||
@@ -1336,9 +1430,14 @@ export type OpusMagnumApiValidateSubmissionResponse = OpusMagnumApiValidateSubmi
|
||||
|
||||
export type OpusMagnumApiGetStatsData = {
|
||||
body?: never;
|
||||
path?: never;
|
||||
path: {
|
||||
/**
|
||||
* Event Path
|
||||
*/
|
||||
event_path: string;
|
||||
};
|
||||
query?: never;
|
||||
url: '/api/opus-magnum/stats';
|
||||
url: '/api/events/{event_path}/opus-magnum/stats';
|
||||
};
|
||||
|
||||
export type OpusMagnumApiGetStatsResponses = {
|
||||
@@ -1350,9 +1449,14 @@ export type OpusMagnumApiGetStatsResponses = {
|
||||
|
||||
export type AnimationsApiResultsData = {
|
||||
body?: never;
|
||||
path?: never;
|
||||
path: {
|
||||
/**
|
||||
* Event Path
|
||||
*/
|
||||
event_path: string;
|
||||
};
|
||||
query?: never;
|
||||
url: '/api/results/results';
|
||||
url: '/api/events/{event_path}/results/results';
|
||||
};
|
||||
|
||||
export type AnimationsApiResultsResponses = {
|
||||
@@ -1366,14 +1470,19 @@ export type AnimationsApiResultsResponse = AnimationsApiResultsResponses[keyof A
|
||||
|
||||
export type AnimationsApiTopSubmissionsData = {
|
||||
body?: never;
|
||||
path?: never;
|
||||
path: {
|
||||
/**
|
||||
* Event Path
|
||||
*/
|
||||
event_path: string;
|
||||
};
|
||||
query?: {
|
||||
/**
|
||||
* Limit
|
||||
*/
|
||||
limit?: number;
|
||||
};
|
||||
url: '/api/results/top-submissions';
|
||||
url: '/api/events/{event_path}/results/top-submissions';
|
||||
};
|
||||
|
||||
export type AnimationsApiTopSubmissionsResponses = {
|
||||
@@ -1387,14 +1496,19 @@ export type AnimationsApiTopSubmissionsResponse = AnimationsApiTopSubmissionsRes
|
||||
|
||||
export type AnimationsApiPuzzleResultsData = {
|
||||
body?: never;
|
||||
path?: never;
|
||||
path: {
|
||||
/**
|
||||
* Event Path
|
||||
*/
|
||||
event_path: string;
|
||||
};
|
||||
query?: {
|
||||
/**
|
||||
* Limit
|
||||
*/
|
||||
limit?: number;
|
||||
};
|
||||
url: '/api/results/puzzle-results';
|
||||
url: '/api/events/{event_path}/results/puzzle-results';
|
||||
};
|
||||
|
||||
export type AnimationsApiPuzzleResultsResponses = {
|
||||
@@ -1408,9 +1522,14 @@ export type AnimationsApiPuzzleResultsResponse = AnimationsApiPuzzleResultsRespo
|
||||
|
||||
export type NoitaApiGetResultsData = {
|
||||
body?: never;
|
||||
path?: never;
|
||||
path: {
|
||||
/**
|
||||
* Event Path
|
||||
*/
|
||||
event_path: string;
|
||||
};
|
||||
query?: never;
|
||||
url: '/api/noita/results';
|
||||
url: '/api/events/{event_path}/noita/results';
|
||||
};
|
||||
|
||||
export type NoitaApiGetResultsResponses = {
|
||||
@@ -1424,9 +1543,14 @@ export type NoitaApiGetResultsResponse = NoitaApiGetResultsResponses[keyof Noita
|
||||
|
||||
export type NoitaApiGetLeaderboardData = {
|
||||
body?: never;
|
||||
path?: never;
|
||||
path: {
|
||||
/**
|
||||
* Event Path
|
||||
*/
|
||||
event_path: string;
|
||||
};
|
||||
query?: never;
|
||||
url: '/api/noita/leaderboard';
|
||||
url: '/api/events/{event_path}/noita/leaderboard';
|
||||
};
|
||||
|
||||
export type NoitaApiGetLeaderboardResponses = {
|
||||
@@ -1448,9 +1572,14 @@ export type NoitaApiSubmitLogFileData = {
|
||||
*/
|
||||
file: Blob | File;
|
||||
};
|
||||
path?: never;
|
||||
path: {
|
||||
/**
|
||||
* Event Path
|
||||
*/
|
||||
event_path: string;
|
||||
};
|
||||
query?: never;
|
||||
url: '/api/noita/submit';
|
||||
url: '/api/events/{event_path}/noita/submit';
|
||||
};
|
||||
|
||||
export type NoitaApiSubmitLogFileErrors = {
|
||||
@@ -1475,14 +1604,19 @@ export type NoitaApiSubmitLogFileResponses = {
|
||||
|
||||
export type NoitaApiSubmitLogFileResponse = NoitaApiSubmitLogFileResponses[keyof NoitaApiSubmitLogFileResponses];
|
||||
|
||||
export type GamesApiListGamesData = {
|
||||
export type GamesApiListEventGamesData = {
|
||||
body?: never;
|
||||
path?: never;
|
||||
path: {
|
||||
/**
|
||||
* Event Path
|
||||
*/
|
||||
event_path: string;
|
||||
};
|
||||
query?: never;
|
||||
url: '/api/games/';
|
||||
url: '/api/events/{event_path}/games';
|
||||
};
|
||||
|
||||
export type GamesApiListGamesResponses = {
|
||||
export type GamesApiListEventGamesResponses = {
|
||||
/**
|
||||
* Response
|
||||
*
|
||||
@@ -1491,7 +1625,48 @@ export type GamesApiListGamesResponses = {
|
||||
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 = {
|
||||
/**
|
||||
@@ -1507,16 +1682,34 @@ export type SubmissionsApiCreateSubmissionData = {
|
||||
*/
|
||||
notes?: string | null;
|
||||
};
|
||||
path?: never;
|
||||
path: {
|
||||
/**
|
||||
* Event Path
|
||||
*/
|
||||
event_path: string;
|
||||
};
|
||||
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 = {
|
||||
/**
|
||||
* OK
|
||||
@@ -1528,16 +1721,34 @@ export type SubmissionsApiCreateSubmissionResponse = SubmissionsApiCreateSubmiss
|
||||
|
||||
export type SubmissionsApiListUserSubmissionsData = {
|
||||
body?: never;
|
||||
path?: never;
|
||||
path: {
|
||||
/**
|
||||
* Event Path
|
||||
*/
|
||||
event_path: string;
|
||||
};
|
||||
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 = {
|
||||
/**
|
||||
* Response
|
||||
@@ -1551,14 +1762,19 @@ export type SubmissionsApiListUserSubmissionsResponse = SubmissionsApiListUserSu
|
||||
|
||||
export type SubmissionsApiListSubmissionsByGameData = {
|
||||
body?: never;
|
||||
path?: never;
|
||||
path: {
|
||||
/**
|
||||
* Event Path
|
||||
*/
|
||||
event_path: string;
|
||||
};
|
||||
query: {
|
||||
/**
|
||||
* Game Path
|
||||
*/
|
||||
game_path: string;
|
||||
};
|
||||
url: '/api/submissions/submission/by/game';
|
||||
url: '/api/events/{event_path}/submissions/submission/by/game';
|
||||
};
|
||||
|
||||
export type SubmissionsApiListSubmissionsByGameResponses = {
|
||||
@@ -1575,13 +1791,17 @@ export type SubmissionsApiListSubmissionsByGameResponse = SubmissionsApiListSubm
|
||||
export type SubmissionsApiGetSubmissionData = {
|
||||
body?: never;
|
||||
path: {
|
||||
/**
|
||||
* Event Path
|
||||
*/
|
||||
event_path: string;
|
||||
/**
|
||||
* Submission Id
|
||||
*/
|
||||
submission_id: string;
|
||||
};
|
||||
query?: never;
|
||||
url: '/api/submissions/submission/{submission_id}';
|
||||
url: '/api/events/{event_path}/submissions/submission/{submission_id}';
|
||||
};
|
||||
|
||||
export type SubmissionsApiGetSubmissionResponses = {
|
||||
@@ -1596,13 +1816,17 @@ export type SubmissionsApiGetSubmissionResponse = SubmissionsApiGetSubmissionRes
|
||||
export type SubmissionsApiCreateImageCropData = {
|
||||
body: ImageCropIn;
|
||||
path: {
|
||||
/**
|
||||
* Event Path
|
||||
*/
|
||||
event_path: string;
|
||||
/**
|
||||
* Submission File Id
|
||||
*/
|
||||
submission_file_id: string;
|
||||
};
|
||||
query?: never;
|
||||
url: '/api/submissions/crops/s/{submission_file_id}';
|
||||
url: '/api/events/{event_path}/submissions/crops/s/{submission_file_id}';
|
||||
};
|
||||
|
||||
export type SubmissionsApiCreateImageCropResponses = {
|
||||
@@ -1617,13 +1841,17 @@ export type SubmissionsApiCreateImageCropResponse = SubmissionsApiCreateImageCro
|
||||
export type SubmissionsApiDeleteImageCropData = {
|
||||
body?: never;
|
||||
path: {
|
||||
/**
|
||||
* Event Path
|
||||
*/
|
||||
event_path: string;
|
||||
/**
|
||||
* Crop Id
|
||||
*/
|
||||
crop_id: string;
|
||||
};
|
||||
query?: never;
|
||||
url: '/api/submissions/crops/{crop_id}';
|
||||
url: '/api/events/{event_path}/submissions/crops/{crop_id}';
|
||||
};
|
||||
|
||||
export type SubmissionsApiDeleteImageCropResponses = {
|
||||
@@ -1642,13 +1870,17 @@ export type SubmissionsApiDeleteImageCropResponse = SubmissionsApiDeleteImageCro
|
||||
export type SubmissionsApiProcessFileOcrData = {
|
||||
body?: never;
|
||||
path: {
|
||||
/**
|
||||
* Event Path
|
||||
*/
|
||||
event_path: string;
|
||||
/**
|
||||
* File Id
|
||||
*/
|
||||
file_id: string;
|
||||
};
|
||||
query?: never;
|
||||
url: '/api/submissions/files/{file_id}/process-ocr';
|
||||
url: '/api/events/{event_path}/submissions/files/{file_id}/process-ocr';
|
||||
};
|
||||
|
||||
export type SubmissionsApiProcessFileOcrResponses = {
|
||||
@@ -1667,13 +1899,17 @@ export type SubmissionsApiProcessFileOcrResponse = SubmissionsApiProcessFileOcrR
|
||||
export type SubmissionsApiProcessCropOcrData = {
|
||||
body?: never;
|
||||
path: {
|
||||
/**
|
||||
* Event Path
|
||||
*/
|
||||
event_path: string;
|
||||
/**
|
||||
* Crop Id
|
||||
*/
|
||||
crop_id: string;
|
||||
};
|
||||
query?: never;
|
||||
url: '/api/submissions/crops/{crop_id}/process-ocr';
|
||||
url: '/api/events/{event_path}/submissions/crops/{crop_id}/process-ocr';
|
||||
};
|
||||
|
||||
export type SubmissionsApiProcessCropOcrResponses = {
|
||||
@@ -1691,9 +1927,14 @@ export type SubmissionsApiProcessCropOcrResponse = SubmissionsApiProcessCropOcrR
|
||||
|
||||
export type MarketApiListMarketsData = {
|
||||
body?: never;
|
||||
path?: never;
|
||||
path: {
|
||||
/**
|
||||
* Event Path
|
||||
*/
|
||||
event_path: string;
|
||||
};
|
||||
query?: never;
|
||||
url: '/api/market/';
|
||||
url: '/api/events/{event_path}/market/';
|
||||
};
|
||||
|
||||
export type MarketApiListMarketsResponses = {
|
||||
@@ -1709,9 +1950,14 @@ export type MarketApiListMarketsResponse = MarketApiListMarketsResponses[keyof M
|
||||
|
||||
export type MarketApiListUserBetsData = {
|
||||
body?: never;
|
||||
path?: never;
|
||||
path: {
|
||||
/**
|
||||
* Event Path
|
||||
*/
|
||||
event_path: string;
|
||||
};
|
||||
query?: never;
|
||||
url: '/api/market/user/bets';
|
||||
url: '/api/events/{event_path}/market/user/bets';
|
||||
};
|
||||
|
||||
export type MarketApiListUserBetsResponses = {
|
||||
@@ -1728,13 +1974,17 @@ export type MarketApiListUserBetsResponse = MarketApiListUserBetsResponses[keyof
|
||||
export type MarketApiCloseMarketData = {
|
||||
body?: never;
|
||||
path: {
|
||||
/**
|
||||
* Event Path
|
||||
*/
|
||||
event_path: string;
|
||||
/**
|
||||
* Market Uuid
|
||||
*/
|
||||
market_uuid: string;
|
||||
};
|
||||
query?: never;
|
||||
url: '/api/market/{market_uuid}/actions/close';
|
||||
url: '/api/events/{event_path}/market/{market_uuid}/actions/close';
|
||||
};
|
||||
|
||||
export type MarketApiCloseMarketResponses = {
|
||||
@@ -1747,13 +1997,17 @@ export type MarketApiCloseMarketResponses = {
|
||||
export type MarketApiResolveMarketData = {
|
||||
body: ResolveMarketSchema;
|
||||
path: {
|
||||
/**
|
||||
* Event Path
|
||||
*/
|
||||
event_path: string;
|
||||
/**
|
||||
* Market Uuid
|
||||
*/
|
||||
market_uuid: string;
|
||||
};
|
||||
query?: never;
|
||||
url: '/api/market/{market_uuid}/actions/resolve';
|
||||
url: '/api/events/{event_path}/market/{market_uuid}/actions/resolve';
|
||||
};
|
||||
|
||||
export type MarketApiResolveMarketResponses = {
|
||||
@@ -1768,13 +2022,17 @@ export type MarketApiResolveMarketResponse = MarketApiResolveMarketResponses[key
|
||||
export type MarketApiCreateBetData = {
|
||||
body: UserBetCreateSchema;
|
||||
path: {
|
||||
/**
|
||||
* Event Path
|
||||
*/
|
||||
event_path: string;
|
||||
/**
|
||||
* Market Uuid
|
||||
*/
|
||||
market_uuid: string;
|
||||
};
|
||||
query?: never;
|
||||
url: '/api/market/{market_uuid}/bets';
|
||||
url: '/api/events/{event_path}/market/{market_uuid}/bets';
|
||||
};
|
||||
|
||||
export type MarketApiCreateBetResponses = {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ref, computed, onMounted } from "vue";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { marketApiCreateBet, marketApiCloseMarket, marketApiResolveMarket } from "../api";
|
||||
import { useMarketStore } from "../stores/market";
|
||||
import { useEventPath } from "../composables/useEventPath";
|
||||
import type { Market, MarketOption } from "../types";
|
||||
import type { UserBetSchema } from "../api/types.gen";
|
||||
|
||||
@@ -14,6 +15,7 @@ const emit = defineEmits<{
|
||||
refresh: [];
|
||||
}>();
|
||||
|
||||
const eventPath = useEventPath();
|
||||
const marketStore = useMarketStore();
|
||||
const { userInfo, userBets } = storeToRefs(marketStore);
|
||||
|
||||
@@ -82,7 +84,7 @@ const closeMarket = async () => {
|
||||
|
||||
try {
|
||||
await marketApiCloseMarket({
|
||||
path: { market_uuid: props.market.uuid },
|
||||
path: { event_path: eventPath, market_uuid: props.market.uuid },
|
||||
});
|
||||
emit("refresh");
|
||||
} catch (e) {
|
||||
@@ -103,7 +105,7 @@ const resolveMarket = async () => {
|
||||
|
||||
try {
|
||||
await marketApiResolveMarket({
|
||||
path: { market_uuid: props.market.uuid },
|
||||
path: { event_path: eventPath, market_uuid: props.market.uuid },
|
||||
body: { winning_option_uuid: selectedWinningOption.value },
|
||||
});
|
||||
emit("refresh");
|
||||
@@ -132,6 +134,7 @@ const placeBet = async () => {
|
||||
try {
|
||||
const response = await marketApiCreateBet({
|
||||
path: {
|
||||
event_path: eventPath,
|
||||
market_uuid: props.market.uuid
|
||||
},
|
||||
body: {
|
||||
@@ -142,7 +145,7 @@ const placeBet = async () => {
|
||||
|
||||
if (!response.error) {
|
||||
// Reload user bets to reflect the new bet
|
||||
await marketStore.loadUserBets();
|
||||
await marketStore.loadUserBets(eventPath);
|
||||
updateExistingBet();
|
||||
betAmount.value = 0;
|
||||
} else {
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from "vue";
|
||||
import RankBadge from "./RankBadge.vue";
|
||||
import { animationsApiResults, polylanSubmitterApiClearCache, polylanSubmitterApiGetUserInfo } from "@/api";
|
||||
import { useEventPath } from "@/composables/useEventPath";
|
||||
|
||||
const eventPath = useEventPath();
|
||||
|
||||
interface User {
|
||||
id: number;
|
||||
@@ -65,10 +69,10 @@ const userInfo = ref({
|
||||
const fetchResults = async () => {
|
||||
isLoading.value = true;
|
||||
try {
|
||||
const response = await fetch("/api/results/results");
|
||||
if (!response.ok) throw new Error("Failed to fetch results");
|
||||
const response = await animationsApiResults({ path: { event_path: eventPath } });
|
||||
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) {
|
||||
console.error("Error fetching results:", error);
|
||||
} finally {
|
||||
@@ -117,16 +121,16 @@ const togglePuzzleExpanded = (puzzleId: number) => {
|
||||
|
||||
const clearCache = async () => {
|
||||
try {
|
||||
const response = await fetch("/api/cache/clear", {
|
||||
method: "POST",
|
||||
const response = await polylanSubmitterApiClearCache({
|
||||
query: { event_path: eventPath },
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
if (!response.error) {
|
||||
alert("Cache cleared successfully!");
|
||||
await fetchResults();
|
||||
} else {
|
||||
const error = await response.json();
|
||||
alert(`Error clearing cache: ${error.detail || "Unknown error"}`);
|
||||
const detail = (response.error as { detail?: string })?.detail;
|
||||
alert(`Error clearing cache: ${detail || "Unknown error"}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error clearing cache:", error);
|
||||
@@ -136,11 +140,10 @@ const clearCache = async () => {
|
||||
|
||||
const loadUserData = async () => {
|
||||
try {
|
||||
const response = await fetch("/api/user");
|
||||
if (response.ok) {
|
||||
const user = await response.json();
|
||||
if (user.is_authenticated) {
|
||||
userInfo.value.username = user.username;
|
||||
const response = await polylanSubmitterApiGetUserInfo();
|
||||
const user = response.data;
|
||||
if (user?.is_authenticated) {
|
||||
userInfo.value.username = user.username ?? "";
|
||||
userInfo.value.isStaff = user.is_staff || false;
|
||||
|
||||
await fetchResults();
|
||||
@@ -155,7 +158,6 @@ const loadUserData = async () => {
|
||||
userInfo.value.puzzlesSolved = ranking[userRankIndex].puzzlesSolved;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading user data:", error);
|
||||
await fetchResults();
|
||||
|
||||
@@ -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 {
|
||||
id: string
|
||||
game_id?: number
|
||||
scope_id?: string
|
||||
notes?: string
|
||||
files?: SubmissionFileUpdate[]
|
||||
created_at: string
|
||||
|
||||
@@ -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)
|
||||
@@ -37,12 +37,28 @@ interface SubmissionStats {
|
||||
|
||||
// API Service Class
|
||||
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>(
|
||||
endpoint: string,
|
||||
options: RequestInit = {}
|
||||
): Promise<ApiResponse<T>> {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
|
||||
const response = await fetch(this.buildUrl(endpoint), {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers,
|
||||
@@ -76,7 +92,7 @@ export class ApiService {
|
||||
formData: FormData
|
||||
): Promise<ApiResponse<T>> {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
|
||||
const response = await fetch(this.buildUrl(endpoint), {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
})
|
||||
|
||||
@@ -25,9 +25,9 @@ export const useMarketStore = defineStore('market', () => {
|
||||
}
|
||||
}
|
||||
|
||||
const loadMarkets = async () => {
|
||||
const loadMarkets = async (eventPath: string) => {
|
||||
try {
|
||||
const response = await marketApiListMarkets()
|
||||
const response = await marketApiListMarkets({ path: { event_path: eventPath } })
|
||||
if (response.data) {
|
||||
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 {
|
||||
const response = await marketApiListUserBets()
|
||||
const response = await marketApiListUserBets({ path: { event_path: eventPath } })
|
||||
if (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
|
||||
error.value = ''
|
||||
|
||||
try {
|
||||
await Promise.all([
|
||||
loadUserInfo(),
|
||||
loadMarkets(),
|
||||
loadMarkets(eventPath),
|
||||
])
|
||||
|
||||
// Load user bets if authenticated
|
||||
if (userInfo.value?.is_authenticated) {
|
||||
await loadUserBets()
|
||||
await loadUserBets(eventPath)
|
||||
}
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const refreshPage = async () => {
|
||||
const refreshPage = async (eventPath: string) => {
|
||||
await Promise.all([
|
||||
loadUserInfo(),
|
||||
loadMarkets(),
|
||||
userInfo.value?.is_authenticated ? loadUserBets() : Promise.resolve(),
|
||||
loadMarkets(eventPath),
|
||||
userInfo.value?.is_authenticated ? loadUserBets(eventPath) : Promise.resolve(),
|
||||
])
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import {
|
||||
noitaApiGetResults,
|
||||
noitaApiGetLeaderboard,
|
||||
noitaApiSubmitLogFile,
|
||||
polylanSubmitterApiGetUserInfo,
|
||||
polylanSubmitterApiClearCache,
|
||||
} from '@/api'
|
||||
import type { ObjectivResultOut, LeaderboardEntryOut } from '@/api/types.gen'
|
||||
|
||||
export type Objective = ObjectivResultOut
|
||||
@@ -30,12 +37,12 @@ export const useNoitaStore = defineStore('noita', () => {
|
||||
const error = ref<string>('')
|
||||
|
||||
// Actions
|
||||
const fetchUserResults = async () => {
|
||||
const fetchUserResults = async (eventPath: string) => {
|
||||
try {
|
||||
const response = await fetch('/api/noita/results')
|
||||
if (!response.ok) throw new Error('Failed to fetch results')
|
||||
const response = await noitaApiGetResults({ path: { event_path: eventPath } })
|
||||
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.deathsCount = results.deaths_count
|
||||
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
|
||||
try {
|
||||
const response = await fetch('/api/noita/leaderboard')
|
||||
if (!response.ok) throw new Error('Failed to fetch leaderboard')
|
||||
const response = await noitaApiGetLeaderboard({ path: { event_path: eventPath } })
|
||||
if (response.error || !response.data) throw new Error('Failed to fetch leaderboard')
|
||||
|
||||
const data = await response.json()
|
||||
leaderboard.value = data.leaderboard
|
||||
leaderboard.value = response.data.leaderboard
|
||||
|
||||
// Find current user's rank
|
||||
const userRank = leaderboard.value.find(
|
||||
@@ -73,48 +79,41 @@ export const useNoitaStore = defineStore('noita', () => {
|
||||
}
|
||||
}
|
||||
|
||||
const loadUserData = async () => {
|
||||
const loadUserData = async (eventPath: string) => {
|
||||
try {
|
||||
const response = await fetch('/api/user')
|
||||
if (response.ok) {
|
||||
const user = await response.json()
|
||||
if (user.is_authenticated) {
|
||||
userInfo.value.username = user.username
|
||||
userInfo.value.isStaff = user.is_staff || false
|
||||
}
|
||||
const response = await polylanSubmitterApiGetUserInfo()
|
||||
if (response.data?.is_authenticated) {
|
||||
userInfo.value.username = response.data.username ?? ''
|
||||
userInfo.value.isStaff = response.data.is_staff || false
|
||||
}
|
||||
} catch (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
|
||||
|
||||
isUploading.value = true
|
||||
try {
|
||||
for (const file of files) {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
|
||||
const response = await fetch('/api/noita/submit', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
const response = await noitaApiSubmitLogFile({
|
||||
path: { event_path: eventPath },
|
||||
body: { file },
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
throw new Error(errorData.detail || 'Unknown error')
|
||||
if (response.error) {
|
||||
const detail = (response.error as { detail?: string })?.detail
|
||||
throw new Error(detail || 'Unknown error')
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
console.log('Submission successful:', result)
|
||||
console.log('Submission successful:', response.data)
|
||||
}
|
||||
|
||||
// Refresh objectives, score, and rank after successful submission
|
||||
await Promise.all([fetchUserResults(), fetchLeaderboard()])
|
||||
await Promise.all([fetchUserResults(eventPath), fetchLeaderboard(eventPath)])
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Unknown error'
|
||||
error.value = `Error submitting run: ${errorMessage}`
|
||||
@@ -124,18 +123,18 @@ export const useNoitaStore = defineStore('noita', () => {
|
||||
}
|
||||
}
|
||||
|
||||
const clearCache = async () => {
|
||||
const clearCache = async (eventPath: string) => {
|
||||
try {
|
||||
const response = await fetch('/api/cache/clear', {
|
||||
method: 'POST',
|
||||
const response = await polylanSubmitterApiClearCache({
|
||||
query: { event_path: eventPath },
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
throw new Error(errorData.detail || 'Unknown error')
|
||||
if (response.error) {
|
||||
const detail = (response.error as { detail?: string })?.detail
|
||||
throw new Error(detail || 'Unknown error')
|
||||
}
|
||||
|
||||
await Promise.all([fetchUserResults(), fetchLeaderboard()])
|
||||
await Promise.all([fetchUserResults(eventPath), fetchLeaderboard(eventPath)])
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Unknown error'
|
||||
error.value = `Error clearing cache: ${errorMessage}`
|
||||
@@ -143,8 +142,8 @@ export const useNoitaStore = defineStore('noita', () => {
|
||||
}
|
||||
}
|
||||
|
||||
const refreshData = async () => {
|
||||
await Promise.all([fetchUserResults(), fetchLeaderboard()])
|
||||
const refreshData = async (eventPath: string) => {
|
||||
await Promise.all([fetchUserResults(eventPath), fetchLeaderboard(eventPath)])
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -31,14 +31,14 @@ export const usePuzzlesStore = defineStore('puzzles', () => {
|
||||
})
|
||||
|
||||
// Actions
|
||||
const loadPuzzles = async () => {
|
||||
const loadPuzzles = async (eventPath: string) => {
|
||||
if (puzzles.value.length > 0) return // Already loaded
|
||||
|
||||
try {
|
||||
isLoading.value = true
|
||||
error.value = ''
|
||||
|
||||
const response = await opusMagnumApiListPuzzles()
|
||||
const response = await opusMagnumApiListPuzzles({ path: { event_path: eventPath } })
|
||||
if (response.error) {
|
||||
error.value = String(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 = []
|
||||
await loadPuzzles()
|
||||
await loadPuzzles(eventPath)
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { polylanSubmitterApiGetUserInfo } from '@/api'
|
||||
|
||||
interface UserInfo {
|
||||
username: string
|
||||
@@ -19,9 +20,9 @@ export const useUserStore = defineStore('user', () => {
|
||||
const loadUserData = async () => {
|
||||
isLoading.value = true
|
||||
try {
|
||||
const response = await fetch('/api/user')
|
||||
if (response.ok) {
|
||||
const user = await response.json()
|
||||
const response = await polylanSubmitterApiGetUserInfo()
|
||||
if (response.data) {
|
||||
const user = response.data
|
||||
userInfo.value.isAuthenticated = user.is_authenticated || false
|
||||
userInfo.value.username = user.username || ''
|
||||
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
@@ -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&<(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
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
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 |
@@ -1,5 +1,6 @@
|
||||
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):
|
||||
@@ -37,16 +38,76 @@ class SubmissionFileInline(admin.TabularInline):
|
||||
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)
|
||||
class SubmissionAdmin(admin.ModelAdmin):
|
||||
list_display = ["id_short", "game", "user", "notes_preview", "created_at"]
|
||||
list_filter = ["game", "created_at", "updated_at"]
|
||||
search_fields = ["id", "user__email", "notes", "game__name"]
|
||||
list_display = ["id_short", "scope", "game", "user", "notes_preview", "created_at"]
|
||||
list_filter = [
|
||||
"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"]
|
||||
autocomplete_fields = ["scope"]
|
||||
inlines = [SubmissionFileInline]
|
||||
|
||||
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")
|
||||
def id_short(self, obj):
|
||||
|
||||
@@ -6,25 +6,56 @@ from ninja.files import UploadedFile
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
||||
from games.models import Game
|
||||
from .models import Submission, SubmissionFile, ImageCrop, OCRStatus
|
||||
from .schemas import SubmissionOut, ImageCropIn, ImageCropOut
|
||||
from .models import Submission, SubmissionFile, ImageCrop, OCRStatus, Scope
|
||||
from .schemas import SubmissionOut, ImageCropIn, ImageCropOut, ScopeOut
|
||||
from .tasks import process_submission_file, process_crop
|
||||
|
||||
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(
|
||||
request: HttpRequest,
|
||||
game_path: str,
|
||||
event_path: str,
|
||||
scope_id: UUID,
|
||||
files: List[UploadedFile] = File(...),
|
||||
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(
|
||||
game=game,
|
||||
user=request.user if request.user.is_authenticated else None,
|
||||
event=request.event,
|
||||
scope=scope,
|
||||
user=request.user,
|
||||
notes=notes or None,
|
||||
)
|
||||
|
||||
@@ -39,32 +70,32 @@ def create_submission(
|
||||
return submission
|
||||
|
||||
|
||||
@router.get("/submission/by/user", response=list[SubmissionOut])
|
||||
def list_user_submissions(request: HttpRequest, game_path: str):
|
||||
game = get_object_or_404(Game, path=game_path)
|
||||
|
||||
@router.get(
|
||||
f"{PREFIX}/submission/by/user", response={200: list[SubmissionOut], 403: dict}
|
||||
)
|
||||
def list_user_submissions(request: HttpRequest, event_path: str, scope_id: UUID):
|
||||
if not request.user.is_authenticated:
|
||||
return 403, {"detail": "Authentication required"}
|
||||
|
||||
submissions = Submission.objects.filter(game=game, user=request.user).order_by(
|
||||
"-created_at"
|
||||
)
|
||||
submissions = Submission.for_event.filter(
|
||||
scope_id=scope_id, user=request.user
|
||||
).order_by("-created_at")
|
||||
return submissions
|
||||
|
||||
|
||||
@router.get("/submission/by/game", response=list[SubmissionOut])
|
||||
def list_submissions_by_game(request: HttpRequest, game_path: str):
|
||||
@router.get(f"{PREFIX}/submission/by/game", response=list[SubmissionOut])
|
||||
def list_submissions_by_game(request: HttpRequest, event_path: str, game_path: str):
|
||||
game = get_object_or_404(Game, path=game_path)
|
||||
|
||||
if not request.user.is_staff:
|
||||
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)
|
||||
def get_submission(request: HttpRequest, submission_id: UUID):
|
||||
submission = get_object_or_404(Submission, id=submission_id)
|
||||
@router.get(f"{PREFIX}/submission/{{submission_id}}", response=SubmissionOut)
|
||||
def get_submission(request: HttpRequest, event_path: str, submission_id: UUID):
|
||||
submission = get_object_or_404(Submission.for_event, id=submission_id)
|
||||
|
||||
if submission.user != request.user and not request.user.is_staff:
|
||||
return 403, {"detail": "Not authorized"}
|
||||
@@ -72,16 +103,19 @@ def get_submission(request: HttpRequest, submission_id: UUID):
|
||||
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(
|
||||
request: HttpRequest,
|
||||
event_path: str,
|
||||
submission_file_id: UUID,
|
||||
data: ImageCropIn,
|
||||
):
|
||||
if not request.user.is_staff:
|
||||
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(
|
||||
submission_file=submission_file,
|
||||
@@ -96,23 +130,27 @@ def create_image_crop(
|
||||
return crop
|
||||
|
||||
|
||||
@router.delete("/crops/{crop_id}", response=dict)
|
||||
def delete_image_crop(request: HttpRequest, crop_id: UUID):
|
||||
@router.delete(f"{PREFIX}/crops/{{crop_id}}", response=dict)
|
||||
def delete_image_crop(request: HttpRequest, event_path: str, crop_id: UUID):
|
||||
if not request.user.is_staff:
|
||||
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()
|
||||
|
||||
return {"status": "deleted"}
|
||||
|
||||
|
||||
@router.post("/files/{file_id}/process-ocr", response=dict)
|
||||
def process_file_ocr(request: HttpRequest, file_id: UUID):
|
||||
@router.post(f"{PREFIX}/files/{{file_id}}/process-ocr", response=dict)
|
||||
def process_file_ocr(request: HttpRequest, event_path: str, file_id: UUID):
|
||||
if not request.user.is_staff:
|
||||
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.save()
|
||||
|
||||
@@ -122,12 +160,14 @@ def process_file_ocr(request: HttpRequest, file_id: UUID):
|
||||
return {"status": "queued"}
|
||||
|
||||
|
||||
@router.post("/crops/{crop_id}/process-ocr", response=dict)
|
||||
def process_crop_ocr(request: HttpRequest, crop_id: UUID):
|
||||
@router.post(f"{PREFIX}/crops/{{crop_id}}/process-ocr", response=dict)
|
||||
def process_crop_ocr(request: HttpRequest, event_path: str, crop_id: UUID):
|
||||
if not request.user.is_staff:
|
||||
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.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",
|
||||
),
|
||||
]
|
||||
@@ -4,7 +4,9 @@ from django.utils.translation import gettext_lazy as _
|
||||
import uuid
|
||||
|
||||
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):
|
||||
@@ -14,14 +16,20 @@ class OCRStatus(models.TextChoices):
|
||||
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 ""
|
||||
|
||||
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
|
||||
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):
|
||||
@@ -35,15 +43,74 @@ class BaseModel(models.Model):
|
||||
abstract = True
|
||||
|
||||
|
||||
class Submission(BaseModel):
|
||||
game = models.ForeignKey(
|
||||
Game,
|
||||
class Scope(BaseModel):
|
||||
"""A "match" that submissions can be scored against.
|
||||
|
||||
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,
|
||||
related_name="scopes",
|
||||
)
|
||||
|
||||
external_id = models.PositiveIntegerField(
|
||||
null=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",
|
||||
)
|
||||
|
||||
scope = models.ForeignKey(
|
||||
Scope,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="submissions",
|
||||
help_text="The match this submission's score is for.",
|
||||
)
|
||||
|
||||
user = models.ForeignKey(
|
||||
CustomUser,
|
||||
on_delete=models.CASCADE,
|
||||
@@ -60,6 +127,9 @@ class Submission(BaseModel):
|
||||
help_text="Optional notes about the submission",
|
||||
)
|
||||
|
||||
objects = models.Manager()
|
||||
for_event = EventScopedManager()
|
||||
|
||||
|
||||
class SubmissionFile(BaseModel):
|
||||
submission = models.ForeignKey(
|
||||
|
||||
@@ -3,6 +3,13 @@ from uuid import UUID
|
||||
from ninja import Schema
|
||||
|
||||
|
||||
class ScopeOut(Schema):
|
||||
id: UUID
|
||||
name: str
|
||||
is_active: bool
|
||||
created_at: datetime
|
||||
|
||||
|
||||
class ImageCropIn(Schema):
|
||||
x1: int
|
||||
y1: int
|
||||
@@ -36,7 +43,7 @@ class SubmissionFileOut(Schema):
|
||||
|
||||
class SubmissionOut(Schema):
|
||||
id: UUID
|
||||
game_id: int | None = None
|
||||
scope_id: UUID
|
||||
notes: str | None = None
|
||||
files: list[SubmissionFileOut] = []
|
||||
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"""
|
||||
+134
@@ -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
|
||||
+174
@@ -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
Reference in New Issue
Block a user