auto-reload for admin + auto validation for all using python

This commit is contained in:
Loïc Gremaud 2025-10-31 02:41:44 +01:00
parent e0ada1e26d
commit 596731a8a7
6 changed files with 110 additions and 8 deletions

View File

@ -11,6 +11,7 @@
}, },
"dependencies": { "dependencies": {
"@tailwindcss/vite": "^4.1.16", "@tailwindcss/vite": "^4.1.16",
"@vueuse/core": "^14.0.0",
"install": "^0.13.0", "install": "^0.13.0",
"pinia": "^3.0.3", "pinia": "^3.0.3",
"tailwindcss": "^4.1.16", "tailwindcss": "^4.1.16",

View File

@ -11,6 +11,9 @@ importers:
'@tailwindcss/vite': '@tailwindcss/vite':
specifier: ^4.1.16 specifier: ^4.1.16
version: 4.1.16(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)) version: 4.1.16(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2))
'@vueuse/core':
specifier: ^14.0.0
version: 14.0.0(vue@3.5.22(typescript@5.9.3))
install: install:
specifier: ^0.13.0 specifier: ^0.13.0
version: 0.13.0 version: 0.13.0
@ -455,6 +458,9 @@ packages:
'@types/node@24.9.2': '@types/node@24.9.2':
resolution: {integrity: sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA==} resolution: {integrity: sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA==}
'@types/web-bluetooth@0.0.21':
resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==}
'@vitejs/plugin-vue@6.0.1': '@vitejs/plugin-vue@6.0.1':
resolution: {integrity: sha512-+MaE752hU0wfPFJEUAIxqw18+20euHHdxVtMvbFcOEpjEyfqXH/5DCoTHiVJ0J29EhTJdoTkjEv5YBKU9dnoTw==} resolution: {integrity: sha512-+MaE752hU0wfPFJEUAIxqw18+20euHHdxVtMvbFcOEpjEyfqXH/5DCoTHiVJ0J29EhTJdoTkjEv5YBKU9dnoTw==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
@ -528,6 +534,19 @@ packages:
vue: vue:
optional: true optional: true
'@vueuse/core@14.0.0':
resolution: {integrity: sha512-d6tKRWkZE8IQElX2aHBxXOMD478fHIYV+Dzm2y9Ag122ICBpNKtGICiXKOhWU3L1kKdttDD9dCMS4bGP3jhCTQ==}
peerDependencies:
vue: ^3.5.0
'@vueuse/metadata@14.0.0':
resolution: {integrity: sha512-6yoGqbJcMldVCevkFiHDBTB1V5Hq+G/haPlGIuaFZHpXC0HADB0EN1ryQAAceiW+ryS3niUwvdFbGiqHqBrfVA==}
'@vueuse/shared@14.0.0':
resolution: {integrity: sha512-mTCA0uczBgurRlwVaQHfG0Ja7UdGe4g9mwffiJmvLiTtp1G4AQyIjej6si/k8c8pUwTfVpNufck+23gXptPAkw==}
peerDependencies:
vue: ^3.5.0
alien-signals@3.0.3: alien-signals@3.0.3:
resolution: {integrity: sha512-2JXjom6R7ZwrISpUphLhf4htUq1aKRCennTJ6u9kFfr3sLmC9+I4CxxVi+McoFnIg+p1HnVrfLT/iCt4Dlz//Q==} resolution: {integrity: sha512-2JXjom6R7ZwrISpUphLhf4htUq1aKRCennTJ6u9kFfr3sLmC9+I4CxxVi+McoFnIg+p1HnVrfLT/iCt4Dlz//Q==}
@ -1107,6 +1126,8 @@ snapshots:
dependencies: dependencies:
undici-types: 7.16.0 undici-types: 7.16.0
'@types/web-bluetooth@0.0.21': {}
'@vitejs/plugin-vue@6.0.1(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2))(vue@3.5.22(typescript@5.9.3))': '@vitejs/plugin-vue@6.0.1(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2))(vue@3.5.22(typescript@5.9.3))':
dependencies: dependencies:
'@rolldown/pluginutils': 1.0.0-beta.29 '@rolldown/pluginutils': 1.0.0-beta.29
@ -1214,6 +1235,19 @@ snapshots:
typescript: 5.9.3 typescript: 5.9.3
vue: 3.5.22(typescript@5.9.3) vue: 3.5.22(typescript@5.9.3)
'@vueuse/core@14.0.0(vue@3.5.22(typescript@5.9.3))':
dependencies:
'@types/web-bluetooth': 0.0.21
'@vueuse/metadata': 14.0.0
'@vueuse/shared': 14.0.0(vue@3.5.22(typescript@5.9.3))
vue: 3.5.22(typescript@5.9.3)
'@vueuse/metadata@14.0.0': {}
'@vueuse/shared@14.0.0(vue@3.5.22(typescript@5.9.3))':
dependencies:
vue: 3.5.22(typescript@5.9.3)
alien-signals@3.0.3: {} alien-signals@3.0.3: {}
birpc@2.6.1: {} birpc@2.6.1: {}

View File

@ -7,6 +7,7 @@ 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 type { SteamCollection, PuzzleResponse, UserInfo } from '@/types' import type { SteamCollection, PuzzleResponse, UserInfo } from '@/types'
import { useCountdown } from '@vueuse/core'
const props = defineProps<{collectionTitle: string, collectionUrl: string, collectionDescription: string}>() const props = defineProps<{collectionTitle: string, collectionUrl: string, collectionDescription: string}>()
@ -19,8 +20,6 @@ const userInfo = ref<UserInfo | null>(null)
const isLoading = ref(true) const isLoading = ref(true)
const error = ref<string>('') const error = ref<string>('')
// Mock data removed - using API data only
// Computed properties // Computed properties
const isSuperuser = computed(() => { const isSuperuser = computed(() => {
return userInfo.value?.is_superuser || false return userInfo.value?.is_superuser || false
@ -42,7 +41,7 @@ const responsesByPuzzle = computed(() => {
return grouped return grouped
}) })
onMounted(async () => { async function initialize() {
try { try {
isLoading.value = true isLoading.value = true
error.value = '' error.value = ''
@ -78,6 +77,20 @@ onMounted(async () => {
isLoading.value = false isLoading.value = false
console.log('Loading state set to false') console.log('Loading state set to false')
} }
if (userInfo.value.is_superuser) {
start()
}
}
const { remaining, start } = useCountdown(60, {
onComplete() {
initialize()
}
})
onMounted(async () => {
await initialize()
}) })
const handleSubmission = async (submissionData: { const handleSubmission = async (submissionData: {
@ -165,6 +178,15 @@ const reloadPage = () => {
<!-- Main Content --> <!-- Main Content -->
<div class="container mx-auto px-4 py-8"> <div class="container mx-auto px-4 py-8">
<!-- Loading State --> <!-- Loading State -->
<div v-if="userInfo?.is_superuser" class="flex justify-center">
<div class="text-center">
<p class="mb-6 text-base-content/70">
<span class="loading loading-spinner loading-lg"></span>
Auto reload page in {{ remaining }} seconds ...
</p>
</div>
</div>
<div v-if="isLoading" class="flex justify-center items-center min-h-[400px]"> <div v-if="isLoading" class="flex justify-center items-center min-h-[400px]">
<div class="text-center"> <div class="text-center">
<span class="loading loading-spinner loading-lg"></span> <span class="loading loading-spinner loading-lg"></span>

View File

@ -25,6 +25,11 @@
<div class="stat-value text-success">{{ Math.round(stats.validation_rate * 100) }}%</div> <div class="stat-value text-success">{{ Math.round(stats.validation_rate * 100) }}%</div>
</div> </div>
</div> </div>
<button class="btn btn-sm btn-primary" @click="autoValidationResponse">
<i class="mdi mdi-check-circle mr-1"></i>
Auto validation for all responses
</button>
<!-- Responses Needing Validation --> <!-- Responses Needing Validation -->
<div v-if="responsesNeedingValidation.length > 0"> <div v-if="responsesNeedingValidation.length > 0">
@ -268,6 +273,23 @@ const loadData = async () => {
} }
} }
const autoValidationResponse = async () => {
for (const response of Array.from(responsesNeedingValidation.value)) {
const {data, error} = await apiService.autoValidateResponses(response.id)
if (data && !data.needs_manual_validation) {
// Remove from validation list
responsesNeedingValidation.value = responsesNeedingValidation.value.filter(
r => r.id !== response.id
)
stats.value.needs_validation -= 1
} else if (error) {
break
}
}
}
const openValidationModal = (response: PuzzleResponse) => { const openValidationModal = (response: PuzzleResponse) => {
validationModal.value.response = response validationModal.value.response = response
validationModal.value.data = { validationModal.value.data = {

View File

@ -158,6 +158,12 @@ export class ApiService {
}) })
} }
async autoValidateResponses(responseId: number): Promise<ApiResponse<PuzzleResponse>> {
return this.request<PuzzleResponse>(`/submissions/responses/${responseId}/validate/auto`, {
method: 'PUT',
})
}
async getResponsesNeedingValidation(): Promise<ApiResponse<PuzzleResponse[]>> { async getResponsesNeedingValidation(): Promise<ApiResponse<PuzzleResponse[]>> {
return this.request<PuzzleResponse[]>('/submissions/responses/needs-validation') return this.request<PuzzleResponse[]>('/submissions/responses/needs-validation')
} }
@ -226,7 +232,7 @@ export const puzzleHelpers = {
export const submissionHelpers = { export const submissionHelpers = {
async createFromFiles( async createFromFiles(
files: SubmissionFile[], files: SubmissionFile[],
puzzles: SteamCollectionItem[], puzzles: SteamCollectionItem[],
notes?: string, notes?: string,
manualValidationRequested?: boolean manualValidationRequested?: boolean
@ -240,7 +246,7 @@ export const submissionHelpers = {
files.forEach(file => { files.forEach(file => {
// Use manual puzzle selection if available, otherwise fall back to OCR // Use manual puzzle selection if available, otherwise fall back to OCR
const puzzleName = file.manualPuzzleSelection || file.ocrData?.puzzle const puzzleName = file.manualPuzzleSelection || file.ocrData?.puzzle
if (puzzleName) { if (puzzleName) {
if (!responsesByPuzzle[puzzleName]) { if (!responsesByPuzzle[puzzleName]) {
responsesByPuzzle[puzzleName] = { responsesByPuzzle[puzzleName] = {
@ -290,10 +296,10 @@ export const submissionHelpers = {
// Extract actual File objects for upload // Extract actual File objects for upload
const fileObjects = files.map(f => f.file) const fileObjects = files.map(f => f.file)
return apiService.createSubmission({ return apiService.createSubmission({
notes, notes,
manual_validation_requested: manualValidationRequested, manual_validation_requested: manualValidationRequested,
responses responses
}, fileObjects) }, fileObjects)
}, },

View File

@ -7,6 +7,8 @@ from django.utils import timezone
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from typing import List from typing import List
from opus_submitter.submissions.utils import verify_and_validate_ocr_date_for_submission
from .models import Submission, PuzzleResponse, SubmissionFile, SteamCollectionItem from .models import Submission, PuzzleResponse, SubmissionFile, SteamCollectionItem
from .schemas import ( from .schemas import (
SubmissionIn, SubmissionIn,
@ -196,6 +198,21 @@ def validate_response(request, response_id: int, data: ValidationIn):
return response return response
@router.put("/responses/{response_id}/validate/auto", response=PuzzleResponseOut)
def validate_response(request, response_id: int):
"""Try to auto validate a puzzle response"""
if not request.user.is_authenticated or not request.user.is_staff:
return 403, {"detail": "Admin access required"}
response = get_object_or_404(PuzzleResponse, id=response_id)
for file in response.files.all():
verify_and_validate_ocr_date_for_submission(file)
return response
@router.get("/responses/needs-validation", response=List[PuzzleResponseOut]) @router.get("/responses/needs-validation", response=List[PuzzleResponseOut])
def list_responses_needing_validation(request): def list_responses_needing_validation(request):
"""Get all responses that need manual validation""" """Get all responses that need manual validation"""