Merge pull request #139 from mawoka-myblock/135-add-a-new-question-type-voting-questions

This commit is contained in:
Mawoka
2022-10-04 16:48:21 +02:00
committed by GitHub
19 changed files with 512 additions and 155 deletions
+11 -2
View File
@@ -85,24 +85,33 @@ class RangeQuizAnswer(BaseModel):
max_correct: int max_correct: int
class VotingQuizAnswer(BaseModel):
answer: str
image: str | None = None
color: str | None
class QuizQuestionType(str, Enum): class QuizQuestionType(str, Enum):
ABCD = "ABCD" ABCD = "ABCD"
RANGE = "RANGE" RANGE = "RANGE"
VOTING = "VOTING"
class QuizQuestion(BaseModel): class QuizQuestion(BaseModel):
question: str question: str
time: str # in Secs time: str # in Secs
type: None | QuizQuestionType = QuizQuestionType.ABCD type: None | QuizQuestionType = QuizQuestionType.ABCD
answers: list[ABCDQuizAnswer] | RangeQuizAnswer answers: list[ABCDQuizAnswer] | RangeQuizAnswer | list[VotingQuizAnswer]
image: str | None = None image: str | None = None
@validator("answers") @validator("answers")
def answers_not_none_if_abcd_type(cls, v, values): def answers_not_none_if_abcd_type(cls, v, values):
if values["type"] == QuizQuestionType.ABCD and len(v) == 0: if values["type"] == QuizQuestionType.ABCD and type(v[0]) != ABCDQuizAnswer:
raise ValueError("Answers can't be none if type is ABCD") raise ValueError("Answers can't be none if type is ABCD")
if values["type"] == QuizQuestionType.RANGE and type(v) != RangeQuizAnswer: if values["type"] == QuizQuestionType.RANGE and type(v) != RangeQuizAnswer:
raise ValueError("Answer must be from type RangeQuizAnswer if type is RANGE") raise ValueError("Answer must be from type RangeQuizAnswer if type is RANGE")
if values["type"] == QuizQuestionType.VOTING and type(v[0]) != VotingQuizAnswer:
raise ValueError("Answer must be from type VotingQuizAnswer if type is VOTING")
return v return v
+2 -1
View File
@@ -17,6 +17,7 @@ from classquiz.db.models import (
RangeQuizAnswer, RangeQuizAnswer,
ABCDQuizAnswer, ABCDQuizAnswer,
QuizQuestionType, QuizQuestionType,
VotingQuizAnswer,
) )
from classquiz.auth import check_api_key from classquiz.auth import check_api_key
from classquiz.socket_server import ReturnQuestion, sio from classquiz.socket_server import ReturnQuestion, sio
@@ -35,7 +36,7 @@ class _ABCDQuizAnswer(ABCDQuizAnswer):
class _QuizQuestion(QuizQuestion): class _QuizQuestion(QuizQuestion):
answers: list[ABCDQuizAnswer] | RangeQuizAnswer answers: list[ABCDQuizAnswer] | RangeQuizAnswer | list[VotingQuizAnswer]
class _GetLivePlayGame(PlayGame): class _GetLivePlayGame(PlayGame):
+18 -9
View File
@@ -11,7 +11,7 @@ import socketio
from cryptography.fernet import Fernet from cryptography.fernet import Fernet
from classquiz.config import redis, settings from classquiz.config import redis, settings
from classquiz.db.models import PlayGame, QuizQuestionType, GameSession, GamePlayer, QuizQuestion from classquiz.db.models import PlayGame, QuizQuestionType, GameSession, GamePlayer, QuizQuestion, VotingQuizAnswer
from pydantic import BaseModel, ValidationError, validator from pydantic import BaseModel, ValidationError, validator
from datetime import datetime from datetime import datetime
@@ -113,8 +113,10 @@ async def join_game(sid: str, data: dict):
{"username": data.username, "sid": sid}, {"username": data.username, "sid": sid},
room=redis_res.admin, room=redis_res.admin,
) )
lol = fernet.encrypt(datetime.now().isoformat().encode("utf-8")).decode("utf-8") # +++ Time-Sync +++
await sio.emit("time_sync", lol, room=sid) encrypted_datetime = fernet.encrypt(datetime.now().isoformat().encode("utf-8")).decode("utf-8")
await sio.emit("time_sync", encrypted_datetime, room=sid)
# --- Time-Sync ---
sio.enter_room(sid, data.game_pin) sio.enter_room(sid, data.game_pin)
@@ -183,14 +185,16 @@ class RangeQuizAnswerWithoutSolution(BaseModel):
class ReturnQuestion(QuizQuestion): class ReturnQuestion(QuizQuestion):
answers: list[ABCDQuizAnswerWithoutSolution] | RangeQuizAnswerWithoutSolution answers: list[ABCDQuizAnswerWithoutSolution] | RangeQuizAnswerWithoutSolution | list[VotingQuizAnswer]
@validator("answers") @validator("answers")
def answers_not_none_if_abcd_type(cls, v, values): def answers_not_none_if_abcd_type(cls, v, values):
if values["type"] == QuizQuestionType.ABCD and len(v) == 0: if values["type"] == QuizQuestionType.ABCD and type(v[0]) != ABCDQuizAnswerWithoutSolution:
raise ValueError("Answers can't be none if type is ABCD") raise ValueError("Answers can't be none if type is ABCD")
if values["type"] == QuizQuestionType.RANGE and type(v) != RangeQuizAnswerWithoutSolution: if values["type"] == QuizQuestionType.RANGE and type(v) != RangeQuizAnswerWithoutSolution:
raise ValueError("Answer must be from type RangeQuizAnswer if type is RANGE") raise ValueError("Answer must be from type RangeQuizAnswer if type is RANGE")
if values["type"] == QuizQuestionType.VOTING and type(v[0]) != VotingQuizAnswer:
print("Answer must be from type VotingQuizAnswer if type is VOTING")
return v return v
@@ -204,13 +208,17 @@ async def set_question_number(sid, data: str):
game_data.current_question = int(float(data)) game_data.current_question = int(float(data))
await redis.set(f"game:{session['game_pin']}", game_data.json()) await redis.set(f"game:{session['game_pin']}", game_data.json())
await redis.set(f"game:{session['game_pin']}:current_time", datetime.now().isoformat()) await redis.set(f"game:{session['game_pin']}:current_time", datetime.now().isoformat())
# print(game_data.dict(include={"questions"})["questions"][int(float(data))])
temp_return = game_data.dict(include={"questions"})["questions"][int(float(data))]
if game_data.questions[int(float(data))].type == QuizQuestionType.VOTING:
for i in range(len(temp_return["answers"])):
temp_return["answers"][i] = VotingQuizAnswer(**temp_return["answers"][i])
print(temp_return)
await sio.emit( await sio.emit(
"set_question_number", "set_question_number",
{ {
"question_index": int(float(data)), "question_index": int(float(data)),
"question": ReturnQuestion( "question": ReturnQuestion(**temp_return).dict(),
**game_data.dict(include={"questions"})["questions"][int(float(data))]
).dict(),
}, },
room=game_pin, room=game_pin,
) )
@@ -258,6 +266,8 @@ async def submit_answer(sid: str, data: dict):
<= game_data.questions[int(float(data.question_index))].answers.max_correct <= game_data.questions[int(float(data.question_index))].answers.max_correct
): ):
answer_right = True answer_right = True
elif game_data.questions[int(float(data.question_index))].type == QuizQuestionType.VOTING:
answer_right = False
else: else:
raise NotImplementedError raise NotImplementedError
latency = int(float((await sio.get_session(sid))["ping"])) latency = int(float((await sio.get_session(sid))["ping"]))
@@ -317,7 +327,6 @@ async def submit_answer(sid: str, data: dict):
async def get_final_results(sid: str, _data: dict): async def get_final_results(sid: str, _data: dict):
session: dict = await sio.get_session(sid) session: dict = await sio.get_session(sid)
game_data = PlayGame(**json.loads(await redis.get(f"game:{session['game_pin']}"))) game_data = PlayGame(**json.loads(await redis.get(f"game:{session['game_pin']}")))
results = {}
if not session["admin"]: if not session["admin"]:
return return
results = await generate_final_results(game_data, session["game_pin"]) results = await generate_final_results(game_data, session["game_pin"])
+26 -64
View File
@@ -77,7 +77,7 @@ devDependencies:
'@sentry/tracing': 7.11.1 '@sentry/tracing': 7.11.1
'@sveltejs/adapter-auto': 1.0.0-next.80 '@sveltejs/adapter-auto': 1.0.0-next.80
'@sveltejs/adapter-node': 1.0.0-next.86 '@sveltejs/adapter-node': 1.0.0-next.86
'@sveltejs/kit': 1.0.0-next.504_svelte@3.49.0+vite@3.1.2 '@sveltejs/kit': 1.0.0-next.508_svelte@3.49.0+vite@3.1.2
'@tailwindcss/typography': 0.5.4_tailwindcss@3.1.8 '@tailwindcss/typography': 0.5.4_tailwindcss@3.1.8
'@types/canvas-confetti': 1.4.3 '@types/canvas-confetti': 1.4.3
'@types/cookie': 0.5.1 '@types/cookie': 0.5.1
@@ -579,10 +579,10 @@ packages:
- supports-color - supports-color
dev: true dev: true
/@sveltejs/kit/1.0.0-next.504_svelte@3.49.0+vite@3.1.2: /@sveltejs/kit/1.0.0-next.508_svelte@3.49.0+vite@3.1.2:
resolution: resolution:
{ {
integrity: sha512-KrhlSHT3aCVnhRgUoN6aGIjIw3nWEdNwfoZcE1x65F5D7Ju/K9D8dQwyal9v0aBAZyN9nFuXxsYGaMLE9sppfw== integrity: sha512-qUnuhuL82meE0lSwrsS/FIvtDoE83Au4SxVLCim3FzoCnJc7bFZ9vYRbRUYxAjfYKRMjKVUrp9iHFObn9LmnUg==
} }
engines: { node: '>=16.14' } engines: { node: '>=16.14' }
hasBin: true hasBin: true
@@ -598,13 +598,12 @@ packages:
kleur: 4.1.5 kleur: 4.1.5
magic-string: 0.26.3 magic-string: 0.26.3
mime: 3.0.0 mime: 3.0.0
node-fetch: 3.2.10
sade: 1.8.1 sade: 1.8.1
set-cookie-parser: 2.5.1 set-cookie-parser: 2.5.1
sirv: 2.0.2 sirv: 2.0.2
svelte: 3.49.0 svelte: 3.49.0
tiny-glob: 0.2.9 tiny-glob: 0.2.9
undici: 5.9.1 undici: 5.11.0
vite: 3.1.2_sass@1.54.4 vite: 3.1.2_sass@1.54.4
transitivePeerDependencies: transitivePeerDependencies:
- diff-match-patch - diff-match-patch
@@ -1398,6 +1397,16 @@ packages:
} }
dev: true dev: true
/busboy/1.6.0:
resolution:
{
integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==
}
engines: { node: '>=10.16.0' }
dependencies:
streamsearch: 1.1.0
dev: true
/callsites/3.1.0: /callsites/3.1.0:
resolution: resolution:
{ {
@@ -1717,14 +1726,6 @@ packages:
css-tree: 1.1.3 css-tree: 1.1.3
dev: true dev: true
/data-uri-to-buffer/4.0.0:
resolution:
{
integrity: sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA==
}
engines: { node: '>= 12' }
dev: true
/debug/4.3.4: /debug/4.3.4:
resolution: resolution:
{ {
@@ -2503,17 +2504,6 @@ packages:
svelte: 3.49.0 svelte: 3.49.0
dev: true dev: true
/fetch-blob/3.2.0:
resolution:
{
integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==
}
engines: { node: ^12.20 || >= 14.13 }
dependencies:
node-domexception: 1.0.0
web-streams-polyfill: 3.2.1
dev: true
/file-entry-cache/6.0.1: /file-entry-cache/6.0.1:
resolution: resolution:
{ {
@@ -2570,16 +2560,6 @@ packages:
} }
dev: true dev: true
/formdata-polyfill/4.0.10:
resolution:
{
integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==
}
engines: { node: '>=12.20.0' }
dependencies:
fetch-blob: 3.2.0
dev: true
/fraction.js/4.2.0: /fraction.js/4.2.0:
resolution: resolution:
{ {
@@ -3358,14 +3338,6 @@ packages:
} }
dev: true dev: true
/node-domexception/1.0.0:
resolution:
{
integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==
}
engines: { node: '>=10.5.0' }
dev: true
/node-fetch/2.6.7: /node-fetch/2.6.7:
resolution: resolution:
{ {
@@ -3381,18 +3353,6 @@ packages:
whatwg-url: 5.0.0 whatwg-url: 5.0.0
dev: true dev: true
/node-fetch/3.2.10:
resolution:
{
integrity: sha512-MhuzNwdURnZ1Cp4XTazr69K0BTizsBroX7Zx3UgDSVcZYKF/6p0CBe4EUb/hLqmzVhl0UpYfgRljQ4yxE+iCxA==
}
engines: { node: ^12.20.0 || ^14.13.1 || >=16.0.0 }
dependencies:
data-uri-to-buffer: 4.0.0
fetch-blob: 3.2.0
formdata-polyfill: 4.0.10
dev: true
/node-gyp-build/4.5.0: /node-gyp-build/4.5.0:
resolution: resolution:
{ {
@@ -4540,6 +4500,14 @@ packages:
deprecated: 'Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility' deprecated: 'Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility'
dev: true dev: true
/streamsearch/1.1.0:
resolution:
{
integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==
}
engines: { node: '>=10.0.0' }
dev: true
/string-width/4.2.3: /string-width/4.2.3:
resolution: resolution:
{ {
@@ -4960,12 +4928,14 @@ packages:
} }
dev: true dev: true
/undici/5.9.1: /undici/5.11.0:
resolution: resolution:
{ {
integrity: sha512-6fB3a+SNnWEm4CJbgo0/CWR8RGcOCQP68SF4X0mxtYTq2VNN8T88NYrWVBAeSX+zb7bny2dx2iYhP3XHi00omg== integrity: sha512-oWjWJHzFet0Ow4YZBkyiJwiK5vWqEYoH7BINzJAJOLedZ++JpAlCbUktW2GQ2DS2FpKmxD/JMtWUUWl1BtghGw==
} }
engines: { node: '>=12.18' } engines: { node: '>=12.18' }
dependencies:
busboy: 1.6.0
dev: true dev: true
/update-browserslist-db/1.0.5_browserslist@4.21.3: /update-browserslist-db/1.0.5_browserslist@4.21.3:
@@ -5057,14 +5027,6 @@ packages:
pbf: 3.2.1 pbf: 3.2.1
dev: true dev: true
/web-streams-polyfill/3.2.1:
resolution:
{
integrity: sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==
}
engines: { node: '>= 8' }
dev: true
/webidl-conversions/3.0.1: /webidl-conversions/3.0.1:
resolution: resolution:
{ {
+29 -11
View File
@@ -114,8 +114,8 @@
{#if selected_question + 1 === quiz_data.questions.length && timer_res === '0' && question_results !== null} {#if selected_question + 1 === quiz_data.questions.length && timer_res === '0' && question_results !== null}
{#if JSON.stringify(final_results) === JSON.stringify([null])} {#if JSON.stringify(final_results) === JSON.stringify([null])}
<button on:click={get_final_results} class="admin-button" <button on:click={get_final_results} class="admin-button"
>Get final results</button >Get final results
> </button>
{/if} {/if}
{:else if timer_res === '0' || selected_question === -1} {:else if timer_res === '0' || selected_question === -1}
{#if (selected_question + 1 !== quiz_data.questions.length && question_results !== null) || selected_question === -1} {#if (selected_question + 1 !== quiz_data.questions.length && question_results !== null) || selected_question === -1}
@@ -129,13 +129,13 @@
{/if} {/if}
{#if question_results === null && selected_question !== -1} {#if question_results === null && selected_question !== -1}
<button on:click={get_question_results} class="admin-button" <button on:click={get_question_results} class="admin-button"
>Show results</button >Show results
> </button>
{/if} {/if}
{:else if selected_question !== -1} {:else if selected_question !== -1}
<button on:click={show_solutions} class="admin-button" <button on:click={show_solutions} class="admin-button"
>Stop time and show solutions</button >Stop time and show solutions
> </button>
{:else} {:else}
<!-- <button <!-- <button
on:click={() => { on:click={() => {
@@ -181,20 +181,28 @@
</div> </div>
{/if} {/if}
{#if game_mode === 'kahoot'} {#if game_mode === 'kahoot'}
{#if quiz_data.questions[selected_question].type === QuizQuestionType.ABCD} {#if quiz_data.questions[selected_question].type === QuizQuestionType.ABCD || quiz_data.questions[selected_question].type === QuizQuestionType.VOTING}
<div class="grid grid-cols-2 gap-2 w-full p-4"> <div class="grid grid-cols-2 gap-2 w-full p-4">
{#each quiz_data.questions[selected_question].answers as answer, i} {#each quiz_data.questions[selected_question].answers as answer, i}
<div <div
class="rounded-lg h-fit flex" class="rounded-lg h-fit flex"
style="background-color: {answer.color ?? '#B45309'}" style="background-color: {answer.color ?? '#B45309'}"
class:opacity-50={!answer.right && timer_res === '0'} class:opacity-50={!answer.right &&
timer_res === '0' &&
quiz_data.questions[selected_question].type !==
QuizQuestionType.ABCD}
> >
<img class="w-14 inline-block pl-4" alt="icon" src={kahoot_icons[i]} /> <img class="w-14 inline-block pl-4" alt="icon" src={kahoot_icons[i]} />
<span <span
class="text-center text-2xl px-2 py-4 w-full text-black" class="text-center text-2xl px-2 py-4 w-full text-black"
class:text-4xl={answer.right && timer_res === '0'} class:text-4xl={answer.right &&
class:underline={answer.right && timer_res === '0'} timer_res === '0' &&
>{answer.answer}</span quiz_data.questions[selected_question].type !==
QuizQuestionType.ABCD}
class:underline={answer.right &&
timer_res === '0' &&
quiz_data.questions[selected_question].type !==
QuizQuestionType.ABCD}>{answer.answer}</span
> >
<span class="pl-4 w-10" /> <span class="pl-4 w-10" />
</div> </div>
@@ -221,6 +229,16 @@
<h1 class="text-3xl">{$t('admin_page.no_answers')}</h1> <h1 class="text-3xl">{$t('admin_page.no_answers')}</h1>
</div> </div>
{/if} {/if}
{:else if quiz_data.questions[selected_question].type === QuizQuestionType.VOTING}
{#await import('$lib/play/admin/voting_results.svelte')}
<Spinner />
{:then c}
<svelte:component
this={c.default}
bind:data={question_results}
bind:question={quiz_data.questions[selected_question]}
/>
{/await}
{:else} {:else}
{#await import('$lib/play/admin/results.svelte')} {#await import('$lib/play/admin/results.svelte')}
<Spinner /> <Spinner />
@@ -247,6 +247,18 @@
numbers between {question.answers.min} and {question numbers between {question.answers.min} and {question
.answers.max} can be selected. .answers.max} can be selected.
</p> </p>
{:else if question.type === QuizQuestionType.VOTING}
<div class="grid grid-cols-2 gap-4 m-4 p-6">
{#each question.answers as answer}
<div
class="p-1 rounded-lg py-4 dark:bg-gray-500 bg-gray-300"
>
<h4 class="text-center">
{answer.answer}
</h4>
</div>
{/each}
</div>
{/if} {/if}
</div> </div>
</SwiperSlide> </SwiperSlide>
@@ -66,10 +66,10 @@
<!-- <RangeSlider bind:value={range_arr} bind:min={answer.min} bind:max={answer.max} range={true} slider={lol} /> --> <!-- <RangeSlider bind:value={range_arr} bind:min={answer.min} bind:max={answer.max} range={true} slider={lol} /> -->
{#await import('svelte-range-slider-pips')} {#await import('svelte-range-slider-pips')}
<Spinner /> <Spinner my_20={false} />
{:then c} {:then c}
{#await sleep(100)} {#await sleep(100)}
<Spinner /> <Spinner my_20={false} />
{:then _} {:then _}
<svelte:component <svelte:component
this={c.default} this={c.default}
@@ -0,0 +1,88 @@
<!--
- 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, VotingAnswer } from '../quiz_types';
import { fade } from 'svelte/transition';
import { reach } from 'yup';
import { getLocalization } from '$lib/i18n';
import { VotingQuestionSchema } from '$lib/yupSchemas';
const { t } = getLocalization();
const empty_answer: VotingAnswer = {
answer: '',
image: undefined
};
export let selected_question: number;
export let data: EditorData;
try {
if (typeof data.questions[selected_question].answers[0].right === 'boolean') {
data.questions[selected_question].answers = [];
}
// eslint-disable-next-line no-empty
} catch {}
/*console.log(data.questions[selected_question].answers, 'moIn!', data.questions[selected_question].answers.length);
onMount(() => {
for (let i = 0; i < data.questions[selected_question].answers; i++) {
console.log(data.questions[selected_question].answers[i], 'iterate');
data.questions[selected_question].answers[i].right = undefined;
}
});*/
</script>
<div class="grid grid-cols-2 gap-4 w-full px-10">
{#if Array.isArray(data.questions[selected_question].answers)}
{#each data.questions[selected_question].answers as answer, index}
<div
out:fade={{ duration: 150 }}
class="p-4 rounded-lg flex justify-center w-full transition"
class:bg-yellow-500={!reach(VotingQuestionSchema, 'answer').isValidSync(
answer.answer
)}
class:dark:bg-gray-500={answer.answer}
class:bg-gray-300={answer.answer}
>
<input
bind:value={answer.answer}
type="text"
on:contextmenu|preventDefault={() => {
data.questions[selected_question].answers.splice(index, 1);
data.questions[selected_question].answers =
data.questions[selected_question].answers;
}}
class="border-b-2 border-dotted w-5/6 text-center rounded-lg"
style="background-color: {answer.color ?? 'transparent'}"
placeholder="Empty..."
/>
<input
class="rounded-lg p-1"
type="color"
bind:value={answer.color}
on:contextmenu|preventDefault={() => {
answer.color = null;
}}
/>
</div>
{/each}
{/if}
{#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">{$t('editor_page.add_an_answer')}</span>
</button>
{/if}
</div>
+10 -3
View File
@@ -96,11 +96,11 @@
use:tippy={{ content: "Click to learn why it's loading so long." }} use:tippy={{ content: "Click to learn why it's loading so long." }}
class="cursor-help" class="cursor-help"
> >
<Spinner /> <Spinner my_20={false} />
</a> </a>
{:else} {:else}
{#await import('$lib/editor/uploader.svelte')} {#await import('$lib/editor/uploader.svelte')}
<Spinner /> <Spinner my_20={false} />
{:then c} {:then c}
<svelte:component <svelte:component
this={c.default} this={c.default}
@@ -146,17 +146,24 @@
> >
<option value={QuizQuestionType.RANGE}>{$t('words.range')}</option> <option value={QuizQuestionType.RANGE}>{$t('words.range')}</option>
<option value={QuizQuestionType.ABCD}>{$t('words.multiple_choice')}</option> <option value={QuizQuestionType.ABCD}>{$t('words.multiple_choice')}</option>
<option value={QuizQuestionType.VOTING}>{$t('words.voting')}</option>
</select> </select>
</div> </div>
<div class="flex justify-center pt-10 w-full"> <div class="flex justify-center pt-10 w-full">
{#if data.questions[selected_question].type === QuizQuestionType.ABCD} {#if data.questions[selected_question].type === QuizQuestionType.ABCD}
{#await import('$lib/editor/ABCDEditorPart.svelte')} {#await import('$lib/editor/ABCDEditorPart.svelte')}
<Spinner /> <Spinner my_20={false} />
{:then c} {:then c}
<svelte:component this={c.default} bind:data bind:selected_question /> <svelte:component this={c.default} bind:data bind:selected_question />
{/await} {/await}
{:else if data.questions[selected_question].type === QuizQuestionType.RANGE} {:else if data.questions[selected_question].type === QuizQuestionType.RANGE}
<RangeEditor bind:selected_question bind:data /> <RangeEditor bind:selected_question bind:data />
{:else if data.questions[selected_question].type === QuizQuestionType.VOTING}
{#await import('$lib/editor/VotingEditorPart.svelte')}
<Spinner my_20={false} />
{:then c}
<svelte:component this={c.default} bind:data bind:selected_question />
{/await}
{/if} {/if}
</div> </div>
<p class="italic text-center mt-auto pt-4">{$t('editor_page.right_click_to_delete')}</p> <p class="italic text-center mt-auto pt-4">{$t('editor_page.right_click_to_delete')}</p>
+2 -2
View File
@@ -67,11 +67,11 @@
</div> </div>
{:else if pow_data === undefined} {:else if pow_data === undefined}
<a href="/docs/pow" target="_blank" class="cursor-help"> <a href="/docs/pow" target="_blank" class="cursor-help">
<Spinner /> <Spinner my_20={false} />
</a> </a>
{:else} {:else}
{#await import('$lib/editor/uploader.svelte')} {#await import('$lib/editor/uploader.svelte')}
<Spinner /> <Spinner my_20={false} />
{:then c} {:then c}
<svelte:component <svelte:component
this={c.default} this={c.default}
+24
View File
@@ -207,6 +207,30 @@
and {question.answers.max_correct} are correct, where numbers between {question and {question.answers.max_correct} are correct, where numbers between {question
.answers.min} and {question.answers.max} can be selected. .answers.min} and {question.answers.max} can be selected.
</p> </p>
{:else if question.type === QuizQuestionType.VOTING}
{#if Array.isArray(question.answers)}
<div class="grid grid-cols-2 gap-2">
{#each question.answers as answer}
<span
class="whitespace-nowrap truncate rounded-lg p-0.5 text-sm text-center border border-gray-700"
class:dark:bg-gray-500={answer.answer}
class:bg-gray-300={answer.answer}
class:bg-yellow-500={!reach(
ABCDQuestionSchema,
'answer'
).isValidSync(answer.answer)}
use:tippy={{
content: answer.answer === '' ? 'Empty...' : answer.answer
}}
>{#if answer.answer === ''}
<i>Empty...</i>
{:else}
{answer.answer}
{/if}</span
>
{/each}
</div>
{/if}
{:else} {:else}
<p>Unknown Question Type (shouldn't happen)</p> <p>Unknown Question Type (shouldn't happen)</p>
{/if} {/if}
@@ -0,0 +1,50 @@
<!--
- 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 { Question } from '$lib/quiz_types';
export let data;
export let question: Question;
let quiz_answers = [];
for (const i of question.answers) {
quiz_answers.push(i.answer);
}
console.log('mounted', question);
let sorted_data = {};
for (const i of quiz_answers) {
sorted_data[i] = 0;
}
for (const i of data) {
sorted_data[i.answer] += 1;
}
</script>
<div class="flex justify-center h-full w-full">
<div
class="m-auto grid grid-rows-3 w-fit gap-4"
style="grid-template-columns: repeat({quiz_answers.length}, minmax(0, 1fr));"
>
{#each quiz_answers as answer}
<span class="text-center self-end"
>{#if sorted_data[answer] > 0}{sorted_data[answer]}{/if}</span
>
{/each}
{#each quiz_answers as answer}
<div
class="w-20 bg-black self-end flex justify-center"
style="height: {(sorted_data[answer] * 20) / data.length}rem"
/>
{/each}
{#each quiz_answers as answer}
<div class="w-20">
<p class="-rotate-45">{answer}</p>
</div>
{/each}
</div>
</div>
+8 -1
View File
@@ -113,7 +113,7 @@
</div> </div>
{/if} {/if}
{#if timer_res !== '0'} {#if timer_res !== '0'}
{#if question.type === QuizQuestionType.ABCD} {#if question.type === QuizQuestionType.ABCD || QuizQuestionType.VOTING}
{#if game_mode === 'normal'} {#if game_mode === 'normal'}
<div class="grid grid-cols-2 gap-2 w-full p-4"> <div class="grid grid-cols-2 gap-2 w-full p-4">
{#each question.answers as answer} {#each question.answers as answer}
@@ -202,4 +202,11 @@
{/if} {/if}
</p> </p>
{/if} {/if}
<!--{:else if question.type === QuizQuestionType.VOTING}
{#await import('$lib/play/admin/voting_results.svelte')}
<Spinner />
{:then c}
<svelte:component this={c.default} bind:data={question_results}
bind:question={quiz_data.questions[selected_question]} />
{/await}-->
{/if} {/if}
+17 -1
View File
@@ -91,7 +91,8 @@
{/if} {/if}
</div> </div>
{/if} {/if}
{:else if timer_res === '0'} {:else if question.type === QuizQuestionType.RANGE}
{#if timer_res === '0'}
{#await import('svelte-range-slider-pips')} {#await import('svelte-range-slider-pips')}
<Spinner /> <Spinner />
{:then c} {:then c}
@@ -135,4 +136,19 @@
</div> </div>
{/await} {/await}
{/if} {/if}
{:else if question.type === QuizQuestionType.VOTING}
{#each question.answers as answer, i}
<button
disabled={selected_answer !== undefined || timer_res === '0'}
class="p-2 rounded-lg flex justify-center w-full transition bg-amber-300 my-5 disabled:grayscale text-black"
on:click={() => {
selected_answer = i;
timer_res = '0';
}}>{answer.answer}</button
>
{/each}
{#if timer_res === '0'}
<p>No correct answers, since this is a poll-question</p>
{/if}
{/if}
</div> </div>
+8 -2
View File
@@ -18,7 +18,8 @@ export interface QuizData {
export enum QuizQuestionType { export enum QuizQuestionType {
ABCD = 'ABCD', // eslint-disable-line no-unused-vars ABCD = 'ABCD', // eslint-disable-line no-unused-vars
RANGE = 'RANGE' // eslint-disable-line no-unused-vars RANGE = 'RANGE', // eslint-disable-line no-unused-vars
VOTING = 'VOTING' // eslint-disable-line no-unused-vars
} }
export interface RangeQuizAnswer { export interface RangeQuizAnswer {
@@ -33,7 +34,7 @@ export interface Question {
question: string; question: string;
type?: QuizQuestionType; type?: QuizQuestionType;
image?: string; image?: string;
answers: Answer[] | RangeQuizAnswer; answers: Answer[] | RangeQuizAnswer | VotingAnswer[];
} }
export interface Answer { export interface Answer {
@@ -41,6 +42,11 @@ export interface Answer {
answer: string; answer: string;
color?: string; color?: string;
} }
export interface VotingAnswer {
answer: string;
image?: string;
color?: string;
}
export interface EditorData { export interface EditorData {
public: boolean; public: boolean;
+22 -3
View File
@@ -17,6 +17,17 @@ export const ABCDQuestionSchema = yup
.min(2, 'You need at least 2 answers') .min(2, 'You need at least 2 answers')
.max(16, "You can't have more than 16 answers"); .max(16, "You can't have more than 16 answers");
export const VotingQuestionSchema = yup
.array()
.of(
yup.object({
answer: yup.string().required('You need an answer'),
image: yup.string().optional().nullable()
})
)
.min(2, 'You need at least 2 answers')
.max(16, "You can't have more than 16 answers");
export const RangeQuestionSchema = yup.object({ export const RangeQuestionSchema = yup.object({
min: yup.number(), min: yup.number(),
max: yup.number(), max: yup.number(),
@@ -50,9 +61,17 @@ export const dataSchema = yup.object({
"The image-url isn't valid" "The image-url isn't valid"
) )
.lowercase(), .lowercase(),
answers: yup.lazy((v) => answers: yup.lazy((v) => {
Array.isArray(v) ? ABCDQuestionSchema : RangeQuestionSchema if (Array.isArray(v)) {
) if (typeof v[0].right === 'boolean') {
return ABCDQuestionSchema;
} else {
return VotingQuestionSchema;
}
} else {
return RangeQuestionSchema;
}
})
}) })
) )
.min(1, 'You need at least one question') .min(1, 'You need at least one question')
+1 -12
View File
@@ -9,6 +9,7 @@
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'; import { QuizQuestionType } from '$lib/quiz_types';
import type { Question } from '$lib/quiz_types';
navbarVisible.set(false); navbarVisible.set(false);
@@ -21,18 +22,6 @@
questions: Question[]; questions: Question[];
} }
interface Question {
question: string;
time: string;
type?: QuizQuestionType;
answers: Answer[];
}
interface Answer {
right: boolean;
answer: string;
}
let responseData = { let responseData = {
open: false open: false
}; };
+129
View File
@@ -0,0 +1,129 @@
<!--
- 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 { navbarVisible } from '$lib/stores';
navbarVisible.set(false);
const data = [
{
username: 'Mawoka',
answer: 'A very long answe-thing',
right: false,
time_taken: 1681.631,
score: 0
},
{
username: 'Mawoka',
answer: 'A very long answe-thing',
right: false,
time_taken: 1681.631,
score: 0
},
{
username: 'Mawoka',
answer: 'A very long answe-thing',
right: false,
time_taken: 1681.631,
score: 0
},
{
username: 'Mawoka',
answer: 'A very long answe-thing',
right: false,
time_taken: 1681.631,
score: 0
},
{
username: 'Mawoka',
answer: 'A very long answe-thing',
right: false,
time_taken: 1681.631,
score: 0
},
{
username: 'Mawoka',
answer: 'A very long answe-thing',
right: false,
time_taken: 1681.631,
score: 0
},
{
username: 'Mawoka',
answer: 'A very long answe-thing',
right: false,
time_taken: 1681.631,
score: 0
},
{
username: 'Mawoka',
answer: 'A very long answe-thing',
right: false,
time_taken: 1681.631,
score: 0
},
{
username: 'Mawoka',
answer: 'A very long answe-thing',
right: false,
time_taken: 1681.631,
score: 0
},
{
username: 'Mawoka',
answer: 'A very long answe-thing',
right: false,
time_taken: 1681.631,
score: 0
},
{
username: 'dewads',
answer: 'A very long answe-thing',
right: false,
time_taken: 1681.631,
score: 0
},
{
username: 'dsadsa',
answer: 'der',
right: false,
time_taken: 2613.532,
score: 0
}
];
const quiz_answers = ['A very long answe-thing', 'dieter', 'moin', 'der'];
let sorted_data = {};
for (const i of quiz_answers) {
sorted_data[i] = 0;
}
for (const i of data) {
sorted_data[i.answer] += 1;
}
</script>
<div class="flex justify-center h-screen w-full">
<div
class="m-auto grid grid-rows-3 w-fit gap-4"
style="grid-template-columns: repeat({quiz_answers.length}, minmax(0, 1fr));"
>
{#each quiz_answers as answer}
<span class="text-center self-end"
>{#if sorted_data[answer] > 0}{sorted_data[answer]}{/if}</span
>
{/each}
{#each quiz_answers as answer}
<div
class="w-20 bg-black self-end flex justify-center"
style="height: {(sorted_data[answer] * 20) / data.length}rem"
/>
{/each}
{#each quiz_answers as answer}
<div class="w-20">
<p class="-rotate-45">{answer}</p>
</div>
{/each}
</div>
</div>
@@ -169,6 +169,17 @@
and {question.answers.max_correct} are correct, where numbers between {question and {question.answers.max_correct} are correct, where numbers between {question
.answers.min} and {question.answers.max} can be selected. .answers.min} and {question.answers.max} can be selected.
</p> </p>
{:else if question.type === QuizQuestionType.VOTING}
<div class="grid grid-cols-2 gap-4 m-4 p-6">
{#each question.answers as answer, index_answer}
<div class="p-1 rounded-lg py-4 dark:bg-gray-500 bg-gray-300">
<h4 class="text-center">
{quiz.questions[index_question].answers[index_answer]
.answer}
</h4>
</div>
{/each}
</div>
{/if} {/if}
</div> </div>
</CollapsSection> </CollapsSection>