Added background-image to quizzes

This commit is contained in:
Mawoka
2022-12-28 19:07:32 +01:00
parent d89f934f5b
commit 1633d56299
9 changed files with 114 additions and 6 deletions
+3 -1
View File
@@ -1,7 +1,7 @@
# 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 functools import lru_cache from functools import lru_cache
from redis import asyncio as redis_lib from redis import asyncio as redis_lib
@@ -71,3 +71,5 @@ storage: Storage = Storage(
meilisearch = MeiliSearch.Client(settings().meilisearch_url) meilisearch = MeiliSearch.Client(settings().meilisearch_url)
ALLOWED_TAGS_FOR_QUIZ = ["b", "strong", "i", "em", "small", "mark", "del", "sub", "sup"] ALLOWED_TAGS_FOR_QUIZ = ["b", "strong", "i", "em", "small", "mark", "del", "sub", "sup"]
server_regex = rf"^{re.escape(settings().root_address)}/api/v1/storage/download/.{{36}}--.{{36}}$"
+23
View File
@@ -2,6 +2,7 @@
# 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 os import os
import re
import uuid import uuid
from datetime import datetime from datetime import datetime
from typing import Optional from typing import Optional
@@ -10,6 +11,7 @@ import ormar
from pydantic import BaseModel, Json, validator from pydantic import BaseModel, Json, validator
from enum import Enum from enum import Enum
from . import metadata, database from . import metadata, database
from ..config import server_regex
class UserAuthTypes(Enum): class UserAuthTypes(Enum):
@@ -150,6 +152,16 @@ class QuizInput(BaseModel):
cover_image: str | None cover_image: str | None
background_color: str | None background_color: str | None
questions: list[QuizQuestion] questions: list[QuizQuestion]
background_image: str | None
@validator("background_image")
def must_come_from_local_cdn(cls, v):
if v is None:
return v
elif bool(re.match(server_regex, v)):
return v
else:
raise ValueError("does not match url scheme")
class Quiz(ormar.Model): class Quiz(ormar.Model):
@@ -164,6 +176,16 @@ class Quiz(ormar.Model):
imported_from_kahoot: Optional[bool] = ormar.Boolean(default=False, nullable=True) imported_from_kahoot: Optional[bool] = ormar.Boolean(default=False, nullable=True)
cover_image: Optional[str] = ormar.Text(nullable=True, unique=False) cover_image: Optional[str] = ormar.Text(nullable=True, unique=False)
background_color: str | None = ormar.Text(nullable=True, unique=False) background_color: str | None = ormar.Text(nullable=True, unique=False)
background_image: str | None = ormar.Text(nullable=True, unique=False)
@validator("background_image")
def must_come_from_local_cdn(cls, v):
if v is None:
return v
elif bool(re.match(server_regex, v)):
return v
else:
raise ValueError("does not match url scheme")
class Meta: class Meta:
tablename = "quiz" tablename = "quiz"
@@ -211,6 +233,7 @@ class PlayGame(BaseModel):
game_mode: str | None game_mode: str | None
current_question: int = -1 current_question: int = -1
background_color: str | None background_color: str | None
background_image: str | None
custom_field: str | None custom_field: str | None
+7 -3
View File
@@ -13,7 +13,7 @@ import bleach
from fastapi import APIRouter, File, UploadFile, HTTPException, Depends from fastapi import APIRouter, File, UploadFile, 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, server_regex
from classquiz.db.models import Quiz, QuizInput, User, QuizQuestionType from classquiz.db.models import Quiz, QuizInput, User, QuizQuestionType
import puremagic import puremagic
from classquiz.auth import get_current_user from classquiz.auth import get_current_user
@@ -143,10 +143,10 @@ async def finish_edit(edit_id: str, quiz_input: QuizInput):
) )
image_id_regex = r"^.{36}--.{36}$" image_id_regex = r"^.{36}--.{36}$"
imgur_regex = r"^https://i\.imgur\.com\/.{7}.(jpg|png|gif)$" imgur_regex = r"^https://i\.imgur\.com\/.{7}.(jpg|png|gif)$"
server_regex = rf"^{re.escape(settings.root_address)}/api/v1/storage/download/.{{36}}--.{{36}}$"
extract_file_name_re = r"^.*/api/v1/storage/download/(.{36}--.{36})$" extract_file_name_re = r"^.*/api/v1/storage/download/(.{36}--.{36})$"
images_to_delete = [] images_to_delete = []
old_quiz_data = 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)
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:
@@ -183,6 +183,9 @@ async def finish_edit(edit_id: str, quiz_input: QuizInput):
if quiz_input.cover_image == "": if quiz_input.cover_image == "":
quiz_input.cover_image = None quiz_input.cover_image = None
# if quiz_input.background_image is None and old_quiz_data.background_image is not None:
# mark_image_for_deletion(quiz_input.background_image)
if quiz_input.cover_image is not None and not bool(re.match(server_regex, quiz_input.cover_image)): 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") raise HTTPException(status_code=400, detail="image url is not valid")
@@ -200,6 +203,7 @@ async def finish_edit(edit_id: str, quiz_input: QuizInput):
quiz.questions = quiz_input.dict()["questions"] quiz.questions = quiz_input.dict()["questions"]
quiz.cover_image = quiz_input.cover_image quiz.cover_image = quiz_input.cover_image
quiz.background_color = quiz_input.background_color quiz.background_color = quiz_input.background_color
quiz.background_image = quiz_input.background_image
for image in images_to_delete: for image in images_to_delete:
if image is not None: if image is not None:
try: try:
+1
View File
@@ -126,6 +126,7 @@ async def start_quiz(
user_id=user.id, user_id=user.id,
background_color=quiz.background_color, background_color=quiz.background_color,
custom_field=custom_field, custom_field=custom_field,
background_image=quiz.background_image,
) )
await redis.set(f"game:{str(game.game_pin)}", game.json(), ex=18000) await redis.set(f"game:{str(game.game_pin)}", game.json(), ex=18000)
await redis.set(f"game_pin:{user.id}:{quiz_id}", game_pin, ex=18000) await redis.set(f"game_pin:{user.id}:{quiz_id}", game_pin, ex=18000)
+41 -1
View File
@@ -14,6 +14,7 @@
const { t } = getLocalization(); const { t } = getLocalization();
let uppyOpen = false; let uppyOpen = false;
let bg_uppy_open = false;
export let edit_id: string; export let edit_id: string;
export let data: EditorData; export let data: EditorData;
@@ -38,7 +39,12 @@
/> />
</div> </div>
</div> </div>
<div class="dark:bg-gray-700"> <div
class="dark:bg-gray-700 h-full"
style="background-repeat: no-repeat;background-size: 100% 100%;background-image: {data.background_image
? `url("${data.background_image}")`
: `unset`}"
>
<div class="flex justify-center pt-10 w-full"> <div class="flex justify-center pt-10 w-full">
<!--<input <!--<input
type="text" type="text"
@@ -173,6 +179,40 @@
</div> </div>
</div> </div>
</div> </div>
<div class="flex justify-center pt-10">
<h3>Background-Image</h3>
</div>
<div class="w-full flex justify-center -mt-8">
{#if data.background_image}
<button
on:click={() => {
data.background_image = undefined;
}}
class="mt-10 bg-red-500 p-2 rounded-lg border-2 border-black transition hover:bg-red-400"
>Remove Background-Image</button
>
{:else if pow_data === undefined}
<a href="/docs/pow" target="_blank" class="cursor-help pt-10">
<Spinner my_20={false} />
</a>
{:else}
{#await import('$lib/editor/uploader.svelte')}
<div class="pt-10">
<Spinner my_20={false} />
</div>
{:then c}
<svelte:component
this={c.default}
bind:modalOpen={bg_uppy_open}
bind:edit_id
bind:data
selected_question={-1}
bind:pow_data
bind:pow_salt
/>
{/await}
{/if}
</div>
</div> </div>
</div> </div>
</div> </div>
+3
View File
@@ -21,6 +21,7 @@
import '@uppy/image-editor/dist/style.css'; import '@uppy/image-editor/dist/style.css';
import type { EditorData } from '../quiz_types'; import type { EditorData } from '../quiz_types';
import { getLocalization } from '$lib/i18n'; import { getLocalization } from '$lib/i18n';
const { t } = getLocalization(); const { t } = getLocalization();
export let modalOpen = false; export let modalOpen = false;
@@ -65,6 +66,8 @@
console.log(pow_data); console.log(pow_data);
if (selected_question === undefined) { if (selected_question === undefined) {
data.cover_image = `${window.location.origin}/api/v1/storage/download/${image_id}`; data.cover_image = `${window.location.origin}/api/v1/storage/download/${image_id}`;
} else if (selected_question === -1) {
data.background_image = `${window.location.origin}/api/v1/storage/download/${image_id}`;
} else { } else {
data.questions[ data.questions[
selected_question selected_question
+2
View File
@@ -22,6 +22,7 @@ export interface QuizData {
started: boolean; started: boolean;
cover_image?: string; cover_image?: string;
background_color?: string; background_color?: string;
background_image?: string;
} }
export enum QuizQuestionType { export enum QuizQuestionType {
@@ -65,4 +66,5 @@ export interface EditorData {
questions: Question[]; questions: Question[];
cover_image?: string; cover_image?: string;
background_color?: string; background_color?: string;
background_image?: string;
} }
+5 -1
View File
@@ -131,7 +131,9 @@
players = players; players = players;
}; };
let bg_color; let bg_color;
let bg_image;
$: bg_color = quiz_data ? quiz_data.background_color : undefined; $: bg_color = quiz_data ? quiz_data.background_color : undefined;
$: bg_image = quiz_data ? quiz_data.background_image : undefined;
let show_final_results = false; let show_final_results = false;
$: show_final_results = JSON.stringify(final_results) !== JSON.stringify([null]); $: show_final_results = JSON.stringify(final_results) !== JSON.stringify([null]);
</script> </script>
@@ -142,7 +144,9 @@
</svelte:head> </svelte:head>
<div <div
class="min-h-screen min-w-full" class="min-h-screen min-w-full"
style="background: {bg_color ? bg_color : 'transparent'}" style="background-repeat: no-repeat;background-size: 100% 100%;background-image: {bg_image
? `url('${bg_image}')`
: `unset`}; background-color: {bg_color ? bg_color : 'transparent'}"
class:text-black={bg_color} class:text-black={bg_color}
> >
{#if JSON.stringify(final_results) !== JSON.stringify([null])} {#if JSON.stringify(final_results) !== JSON.stringify([null])}
@@ -0,0 +1,29 @@
"""Added background_image
Revision ID: 820e06ef2c2a
Revises: 694cb11c6886
Create Date: 2022-12-28 18:27:57.035467
"""
from alembic import op
import sqlalchemy as sa
import ormar
# revision identifiers, used by Alembic.
revision = "820e06ef2c2a"
down_revision = "694cb11c6886"
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column("quiz", sa.Column("background_image", sa.Text(), nullable=True))
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column("quiz", "background_image")
# ### end Alembic commands ###