feat(submissions): show all submissions for admin
This commit is contained in:
@@ -8,6 +8,7 @@ import { useUser } from "@/composables/useUser";
|
|||||||
import { eventGamesQuery } from "@/queries";
|
import { eventGamesQuery } from "@/queries";
|
||||||
import { usePageHeaderStore } from "@/stores/pageHeader";
|
import { usePageHeaderStore } from "@/stores/pageHeader";
|
||||||
import { submissionsApiListAvailableScopes, submissionsApiCreateSubmission, submissionsApiListUserSubmissions, submissionsApiCreateImageCrop, submissionsApiDeleteImageCrop, submissionsApiProcessFileOcr, submissionsApiProcessCropOcr } from "@/api";
|
import { submissionsApiListAvailableScopes, submissionsApiCreateSubmission, submissionsApiListUserSubmissions, submissionsApiCreateImageCrop, submissionsApiDeleteImageCrop, submissionsApiProcessFileOcr, submissionsApiProcessCropOcr } from "@/api";
|
||||||
|
import type { SubmissionOut } from "@/api";
|
||||||
import { useSubmissionsWebSocket } from "@/composables/useSubmissionsWebSocket";
|
import { useSubmissionsWebSocket } from "@/composables/useSubmissionsWebSocket";
|
||||||
import { provideEventPath } from "@/composables/useEventPath";
|
import { provideEventPath } from "@/composables/useEventPath";
|
||||||
|
|
||||||
@@ -42,7 +43,7 @@ const notes = ref("");
|
|||||||
const selectedFile = ref<File | null>(null);
|
const selectedFile = ref<File | null>(null);
|
||||||
const filePreviewUrl = ref<string>("");
|
const filePreviewUrl = ref<string>("");
|
||||||
const isModalOpen = ref(false);
|
const isModalOpen = ref(false);
|
||||||
const userSubmissions = ref<any[]>([]);
|
const userSubmissions = ref<SubmissionOut[]>([]);
|
||||||
const isLoadingSubmissions = ref(false);
|
const isLoadingSubmissions = ref(false);
|
||||||
const selectedSubmissionFile = ref<any>(null);
|
const selectedSubmissionFile = ref<any>(null);
|
||||||
const isSubmissionFileModalOpen = ref(false);
|
const isSubmissionFileModalOpen = ref(false);
|
||||||
@@ -728,7 +729,7 @@ onUnmounted(() => {
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h2 class="card-title text-2xl mb-6">
|
<h2 class="card-title text-2xl mb-6">
|
||||||
<i class="mdi mdi-history text-primary mr-2"></i>
|
<i class="mdi mdi-history text-primary mr-2"></i>
|
||||||
{{ t("submissions.yourSubmissions") }}
|
{{ userInfo.isStaff ? t("submissions.allSubmissions") : t("submissions.yourSubmissions") }}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div v-if="isLoadingSubmissions" class="flex justify-center py-8">
|
<div v-if="isLoadingSubmissions" class="flex justify-center py-8">
|
||||||
@@ -745,6 +746,7 @@ onUnmounted(() => {
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>{{ t("common.date") }}</th>
|
<th>{{ t("common.date") }}</th>
|
||||||
|
<th v-if="userInfo.isStaff">{{ t("common.user") }}</th>
|
||||||
<th>{{ t("common.files") }}</th>
|
<th>{{ t("common.files") }}</th>
|
||||||
<th>{{ t("submissions.fileStatus") }}</th>
|
<th>{{ t("submissions.fileStatus") }}</th>
|
||||||
<th>{{ t("submissions.ocrStatus") }}</th>
|
<th>{{ t("submissions.ocrStatus") }}</th>
|
||||||
@@ -754,6 +756,7 @@ onUnmounted(() => {
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="submission in userSubmissions" :key="submission.id" class="hover">
|
<tr v-for="submission in userSubmissions" :key="submission.id" class="hover">
|
||||||
<td class="text-sm">{{ formatDate(submission.created_at) }}</td>
|
<td class="text-sm">{{ formatDate(submission.created_at) }}</td>
|
||||||
|
<td v-if="userInfo.isStaff" class="text-sm">{{ submission.username ?? "-" }}</td>
|
||||||
<td class="text-sm">
|
<td class="text-sm">
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
<div v-for="file in submission.files" :key="file.id" class="flex items-center gap-2">
|
<div v-for="file in submission.files" :key="file.id" class="flex items-center gap-2">
|
||||||
|
|||||||
@@ -423,6 +423,10 @@ export type SubmissionOut = {
|
|||||||
* Scope Id
|
* Scope Id
|
||||||
*/
|
*/
|
||||||
scope_id: string;
|
scope_id: string;
|
||||||
|
/**
|
||||||
|
* Username
|
||||||
|
*/
|
||||||
|
username?: string | null;
|
||||||
/**
|
/**
|
||||||
* Notes
|
* Notes
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
"puzzle": "Puzzle",
|
"puzzle": "Puzzle",
|
||||||
"files": "Files",
|
"files": "Files",
|
||||||
"date": "Date",
|
"date": "Date",
|
||||||
|
"user": "User",
|
||||||
"notes": "Notes",
|
"notes": "Notes",
|
||||||
"actions": "Actions",
|
"actions": "Actions",
|
||||||
"noRankYet": "No rank yet",
|
"noRankYet": "No rank yet",
|
||||||
@@ -168,6 +169,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"yourSubmissions": "Your Submissions",
|
"yourSubmissions": "Your Submissions",
|
||||||
|
"allSubmissions": "All Submissions",
|
||||||
"noSubmissionsHint": "Upload your first submission above to get started",
|
"noSubmissionsHint": "Upload your first submission above to get started",
|
||||||
"fileStatus": "File Status",
|
"fileStatus": "File Status",
|
||||||
"ocrStatus": "OCR Status",
|
"ocrStatus": "OCR Status",
|
||||||
|
|||||||
@@ -202,10 +202,12 @@ def list_user_submissions(request: HttpRequest, event_path: str, scope_id: UUID)
|
|||||||
if not request.user.is_authenticated:
|
if not request.user.is_authenticated:
|
||||||
return 403, {"detail": "Authentication required"}
|
return 403, {"detail": "Authentication required"}
|
||||||
|
|
||||||
submissions = Submission.for_event.filter(
|
submissions = Submission.for_event.filter(scope_id=scope_id)
|
||||||
scope_id=scope_id, user=request.user
|
# Admins see every player's submissions for the scope
|
||||||
).order_by("-created_at")
|
if not request.user.is_staff:
|
||||||
return submissions
|
submissions = submissions.filter(user=request.user)
|
||||||
|
|
||||||
|
return submissions.select_related("user").order_by("-created_at")
|
||||||
|
|
||||||
|
|
||||||
@router.get(f"{PREFIX}/submission/by/game", response=list[SubmissionOut])
|
@router.get(f"{PREFIX}/submission/by/game", response=list[SubmissionOut])
|
||||||
|
|||||||
@@ -71,7 +71,12 @@ class SubmissionFileOut(Schema):
|
|||||||
class SubmissionOut(Schema):
|
class SubmissionOut(Schema):
|
||||||
id: UUID
|
id: UUID
|
||||||
scope_id: UUID
|
scope_id: UUID
|
||||||
|
username: str | None = None
|
||||||
notes: str | None = None
|
notes: str | None = None
|
||||||
files: list[SubmissionFileOut] = []
|
files: list[SubmissionFileOut] = []
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
updated_at: datetime
|
updated_at: datetime
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def resolve_username(obj) -> str | None:
|
||||||
|
return obj.user.username if obj.user else None
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from events.models import Event
|
|||||||
from games.models import EventGame, Game
|
from games.models import EventGame, Game
|
||||||
|
|
||||||
from . import tasks
|
from . import tasks
|
||||||
from .models import Scope
|
from .models import Scope, Submission
|
||||||
from .services.aandt_client.models import (
|
from .services.aandt_client.models import (
|
||||||
AandtApiInfosResponse200,
|
AandtApiInfosResponse200,
|
||||||
AandtApiMatchesResponse200,
|
AandtApiMatchesResponse200,
|
||||||
@@ -388,3 +388,49 @@ class SubmissionHomeViewTests(TestCase):
|
|||||||
response = self.client.get("/nope/submissions/unknown-game")
|
response = self.client.get("/nope/submissions/unknown-game")
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 404)
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|
||||||
|
|
||||||
|
class ListUserSubmissionsApiTests(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.event = Event.objects.create(name="Event", path="event")
|
||||||
|
game = Game.objects.create(name="Game", path="game")
|
||||||
|
event_game = EventGame.objects.create(event=self.event, game=game)
|
||||||
|
self.scope = Scope.objects.create(event_game=event_game, name="M1")
|
||||||
|
other_scope = Scope.objects.create(event_game=event_game, name="M2")
|
||||||
|
|
||||||
|
self.player = CustomUser.objects.create_user(username="player", password="x")
|
||||||
|
other = CustomUser.objects.create_user(username="other", password="x")
|
||||||
|
self.mine = Submission.objects.create(
|
||||||
|
event=self.event, scope=self.scope, user=self.player
|
||||||
|
)
|
||||||
|
self.theirs = Submission.objects.create(
|
||||||
|
event=self.event, scope=self.scope, user=other
|
||||||
|
)
|
||||||
|
Submission.objects.create(event=self.event, scope=other_scope, user=other)
|
||||||
|
|
||||||
|
self.url = f"/api/events/{self.event.path}/submissions/submission/by/user"
|
||||||
|
|
||||||
|
def _list(self):
|
||||||
|
response = self.client.get(self.url, {"scope_id": self.scope.id})
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
def test_player_sees_only_own_submissions(self):
|
||||||
|
self.client.force_login(self.player)
|
||||||
|
|
||||||
|
data = self._list()
|
||||||
|
|
||||||
|
self.assertEqual([s["id"] for s in data], [str(self.mine.id)])
|
||||||
|
self.assertEqual(data[0]["username"], "player")
|
||||||
|
|
||||||
|
def test_staff_sees_all_submissions_of_scope(self):
|
||||||
|
staff = CustomUser.objects.create_user(
|
||||||
|
username="staff", password="x", is_staff=True
|
||||||
|
)
|
||||||
|
self.client.force_login(staff)
|
||||||
|
|
||||||
|
data = self._list()
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
{s["id"] for s in data}, {str(self.mine.id), str(self.theirs.id)}
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user