✨ Added button to view imported quiz on Kahoot!.
This commit is contained in:
@@ -181,6 +181,7 @@ class Quiz(ormar.Model):
|
|||||||
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)
|
background_image: str | None = ormar.Text(nullable=True, unique=False)
|
||||||
|
kahoot_id: uuid.UUID | None = ormar.UUID(nullable=True, default=None)
|
||||||
|
|
||||||
@validator("background_image")
|
@validator("background_image")
|
||||||
def must_come_from_local_cdn(cls, v):
|
def must_come_from_local_cdn(cls, v):
|
||||||
|
|||||||
@@ -30,7 +30,8 @@ async def import_quiz(quiz_id: str, user: User) -> Quiz | str:
|
|||||||
:param quiz_id: The ID of the quiz to import.
|
:param quiz_id: The ID of the quiz to import.
|
||||||
:return: True if the import was successful, False otherwise.
|
:return: True if the import was successful, False otherwise.
|
||||||
"""
|
"""
|
||||||
quiz = await get_quiz(quiz_id)
|
kahoot_quiz_id = quiz_id
|
||||||
|
quiz = await get_quiz(kahoot_quiz_id)
|
||||||
if quiz is None:
|
if quiz is None:
|
||||||
return "quiz not found"
|
return "quiz not found"
|
||||||
quiz_questions: list[dict] = []
|
quiz_questions: list[dict] = []
|
||||||
@@ -60,7 +61,6 @@ async def import_quiz(quiz_id: str, user: User) -> Quiz | str:
|
|||||||
).dict()
|
).dict()
|
||||||
)
|
)
|
||||||
cover = None
|
cover = None
|
||||||
print(quiz.kahoot.cover)
|
|
||||||
if quiz.kahoot.cover != "" and quiz.kahoot.cover is not None:
|
if quiz.kahoot.cover != "" and quiz.kahoot.cover is not None:
|
||||||
image_bytes = await _download_image(quiz.kahoot.cover)
|
image_bytes = await _download_image(quiz.kahoot.cover)
|
||||||
image_name = f"{quiz_id}--{uuid.uuid4()}"
|
image_name = f"{quiz_id}--{uuid.uuid4()}"
|
||||||
@@ -77,6 +77,7 @@ async def import_quiz(quiz_id: str, user: User) -> Quiz | str:
|
|||||||
questions=json.dumps(quiz_questions),
|
questions=json.dumps(quiz_questions),
|
||||||
imported_from_kahoot=True,
|
imported_from_kahoot=True,
|
||||||
cover_image=cover,
|
cover_image=cover,
|
||||||
|
kahoot_id=uuid.UUID(kahoot_quiz_id),
|
||||||
)
|
)
|
||||||
meilisearch.index(settings.meilisearch_index).add_documents([await get_meili_data(quiz_data)])
|
meilisearch.index(settings.meilisearch_index).add_documents([await get_meili_data(quiz_data)])
|
||||||
return await quiz_data.save()
|
return await quiz_data.save()
|
||||||
|
|||||||
@@ -105,7 +105,6 @@ async def auth(request: Request, response: Response):
|
|||||||
if user_data.email is None:
|
if user_data.email is None:
|
||||||
return RedirectResponse("/account/oauth-error?error=email")
|
return RedirectResponse("/account/oauth-error?error=email")
|
||||||
user_in_db = await User.objects.get_or_none(email=user_data.email)
|
user_in_db = await User.objects.get_or_none(email=user_data.email)
|
||||||
print(user_data.id)
|
|
||||||
if user_in_db is None:
|
if user_in_db is None:
|
||||||
# REGISTER USER
|
# REGISTER USER
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -92,7 +92,6 @@ async def get_public_quiz(quiz_id: str):
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
raise HTTPException(status_code=400, detail="badly formed quiz id")
|
raise HTTPException(status_code=400, detail="badly formed quiz id")
|
||||||
quiz = await Quiz.objects.select_related("user_id").get_or_none(id=quiz_id)
|
quiz = await Quiz.objects.select_related("user_id").get_or_none(id=quiz_id)
|
||||||
print(quiz.dict(exclude={"user_id": {"avatar"}}))
|
|
||||||
if quiz is None:
|
if quiz is None:
|
||||||
return JSONResponse(status_code=404, content={"detail": "quiz not found"})
|
return JSONResponse(status_code=404, content={"detail": "quiz not found"})
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -331,8 +331,7 @@ async def get_email_from_jwt(data: GetEmailFromJWT):
|
|||||||
try:
|
try:
|
||||||
payload = jwt.decode(data.jwt, settings.secret_key, algorithms=["HS256"])
|
payload = jwt.decode(data.jwt, settings.secret_key, algorithms=["HS256"])
|
||||||
return payload.get("sub")
|
return payload.get("sub")
|
||||||
except JWTError as e:
|
except JWTError:
|
||||||
print(e)
|
|
||||||
raise HTTPException(status_code=401)
|
raise HTTPException(status_code=401)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,14 +3,17 @@
|
|||||||
export let flex = false;
|
export let flex = false;
|
||||||
|
|
||||||
export let href: undefined | string = undefined;
|
export let href: undefined | string = undefined;
|
||||||
|
export let target: undefined | string = '_self';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if href}
|
{#if href}
|
||||||
<a
|
<a
|
||||||
{href}
|
{href}
|
||||||
|
{target}
|
||||||
class="w-full px-4 py-2 leading-5 text-black dark:text-white transition-colors duration-200 transform bg-gray-50 dark:bg-gray-700 rounded text-center hover:bg-gray-300 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 dark:hover:bg-gray-600"
|
class="w-full px-4 py-2 leading-5 text-black dark:text-white transition-colors duration-200 transform bg-gray-50 dark:bg-gray-700 rounded text-center hover:bg-gray-300 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 dark:hover:bg-gray-600"
|
||||||
on:click
|
on:click
|
||||||
class:flex
|
class:flex
|
||||||
|
class:block={!flex}
|
||||||
class:justify-center={flex}
|
class:justify-center={flex}
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
import StartGamePopup from '$lib/dashboard/start_game.svelte';
|
import StartGamePopup from '$lib/dashboard/start_game.svelte';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import Spinner from '$lib/Spinner.svelte';
|
import Spinner from '$lib/Spinner.svelte';
|
||||||
|
import GrayButton from '$lib/components/buttons/gray.svelte';
|
||||||
|
|
||||||
const tippy = createTippy({
|
const tippy = createTippy({
|
||||||
arrow: true,
|
arrow: true,
|
||||||
@@ -56,6 +57,7 @@
|
|||||||
user_id: string;
|
user_id: string;
|
||||||
imported_from_kahoot?: boolean;
|
imported_from_kahoot?: boolean;
|
||||||
questions: Question[];
|
questions: Question[];
|
||||||
|
kahoot_id?: string;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -82,87 +84,95 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="text-center text-sm pt-1">
|
<div class="text-center text-sm pt-1 mb-4">
|
||||||
<ImportedOrNot imported={quiz.imported_from_kahoot} />
|
<ImportedOrNot imported={quiz.imported_from_kahoot} />
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex flex-col justify-center">
|
||||||
<div class="flex justify-center mt-8 mb-4">
|
<div class="mx-auto flex flex-col gap-2 justify-center w-fit">
|
||||||
{#if logged_in}
|
{#if quiz.imported_from_kahoot && quiz.kahoot_id}
|
||||||
<button
|
<div class="w-full">
|
||||||
class="admin-button"
|
<GrayButton
|
||||||
on:click={() => {
|
href="https://create.kahoot.it/details/{quiz.kahoot_id}"
|
||||||
start_game = quiz.id;
|
target="_blank"
|
||||||
}}
|
|
||||||
>
|
|
||||||
{$t('words.start')}
|
|
||||||
</button>
|
|
||||||
{:else}
|
|
||||||
<div use:tippy={{ content: 'You need to be logged in to start a game' }}>
|
|
||||||
<button
|
|
||||||
class="px-4 py-2 leading-5 text-white transition-colors duration-200 transform bg-gray-700 rounded text-center hover:bg-gray-600 focus:outline-none cursor-not-allowed opacity-50"
|
|
||||||
disabled
|
|
||||||
>
|
|
||||||
{$t('words.start')}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-center m-2">
|
|
||||||
<a class="admin-button" href="/practice?quiz_id={quiz.id}">
|
|
||||||
{$t('words.practice')}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-center">
|
|
||||||
{#if logged_in}
|
|
||||||
<a
|
|
||||||
href="/api/v1/eximport/{quiz.id}"
|
|
||||||
class="px-4 py-2 leading-5 text-white transition-colors duration-200 transform bg-gray-700 rounded text-center hover:bg-gray-600 focus:outline-none"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
class="w-5 h-5 inline-block"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
{$t('words.download')}
|
|
||||||
</a>
|
|
||||||
{:else}
|
|
||||||
<div use:tippy={{ content: 'You need to be logged in to download a game' }}>
|
|
||||||
<button
|
|
||||||
class="px-4 py-2 leading-5 text-white transition-colors duration-200 transform bg-gray-700 rounded text-center hover:bg-gray-600 focus:outline-none cursor-not-allowed opacity-50"
|
|
||||||
disabled
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
class="w-5 h-5 inline-block"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
>
|
||||||
<path
|
View on <i>Kahoot!</i>
|
||||||
stroke-linecap="round"
|
</GrayButton>
|
||||||
stroke-linejoin="round"
|
</div>
|
||||||
stroke-width="2"
|
{/if}
|
||||||
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
|
{#if logged_in}
|
||||||
/>
|
<div class="w-full">
|
||||||
</svg>
|
<GrayButton
|
||||||
{$t('words.download')}
|
on:click={() => {
|
||||||
</button>
|
start_game = quiz.id;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{$t('words.start')}
|
||||||
|
</GrayButton>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div use:tippy={{ content: 'You need to be logged in to start a game' }}>
|
||||||
|
<div class="w-full">
|
||||||
|
<GrayButton disabled={true}>
|
||||||
|
{$t('words.start')}
|
||||||
|
</GrayButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div class="w-full">
|
||||||
|
<GrayButton href="/practice?quiz_id={quiz.id}">
|
||||||
|
{$t('words.practice')}
|
||||||
|
</GrayButton>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
<div class="w-full">
|
||||||
</div>
|
{#if logged_in}
|
||||||
<div class="flex justify-center">
|
<GrayButton flex={true} href="/api/v1/eximport/{quiz.id}">
|
||||||
<a href="mailto:report@mawoka.eu?subject=Report quiz {quiz.id}" class="text-sm underline">
|
<svg
|
||||||
{$t('words.report')}
|
class="w-5 h-5 inline-block"
|
||||||
</a>
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{$t('words.download')}
|
||||||
|
</GrayButton>
|
||||||
|
{:else}
|
||||||
|
<div use:tippy={{ content: 'You need to be logged in to download a game' }}>
|
||||||
|
<GrayButton disabled={true} flex={true}>
|
||||||
|
<svg
|
||||||
|
class="w-5 h-5 inline-block"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{$t('words.download')}
|
||||||
|
</GrayButton>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<a
|
||||||
|
href="mailto:report@mawoka.eu?subject=Report quiz {quiz.id}"
|
||||||
|
class="text-sm underline"
|
||||||
|
>
|
||||||
|
{$t('words.report')}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#each quiz.questions as question, index_question}
|
{#each quiz.questions as question, index_question}
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
"""Added kahoot_id
|
||||||
|
|
||||||
|
Revision ID: 7ad8502af419
|
||||||
|
Revises: 820e06ef2c2a
|
||||||
|
Create Date: 2023-01-27 22:30:00.331395
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
import ormar
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = "7ad8502af419"
|
||||||
|
down_revision = "820e06ef2c2a"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column("quiz", sa.Column("kahoot_id", ormar.fields.sqlalchemy_uuid.CHAR(32), nullable=True))
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column("quiz", "kahoot_id")
|
||||||
|
# ### end Alembic commands ###
|
||||||
Reference in New Issue
Block a user