add manual validation request on the submission form
This commit is contained in:
parent
2260c7cc27
commit
8960f551e6
@ -97,7 +97,8 @@ onMounted(async () => {
|
|||||||
|
|
||||||
const handleSubmission = async (submissionData: {
|
const handleSubmission = async (submissionData: {
|
||||||
files: any[],
|
files: any[],
|
||||||
notes?: string
|
notes?: string,
|
||||||
|
manualValidationRequested?: boolean
|
||||||
}) => {
|
}) => {
|
||||||
try {
|
try {
|
||||||
isLoading.value = true
|
isLoading.value = true
|
||||||
@ -107,7 +108,8 @@ const handleSubmission = async (submissionData: {
|
|||||||
const response = await submissionHelpers.createFromFiles(
|
const response = await submissionHelpers.createFromFiles(
|
||||||
submissionData.files,
|
submissionData.files,
|
||||||
puzzles.value,
|
puzzles.value,
|
||||||
submissionData.notes
|
submissionData.notes,
|
||||||
|
submissionData.manualValidationRequested
|
||||||
)
|
)
|
||||||
|
|
||||||
if (response.error) {
|
if (response.error) {
|
||||||
|
|||||||
@ -40,6 +40,25 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Manual Validation Request -->
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label cursor-pointer justify-start gap-3">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
v-model="manualValidationRequested"
|
||||||
|
class="checkbox checkbox-primary"
|
||||||
|
/>
|
||||||
|
<div class="flex-1">
|
||||||
|
<span class="label-text font-medium">Request manual validation</span>
|
||||||
|
<div class="label-text-alt text-xs opacity-70 mt-1">
|
||||||
|
Check this if you want an admin to manually review your submission, even if OCR confidence is high.
|
||||||
|
<br>
|
||||||
|
<em>Note: This will be automatically checked if any OCR confidence is below 50%.</em>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Submit Button -->
|
<!-- Submit Button -->
|
||||||
<div class="card-actions justify-end">
|
<div class="card-actions justify-end">
|
||||||
<button
|
<button
|
||||||
@ -57,7 +76,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed, watch } from 'vue'
|
||||||
import FileUpload from './FileUpload.vue'
|
import FileUpload from './FileUpload.vue'
|
||||||
import type { SteamCollectionItem, SubmissionFile } from '@/types'
|
import type { SteamCollectionItem, SubmissionFile } from '@/types'
|
||||||
|
|
||||||
@ -67,7 +86,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface Emits {
|
interface Emits {
|
||||||
submit: [submissionData: { files: SubmissionFile[], notes?: string }]
|
submit: [submissionData: { files: SubmissionFile[], notes?: string, manualValidationRequested?: boolean }]
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<Props>()
|
const props = defineProps<Props>()
|
||||||
@ -75,6 +94,7 @@ const emit = defineEmits<Emits>()
|
|||||||
|
|
||||||
const submissionFiles = ref<SubmissionFile[]>([])
|
const submissionFiles = ref<SubmissionFile[]>([])
|
||||||
const notes = ref('')
|
const notes = ref('')
|
||||||
|
const manualValidationRequested = ref(false)
|
||||||
const isSubmitting = ref(false)
|
const isSubmitting = ref(false)
|
||||||
|
|
||||||
const notesLength = computed(() => notes.value.length)
|
const notesLength = computed(() => notes.value.length)
|
||||||
@ -104,6 +124,23 @@ const responsesByPuzzle = computed(() => {
|
|||||||
return grouped
|
return grouped
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Check if any OCR confidence is below 50%
|
||||||
|
const hasLowConfidence = computed(() => {
|
||||||
|
return submissionFiles.value.some(file => {
|
||||||
|
if (!file.ocrData?.confidence) return false
|
||||||
|
return file.ocrData.confidence.cost < 0.5 ||
|
||||||
|
file.ocrData.confidence.cycles < 0.5 ||
|
||||||
|
file.ocrData.confidence.area < 0.5
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Auto-check manual validation when confidence is low
|
||||||
|
watch(hasLowConfidence, (newValue) => {
|
||||||
|
if (newValue && !manualValidationRequested.value) {
|
||||||
|
manualValidationRequested.value = true
|
||||||
|
}
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!canSubmit.value) return
|
if (!canSubmit.value) return
|
||||||
|
|
||||||
@ -113,12 +150,14 @@ const handleSubmit = async () => {
|
|||||||
// Emit the files and notes for the parent to handle API submission
|
// Emit the files and notes for the parent to handle API submission
|
||||||
emit('submit', {
|
emit('submit', {
|
||||||
files: submissionFiles.value,
|
files: submissionFiles.value,
|
||||||
notes: notes.value.trim() || undefined
|
notes: notes.value.trim() || undefined,
|
||||||
|
manualValidationRequested: manualValidationRequested.value
|
||||||
})
|
})
|
||||||
|
|
||||||
// Reset form
|
// Reset form
|
||||||
submissionFiles.value = []
|
submissionFiles.value = []
|
||||||
notes.value = ''
|
notes.value = ''
|
||||||
|
manualValidationRequested.value = false
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Submission error:', error)
|
console.error('Submission error:', error)
|
||||||
|
|||||||
@ -115,6 +115,7 @@ export class ApiService {
|
|||||||
async createSubmission(
|
async createSubmission(
|
||||||
submissionData: {
|
submissionData: {
|
||||||
notes?: string
|
notes?: string
|
||||||
|
manual_validation_requested?: boolean
|
||||||
responses: Array<{
|
responses: Array<{
|
||||||
puzzle_id: number
|
puzzle_id: number
|
||||||
puzzle_name: string
|
puzzle_name: string
|
||||||
@ -225,9 +226,10 @@ 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
|
||||||
): Promise<ApiResponse<Submission>> {
|
): Promise<ApiResponse<Submission>> {
|
||||||
// Group files by detected puzzle
|
// Group files by detected puzzle
|
||||||
const responsesByPuzzle: Record<string, {
|
const responsesByPuzzle: Record<string, {
|
||||||
@ -286,7 +288,11 @@ 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({ notes, responses }, fileObjects)
|
return apiService.createSubmission({
|
||||||
|
notes,
|
||||||
|
manual_validation_requested: manualValidationRequested,
|
||||||
|
responses
|
||||||
|
}, fileObjects)
|
||||||
},
|
},
|
||||||
|
|
||||||
async loadSubmissions(limit = 20, offset = 0): Promise<Submission[]> {
|
async loadSubmissions(limit = 20, offset = 0): Promise<Submission[]> {
|
||||||
|
|||||||
@ -78,6 +78,7 @@ export interface Submission {
|
|||||||
is_validated?: boolean
|
is_validated?: boolean
|
||||||
validated_by?: number | null
|
validated_by?: number | null
|
||||||
validated_at?: string | null
|
validated_at?: string | null
|
||||||
|
manual_validation_requested?: boolean
|
||||||
total_responses?: number
|
total_responses?: number
|
||||||
needs_validation?: boolean
|
needs_validation?: boolean
|
||||||
created_at?: string
|
created_at?: string
|
||||||
|
|||||||
@ -168,10 +168,10 @@ class PuzzleResponseInline(admin.TabularInline):
|
|||||||
class SubmissionAdmin(admin.ModelAdmin):
|
class SubmissionAdmin(admin.ModelAdmin):
|
||||||
list_display = [
|
list_display = [
|
||||||
"id", "user", "total_responses", "needs_validation",
|
"id", "user", "total_responses", "needs_validation",
|
||||||
"is_validated", "created_at"
|
"manual_validation_requested", "is_validated", "created_at"
|
||||||
]
|
]
|
||||||
list_filter = [
|
list_filter = [
|
||||||
"is_validated", "created_at", "updated_at"
|
"is_validated", "manual_validation_requested", "created_at", "updated_at"
|
||||||
]
|
]
|
||||||
search_fields = ["id", "user__username", "notes"]
|
search_fields = ["id", "user__username", "notes"]
|
||||||
readonly_fields = ["id", "created_at", "updated_at", "total_responses", "needs_validation"]
|
readonly_fields = ["id", "created_at", "updated_at", "total_responses", "needs_validation"]
|
||||||
@ -182,7 +182,7 @@ class SubmissionAdmin(admin.ModelAdmin):
|
|||||||
"fields": ("id", "user", "notes")
|
"fields": ("id", "user", "notes")
|
||||||
}),
|
}),
|
||||||
("Validation", {
|
("Validation", {
|
||||||
"fields": ("is_validated", "validated_by", "validated_at")
|
"fields": ("manual_validation_requested", "is_validated", "validated_by", "validated_at")
|
||||||
}),
|
}),
|
||||||
("Statistics", {
|
("Statistics", {
|
||||||
"fields": ("total_responses", "needs_validation"),
|
"fields": ("total_responses", "needs_validation"),
|
||||||
|
|||||||
@ -67,10 +67,19 @@ def create_submission(
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
|
# Check if any confidence score is below 50% to auto-request validation
|
||||||
|
auto_request_validation = any(
|
||||||
|
(response_data.ocr_confidence_cost is not None and response_data.ocr_confidence_cost < 0.5) or
|
||||||
|
(response_data.ocr_confidence_cycles is not None and response_data.ocr_confidence_cycles < 0.5) or
|
||||||
|
(response_data.ocr_confidence_area is not None and response_data.ocr_confidence_area < 0.5)
|
||||||
|
for response_data in data.responses
|
||||||
|
)
|
||||||
|
|
||||||
# Create the submission
|
# Create the submission
|
||||||
submission = Submission.objects.create(
|
submission = Submission.objects.create(
|
||||||
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 or auto_request_validation,
|
||||||
)
|
)
|
||||||
|
|
||||||
file_index = 0
|
file_index = 0
|
||||||
|
|||||||
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2025-10-30 11:05
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('submissions', '0006_remove_puzzleresponse_ocr_confidence_score_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='submission',
|
||||||
|
name='manual_validation_requested',
|
||||||
|
field=models.BooleanField(default=False, help_text='Whether the user specifically requested manual validation'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -242,6 +242,12 @@ class Submission(models.Model):
|
|||||||
validated_at = models.DateTimeField(
|
validated_at = models.DateTimeField(
|
||||||
null=True, blank=True, help_text="When this submission was validated"
|
null=True, blank=True, help_text="When this submission was validated"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Manual validation request
|
||||||
|
manual_validation_requested = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text="Whether the user specifically requested manual validation"
|
||||||
|
)
|
||||||
|
|
||||||
# Timestamps
|
# Timestamps
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|||||||
@ -34,6 +34,7 @@ class SubmissionIn(Schema):
|
|||||||
"""Schema for creating a submission"""
|
"""Schema for creating a submission"""
|
||||||
|
|
||||||
notes: Optional[str] = None
|
notes: Optional[str] = None
|
||||||
|
manual_validation_requested: bool = False
|
||||||
responses: List[PuzzleResponseIn]
|
responses: List[PuzzleResponseIn]
|
||||||
|
|
||||||
|
|
||||||
@ -102,6 +103,7 @@ class SubmissionOut(ModelSchema):
|
|||||||
"is_validated",
|
"is_validated",
|
||||||
"validated_by",
|
"validated_by",
|
||||||
"validated_at",
|
"validated_at",
|
||||||
|
"manual_validation_requested",
|
||||||
"created_at",
|
"created_at",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
]
|
]
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user