From 2ab9d14d3e3e994f5129d3762a1c6fd2f1a2139d Mon Sep 17 00:00:00 2001 From: Mawoka Date: Thu, 8 Sep 2022 17:47:29 +0200 Subject: [PATCH] :sparkles: Added cover-image (Closes #103) --- classquiz/db/models.py | 3 ++ classquiz/kahoot_importer/import_quiz.py | 7 ++++ classquiz/routers/editor.py | 8 ++++ classquiz/routers/quiz.py | 16 ++++++++ frontend/src/lib/dashboard/main_slider.svelte | 24 ++++++++++-- frontend/src/lib/editor.svelte | 26 ++++++++++++- frontend/src/lib/editor/card.svelte | 18 +-------- frontend/src/lib/editor/settings-card.svelte | 37 +++++++++++++++++++ frontend/src/lib/editor/uploader.svelte | 12 ++++-- frontend/src/lib/play/title.svelte | 8 ++++ frontend/src/lib/quiz_types.ts | 2 + frontend/src/routes/+page.svelte | 6 ++- frontend/src/routes/play/+page.svelte | 6 ++- .../src/routes/view/[quiz_id]/+page.svelte | 12 +++++- .../cda6903dfc0c_added_cover_image.py | 30 +++++++++++++++ 15 files changed, 186 insertions(+), 29 deletions(-) create mode 100644 migrations/versions/cda6903dfc0c_added_cover_image.py diff --git a/classquiz/db/models.py b/classquiz/db/models.py index 8ce627c..461e5aa 100644 --- a/classquiz/db/models.py +++ b/classquiz/db/models.py @@ -99,6 +99,7 @@ class QuizInput(BaseModel): public: bool = False title: str description: str + cover_image: str | None questions: list[QuizQuestion] @@ -112,6 +113,7 @@ class Quiz(ormar.Model): user_id: uuid.UUID = ormar.ForeignKey(User) questions: Json[list[QuizQuestion]] = ormar.JSON(nullable=False) imported_from_kahoot: Optional[bool] = ormar.Boolean(default=False, nullable=True) + cover_image: Optional[str] = ormar.Text(nullable=True, unique=False) class Meta: tablename = "quiz" @@ -154,6 +156,7 @@ class PlayGame(BaseModel): game_pin: str started: bool = False captcha_enabled: bool = False + cover_image: str | None class GamePlayer(BaseModel): diff --git a/classquiz/kahoot_importer/import_quiz.py b/classquiz/kahoot_importer/import_quiz.py index 3e0b5f7..4475920 100644 --- a/classquiz/kahoot_importer/import_quiz.py +++ b/classquiz/kahoot_importer/import_quiz.py @@ -55,6 +55,12 @@ async def import_quiz(quiz_id: str, user: User) -> Quiz | str: image=image, ).dict() ) + cover = None + if quiz.kahoot.cover != "": + image_bytes = await _download_image(quiz.kahoot.cover) + image_name = f"{quiz_id}--{uuid.uuid4()}" + await storage.upload(file_name=image_name, file_data=image_bytes) + cover = f"{settings.root_address}/api/v1/storage/download/{image_name}" quiz_data = Quiz( id=quiz_id, public=True, @@ -65,6 +71,7 @@ async def import_quiz(quiz_id: str, user: User) -> Quiz | str: user_id=user.id, questions=json.dumps(quiz_questions), imported_from_kahoot=True, + cover_image=cover, ) meilisearch.index(settings.meilisearch_index).add_documents([await get_meili_data(quiz_data)]) return await quiz_data.save() diff --git a/classquiz/routers/editor.py b/classquiz/routers/editor.py index a9cb010..4699061 100644 --- a/classquiz/routers/editor.py +++ b/classquiz/routers/editor.py @@ -157,6 +157,13 @@ async def finish_edit(edit_id: str, quiz_input: QuizInput): mark_image_for_deletion(question.image, i, old_quiz_data) else: raise HTTPException(status_code=400, detail="Image URL(s) aren't valid!") + + if quiz_input.cover_image == "": + quiz_input.cover_image = None + + if quiz_input.cover_image is not None and not bool(re.match(server_regex, quiz_input.cover_image)): + raise HTTPException(status_code=400, detail="image url is not valid") + if session_data.edit: quiz = old_quiz_data meilisearch.index(settings.meilisearch_index).update_documents([await get_meili_data(quiz)]) @@ -169,6 +176,7 @@ 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"] + quiz.cover_image = quiz_input.cover_image for image in images_to_delete: if image is not None: try: diff --git a/classquiz/routers/quiz.py b/classquiz/routers/quiz.py index d80f27a..b2439d4 100644 --- a/classquiz/routers/quiz.py +++ b/classquiz/routers/quiz.py @@ -42,6 +42,11 @@ async def create_quiz_lol(quiz_input: QuizInput, user: User = Depends(get_curren and not re.match(server_regex, question.image) ): raise HTTPException(status_code=400, detail="image url is not valid") + if quiz_input.cover_image == "": + quiz_input.cover_image = None + + if quiz_input.cover_image is not None and not bool(re.match(server_regex, quiz_input.cover_image)): + raise HTTPException(status_code=400, detail="image url is not valid") quiz = Quiz(**quiz_input.dict(), user_id=user.id, id=uuid.uuid4()) await redis.delete("global_quiz_count") if quiz_input.public: @@ -102,6 +107,7 @@ async def start_quiz(quiz_id: str, captcha_enabled: bool = True, user: User = De title=quiz.title, description=quiz.description, captcha_enabled=captcha_enabled, + cover_image=quiz.cover_image, ) await redis.set(f"game:{str(game.game_pin)}", (game.json()), ex=18000) return {**quiz.dict(exclude={"id"}), **game.dict(exclude={"questions"})} @@ -156,6 +162,15 @@ async def update_quiz(quiz_id: str, quiz_input: QuizInput, user: User = Depends( quiz_id = uuid.UUID(quiz_id) except ValueError: raise HTTPException(status_code=400, detail="badly formed quiz id") + ## Check Cover-Image + + print(quiz_input.cover_image) + if quiz_input.cover_image == "": + quiz_input.cover_image = None + + if quiz_input.cover_image is not None and not bool(re.match(server_regex, quiz_input.cover_image)): + raise HTTPException(status_code=400, detail="image url is not valid") + quiz = await Quiz.objects.get_or_none(id=quiz_id, user_id=user.id) if quiz is None: return JSONResponse(status_code=404, content={"detail": "quiz not found"}) @@ -168,6 +183,7 @@ async def update_quiz(quiz_id: str, quiz_input: QuizInput, user: User = Depends( if not quiz.public and quiz_input.public: meilisearch.index(settings.meilisearch_index).add_documents([await get_meili_data(quiz)]) quiz.title = quiz_input.title + quiz.cover_image = quiz_input.cover_image quiz.public = quiz_input.public quiz.description = quiz_input.description quiz.updated_at = datetime.now() diff --git a/frontend/src/lib/dashboard/main_slider.svelte b/frontend/src/lib/dashboard/main_slider.svelte index b95aef8..a8ea7c6 100644 --- a/frontend/src/lib/dashboard/main_slider.svelte +++ b/frontend/src/lib/dashboard/main_slider.svelte @@ -71,9 +71,25 @@ > {quiz.title}

-

- {quiz.description} -

+
+

+ {quiz.description} +

+ {#if quiz.cover_image} +
+
+ Not provided +
+
+ {/if} +
+

+

{quiz.questions.length} {$t('words.question', { count: quiz.questions.length })}

+
{#if quiz.public} { + if (pow_salt === undefined) { + return; + } + console.log('Computing POW'); + pow_data = await mint(salt, 16, '', 8, false); + pow_salt = undefined; + return; + }; + + $: { + pow_salt; + computePOW(pow_salt); + } const validateInput = async (data: EditorData) => { try { @@ -156,9 +172,15 @@
{#if selected_question === -1} - + {:else} - + {/if}
diff --git a/frontend/src/lib/editor/card.svelte b/frontend/src/lib/editor/card.svelte index ab05e41..c2f4650 100644 --- a/frontend/src/lib/editor/card.svelte +++ b/frontend/src/lib/editor/card.svelte @@ -10,7 +10,6 @@ import { reach } from 'yup'; import { dataSchema } from '$lib/yupSchemas'; import Spinner from '../Spinner.svelte'; - import { mint } from '$lib/hashcash'; import { createTippy } from 'svelte-tippy'; import { getLocalization } from '$lib/i18n'; @@ -26,25 +25,10 @@ export let selected_question: number; export let edit_id: string; export let pow_data; - let pow_salt: string; + export let pow_salt: string; let uppyOpen = false; - const computePOW = async (salt: string) => { - if (pow_salt === undefined) { - return; - } - console.log('Computing POW'); - pow_data = await mint(salt, 16, '', 8, false); - pow_salt = undefined; - return; - }; - - $: { - pow_salt; - computePOW(pow_salt); - } - /*eslint no-unused-vars: ["error", { "argsIgnorePattern": "^_" }]*/ const correctTimeInput = (_) => { let time = data.questions[selected_question].time; diff --git a/frontend/src/lib/editor/settings-card.svelte b/frontend/src/lib/editor/settings-card.svelte index f9f3363..6e3f2be 100644 --- a/frontend/src/lib/editor/settings-card.svelte +++ b/frontend/src/lib/editor/settings-card.svelte @@ -6,9 +6,16 @@ @@ -42,6 +49,36 @@ class="p-3 rounded-lg border-gray-500 border text-center w-1/3 h-20 resize-none dark:bg-gray-500" /> + + {#if data.cover_image != undefined && data.cover_image !== ''} +
+ not available { + data.cover_image = ''; + }} + /> +
+ {:else if pow_data === undefined} + + + + {:else} + {#await import('$lib/editor/uploader.svelte')} + + {:then c} + + {/await} + {/if}
+

How does ClassQuiz even work?

+
-

+

{$t('index_page.get_a_quiz')} -

+
{:else if gameData !== undefined && question_index === ''} - + {:else if gameMeta.started && gameData !== undefined && question_index !== '' && answer_results === undefined} {#key unique}

{quiz.description}

+ {#if quiz.cover_image} +
+
+ Not provided +
+
+ {/if}
@@ -98,7 +109,6 @@ {#if question.image} - {$t('words.image')}: Not provided {/if} diff --git a/migrations/versions/cda6903dfc0c_added_cover_image.py b/migrations/versions/cda6903dfc0c_added_cover_image.py new file mode 100644 index 0000000..76f54f8 --- /dev/null +++ b/migrations/versions/cda6903dfc0c_added_cover_image.py @@ -0,0 +1,30 @@ +"""added cover_image + +Revision ID: cda6903dfc0c +Revises: 3f63c0130bce +Create Date: 2022-09-08 16:40:01.675020 + +""" +from alembic import op +import sqlalchemy as sa +import ormar + + +# revision identifiers, used by Alembic. +revision = "cda6903dfc0c" +down_revision = "3f63c0130bce" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_unique_constraint(None, "instance_data", ["instance_id"]) + op.add_column("quiz", sa.Column("cover_image", sa.Text(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("quiz", "cover_image") + # ### end Alembic commands ###