some fixes
This commit is contained in:
parent
0e1e77c2dd
commit
f98145d6db
@ -28,7 +28,15 @@ from .api import api
|
||||
|
||||
@login_required
|
||||
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 = [
|
||||
|
||||
@ -1,19 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import PuzzleCard from './components/PuzzleCard.vue'
|
||||
import SubmissionForm from './components/SubmissionForm.vue'
|
||||
import AdminPanel from './components/AdminPanel.vue'
|
||||
import { apiService, errorHelpers } from './services/apiService'
|
||||
import { usePuzzlesStore } from './stores/puzzles'
|
||||
import { useSubmissionsStore } from './stores/submissions'
|
||||
import type { SteamCollection, PuzzleResponse, UserInfo } from './types'
|
||||
import { ref, onMounted, computed, defineProps } from 'vue'
|
||||
import PuzzleCard from '@/components/PuzzleCard.vue'
|
||||
import SubmissionForm from '@/components/SubmissionForm.vue'
|
||||
import AdminPanel from '@/components/AdminPanel.vue'
|
||||
import { apiService, errorHelpers } from '@/services/apiService'
|
||||
import { usePuzzlesStore } from '@/stores/puzzles'
|
||||
import { useSubmissionsStore } from '@/stores/submissions'
|
||||
import type { SteamCollection, PuzzleResponse, UserInfo } from '@/types'
|
||||
|
||||
const props = defineProps<{collectionTitle: string, collectionUrl: string, collectionDescription: string}>()
|
||||
|
||||
// Pinia stores
|
||||
const puzzlesStore = usePuzzlesStore()
|
||||
const submissionsStore = useSubmissionsStore()
|
||||
|
||||
// Local state
|
||||
const collections = ref<SteamCollection[]>([])
|
||||
const userInfo = ref<UserInfo | null>(null)
|
||||
const isLoading = ref(true)
|
||||
const error = ref<string>('')
|
||||
@ -63,23 +64,6 @@ onMounted(async () => {
|
||||
await puzzlesStore.loadPuzzles()
|
||||
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
|
||||
console.log('Loading submissions...')
|
||||
await submissionsStore.loadSubmissions()
|
||||
@ -204,11 +188,11 @@ const reloadPage = () => {
|
||||
<!-- Main Content -->
|
||||
<div v-else class="space-y-8">
|
||||
<!-- 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-body">
|
||||
<h2 class="card-title text-2xl">{{ collections[0].title }}</h2>
|
||||
<p class="text-base-content/70">{{ collections[0].description }}</p>
|
||||
<h2 class="card-title text-2xl">{{ props.collectionTitle }}</h2>
|
||||
<p class="text-base-content/70">{{ props.collectionDescription }}</p>
|
||||
<div class="flex flex-wrap gap-4 mt-4">
|
||||
<button
|
||||
@click="openSubmissionModal"
|
||||
|
||||
@ -43,7 +43,7 @@
|
||||
<tbody>
|
||||
<tr v-for="response in responsesNeedingValidation" :key="response.id">
|
||||
<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>
|
||||
</td>
|
||||
<td>
|
||||
@ -110,19 +110,43 @@
|
||||
|
||||
<!-- Validation Modal -->
|
||||
<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>
|
||||
|
||||
<div v-for="file in validationModal.response.files">
|
||||
<img :src="file.file_url">
|
||||
</div>
|
||||
|
||||
<div v-if="validationModal.response" class="space-y-4">
|
||||
<div class="alert alert-info">
|
||||
<i class="mdi mdi-information-outline"></i>
|
||||
<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>
|
||||
</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">
|
||||
<label class="label">
|
||||
<span class="label-text">Cost</span>
|
||||
@ -179,8 +203,10 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { apiService } from '../services/apiService'
|
||||
import type { PuzzleResponse } from '../types'
|
||||
import { apiService } from '@/services/apiService'
|
||||
import type { PuzzleResponse } from '@/types'
|
||||
import {usePuzzlesStore} from '@/stores/puzzles'
|
||||
const puzzlesStore = usePuzzlesStore()
|
||||
|
||||
// Reactive data
|
||||
const stats = ref({
|
||||
@ -199,6 +225,7 @@ const validationModal = ref({
|
||||
show: false,
|
||||
response: null as PuzzleResponse | null,
|
||||
data: {
|
||||
puzzle_title: '',
|
||||
validated_cost: '',
|
||||
validated_cycles: '',
|
||||
validated_area: ''
|
||||
@ -244,6 +271,7 @@ const loadData = async () => {
|
||||
const openValidationModal = (response: PuzzleResponse) => {
|
||||
validationModal.value.response = response
|
||||
validationModal.value.data = {
|
||||
puzzle: response.puzzle || '',
|
||||
validated_cost: response.cost || '',
|
||||
validated_cycles: response.cycles || '',
|
||||
validated_area: response.area || ''
|
||||
@ -255,6 +283,7 @@ const closeValidationModal = () => {
|
||||
validationModal.value.show = false
|
||||
validationModal.value.response = null
|
||||
validationModal.value.data = {
|
||||
puzzle: '',
|
||||
validated_cost: '',
|
||||
validated_cycles: '',
|
||||
validated_area: ''
|
||||
|
||||
@ -207,7 +207,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, nextTick } from 'vue'
|
||||
import { ocrService } from '../services/ocrService'
|
||||
import { ocrService } from '@/services/ocrService'
|
||||
import { usePuzzlesStore } from '@/stores/puzzles'
|
||||
import type { SubmissionFile, SteamCollectionItem } from '@/types'
|
||||
|
||||
|
||||
@ -93,7 +93,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import FileUpload from './FileUpload.vue'
|
||||
import FileUpload from '@/components/FileUpload.vue'
|
||||
import type { SteamCollectionItem, SubmissionFile } from '@/types'
|
||||
|
||||
interface Props {
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from '@/App.vue'
|
||||
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.mount('#app')
|
||||
app.mount(selector)
|
||||
|
||||
@ -2,7 +2,7 @@ import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import type { Submission, SubmissionFile } from '@/types'
|
||||
import { submissionHelpers } from '@/services/apiService'
|
||||
import { usePuzzlesStore } from './puzzles'
|
||||
import { usePuzzlesStore } from '@/stores/puzzles'
|
||||
|
||||
export const useSubmissionsStore = defineStore('submissions', () => {
|
||||
// State
|
||||
@ -16,9 +16,9 @@ export const useSubmissionsStore = defineStore('submissions', () => {
|
||||
try {
|
||||
isLoading.value = true
|
||||
error.value = ''
|
||||
|
||||
|
||||
const loadedSubmissions = await submissionHelpers.loadSubmissions(limit, offset)
|
||||
|
||||
|
||||
if (offset === 0) {
|
||||
submissions.value = loadedSubmissions
|
||||
} else {
|
||||
@ -33,34 +33,34 @@ export const useSubmissionsStore = defineStore('submissions', () => {
|
||||
}
|
||||
|
||||
const createSubmission = async (
|
||||
files: SubmissionFile[],
|
||||
notes?: string,
|
||||
files: SubmissionFile[],
|
||||
notes?: string,
|
||||
manualValidationRequested?: boolean
|
||||
): Promise<Submission | undefined> => {
|
||||
try {
|
||||
isLoading.value = true
|
||||
error.value = ''
|
||||
|
||||
|
||||
const puzzlesStore = usePuzzlesStore()
|
||||
|
||||
|
||||
const response = await submissionHelpers.createFromFiles(
|
||||
files,
|
||||
puzzlesStore.puzzles,
|
||||
notes,
|
||||
manualValidationRequested
|
||||
)
|
||||
|
||||
|
||||
if (response.error) {
|
||||
error.value = response.error
|
||||
throw new Error(response.error)
|
||||
}
|
||||
|
||||
|
||||
if (response.data) {
|
||||
// Add to local submissions list
|
||||
submissions.value.unshift(response.data)
|
||||
return response.data
|
||||
}
|
||||
|
||||
|
||||
return undefined
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err.message : 'Failed to create submission'
|
||||
@ -89,7 +89,7 @@ export const useSubmissionsStore = defineStore('submissions', () => {
|
||||
isLoading,
|
||||
error,
|
||||
isSubmissionModalOpen,
|
||||
|
||||
|
||||
// Actions
|
||||
loadSubmissions,
|
||||
createSubmission,
|
||||
|
||||
@ -32,9 +32,11 @@ def list_puzzles(request):
|
||||
@paginate
|
||||
def list_submissions(request):
|
||||
"""Get paginated list of submissions"""
|
||||
return Submission.objects.prefetch_related(
|
||||
"responses__files", "responses__puzzle"
|
||||
).filter(user=request.user)
|
||||
return (
|
||||
Submission.objects.prefetch_related("responses__files", "responses__puzzle")
|
||||
.filter(user=request.user)
|
||||
.filter()
|
||||
)
|
||||
|
||||
|
||||
@router.get("/submissions/{submission_id}", response=SubmissionOut)
|
||||
@ -172,27 +174,29 @@ def validate_response(request, response_id: int, data: ValidationIn):
|
||||
if not request.user.is_authenticated or not request.user.is_staff:
|
||||
return 403, {"detail": "Admin access required"}
|
||||
|
||||
try:
|
||||
response = PuzzleResponse.objects.select_related("puzzle").get(id=response_id)
|
||||
response = get_object_or_404(PuzzleResponse, id=response_id)
|
||||
|
||||
# Update validated values
|
||||
if data.validated_cost is not None:
|
||||
response.validated_cost = data.validated_cost
|
||||
if data.validated_cycles is not None:
|
||||
response.validated_cycles = data.validated_cycles
|
||||
if data.validated_area is not None:
|
||||
response.validated_area = data.validated_area
|
||||
if data.puzzle is not None:
|
||||
puzzle = get_object_or_404(SteamCollectionItem, id=data.puzzle)
|
||||
response.puzzle = puzzle
|
||||
|
||||
# Mark as no longer needing validation if we have all values
|
||||
if all([response.final_cost, response.final_cycles, response.final_area]):
|
||||
response.needs_manual_validation = False
|
||||
# Update validated values
|
||||
if data.validated_cost is not None:
|
||||
response.validated_cost = data.validated_cost
|
||||
|
||||
response.save()
|
||||
if data.validated_cycles is not None:
|
||||
response.validated_cycles = data.validated_cycles
|
||||
|
||||
return response
|
||||
if data.validated_area is not None:
|
||||
response.validated_area = data.validated_area
|
||||
|
||||
except PuzzleResponse.DoesNotExist:
|
||||
raise Http404("Response not found")
|
||||
# Mark as no longer needing validation if we have all values
|
||||
if all([response.final_cost, response.final_cycles, response.final_area]):
|
||||
response.needs_manual_validation = False
|
||||
|
||||
response.save()
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@router.get("/responses/needs-validation", response=List[PuzzleResponseOut])
|
||||
@ -204,6 +208,7 @@ def list_responses_needing_validation(request):
|
||||
|
||||
return (
|
||||
PuzzleResponse.objects.filter(needs_manual_validation=True)
|
||||
.filter(puzzle__collection__is_active=True)
|
||||
.select_related("puzzle", "submission")
|
||||
.prefetch_related("files")
|
||||
)
|
||||
@ -216,26 +221,22 @@ def validate_submission(request, submission_id: str):
|
||||
if not request.user.is_authenticated or not request.user.is_staff:
|
||||
return 403, {"detail": "Admin access required"}
|
||||
|
||||
try:
|
||||
submission = Submission.objects.get(id=submission_id)
|
||||
submission = get_object_or_404(Submission, id=submission_id)
|
||||
|
||||
submission.is_validated = True
|
||||
submission.validated_by = request.user
|
||||
submission.validated_at = timezone.now()
|
||||
submission.save()
|
||||
submission.is_validated = True
|
||||
submission.validated_by = request.user
|
||||
submission.validated_at = timezone.now()
|
||||
submission.save()
|
||||
|
||||
# Also mark all responses as not needing validation
|
||||
submission.responses.update(needs_manual_validation=False)
|
||||
# Also mark all responses as not needing validation
|
||||
submission.responses.update(needs_manual_validation=False)
|
||||
|
||||
# Reload with relations
|
||||
submission = Submission.objects.prefetch_related(
|
||||
"responses__files", "responses__puzzle"
|
||||
).get(id=submission.id)
|
||||
# Reload with relations
|
||||
submission = Submission.objects.prefetch_related(
|
||||
"responses__files", "responses__puzzle"
|
||||
).get(id=submission.id)
|
||||
|
||||
return submission
|
||||
|
||||
except Submission.DoesNotExist:
|
||||
raise Http404("Submission not found")
|
||||
return submission
|
||||
|
||||
|
||||
@router.delete("/submissions/{submission_id}")
|
||||
@ -245,13 +246,9 @@ def delete_submission(request, submission_id: str):
|
||||
if not request.user.is_authenticated or not request.user.is_staff:
|
||||
return 403, {"detail": "Admin access required"}
|
||||
|
||||
try:
|
||||
submission = Submission.objects.get(id=submission_id)
|
||||
submission.delete()
|
||||
return {"detail": "Submission deleted successfully"}
|
||||
|
||||
except Submission.DoesNotExist:
|
||||
raise Http404("Submission not found")
|
||||
submission = get_object_or_404(Submission, id=submission_id)
|
||||
submission.delete()
|
||||
return {"detail": "Submission deleted successfully"}
|
||||
|
||||
|
||||
@router.get("/stats")
|
||||
@ -270,7 +267,7 @@ def get_stats(request):
|
||||
"total_responses": total_responses,
|
||||
"needs_validation": needs_validation,
|
||||
"validated_submissions": validated_submissions,
|
||||
"validation_rate": validated_submissions / total_submissions
|
||||
if total_submissions > 0
|
||||
"validation_rate": (total_responses - needs_validation) / total_responses
|
||||
if total_responses
|
||||
else 0,
|
||||
}
|
||||
|
||||
@ -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(),
|
||||
),
|
||||
]
|
||||
@ -327,7 +327,6 @@ class PuzzleResponse(models.Model):
|
||||
|
||||
class Meta:
|
||||
ordering = ["submission", "puzzle__order_index"]
|
||||
unique_together = ["submission", "puzzle"]
|
||||
verbose_name = "Puzzle Response"
|
||||
verbose_name_plural = "Puzzle Responses"
|
||||
|
||||
|
||||
@ -125,6 +125,7 @@ class SubmissionListOut(Schema):
|
||||
class ValidationIn(Schema):
|
||||
"""Schema for manual validation input"""
|
||||
|
||||
puzzle: Optional[int] = None
|
||||
validated_cost: Optional[str] = None
|
||||
validated_cycles: Optional[str] = None
|
||||
validated_area: Optional[str] = None
|
||||
|
||||
@ -10,6 +10,6 @@
|
||||
{% vite_asset 'src/main.ts' %}
|
||||
</head>
|
||||
<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>
|
||||
</html>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user