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 @@ + + + +
+ {#each data.questions[selected_question].answers as answer, index} +
{ + data.questions[selected_question].answers.splice(index, 1); + data.questions[selected_question].answers = + data.questions[selected_question].answers; + }} + out:fade={{ duration: 150 }} + class="p-4 rounded-lg flex justify-center w-full transition" + class:bg-red-500={!answer.right} + class:bg-green-500={answer.right} + class:bg-yellow-500={!reach(ABCDQuestionSchema, 'answer').isValidSync(answer.answer)} + > + + +
+ {/each} + {#if data.questions[selected_question].answers.length < 4} + + {/if} +
diff --git a/frontend/src/lib/editor/RangeSelectorEditorPart.svelte b/frontend/src/lib/editor/RangeSelectorEditorPart.svelte new file mode 100644 index 0000000..0ccf2cd --- /dev/null +++ b/frontend/src/lib/editor/RangeSelectorEditorPart.svelte @@ -0,0 +1,74 @@ + + +
+
+
+ + +
+
+
+ + + {#await import('svelte-range-slider-pips')} + + {:then c} + {#await sleep(100)} + + {:then _} + + {/await} + {/await} +
+
diff --git a/frontend/src/lib/editor/card.svelte b/frontend/src/lib/editor/card.svelte index 1059ea6..66b38c0 100644 --- a/frontend/src/lib/editor/card.svelte +++ b/frontend/src/lib/editor/card.svelte @@ -4,11 +4,12 @@ - file, You can obtain one at https://mozilla.org/MPL/2.0/. -->
@@ -149,88 +154,26 @@ />
- +
+ +
-
- {#each data.questions[selected_question].answers as answer, index} -
{ - data.questions[selected_question].answers.splice(index, 1); - data.questions[selected_question].answers = - data.questions[selected_question].answers; - }} - out:fade={{ duration: 150 }} - class="p-4 rounded-lg flex justify-center w-full transition" - class:bg-red-500={!answer.right} - class:bg-green-500={answer.right} - class:bg-yellow-500={!reach( - dataSchema, - 'questions[].answers[].answer' - ).isValidSync(answer.answer)} - > - - -
- {/each} - {#if data.questions[selected_question].answers.length < 4} - - {/if} -
+ {#if data.questions[selected_question].type === QuizQuestionType.ABCD} + {#await import('$lib/editor/ABCDEditorPart.svelte')} + + {:then c} + + {/await} + {:else if data.questions[selected_question].type === QuizQuestionType.RANGE} + + {/if}

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 @@ -->
@@ -62,16 +79,40 @@
{/if} {#if timer_res !== '0'} -
- {#each question.answers as answer} - - {/each} -
-{:else} + {#if question.type === QuizQuestionType.ABCD} +
+ {#each question.answers as answer} + + {/each} +
+ {:else if question.type === QuizQuestionType.RANGE} + {#await import('svelte-range-slider-pips')} + + {:then c} + +
+ +
+ {/await} + {/if} +{:else if question.type === QuizQuestionType.ABCD}
{#each question.answers as answer} {#if answer.right} @@ -89,4 +130,14 @@ {/if} {/each}
+{:else if question.type === QuizQuestionType.RANGE} +

+ 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 @@ + + + + + + + Error - {status} + +

{status}

+ +{#if status === 404} +

+ 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} + +
+ Cat representing the {status}-http error code +
diff --git a/frontend/src/routes/edit.svelte b/frontend/src/routes/edit.svelte index 759d9e7..bfc2a0a 100644 --- a/frontend/src/routes/edit.svelte +++ b/frontend/src/routes/edit.svelte @@ -36,6 +36,7 @@ import Editor from '$lib/editor.svelte'; import { getLocalization } from '$lib/i18n'; import { navbarVisible } from '$lib/stores'; + import { QuizQuestionType } from '../lib/quiz_types'; navbarVisible.set(false); @@ -72,7 +73,16 @@ if (response.status === 404) { throw new Error('Quiz not found'); } else if (response.status === 200) { - data = await response.json(); + let temp_data = await response.json(); + for (let i = 0; i < temp_data.questions.length; i++) { + let question = temp_data.questions[i]; + if (question.type === undefined) { + temp_data.questions[i].type = QuizQuestionType.ABCD; + } else { + temp_data.questions[i].type = QuizQuestionType[question.type]; + } + } + data = temp_data; return; } }; diff --git a/frontend/src/routes/explore.svelte b/frontend/src/routes/explore.svelte index 295110a..e03ebfa 100644 --- a/frontend/src/routes/explore.svelte +++ b/frontend/src/routes/explore.svelte @@ -15,6 +15,8 @@ + + + + + Error - {status} + +

{status}

+ +{#if status === 404} +

+ 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} + +
+ Cat representing the {status}-http error code +
diff --git a/run_tests.sh b/run_tests.sh index 8faf760..7fe5e53 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -20,6 +20,7 @@ init() { docker run -it --rm -d -p 7700:7700 --name test_meili getmeili/meilisearch:latest docker volume create classquiz_db_data docker run --name classquiz_db -p 5432:5432 --rm -d -e POSTGRES_PASSWORD=mysecretpassword -v classquiz_db_data:/var/lib/postgresql/data -e POSTGRES_DB=classquiz postgres + sleep 1 pipenv run alembic upgrade head }