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'); + } + }; + {#await getEditID()} {:then _} -
-
- -
-
-
- {#if schemaInvalid} -

- {yupErrorMessage} -

- {:else} -

- {data.title} -

- {/if} - -
-
- {#if selected_question === -1} - - {:else} - - {/if} + Save + + + + +
+
+ {#if selected_question === -1} + + {:else} + + {/if} +
- + {/await} diff --git a/frontend/src/lib/editor/card.svelte b/frontend/src/lib/editor/card.svelte index 6ff4421..3258d87 100644 --- a/frontend/src/lib/editor/card.svelte +++ b/frontend/src/lib/editor/card.svelte @@ -4,6 +4,7 @@ import { reach } from 'yup'; import { dataSchema } from '$lib/yupSchemas'; import Spinner from '../Spinner.svelte'; + import { fade } from 'svelte/transition'; export let data: EditorData; export let selected_question: number; @@ -49,7 +50,14 @@ {#if question.image != undefined && question.image !== ''}
- not available + not available { + question.image = ''; + }} + />
{:else} {#await import('$lib/editor/uploader.svelte')} @@ -60,6 +68,7 @@ bind:modalOpen={uppyOpen} bind:edit_id bind:data + bind:selected_question /> {/await} {/if} @@ -71,7 +80,8 @@ question.answers.splice(index, 1); question.answers = question.answers; }} - class="p-4 rounded-lg flex justify-center w-full" + out:fade={{ duration: 150 }} + class="p-4 rounded-lg flex justify-center w-full transition" class:bg-red-500={!answer.right} class:bg-green-500={answer.right} class:bg-yellow-500={!reach( @@ -127,8 +137,9 @@ {/each} {#if question.answers.length < 4} + }} + >Close +
-{:else} +{/if} +
-{/if} + }} + >Add Image + + + + +
diff --git a/frontend/src/routes/create.svelte b/frontend/src/routes/create.svelte index 7431caf..46e3e2f 100644 --- a/frontend/src/routes/create.svelte +++ b/frontend/src/routes/create.svelte @@ -64,45 +64,14 @@ data = JSON.parse(from_localstorage); } }); - - const submit = async () => { - if (!(await dataSchema.isValid(data))) { - return; - } - const res = await fetch('/api/v1/quiz/create', { - method: 'POST', - body: JSON.stringify(data), - headers: { - 'Content-Type': 'application/json' - } - }); - if (res.status === 401) { - localStorage.setItem('create_game', JSON.stringify(data)); - window.location.href = '/account/login'; - } else if (res.status === 200) { - localStorage.removeItem('create_game'); - responseData.open = true; - } - }; - const confirmUnload = () => { - if (!confirm_to_leave) { - return; - } - event.preventDefault(); - event.returnValue = ''; - localStorage.setItem('create_game', JSON.stringify(data)); - }; - ClassQuiz - Create {#if data !== undefined} -
- - + {/if}
{ - if (!(await dataSchema.isValid(data))) { - return; - } - const res = await fetch(`/api/v1/quiz/update/${quiz_id}`, { - method: 'PUT', - body: JSON.stringify(data), - headers: { - 'Content-Type': 'application/json' - } - }); - if (res.status === 401) { - throw new Error('Unauthorized'); - } else if (res.status === 404) { - throw new Error('Quiz not found'); - } else if (res.status === 200) { - localStorage.removeItem('edit_game'); - responseData.data = '200'; - responseData.open = true; - } - }; - const confirmUnload = () => { - if (!confirm_to_leave) { - return; - } - event.preventDefault(); - event.returnValue = ''; - localStorage.setItem('edit_game', JSON.stringify(data)); - }; - ClassQuiz - Edit @@ -121,9 +91,7 @@ {:then _} {#if data !== undefined} -
- - + {/if} {:catch err}