user info + max size

This commit is contained in:
Loïc Gremaud 2025-10-29 03:31:32 +01:00
parent 52723b200a
commit 961e14bd43
10 changed files with 210 additions and 53 deletions

View File

@ -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,
}

View File

@ -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

View File

@ -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<SteamCollection[]>([])
const puzzles = ref<SteamCollectionItem[]>([])
const submissions = ref<Submission[]>([])
const userInfo = ref<UserInfo | null>(null)
const isLoading = ref(true)
const showSubmissionModal = ref(false)
const error = ref<string>('')
@ -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<number, PuzzleResponse[]> = {}
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 =>
<div class="flex-1">
<h1 class="text-xl font-bold">Opus Magnum Puzzle Submitter</h1>
</div>
<div class="flex-none">
<div v-if="userInfo?.is_authenticated" class="flex items-center gap-2">
<div class="text-sm">
<span class="font-medium">{{ userInfo.username }}</span>
<span v-if="userInfo.is_superuser" class="badge badge-warning badge-xs ml-1">Admin</span>
</div>
</div>
<div v-else class="text-sm text-base-content/70">
Not logged in
</div>
</div>
</div>
</div>
@ -255,6 +295,11 @@ const findPuzzleByName = (ocrPuzzleName: string): SteamCollectionItem | null =>
</div>
</div>
<!-- Admin Panel (only for superusers) -->
<div v-if="isSuperuser">
<AdminPanel />
</div>
<!-- Puzzles Grid -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<PuzzleCard

View File

@ -183,10 +183,22 @@ const loadData = async () => {
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

View File

@ -37,7 +37,7 @@
</button>
</div>
<p class="text-xs text-base-content/50">
Supported formats: JPG, PNG, GIF (max 10MB each)
Supported formats: JPG, PNG, GIF (max 256MB each)
</p>
</div>
@ -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
}

View File

@ -48,7 +48,7 @@
<span class="text-sm font-medium">Solutions ({{ responses.length }})</span>
</div>
<div class="overflow-x-auto">
<div>
<table class="table table-xs">
<thead>
<tr>

View File

@ -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<ApiResponse<{ status: string; service: string }>> {
return this.request<{ status: string; service: string }>('/health')
}
// User info
async getUserInfo(): Promise<ApiResponse<UserInfo>> {
return this.request<UserInfo>('/user')
}
}
// Singleton instance

View File

@ -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[]
}

View File

@ -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(

View File

@ -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