diff --git a/polylan_submitter/.gitignore b/polylan_submitter/.gitignore index 0178178..5269a2a 100644 --- a/polylan_submitter/.gitignore +++ b/polylan_submitter/.gitignore @@ -15,7 +15,7 @@ dist-ssr # Vite build output (generated locally via `pnpm build`, served by whitenoise) static_source/vite/assets static_source/vite/manifest.json -static_source/vite/vite.svg +static_source/vite/icon.png tsconfig.tsbuildinfo # vue-tsc emits a compiled copy of vite.config.ts here (tsconfig.node.json, diff --git a/polylan_submitter/market/views.py b/polylan_submitter/market/views.py index 5744326..9c9b58e 100644 --- a/polylan_submitter/market/views.py +++ b/polylan_submitter/market/views.py @@ -8,4 +8,4 @@ from events.models import Event @login_required def market_home(request: HttpRequest, event_path: str): get_object_or_404(Event, path=event_path) - return render(request, "app.html", {"bootstrap": {}}) + return render(request, "app.html") diff --git a/polylan_submitter/polylan_submitter/api.py b/polylan_submitter/polylan_submitter/api.py index dfc13de..9ff1725 100644 --- a/polylan_submitter/polylan_submitter/api.py +++ b/polylan_submitter/polylan_submitter/api.py @@ -1,4 +1,4 @@ -from ninja import NinjaAPI +from ninja import NinjaAPI, Schema from django.conf import settings from django.core.cache import cache from django.http import HttpRequest @@ -62,6 +62,17 @@ def health_check(request): return {"status": "healthy", "service": "polylan-submitter-api"} +class ConfigOut(Schema): + market_enabled: bool + + +# Site-wide config the SPA needs but that isn't tied to any one event +# (e.g. whether to render the market card at all on the home page). +@api.get("/config", response=ConfigOut) +def get_config(request): + return {"market_enabled": settings.MARKET_ENABLED} + + # Cache management endpoint @api.post("/cache/clear") def clear_cache(request: HttpRequest, event_path: str | None = None): diff --git a/polylan_submitter/polylan_submitter/urls.py b/polylan_submitter/polylan_submitter/urls.py index 3ee8be4..b6c56c9 100644 --- a/polylan_submitter/polylan_submitter/urls.py +++ b/polylan_submitter/polylan_submitter/urls.py @@ -44,44 +44,21 @@ def home(request: HttpRequest): @login_required def event_home(request: HttpRequest, event_path: str): get_object_or_404(Event, path=event_path) - - return render( - request, - "app.html", - { - "bootstrap": { - "marketEnabled": settings.MARKET_ENABLED, - }, - }, - ) + return render(request, "app.html") @login_required @require_game_enabled(OPUS_MAGNUM_APP_ID) def opus_magnum_home(request: HttpRequest, event_path: str): - from opus_magnum.models import SteamCollection - - event = get_object_or_404(Event, path=event_path) - collection = SteamCollection.objects.filter(is_active=True, event=event).last() - - return render( - request, - "app.html", - { - "bootstrap": { - "collectionTitle": collection.title if collection else "", - "collectionUrl": collection.url if collection else "", - "collectionDescription": collection.description if collection else "", - }, - }, - ) + get_object_or_404(Event, path=event_path) + return render(request, "app.html") @login_required @require_game_enabled(NOITA_APP_ID) def noita_home(request: HttpRequest, event_path: str): get_object_or_404(Event, path=event_path) - return render(request, "app.html", {"bootstrap": {}}) + return render(request, "app.html") urlpatterns = [ diff --git a/polylan_submitter/public/favicon.ico b/polylan_submitter/public/favicon.ico new file mode 100644 index 0000000..22052ab Binary files /dev/null and b/polylan_submitter/public/favicon.ico differ diff --git a/polylan_submitter/public/vite.svg b/polylan_submitter/public/vite.svg deleted file mode 100644 index e7b8dfb..0000000 --- a/polylan_submitter/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/polylan_submitter/src/Home.vue b/polylan_submitter/src/Home.vue index 58d52ef..9841bf8 100644 --- a/polylan_submitter/src/Home.vue +++ b/polylan_submitter/src/Home.vue @@ -1,25 +1,25 @@ - + + + + {{ t("submissionsSelect.title") }} @@ -38,7 +71,7 @@ const goToScope = (scopeId: string) => { - {{ scope.name }} diff --git a/polylan_submitter/src/api/index.ts b/polylan_submitter/src/api/index.ts index fa0edc3..bb51bef 100644 --- a/polylan_submitter/src/api/index.ts +++ b/polylan_submitter/src/api/index.ts @@ -1,4 +1,4 @@ // This file is auto-generated by @hey-api/openapi-ts -export { animationsApiPuzzleResults, animationsApiResults, animationsApiTopSubmissions, gamesApiListEventGames, gamesApiListGames, marketApiCloseMarket, marketApiCreateBet, marketApiListMarkets, marketApiListUserBets, marketApiResolveMarket, noitaApiGetLeaderboard, noitaApiGetResults, noitaApiSubmitLogFile, type Options, opusMagnumApiCreateSubmission, opusMagnumApiDeleteSubmission, opusMagnumApiGetCollection, opusMagnumApiGetStats, opusMagnumApiGetSubmission, opusMagnumApiListPuzzles, opusMagnumApiListResponsesNeedingValidation, opusMagnumApiListSubmissions, opusMagnumApiValidateAuto, opusMagnumApiValidateResponse, opusMagnumApiValidateSubmission, polylanSubmitterApiClearCache, polylanSubmitterApiGetUserInfo, polylanSubmitterApiHealthCheck, submissionsApiCreateImageCrop, submissionsApiCreateSubmission, submissionsApiDeleteImageCrop, submissionsApiGetSubmission, submissionsApiListMyScopes, submissionsApiListSubmissionsByGame, submissionsApiListUserSubmissions, submissionsApiProcessCropOcr, submissionsApiProcessFileOcr } from './sdk.gen'; -export type { AnimationsApiPuzzleResultsData, AnimationsApiPuzzleResultsResponse, AnimationsApiPuzzleResultsResponses, AnimationsApiResultsData, AnimationsApiResultsResponse, AnimationsApiResultsResponses, AnimationsApiTopSubmissionsData, AnimationsApiTopSubmissionsResponse, AnimationsApiTopSubmissionsResponses, ClientOptions, GameOut, GamesApiListEventGamesData, GamesApiListEventGamesResponse, GamesApiListEventGamesResponses, GamesApiListGamesData, GamesApiListGamesResponse, GamesApiListGamesResponses, ImageCropIn, ImageCropOut, Input, LeaderboardEntryOut, LeaderboardOut, MarketApiCloseMarketData, MarketApiCloseMarketResponses, MarketApiCreateBetData, MarketApiCreateBetResponse, MarketApiCreateBetResponses, MarketApiListMarketsData, MarketApiListMarketsResponse, MarketApiListMarketsResponses, MarketApiListUserBetsData, MarketApiListUserBetsResponse, MarketApiListUserBetsResponses, MarketApiResolveMarketData, MarketApiResolveMarketResponse, MarketApiResolveMarketResponses, MarketListSchema, MarketOptionSchema, NoitaApiGetLeaderboardData, NoitaApiGetLeaderboardResponse, NoitaApiGetLeaderboardResponses, NoitaApiGetResultsData, NoitaApiGetResultsResponse, NoitaApiGetResultsResponses, NoitaApiSubmitLogFileData, NoitaApiSubmitLogFileError, NoitaApiSubmitLogFileErrors, NoitaApiSubmitLogFileResponse, NoitaApiSubmitLogFileResponses, NoitaSubmissionOut, ObjectivResultOut, OpusMagnumApiCreateSubmissionData, OpusMagnumApiCreateSubmissionResponse, OpusMagnumApiCreateSubmissionResponses, OpusMagnumApiDeleteSubmissionData, OpusMagnumApiDeleteSubmissionResponses, OpusMagnumApiGetCollectionData, OpusMagnumApiGetCollectionResponse, OpusMagnumApiGetCollectionResponses, OpusMagnumApiGetStatsData, OpusMagnumApiGetStatsResponses, OpusMagnumApiGetSubmissionData, OpusMagnumApiGetSubmissionResponse, OpusMagnumApiGetSubmissionResponses, OpusMagnumApiListPuzzlesData, OpusMagnumApiListPuzzlesResponse, OpusMagnumApiListPuzzlesResponses, OpusMagnumApiListResponsesNeedingValidationData, OpusMagnumApiListResponsesNeedingValidationResponse, OpusMagnumApiListResponsesNeedingValidationResponses, OpusMagnumApiListSubmissionsData, OpusMagnumApiListSubmissionsResponse, OpusMagnumApiListSubmissionsResponses, OpusMagnumApiValidateAutoData, OpusMagnumApiValidateAutoResponse, OpusMagnumApiValidateAutoResponses, OpusMagnumApiValidateResponseData, OpusMagnumApiValidateResponseResponse, OpusMagnumApiValidateResponseResponses, OpusMagnumApiValidateSubmissionData, OpusMagnumApiValidateSubmissionResponse, OpusMagnumApiValidateSubmissionResponses, PagedSubmissionOut, PolylanSubmitterApiClearCacheData, PolylanSubmitterApiClearCacheResponses, PolylanSubmitterApiGetUserInfoData, PolylanSubmitterApiGetUserInfoResponse, PolylanSubmitterApiGetUserInfoResponses, PolylanSubmitterApiHealthCheckData, PolylanSubmitterApiHealthCheckResponses, PuzzlePointsFactorOut, PuzzleResponseIn, PuzzleResponseOut, PuzzleResponseRankingOut, PuzzleResultsOut, PuzzleSubmissionsOut, PuzzleSubmissionWithRankOut, RankingSchema, ResolveMarketSchema, ResultsOut, ScopeOut, SteamCollectionItemOut, SteamCollectionOut, SubmissionFileOut, SubmissionIn, SubmissionOut, SubmissionsApiCreateImageCropData, SubmissionsApiCreateImageCropResponse, SubmissionsApiCreateImageCropResponses, SubmissionsApiCreateSubmissionData, SubmissionsApiCreateSubmissionError, SubmissionsApiCreateSubmissionErrors, SubmissionsApiCreateSubmissionResponse, SubmissionsApiCreateSubmissionResponses, SubmissionsApiDeleteImageCropData, SubmissionsApiDeleteImageCropResponse, SubmissionsApiDeleteImageCropResponses, SubmissionsApiGetSubmissionData, SubmissionsApiGetSubmissionResponse, SubmissionsApiGetSubmissionResponses, SubmissionsApiListMyScopesData, SubmissionsApiListMyScopesError, SubmissionsApiListMyScopesErrors, SubmissionsApiListMyScopesResponse, SubmissionsApiListMyScopesResponses, SubmissionsApiListSubmissionsByGameData, SubmissionsApiListSubmissionsByGameResponse, SubmissionsApiListSubmissionsByGameResponses, SubmissionsApiListUserSubmissionsData, SubmissionsApiListUserSubmissionsError, SubmissionsApiListUserSubmissionsErrors, SubmissionsApiListUserSubmissionsResponse, SubmissionsApiListUserSubmissionsResponses, SubmissionsApiProcessCropOcrData, SubmissionsApiProcessCropOcrResponse, SubmissionsApiProcessCropOcrResponses, SubmissionsApiProcessFileOcrData, SubmissionsApiProcessFileOcrResponse, SubmissionsApiProcessFileOcrResponses, TournamentPuzzleResultsOut, TournamentSubmissionsOut, UserBetCreateSchema, UserBetSchema, UserDisplayOut, UserInfoOut, ValidationIn, WinnerFileOut, WinnerResponseOut } from './types.gen'; +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, polylanSubmitterApiGetConfig, polylanSubmitterApiGetUserInfo, polylanSubmitterApiHealthCheck, submissionsApiCreateImageCrop, submissionsApiCreateSubmission, submissionsApiDeleteImageCrop, submissionsApiGetSubmission, submissionsApiListAvailableScopes, submissionsApiListMyScopes, submissionsApiListSubmissionsByGame, submissionsApiListUserSubmissions, submissionsApiProcessCropOcr, submissionsApiProcessFileOcr } from './sdk.gen'; +export type { AnimationsApiPuzzleResultsData, AnimationsApiPuzzleResultsResponse, AnimationsApiPuzzleResultsResponses, AnimationsApiResultsData, AnimationsApiResultsResponse, AnimationsApiResultsResponses, AnimationsApiTopSubmissionsData, AnimationsApiTopSubmissionsResponse, AnimationsApiTopSubmissionsResponses, ClientOptions, ConfigOut, 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, PolylanSubmitterApiGetConfigData, PolylanSubmitterApiGetConfigResponse, PolylanSubmitterApiGetConfigResponses, PolylanSubmitterApiGetUserInfoData, PolylanSubmitterApiGetUserInfoResponse, PolylanSubmitterApiGetUserInfoResponses, PolylanSubmitterApiHealthCheckData, PolylanSubmitterApiHealthCheckResponses, PuzzlePointsFactorOut, PuzzleResponseIn, PuzzleResponseOut, PuzzleResponseRankingOut, PuzzleResultsOut, PuzzleSubmissionsOut, PuzzleSubmissionWithRankOut, RankingSchema, ResolveMarketSchema, ResultsOut, ScopeOut, SteamCollectionItemOut, SteamCollectionOut, SubmissionFileOut, SubmissionIn, SubmissionOut, SubmissionsApiCreateImageCropData, SubmissionsApiCreateImageCropResponse, SubmissionsApiCreateImageCropResponses, SubmissionsApiCreateSubmissionData, SubmissionsApiCreateSubmissionError, SubmissionsApiCreateSubmissionErrors, SubmissionsApiCreateSubmissionResponse, SubmissionsApiCreateSubmissionResponses, SubmissionsApiDeleteImageCropData, SubmissionsApiDeleteImageCropResponse, SubmissionsApiDeleteImageCropResponses, SubmissionsApiGetSubmissionData, SubmissionsApiGetSubmissionResponse, SubmissionsApiGetSubmissionResponses, SubmissionsApiListAvailableScopesData, SubmissionsApiListAvailableScopesError, SubmissionsApiListAvailableScopesErrors, SubmissionsApiListAvailableScopesResponse, SubmissionsApiListAvailableScopesResponses, SubmissionsApiListMyScopesData, SubmissionsApiListMyScopesError, SubmissionsApiListMyScopesErrors, SubmissionsApiListMyScopesResponse, SubmissionsApiListMyScopesResponses, SubmissionsApiListSubmissionsByGameData, SubmissionsApiListSubmissionsByGameResponse, SubmissionsApiListSubmissionsByGameResponses, SubmissionsApiListUserSubmissionsData, SubmissionsApiListUserSubmissionsError, SubmissionsApiListUserSubmissionsErrors, SubmissionsApiListUserSubmissionsResponse, SubmissionsApiListUserSubmissionsResponses, SubmissionsApiProcessCropOcrData, SubmissionsApiProcessCropOcrResponse, SubmissionsApiProcessCropOcrResponses, SubmissionsApiProcessFileOcrData, SubmissionsApiProcessFileOcrResponse, SubmissionsApiProcessFileOcrResponses, TournamentPuzzleResultsOut, TournamentSubmissionsOut, UserBetCreateSchema, UserBetSchema, UserDisplayOut, UserInfoOut, ValidationIn, WinnerFileOut, WinnerResponseOut } from './types.gen'; diff --git a/polylan_submitter/src/api/sdk.gen.ts b/polylan_submitter/src/api/sdk.gen.ts index a49fa31..a80754a 100644 --- a/polylan_submitter/src/api/sdk.gen.ts +++ b/polylan_submitter/src/api/sdk.gen.ts @@ -2,7 +2,7 @@ import { type Client, formDataBodySerializer, type Options as Options2, type TDataShape } from './client'; import { client } from './client.gen'; -import type { AnimationsApiPuzzleResultsData, AnimationsApiPuzzleResultsResponses, AnimationsApiResultsData, AnimationsApiResultsResponses, AnimationsApiTopSubmissionsData, AnimationsApiTopSubmissionsResponses, GamesApiListEventGamesData, GamesApiListEventGamesResponses, GamesApiListGamesData, GamesApiListGamesResponses, MarketApiCloseMarketData, MarketApiCloseMarketResponses, MarketApiCreateBetData, MarketApiCreateBetResponses, MarketApiListMarketsData, MarketApiListMarketsResponses, MarketApiListUserBetsData, MarketApiListUserBetsResponses, MarketApiResolveMarketData, MarketApiResolveMarketResponses, NoitaApiGetLeaderboardData, NoitaApiGetLeaderboardResponses, NoitaApiGetResultsData, NoitaApiGetResultsResponses, NoitaApiSubmitLogFileData, NoitaApiSubmitLogFileErrors, NoitaApiSubmitLogFileResponses, OpusMagnumApiCreateSubmissionData, OpusMagnumApiCreateSubmissionResponses, OpusMagnumApiDeleteSubmissionData, OpusMagnumApiDeleteSubmissionResponses, OpusMagnumApiGetCollectionData, OpusMagnumApiGetCollectionResponses, OpusMagnumApiGetStatsData, OpusMagnumApiGetStatsResponses, OpusMagnumApiGetSubmissionData, OpusMagnumApiGetSubmissionResponses, OpusMagnumApiListPuzzlesData, OpusMagnumApiListPuzzlesResponses, OpusMagnumApiListResponsesNeedingValidationData, OpusMagnumApiListResponsesNeedingValidationResponses, OpusMagnumApiListSubmissionsData, OpusMagnumApiListSubmissionsResponses, OpusMagnumApiValidateAutoData, OpusMagnumApiValidateAutoResponses, OpusMagnumApiValidateResponseData, OpusMagnumApiValidateResponseResponses, OpusMagnumApiValidateSubmissionData, OpusMagnumApiValidateSubmissionResponses, PolylanSubmitterApiClearCacheData, PolylanSubmitterApiClearCacheResponses, PolylanSubmitterApiGetUserInfoData, PolylanSubmitterApiGetUserInfoResponses, PolylanSubmitterApiHealthCheckData, PolylanSubmitterApiHealthCheckResponses, SubmissionsApiCreateImageCropData, SubmissionsApiCreateImageCropResponses, SubmissionsApiCreateSubmissionData, SubmissionsApiCreateSubmissionErrors, SubmissionsApiCreateSubmissionResponses, SubmissionsApiDeleteImageCropData, SubmissionsApiDeleteImageCropResponses, SubmissionsApiGetSubmissionData, SubmissionsApiGetSubmissionResponses, SubmissionsApiListMyScopesData, SubmissionsApiListMyScopesErrors, SubmissionsApiListMyScopesResponses, SubmissionsApiListSubmissionsByGameData, SubmissionsApiListSubmissionsByGameResponses, SubmissionsApiListUserSubmissionsData, SubmissionsApiListUserSubmissionsErrors, SubmissionsApiListUserSubmissionsResponses, SubmissionsApiProcessCropOcrData, SubmissionsApiProcessCropOcrResponses, SubmissionsApiProcessFileOcrData, SubmissionsApiProcessFileOcrResponses } from './types.gen'; +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, PolylanSubmitterApiGetConfigData, PolylanSubmitterApiGetConfigResponses, PolylanSubmitterApiGetUserInfoData, PolylanSubmitterApiGetUserInfoResponses, PolylanSubmitterApiHealthCheckData, PolylanSubmitterApiHealthCheckResponses, SubmissionsApiCreateImageCropData, SubmissionsApiCreateImageCropResponses, SubmissionsApiCreateSubmissionData, SubmissionsApiCreateSubmissionErrors, SubmissionsApiCreateSubmissionResponses, SubmissionsApiDeleteImageCropData, SubmissionsApiDeleteImageCropResponses, SubmissionsApiGetSubmissionData, SubmissionsApiGetSubmissionResponses, SubmissionsApiListAvailableScopesData, SubmissionsApiListAvailableScopesErrors, SubmissionsApiListAvailableScopesResponses, SubmissionsApiListMyScopesData, SubmissionsApiListMyScopesErrors, SubmissionsApiListMyScopesResponses, SubmissionsApiListSubmissionsByGameData, SubmissionsApiListSubmissionsByGameResponses, SubmissionsApiListUserSubmissionsData, SubmissionsApiListUserSubmissionsErrors, SubmissionsApiListUserSubmissionsResponses, SubmissionsApiProcessCropOcrData, SubmissionsApiProcessCropOcrResponses, SubmissionsApiProcessFileOcrData, SubmissionsApiProcessFileOcrResponses } from './types.gen'; export type Options = Options2 & { /** @@ -25,6 +25,11 @@ export type Options(options?: Options) => (options?.client ?? client).get({ url: '/api/health', ...options }); +/** + * Get Config + */ +export const polylanSubmitterApiGetConfig = (options?: Options) => (options?.client ?? client).get({ url: '/api/config', ...options }); + /** * Clear Cache * @@ -201,6 +206,17 @@ export const gamesApiListEventGames = (opt */ export const submissionsApiListMyScopes = (options: Options) => (options.client ?? client).get({ url: '/api/events/{event_path}/submissions/scopes/mine', ...options }); +/** + * List Available Scopes + * + * Scopes the current user may submit to for this game - superusers see + * every active scope, everyone else only the ones assigned to them. Used + * by the submissions-select and submissions-detail pages, both to list + * scopes and (for the detail page) to confirm the requested scope_id is + * one the current user is actually allowed to see. + */ +export const submissionsApiListAvailableScopes = (options: Options) => (options.client ?? client).get({ url: '/api/events/{event_path}/submissions/{game_path}/available-scopes', ...options }); + /** * Create Submission */ diff --git a/polylan_submitter/src/api/types.gen.ts b/polylan_submitter/src/api/types.gen.ts index 078d7cd..7a29117 100644 --- a/polylan_submitter/src/api/types.gen.ts +++ b/polylan_submitter/src/api/types.gen.ts @@ -4,6 +4,16 @@ export type ClientOptions = { baseUrl: string; }; +/** + * ConfigOut + */ +export type ConfigOut = { + /** + * Market Enabled + */ + market_enabled: boolean; +}; + /** * UserInfoOut * @@ -1125,6 +1135,22 @@ export type PolylanSubmitterApiHealthCheckResponses = { 200: unknown; }; +export type PolylanSubmitterApiGetConfigData = { + body?: never; + path?: never; + query?: never; + url: '/api/config'; +}; + +export type PolylanSubmitterApiGetConfigResponses = { + /** + * OK + */ + 200: ConfigOut; +}; + +export type PolylanSubmitterApiGetConfigResponse = PolylanSubmitterApiGetConfigResponses[keyof PolylanSubmitterApiGetConfigResponses]; + export type PolylanSubmitterApiClearCacheData = { body?: never; path?: never; @@ -1668,6 +1694,46 @@ export type SubmissionsApiListMyScopesResponses = { export type SubmissionsApiListMyScopesResponse = SubmissionsApiListMyScopesResponses[keyof SubmissionsApiListMyScopesResponses]; +export type SubmissionsApiListAvailableScopesData = { + body?: never; + path: { + /** + * Event Path + */ + event_path: string; + /** + * Game Path + */ + game_path: string; + }; + query?: never; + url: '/api/events/{event_path}/submissions/{game_path}/available-scopes'; +}; + +export type SubmissionsApiListAvailableScopesErrors = { + /** + * Response + * + * Forbidden + */ + 403: { + [key: string]: unknown; + }; +}; + +export type SubmissionsApiListAvailableScopesError = SubmissionsApiListAvailableScopesErrors[keyof SubmissionsApiListAvailableScopesErrors]; + +export type SubmissionsApiListAvailableScopesResponses = { + /** + * Response + * + * OK + */ + 200: Array; +}; + +export type SubmissionsApiListAvailableScopesResponse = SubmissionsApiListAvailableScopesResponses[keyof SubmissionsApiListAvailableScopesResponses]; + export type SubmissionsApiCreateSubmissionData = { /** * MultiPartBodyParams diff --git a/polylan_submitter/src/bootstrap.ts b/polylan_submitter/src/bootstrap.ts deleted file mode 100644 index 156b900..0000000 --- a/polylan_submitter/src/bootstrap.ts +++ /dev/null @@ -1,11 +0,0 @@ -// Per-page data Django injects via `{{ bootstrap|json_script:"bootstrap-data" }}` -// in templates/app.html. Read once at boot, before the router/app are created. -export const bootstrap: Record = (() => { - const el = document.getElementById("bootstrap-data"); - if (!el?.textContent) return {}; - try { - return JSON.parse(el.textContent); - } catch { - return {}; - } -})(); diff --git a/polylan_submitter/src/layout/AppHeader.vue b/polylan_submitter/src/layout/AppHeader.vue index 4490b13..ee69994 100644 --- a/polylan_submitter/src/layout/AppHeader.vue +++ b/polylan_submitter/src/layout/AppHeader.vue @@ -1,30 +1,29 @@ @@ -32,7 +31,7 @@ const goBack = () => { - + {{ t("common.back") }} diff --git a/polylan_submitter/src/router/index.ts b/polylan_submitter/src/router/index.ts index 82c068c..89a8e34 100644 --- a/polylan_submitter/src/router/index.ts +++ b/polylan_submitter/src/router/index.ts @@ -1,29 +1,27 @@ -import { createRouter, createWebHistory, type RouteLocationNormalizedLoaded } from "vue-router"; +import { createRouter, createWebHistory, type RouteLocationNormalizedLoaded, type RouteLocationRaw } from "vue-router"; import Home from "@/Home.vue"; import Market from "@/Market.vue"; import Noita from "@/Noita.vue"; import OpusMagnum from "@/OpusMagnum.vue"; import Submissions from "@/Submissions.vue"; import SubmissionsSelect from "@/SubmissionsSelect.vue"; -import { bootstrap } from "@/bootstrap"; +import { usePageHeaderStore } from "@/stores/pageHeader"; declare module "vue-router" { interface RouteMeta { + // Static label for AppHeader's title bar. Pages whose title depends on + // fetched data (e.g. submissions) set one at runtime via + // usePageHeaderStore() instead and leave this out. titleKey?: string; - getBackPath?: (route: RouteLocationNormalizedLoaded) => string; + getBackRoute?: (route: RouteLocationNormalizedLoaded) => RouteLocationRaw; } } -// Route params (eventPath, gamePath, scopeId, ...) come from the URL; -// page-specific extras Django can't express as URL segments (collection -// title, scope name, ...) come from the bootstrap JSON island instead. -const withBootstrap = (route: RouteLocationNormalizedLoaded) => ({ - ...route.params, - ...bootstrap, +const eventHome = (route: RouteLocationNormalizedLoaded): RouteLocationRaw => ({ + name: "home", + params: { eventPath: route.params.eventPath }, }); -const eventHome = (route: RouteLocationNormalizedLoaded) => `/${route.params.eventPath}/`; - export const router = createRouter({ history: createWebHistory("/"), routes: [ @@ -31,45 +29,55 @@ export const router = createRouter({ path: "/:eventPath/", name: "home", component: Home, - props: withBootstrap, + props: true, }, { path: "/:eventPath/market", name: "market", component: Market, - props: withBootstrap, - meta: { titleKey: "market.header", getBackPath: eventHome }, + props: true, + meta: { titleKey: "market.header", getBackRoute: eventHome }, }, { path: "/:eventPath/opus-magnum", name: "opus-magnum", component: OpusMagnum, - props: withBootstrap, - meta: { titleKey: "opusMagnum.header", getBackPath: eventHome }, + props: true, + meta: { titleKey: "opusMagnum.header", getBackRoute: eventHome }, }, { path: "/:eventPath/noita", name: "noita", component: Noita, - props: withBootstrap, - meta: { titleKey: "noita.header", getBackPath: eventHome }, + props: true, + meta: { titleKey: "noita.header", getBackRoute: eventHome }, }, { path: "/:eventPath/submissions/:gamePath", name: "submissions-select", component: SubmissionsSelect, - props: withBootstrap, - meta: { titleKey: "submissionsSelect.title", getBackPath: eventHome }, + props: true, + meta: { titleKey: "submissionsSelect.title", getBackRoute: eventHome }, }, { path: "/:eventPath/submissions/:gamePath/:scopeId", name: "submissions", component: Submissions, - props: withBootstrap, + props: true, meta: { - titleKey: "submissions.pageTitle", - getBackPath: (route) => `/${route.params.eventPath}/submissions/${route.params.gamePath}`, + // No static titleKey: Submissions.vue sets a dynamic + // "{game} Submissions — {scope}" title once it has fetched both. + getBackRoute: (route) => ({ + name: "submissions-select", + params: { eventPath: route.params.eventPath, gamePath: route.params.gamePath }, + }), }, }, ], }); + +// Every route change starts from a clean header title; pages that need a +// dynamic one (see submissions above) set it again once their data lands. +router.beforeEach(() => { + usePageHeaderStore().clear(); +}); diff --git a/polylan_submitter/src/stores/pageHeader.ts b/polylan_submitter/src/stores/pageHeader.ts new file mode 100644 index 0000000..d9af51a --- /dev/null +++ b/polylan_submitter/src/stores/pageHeader.ts @@ -0,0 +1,20 @@ +import { defineStore } from "pinia"; +import { ref } from "vue"; + +// Lets a routed page hand AppHeader a title built from data only that page +// has fetched (e.g. "Noita Submissions — Semifinal", assembled from an API +// call keyed by the scopeId route param). Cleared on every navigation so a +// stale title never lingers while the next page is still loading. +export const usePageHeaderStore = defineStore("pageHeader", () => { + const title = ref(""); + + const setTitle = (value: string) => { + title.value = value; + }; + + const clear = () => { + title.value = ""; + }; + + return { title, setTitle, clear }; +}); diff --git a/polylan_submitter/static_source/vite/favicon.ico b/polylan_submitter/static_source/vite/favicon.ico new file mode 100644 index 0000000..22052ab Binary files /dev/null and b/polylan_submitter/static_source/vite/favicon.ico differ diff --git a/polylan_submitter/submissions/api.py b/polylan_submitter/submissions/api.py index d15fbce..5629729 100644 --- a/polylan_submitter/submissions/api.py +++ b/polylan_submitter/submissions/api.py @@ -31,6 +31,38 @@ def list_my_scopes(request: HttpRequest, event_path: str, game_path: str): ) +@router.get(f"{PREFIX}/{{game_path}}/available-scopes", response={200: list[ScopeOut], 403: dict}) +def list_available_scopes(request: HttpRequest, event_path: str, game_path: str): + """Scopes the current user may submit to for this game - superusers see + every active scope, everyone else only the ones assigned to them. Used + by the submissions-select and submissions-detail pages, both to list + scopes and (for the detail page) to confirm the requested scope_id is + one the current user is actually allowed to see.""" + if not request.user.is_authenticated: + return 403, {"detail": "Authentication required"} + + game = get_object_or_404( + Game.objects.enabled_for_event(request.event), path=game_path + ) + + scopes = ( + Scope.objects.filter( + event_game__event=request.event, + event_game__game=game, + is_active=True, + ) + if request.user.is_superuser + else Scope.objects.filter( + event_game__event=request.event, + event_game__game=game, + assigned_users=request.user, + is_active=True, + ) + ) + + return list(scopes.order_by("name")) + + @router.post(f"{PREFIX}/submission/create", response={200: SubmissionOut, 403: dict}) def create_submission( request: HttpRequest, diff --git a/polylan_submitter/submissions/views.py b/polylan_submitter/submissions/views.py index 71a2bd0..fb8a5e9 100644 --- a/polylan_submitter/submissions/views.py +++ b/polylan_submitter/submissions/views.py @@ -39,19 +39,7 @@ def submission_home(request: HttpRequest, event_path: str, game_path: str): if not scopes: raise Http404 - return render( - request, - "app.html", - { - "bootstrap": { - "gameName": game.name, - "scopes": [ - {"id": str(scope.id), "name": scope.name} - for scope in scopes.order_by("name") - ], - }, - }, - ) + return render(request, "app.html") @login_required @@ -74,17 +62,4 @@ def submission_view( ): raise Http404 - game = scope.event_game.game - return render( - request, - "app.html", - { - "bootstrap": { - "scopeName": scope.name, - "gameName": game.name, - "gamePath": game.path, - "gameImage": game.image or "", - "gameSteamId": str(game.steam_app_id or ""), - }, - }, - ) + return render(request, "app.html") diff --git a/polylan_submitter/templates/app.html b/polylan_submitter/templates/app.html index 4aa4517..e0fe92f 100644 --- a/polylan_submitter/templates/app.html +++ b/polylan_submitter/templates/app.html @@ -7,6 +7,5 @@ {% endblock %} {% block content %} - {{ bootstrap|json_script:"bootstrap-data" }} {% endblock %} diff --git a/polylan_submitter/templates/base.html b/polylan_submitter/templates/base.html index f10c036..456c95d 100644 --- a/polylan_submitter/templates/base.html +++ b/polylan_submitter/templates/base.html @@ -1,9 +1,10 @@ {% load django_vite %} +{% load static %} - + {% block title %}PolyLAN Submitter{% endblock %} {% if debug %}{% vite_hmr_client %}{% endif %}
@@ -38,7 +71,7 @@ const goToScope = (scopeId: string) => {