diff --git a/opus_submitter/opus_submitter/api.py b/opus_submitter/opus_submitter/api.py index 56c2ec1..e75c90d 100644 --- a/opus_submitter/opus_submitter/api.py +++ b/opus_submitter/opus_submitter/api.py @@ -1,6 +1,7 @@ from ninja import NinjaAPI from ninja.security import django_auth from submissions.api import router as submissions_router +from submissions.schemas import UserInfoOut # Create the main API instance api = NinjaAPI( @@ -39,3 +40,29 @@ def api_info(request): "Admin validation tools", ], } + + +# User info endpoint +@api.get("/user", response=UserInfoOut) +def get_user_info(request): + """Get current user information""" + user = request.user + + if user.is_authenticated: + return { + "id": user.id, + "username": user.username, + "first_name": user.first_name, + "last_name": user.last_name, + "email": user.email, + "is_authenticated": True, + "is_staff": user.is_staff, + "is_superuser": user.is_superuser, + "cas_groups": getattr(user, 'cas_groups', []) + } + else: + return { + "is_authenticated": False, + "is_staff": False, + "is_superuser": False, + } diff --git a/opus_submitter/opus_submitter/settings.py b/opus_submitter/opus_submitter/settings.py index bced24b..e1b900c 100644 --- a/opus_submitter/opus_submitter/settings.py +++ b/opus_submitter/opus_submitter/settings.py @@ -133,24 +133,24 @@ AUTH_USER_MODEL = "accounts.CustomUser" CAS_SERVER_URL = "https://polylan.ch/cas/" # Steam API Configuration -STEAM_API_KEY = os.environ.get('STEAM_API_KEY', None) # Set via environment variable +STEAM_API_KEY = os.environ.get("STEAM_API_KEY", None) # Set via environment variable # File Upload Settings -MEDIA_URL = '/media/' -MEDIA_ROOT = BASE_DIR / 'media' +MEDIA_URL = "/media/" +MEDIA_ROOT = BASE_DIR / "media" # File Upload Limits -FILE_UPLOAD_MAX_MEMORY_SIZE = 10 * 1024 * 1024 # 10MB -DATA_UPLOAD_MAX_MEMORY_SIZE = 10 * 1024 * 1024 # 10MB +FILE_UPLOAD_MAX_MEMORY_SIZE = 256 * 1024 * 1024 # 256MB +DATA_UPLOAD_MAX_MEMORY_SIZE = 256 * 1024 * 1024 # 256MB # Allowed file types for submissions ALLOWED_SUBMISSION_TYPES = [ - 'image/jpeg', - 'image/jpg', - 'image/png', - 'image/gif', - 'video/mp4', - 'video/webm' + "image/jpeg", + "image/jpg", + "image/png", + "image/gif", + "video/mp4", + "video/webm", ] # Authentication backends diff --git a/opus_submitter/src/App.vue b/opus_submitter/src/App.vue index e2ed0ab..e8853c3 100644 --- a/opus_submitter/src/App.vue +++ b/opus_submitter/src/App.vue @@ -2,13 +2,15 @@ import { ref, onMounted, computed } from 'vue' import PuzzleCard from './components/PuzzleCard.vue' import SubmissionForm from './components/SubmissionForm.vue' -import { puzzleHelpers, submissionHelpers, errorHelpers } from './services/apiService' -import type { SteamCollection, SteamCollectionItem, Submission, PuzzleResponse } from './types' +import AdminPanel from './components/AdminPanel.vue' +import { puzzleHelpers, submissionHelpers, errorHelpers, apiService } from './services/apiService' +import type { SteamCollection, SteamCollectionItem, Submission, PuzzleResponse, UserInfo } from './types' // API data const collections = ref([]) const puzzles = ref([]) const submissions = ref([]) +const userInfo = ref(null) const isLoading = ref(true) const showSubmissionModal = ref(false) const error = ref('') @@ -92,15 +94,22 @@ const mockPuzzles: SteamCollectionItem[] = [ } ] +// Computed properties +const isSuperuser = computed(() => { + return userInfo.value?.is_superuser || false +}) + // Computed property to get responses grouped by puzzle const responsesByPuzzle = computed(() => { const grouped: Record = {} submissions.value.forEach(submission => { submission.responses.forEach(response => { - if (!grouped[response.puzzle_id]) { - grouped[response.puzzle_id] = [] + // Handle both number and object types for puzzle field + const puzzleId = typeof response.puzzle === 'number' ? response.puzzle : response.puzzle.id + if (!grouped[puzzleId]) { + grouped[puzzleId] = [] } - grouped[response.puzzle_id].push(response) + grouped[puzzleId].push(response) }) }) return grouped @@ -111,9 +120,23 @@ onMounted(async () => { isLoading.value = true error.value = '' + console.log('Starting data load...') + + // Load user info + console.log('Loading user info...') + const userResponse = await apiService.getUserInfo() + if (userResponse.data) { + userInfo.value = userResponse.data + console.log('User info loaded:', userResponse.data) + } else if (userResponse.error) { + console.warn('User info error:', userResponse.error) + } + // Load puzzles from API + console.log('Loading puzzles...') const loadedPuzzles = await puzzleHelpers.loadPuzzles() puzzles.value = loadedPuzzles + console.log('Puzzles loaded:', loadedPuzzles.length) // Create mock collection from loaded puzzles for display if (loadedPuzzles.length > 0) { @@ -129,17 +152,23 @@ onMounted(async () => { created_at: new Date().toISOString(), updated_at: new Date().toISOString() }] + console.log('Collection created') } // Load existing submissions + console.log('Loading submissions...') const loadedSubmissions = await submissionHelpers.loadSubmissions() submissions.value = loadedSubmissions + console.log('Submissions loaded:', loadedSubmissions.length) + + console.log('Data load complete!') } catch (err) { error.value = errorHelpers.getErrorMessage(err) console.error('Failed to load data:', err) } finally { isLoading.value = false + console.log('Loading state set to false') } }) @@ -208,6 +237,17 @@ const findPuzzleByName = (ocrPuzzleName: string): SteamCollectionItem | null =>

Opus Magnum Puzzle Submitter

+
+
+
+ {{ userInfo.username }} + Admin +
+
+
+ Not logged in +
+
@@ -255,6 +295,11 @@ const findPuzzleByName = (ocrPuzzleName: string): SteamCollectionItem | null => + +
+ +
+
{ try { isLoading.value = true - // Load stats - const statsResponse = await apiService.getStats() - if (statsResponse.data) { - stats.value = statsResponse.data + // Load stats (skip if endpoint doesn't exist) + try { + const statsResponse = await apiService.getStats() + if (statsResponse.data) { + stats.value = statsResponse.data + } + } catch (error) { + console.warn('Stats endpoint not available:', error) + // Set default stats + stats.value = { + total_submissions: 0, + total_responses: 0, + needs_validation: 0, + validated_submissions: 0, + validation_rate: 0 + } } // Load responses needing validation diff --git a/opus_submitter/src/components/FileUpload.vue b/opus_submitter/src/components/FileUpload.vue index 77bcd10..aa9a657 100644 --- a/opus_submitter/src/components/FileUpload.vue +++ b/opus_submitter/src/components/FileUpload.vue @@ -37,7 +37,7 @@

- Supported formats: JPG, PNG, GIF (max 10MB each) + Supported formats: JPG, PNG, GIF (max 256MB each)

@@ -238,9 +238,9 @@ const isValidFile = (file: File): boolean => { return false } - // Check file size (10MB limit) - if (file.size > 10 * 1024 * 1024) { - error.value = `${file.name} is too large (max 10MB)` + // Check file size (256MB limit) + if (file.size > 256 * 1024 * 1024) { + error.value = `${file.name} is too large (max 256MB)` return false } diff --git a/opus_submitter/src/components/PuzzleCard.vue b/opus_submitter/src/components/PuzzleCard.vue index 44f0896..77b1a42 100644 --- a/opus_submitter/src/components/PuzzleCard.vue +++ b/opus_submitter/src/components/PuzzleCard.vue @@ -48,7 +48,7 @@ Solutions ({{ responses.length }}) -
+
diff --git a/opus_submitter/src/services/apiService.ts b/opus_submitter/src/services/apiService.ts index 0662aa1..38c3779 100644 --- a/opus_submitter/src/services/apiService.ts +++ b/opus_submitter/src/services/apiService.ts @@ -2,7 +2,8 @@ import type { SteamCollectionItem, Submission, PuzzleResponse, - SubmissionFile + SubmissionFile, + UserInfo } from '../types' // API Configuration @@ -179,6 +180,11 @@ export class ApiService { async healthCheck(): Promise> { return this.request<{ status: string; service: string }>('/health') } + + // User info + async getUserInfo(): Promise> { + return this.request('/user') + } } // Singleton instance diff --git a/opus_submitter/src/types/index.ts b/opus_submitter/src/types/index.ts index 6a18eb3..491f3a9 100644 --- a/opus_submitter/src/types/index.ts +++ b/opus_submitter/src/types/index.ts @@ -73,3 +73,15 @@ export interface Submission { created_at?: string updated_at?: string } + +export interface UserInfo { + id?: number + username?: string + first_name?: string + last_name?: string + email?: string + is_authenticated: boolean + is_staff: boolean + is_superuser: boolean + cas_groups?: string[] +} diff --git a/opus_submitter/submissions/api.py b/opus_submitter/submissions/api.py index 68b9054..30c149f 100644 --- a/opus_submitter/submissions/api.py +++ b/opus_submitter/submissions/api.py @@ -5,13 +5,13 @@ from django.db import transaction from django.core.files.base import ContentFile from django.http import Http404 from django.utils import timezone +from django.shortcuts import get_object_or_404 from typing import List from .models import Submission, PuzzleResponse, SubmissionFile, SteamCollectionItem from .schemas import ( SubmissionIn, SubmissionOut, - SubmissionListOut, PuzzleResponseOut, ValidationIn, SteamCollectionItemOut, @@ -26,23 +26,24 @@ def list_puzzles(request): return SteamCollectionItem.objects.select_related("collection").all() -@router.get("/submissions", response=List[SubmissionListOut]) +@router.get("/submissions", response=List[SubmissionOut]) @paginate def list_submissions(request): """Get paginated list of submissions""" - return Submission.objects.prefetch_related("responses").all() + return Submission.objects.prefetch_related( + "responses__files", "responses__puzzle" + ).filter(user=request.user) @router.get("/submissions/{submission_id}", response=SubmissionOut) def get_submission(request, submission_id: str): """Get detailed submission by ID""" - try: - submission = Submission.objects.prefetch_related( + return get_object_or_404( + Submission.objects.prefetch_related( "responses__files", "responses__puzzle" - ).get(id=submission_id) - return submission - except Submission.DoesNotExist: - raise Http404("Submission not found") + ).filter(user=request.user), + id=submission_id, + ) @router.post("/submissions", response=SubmissionOut) @@ -104,9 +105,9 @@ def create_submission( "detail": f"Invalid file type: {uploaded_file.content_type}" } - # Validate file size (10MB limit) - if uploaded_file.size > 10 * 1024 * 1024: - return 400, {"detail": "File too large (max 10MB)"} + # Validate file size (256MB limit) + if uploaded_file.size > 256 * 1024 * 1024: + return 400, {"detail": "File too large (max 256MB)"} # Create submission file submission_file = SubmissionFile.objects.create( diff --git a/opus_submitter/submissions/schemas.py b/opus_submitter/submissions/schemas.py index 506e22e..ad8c191 100644 --- a/opus_submitter/submissions/schemas.py +++ b/opus_submitter/submissions/schemas.py @@ -10,6 +10,7 @@ from .models import Submission, PuzzleResponse, SubmissionFile, SteamCollectionI # Input Schemas class SubmissionFileIn(Schema): """Schema for file upload data""" + original_filename: str content_type: str ocr_data: Optional[dict] = None @@ -17,6 +18,7 @@ class SubmissionFileIn(Schema): class PuzzleResponseIn(Schema): """Schema for creating a puzzle response""" + puzzle_id: int puzzle_name: str cost: Optional[str] = None @@ -28,6 +30,7 @@ class PuzzleResponseIn(Schema): class SubmissionIn(Schema): """Schema for creating a submission""" + notes: Optional[str] = None responses: List[PuzzleResponseIn] @@ -35,51 +38,76 @@ class SubmissionIn(Schema): # Output Schemas class SubmissionFileOut(ModelSchema): """Schema for submission file output""" + file_url: Optional[str] - + class Meta: model = SubmissionFile fields = [ - 'id', 'original_filename', 'file_size', 'content_type', - 'ocr_processed', 'ocr_raw_data', 'ocr_error', 'created_at' + "id", + "original_filename", + "file_size", + "content_type", + "ocr_processed", + "ocr_raw_data", + "ocr_error", + "created_at", ] class PuzzleResponseOut(ModelSchema): """Schema for puzzle response output""" + files: List[SubmissionFileOut] final_cost: Optional[str] final_cycles: Optional[str] final_area: Optional[str] - + class Meta: model = PuzzleResponse fields = [ - 'id', 'puzzle', 'puzzle_name', 'cost', 'cycles', 'area', - 'needs_manual_validation', 'ocr_confidence_score', - 'validated_cost', 'validated_cycles', 'validated_area', - 'created_at', 'updated_at' + "id", + "puzzle", + "puzzle_name", + "cost", + "cycles", + "area", + "needs_manual_validation", + "ocr_confidence_score", + "validated_cost", + "validated_cycles", + "validated_area", + "created_at", + "updated_at", ] class SubmissionOut(ModelSchema): """Schema for submission output""" + responses: List[PuzzleResponseOut] total_responses: int needs_validation: bool - + class Meta: model = Submission fields = [ - 'id', 'user', 'notes', 'is_validated', 'validated_by', - 'validated_at', 'created_at', 'updated_at' + "id", + "user", + "notes", + "is_validated", + "validated_by", + "validated_at", + "created_at", + "updated_at", ] class SubmissionListOut(Schema): """Schema for submission list output""" + id: UUID - user: Optional[int] + # user: int notes: Optional[str] total_responses: int needs_validation: bool @@ -91,6 +119,7 @@ class SubmissionListOut(Schema): # Validation Schemas class ValidationIn(Schema): """Schema for manual validation input""" + validated_cost: Optional[str] = None validated_cycles: Optional[str] = None validated_area: Optional[str] = None @@ -99,24 +128,49 @@ class ValidationIn(Schema): # Collection Schemas class SteamCollectionItemOut(ModelSchema): """Schema for Steam collection item output""" + steam_url: str - + class Meta: model = SteamCollectionItem fields = [ - 'id', 'steam_item_id', 'title', 'author_name', 'description', - 'tags', 'order_index', 'created_at', 'updated_at' + "id", + "steam_item_id", + "title", + "author_name", + "description", + "tags", + "order_index", + "created_at", + "updated_at", ] # Error Schemas class ErrorOut(Schema): """Schema for error responses""" + detail: str code: Optional[str] = None class ValidationErrorOut(Schema): """Schema for validation error responses""" + detail: str errors: dict + + +# User Schemas +class UserInfoOut(Schema): + """Schema for user information output""" + + id: Optional[int] = None + username: Optional[str] = None + first_name: Optional[str] = None + last_name: Optional[str] = None + email: Optional[str] = None + is_authenticated: bool + is_staff: bool + is_superuser: bool + cas_groups: Optional[List[str]] = None