From 31d1e3e3e6dfc04920aeb7ef6793bd36bed67bfc Mon Sep 17 00:00:00 2001 From: Mawoka Date: Mon, 30 Oct 2023 11:11:50 +0100 Subject: [PATCH] :sparkles: Quizzes can now be exported as excel files --- classquiz/routers/eximport.py | 61 ++++++++++++++++++- .../src/lib/components/DownloadQuiz.svelte | 50 +++++++++++++++ frontend/src/lib/i18n/locales/en.json | 21 +++++-- frontend/src/routes/dashboard/+page.svelte | 5 +- .../src/routes/view/[quiz_id]/+page.svelte | 6 +- 5 files changed, 135 insertions(+), 8 deletions(-) create mode 100644 frontend/src/lib/components/DownloadQuiz.svelte diff --git a/classquiz/routers/eximport.py b/classquiz/routers/eximport.py index 4ded1bd..a9944d5 100644 --- a/classquiz/routers/eximport.py +++ b/classquiz/routers/eximport.py @@ -7,14 +7,16 @@ import io import json import uuid from datetime import datetime +from typing import Any import ormar.exceptions +import xlsxwriter from aiohttp import ClientSession from fastapi import APIRouter, Depends, File, UploadFile, HTTPException from fastapi.responses import StreamingResponse from classquiz.auth import get_current_user from classquiz.config import storage, settings, arq -from classquiz.db.models import Quiz, User, StorageItem +from classquiz.db.models import Quiz, User, StorageItem, QuizQuestionType, QuizQuestion import gzip import urllib.parse import magic @@ -131,3 +133,60 @@ async def import_quiz(file: UploadFile = File(), user: User = Depends(get_curren quiz.mod_rating = None await quiz.save() return quiz + + +@router.get("/excel/{quiz_id}") +async def export_quiz_as_excel(quiz_id: uuid.UUID, user: User = Depends(get_current_user)): + try: + quiz: Quiz = await Quiz.objects.filter(Quiz.id == quiz_id).first() + except ormar.exceptions.NoMatch: + raise HTTPException(status_code=404, detail="Quiz not found") + storage = io.BytesIO() + workbook = xlsxwriter.Workbook(storage, {"in_memory": True}) + ws = workbook.add_worksheet() + ws.name = "Main" + ws.write(4, 1, "Title") + ws.write(4, 2, quiz.title) + ws.write(5, 1, "Description") + ws.write(5, 2, quiz.description) + ws.write_row( + 12, + 1, + [ + "Question", + "1st Answer", + "2nd Answer", + "3rd Answer", + "4th Answer", + "Time Limit (max. 120)", + "Correct Answers", + ], + ) + for i, question in enumerate(quiz.questions): + question = QuizQuestion.parse_obj(question) + data: list[Any] = [None] * 9 + data[0] = i + 1 + data[1] = question.question + if question.type not in [QuizQuestionType.ABCD, QuizQuestionType.VOTING]: + continue + correct_answers = [] + for a, answer in enumerate(question.answers): + data[2 + a] = answer.answer + if question.type == QuizQuestionType.ABCD and answer.right: + correct_answers.append(str(a + 1)) + data[6] = question.time + data[7] = ",".join(correct_answers) + ws.write_row(13 + i, 0, data) + workbook.close() + storage.seek(0) + + def iter_file(): + yield from storage + + return StreamingResponse( + iter_file(), + media_type="application/vnd.ms-excel", + headers={ + "Content-Disposition": f"attachment;filename=ClassQuiz-{urllib.parse.quote(quiz.title)}.xlsx" # noqa: E501 + }, + ) diff --git a/frontend/src/lib/components/DownloadQuiz.svelte b/frontend/src/lib/components/DownloadQuiz.svelte new file mode 100644 index 0000000..79a8c44 --- /dev/null +++ b/frontend/src/lib/components/DownloadQuiz.svelte @@ -0,0 +1,50 @@ + + + +{#if quiz_id} +
+
+

{$t('downloader.select_download_type')}

+
+
+ {$t('downloader.own_format')} + +
+
+ {$t('downloader.excel_format')} + +
+
+

{$t('downloader.help')}

+
+
+{/if} diff --git a/frontend/src/lib/i18n/locales/en.json b/frontend/src/lib/i18n/locales/en.json index e753dfc..a54b1fd 100644 --- a/frontend/src/lib/i18n/locales/en.json +++ b/frontend/src/lib/i18n/locales/en.json @@ -45,7 +45,8 @@ "community_driven_content": "ClassQuiz depends on its community for funding, testing ideas, feature requests, translations and more! You can also be a part of the ClassQuiz-community!", "how_does_classquiz_work": "How does ClassQuiz even work?", "no_player_limit": "No player limit", - "no_player_limit_content": "ClassQuiz doesn't have an artificial player limit. In theory, 1000+ players could play the same quiz, but it's tested with up to 300 players per quiz." + "no_player_limit_content": "ClassQuiz doesn't have an artificial player limit. In theory, 1000+ players could play the same quiz, but it's tested with up to 300 players per quiz.", + "import_quiz_from_kahoot_and_edit": "" }, "overview_page": { "created_at": "Created at", @@ -193,7 +194,8 @@ "dislike": "Dislike", "dislike_plural": "Dislikes", "play_plural": "Plays", - "info": "Info" + "info": "Info", + "player": "Player" }, "editor": { "time_in_seconds": "Time in seconds", @@ -254,7 +256,8 @@ "enter_answer_into_field": "Enter your answer into the input field!", "answers_submitted": "{{answer_count}} Answers submitted", "request_export_results": "Request result download", - "download_export_results": "Download results" + "download_export_results": "Download results", + "export_results": "Export results" }, "password_reset_page": { "reset_password": "Reset password" @@ -292,7 +295,8 @@ "join_by_entering_code": "Join by entering the following code", "points_added": "Points added", "your_place": "You're on place {{place}}!", - "players_waiting": "{{count}} players are waiting" + "players_waiting": "{{count}} player are waiting", + "players_waiting_plural": "{{count}} players are waiting" }, "editor_page": { "add_an_answer": "Add an answer", @@ -448,7 +452,8 @@ }, "public_user_page": { "joined_on": "Joined on {{date}}", - "no_original_quizzes": "This user doesn't have any original quizzes" + "no_original_quizzes": "This user doesn't have any original quizzes", + "user_not_found": "User not found" }, "file_dashboard": { "not_available": "Not available", @@ -469,5 +474,11 @@ "video_uploader": { "time_elapsed": "Time elapsed", "time_remaining": "Time remaining" + }, + "downloader": { + "select_download_type": "Select download type", + "own_format": "Own Format", + "excel_format": "Excel Format", + "help": "The own format (.cqa) also includes pictures and supports every question type, whereas the excel format only supports voting and multiple-choice questions." } } diff --git a/frontend/src/routes/dashboard/+page.svelte b/frontend/src/routes/dashboard/+page.svelte index 3c2501c..82bd9aa 100644 --- a/frontend/src/routes/dashboard/+page.svelte +++ b/frontend/src/routes/dashboard/+page.svelte @@ -5,6 +5,7 @@ SPDX-License-Identifier: MPL-2.0 -->