✨ 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
|
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
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user