From 2c02f2ef54a564d831232cb93de1f8b455438332 Mon Sep 17 00:00:00 2001
From: Mawoka
Date: Mon, 19 Sep 2022 19:21:24 +0200
Subject: [PATCH] :sparkles: Quiz is no loner stored in player's browser and
more improvements under the hood
---
classquiz/db/models.py | 3 +-
classquiz/routers/quiz.py | 2 +-
classquiz/socket_server/__init__.py | 80 +++++++++++++++++----
frontend/src/lib/admin.svelte | 45 +++++++++---
frontend/src/lib/play/end.svelte | 19 +++--
frontend/src/lib/play/question.svelte | 84 +++++++++++++++--------
frontend/src/lib/play/show_results.svelte | 26 ++++---
frontend/src/routes/admin/+page.svelte | 6 +-
frontend/src/routes/play/+page.svelte | 41 +++++------
9 files changed, 209 insertions(+), 97 deletions(-)
diff --git a/classquiz/db/models.py b/classquiz/db/models.py
index ae63ff4..c653a84 100644
--- a/classquiz/db/models.py
+++ b/classquiz/db/models.py
@@ -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]
diff --git a/classquiz/routers/quiz.py b/classquiz/routers/quiz.py
index 3650d54..ca7f330 100644
--- a/classquiz/routers/quiz.py
+++ b/classquiz/routers/quiz.py
@@ -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"})}
diff --git a/classquiz/socket_server/__init__.py b/classquiz/socket_server/__init__.py
index 7c0379f..781ec5a 100644
--- a/classquiz/socket_server/__init__.py
+++ b/classquiz/socket_server/__init__.py
@@ -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"])
diff --git a/frontend/src/lib/admin.svelte b/frontend/src/lib/admin.svelte
index 663ab3f..6632342 100644
--- a/frontend/src/lib/admin.svelte
+++ b/frontend/src/lib/admin.svelte
@@ -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');
{#if game_mode === 'kahoot'}
@@ -100,13 +106,32 @@
{/if}
-
{
- set_question_number(selected_question + 1);
- }}
- >Next Question ({selected_question + 2}
- )
-
+ {#if selected_question + 1 === quiz_data.questions.length && timer_res === '0'}
+ {#if JSON.stringify(final_results) === JSON.stringify([null])}
+
Get final results
+ {/if}
+ {:else if timer_res === '0' || selected_question === -1}
+
{
+ set_question_number(selected_question + 1);
+ }}
+ >Next Question ({selected_question + 2})
+
+ {#if question_results === null && selected_question !== -1}
+
Show results
+ {/if}
+ {:else if selected_question !== -1}
+
Stop time and show solutions
+ {:else}
+
!OK!
+
{
+ set_question_number(selected_question + 1);
+ }}
+ >Next Question ({selected_question + 2}
+ )
+
+ {/if}
{#if timer_res !== '0' && selected_question >= 0}
@@ -228,10 +253,12 @@
{#if game_mode === 'normal'}
{
+ 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')} {$t('admin_page.stop_time')}
{/if}
diff --git a/frontend/src/lib/play/end.svelte b/frontend/src/lib/play/end.svelte
index 64fbfdd..11a5e7e 100644
--- a/frontend/src/lib/play/end.svelte
+++ b/frontend/src/lib/play/end.svelte
@@ -9,7 +9,7 @@
const { t } = getLocalization();
- export let quiz_data: QuizData;
+ export let question_count: number;
export let final_results: Array | Array>;
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 @@
{$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
})}
@@ -106,7 +113,7 @@
{$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
})}
@@ -120,7 +127,7 @@
{$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
})}
diff --git a/frontend/src/lib/play/question.svelte b/frontend/src/lib/play/question.svelte
index fca46a3..22f7af2 100644
--- a/frontend/src/lib/play/question.svelte
+++ b/frontend/src/lib/play/question.svelte
@@ -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);
@@ -149,31 +167,39 @@
{/await}
{/if}
{:else if question.type === QuizQuestionType.ABCD}
-
- {#each question.answers as answer}
- {#if answer.right}
- {answer.answer}
- {:else}
- {answer.answer}
- {/if}
- {/each}
-
+ {#if solution === undefined}
+
+ {:else}
+
+ {#each solution.answers as answer}
+ {#if answer.right}
+ {answer.answer}
+ {:else}
+ {answer.answer}
+ {/if}
+ {/each}
+
+ {/if}
{:else if question.type === QuizQuestionType.RANGE}
-
- Every number between {question.answers.min_correct} and {question.answers.max_correct} was correct.
- You got {selected_answer}, so you have been
- {#if question.answers.min_correct <= parseInt(selected_answer) && parseInt(selected_answer) <= question.answers.max_correct}
- correct
- {:else}
- wrong.
- {/if}
-
+ {#if solution === undefined}
+
+ {:else}
+
+ 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}
+
+ {/if}
{/if}
diff --git a/frontend/src/lib/play/show_results.svelte b/frontend/src/lib/play/show_results.svelte
index 16f85b1..e7d3ac8 100644
--- a/frontend/src/lib/play/show_results.svelte
+++ b/frontend/src/lib/play/show_results.svelte
@@ -4,7 +4,7 @@
- file, You can obtain one at https://mozilla.org/MPL/2.0/.
-->
@@ -38,7 +36,7 @@
{$t('words.result', { count: 2 })}
- {#if game_data.questions[parseInt(question_index)].type === QuizQuestionType.ABCD}
+ {#if solution.type === QuizQuestionType.ABCD}
@@ -64,7 +62,7 @@
- {#each game_data.questions[parseInt(question_index)].answers as answer}
+ {#each solution.answers as answer}
- {:else if game_data.questions[parseInt(question_index)].type === QuizQuestionType.RANGE}
+ {:else if solution.type === QuizQuestionType.RANGE}
{#await import('svelte-range-slider-pips')}
@@ -103,8 +101,8 @@
{#await import('$lib/play/end.svelte') then c}
-
+
{/await}
{/if}
{#if !success}
diff --git a/frontend/src/routes/play/+page.svelte b/frontend/src/routes/play/+page.svelte
index 44e5b61..804c7f3 100644
--- a/frontend/src/routes/play/+page.svelte
+++ b/frontend/src/routes/play/+page.svelte
@@ -2,7 +2,7 @@
@@ -109,19 +109,19 @@
ClassQuiz - Play
- {#if gameData !== undefined && game_mode !== 'kahoot'}
+
{#if !gameMeta.started && gameData === undefined}
{:else if JSON.stringify(final_results) !== JSON.stringify([null])}
-
+
{:else if gameData !== undefined && question_index === ''}
{:else if gameMeta.started && gameData !== undefined && question_index !== '' && answer_results === undefined}
{#key unique}
-
+
{/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}