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
-->