From 0a73c3a8116ba7273c57def6ffe4082118b4902c Mon Sep 17 00:00:00 2001 From: Mawoka Date: Sun, 28 May 2023 12:03:13 +0200 Subject: [PATCH] :sparkles: Saving image relations on save in editor --- classquiz/helpers/__init__.py | 14 +++++++ classquiz/routers/editor.py | 9 ++-- classquiz/routers/storage.py | 13 ++++-- classquiz/worker/__init__.py | 4 +- classquiz/worker/storage.py | 44 +++++++++++++++++++- frontend/src/lib/editor/card.svelte | 2 +- frontend/src/lib/editor/settings-card.svelte | 2 +- 7 files changed, 77 insertions(+), 11 deletions(-) diff --git a/classquiz/helpers/__init__.py b/classquiz/helpers/__init__.py index 89698b9..4f44635 100644 --- a/classquiz/helpers/__init__.py +++ b/classquiz/helpers/__init__.py @@ -196,3 +196,17 @@ def check_image_string(image: str) -> (bool, uuid.UUID | None): return True, None except ValueError: return False, None + + +def extract_image_ids_from_quiz(quiz: Quiz) -> list[str | uuid.UUID]: + quiz_images = [] + if quiz.background_image is not None: + quiz_images.append(quiz.background_image) + if quiz.cover_image is not None: + quiz_images.append(quiz.cover_image) + + for question in quiz.questions: + if question["image"] is None: + continue + quiz_images.append(question["image"]) + return quiz_images diff --git a/classquiz/routers/editor.py b/classquiz/routers/editor.py index df6f6a4..9faa26a 100644 --- a/classquiz/routers/editor.py +++ b/classquiz/routers/editor.py @@ -12,7 +12,7 @@ import bleach from fastapi import APIRouter, HTTPException, Depends from pydantic import BaseModel -from classquiz.config import settings, redis, storage, meilisearch, ALLOWED_TAGS_FOR_QUIZ +from classquiz.config import settings, redis, storage, meilisearch, ALLOWED_TAGS_FOR_QUIZ, arq from classquiz.db.models import Quiz, QuizInput, User, QuizQuestionType from classquiz.auth import get_current_user import os @@ -111,6 +111,7 @@ async def finish_edit(edit_id: str, quiz_input: QuizInput): images_to_delete = [] old_quiz_data: Quiz = await Quiz.objects.get_or_none(id=session_data.quiz_id, user_id=session_data.user_id) + print(old_quiz_data) def mark_image_for_deletion(new: str | None, index: int, old_quiz: Quiz | None): if old_quiz is None: @@ -133,7 +134,7 @@ async def finish_edit(edit_id: str, quiz_input: QuizInput): question.image = None if image is None: mark_image_for_deletion(question.image, i, old_quiz_data) - elif check_image_string(quiz_input.cover_image)[0]: + elif check_image_string(question.image)[0]: mark_image_for_deletion(question.image, i, old_quiz_data) else: raise HTTPException(status_code=400, detail="Image URL(s) aren't valid!") @@ -151,6 +152,7 @@ async def finish_edit(edit_id: str, quiz_input: QuizInput): raise HTTPException(status_code=400, detail="image url is not valid") if session_data.edit: + await arq.enqueue_job("quiz_update", old_quiz_data, old_quiz_data.id, _defer_by=2) quiz = old_quiz_data meilisearch.index(settings.meilisearch_index).update_documents([await get_meili_data(quiz)]) if not quiz_input.public: @@ -174,7 +176,8 @@ async def finish_edit(edit_id: str, quiz_input: QuizInput): await redis.srem("edit_sessions", edit_id) await redis.delete(f"edit_session:{edit_id}") await redis.delete(f"edit_session:{edit_id}:images") - return await quiz.update() + await quiz.update() + return quiz else: quiz = Quiz( **quiz_input.dict(), diff --git a/classquiz/routers/storage.py b/classquiz/routers/storage.py index 1831f34..a9ad953 100644 --- a/classquiz/routers/storage.py +++ b/classquiz/routers/storage.py @@ -1,7 +1,6 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. -import re from datetime import datetime from fastapi import APIRouter, HTTPException, UploadFile, File, Depends @@ -10,6 +9,7 @@ from fastapi.responses import StreamingResponse, RedirectResponse from classquiz.auth import get_current_user from classquiz.config import settings, storage, arq from classquiz.db.models import User, StorageItem, PublicStorageItem, UpdateStorageItem +from classquiz.helpers import check_image_string from classquiz.storage.errors import DownloadingFailedError from uuid import uuid4, UUID @@ -22,10 +22,17 @@ file_regex = r"^[a-z0-9]{8}-[a-z0-9-]{27}--[a-z0-9-]{36}$" @router.get("/download/{file_name}") async def download_file(file_name: str): - if not re.match(file_regex, file_name): + checked_image_string = check_image_string(file_name) + if not checked_image_string[0]: raise HTTPException(status_code=400, detail="Invalid file name") + if checked_image_string[1] is not None: + item = await StorageItem.objects.get_or_none(id=checked_image_string[1]) + if item is None: + raise HTTPException(status_code=404, detail="File not found") + file_name = item.storage_path + if file_name is None: + file_name = item.id.hex if storage.backend == "s3": - print("redir") return RedirectResponse(url=await storage.get_url(file_name, 300)) try: download = await storage.download(file_name) diff --git a/classquiz/worker/__init__.py b/classquiz/worker/__init__.py index 62409cb..9e4e05e 100644 --- a/classquiz/worker/__init__.py +++ b/classquiz/worker/__init__.py @@ -6,7 +6,7 @@ from arq.connections import RedisSettings from classquiz import settings from classquiz.db import database -from classquiz.worker.storage import clean_editor_images_up, calculate_hash +from classquiz.worker.storage import clean_editor_images_up, calculate_hash, quiz_update async def startup(ctx): @@ -22,7 +22,7 @@ async def shutdown(ctx): class WorkerSettings: # functions = [add_track] - functions = [calculate_hash] + functions = [calculate_hash, quiz_update] cron_jobs = [cron(clean_editor_images_up, hour={0, 6, 12, 18}, minute=0)] on_startup = startup on_shutdown = shutdown diff --git a/classquiz/worker/storage.py b/classquiz/worker/storage.py index 376e283..b6b76bb 100644 --- a/classquiz/worker/storage.py +++ b/classquiz/worker/storage.py @@ -3,13 +3,15 @@ # file, You can obtain one at https://mozilla.org/MPL/2.0/. import uuid +import ormar.exceptions from arq.worker import Retry import xxhash from classquiz.config import redis, storage from tempfile import SpooledTemporaryFile -from classquiz.db.models import StorageItem +from classquiz.db.models import StorageItem, Quiz +from classquiz.helpers import extract_image_ids_from_quiz from classquiz.storage.errors import DeletionFailedError @@ -50,3 +52,43 @@ async def calculate_hash(ctx, file_id_as_str: str): print("Got hash!") await file_data.update() file.close() + + +async def quiz_update(ctx, old_quiz: Quiz, quiz_id: uuid.UUID): + new_quiz: Quiz = await Quiz.objects.get(id=quiz_id) + old_images = extract_image_ids_from_quiz(old_quiz) + new_images = extract_image_ids_from_quiz(new_quiz) + + # If images are identical, then return + if sorted(old_images) == sorted(new_images): + print("Nothing's changed") + return + print("Change detected") + removed_images = list(set(old_images) - set(new_images)) + added_images = list(set(new_images) - set(old_images)) + change_made = False + # print("added:", added_images) + # print("removed:", removed_images) + for image in removed_images: + if "--" in image: + await storage.delete([image]) + else: + item = await StorageItem.objects.get_or_none(id=uuid.UUID(image)) + if item is None: + continue + # print("removed item") + try: + await new_quiz.storageitems.remove(item) + except ormar.exceptions.NoMatch: + continue + change_made = True + for image in added_images: + if "--" not in image: + item = await StorageItem.objects.get_or_none(id=uuid.UUID(image)) + if item is None: + continue + # print("added item") + await new_quiz.storageitems.add(item) + change_made = True + if change_made: + await new_quiz.update() diff --git a/frontend/src/lib/editor/card.svelte b/frontend/src/lib/editor/card.svelte index 5c80b61..c0b88fe 100644 --- a/frontend/src/lib/editor/card.svelte +++ b/frontend/src/lib/editor/card.svelte @@ -110,7 +110,7 @@ class="rounded-full absolute -top-2 -right-2 opacity-70 hover:opacity-100 transition" type="button" on:click={() => { - data.questions[selected_question].image = ''; + data.questions[selected_question].image = null; }} > { - data.cover_image = ''; + data.cover_image = null; }} />