Added new range-answer type

This commit is contained in:
Mawoka
2022-07-09 23:02:59 +02:00
parent 10a2adb57e
commit 54e13077f7
24 changed files with 566 additions and 206 deletions
+24 -3
View File
@@ -7,7 +7,7 @@ from datetime import datetime
from typing import Optional from typing import Optional
import ormar import ormar
from pydantic import BaseModel, Json from pydantic import BaseModel, Json, validator
from enum import Enum from enum import Enum
from . import metadata, database from . import metadata, database
@@ -62,17 +62,38 @@ class UserSession(ormar.Model):
database = database database = database
class QuizAnswer(BaseModel): class ABCDQuizAnswer(BaseModel):
right: bool right: bool
answer: str 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): class QuizQuestion(BaseModel):
question: str question: str
time: str # in Secs time: str # in Secs
answers: list[QuizAnswer] type: None | QuizQuestionType = QuizQuestionType.ABCD
answers: list[ABCDQuizAnswer] | RangeQuizAnswer
image: str | None = None 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): class QuizInput(BaseModel):
public: bool = False public: bool = False
+3 -3
View File
@@ -10,7 +10,7 @@ from datetime import datetime
from aiohttp import ClientSession from aiohttp import ClientSession
from classquiz.config import settings, storage, meilisearch 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.kahoot_importer.get import get as get_quiz
from classquiz.helpers import get_meili_data 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) meilisearch.create_index(settings.meilisearch_index)
for q in quiz.kahoot.questions: for q in quiz.kahoot.questions:
answers: list[QuizAnswer] = [] answers: list[ABCDQuizAnswer] = []
image = None image = None
if q.image is not None and q.image != "": if q.image is not None and q.image != "":
image_bytes = await _download_image(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 = await storage.upload(file_name=image_name, file_data=image_bytes)
image = f"{settings.root_address}/api/v1/storage/download/{image_name}" image = f"{settings.root_address}/api/v1/storage/download/{image_name}"
for a in q.choices: 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( quiz_questions.append(
QuizQuestion( QuizQuestion(
question=q.question, question=q.question,
+2 -2
View File
@@ -96,9 +96,9 @@ async def upload_image(edit_id: str, pow_data: str, file: UploadFile = File()):
if session_data is None: if session_data is None:
raise HTTPException(status_code=401, detail="Edit ID not found!") 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!") 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!") raise HTTPException(status_code=401, detail="Edit ID not found!")
file_bytes = await file.read() file_bytes = await file.read()
if len(file_bytes) < 2000: if len(file_bytes) < 2000:
+14 -4
View File
@@ -10,7 +10,7 @@ import socketio
from typing import Any from typing import Any
from classquiz.config import redis, settings from classquiz.config import redis, settings
from classquiz.db.models import PlayGame from classquiz.db.models import PlayGame, QuizQuestionType
from pydantic import BaseModel, ValidationError from pydantic import BaseModel, ValidationError
sio = socketio.AsyncServer(async_mode="asgi", cors_allowed_origins=[]) 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) session = await sio.get_session(sid)
game_data = PlayGame.parse_raw(await redis.get(f"game:{session['game_pin']}")) game_data = PlayGame.parse_raw(await redis.get(f"game:{session['game_pin']}"))
answer_right = False answer_right = False
for answer in game_data.questions[int(data.question_index)].answers: if game_data.questions[int(data.question_index)].type == QuizQuestionType.ABCD:
if answer.answer == data.answer and answer.right: 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 answer_right = True
break else:
raise NotImplementedError
answers = await redis.get(f"game_session:{session['game_pin']}:{data.question_index}") answers = await redis.get(f"game_session:{session['game_pin']}:{data.question_index}")
if answers is None: if answers is None:
await redis.set( await redis.set(
+2 -1
View File
@@ -62,6 +62,7 @@
"svelte": "^3.49.0", "svelte": "^3.49.0",
"svelte-check": "^2.8.0", "svelte-check": "^2.8.0",
"svelte-preprocess": "^4.10.7", "svelte-preprocess": "^4.10.7",
"svelte-range-slider-pips": "^2.0.3",
"svelte-tippy": "^1.3.2", "svelte-tippy": "^1.3.2",
"swiper": "^8.3.0", "swiper": "^8.3.0",
"tailwindcss": "^3.1.5", "tailwindcss": "^3.1.5",
@@ -69,8 +70,8 @@
"tslib": "^2.4.0", "tslib": "^2.4.0",
"typescript": "~4.7.4", "typescript": "~4.7.4",
"ua-parser-js": "^1.0.2", "ua-parser-js": "^1.0.2",
"vite-plugin-iso-import": "^0.1.3",
"vite": "^2.9.14", "vite": "^2.9.14",
"vite-plugin-iso-import": "^0.1.3",
"yup": "^0.32.11" "yup": "^0.32.11"
}, },
"type": "module", "type": "module",
+9
View File
@@ -50,6 +50,7 @@ specifiers:
svelte: ^3.49.0 svelte: ^3.49.0
svelte-check: ^2.8.0 svelte-check: ^2.8.0
svelte-preprocess: ^4.10.7 svelte-preprocess: ^4.10.7
svelte-range-slider-pips: ^2.0.3
svelte-tippy: ^1.3.2 svelte-tippy: ^1.3.2
swiper: ^8.3.0 swiper: ^8.3.0
tailwindcss: ^3.1.5 tailwindcss: ^3.1.5
@@ -113,6 +114,7 @@ devDependencies:
svelte: 3.49.0 svelte: 3.49.0
svelte-check: 2.8.0_zcc6v35ghcnbcbmplt33oqigiy svelte-check: 2.8.0_zcc6v35ghcnbcbmplt33oqigiy
svelte-preprocess: 4.10.7_ggcoocdz6dccpsdipp2taqk6cq svelte-preprocess: 4.10.7_ggcoocdz6dccpsdipp2taqk6cq
svelte-range-slider-pips: 2.0.3
svelte-tippy: 1.3.2 svelte-tippy: 1.3.2
swiper: 8.3.0 swiper: 8.3.0
tailwindcss: 3.1.5 tailwindcss: 3.1.5
@@ -4575,6 +4577,13 @@ packages:
typescript: 4.7.4 typescript: 4.7.4
dev: true dev: true
/svelte-range-slider-pips/2.0.3:
resolution:
{
integrity: sha512-43zYhIZtGZywiS0nJPAFDfQipDz8Hs2lpj6gVU8WbC+NSu0Lu8VINHjDazeLpoa9X/3rMlaVGT2lPO2aTfLc6w==
}
dev: true
/svelte-tippy/1.3.2: /svelte-tippy/1.3.2:
resolution: resolution:
{ {
@@ -0,0 +1,97 @@
<!--
- 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/.
-->
<script lang="ts">
import type { EditorData, Answer } from '../quiz_types';
import { fade } from 'svelte/transition';
import { reach } from 'yup';
import { ABCDQuestionSchema } from '$lib/yupSchemas';
const empty_answer: Answer = {
right: false,
answer: ''
};
export let selected_question: number;
export let data: EditorData;
</script>
<div class="grid grid-cols-2 gap-4 w-full px-10">
{#each data.questions[selected_question].answers as answer, index}
<div
on:contextmenu|preventDefault={() => {
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)}
>
<input
bind:value={answer.answer}
type="text"
class="bg-transparent border-b-2 border-dotted w-5/6 text-center"
placeholder="Empty..."
/>
<button
type="button"
on:click={() => {
answer.right = !answer.right;
console.log(answer.right);
}}
>
{#if answer.right}
<svg
class="w-6 h-6 inline-block"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
{:else}
<svg
class="w-6 h-6 inline-block"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
{/if}
</button>
</div>
{/each}
{#if data.questions[selected_question].answers.length < 4}
<button
class="p-4 rounded-lg bg-transparent border-gray-500 border-2 hover:bg-gray-300 transition dark:hover:bg-gray-600"
type="button"
in:fade={{ duration: 150 }}
on:click={() => {
data.questions[selected_question].answers = [
...data.questions[selected_question].answers,
{ ...empty_answer }
];
}}
>
<span class="italic text-center">Add an answer</span>
</button>
{/if}
</div>
@@ -0,0 +1,74 @@
<script lang="ts">
import type { EditorData } from '../quiz_types';
import Spinner from '../Spinner.svelte';
export let selected_question: number;
export let data: EditorData;
let question = data.questions[selected_question];
if (question.answers.max === undefined || question.answers.min_correct === undefined) {
question.answers = {
max: 10,
min: 0,
max_correct: 7,
min_correct: 3
};
}
let answer = question.answers;
let range_arr = [answer.min_correct, answer.max_correct];
$: data.questions[selected_question].answers.min_correct = range_arr[0];
$: data.questions[selected_question].answers.max_correct = range_arr[1];
$: data.questions[selected_question].answers.min =
data.questions[selected_question].answers.min === null
? 0
: data.questions[selected_question].answers.min;
$: data.questions[selected_question].answers.max =
data.questions[selected_question].answers.max === null
? 0
: data.questions[selected_question].answers.max;
$: console.log(data.questions[selected_question].answers.min);
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
</script>
<div class="w-full mx-8">
<div class="flex justify-center">
<div class="grid grid-cols-2 gap-4">
<input
type="number"
class="w-16 bg-transparent rounded-lg text-lg border-2 border-gray-500 p-1"
bind:value={data.questions[selected_question].answers.min}
/>
<input
type="number"
class="w-16 bg-transparent rounded-lg text-lg border-2 border-gray-500 p-1"
bind:value={data.questions[selected_question].answers.max}
/>
</div>
</div>
<div class="w-full">
<!-- <RangeSlider bind:value={range_arr} bind:min={answer.min} bind:max={answer.max} range={true} slider={lol} /> -->
{#await import('svelte-range-slider-pips')}
<Spinner />
{:then c}
{#await sleep(100)}
<Spinner />
{:then _}
<svelte:component
this={c.default}
bind:values={range_arr}
bind:min={data.questions[selected_question].answers.min}
bind:max={data.questions[selected_question].answers.max}
pips
float
all="label"
range
/>
{/await}
{/await}
</div>
</div>
+30 -87
View File
@@ -4,11 +4,12 @@
- file, You can obtain one at https://mozilla.org/MPL/2.0/. - file, You can obtain one at https://mozilla.org/MPL/2.0/.
--> -->
<script lang="ts"> <script lang="ts">
import type { EditorData, Answer } from '$lib/quiz_types'; import type { EditorData } from '$lib/quiz_types';
import { QuizQuestionType } from '$lib/quiz_types';
import RangeEditor from '$lib/editor/RangeSelectorEditorPart.svelte';
import { reach } from 'yup'; import { reach } from 'yup';
import { dataSchema } from '$lib/yupSchemas'; import { dataSchema } from '$lib/yupSchemas';
import Spinner from '../Spinner.svelte'; import Spinner from '../Spinner.svelte';
import { fade } from 'svelte/transition';
import { mint } from '$lib/hashcash'; import { mint } from '$lib/hashcash';
import { createTippy } from 'svelte-tippy'; import { createTippy } from 'svelte-tippy';
import 'tippy.js/animations/perspective-subtle.css'; import 'tippy.js/animations/perspective-subtle.css';
@@ -25,10 +26,7 @@
export let edit_id: string; export let edit_id: string;
export let pow_data; export let pow_data;
let pow_salt: string; let pow_salt: string;
const empty_answer: Answer = {
right: false,
answer: ''
};
let uppyOpen = false; let uppyOpen = false;
const computePOW = async (salt: string) => { const computePOW = async (salt: string) => {
@@ -60,6 +58,13 @@
} }
}; };
$: correctTimeInput(data.questions[selected_question].time); $: correctTimeInput(data.questions[selected_question].time);
/*
if (typeof data.questions[selected_question].type !== QuizQuestionType) {
console.log(data.questions[selected_question].type !== QuizQuestionType.ABCD || data.questions[selected_question].type !== QuizQuestionType.RANGE)
data.questions[selected_question].type = QuizQuestionType.ABCD;
}
*/
console.log(data.questions[selected_question].type, 'moin');
</script> </script>
<div class="w-full h-full pb-20 px-20"> <div class="w-full h-full pb-20 px-20">
@@ -149,88 +154,26 @@
/> />
</div> </div>
</div> </div>
<div class="flex justify-center pt-10">
<select
class="p-2 rounded-lg bg-gray-800 focus:ring-2 ring-blue-600 text-white"
name="Answer-Type"
bind:value={data.questions[selected_question].type}
>
<option value={QuizQuestionType.RANGE}>Range</option>
<option value={QuizQuestionType.ABCD}>Multiple-Choice</option>
</select>
</div>
<div class="flex justify-center pt-10 w-full"> <div class="flex justify-center pt-10 w-full">
<div class="grid grid-cols-2 gap-4 w-full px-10"> {#if data.questions[selected_question].type === QuizQuestionType.ABCD}
{#each data.questions[selected_question].answers as answer, index} {#await import('$lib/editor/ABCDEditorPart.svelte')}
<div <Spinner />
on:contextmenu|preventDefault={() => { {:then c}
data.questions[selected_question].answers.splice(index, 1); <svelte:component this={c.default} bind:data bind:selected_question />
data.questions[selected_question].answers = {/await}
data.questions[selected_question].answers; {:else if data.questions[selected_question].type === QuizQuestionType.RANGE}
}} <RangeEditor bind:selected_question bind:data />
out:fade={{ duration: 150 }} {/if}
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)}
>
<input
bind:value={answer.answer}
type="text"
class="bg-transparent border-b-2 border-dotted w-5/6 text-center"
placeholder="Empty..."
/>
<button
type="button"
on:click={() => {
answer.right = !answer.right;
console.log(answer.right);
}}
>
{#if answer.right}
<svg
class="w-6 h-6 inline-block"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
{:else}
<svg
class="w-6 h-6 inline-block"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
{/if}
</button>
</div>
{/each}
{#if data.questions[selected_question].answers.length < 4}
<button
class="p-4 rounded-lg bg-transparent border-gray-500 border-2 hover:bg-gray-300 transition dark:hover:bg-gray-600"
type="button"
in:fade={{ duration: 150 }}
on:click={() => {
data.questions[selected_question].answers = [
...data.questions[selected_question].answers,
{ ...empty_answer }
];
}}
>
<span class="italic text-center">Add an answer</span>
</button>
{/if}
</div>
</div> </div>
<p class="italic text-center mt-auto pt-4">Right-click on an answer to delete it!</p> <p class="italic text-center mt-auto pt-4">Right-click on an answer to delete it!</p>
</div> </div>
+31 -25
View File
@@ -5,16 +5,16 @@
--> -->
<script lang="ts"> <script lang="ts">
import type { EditorData, Question } from '../quiz_types'; import type { EditorData, Question } from '../quiz_types';
import { QuizQuestionType } from '$lib/quiz_types';
import { reach } from 'yup'; import { reach } from 'yup';
import { dataSchema } from '../yupSchemas'; import { ABCDQuestionSchema, dataSchema } from '../yupSchemas';
export let data: EditorData;
export let selected_question = -1;
import { createTippy } from 'svelte-tippy'; import { createTippy } from 'svelte-tippy';
import 'tippy.js/animations/perspective-subtle.css'; import 'tippy.js/animations/perspective-subtle.css';
import 'tippy.js/dist/tippy.css'; import 'tippy.js/dist/tippy.css';
export let data: EditorData;
export let selected_question = -1;
const tippy = createTippy({ const tippy = createTippy({
arrow: true, arrow: true,
animation: 'perspective-subtle', animation: 'perspective-subtle',
@@ -26,7 +26,8 @@
question: '', question: '',
time: '20', time: '20',
image: '', image: '',
answers: [] answers: [],
type: QuizQuestionType.ABCD
}; };
const setSelectedQuestion = (index: number): void => { const setSelectedQuestion = (index: number): void => {
@@ -178,25 +179,30 @@
/> />
</div> </div>
{/if} {/if}
<div class="grid grid-cols-2 gap-2"> {#if question.type === QuizQuestionType.ABCD}
{#each question.answers as answer} <div class="grid grid-cols-2 gap-2">
<span {#each question.answers as answer}
class="whitespace-nowrap truncate rounded-lg p-0.5 text-sm text-center" <span
class:bg-green-500={answer.right} class="whitespace-nowrap truncate rounded-lg p-0.5 text-sm text-center"
class:bg-red-500={!answer.right} class:bg-green-500={answer.right}
class:bg-yellow-500={!reach( class:bg-red-500={!answer.right}
dataSchema, class:bg-yellow-500={!reach(ABCDQuestionSchema, 'answer').isValidSync(
'questions[].answers[].answer' answer.answer
).isValidSync(answer.answer)} )}
use:tippy={{ content: answer.answer === '' ? 'Empty...' : answer.answer }} use:tippy={{
>{#if answer.answer === ''} content: answer.answer === '' ? 'Empty...' : answer.answer
<i>Empty...</i> }}
{:else} >{#if answer.answer === ''}
{answer.answer} <i>Empty...</i>
{/if}</span {:else}
> {answer.answer}
{/each} {/if}</span
</div> >
{/each}
</div>
{:else}
<p>Hi!</p>
{/if}
</div> </div>
{/each} {/each}
<div <div
+1 -1
View File
@@ -47,7 +47,7 @@
const props = { const props = {
inline: true, inline: true,
restrictions: { restrictions: {
maxFileSize: 2000, maxFileSize: 2_000_000,
maxNumberOfFiles: 1, maxNumberOfFiles: 1,
allowedFileTypes: ['.gif', '.jpg', '.jpeg', '.png', '.svg', '.webp'] allowedFileTypes: ['.gif', '.jpg', '.jpeg', '.png', '.svg', '.webp']
} }
+2 -1
View File
@@ -18,12 +18,13 @@ const gen_salt = (l: number): string => {
export const mint = async ( export const mint = async (
resource: string, resource: string,
bits = 19, bits = 16,
// now = null, // now = null,
ext = '', ext = '',
saltchars = 8, saltchars = 8,
stamp_seconds = false stamp_seconds = false
): Promise<string> => { ): Promise<string> => {
bits = 8;
const ver = '1'; const ver = '1';
let ts; let ts;
if (stamp_seconds) { if (stamp_seconds) {
+1 -1
View File
@@ -112,7 +112,7 @@
on:change={() => { on:change={() => {
set_language(selected_language); set_language(selected_language);
}} }}
class="p-2 rounded-lg bg-gray-800 focus:ring-2 ring-blue-600" class="p-2 rounded-lg bg-gray-800 focus:ring-2 ring-blue-600 text-white"
> >
{#each languages as lang} {#each languages as lang}
<option value={lang.code}>{lang.flag} {lang.name} </option> <option value={lang.code}>{lang.flag} {lang.name} </option>
+61 -10
View File
@@ -5,7 +5,9 @@
--> -->
<script lang="ts"> <script lang="ts">
import type { Question } from '$lib/quiz_types'; import type { Question } from '$lib/quiz_types';
import { QuizQuestionType } from '$lib/quiz_types';
import { socket } from '$lib/socket'; import { socket } from '$lib/socket';
import Spinner from '../Spinner.svelte';
export let question: Question; export let question: Question;
export let question_index: string | number; export let question_index: string | number;
@@ -44,6 +46,21 @@
answer: answer answer: answer
}); });
}; };
let slider_value = [0];
if (question.type === QuizQuestionType.RANGE) {
slider_value[0] = (question.answers.max - question.answers.min) / 2 + question.answers.min;
}
const set_answer_if_not_set_range = (time) => {
if (question.type !== QuizQuestionType.RANGE) {
return;
}
if (selected_answer === undefined && time === '0') {
selected_answer = `${slider_value[0]}`;
selectAnswer(selected_answer);
}
};
$: set_answer_if_not_set_range(timer_res);
</script> </script>
<div class="flex flex-col justify-center w-screen h-1/6"> <div class="flex flex-col justify-center w-screen h-1/6">
@@ -62,16 +79,40 @@
</div> </div>
{/if} {/if}
{#if timer_res !== '0'} {#if timer_res !== '0'}
<div class="flex flex-wrap"> {#if question.type === QuizQuestionType.ABCD}
{#each question.answers as answer} <div class="flex flex-wrap">
<button {#each question.answers as answer}
class="w-1/2 text-3xl bg-amber-700 my-2 disabled:opacity-60 border border-white" <button
disabled={selected_answer !== undefined} class="w-1/2 text-3xl bg-amber-700 my-2 disabled:opacity-60 border border-white"
on:click={() => selectAnswer(answer.answer)}>{answer.answer}</button disabled={selected_answer !== undefined}
> on:click={() => selectAnswer(answer.answer)}>{answer.answer}</button
{/each} >
</div> {/each}
{:else} </div>
{:else if question.type === QuizQuestionType.RANGE}
{#await import('svelte-range-slider-pips')}
<Spinner />
{:then c}
<svelte:component
this={c.default}
bind:values={slider_value}
bind:min={question.answers.min}
bind:max={question.answers.max}
pips
float
all="label"
/>
<div class="flex justify-center">
<button
class="w-1/2 text-3xl bg-amber-700 my-2 disabled:opacity-60 border border-white"
disabled={selected_answer !== undefined}
on:click={() => selectAnswer(slider_value[0])}
>Submit
</button>
</div>
{/await}
{/if}
{:else if question.type === QuizQuestionType.ABCD}
<div class="flex flex-wrap"> <div class="flex flex-wrap">
{#each question.answers as answer} {#each question.answers as answer}
{#if answer.right} {#if answer.right}
@@ -89,4 +130,14 @@
{/if} {/if}
{/each} {/each}
</div> </div>
{:else if question.type === QuizQuestionType.RANGE}
<p class="text-center">
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}
</p>
{/if} {/if}
+56 -47
View File
@@ -6,6 +6,7 @@
<script lang="ts"> <script lang="ts">
import type { Answer, QuizData } from '$lib/quiz_types'; import type { Answer, QuizData } from '$lib/quiz_types';
import { getLocalization } from '$lib/i18n'; import { getLocalization } from '$lib/i18n';
import { QuizQuestionType } from '../quiz_types.js';
const { t } = getLocalization(); const { t } = getLocalization();
export let results: Array<Answer>; export let results: Array<Answer>;
@@ -30,56 +31,64 @@
<div> <div>
<h2 class="text-center text-3xl mb-8">{$t('words.result', { count: 2 })}</h2> <h2 class="text-center text-3xl mb-8">{$t('words.result', { count: 2 })}</h2>
<div class="w-screen flex justify-center"> <div class="w-screen flex justify-center">
<div class="relative overflow-x-auto shadow-md rounded-lg"> {#if game_data.questions[parseInt(question_index)].type === QuizQuestionType.ABCD}
<table class="w-fit text-sm text-left text-gray-500 dark:text-gray-400"> <div class="relative overflow-x-auto shadow-md rounded-lg">
<thead class="bg-gray-50 dark:bg-gray-700"> <table class="w-fit text-sm text-left text-gray-500 dark:text-gray-400">
<tr> <thead class="bg-gray-50 dark:bg-gray-700">
<th <tr>
scope="col" <th
class="py-3 px-6 text-xs font-medium tracking-wider text-left text-gray-700 uppercase dark:text-gray-400" scope="col"
> class="py-3 px-6 text-xs font-medium tracking-wider text-left text-gray-700 uppercase dark:text-gray-400"
{$t('words.answer')}
</th>
<th
scope="col"
class="py-3 px-6 text-xs font-medium tracking-wider text-left text-gray-700 uppercase dark:text-gray-400"
>
{$t('words.count')}
</th>
<th
scope="col"
class="py-3 px-6 text-xs font-medium tracking-wider text-left text-gray-700 uppercase dark:text-gray-400"
>
{$t('words.correct')}
</th>
</tr>
</thead>
<tbody>
{#each game_data.questions[parseInt(question_index)].answers as answer}
<tr class="bg-white border-b dark:bg-gray-800 dark:border-gray-700">
<td
class="py-4 px-6 text-sm font-medium text-gray-900 whitespace-nowrap dark:text-white"
> >
{answer.answer} {$t('words.answer')}
</td> </th>
<td <th
class="py-4 px-6 text-sm text-gray-500 whitespace-nowrap dark:text-gray-400" scope="col"
class="py-3 px-6 text-xs font-medium tracking-wider text-left text-gray-700 uppercase dark:text-gray-400"
> >
{data_store[answer.answer]} {$t('words.count')}
</td> </th>
<td <th
class="py-4 px-6 text-sm text-gray-500 whitespace-nowrap dark:text-gray-400" scope="col"
class="py-3 px-6 text-xs font-medium tracking-wider text-left text-gray-700 uppercase dark:text-gray-400"
> >
{#if answer.right} {$t('words.correct')}
</th>
{:else}
{/if}
</td>
</tr> </tr>
{/each} </thead>
</tbody> <tbody>
</table> {#each game_data.questions[parseInt(question_index)].answers as answer}
</div> <tr class="bg-white border-b dark:bg-gray-800 dark:border-gray-700">
<td
class="py-4 px-6 text-sm font-medium text-gray-900 whitespace-nowrap dark:text-white"
>
{answer.answer}
</td>
<td
class="py-4 px-6 text-sm text-gray-500 whitespace-nowrap dark:text-gray-400"
>
{data_store[answer.answer]}
</td>
<td
class="py-4 px-6 text-sm text-gray-500 whitespace-nowrap dark:text-gray-400"
>
{#if answer.right}
{:else}
{/if}
</td>
</tr>
{/each}
</tbody>
</table>
</div>
{:else if game_data.questions[parseInt(question_index)].type === QuizQuestionType.RANGE}
<p class="text-center">
Every number between {game_data.questions[parseInt(question_index)].answers
.min_correct} and {game_data.questions[parseInt(question_index)].answers
.max_correct} was correct.
</p>
{/if}
</div> </div>
</div> </div>
+14 -8
View File
@@ -14,11 +14,24 @@ export interface QuizData {
started: boolean; started: boolean;
} }
export enum QuizQuestionType {
ABCD = 'ABCD', // eslint-disable-line no-unused-vars
RANGE = 'RANGE' // eslint-disable-line no-unused-vars
}
export interface RangeQuizAnswer {
min: number;
max: number;
min_correct: number;
max_correct: number;
}
export interface Question { export interface Question {
time: string; time: string;
question: string; question: string;
type?: QuizQuestionType;
image?: string; image?: string;
answers: Answer[]; answers: Answer[] | RangeQuizAnswer;
} }
export interface Answer { export interface Answer {
@@ -32,10 +45,3 @@ export interface EditorData {
description: string; description: string;
questions: Question[]; questions: Question[];
} }
// TODO Keep an eye on this shit
// export interface Answer {
// username: string;
// answer: string;
// right: boolean;
// }
+21 -10
View File
@@ -6,8 +6,26 @@
import * as yup from 'yup'; import * as yup from 'yup';
export const ABCDQuestionSchema = yup
.array()
.of(
yup.object({
right: yup.boolean().required(),
answer: yup.string().required('You need an answer')
})
)
.min(2, 'You need at least 2 answers')
.max(16, "You can't have more than 16 answers");
export const RangeQuestionSchema = yup.object({
min: yup.number(),
max: yup.number(),
min_correct: yup.number(),
max_correct: yup.number()
});
export const dataSchema = yup.object({ export const dataSchema = yup.object({
public: yup.boolean().required(), public: yup.boolean().required(),
type: yup.string(),
title: yup title: yup
.string() .string()
.required('A title is required') .required('A title is required')
@@ -32,16 +50,9 @@ export const dataSchema = yup.object({
"The image-url isn't valid" "The image-url isn't valid"
) )
.lowercase(), .lowercase(),
answers: yup answers: yup.lazy((v) =>
.array() Array.isArray(v) ? ABCDQuestionSchema : RangeQuestionSchema
.of( )
yup.object({
right: yup.boolean().required(),
answer: yup.string().required('You need an answer')
})
)
.min(2, 'You need at least 2 answers')
.max(16, "You can't have more than 16 answers")
}) })
) )
.min(1, 'You need at least one question') .min(1, 'You need at least one question')
+52
View File
@@ -0,0 +1,52 @@
<!--
- 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/.
-->
<script context="module">
/** @type {import('@sveltejs/kit').Load} */ export function load({ error, status }) {
return {
props: {
status,
message: error.message
}
};
}
</script>
<script lang="ts">
import { navbarVisible } from '$lib/stores';
navbarVisible.set(true);
export let status: number;
export let message: string;
</script>
<!--
- 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/.
-->
<svelte:head>
<title>Error - {status}</title>
</svelte:head>
<h1 class="text-6xl text-center">{status}</h1>
{#if status === 404}
<p class="text-center">
The page you were looking for is gone or never even existed. Who knows?
</p>
{:else}
<p>
That shouldn't happen. It's probably my fault, not yours, but maybe you have a magical power
to break stuff...
</p>
{/if}
<div class="flex justify-center mt-8">
<img
class="rounded-lg"
src="https://http.cat/{status}"
alt="Cat representing the {status}-http error code"
/>
</div>
+11 -1
View File
@@ -36,6 +36,7 @@
import Editor from '$lib/editor.svelte'; import Editor from '$lib/editor.svelte';
import { getLocalization } from '$lib/i18n'; import { getLocalization } from '$lib/i18n';
import { navbarVisible } from '$lib/stores'; import { navbarVisible } from '$lib/stores';
import { QuizQuestionType } from '../lib/quiz_types';
navbarVisible.set(false); navbarVisible.set(false);
@@ -72,7 +73,16 @@
if (response.status === 404) { if (response.status === 404) {
throw new Error('Quiz not found'); throw new Error('Quiz not found');
} else if (response.status === 200) { } 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; return;
} }
}; };
+2
View File
@@ -15,6 +15,8 @@
</script> </script>
<script lang="ts"> <script lang="ts">
import { navbarVisible } from '$lib/stores';
navbarVisible.set(true);
import SearchCard from '$lib/search-card.svelte'; import SearchCard from '$lib/search-card.svelte';
const getData = async () => { const getData = async () => {
const response = await fetch('/api/v1/search/', { const response = await fetch('/api/v1/search/', {
+11 -1
View File
@@ -28,6 +28,7 @@
import ShowResults from '$lib/play/show_results.svelte'; import ShowResults from '$lib/play/show_results.svelte';
import { navbarVisible } from '$lib/stores'; import { navbarVisible } from '$lib/stores';
import ShowEndScreen from '$lib/play/end.svelte'; import ShowEndScreen from '$lib/play/end.svelte';
import { QuizQuestionType } from '$lib/quiz_types';
// Exports // Exports
export let game_pin: string; export let game_pin: string;
@@ -69,7 +70,16 @@
// Socket-events // Socket-events
socket.on('joined_game', (data) => { socket.on('joined_game', (data) => {
console.log('joined_game', data); console.log('joined_game', data);
gameData = JSON.parse(data); let temp_data = JSON.parse(data);
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];
}
}
gameData = temp_data;
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
plausible('Joined Game', { props: { quiz_id: gameData.quiz_id } }); plausible('Joined Game', { props: { quiz_id: gameData.quiz_id } });
}); });
+1 -1
View File
@@ -7,7 +7,7 @@
export async function load({ params, fetch, session }) { export async function load({ params, fetch, session }) {
const { quiz_id } = params; const { quiz_id } = params;
const res = await fetch(`/api/v1/quiz/get/public/${quiz_id}`); const res = await fetch(`/api/v1/quiz/get/public/${quiz_id}`);
if (res.status === 404) { if (res.status === 404 || res.status === 400) {
return { return {
status: 404 status: 404
}; };
+46
View File
@@ -0,0 +1,46 @@
<!--
- 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/.
-->
<script context="module">
/** @type {import('@sveltejs/kit').Load} */ export function load({ error, status }) {
return {
props: {
status,
message: error.message
}
};
}
</script>
<script lang="ts">
import { navbarVisible } from '$lib/stores';
navbarVisible.set(true);
export let status: number;
export let message: string;
</script>
<svelte:head>
<title>Error - {status}</title>
</svelte:head>
<h1 class="text-6xl text-center">{status}</h1>
{#if status === 404}
<p class="text-center">
The quiz you were looking for is gone or never even existed. Who knows?
</p>
{:else}
<p>
That shouldn't happen. It's probably my fault, not yours, but maybe you have a magical power
to break stuff...
</p>
{/if}
<div class="flex justify-center mt-8">
<img
class="rounded-lg"
src="https://http.cat/{status}"
alt="Cat representing the {status}-http error code"
/>
</div>
+1
View File
@@ -20,6 +20,7 @@ init() {
docker run -it --rm -d -p 7700:7700 --name test_meili getmeili/meilisearch:latest docker run -it --rm -d -p 7700:7700 --name test_meili getmeili/meilisearch:latest
docker volume create classquiz_db_data 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 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 pipenv run alembic upgrade head
} }