diff --git a/classquiz/db/models.py b/classquiz/db/models.py index eaa2395..f7b5103 100644 --- a/classquiz/db/models.py +++ b/classquiz/db/models.py @@ -7,7 +7,7 @@ from datetime import datetime from typing import Optional import ormar -from pydantic import BaseModel, Json +from pydantic import BaseModel, Json, validator from enum import Enum from . import metadata, database @@ -62,17 +62,38 @@ class UserSession(ormar.Model): database = database -class QuizAnswer(BaseModel): +class ABCDQuizAnswer(BaseModel): right: bool answer: str +class RangeQuizAnswer(BaseModel): + min: int + max: int + min_correct: int + max_correct: int + + +class QuizQuestionType(str, Enum): + ABCD = "ABCD" + RANGE = "RANGE" + + class QuizQuestion(BaseModel): question: str time: str # in Secs - answers: list[QuizAnswer] + type: None | QuizQuestionType = QuizQuestionType.ABCD + answers: list[ABCDQuizAnswer] | RangeQuizAnswer image: str | None = None + @validator("answers") + def answers_not_none_if_abcd_type(cls, v, values): + if values["type"] == QuizQuestionType.ABCD and len(v) == 0: + raise ValueError("Answers can't be none if type is ABCD") + if values["type"] == QuizQuestionType.RANGE and type(v) != RangeQuizAnswer: + raise ValueError("Answer must be from type RangeQuizAnswer if type is RANGE") + return v + class QuizInput(BaseModel): public: bool = False diff --git a/classquiz/kahoot_importer/import_quiz.py b/classquiz/kahoot_importer/import_quiz.py index 100cc25..3e0b5f7 100644 --- a/classquiz/kahoot_importer/import_quiz.py +++ b/classquiz/kahoot_importer/import_quiz.py @@ -10,7 +10,7 @@ from datetime import datetime from aiohttp import ClientSession from classquiz.config import settings, storage, meilisearch -from classquiz.db.models import Quiz, QuizAnswer, QuizQuestion, User +from classquiz.db.models import Quiz, ABCDQuizAnswer, QuizQuestion, User from classquiz.kahoot_importer.get import get as get_quiz from classquiz.helpers import get_meili_data @@ -38,7 +38,7 @@ async def import_quiz(quiz_id: str, user: User) -> Quiz | str: meilisearch.create_index(settings.meilisearch_index) for q in quiz.kahoot.questions: - answers: list[QuizAnswer] = [] + answers: list[ABCDQuizAnswer] = [] image = None if q.image is not None and q.image != "": image_bytes = await _download_image(q.image) @@ -46,7 +46,7 @@ async def import_quiz(quiz_id: str, user: User) -> Quiz | str: image = await storage.upload(file_name=image_name, file_data=image_bytes) image = f"{settings.root_address}/api/v1/storage/download/{image_name}" for a in q.choices: - answers.append((QuizAnswer(right=a.correct, answer=html.unescape(a.answer)))) + answers.append((ABCDQuizAnswer(right=a.correct, answer=html.unescape(a.answer)))) quiz_questions.append( QuizQuestion( question=q.question, diff --git a/classquiz/routers/editor.py b/classquiz/routers/editor.py index 1add4c9..a9cb010 100644 --- a/classquiz/routers/editor.py +++ b/classquiz/routers/editor.py @@ -96,9 +96,9 @@ async def upload_image(edit_id: str, pow_data: str, file: UploadFile = File()): if session_data is None: raise HTTPException(status_code=401, detail="Edit ID not found!") - if uploaded_images == 0 and not check_hashcash(pow_data, pow_data_server, "16"): + if uploaded_images == 0 and not check_hashcash(pow_data, pow_data_server, "8"): raise HTTPException(status_code=401, detail="Edit ID not found!") - if uploaded_images != 0 and not check_hashcash(pow_data, pow_data_server, "16"): + if uploaded_images != 0 and not check_hashcash(pow_data, pow_data_server, "8"): raise HTTPException(status_code=401, detail="Edit ID not found!") file_bytes = await file.read() if len(file_bytes) < 2000: diff --git a/classquiz/socket_server/__init__.py b/classquiz/socket_server/__init__.py index 5bba9ed..df2e397 100644 --- a/classquiz/socket_server/__init__.py +++ b/classquiz/socket_server/__init__.py @@ -10,7 +10,7 @@ import socketio from typing import Any from classquiz.config import redis, settings -from classquiz.db.models import PlayGame +from classquiz.db.models import PlayGame, QuizQuestionType from pydantic import BaseModel, ValidationError sio = socketio.AsyncServer(async_mode="asgi", cors_allowed_origins=[]) @@ -188,10 +188,20 @@ async def submit_answer(sid: str, data: dict): session = await sio.get_session(sid) game_data = PlayGame.parse_raw(await redis.get(f"game:{session['game_pin']}")) answer_right = False - for answer in game_data.questions[int(data.question_index)].answers: - if answer.answer == data.answer and answer.right: + if game_data.questions[int(data.question_index)].type == QuizQuestionType.ABCD: + for answer in game_data.questions[int(data.question_index)].answers: + if answer.answer == data.answer and answer.right: + answer_right = True + break + elif game_data.questions[int(data.question_index)].type == QuizQuestionType.RANGE: + if ( + game_data.questions[int(data.question_index)].answers.min_correct + <= int(data.answer) + <= game_data.questions[int(data.question_index)].answers.max_correct + ): answer_right = True - break + else: + raise NotImplementedError answers = await redis.get(f"game_session:{session['game_pin']}:{data.question_index}") if answers is None: await redis.set( diff --git a/frontend/package.json b/frontend/package.json index 46e70b7..8ab605a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -62,6 +62,7 @@ "svelte": "^3.49.0", "svelte-check": "^2.8.0", "svelte-preprocess": "^4.10.7", + "svelte-range-slider-pips": "^2.0.3", "svelte-tippy": "^1.3.2", "swiper": "^8.3.0", "tailwindcss": "^3.1.5", @@ -69,8 +70,8 @@ "tslib": "^2.4.0", "typescript": "~4.7.4", "ua-parser-js": "^1.0.2", - "vite-plugin-iso-import": "^0.1.3", "vite": "^2.9.14", + "vite-plugin-iso-import": "^0.1.3", "yup": "^0.32.11" }, "type": "module", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index c0224b4..37b68bc 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -50,6 +50,7 @@ specifiers: svelte: ^3.49.0 svelte-check: ^2.8.0 svelte-preprocess: ^4.10.7 + svelte-range-slider-pips: ^2.0.3 svelte-tippy: ^1.3.2 swiper: ^8.3.0 tailwindcss: ^3.1.5 @@ -113,6 +114,7 @@ devDependencies: svelte: 3.49.0 svelte-check: 2.8.0_zcc6v35ghcnbcbmplt33oqigiy svelte-preprocess: 4.10.7_ggcoocdz6dccpsdipp2taqk6cq + svelte-range-slider-pips: 2.0.3 svelte-tippy: 1.3.2 swiper: 8.3.0 tailwindcss: 3.1.5 @@ -4575,6 +4577,13 @@ packages: typescript: 4.7.4 dev: true + /svelte-range-slider-pips/2.0.3: + resolution: + { + integrity: sha512-43zYhIZtGZywiS0nJPAFDfQipDz8Hs2lpj6gVU8WbC+NSu0Lu8VINHjDazeLpoa9X/3rMlaVGT2lPO2aTfLc6w== + } + dev: true + /svelte-tippy/1.3.2: resolution: { diff --git a/frontend/src/lib/editor/ABCDEditorPart.svelte b/frontend/src/lib/editor/ABCDEditorPart.svelte new file mode 100644 index 0000000..1d5cab7 --- /dev/null +++ b/frontend/src/lib/editor/ABCDEditorPart.svelte @@ -0,0 +1,97 @@ + + + +
Right-click on an answer to delete it!
diff --git a/frontend/src/lib/editor/sidebar.svelte b/frontend/src/lib/editor/sidebar.svelte index 149477c..d5889b4 100644 --- a/frontend/src/lib/editor/sidebar.svelte +++ b/frontend/src/lib/editor/sidebar.svelte @@ -5,16 +5,16 @@ -->+ Every number between {question.answers.min_correct} and {question.answers.max_correct} was correct. + You got {selected_answer}, so you have been + {#if question.answers.min_correct <= parseInt(selected_answer) && parseInt(selected_answer) <= question.answers.max_correct} + correct + {:else} + wrong. + {/if} +
{/if} diff --git a/frontend/src/lib/play/show_results.svelte b/frontend/src/lib/play/show_results.svelte index 0966cb0..c822cb2 100644 --- a/frontend/src/lib/play/show_results.svelte +++ b/frontend/src/lib/play/show_results.svelte @@ -6,6 +6,7 @@ + + + + + ++ The page you were looking for is gone or never even existed. Who knows? +
+{:else} ++ That shouldn't happen. It's probably my fault, not yours, but maybe you have a magical power + to break stuff... +
+{/if} + ++ The quiz you were looking for is gone or never even existed. Who knows? +
+{:else} ++ That shouldn't happen. It's probably my fault, not yours, but maybe you have a magical power + to break stuff... +
+{/if} + +