diff --git a/classquiz/__init__.py b/classquiz/__init__.py index edc71b9..0afb815 100644 --- a/classquiz/__init__.py +++ b/classquiz/__init__.py @@ -12,7 +12,7 @@ from starlette.middleware.sessions import SessionMiddleware from classquiz.config import settings from classquiz.db import database from datetime import timedelta -from classquiz.routers import users, quiz, utils, stats, storage, search, testing_routes, editor, live +from classquiz.routers import users, quiz, utils, stats, storage, search, testing_routes, editor, live, eximport from classquiz.socket_server import sio from classquiz.helpers import meilisearch_init, telemetry_ping, bg_tasks from scheduler.asyncio import Scheduler @@ -72,4 +72,5 @@ app.include_router( testing_routes.router, tags=["internal", "testing"], prefix="/api/v1/internal/testing", include_in_schema=False ) app.include_router(editor.router, tags=["editor"], prefix="/api/v1/editor", include_in_schema=True) +app.include_router(eximport.router, tags=["export", "import"], prefix="/api/v1/eximport", include_in_schema=True) app.mount("/", ASGIApp(sio)) diff --git a/classquiz/routers/eximport.py b/classquiz/routers/eximport.py new file mode 100644 index 0000000..c208af1 --- /dev/null +++ b/classquiz/routers/eximport.py @@ -0,0 +1,91 @@ +# 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 json +import uuid +from datetime import datetime + +from aiohttp import ClientSession +from fastapi import APIRouter, Depends, Response, File, UploadFile + +from classquiz.auth import get_current_user +from classquiz.config import storage, settings +from classquiz.db.models import Quiz, User +import gzip + +router = APIRouter() +settings = settings() +quiz_delimiter = b"\xc7\xc7\xc7\x00" +image_delimiter = b"\xc6\xc6\xc6\x00" +image_index_delimiter = b"\xc5\xc5\x00" + + +@router.get("/{quiz_id}") +async def export_quiz(quiz_id: uuid.UUID, user: User = Depends(get_current_user)): + quiz: Quiz = await Quiz.objects.filter(Quiz.id == quiz_id).first() + image_urls = {} + for i, question in enumerate(quiz.questions): + if question["image"] is None: + continue + else: + image_urls[i] = question["image"] + question["image"] = i + if quiz.cover_image is not None: + image_urls[-1] = quiz.cover_image + quiz.cover_image = -1 + quiz_dict = quiz.dict() + del quiz_dict["user_id"], quiz_dict["id"] + quiz_dict["created_at"] = quiz_dict["created_at"].isoformat() + quiz_dict["updated_at"] = quiz_dict["updated_at"].isoformat() + quiz_json = json.dumps(quiz_dict) + bin_data = gzip.compress(quiz_json.encode("utf-8"), compresslevel=9) + # bin_data = quiz_json.encode("utf-8") + bin_data = bin_data + quiz_delimiter + for image_key in image_urls.keys(): + bin_data = bin_data + image_delimiter + str(image_key).encode("utf-8") + image_index_delimiter + image_data = None + async with ClientSession() as session, session.get(image_urls[image_key]) as resp: + image_data = await resp.read() + bin_data = bin_data + image_data + + return Response( + content=bin_data, + media_type="application/octet-stream", + headers={ + "Content-Disposition": f"attachment;filename={quiz.title}.cqa" + # noqa: E501 + }, + ) + + +@router.post("/") +async def import_quiz(file: UploadFile = File(), user: User = Depends(get_current_user)): + data = await file.read() + [split_data, images] = data.split(quiz_delimiter) + decompressed_quiz = gzip.decompress(split_data) + quiz_dict = json.loads(decompressed_quiz.decode("utf-8")) + image_splits = images.split(image_delimiter) + quiz_id = uuid.uuid4() + image_urls = {} + for image_split in image_splits: + res = image_split.split(image_index_delimiter) + if len(res) != 2: + continue + [index, image_data] = res + index = int(index.decode("utf-8")) + image_name = f"{quiz_id}--{uuid.uuid4()}" + await storage.upload(file_name=image_name, file_data=image_data) + image = f"{settings.root_address}/api/v1/storage/download/{image_name}" + image_urls[index] = image + quiz_dict["created_at"] = datetime.fromisoformat(quiz_dict["created_at"]) + quiz_dict["updated_at"] = datetime.fromisoformat(quiz_dict["updated_at"]) + quiz_dict["id"] = quiz_id + for question in quiz_dict["questions"]: + if question["image"] is not None: + question["image"] = image_urls[question["image"]] + if quiz_dict["cover_image"] is not None: + quiz_dict["cover_image"] = image_urls[-1] + quiz = Quiz.parse_obj(quiz_dict) + quiz.user_id = user.id + await quiz.save() + return quiz diff --git a/frontend/src/lib/dashboard/main_slider.svelte b/frontend/src/lib/dashboard/main_slider.svelte index 97d803d..a14b6f8 100644 --- a/frontend/src/lib/dashboard/main_slider.svelte +++ b/frontend/src/lib/dashboard/main_slider.svelte @@ -160,7 +160,7 @@ {$t('words.question', { count: quiz.questions.length })}

-
+
{#if quiz.public}
- +
+

{ + copy_id(quiz.id); + }} + > + {quiz.id} +

+
{#each quiz.questions as question} diff --git a/frontend/src/routes/import/+page.svelte b/frontend/src/routes/import/+page.svelte index 0fa7a44..41cecc8 100644 --- a/frontend/src/routes/import/+page.svelte +++ b/frontend/src/routes/import/+page.svelte @@ -11,6 +11,7 @@ const { t } = getLocalization(); let url_input = ''; + let file_input; let url_valid = false; let kahoot_regex = /^https:\/\/create\.kahoot\.it\/details\/([a-zA-Z-\d]{36})\/?$/; @@ -48,6 +49,28 @@ } is_loading = false; }; + + const file_submit = async () => { + is_loading = true; + const formdata = new FormData(); + formdata.append('file', file_input[0]); + const res = await fetch('/api/v1/eximport/', { + method: 'POST', + body: formdata + }); + if (res.status === 200) { + window.location.href = '/dashboard'; + } else { + alertModal.set({ + open: true, + title: 'Import failed', + body: 'Something went wrong!' + }); + } + is_loading = false; + }; + + $: console.log(file_input); @@ -90,59 +113,125 @@ +
+
+
+

A Kahoot!-Quiz

+
+
+ + +

+ The URl should look like this: + https://create.kahoot.it/details/... +

+
+

+ On this side you can import quizzes, which live on Kahoot!. +

+
- -
-
-
- -
+ +
+
+

A ClassQuiz-Quiz

+
+
+ +

Upload the file ending in .cqa

+
+

+ On this side you can import quizzes, which were exported from ClassQuiz. +

+
-
- +
+ - + +
-
-
+ +
{$t('import_page.need_help')}