some fixes

This commit is contained in:
Loïc Gremaud 2025-10-31 01:05:57 +01:00
parent 0e1e77c2dd
commit f98145d6db
12 changed files with 136 additions and 98 deletions

View File

@ -28,7 +28,15 @@ from .api import api
@login_required @login_required
def home(request: HttpRequest): def home(request: HttpRequest):
return render(request, "index.html", {}) from submissions.models import SteamCollection
return render(
request,
"index.html",
{
"collection": SteamCollection.objects.filter(is_active=True).last(),
},
)
urlpatterns = [ urlpatterns = [

View File

@ -1,19 +1,20 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, computed } from 'vue' import { ref, onMounted, computed, defineProps } from 'vue'
import PuzzleCard from './components/PuzzleCard.vue' import PuzzleCard from '@/components/PuzzleCard.vue'
import SubmissionForm from './components/SubmissionForm.vue' import SubmissionForm from '@/components/SubmissionForm.vue'
import AdminPanel from './components/AdminPanel.vue' import AdminPanel from '@/components/AdminPanel.vue'
import { apiService, 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 type { SteamCollection, PuzzleResponse, UserInfo } from './types' import type { SteamCollection, PuzzleResponse, UserInfo } from '@/types'
const props = defineProps<{collectionTitle: string, collectionUrl: string, collectionDescription: string}>()
// Pinia stores // Pinia stores
const puzzlesStore = usePuzzlesStore() const puzzlesStore = usePuzzlesStore()
const submissionsStore = useSubmissionsStore() const submissionsStore = useSubmissionsStore()
// Local state // Local state
const collections = ref<SteamCollection[]>([])
const userInfo = ref<UserInfo | null>(null) const userInfo = ref<UserInfo | null>(null)
const isLoading = ref(true) const isLoading = ref(true)
const error = ref<string>('') const error = ref<string>('')
@ -63,23 +64,6 @@ onMounted(async () => {
await puzzlesStore.loadPuzzles() await puzzlesStore.loadPuzzles()
console.log('Puzzles loaded:', puzzlesStore.puzzles.length) console.log('Puzzles loaded:', puzzlesStore.puzzles.length)
// Create mock collection from loaded puzzles for display
if (puzzlesStore.puzzles.length > 0) {
collections.value = [{
id: 1,
steam_id: '3479142989',
title: 'PolyLAN 41',
description: 'Puzzle collection for PolyLAN 41 fil rouge',
author_name: 'Flame Legrems',
total_items: puzzlesStore.puzzles.length,
unique_visitors: 31,
current_favorites: 1,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
}]
console.log('Collection created')
}
// Load existing submissions using store // Load existing submissions using store
console.log('Loading submissions...') console.log('Loading submissions...')
await submissionsStore.loadSubmissions() await submissionsStore.loadSubmissions()
@ -204,11 +188,11 @@ const reloadPage = () => {
<!-- Main Content --> <!-- Main Content -->
<div v-else class="space-y-8"> <div v-else class="space-y-8">
<!-- Collection Info --> <!-- Collection Info -->
<div v-if="collections.length > 0" class="mb-8"> <div class="mb-8">
<div class="card bg-base-100 shadow-lg"> <div class="card bg-base-100 shadow-lg">
<div class="card-body"> <div class="card-body">
<h2 class="card-title text-2xl">{{ collections[0].title }}</h2> <h2 class="card-title text-2xl">{{ props.collectionTitle }}</h2>
<p class="text-base-content/70">{{ collections[0].description }}</p> <p class="text-base-content/70">{{ props.collectionDescription }}</p>
<div class="flex flex-wrap gap-4 mt-4"> <div class="flex flex-wrap gap-4 mt-4">
<button <button
@click="openSubmissionModal" @click="openSubmissionModal"

View File

@ -43,7 +43,7 @@
<tbody> <tbody>
<tr v-for="response in responsesNeedingValidation" :key="response.id"> <tr v-for="response in responsesNeedingValidation" :key="response.id">
<td> <td>
<div class="font-bold">{{ response.puzzle_name }}</div> <div class="font-bold">{{ response.puzzle_title }}</div>
<div class="text-sm opacity-50">ID: {{ response.id }}</div> <div class="text-sm opacity-50">ID: {{ response.id }}</div>
</td> </td>
<td> <td>
@ -110,19 +110,43 @@
<!-- Validation Modal --> <!-- Validation Modal -->
<div v-if="validationModal.show" class="modal modal-open"> <div v-if="validationModal.show" class="modal modal-open">
<div class="modal-box"> <div class="modal-box w-11/12 max-w-5xl">
<h3 class="font-bold text-lg mb-4">Validate Response</h3> <h3 class="font-bold text-lg mb-4">Validate Response</h3>
<div v-for="file in validationModal.response.files">
<img :src="file.file_url">
</div>
<div v-if="validationModal.response" class="space-y-4"> <div v-if="validationModal.response" class="space-y-4">
<div class="alert alert-info"> <div class="alert alert-info">
<i class="mdi mdi-information-outline"></i> <i class="mdi mdi-information-outline"></i>
<div> <div>
<div class="font-bold">{{ validationModal.response.puzzle_name }}</div> <div class="font-bold">{{ validationModal.response.puzzle_title }}</div>
<div class="text-sm">Review and correct the OCR data below</div> <div class="text-sm">Review and correct the OCR data below</div>
</div> </div>
</div> </div>
<div class="grid grid-cols-3 gap-4"> <div class="grid grid-cols-4 gap-4">
<div class="form-control">
<label class="label">
<span class="label-text">Puzzle</span>
</label>
<select
v-model="validationModal.data.puzzle"
class="select select-bordered select-sm w-full"
>
<option value="">Select puzzle...</option>
<option
v-for="puzzle in puzzlesStore.puzzles"
:key="puzzle.id"
:value="puzzle.id"
>
{{ puzzle.title }}
</option>
</select>
</div>
<div class="form-control"> <div class="form-control">
<label class="label"> <label class="label">
<span class="label-text">Cost</span> <span class="label-text">Cost</span>
@ -179,8 +203,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { apiService } from '../services/apiService' import { apiService } from '@/services/apiService'
import type { PuzzleResponse } from '../types' import type { PuzzleResponse } from '@/types'
import {usePuzzlesStore} from '@/stores/puzzles'
const puzzlesStore = usePuzzlesStore()
// Reactive data // Reactive data
const stats = ref({ const stats = ref({
@ -199,6 +225,7 @@ const validationModal = ref({
show: false, show: false,
response: null as PuzzleResponse | null, response: null as PuzzleResponse | null,
data: { data: {
puzzle_title: '',
validated_cost: '', validated_cost: '',
validated_cycles: '', validated_cycles: '',
validated_area: '' validated_area: ''
@ -244,6 +271,7 @@ const loadData = async () => {
const openValidationModal = (response: PuzzleResponse) => { const openValidationModal = (response: PuzzleResponse) => {
validationModal.value.response = response validationModal.value.response = response
validationModal.value.data = { validationModal.value.data = {
puzzle: response.puzzle || '',
validated_cost: response.cost || '', validated_cost: response.cost || '',
validated_cycles: response.cycles || '', validated_cycles: response.cycles || '',
validated_area: response.area || '' validated_area: response.area || ''
@ -255,6 +283,7 @@ const closeValidationModal = () => {
validationModal.value.show = false validationModal.value.show = false
validationModal.value.response = null validationModal.value.response = null
validationModal.value.data = { validationModal.value.data = {
puzzle: '',
validated_cost: '', validated_cost: '',
validated_cycles: '', validated_cycles: '',
validated_area: '' validated_area: ''

View File

@ -207,7 +207,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch, nextTick } from 'vue' import { ref, watch, nextTick } from 'vue'
import { ocrService } from '../services/ocrService' import { ocrService } from '@/services/ocrService'
import { usePuzzlesStore } from '@/stores/puzzles' import { usePuzzlesStore } from '@/stores/puzzles'
import type { SubmissionFile, SteamCollectionItem } from '@/types' import type { SubmissionFile, SteamCollectionItem } from '@/types'

View File

@ -93,7 +93,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, watch } from 'vue' import { ref, computed, watch } from 'vue'
import FileUpload from './FileUpload.vue' import FileUpload from '@/components/FileUpload.vue'
import type { SteamCollectionItem, SubmissionFile } from '@/types' import type { SteamCollectionItem, SubmissionFile } from '@/types'
interface Props { interface Props {

View File

@ -1,8 +1,11 @@
import { createApp } from 'vue' import { createApp } from 'vue'
import App from '@/App.vue' import App from '@/App.vue'
import { pinia } from '@/stores' import { pinia } from '@/stores'
import './style.css' import '@/style.css'
const app = createApp(App) // const app = createApp(App)
const selector = "#app"
const mountData = document.querySelector<HTMLElement>(selector)
const app = createApp(App, { ...mountData?.dataset })
app.use(pinia) app.use(pinia)
app.mount('#app') app.mount(selector)

View File

@ -2,7 +2,7 @@ import { defineStore } from 'pinia'
import { ref } from 'vue' import { ref } from 'vue'
import type { Submission, SubmissionFile } from '@/types' import type { Submission, SubmissionFile } from '@/types'
import { submissionHelpers } from '@/services/apiService' import { submissionHelpers } from '@/services/apiService'
import { usePuzzlesStore } from './puzzles' import { usePuzzlesStore } from '@/stores/puzzles'
export const useSubmissionsStore = defineStore('submissions', () => { export const useSubmissionsStore = defineStore('submissions', () => {
// State // State

View File

@ -32,9 +32,11 @@ def list_puzzles(request):
@paginate @paginate
def list_submissions(request): def list_submissions(request):
"""Get paginated list of submissions""" """Get paginated list of submissions"""
return Submission.objects.prefetch_related( return (
"responses__files", "responses__puzzle" Submission.objects.prefetch_related("responses__files", "responses__puzzle")
).filter(user=request.user) .filter(user=request.user)
.filter()
)
@router.get("/submissions/{submission_id}", response=SubmissionOut) @router.get("/submissions/{submission_id}", response=SubmissionOut)
@ -172,14 +174,19 @@ def validate_response(request, response_id: int, data: ValidationIn):
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"}
try: response = get_object_or_404(PuzzleResponse, id=response_id)
response = PuzzleResponse.objects.select_related("puzzle").get(id=response_id)
if data.puzzle is not None:
puzzle = get_object_or_404(SteamCollectionItem, id=data.puzzle)
response.puzzle = puzzle
# Update validated values # Update validated values
if data.validated_cost is not None: if data.validated_cost is not None:
response.validated_cost = data.validated_cost response.validated_cost = data.validated_cost
if data.validated_cycles is not None: if data.validated_cycles is not None:
response.validated_cycles = data.validated_cycles response.validated_cycles = data.validated_cycles
if data.validated_area is not None: if data.validated_area is not None:
response.validated_area = data.validated_area response.validated_area = data.validated_area
@ -191,9 +198,6 @@ def validate_response(request, response_id: int, data: ValidationIn):
return response return response
except PuzzleResponse.DoesNotExist:
raise Http404("Response not found")
@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):
@ -204,6 +208,7 @@ 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)
.select_related("puzzle", "submission") .select_related("puzzle", "submission")
.prefetch_related("files") .prefetch_related("files")
) )
@ -216,8 +221,7 @@ def validate_submission(request, submission_id: str):
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"}
try: submission = get_object_or_404(Submission, id=submission_id)
submission = Submission.objects.get(id=submission_id)
submission.is_validated = True submission.is_validated = True
submission.validated_by = request.user submission.validated_by = request.user
@ -234,9 +238,6 @@ def validate_submission(request, submission_id: str):
return submission return submission
except Submission.DoesNotExist:
raise Http404("Submission not found")
@router.delete("/submissions/{submission_id}") @router.delete("/submissions/{submission_id}")
def delete_submission(request, submission_id: str): def delete_submission(request, submission_id: str):
@ -245,14 +246,10 @@ def delete_submission(request, submission_id: str):
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"}
try: submission = get_object_or_404(Submission, id=submission_id)
submission = Submission.objects.get(id=submission_id)
submission.delete() submission.delete()
return {"detail": "Submission deleted successfully"} return {"detail": "Submission deleted successfully"}
except Submission.DoesNotExist:
raise Http404("Submission not found")
@router.get("/stats") @router.get("/stats")
def get_stats(request): def get_stats(request):
@ -270,7 +267,7 @@ def get_stats(request):
"total_responses": total_responses, "total_responses": total_responses,
"needs_validation": needs_validation, "needs_validation": needs_validation,
"validated_submissions": validated_submissions, "validated_submissions": validated_submissions,
"validation_rate": validated_submissions / total_submissions "validation_rate": (total_responses - needs_validation) / total_responses
if total_submissions > 0 if total_responses
else 0, else 0,
} }

View File

@ -0,0 +1,17 @@
# Generated by Django 5.2.7 on 2025-10-30 20:39
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('submissions', '0007_submission_manual_validation_requested'),
]
operations = [
migrations.AlterUniqueTogether(
name='puzzleresponse',
unique_together=set(),
),
]

View File

@ -327,7 +327,6 @@ class PuzzleResponse(models.Model):
class Meta: class Meta:
ordering = ["submission", "puzzle__order_index"] ordering = ["submission", "puzzle__order_index"]
unique_together = ["submission", "puzzle"]
verbose_name = "Puzzle Response" verbose_name = "Puzzle Response"
verbose_name_plural = "Puzzle Responses" verbose_name_plural = "Puzzle Responses"

View File

@ -125,6 +125,7 @@ class SubmissionListOut(Schema):
class ValidationIn(Schema): class ValidationIn(Schema):
"""Schema for manual validation input""" """Schema for manual validation input"""
puzzle: Optional[int] = None
validated_cost: Optional[str] = None validated_cost: Optional[str] = None
validated_cycles: Optional[str] = None validated_cycles: Optional[str] = None
validated_area: Optional[str] = None validated_area: Optional[str] = None

View File

@ -10,6 +10,6 @@
{% vite_asset 'src/main.ts' %} {% vite_asset 'src/main.ts' %}
</head> </head>
<body> <body>
<div id="app"></div> <div id="app" data-collection-title="{{ collection.title }}" data-collection-url="{{ collection.url }}" data-collection-description="{{ collection.description }}"></div>
</body> </body>
</html> </html>