Compare commits
No commits in common. "5aeff9c2180d187c4028a5680c328da6fcbb6c77" and "9662181d4db606423ccd14be8c4d98562ce2e2db" have entirely different histories.
5aeff9c218
...
9662181d4d
@ -1,7 +1,6 @@
|
|||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
from ninja import Router
|
from ninja import Router
|
||||||
from django.core.cache import cache
|
|
||||||
|
|
||||||
from accounts.models import CustomUser
|
from accounts.models import CustomUser
|
||||||
from animations.schemas import RankingSchema
|
from animations.schemas import RankingSchema
|
||||||
@ -13,12 +12,6 @@ router = Router()
|
|||||||
|
|
||||||
@router.get("results", response=RankingSchema)
|
@router.get("results", response=RankingSchema)
|
||||||
def results(request: HttpRequest) -> dict:
|
def results(request: HttpRequest) -> dict:
|
||||||
cache_key = "api:results:results"
|
|
||||||
cached_data = cache.get(cache_key)
|
|
||||||
|
|
||||||
if cached_data is not None:
|
|
||||||
return cached_data
|
|
||||||
|
|
||||||
responses_by_userid = defaultdict(list)
|
responses_by_userid = defaultdict(list)
|
||||||
responses_by_puzzleid = defaultdict(list)
|
responses_by_puzzleid = defaultdict(list)
|
||||||
|
|
||||||
@ -37,12 +30,9 @@ def results(request: HttpRequest) -> dict:
|
|||||||
responses, key=lambda x: (x.rank_points is None, x.rank_points or 0)
|
responses, key=lambda x: (x.rank_points is None, x.rank_points or 0)
|
||||||
)
|
)
|
||||||
|
|
||||||
data = {
|
return {
|
||||||
"users": list(CustomUser.objects.filter(pk__in=responses_by_userid.keys())),
|
"users": CustomUser.objects.filter(pk__in=responses_by_userid.keys()),
|
||||||
"puzzles": list(SteamCollectionItem.objects.all()),
|
"puzzles": SteamCollectionItem.objects.all(),
|
||||||
"responses_by_userid": dict(responses_by_userid),
|
"responses_by_userid": responses_by_userid,
|
||||||
"ranking_by_puzzle": ranking,
|
"ranking_by_puzzle": ranking,
|
||||||
}
|
}
|
||||||
|
|
||||||
cache.set("api:results:results", data, 300)
|
|
||||||
return data
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
from ninja import ModelSchema, Schema
|
from ninja import ModelSchema, Schema
|
||||||
|
|
||||||
from submissions.models import PuzzleResponse
|
from submissions.models import PuzzleResponse
|
||||||
from submissions.schemas import SteamCollectionItemOut
|
from submissions.schemas import SteamCollectionItemOut, UserInfoOut
|
||||||
|
|
||||||
|
|
||||||
class PuzzleResponseRankingOut(ModelSchema):
|
class PuzzleResponseRankingOut(ModelSchema):
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
from django.core.files.base import ContentFile
|
from django.core.files.base import ContentFile
|
||||||
from django.core.cache import cache
|
|
||||||
from django.db.models import (
|
from django.db.models import (
|
||||||
F,
|
F,
|
||||||
Case,
|
Case,
|
||||||
@ -33,11 +32,6 @@ def get_my_objectives(request: HttpRequest):
|
|||||||
|
|
||||||
@router.get("results", response=ResultsOut)
|
@router.get("results", response=ResultsOut)
|
||||||
def get_results(request: HttpRequest):
|
def get_results(request: HttpRequest):
|
||||||
cache_key = f"api:noita:results:{request.user.id}"
|
|
||||||
cached_data = cache.get(cache_key)
|
|
||||||
|
|
||||||
if cached_data is not None:
|
|
||||||
return cached_data
|
|
||||||
"""
|
"""
|
||||||
Get the user's score based on their objectives.
|
Get the user's score based on their objectives.
|
||||||
|
|
||||||
@ -98,14 +92,11 @@ def get_results(request: HttpRequest):
|
|||||||
for obj in user_objectives.order_by("-total_points")
|
for obj in user_objectives.order_by("-total_points")
|
||||||
]
|
]
|
||||||
|
|
||||||
data = {
|
return {
|
||||||
"total_score": total_score,
|
"total_score": total_score,
|
||||||
"objectives": objectives_with_points,
|
"objectives": objectives_with_points,
|
||||||
}
|
}
|
||||||
|
|
||||||
cache.set(f"api:noita:results:{request.user.id}", data, 300)
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("leaderboard", response=LeaderboardOut)
|
@router.get("leaderboard", response=LeaderboardOut)
|
||||||
def get_leaderboard(request: HttpRequest):
|
def get_leaderboard(request: HttpRequest):
|
||||||
@ -114,12 +105,6 @@ def get_leaderboard(request: HttpRequest):
|
|||||||
|
|
||||||
Uses Window functions to rank users by their total score in descending order.
|
Uses Window functions to rank users by their total score in descending order.
|
||||||
"""
|
"""
|
||||||
cache_key = "api:noita:leaderboard"
|
|
||||||
cached_data = cache.get(cache_key)
|
|
||||||
|
|
||||||
if cached_data is not None:
|
|
||||||
return cached_data
|
|
||||||
|
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
|
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
@ -189,7 +174,7 @@ def get_leaderboard(request: HttpRequest):
|
|||||||
.order_by("rank")
|
.order_by("rank")
|
||||||
)
|
)
|
||||||
|
|
||||||
data = {
|
return {
|
||||||
"leaderboard": [
|
"leaderboard": [
|
||||||
{
|
{
|
||||||
"rank": entry["rank"],
|
"rank": entry["rank"],
|
||||||
@ -201,9 +186,6 @@ def get_leaderboard(request: HttpRequest):
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
cache.set("api:noita:leaderboard", data, 300)
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("submit", response={200: NoitaSubmissionOut, 400: dict})
|
@router.post("submit", response={200: NoitaSubmissionOut, 400: dict})
|
||||||
def submit_log_file(request: HttpRequest, file: UploadedFile = File(...)):
|
def submit_log_file(request: HttpRequest, file: UploadedFile = File(...)):
|
||||||
@ -221,6 +203,12 @@ def submit_log_file(request: HttpRequest, file: UploadedFile = File(...)):
|
|||||||
allowed_types = [
|
allowed_types = [
|
||||||
"text/plain",
|
"text/plain",
|
||||||
"text/x-log",
|
"text/x-log",
|
||||||
|
"image/jpeg",
|
||||||
|
"image/jpg",
|
||||||
|
"image/png",
|
||||||
|
"image/gif",
|
||||||
|
"video/mp4",
|
||||||
|
"video/webm",
|
||||||
]
|
]
|
||||||
|
|
||||||
if file.content_type not in allowed_types:
|
if file.content_type not in allowed_types:
|
||||||
@ -250,11 +238,6 @@ def submit_log_file(request: HttpRequest, file: UploadedFile = File(...)):
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Invalidate caches on successful submission
|
|
||||||
if submission.user:
|
|
||||||
cache.delete(f"api:noita:results:{submission.user.id}")
|
|
||||||
cache.delete("api:noita:leaderboard")
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"id": str(submission.id),
|
"id": str(submission.id),
|
||||||
"user_id": submission.user_id,
|
"user_id": submission.user_id,
|
||||||
|
|||||||
@ -1,6 +1,4 @@
|
|||||||
from ninja import NinjaAPI
|
from ninja import NinjaAPI
|
||||||
from django.core.cache import cache
|
|
||||||
from django.http import HttpRequest
|
|
||||||
from submissions.api import router as submissions_router
|
from submissions.api import router as submissions_router
|
||||||
from submissions.schemas import UserInfoOut
|
from submissions.schemas import UserInfoOut
|
||||||
from animations.api import router as results_router
|
from animations.api import router as results_router
|
||||||
@ -43,19 +41,6 @@ def health_check(request):
|
|||||||
return {"status": "healthy", "service": "polylan-submitter-api"}
|
return {"status": "healthy", "service": "polylan-submitter-api"}
|
||||||
|
|
||||||
|
|
||||||
# Cache management endpoint
|
|
||||||
@api.post("/cache/clear")
|
|
||||||
def clear_cache(request: HttpRequest):
|
|
||||||
"""Clear all API caches (admin only)"""
|
|
||||||
if not request.user.is_authenticated or not request.user.is_staff:
|
|
||||||
return 403, {"detail": "Admin access required"}
|
|
||||||
|
|
||||||
keys = cache.keys("api:*")
|
|
||||||
cache.delete_many(keys)
|
|
||||||
|
|
||||||
return {"detail": f"Cleared {len(keys)} cache entries"}
|
|
||||||
|
|
||||||
|
|
||||||
# User info endpoint
|
# User info endpoint
|
||||||
@api.get("/user", response=UserInfoOut)
|
@api.get("/user", response=UserInfoOut)
|
||||||
def get_user_info(request):
|
def get_user_info(request):
|
||||||
|
|||||||
@ -145,18 +145,6 @@ MEDIA_ROOT = BASE_DIR / "media"
|
|||||||
FILE_UPLOAD_MAX_MEMORY_SIZE = 256 * 1024 * 1024 # 256MB
|
FILE_UPLOAD_MAX_MEMORY_SIZE = 256 * 1024 * 1024 # 256MB
|
||||||
DATA_UPLOAD_MAX_MEMORY_SIZE = 256 * 1024 * 1024 # 256MB
|
DATA_UPLOAD_MAX_MEMORY_SIZE = 256 * 1024 * 1024 # 256MB
|
||||||
|
|
||||||
# Caching Configuration
|
|
||||||
CACHES = {
|
|
||||||
"default": {
|
|
||||||
"BACKEND": "django_redis.cache.RedisCache",
|
|
||||||
"LOCATION": "redis://127.0.0.1:6379/11",
|
|
||||||
"TIMEOUT": 300, # 5 minutes default
|
|
||||||
"OPTIONS": {
|
|
||||||
"CLIENT_CLASS": "django_redis.client.DefaultClient",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Allowed file types for submissions
|
# Allowed file types for submissions
|
||||||
ALLOWED_SUBMISSION_TYPES = [
|
ALLOWED_SUBMISSION_TYPES = [
|
||||||
"image/jpeg",
|
"image/jpeg",
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import requests
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.contrib.auth.backends import BaseBackend
|
from django.contrib.auth.backends import BaseBackend
|
||||||
|
from furl import furl
|
||||||
|
|
||||||
|
|
||||||
class SimpleCASBackend(BaseBackend):
|
class SimpleCASBackend(BaseBackend):
|
||||||
|
|||||||
@ -11,7 +11,6 @@ const userInfo = ref({
|
|||||||
rank: null as number | null,
|
rank: null as number | null,
|
||||||
score: 0,
|
score: 0,
|
||||||
runsSubmitted: 0,
|
runsSubmitted: 0,
|
||||||
isStaff: false,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const uploadedFiles = ref<File[]>([]);
|
const uploadedFiles = ref<File[]>([]);
|
||||||
@ -149,30 +148,6 @@ const fetchLeaderboard = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const clearCache = async () => {
|
|
||||||
try {
|
|
||||||
const response = await fetch("/api/cache/clear", {
|
|
||||||
method: "POST",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
alert("Cache cleared successfully!");
|
|
||||||
// Refresh data after clearing cache
|
|
||||||
await Promise.all([
|
|
||||||
fetchObjectives(),
|
|
||||||
fetchUserResults(),
|
|
||||||
fetchLeaderboard(),
|
|
||||||
]);
|
|
||||||
} else {
|
|
||||||
const error = await response.json();
|
|
||||||
alert(`Error clearing cache: ${error.detail || "Unknown error"}`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error clearing cache:", error);
|
|
||||||
alert("Error clearing cache. Please try again.");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadUserData = async () => {
|
const loadUserData = async () => {
|
||||||
// Get user info first
|
// Get user info first
|
||||||
try {
|
try {
|
||||||
@ -181,7 +156,6 @@ const loadUserData = async () => {
|
|||||||
const user = await response.json();
|
const user = await response.json();
|
||||||
if (user.is_authenticated) {
|
if (user.is_authenticated) {
|
||||||
userInfo.value.username = user.username;
|
userInfo.value.username = user.username;
|
||||||
userInfo.value.isStaff = user.is_staff || false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -263,11 +237,6 @@ onMounted(() => {
|
|||||||
<i class="mdi mdi-trophy mr-1"></i>
|
<i class="mdi mdi-trophy mr-1"></i>
|
||||||
View Full Leaderboard
|
View Full Leaderboard
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button v-if="userInfo.isStaff" @click="clearCache" class="btn btn-error btn-sm w-full mt-3">
|
|
||||||
<i class="mdi mdi-cache-clear mr-1"></i>
|
|
||||||
Clear Cache
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -287,13 +256,13 @@ onMounted(() => {
|
|||||||
isDragover ? 'border-primary bg-primary/10' : 'border-base-300 hover:border-primary'
|
isDragover ? 'border-primary bg-primary/10' : 'border-base-300 hover:border-primary'
|
||||||
]">
|
]">
|
||||||
<input type="file" multiple @change="handleFileUpload" class="hidden" id="file-upload"
|
<input type="file" multiple @change="handleFileUpload" class="hidden" id="file-upload"
|
||||||
accept="text/plain,text/x-log" />
|
accept="video/*,image/*" />
|
||||||
<label for="file-upload" class="cursor-pointer flex flex-col items-center gap-3">
|
<label for="file-upload" class="cursor-pointer flex flex-col items-center gap-3">
|
||||||
<i
|
<i
|
||||||
:class="['mdi text-4xl', isDragover ? 'mdi-cloud-check text-primary' : 'mdi-file-upload text-base-content/50']"></i>
|
:class="['mdi text-4xl', isDragover ? 'mdi-cloud-check text-primary' : 'mdi-file-upload text-base-content/50']"></i>
|
||||||
<div>
|
<div>
|
||||||
<p class="font-semibold">Click to upload or drag and drop</p>
|
<p class="font-semibold">Click to upload or drag and drop</p>
|
||||||
<p class="text-sm text-base-content/70">The log file <code>polylan_mod_log.txt</code></p>
|
<p class="text-sm text-base-content/70">Video or image files (MP4, PNG, etc.)</p>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@ -384,7 +353,10 @@ onMounted(() => {
|
|||||||
<i class="mdi mdi-trophy text-yellow-500 mr-2"></i>
|
<i class="mdi mdi-trophy text-yellow-500 mr-2"></i>
|
||||||
Global Leaderboard
|
Global Leaderboard
|
||||||
</h3>
|
</h3>
|
||||||
<button @click="isLeaderboardModalOpen = false" class="btn btn-sm btn-circle btn-ghost">
|
<button
|
||||||
|
@click="isLeaderboardModalOpen = false"
|
||||||
|
class="btn btn-sm btn-circle btn-ghost"
|
||||||
|
>
|
||||||
<i class="mdi mdi-close"></i>
|
<i class="mdi mdi-close"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -401,10 +373,16 @@ onMounted(() => {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="entry in leaderboard" :key="entry.username"
|
<tr
|
||||||
:class="{ 'bg-primary/20': entry.username === userInfo.username }">
|
v-for="entry in leaderboard"
|
||||||
|
:key="entry.username"
|
||||||
|
:class="{ 'bg-primary/20': entry.username === userInfo.username }"
|
||||||
|
>
|
||||||
<td class="font-bold">
|
<td class="font-bold">
|
||||||
<span v-if="entry.rank === 1" class="badge badge-warning badge-lg">
|
<span
|
||||||
|
v-if="entry.rank === 1"
|
||||||
|
class="badge badge-warning badge-lg"
|
||||||
|
>
|
||||||
🏆 #{{ entry.rank }}
|
🏆 #{{ entry.rank }}
|
||||||
</span>
|
</span>
|
||||||
<span v-else-if="entry.rank === 2" class="badge badge-lg">
|
<span v-else-if="entry.rank === 2" class="badge badge-lg">
|
||||||
@ -417,7 +395,10 @@ onMounted(() => {
|
|||||||
</td>
|
</td>
|
||||||
<td class="font-medium">
|
<td class="font-medium">
|
||||||
{{ entry.username }}
|
{{ entry.username }}
|
||||||
<span v-if="entry.username === userInfo.username" class="badge badge-primary badge-sm ml-2">
|
<span
|
||||||
|
v-if="entry.username === userInfo.username"
|
||||||
|
class="badge badge-primary badge-sm ml-2"
|
||||||
|
>
|
||||||
You
|
You
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@ -45,13 +45,6 @@ const isLoading = ref(true);
|
|||||||
const resultsData = ref<ResultsData | null>(null);
|
const resultsData = ref<ResultsData | null>(null);
|
||||||
const selectedTab = ref<"overall" | "byPuzzle">("overall");
|
const selectedTab = ref<"overall" | "byPuzzle">("overall");
|
||||||
const expandedPuzzleId = ref<number | null>(null);
|
const expandedPuzzleId = ref<number | null>(null);
|
||||||
const userInfo = ref({
|
|
||||||
username: "Player",
|
|
||||||
rank: null as number | null,
|
|
||||||
totalPoints: 0,
|
|
||||||
puzzlesSolved: 0,
|
|
||||||
isStaff: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const fetchResults = async () => {
|
const fetchResults = async () => {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
@ -107,120 +100,23 @@ const togglePuzzleExpanded = (puzzleId: number) => {
|
|||||||
expandedPuzzleId.value = expandedPuzzleId.value === puzzleId ? null : puzzleId;
|
expandedPuzzleId.value = expandedPuzzleId.value === puzzleId ? null : puzzleId;
|
||||||
};
|
};
|
||||||
|
|
||||||
const clearCache = async () => {
|
|
||||||
try {
|
|
||||||
const response = await fetch("/api/cache/clear", {
|
|
||||||
method: "POST",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
alert("Cache cleared successfully!");
|
|
||||||
await fetchResults();
|
|
||||||
} else {
|
|
||||||
const error = await response.json();
|
|
||||||
alert(`Error clearing cache: ${error.detail || "Unknown error"}`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error clearing cache:", error);
|
|
||||||
alert("Error clearing cache. Please try again.");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadUserData = async () => {
|
|
||||||
try {
|
|
||||||
const response = await fetch("/api/user");
|
|
||||||
if (response.ok) {
|
|
||||||
const user = await response.json();
|
|
||||||
if (user.is_authenticated) {
|
|
||||||
userInfo.value.username = user.username;
|
|
||||||
userInfo.value.isStaff = user.is_staff || false;
|
|
||||||
|
|
||||||
await fetchResults();
|
|
||||||
|
|
||||||
// Calculate user's rank and stats
|
|
||||||
const ranking = getOverallRanking();
|
|
||||||
const userRankIndex = ranking.findIndex((u) => u.username === user.username);
|
|
||||||
|
|
||||||
if (userRankIndex !== -1) {
|
|
||||||
userInfo.value.rank = userRankIndex + 1;
|
|
||||||
userInfo.value.totalPoints = ranking[userRankIndex].totalPoints;
|
|
||||||
userInfo.value.puzzlesSolved = ranking[userRankIndex].puzzlesSolved;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error loading user data:", error);
|
|
||||||
await fetchResults();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadUserData();
|
fetchResults();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
<div class="card bg-base-100 shadow-lg">
|
||||||
<!-- Left Column: Your Ranking -->
|
<div class="card-body">
|
||||||
<div class="lg:col-span-1">
|
<h2 class="card-title text-2xl mb-6">
|
||||||
<div class="card bg-base-100 shadow-lg sticky top-8">
|
<i class="mdi mdi-trophy text-yellow-500 mr-2"></i>
|
||||||
<div class="bg-gradient-to-br from-blue-600 to-blue-400 p-6 text-white rounded-t-2xl">
|
General Results
|
||||||
<i class="mdi mdi-trophy text-4xl"></i>
|
</h2>
|
||||||
<h3 class="text-2xl font-bold mt-2">Your Ranking</h3>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="text-center mb-6">
|
|
||||||
<p class="text-sm text-base-content/70">Player</p>
|
|
||||||
<p class="text-3xl font-bold">{{ userInfo.username }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="divider"></div>
|
<div v-if="isLoading" class="flex justify-center py-8">
|
||||||
|
<span class="loading loading-spinner loading-lg"></span>
|
||||||
<div v-if="isLoading" class="flex justify-center py-8">
|
|
||||||
<span class="loading loading-spinner loading-lg"></span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else class="space-y-4">
|
|
||||||
<div class="text-center">
|
|
||||||
<p class="text-sm text-base-content/70 mb-1">Current Rank</p>
|
|
||||||
<p v-if="userInfo.rank !== null" class="text-4xl font-bold text-primary">
|
|
||||||
#{{ userInfo.rank }}
|
|
||||||
</p>
|
|
||||||
<p v-else class="text-2xl text-base-content/50">No rank yet</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="text-center">
|
|
||||||
<p class="text-sm text-base-content/70 mb-1">Total Points</p>
|
|
||||||
<p class="text-2xl font-bold">{{ userInfo.totalPoints.toLocaleString() }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="text-center">
|
|
||||||
<p class="text-sm text-base-content/70 mb-1">Puzzles Solved</p>
|
|
||||||
<p class="text-2xl font-bold">{{ userInfo.puzzlesSolved }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button v-if="userInfo.isStaff" @click="clearCache" class="btn btn-error btn-sm w-full mt-6">
|
|
||||||
<i class="mdi mdi-cache-clear mr-1"></i>
|
|
||||||
Clear Cache
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Right Column: Results -->
|
|
||||||
<div class="lg:col-span-2">
|
|
||||||
<div class="card bg-base-100 shadow-lg">
|
|
||||||
<div class="card-body">
|
|
||||||
<h2 class="card-title text-2xl mb-6">
|
|
||||||
<i class="mdi mdi-trophy text-yellow-500 mr-2"></i>
|
|
||||||
General Results
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<div v-if="isLoading" class="flex justify-center py-8">
|
|
||||||
<span class="loading loading-spinner loading-lg"></span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else-if="!resultsData" class="text-center py-8">
|
<div v-else-if="!resultsData" class="text-center py-8">
|
||||||
<p class="text-base-content/70">No results available yet</p>
|
<p class="text-base-content/70">No results available yet</p>
|
||||||
@ -390,8 +286,6 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -3,7 +3,6 @@ from ninja.files import UploadedFile
|
|||||||
from ninja.pagination import paginate
|
from ninja.pagination import paginate
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.core.files.base import ContentFile
|
from django.core.files.base import ContentFile
|
||||||
from django.core.cache import cache
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from typing import List
|
from typing import List
|
||||||
@ -166,9 +165,6 @@ def create_submission(
|
|||||||
"responses__files", "responses__puzzle"
|
"responses__files", "responses__puzzle"
|
||||||
).get(id=submission.id)
|
).get(id=submission.id)
|
||||||
|
|
||||||
# Invalidate results cache on successful submission
|
|
||||||
cache.delete("api:results:results")
|
|
||||||
|
|
||||||
return submission
|
return submission
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -205,9 +201,6 @@ def validate_response(request, response_id: int, data: ValidationIn):
|
|||||||
|
|
||||||
response.save()
|
response.save()
|
||||||
|
|
||||||
# Invalidate results cache when a response is validated
|
|
||||||
cache.delete("api:results:results")
|
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
@ -263,9 +256,6 @@ def validate_submission(request, submission_id: str):
|
|||||||
"responses__files", "responses__puzzle"
|
"responses__files", "responses__puzzle"
|
||||||
).get(id=submission.id)
|
).get(id=submission.id)
|
||||||
|
|
||||||
# Invalidate results cache when submission is validated
|
|
||||||
cache.delete("api:results:results")
|
|
||||||
|
|
||||||
return submission
|
return submission
|
||||||
|
|
||||||
|
|
||||||
@ -278,10 +268,6 @@ def delete_submission(request, submission_id: str):
|
|||||||
|
|
||||||
submission = get_object_or_404(Submission, id=submission_id)
|
submission = get_object_or_404(Submission, id=submission_id)
|
||||||
submission.delete()
|
submission.delete()
|
||||||
|
|
||||||
# Invalidate results cache when submission is deleted
|
|
||||||
cache.delete("api:results:results")
|
|
||||||
|
|
||||||
return {"detail": "Submission deleted successfully"}
|
return {"detail": "Submission deleted successfully"}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -16,7 +16,6 @@ dependencies = [
|
|||||||
"psycopg>=3.2.13",
|
"psycopg>=3.2.13",
|
||||||
"sentry-sdk[django]>=2.59.0",
|
"sentry-sdk[django]>=2.59.0",
|
||||||
"furl>=2.1.4",
|
"furl>=2.1.4",
|
||||||
"django-redis>=6.0.0",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
|
|||||||
36
uv.lock
36
uv.lock
@ -37,15 +37,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl", hash = "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2", size = 26918, upload-time = "2024-11-30T04:30:10.946Z" },
|
{ url = "https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl", hash = "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2", size = 26918, upload-time = "2024-11-30T04:30:10.946Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "async-timeout"
|
|
||||||
version = "5.0.1"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274, upload-time = "2024-11-06T16:41:39.6Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233, upload-time = "2024-11-06T16:41:37.9Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "certifi"
|
name = "certifi"
|
||||||
version = "2025.10.5"
|
version = "2025.10.5"
|
||||||
@ -206,19 +197,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/64/96/d967ca440d6a8e3861120f51985d8e5aec79b9a8bdda16041206adfe7adc/django_extensions-4.1-py3-none-any.whl", hash = "sha256:0699a7af28f2523bf8db309a80278519362cd4b6e1fd0a8cd4bf063e1e023336", size = 232980, upload-time = "2025-04-11T01:15:37.701Z" },
|
{ url = "https://files.pythonhosted.org/packages/64/96/d967ca440d6a8e3861120f51985d8e5aec79b9a8bdda16041206adfe7adc/django_extensions-4.1-py3-none-any.whl", hash = "sha256:0699a7af28f2523bf8db309a80278519362cd4b6e1fd0a8cd4bf063e1e023336", size = 232980, upload-time = "2025-04-11T01:15:37.701Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "django-redis"
|
|
||||||
version = "6.0.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "django" },
|
|
||||||
{ name = "redis" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/08/53/dbcfa1e528e0d6c39947092625b2c89274b5d88f14d357cee53c4d6dbbd4/django_redis-6.0.0.tar.gz", hash = "sha256:2d9cb12a20424a4c4dde082c6122f486628bae2d9c2bee4c0126a4de7fda00dd", size = 56904, upload-time = "2025-06-17T18:15:46.376Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7e/79/055dfcc508cfe9f439d9f453741188d633efa9eab90fc78a67b0ab50b137/django_redis-6.0.0-py3-none-any.whl", hash = "sha256:20bf0063a8abee567eb5f77f375143c32810c8700c0674ced34737f8de4e36c0", size = 33687, upload-time = "2025-06-17T18:15:34.165Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "django-shinobi"
|
name = "django-shinobi"
|
||||||
version = "1.4.0"
|
version = "1.4.0"
|
||||||
@ -749,7 +727,6 @@ version = "0.1.0"
|
|||||||
source = { virtual = "." }
|
source = { virtual = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "django" },
|
{ name = "django" },
|
||||||
{ name = "django-redis" },
|
|
||||||
{ name = "django-shinobi" },
|
{ name = "django-shinobi" },
|
||||||
{ name = "django-vite" },
|
{ name = "django-vite" },
|
||||||
{ name = "furl" },
|
{ name = "furl" },
|
||||||
@ -780,7 +757,6 @@ dev = [
|
|||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "django", specifier = ">=5.2.7" },
|
{ name = "django", specifier = ">=5.2.7" },
|
||||||
{ name = "django-redis", specifier = ">=6.0.0" },
|
|
||||||
{ name = "django-shinobi", specifier = ">=1.4.0" },
|
{ name = "django-shinobi", specifier = ">=1.4.0" },
|
||||||
{ name = "django-vite", specifier = ">=3.1.0" },
|
{ name = "django-vite", specifier = ">=3.1.0" },
|
||||||
{ name = "furl", specifier = ">=2.1.4" },
|
{ name = "furl", specifier = ">=2.1.4" },
|
||||||
@ -1097,18 +1073,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
|
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "redis"
|
|
||||||
version = "7.4.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "async-timeout", marker = "python_full_version < '3.11.3'" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/7b/7f/3759b1d0d72b7c92f0d70ffd9dc962b7b7b5ee74e135f9d7d8ab06b8a318/redis-7.4.0.tar.gz", hash = "sha256:64a6ea7bf567ad43c964d2c30d82853f8df927c5c9017766c55a1d1ed95d18ad", size = 4943913, upload-time = "2026-03-24T09:14:37.53Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/74/3a/95deec7db1eb53979973ebd156f3369a72732208d1391cd2e5d127062a32/redis-7.4.0-py3-none-any.whl", hash = "sha256:a9c74a5c893a5ef8455a5adb793a31bb70feb821c86eccb62eebef5a19c429ec", size = 409772, upload-time = "2026-03-24T09:14:35.968Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "requests"
|
name = "requests"
|
||||||
version = "2.32.5"
|
version = "2.32.5"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user