Quiz is no loner stored in player's browser and more improvements under the hood

This commit is contained in:
Mawoka
2022-09-19 19:21:24 +02:00
parent 8039269cf3
commit 2c02f2ef54
9 changed files with 209 additions and 97 deletions
+2 -1
View File
@@ -159,6 +159,7 @@ class PlayGame(BaseModel):
captcha_enabled: bool = False
cover_image: str | None
game_mode: str | None
current_question: int = -1
class GamePlayer(BaseModel):
@@ -180,7 +181,7 @@ class GameAnswer1(BaseModel):
class GameSession(BaseModel):
admin: str
game_id: str
players: list[GamePlayer | None]
# players: list[GamePlayer | None]
answers: list[GameAnswer1 | None]
+1 -1
View File
@@ -114,7 +114,7 @@ async def start_quiz(
cover_image=quiz.cover_image,
game_mode=game_mode,
)
await redis.set(f"game:{str(game.game_pin)}", (game.json()), ex=18000)
await redis.set(f"game:{str(game.game_pin)}", game.json(), ex=18000)
return {**quiz.dict(exclude={"id"}), **game.dict(exclude={"questions"})}
+66 -14
View File
@@ -8,10 +8,9 @@ import os
import aiohttp
import socketio
from typing import Any
from classquiz.config import redis, settings
from classquiz.db.models import PlayGame, QuizQuestionType, GameSession, GamePlayer
from pydantic import BaseModel, ValidationError
from classquiz.db.models import PlayGame, QuizQuestionType, GameSession, GamePlayer, RangeQuizAnswer, QuizQuestion
from pydantic import BaseModel, ValidationError, validator
sio = socketio.AsyncServer(async_mode="asgi", cors_allowed_origins=[])
settings = settings()
@@ -22,7 +21,7 @@ async def generate_final_results(game_data: PlayGame, game_pin: str) -> dict:
for i in range(len(game_data.questions)):
redis_res = await redis.get(f"game_session:{game_pin}:{i}")
if redis_res is None:
break
continue
else:
results[str(i)] = json.loads(redis_res)
return results
@@ -46,10 +45,14 @@ async def join_game(sid: str, data: dict):
await sio.emit("error", room=sid)
print(e)
return
game_data = PlayGame.parse_raw(redis_res)
if game_data.started:
await sio.emit("game_already_started", room=sid)
return
# +++ START checking captcha +++
async with aiohttp.ClientSession() as session:
try:
if json.loads(redis_res)["captcha_enabled"]:
if game_data.captcha_enabled:
try:
async with session.post(
"https://hcaptcha.com/siteverify",
@@ -72,15 +75,19 @@ async def join_game(sid: str, data: dict):
"admin": False,
}
await sio.save_session(sid, session)
await sio.emit("joined_game", redis_res, room=sid)
await sio.emit(
"joined_game",
{**json.loads(game_data.json(exclude={"quiz_id", "questions"})), "question_count": len(game_data.questions)},
room=sid,
)
redis_res = await redis.get(f"game_session:{data.game_pin}")
redis_res = GameSession.parse_raw(redis_res)
redis_res.players.append(GamePlayer(username=data.username, sid=sid))
await redis.set(
f"game_session:{data.game_pin}",
GameSession(admin=redis_res.admin, game_id=redis_res.game_id, players=redis_res.players, answers=[]).json(),
ex=18000,
)
await redis.sadd(f"game_session:{data.game_pin}:players", GamePlayer(username=data.username, sid=sid).json())
# await redis.set(
# f"game_session:{data.game_pin}",
# GameSession(admin=redis_res.admin, game_id=redis_res.game_id, answers=[]).json(),
# ex=18000,
# )
await sio.emit(
"player_joined",
{"username": data.username, "sid": sid},
@@ -93,6 +100,9 @@ async def join_game(sid: str, data: dict):
async def start_game(sid: str, _data: dict):
session = await sio.get_session(sid)
if session["admin"]:
game_data = PlayGame.parse_raw(await redis.get(f"game:{session['game_pin']}"))
game_data.started = True
await redis.set(f"game:{session['game_pin']}", game_data.json())
await sio.emit("start_game", room=session["game_pin"])
@@ -114,7 +124,7 @@ async def register_as_admin(sid: str, data: dict):
if (await redis.get(f"game_session:{game_pin}")) is None:
await redis.set(
f"game_session:{game_pin}",
GameSession(admin=sid, game_id=game_id, answers=[], players=[]).json(),
GameSession(admin=sid, game_id=game_id, answers=[]).json(),
ex=18000,
)
@@ -140,13 +150,45 @@ async def get_question_results(sid: str, data: dict):
await sio.emit("question_results", redis_res, room=game_pin)
class ABCDQuizAnswerWithoutSolution(BaseModel):
answer: str
color: str | None
class RangeQuizAnswerWithoutSolution(BaseModel):
min: int
max: int
class ReturnQuestion(QuizQuestion):
answers: list[ABCDQuizAnswerWithoutSolution] | RangeQuizAnswerWithoutSolution
@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) != RangeQuizAnswerWithoutSolution:
raise ValueError("Answer must be from type RangeQuizAnswer if type is RANGE")
return v
@sio.event
async def set_question_number(sid, data: str):
# data is just a number (as a str) of the question
session = await sio.get_session(sid)
if session["admin"]:
game_pin = session["game_pin"]
await sio.emit("set_question_number", data, room=game_pin)
game_data = PlayGame.parse_raw(await redis.get(f"game:{session['game_pin']}"))
game_data.current_question = int(data)
await redis.set(f"game:{session['game_pin']}", game_data.json())
await sio.emit(
"set_question_number",
{
"question_index": int(data),
"question": ReturnQuestion(**game_data.dict(include={"questions"})["questions"][int(data)]).dict(),
},
room=game_pin,
)
class _SubmitAnswerData(BaseModel):
@@ -219,6 +261,7 @@ async def get_final_results(sid: str, _data: dict):
if not session["admin"]:
return
results = await generate_final_results(game_data, session["game_pin"])
print(results)
await sio.emit("final_results", results, room=session["game_pin"])
@@ -232,3 +275,12 @@ async def get_export_token(sid):
token = os.urandom(32).hex()
await redis.set(f"export_token:{token}", json.dumps(results))
await sio.emit("export_token", token, room=sid)
@sio.event
async def show_solutions(sid: str, _data: dict):
session: dict = await sio.get_session(sid)
game_data = PlayGame(**json.loads(await redis.get(f"game:{session['game_pin']}")))
if not session["admin"]:
return
await sio.emit("solutions", game_data.questions[game_data.current_question].dict(), room=session["game_pin"])
+36 -9
View File
@@ -43,6 +43,10 @@
});
timer_res = '0';
};
const show_solutions = () => {
socket.emit('show_solutions', {});
timer_res = '0';
};
const get_final_results = () => {
socket.emit('get_final_results', {});
@@ -67,6 +71,7 @@
let timer_interval = setInterval(() => {
if (timer_res === '0') {
clearInterval(timer_interval);
socket.emit('show_solutions', {});
return;
} else {
seconds--;
@@ -85,6 +90,7 @@
circular_prgoress = 0;
}
}
$: console.log(quiz_data.questions.length, 'length of quiz');
</script>
{#if game_mode === 'kahoot'}
@@ -100,13 +106,32 @@
</div>
{/if}
<div class="justify-self-end ml-auto mr-0 col-start-3 col-end-3">
<button
on:click={() => {
set_question_number(selected_question + 1);
}}
>Next Question ({selected_question + 2}
)
</button>
{#if selected_question + 1 === quiz_data.questions.length && timer_res === '0'}
{#if JSON.stringify(final_results) === JSON.stringify([null])}
<button on:click={get_final_results}>Get final results</button>
{/if}
{:else if timer_res === '0' || selected_question === -1}
<button
on:click={() => {
set_question_number(selected_question + 1);
}}
>Next Question ({selected_question + 2})
</button>
{#if question_results === null && selected_question !== -1}
<button on:click={get_question_results}>Show results</button>
{/if}
{:else if selected_question !== -1}
<button on:click={show_solutions}>Stop time and show solutions</button>
{:else}
<p>!OK!</p>
<button
on:click={() => {
set_question_number(selected_question + 1);
}}
>Next Question ({selected_question + 2}
)
</button>
{/if}
</div>
</div>
{#if timer_res !== '0' && selected_question >= 0}
@@ -228,10 +253,12 @@
{#if game_mode === 'normal'}
<div class="w-full flex justify-center">
<button
on:click={get_question_results}
on:click={() => {
timer_res = '0';
}}
id="GetQuestionResultsAndStopTime"
class="px-4 py-2 leading-5 text-white transition-colors duration-200 transform bg-gray-700 rounded text-center hover:bg-gray-600 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
>{$t('admin_page.get_results_and_stop_time')}</button
>{$t('admin_page.stop_time')}</button
>
</div>
{/if}
+13 -6
View File
@@ -9,7 +9,7 @@
const { t } = getLocalization();
export let quiz_data: QuizData;
export let question_count: number;
export let final_results: Array<null> | Array<Array<PlayerAnswer>>;
interface PlayerAnswer {
@@ -23,7 +23,7 @@
const getWinnersSorted = () => {
let winners = {};
let q_count = quiz_data.questions.length;
let q_count = question_count;
console.log(
JSON.stringify(final_results),
JSON.stringify(final_results) === '{}',
@@ -42,9 +42,15 @@
try {
for (let i = 0; i < q_count; i++) {
let q_res = final_results[i];
if (q_res === null) {
if (!q_res) {
continue;
} else {
q_res = final_results[String(i)];
if (!q_res) {
continue;
}
}
console.log(q_res);
for (let j = 0; j < q_res.length; j++) {
let res = q_res[j];
if (res['right']) {
@@ -68,6 +74,7 @@
data_available = true;
return close_to_res;
} catch (e) {
console.log(e);
data_available = false;
}
};
@@ -93,7 +100,7 @@
<span
>{$t('play_page.with_out_of', {
correct_questions: winners_arr[0][1] ?? 0,
total_question_count: quiz_data.questions.length
total_question_count: question_count
})}</span
>
</p>
@@ -106,7 +113,7 @@
<span
>{$t('play_page.with_out_of', {
correct_questions: winners_arr[1][1] ?? 0,
total_question_count: quiz_data.questions.length
total_question_count: question_count
})}</span
>
</p>
@@ -120,7 +127,7 @@
<span>
{$t('play_page.with_out_of', {
correct_questions: winners_arr[2][1] ?? 0,
total_question_count: quiz_data.questions.length
total_question_count: question_count
})}
</span>
</p>
+55 -29
View File
@@ -17,13 +17,23 @@
export let question: Question;
export let game_mode;
export let question_index: string | number;
export let solution;
if (typeof question_index === 'string') {
question_index = parseInt(question_index);
$: console.log(question_index, question, 'hi!');
console.log(question);
if (question.type === undefined) {
question.type = QuizQuestionType.ABCD;
} else {
throw new Error('question_index must be a string or number');
question.type = QuizQuestionType[question.type];
}
/* if (typeof question_index === 'string') {
question_index = parseInt(question_index);
} else {
throw new Error('question_index must be a string or number');
}*/
let timer_res = question.time;
let selected_answer: string;
@@ -44,6 +54,12 @@
timer(question.time);
$: {
if (solution !== undefined) {
timer_res = '0';
}
}
const selectAnswer = (answer: string) => {
selected_answer = answer;
//timer_res = '0';
@@ -75,6 +91,8 @@
circular_prgoress = 0;
}
}
$: console.log(solution);
</script>
<div class="flex flex-col justify-center w-screen h-1/6">
@@ -149,31 +167,39 @@
{/await}
{/if}
{:else if question.type === QuizQuestionType.ABCD}
<div class="flex flex-wrap">
{#each question.answers as answer}
{#if answer.right}
<button
class="w-1/2 text-3xl bg-green-600 border border-white"
disabled
class:opacity-30={answer.answer !== selected_answer}>{answer.answer}</button
>
{:else}
<button
class="w-1/2 text-3xl bg-red-500 border border-white"
disabled
class:opacity-30={answer.answer !== selected_answer}>{answer.answer}</button
>
{/if}
{/each}
</div>
{#if solution === undefined}
<Spinner />
{:else}
<div class="flex flex-wrap">
{#each solution.answers as answer}
{#if answer.right}
<button
class="w-1/2 text-3xl bg-green-600 border border-white"
disabled
class:opacity-30={answer.answer !== selected_answer}>{answer.answer}</button
>
{:else}
<button
class="w-1/2 text-3xl bg-red-500 border border-white"
disabled
class:opacity-30={answer.answer !== selected_answer}>{answer.answer}</button
>
{/if}
{/each}
</div>
{/if}
{: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 solution === undefined}
<Spinner />
{:else}
<p class="text-center">
Every number between {solution.answers.min_correct} and {solution.answers.max_correct} was
correct. You got {selected_answer}, so you have been
{#if solution.answers.min_correct <= parseInt(selected_answer) && parseInt(selected_answer) <= solution.answers.max_correct}
correct
{:else}
wrong.
{/if}
</p>
{/if}
{/if}
+12 -14
View File
@@ -4,7 +4,7 @@
- file, You can obtain one at https://mozilla.org/MPL/2.0/.
-->
<script lang="ts">
import type { Answer, QuizData } from '$lib/quiz_types';
import type { Answer, Question, QuizData } from '$lib/quiz_types';
import { getLocalization } from '$lib/i18n';
import { QuizQuestionType } from '../quiz_types.js';
import Spinner from '$lib/Spinner.svelte';
@@ -12,10 +12,11 @@
const { t } = getLocalization();
export let results: Array<Answer>;
export let game_data: QuizData;
export let solution: Question;
export let question_index: string;
let data_store = {};
const question = game_data.questions[parseInt(question_index)].answers;
const question = solution.answers;
for (let i = 0; i < question.length; i++) {
data_store[question[i].answer] = 0;
}
@@ -24,11 +25,8 @@
data_store[results[i].answer] += 1;
}
let slider_values = [
game_data.questions[parseInt(question_index)].answers.min_correct ?? 0,
game_data.questions[parseInt(question_index)].answers.max_correct ?? 0
];
console.log(slider_values, game_data.questions[parseInt(question_index)].answers);
let slider_values = [solution.answers.min_correct ?? 0, solution.answers.max_correct ?? 0];
console.log(slider_values, solution.answers);
</script>
<!-- Show the results from the results object -->
@@ -38,7 +36,7 @@
<div>
<h2 class="text-center text-3xl mb-8">{$t('words.result', { count: 2 })}</h2>
<div class="w-screen flex justify-center">
{#if game_data.questions[parseInt(question_index)].type === QuizQuestionType.ABCD}
{#if solution.type === QuizQuestionType.ABCD}
<div class="relative overflow-x-auto shadow-md rounded-lg">
<table class="w-fit text-sm text-left text-gray-500 dark:text-gray-400">
<thead class="bg-gray-50 dark:bg-gray-700">
@@ -64,7 +62,7 @@
</tr>
</thead>
<tbody>
{#each game_data.questions[parseInt(question_index)].answers as answer}
{#each solution.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"
@@ -90,10 +88,10 @@
</tbody>
</table>
</div>
{:else if game_data.questions[parseInt(question_index)].type === QuizQuestionType.RANGE}
{:else if solution.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
Every number between {solution.answers
.min_correct} and {solution.answers
.max_correct} was correct.
</p>-->
{#await import('svelte-range-slider-pips')}
@@ -103,8 +101,8 @@
<svelte:component
this={c.default}
bind:values={slider_values}
bind:min={game_data.questions[parseInt(question_index)].answers.min}
bind:max={game_data.questions[parseInt(question_index)].answers.max}
bind:min={solution.answers.min}
bind:max={solution.answers.max}
pips
float
all="label"
+5 -1
View File
@@ -119,7 +119,11 @@
>
</div>
{#await import('$lib/play/end.svelte') then c}
<svelte:component this={c.default} bind:final_results bind:quiz_data />
<svelte:component
this={c.default}
bind:final_results
bind:question_count={quiz_data.questions.length}
/>
{/await}
{/if}
{#if !success}
+19 -22
View File
@@ -2,7 +2,7 @@
<script lang="ts">
import { socket } from '$lib/socket';
import JoinGame from '$lib/play/join.svelte';
import type { Answer, QuizData } from '$lib/quiz_types';
import type { Answer, QuizData, Question } from '$lib/quiz_types';
import ShowTitle from '$lib/play/title.svelte';
import Question from '$lib/play/question.svelte';
import ShowResults from '$lib/play/show_results.svelte';
@@ -37,11 +37,14 @@
navbarVisible.set(false);
let game_pin_valid: boolean;
let answer_results: Array<Answer>;
let gameData: QuizData;
let gameData;
let solution: Question;
let gameMeta: GameMeta = {
started: false
};
let question;
// Functions
function restart() {
unique = {};
@@ -56,17 +59,7 @@
// Socket-events
socket.on('joined_game', (data) => {
console.log('joined_game', 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;
gameData = data;
// eslint-disable-next-line no-undef
plausible('Joined Game', { props: { quiz_id: gameData.quiz_id } });
});
@@ -76,9 +69,12 @@
});
socket.on('set_question_number', (data) => {
solution = undefined;
restart();
console.log(data, data.question_index);
question = data.question;
question_index = data.question_index;
answer_results = undefined;
question_index = data;
});
socket.on('start_game', () => {
@@ -97,6 +93,10 @@
socket.on('final_results', (data) => {
final_results = data;
});
socket.on('solutions', (data) => {
solution = data;
});
// The rest
</script>
@@ -109,19 +109,19 @@
<svelte:window on:beforeunload={confirmUnload} />
<svelte:head>
<title>ClassQuiz - Play</title>
{#if gameData !== undefined && game_mode !== 'kahoot'}
<!-- {#if gameData !== undefined && game_mode !== 'kahoot'}
{#each gameData.questions as question}
{#if question.image !== undefined}
<link rel="preload" as="image" href={question.image} />
{/if}
{/each}
{/if}
{/if}-->
</svelte:head>
<div>
{#if !gameMeta.started && gameData === undefined}
<JoinGame {game_pin} bind:game_mode />
{:else if JSON.stringify(final_results) !== JSON.stringify([null])}
<ShowEndScreen bind:final_results bind:quiz_data={gameData} />
<ShowEndScreen bind:final_results bind:question_count={gameData.question_count} />
{:else if gameData !== undefined && question_index === ''}
<ShowTitle
bind:title={gameData.title}
@@ -130,11 +130,7 @@
/>
{:else if gameMeta.started && gameData !== undefined && question_index !== '' && answer_results === undefined}
{#key unique}
<Question
bind:game_mode
bind:question={gameData.questions[parseInt(question_index)]}
bind:question_index
/>
<Question bind:game_mode bind:question bind:question_index bind:solution />
{/key}
{:else if gameMeta.started && answer_results !== undefined}
{#if answer_results === null}
@@ -147,6 +143,7 @@
bind:results={answer_results}
bind:game_data={gameData}
bind:question_index
bind:solution
/>
{/key}
{/if}