✨ Saving image relations on save in editor
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
alt="not available"
|
||||
class="max-h-72 h-auto w-auto"
|
||||
on:contextmenu|preventDefault={() => {
|
||||
data.cover_image = '';
|
||||
data.cover_image = null;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user