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}
- + {#if selected_question + 1 === quiz_data.questions.length && timer_res === '0'} + {#if JSON.stringify(final_results) === JSON.stringify([null])} + + {/if} + {:else if timer_res === '0' || selected_question === -1} + + {#if question_results === null && selected_question !== -1} + + {/if} + {:else if selected_question !== -1} + + {:else} +

!OK!

+ + {/if}
{#if timer_res !== '0' && selected_question >= 0} @@ -228,10 +253,12 @@ {#if game_mode === 'normal'}
{$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} - - {:else} - - {/if} - {/each} -
+ {#if solution === undefined} + + {:else} +
+ {#each solution.answers as answer} + {#if answer.right} + + {:else} + + {/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}