diff --git a/classquiz/routers/editor.py b/classquiz/routers/editor.py
index 5866362..46f30ff 100644
--- a/classquiz/routers/editor.py
+++ b/classquiz/routers/editor.py
@@ -1,8 +1,10 @@
import asyncio
import html
+import re
import uuid
from typing import Optional
+import asyncpg.exceptions
import bleach
import pydantic
from fastapi import APIRouter, File, Form, UploadFile, HTTPException, BackgroundTasks, Depends
@@ -16,6 +18,9 @@ from datetime import datetime
from uuid import UUID
from classquiz.helpers import get_meili_data
+from classquiz.storage.errors import DeletionFailedError
+
+settings = settings()
router = APIRouter()
@@ -83,8 +88,41 @@ async def finish_edit(edit_id: str, quiz_input: QuizInput):
session_data = EditSessionData.parse_raw(session_data)
quiz_input.title = html.unescape(bleach.clean(quiz_input.title, tags=[], strip=True))
quiz_input.description = html.unescape(bleach.clean(quiz_input.description, tags=[], strip=True))
+ image_id_regex = r"^.{36}--.{36}$"
+ imgur_regex = r"^https://i\.imgur\.com\/.{7}.(jpg|png|gif)$"
+ server_regex = rf"^{re.escape(settings.root_address)}/api/v1/storage/download/.{{36}}--.{{36}}$"
+ extract_file_name_re = r"^.*/api/v1/storage/download/(.{36}--.{36})$"
+ images_to_delete = []
+ old_quiz_data = await Quiz.objects.get_or_none(id=session_data.quiz_id, user_id=session_data.user_id)
+
+ def mark_image_for_deletion(new: str | None, index: int, old_quiz: Quiz | None):
+ if old_quiz is None:
+ return
+ print(new, old_quiz.questions[index]["image"])
+ if new == old_quiz.questions[index]["image"]:
+ return
+ else:
+ images_to_delete.append(old_quiz.questions[index]["image"])
+
+ for i, question in enumerate(quiz_input.questions):
+ image = question.image
+ if image == "":
+ question.image = None
+ mark_image_for_deletion(question.image, i, old_quiz_data)
+ elif image is None:
+ mark_image_for_deletion(question.image, i, old_quiz_data)
+ elif bool(re.match(image_id_regex, question.image)):
+ question.image = f"{settings.root_address}/api/v1/storage/download/{image}"
+ mark_image_for_deletion(question.image, i, old_quiz_data)
+ elif bool(re.match(imgur_regex, image)):
+ mark_image_for_deletion(question.image, i, old_quiz_data)
+ elif bool(re.match(server_regex, image)):
+ mark_image_for_deletion(question.image, i, old_quiz_data)
+ else:
+ raise HTTPException(status_code=400, detail="Image URL(s) aren't valid!")
+ print(images_to_delete)
if session_data.edit:
- quiz = await Quiz.objects.get_or_none(id=session_data.quiz_id, user_id=session_data.user_id)
+ quiz = old_quiz_data
meilisearch.index(settings.meilisearch_index).update_documents([await get_meili_data(quiz)])
if quiz.public and not quiz_input.public:
meilisearch.index(settings.meilisearch_index).delete_document(str(quiz.id))
@@ -95,10 +133,19 @@ async def finish_edit(edit_id: str, quiz_input: QuizInput):
quiz.description = quiz_input.description
quiz.updated_at = datetime.now()
quiz.questions = quiz_input.dict()["questions"]
- return await quiz.save()
+ for image in images_to_delete:
+ if image is not None:
+ try:
+ await storage.delete(re.search(extract_file_name_re, image).group(1))
+ except DeletionFailedError:
+ pass
+ return await quiz.update()
else:
quiz = Quiz(**quiz_input.dict(), user_id=session_data.user_id, id=session_data.quiz_id)
await redis.delete("global_quiz_count")
if quiz_input.public:
meilisearch.index(settings.meilisearch_index).add_documents([await get_meili_data(quiz)])
- return await quiz.save()
+ try:
+ return await quiz.save()
+ except asyncpg.exceptions.UniqueViolationError:
+ raise HTTPException(status_code=400, detail="The quiz already exists")
diff --git a/classquiz/storage/deta_storage.py b/classquiz/storage/deta_storage.py
index 5fa4049..f186117 100644
--- a/classquiz/storage/deta_storage.py
+++ b/classquiz/storage/deta_storage.py
@@ -1,4 +1,5 @@
from io import BytesIO
+from classquiz.storage.errors import DeletionFailedError, SavingFailedError, DownloadingFailedError
from aiohttp import ClientSession
@@ -26,7 +27,7 @@ class DetaStorage:
elif response.status == 404:
return None
else:
- raise Exception("Download failed")
+ raise DownloadingFailedError
async def upload(self, file: bytes, file_name: str) -> None:
"""
@@ -40,7 +41,7 @@ class DetaStorage:
if response.status == 201:
return None
else:
- raise Exception("Upload failed")
+ raise SavingFailedError
async def delete(self, file_names: [str]) -> None:
async with ClientSession(headers=self.headers) as session, session.delete(
@@ -49,4 +50,4 @@ class DetaStorage:
if response.status == 200:
return None
else:
- raise Exception("Delete failed")
+ raise DeletionFailedError
diff --git a/classquiz/storage/errors.py b/classquiz/storage/errors.py
new file mode 100644
index 0000000..ccbd115
--- /dev/null
+++ b/classquiz/storage/errors.py
@@ -0,0 +1,10 @@
+class DeletionFailedError(Exception):
+ pass
+
+
+class SavingFailedError(Exception):
+ pass
+
+
+class DownloadingFailedError(Exception):
+ pass
diff --git a/frontend/src/lib/editor.svelte b/frontend/src/lib/editor.svelte
index 4129e5b..37b30e2 100644
--- a/frontend/src/lib/editor.svelte
+++ b/frontend/src/lib/editor.svelte
@@ -66,6 +66,7 @@
answer: ''
};
let edit_id;
+ let confirm_to_leave = true;
const getEditID = async () => {
let res;
@@ -86,54 +87,90 @@
alert('Error!');
}
};
+
+ const confirmUnload = (event) => {
+ console.log(confirm_to_leave);
+ if (!confirm_to_leave) {
+ return;
+ }
+ event.preventDefault();
+ event.returnValue = 'Are you sure you want to leave?';
+ localStorage.setItem('edit_game', JSON.stringify(data));
+ return 'unload';
+ };
+ const saveQuiz = async () => {
+ if (schemaInvalid) {
+ return;
+ }
+ const res = await fetch(`/api/v1/editor/finish?edit_id=${edit_id}`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify(data)
+ });
+ if (res.ok) {
+ console.log('Hier');
+ confirm_to_leave = false;
+ console.log(confirm_to_leave);
+ window.location.href = '/overview';
+ } else {
+ alert('Error');
+ }
+ };
+
- {yupErrorMessage} -
- {:else} -- {data.title} -
- {/if} -