fix(submissions): broadcast crop delete + fix for crop admin
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, onUnmounted, computed } from "vue";
|
import { ref, onMounted, onUnmounted, computed, watch } from "vue";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import { useTitle } from "@vueuse/core";
|
import { useTitle } from "@vueuse/core";
|
||||||
@@ -47,7 +47,8 @@ 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);
|
||||||
const copyFeedback = ref<string>("");
|
// Feedback shown on the copy button that was clicked, keyed by "file" or crop id
|
||||||
|
const copyFeedback = ref<{ key: string; message: string } | null>(null);
|
||||||
const isDrawingCrop = ref(false);
|
const isDrawingCrop = ref(false);
|
||||||
const cropStartX = ref(0);
|
const cropStartX = ref(0);
|
||||||
const cropStartY = ref(0);
|
const cropStartY = ref(0);
|
||||||
@@ -141,23 +142,23 @@ const closeSubmissionFileModal = () => {
|
|||||||
selectedSubmissionFile.value = null;
|
selectedSubmissionFile.value = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const copyOCRText = async () => {
|
const copyOCRText = async (text: string | undefined, key: string) => {
|
||||||
if (selectedSubmissionFile.value?.ocr_raw_data) {
|
if (!text) return;
|
||||||
try {
|
|
||||||
await navigator.clipboard.writeText(selectedSubmissionFile.value.ocr_raw_data);
|
try {
|
||||||
copyFeedback.value = t("common.copied");
|
await navigator.clipboard.writeText(text);
|
||||||
setTimeout(() => {
|
copyFeedback.value = { key, message: t("common.copied") };
|
||||||
copyFeedback.value = "";
|
} catch (err) {
|
||||||
}, 2000);
|
copyFeedback.value = { key, message: t("submissions.copyFailed") };
|
||||||
} catch (err) {
|
|
||||||
copyFeedback.value = t("submissions.copyFailed");
|
|
||||||
setTimeout(() => {
|
|
||||||
copyFeedback.value = "";
|
|
||||||
}, 2000);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
if (copyFeedback.value?.key === key) copyFeedback.value = null;
|
||||||
|
}, 2000);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const copyLabel = (key: string) =>
|
||||||
|
copyFeedback.value?.key === key ? copyFeedback.value.message : t("common.copy");
|
||||||
|
|
||||||
const getCanvasCoordinates = (canvas: HTMLCanvasElement, clientX: number, clientY: number) => {
|
const getCanvasCoordinates = (canvas: HTMLCanvasElement, clientX: number, clientY: number) => {
|
||||||
const rect = canvas.getBoundingClientRect();
|
const rect = canvas.getBoundingClientRect();
|
||||||
const scaleX = canvas.width / rect.width;
|
const scaleX = canvas.width / rect.width;
|
||||||
@@ -246,6 +247,12 @@ const redrawCropCanvas = () => {
|
|||||||
img.src = selectedSubmissionFile.value?.file_url;
|
img.src = selectedSubmissionFile.value?.file_url;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Crops can also arrive over the websocket (e.g. drawn by another admin)
|
||||||
|
watch(
|
||||||
|
() => selectedSubmissionFile.value?.crops?.length,
|
||||||
|
() => redrawCropCanvas(),
|
||||||
|
);
|
||||||
|
|
||||||
const submitCrop = async () => {
|
const submitCrop = async () => {
|
||||||
if (!currentCrop.value || !selectedSubmissionFile.value) return;
|
if (!currentCrop.value || !selectedSubmissionFile.value) return;
|
||||||
|
|
||||||
@@ -262,10 +269,12 @@ const submitCrop = async () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add the new crop to the list immediately
|
// Add the new crop to the list immediately, unless the websocket beat us to it
|
||||||
if (newCrop) {
|
if (newCrop) {
|
||||||
selectedSubmissionFile.value.crops = selectedSubmissionFile.value.crops || [];
|
selectedSubmissionFile.value.crops = selectedSubmissionFile.value.crops || [];
|
||||||
selectedSubmissionFile.value.crops.push(newCrop);
|
if (!selectedSubmissionFile.value.crops.some((c: any) => c.id === newCrop.id)) {
|
||||||
|
selectedSubmissionFile.value.crops.push(newCrop);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
currentCrop.value = null;
|
currentCrop.value = null;
|
||||||
@@ -480,6 +489,7 @@ const loadUserSubmissions = async () => {
|
|||||||
// the next match when there's only one, so players follow along without a refresh.
|
// the next match when there's only one, so players follow along without a refresh.
|
||||||
useSubmissionsWebSocket({
|
useSubmissionsWebSocket({
|
||||||
userSubmissions,
|
userSubmissions,
|
||||||
|
scopeId: props.scopeId,
|
||||||
onScopeUpdate: (scope) => {
|
onScopeUpdate: (scope) => {
|
||||||
if (scope.id !== props.scopeId || (scope.is_active && scope.assigned)) return;
|
if (scope.id !== props.scopeId || (scope.is_active && scope.assigned)) return;
|
||||||
|
|
||||||
@@ -909,9 +919,10 @@ onUnmounted(() => {
|
|||||||
<div>
|
<div>
|
||||||
<div class="flex items-center justify-between mb-2">
|
<div class="flex items-center justify-between mb-2">
|
||||||
<p class="text-xs font-semibold text-base-content/70">{{ t("submissions.extractedText") }}</p>
|
<p class="text-xs font-semibold text-base-content/70">{{ t("submissions.extractedText") }}</p>
|
||||||
<button @click="copyOCRText" class="btn btn-xs btn-ghost gap-1">
|
<button @click="copyOCRText(selectedSubmissionFile.ocr_raw_data, 'file')"
|
||||||
|
class="btn btn-xs btn-ghost gap-1">
|
||||||
<i class="mdi mdi-content-copy"></i>
|
<i class="mdi mdi-content-copy"></i>
|
||||||
{{ copyFeedback || t("common.copy") }}
|
{{ copyLabel("file") }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -948,7 +959,14 @@ onUnmounted(() => {
|
|||||||
<div v-for="crop in selectedSubmissionFile.crops" :key="crop.id" class="space-y-2">
|
<div v-for="crop in selectedSubmissionFile.crops" :key="crop.id" class="space-y-2">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<p class="text-xs font-medium">{{ crop.label || t("submissions.cropLabelFallback", { x: crop.x1, y: crop.y1 }) }}</p>
|
<p class="text-xs font-medium">{{ crop.label || t("submissions.cropLabelFallback", { x: crop.x1, y: crop.y1 }) }}</p>
|
||||||
<span :class="getOCRStatusColor(crop.ocr_status)">{{ crop.ocr_status }}</span>
|
<div class="flex items-center gap-1">
|
||||||
|
<button v-if="crop.ocr_status === 'processed' && crop.ocr_raw_data"
|
||||||
|
@click="copyOCRText(crop.ocr_raw_data, crop.id)" class="btn btn-xs btn-ghost gap-1">
|
||||||
|
<i class="mdi mdi-content-copy"></i>
|
||||||
|
{{ copyLabel(crop.id) }}
|
||||||
|
</button>
|
||||||
|
<span :class="getOCRStatusColor(crop.ocr_status)">{{ crop.ocr_status }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="crop.ocr_status === 'processed' && crop.ocr_raw_data"
|
<div v-if="crop.ocr_status === 'processed' && crop.ocr_raw_data"
|
||||||
class="bg-base-100 p-2 rounded text-xs whitespace-pre-wrap break-words font-mono max-h-32 overflow-y-auto">
|
class="bg-base-100 p-2 rounded text-xs whitespace-pre-wrap break-words font-mono max-h-32 overflow-y-auto">
|
||||||
|
|||||||
@@ -1080,6 +1080,10 @@ export type ImageCropOut = {
|
|||||||
* Id
|
* Id
|
||||||
*/
|
*/
|
||||||
id: string;
|
id: string;
|
||||||
|
/**
|
||||||
|
* Submission File Id
|
||||||
|
*/
|
||||||
|
submission_file_id: string;
|
||||||
/**
|
/**
|
||||||
* X1
|
* X1
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useLiveBadge } from '@/composables/useLiveBadge'
|
|||||||
interface SubmissionUpdate {
|
interface SubmissionUpdate {
|
||||||
id: string
|
id: string
|
||||||
scope_id?: string
|
scope_id?: string
|
||||||
|
username?: string | null
|
||||||
notes?: string
|
notes?: string
|
||||||
files?: SubmissionFileUpdate[]
|
files?: SubmissionFileUpdate[]
|
||||||
created_at: string
|
created_at: string
|
||||||
@@ -24,6 +25,7 @@ interface SubmissionFileUpdate {
|
|||||||
|
|
||||||
interface ImageCropUpdate {
|
interface ImageCropUpdate {
|
||||||
id: string
|
id: string
|
||||||
|
submission_file_id: string
|
||||||
x1: number
|
x1: number
|
||||||
y1: number
|
y1: number
|
||||||
x2: number
|
x2: number
|
||||||
@@ -42,7 +44,14 @@ export interface ScopeUpdate {
|
|||||||
assigned: boolean
|
assigned: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// See broadcast_image_crop_delete in signals.py
|
||||||
|
interface DeletedCrop {
|
||||||
|
id: string
|
||||||
|
submission_file_id: string
|
||||||
|
}
|
||||||
|
|
||||||
type WebSocketData =
|
type WebSocketData =
|
||||||
|
| { deleted_crop: DeletedCrop }
|
||||||
| SubmissionUpdate
|
| SubmissionUpdate
|
||||||
| SubmissionFileUpdate
|
| SubmissionFileUpdate
|
||||||
| ImageCropUpdate
|
| ImageCropUpdate
|
||||||
@@ -50,10 +59,12 @@ type WebSocketData =
|
|||||||
|
|
||||||
interface Options {
|
interface Options {
|
||||||
userSubmissions?: any
|
userSubmissions?: any
|
||||||
|
// Only submissions of this scope get added to the list (staff get them all)
|
||||||
|
scopeId?: string
|
||||||
onScopeUpdate?: (scope: ScopeUpdate) => void
|
onScopeUpdate?: (scope: ScopeUpdate) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useSubmissionsWebSocket({ userSubmissions, onScopeUpdate }: Options) {
|
export function useSubmissionsWebSocket({ userSubmissions, scopeId, onScopeUpdate }: Options) {
|
||||||
const isConnected = ref(false)
|
const isConnected = ref(false)
|
||||||
|
|
||||||
const wsUrl = computed(() => {
|
const wsUrl = computed(() => {
|
||||||
@@ -86,6 +97,8 @@ export function useSubmissionsWebSocket({ userSubmissions, onScopeUpdate }: Opti
|
|||||||
onScopeUpdate?.(data.scope)
|
onScopeUpdate?.(data.scope)
|
||||||
} else if (!userSubmissions) {
|
} else if (!userSubmissions) {
|
||||||
return
|
return
|
||||||
|
} else if ('deleted_crop' in data) {
|
||||||
|
handleCropDelete(data.deleted_crop)
|
||||||
} else if ('files' in data) {
|
} else if ('files' in data) {
|
||||||
handleSubmissionUpdate(data as SubmissionUpdate)
|
handleSubmissionUpdate(data as SubmissionUpdate)
|
||||||
} else if ('original_filename' in data) {
|
} else if ('original_filename' in data) {
|
||||||
@@ -111,6 +124,8 @@ export function useSubmissionsWebSocket({ userSubmissions, onScopeUpdate }: Opti
|
|||||||
useLiveBadge('submissions', isConnected)
|
useLiveBadge('submissions', isConnected)
|
||||||
|
|
||||||
const handleSubmissionUpdate = (submission: SubmissionUpdate) => {
|
const handleSubmissionUpdate = (submission: SubmissionUpdate) => {
|
||||||
|
if (scopeId && submission.scope_id !== scopeId) return
|
||||||
|
|
||||||
const existingIndex = userSubmissions.value.findIndex((s: any) => s.id === submission.id)
|
const existingIndex = userSubmissions.value.findIndex((s: any) => s.id === submission.id)
|
||||||
|
|
||||||
if (existingIndex >= 0) {
|
if (existingIndex >= 0) {
|
||||||
@@ -137,6 +152,8 @@ export function useSubmissionsWebSocket({ userSubmissions, onScopeUpdate }: Opti
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleCropUpdate = (crop: ImageCropUpdate) => {
|
const handleCropUpdate = (crop: ImageCropUpdate) => {
|
||||||
|
let found = false
|
||||||
|
|
||||||
userSubmissions.value.forEach((submission: any) => {
|
userSubmissions.value.forEach((submission: any) => {
|
||||||
if (!submission.files) return
|
if (!submission.files) return
|
||||||
|
|
||||||
@@ -147,10 +164,37 @@ export function useSubmissionsWebSocket({ userSubmissions, onScopeUpdate }: Opti
|
|||||||
if (cropIndex >= 0) {
|
if (cropIndex >= 0) {
|
||||||
// Merge into existing object to preserve references
|
// Merge into existing object to preserve references
|
||||||
Object.assign(file.crops[cropIndex], crop)
|
Object.assign(file.crops[cropIndex], crop)
|
||||||
|
found = true
|
||||||
console.log('Updated crop:', crop.id)
|
console.log('Updated crop:', crop.id)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (!found) handleNewCrop(crop)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCropDelete = (crop: DeletedCrop) => {
|
||||||
|
userSubmissions.value.forEach((submission: any) => {
|
||||||
|
submission.files?.forEach((file: any) => {
|
||||||
|
if (file.id !== crop.submission_file_id || !file.crops) return
|
||||||
|
|
||||||
|
file.crops = file.crops.filter((c: any) => c.id !== crop.id)
|
||||||
|
console.log('Deleted crop:', crop.id)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// New crop (e.g. drawn by another admin): add it to its file
|
||||||
|
const handleNewCrop = (crop: ImageCropUpdate) => {
|
||||||
|
userSubmissions.value.forEach((submission: any) => {
|
||||||
|
submission.files?.forEach((file: any) => {
|
||||||
|
if (file.id !== crop.submission_file_id) return
|
||||||
|
|
||||||
|
file.crops = file.crops || []
|
||||||
|
file.crops.push(crop)
|
||||||
|
console.log('Added crop:', crop.id)
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import json
|
|||||||
from channels.generic.websocket import AsyncWebsocketConsumer
|
from channels.generic.websocket import AsyncWebsocketConsumer
|
||||||
from django.contrib.auth.models import AnonymousUser
|
from django.contrib.auth.models import AnonymousUser
|
||||||
|
|
||||||
|
from submissions.signals import STAFF_GROUP_NAME
|
||||||
|
|
||||||
|
|
||||||
class SubmissionsConsumer(AsyncWebsocketConsumer):
|
class SubmissionsConsumer(AsyncWebsocketConsumer):
|
||||||
async def connect(self):
|
async def connect(self):
|
||||||
@@ -11,12 +13,18 @@ class SubmissionsConsumer(AsyncWebsocketConsumer):
|
|||||||
await self.close()
|
await self.close()
|
||||||
return
|
return
|
||||||
|
|
||||||
self.user_group_name = f"submissions_user_{self.user.id}"
|
self.group_names = [f"submissions_user_{self.user.id}"]
|
||||||
await self.channel_layer.group_add(self.user_group_name, self.channel_name)
|
if self.user.is_staff:
|
||||||
|
self.group_names.append(STAFF_GROUP_NAME)
|
||||||
|
|
||||||
|
for group_name in self.group_names:
|
||||||
|
await self.channel_layer.group_add(group_name, self.channel_name)
|
||||||
await self.accept()
|
await self.accept()
|
||||||
|
|
||||||
async def disconnect(self, close_code):
|
async def disconnect(self, close_code):
|
||||||
await self.channel_layer.group_discard(self.user_group_name, self.channel_name)
|
# closed before joining (anonymous)
|
||||||
|
for group_name in getattr(self, "group_names", []):
|
||||||
|
await self.channel_layer.group_discard(group_name, self.channel_name)
|
||||||
|
|
||||||
async def submission_updated(self, event):
|
async def submission_updated(self, event):
|
||||||
submission_data = event["submission"]
|
submission_data = event["submission"]
|
||||||
@@ -30,5 +38,8 @@ class SubmissionsConsumer(AsyncWebsocketConsumer):
|
|||||||
crop_data = event["crop"]
|
crop_data = event["crop"]
|
||||||
await self.send(text_data=json.dumps(crop_data))
|
await self.send(text_data=json.dumps(crop_data))
|
||||||
|
|
||||||
|
async def submission_crop_deleted(self, event):
|
||||||
|
await self.send(text_data=json.dumps({"deleted_crop": event["crop"]}))
|
||||||
|
|
||||||
async def scope_updated(self, event):
|
async def scope_updated(self, event):
|
||||||
await self.send(text_data=json.dumps({"scope": event["scope"]}))
|
await self.send(text_data=json.dumps({"scope": event["scope"]}))
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ class ImageCropIn(Schema):
|
|||||||
|
|
||||||
class ImageCropOut(Schema):
|
class ImageCropOut(Schema):
|
||||||
id: UUID
|
id: UUID
|
||||||
|
submission_file_id: UUID
|
||||||
x1: int
|
x1: int
|
||||||
y1: int
|
y1: int
|
||||||
x2: int
|
x2: int
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.db.models.signals import post_save
|
from django.db.models.signals import post_delete, post_save
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from asgiref.sync import async_to_sync
|
from asgiref.sync import async_to_sync
|
||||||
from channels.layers import get_channel_layer
|
from channels.layers import get_channel_layer
|
||||||
@@ -8,55 +8,63 @@ from submissions.models import Submission, SubmissionFile, ImageCrop, Scope
|
|||||||
from submissions.schemas import SubmissionOut, SubmissionFileOut, ImageCropOut
|
from submissions.schemas import SubmissionOut, SubmissionFileOut, ImageCropOut
|
||||||
|
|
||||||
|
|
||||||
|
# Staff see (and OCR/crop) every player's submissions, so they get every update
|
||||||
|
STAFF_GROUP_NAME = "submissions_staff"
|
||||||
|
|
||||||
|
|
||||||
|
def _broadcast_submission_event(submission: Submission, message: dict):
|
||||||
|
channel_layer = get_channel_layer()
|
||||||
|
groups = [STAFF_GROUP_NAME]
|
||||||
|
if submission.user_id:
|
||||||
|
groups.append(f"submissions_user_{submission.user_id}")
|
||||||
|
|
||||||
|
for group_name in groups:
|
||||||
|
async_to_sync(channel_layer.group_send)(group_name, message)
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save, sender=Submission)
|
@receiver(post_save, sender=Submission)
|
||||||
def broadcast_submission_update(sender, instance, created, **kwargs):
|
def broadcast_submission_update(sender, instance, created, **kwargs):
|
||||||
if not instance.user:
|
_broadcast_submission_event(
|
||||||
return
|
instance,
|
||||||
|
|
||||||
user_group_name = f"submissions_user_{instance.user.id}"
|
|
||||||
submission_data = SubmissionOut.from_orm(instance).dict()
|
|
||||||
|
|
||||||
async_to_sync(get_channel_layer().group_send)(
|
|
||||||
user_group_name,
|
|
||||||
{
|
{
|
||||||
"type": "submission.updated",
|
"type": "submission.updated",
|
||||||
"submission": submission_data,
|
"submission": SubmissionOut.from_orm(instance).dict(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save, sender=SubmissionFile)
|
@receiver(post_save, sender=SubmissionFile)
|
||||||
def broadcast_submission_file_update(sender, instance, created, **kwargs):
|
def broadcast_submission_file_update(sender, instance, created, **kwargs):
|
||||||
submission = instance.submission
|
_broadcast_submission_event(
|
||||||
if not submission.user:
|
instance.submission,
|
||||||
return
|
|
||||||
|
|
||||||
user_group_name = f"submissions_user_{submission.user.id}"
|
|
||||||
file_data = SubmissionFileOut.from_orm(instance).dict()
|
|
||||||
|
|
||||||
async_to_sync(get_channel_layer().group_send)(
|
|
||||||
user_group_name,
|
|
||||||
{
|
{
|
||||||
"type": "submission.file.updated",
|
"type": "submission.file.updated",
|
||||||
"file": file_data,
|
"file": SubmissionFileOut.from_orm(instance).dict(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save, sender=ImageCrop)
|
@receiver(post_save, sender=ImageCrop)
|
||||||
def broadcast_image_crop_update(sender, instance, created, **kwargs):
|
def broadcast_image_crop_update(sender, instance, created, **kwargs):
|
||||||
submission = instance.submission_file.submission
|
_broadcast_submission_event(
|
||||||
if not submission.user:
|
instance.submission_file.submission,
|
||||||
return
|
|
||||||
|
|
||||||
user_group_name = f"submissions_user_{submission.user.id}"
|
|
||||||
crop_data = ImageCropOut.from_orm(instance).dict()
|
|
||||||
|
|
||||||
async_to_sync(get_channel_layer().group_send)(
|
|
||||||
user_group_name,
|
|
||||||
{
|
{
|
||||||
"type": "submission.crop.updated",
|
"type": "submission.crop.updated",
|
||||||
"crop": crop_data,
|
"crop": ImageCropOut.from_orm(instance).dict(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(post_delete, sender=ImageCrop)
|
||||||
|
def broadcast_image_crop_delete(sender, instance, **kwargs):
|
||||||
|
_broadcast_submission_event(
|
||||||
|
instance.submission_file.submission,
|
||||||
|
{
|
||||||
|
"type": "submission.crop.deleted",
|
||||||
|
"crop": {
|
||||||
|
"id": str(instance.id),
|
||||||
|
"submission_file_id": str(instance.submission_file_id),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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, Submission
|
from .models import ImageCrop, Scope, Submission, SubmissionFile
|
||||||
from .services.aandt_client.models import (
|
from .services.aandt_client.models import (
|
||||||
AandtApiInfosResponse200,
|
AandtApiInfosResponse200,
|
||||||
AandtApiMatchesResponse200,
|
AandtApiMatchesResponse200,
|
||||||
@@ -434,3 +434,55 @@ class ListUserSubmissionsApiTests(TestCase):
|
|||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
{s["id"] for s in data}, {str(self.mine.id), str(self.theirs.id)}
|
{s["id"] for s in data}, {str(self.mine.id), str(self.theirs.id)}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SubmissionBroadcastTests(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
event = Event.objects.create(name="Event", path="event")
|
||||||
|
game = Game.objects.create(name="Game", path="game")
|
||||||
|
event_game = EventGame.objects.create(event=event, game=game)
|
||||||
|
scope = Scope.objects.create(event_game=event_game, name="M1")
|
||||||
|
self.player = CustomUser.objects.create_user(username="player", password="x")
|
||||||
|
submission = Submission.objects.create(
|
||||||
|
event=event, scope=scope, user=self.player
|
||||||
|
)
|
||||||
|
# bulk_create: skips the file broadcast, which would sign an S3 url
|
||||||
|
[submission_file] = SubmissionFile.objects.bulk_create(
|
||||||
|
[
|
||||||
|
SubmissionFile(
|
||||||
|
submission=submission,
|
||||||
|
file="shot.png",
|
||||||
|
original_filename="shot.png",
|
||||||
|
file_size=1,
|
||||||
|
content_type="image/png",
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
self.crop = ImageCrop.objects.create(
|
||||||
|
submission_file=submission_file, x1=0, y1=0, x2=1, y2=1
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_crop_update_reaches_owner_and_staff(self):
|
||||||
|
with mock.patch("submissions.signals.get_channel_layer") as get_layer:
|
||||||
|
get_layer.return_value.group_send = mock.AsyncMock()
|
||||||
|
self.crop.save()
|
||||||
|
|
||||||
|
groups = [c.args[0] for c in get_layer.return_value.group_send.call_args_list]
|
||||||
|
self.assertEqual(
|
||||||
|
groups, ["submissions_staff", f"submissions_user_{self.player.id}"]
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_crop_delete_reaches_owner_and_staff(self):
|
||||||
|
crop_id = str(self.crop.id)
|
||||||
|
|
||||||
|
with mock.patch("submissions.signals.get_channel_layer") as get_layer:
|
||||||
|
get_layer.return_value.group_send = mock.AsyncMock()
|
||||||
|
self.crop.delete()
|
||||||
|
|
||||||
|
calls = get_layer.return_value.group_send.call_args_list
|
||||||
|
self.assertEqual(
|
||||||
|
[c.args[0] for c in calls],
|
||||||
|
["submissions_staff", f"submissions_user_{self.player.id}"],
|
||||||
|
)
|
||||||
|
self.assertEqual(calls[0].args[1]["type"], "submission.crop.deleted")
|
||||||
|
self.assertEqual(calls[0].args[1]["crop"]["id"], crop_id)
|
||||||
|
|||||||
Reference in New Issue
Block a user