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