Saving image relations on save in editor

This commit is contained in:
Mawoka
2023-05-28 12:03:13 +02:00
parent 10668e420b
commit 0a73c3a811
7 changed files with 77 additions and 11 deletions
+14
View File
@@ -196,3 +196,17 @@ def check_image_string(image: str) -> (bool, uuid.UUID | None):
return True, None return True, None
except ValueError: except ValueError:
return False, None 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
+6 -3
View File
@@ -12,7 +12,7 @@ import bleach
from fastapi import APIRouter, HTTPException, Depends from fastapi import APIRouter, HTTPException, Depends
from pydantic import BaseModel 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.db.models import Quiz, QuizInput, User, QuizQuestionType
from classquiz.auth import get_current_user from classquiz.auth import get_current_user
import os import os
@@ -111,6 +111,7 @@ async def finish_edit(edit_id: str, quiz_input: QuizInput):
images_to_delete = [] images_to_delete = []
old_quiz_data: Quiz = await Quiz.objects.get_or_none(id=session_data.quiz_id, user_id=session_data.user_id) 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): def mark_image_for_deletion(new: str | None, index: int, old_quiz: Quiz | None):
if old_quiz is None: if old_quiz is None:
@@ -133,7 +134,7 @@ async def finish_edit(edit_id: str, quiz_input: QuizInput):
question.image = None question.image = None
if image is None: if image is None:
mark_image_for_deletion(question.image, i, old_quiz_data) 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) mark_image_for_deletion(question.image, i, old_quiz_data)
else: else:
raise HTTPException(status_code=400, detail="Image URL(s) aren't valid!") 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") raise HTTPException(status_code=400, detail="image url is not valid")
if session_data.edit: if session_data.edit:
await arq.enqueue_job("quiz_update", old_quiz_data, old_quiz_data.id, _defer_by=2)
quiz = old_quiz_data quiz = old_quiz_data
meilisearch.index(settings.meilisearch_index).update_documents([await get_meili_data(quiz)]) meilisearch.index(settings.meilisearch_index).update_documents([await get_meili_data(quiz)])
if not quiz_input.public: 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.srem("edit_sessions", edit_id)
await redis.delete(f"edit_session:{edit_id}") await redis.delete(f"edit_session:{edit_id}")
await redis.delete(f"edit_session:{edit_id}:images") await redis.delete(f"edit_session:{edit_id}:images")
return await quiz.update() await quiz.update()
return quiz
else: else:
quiz = Quiz( quiz = Quiz(
**quiz_input.dict(), **quiz_input.dict(),
+10 -3
View File
@@ -1,7 +1,6 @@
# This Source Code Form is subject to the terms of the Mozilla Public # 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 # 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/. # file, You can obtain one at https://mozilla.org/MPL/2.0/.
import re
from datetime import datetime from datetime import datetime
from fastapi import APIRouter, HTTPException, UploadFile, File, Depends 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.auth import get_current_user
from classquiz.config import settings, storage, arq from classquiz.config import settings, storage, arq
from classquiz.db.models import User, StorageItem, PublicStorageItem, UpdateStorageItem from classquiz.db.models import User, StorageItem, PublicStorageItem, UpdateStorageItem
from classquiz.helpers import check_image_string
from classquiz.storage.errors import DownloadingFailedError from classquiz.storage.errors import DownloadingFailedError
from uuid import uuid4, UUID 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}") @router.get("/download/{file_name}")
async def download_file(file_name: str): 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") 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": if storage.backend == "s3":
print("redir")
return RedirectResponse(url=await storage.get_url(file_name, 300)) return RedirectResponse(url=await storage.get_url(file_name, 300))
try: try:
download = await storage.download(file_name) download = await storage.download(file_name)
+2 -2
View File
@@ -6,7 +6,7 @@ from arq.connections import RedisSettings
from classquiz import settings from classquiz import settings
from classquiz.db import database 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): async def startup(ctx):
@@ -22,7 +22,7 @@ async def shutdown(ctx):
class WorkerSettings: class WorkerSettings:
# functions = [add_track] # 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)] cron_jobs = [cron(clean_editor_images_up, hour={0, 6, 12, 18}, minute=0)]
on_startup = startup on_startup = startup
on_shutdown = shutdown on_shutdown = shutdown
+43 -1
View File
@@ -3,13 +3,15 @@
# file, You can obtain one at https://mozilla.org/MPL/2.0/. # file, You can obtain one at https://mozilla.org/MPL/2.0/.
import uuid import uuid
import ormar.exceptions
from arq.worker import Retry from arq.worker import Retry
import xxhash import xxhash
from classquiz.config import redis, storage from classquiz.config import redis, storage
from tempfile import SpooledTemporaryFile 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 from classquiz.storage.errors import DeletionFailedError
@@ -50,3 +52,43 @@ async def calculate_hash(ctx, file_id_as_str: str):
print("Got hash!") print("Got hash!")
await file_data.update() await file_data.update()
file.close() 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()
+1 -1
View File
@@ -110,7 +110,7 @@
class="rounded-full absolute -top-2 -right-2 opacity-70 hover:opacity-100 transition" class="rounded-full absolute -top-2 -right-2 opacity-70 hover:opacity-100 transition"
type="button" type="button"
on:click={() => { on:click={() => {
data.questions[selected_question].image = ''; data.questions[selected_question].image = null;
}} }}
> >
<svg <svg
+1 -1
View File
@@ -72,7 +72,7 @@
alt="not available" alt="not available"
class="max-h-72 h-auto w-auto" class="max-h-72 h-auto w-auto"
on:contextmenu|preventDefault={() => { on:contextmenu|preventDefault={() => {
data.cover_image = ''; data.cover_image = null;
}} }}
/> />
</div> </div>