Implemented in Live-Game

This commit is contained in:
Mawoka
2022-10-04 15:38:15 +02:00
parent 512fc9e8ea
commit 60822283e3
11 changed files with 338 additions and 144 deletions
+1 -1
View File
@@ -111,7 +111,7 @@ class QuizQuestion(BaseModel):
if values["type"] == QuizQuestionType.RANGE and type(v) != RangeQuizAnswer:
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 RangeQuizAnswer if type is RANGE")
raise ValueError("Answer must be from type VotingQuizAnswer if type is VOTING")
return v
+18 -9
View File
@@ -11,7 +11,7 @@ import socketio
from cryptography.fernet import Fernet
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 datetime import datetime
@@ -113,8 +113,10 @@ async def join_game(sid: str, data: dict):
{"username": data.username, "sid": sid},
room=redis_res.admin,
)
lol = fernet.encrypt(datetime.now().isoformat().encode("utf-8")).decode("utf-8")
await sio.emit("time_sync", lol, room=sid)
# +++ Time-Sync +++
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)
@@ -183,14 +185,16 @@ class RangeQuizAnswerWithoutSolution(BaseModel):
class ReturnQuestion(QuizQuestion):
answers: list[ABCDQuizAnswerWithoutSolution] | RangeQuizAnswerWithoutSolution
answers: list[ABCDQuizAnswerWithoutSolution] | RangeQuizAnswerWithoutSolution | list[VotingQuizAnswer]
@validator("answers")
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")
if values["type"] == QuizQuestionType.RANGE and type(v) != RangeQuizAnswerWithoutSolution:
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
@@ -204,13 +208,17 @@ async def set_question_number(sid, data: str):
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']}: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(
"set_question_number",
{
"question_index": int(float(data)),
"question": ReturnQuestion(
**game_data.dict(include={"questions"})["questions"][int(float(data))]
).dict(),
"question": ReturnQuestion(**temp_return).dict(),
},
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
):
answer_right = True
elif game_data.questions[int(float(data.question_index))].type == QuizQuestionType.VOTING:
answer_right = False
else:
raise NotImplementedError
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):
session: dict = await sio.get_session(sid)
game_data = PlayGame(**json.loads(await redis.get(f"game:{session['game_pin']}")))
results = {}
if not session["admin"]:
return
results = await generate_final_results(game_data, session["game_pin"])
+26 -64
View File
@@ -77,7 +77,7 @@ devDependencies:
'@sentry/tracing': 7.11.1
'@sveltejs/adapter-auto': 1.0.0-next.80
'@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
'@types/canvas-confetti': 1.4.3
'@types/cookie': 0.5.1
@@ -579,10 +579,10 @@ packages:
- supports-color
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:
{
integrity: sha512-KrhlSHT3aCVnhRgUoN6aGIjIw3nWEdNwfoZcE1x65F5D7Ju/K9D8dQwyal9v0aBAZyN9nFuXxsYGaMLE9sppfw==
integrity: sha512-qUnuhuL82meE0lSwrsS/FIvtDoE83Au4SxVLCim3FzoCnJc7bFZ9vYRbRUYxAjfYKRMjKVUrp9iHFObn9LmnUg==
}
engines: { node: '>=16.14' }
hasBin: true
@@ -598,13 +598,12 @@ packages:
kleur: 4.1.5
magic-string: 0.26.3
mime: 3.0.0
node-fetch: 3.2.10
sade: 1.8.1
set-cookie-parser: 2.5.1
sirv: 2.0.2
svelte: 3.49.0
tiny-glob: 0.2.9
undici: 5.9.1
undici: 5.11.0
vite: 3.1.2_sass@1.54.4
transitivePeerDependencies:
- diff-match-patch
@@ -1398,6 +1397,16 @@ packages:
}
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:
resolution:
{
@@ -1717,14 +1726,6 @@ packages:
css-tree: 1.1.3
dev: true
/data-uri-to-buffer/4.0.0:
resolution:
{
integrity: sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA==
}
engines: { node: '>= 12' }
dev: true
/debug/4.3.4:
resolution:
{
@@ -2503,17 +2504,6 @@ packages:
svelte: 3.49.0
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:
resolution:
{
@@ -2570,16 +2560,6 @@ packages:
}
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:
resolution:
{
@@ -3358,14 +3338,6 @@ packages:
}
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:
resolution:
{
@@ -3381,18 +3353,6 @@ packages:
whatwg-url: 5.0.0
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:
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'
dev: true
/streamsearch/1.1.0:
resolution:
{
integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==
}
engines: { node: '>=10.0.0' }
dev: true
/string-width/4.2.3:
resolution:
{
@@ -4960,12 +4928,14 @@ packages:
}
dev: true
/undici/5.9.1:
/undici/5.11.0:
resolution:
{
integrity: sha512-6fB3a+SNnWEm4CJbgo0/CWR8RGcOCQP68SF4X0mxtYTq2VNN8T88NYrWVBAeSX+zb7bny2dx2iYhP3XHi00omg==
integrity: sha512-oWjWJHzFet0Ow4YZBkyiJwiK5vWqEYoH7BINzJAJOLedZ++JpAlCbUktW2GQ2DS2FpKmxD/JMtWUUWl1BtghGw==
}
engines: { node: '>=12.18' }
dependencies:
busboy: 1.6.0
dev: true
/update-browserslist-db/1.0.5_browserslist@4.21.3:
@@ -5057,14 +5027,6 @@ packages:
pbf: 3.2.1
dev: true
/web-streams-polyfill/3.2.1:
resolution:
{
integrity: sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==
}
engines: { node: '>= 8' }
dev: true
/webidl-conversions/3.0.1:
resolution:
{
+29 -11
View File
@@ -114,8 +114,8 @@
{#if selected_question + 1 === quiz_data.questions.length && timer_res === '0' && question_results !== null}
{#if JSON.stringify(final_results) === JSON.stringify([null])}
<button on:click={get_final_results} class="admin-button"
>Get final results</button
>
>Get final results
</button>
{/if}
{:else if timer_res === '0' || selected_question === -1}
{#if (selected_question + 1 !== quiz_data.questions.length && question_results !== null) || selected_question === -1}
@@ -129,13 +129,13 @@
{/if}
{#if question_results === null && selected_question !== -1}
<button on:click={get_question_results} class="admin-button"
>Show results</button
>
>Show results
</button>
{/if}
{:else if selected_question !== -1}
<button on:click={show_solutions} class="admin-button"
>Stop time and show solutions</button
>
>Stop time and show solutions
</button>
{:else}
<!-- <button
on:click={() => {
@@ -181,20 +181,28 @@
</div>
{/if}
{#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">
{#each quiz_data.questions[selected_question].answers as answer, i}
<div
class="rounded-lg h-fit flex"
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]} />
<span
class="text-center text-2xl px-2 py-4 w-full text-black"
class:text-4xl={answer.right && timer_res === '0'}
class:underline={answer.right && timer_res === '0'}
>{answer.answer}</span
class:text-4xl={answer.right &&
timer_res === '0' &&
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" />
</div>
@@ -221,6 +229,16 @@
<h1 class="text-3xl">{$t('admin_page.no_answers')}</h1>
</div>
{/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}
{#await import('$lib/play/admin/results.svelte')}
<Spinner />
@@ -9,6 +9,7 @@
import { reach } from 'yup';
import { getLocalization } from '$lib/i18n';
import { VotingQuestionSchema } from '$lib/yupSchemas';
import { onMount } from 'svelte';
const { t } = getLocalization();
@@ -19,9 +20,19 @@
export let selected_question: number;
export let data: EditorData;
if (!Array.isArray(data.questions[selected_question].answers)) {
data.questions[selected_question].answers = [];
}
try {
if (typeof data.questions[selected_question].answers[0].right === 'boolean') {
data.questions[selected_question].answers = [];
}
} 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>
<!--
@@ -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>
{/if}
{#if timer_res !== '0'}
{#if question.type === QuizQuestionType.ABCD}
{#if question.type === QuizQuestionType.ABCD || QuizQuestionType.VOTING}
{#if game_mode === 'normal'}
<div class="grid grid-cols-2 gap-2 w-full p-4">
{#each question.answers as answer}
@@ -202,4 +202,11 @@
{/if}
</p>
{/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}
+59 -43
View File
@@ -91,48 +91,64 @@
{/if}
</div>
{/if}
{:else if timer_res === '0'}
{#await import('svelte-range-slider-pips')}
<Spinner />
{:then c}
<div class="grayscale pointer-events-none w-full">
<svelte:component
this={c.default}
bind:values={slider_values}
bind:min={question.answers.min}
bind:max={question.answers.max}
pips
float
all="label"
/>
</div>
{/await}
{:else}
{#await import('svelte-range-slider-pips')}
<Spinner />
{:then c}
<div class:pointer-events-none={selected_answer !== undefined}>
<svelte:component
this={c.default}
bind:values={slider_value}
bind:min={question.answers.min}
bind:max={question.answers.max}
pips
float
all="label"
/>
</div>
<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={() => {
selected_answer = slider_value[0];
timer_res = '0';
}}
>{$t('words.submit')}
</button>
</div>
{/await}
{:else if question.type === QuizQuestionType.RANGE}
{#if timer_res === '0'}
{#await import('svelte-range-slider-pips')}
<Spinner />
{:then c}
<div class="grayscale pointer-events-none w-full">
<svelte:component
this={c.default}
bind:values={slider_values}
bind:min={question.answers.min}
bind:max={question.answers.max}
pips
float
all="label"
/>
</div>
{/await}
{:else}
{#await import('svelte-range-slider-pips')}
<Spinner />
{:then c}
<div class:pointer-events-none={selected_answer !== undefined}>
<svelte:component
this={c.default}
bind:values={slider_value}
bind:min={question.answers.min}
bind:max={question.answers.max}
pips
float
all="label"
/>
</div>
<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={() => {
selected_answer = slider_value[0];
timer_res = '0';
}}
>{$t('words.submit')}
</button>
</div>
{/await}
{/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>
+3
View File
@@ -63,9 +63,12 @@ export const dataSchema = yup.object({
.lowercase(),
answers: yup.lazy((v) => {
if (Array.isArray(v)) {
console.log(v);
if (typeof v[0].right === 'boolean') {
console.log('Yup, ABCD');
return ABCDQuestionSchema;
} else {
console.log('Yup, Poll');
return VotingQuestionSchema;
}
} else {
+1 -12
View File
@@ -9,6 +9,7 @@
import { getLocalization } from '$lib/i18n';
import { navbarVisible } from '$lib/stores';
import { QuizQuestionType } from '$lib/quiz_types';
import type { Question } from '$lib/quiz_types';
navbarVisible.set(false);
@@ -21,18 +22,6 @@
questions: Question[];
}
interface Question {
question: string;
time: string;
type?: QuizQuestionType;
answers: Answer[];
}
interface Answer {
right: boolean;
answer: string;
}
let responseData = {
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>