Game results can now be saved in db

This commit is contained in:
Mawoka
2023-01-29 18:16:05 +01:00
parent 3e61ecc8d4
commit a980c70d85
6 changed files with 256 additions and 21 deletions
+30
View File
@@ -270,6 +270,19 @@ class UpdatePassword(BaseModel):
new_password: str new_password: str
class AnswerData(BaseModel):
username: str
answer: str
right: bool
time_taken: float # In milliseconds
score: int
class AnswerDataList(BaseModel):
# Just a method to make a top-level list
__root__: list[AnswerData]
class GameInLobby(BaseModel): class GameInLobby(BaseModel):
game_pin: str game_pin: str
quiz_title: str quiz_title: str
@@ -283,3 +296,20 @@ class GameInLobby(BaseModel):
# github_username: str | None = ormar.Text(nullable=True) # github_username: str | None = ormar.Text(nullable=True)
# reddit_username: str | None = ormar.Text(nullable=True) # reddit_username: str | None = ormar.Text(nullable=True)
# kahoot_user_id: str | None = ormar.Text(nullable=True) # kahoot_user_id: str | None = ormar.Text(nullable=True)
class GameResults(ormar.Model):
id: uuid.UUID = ormar.UUID(primary_key=True)
quiz_id: uuid.UUID = ormar.ForeignKey(Quiz)
user_id: uuid.UUID = ormar.ForeignKey(User)
timestamp: datetime = ormar.DateTime(default=datetime.now(), nullable=False)
player_count: int = ormar.Integer(nullable=False, default=0)
note: str | None = ormar.Text(nullable=True)
answers: Json[AnswerDataList] = ormar.JSON(True)
player_scores: Json[dict[str, str]] = ormar.JSON(nullable=True)
custom_field_data: Json[dict[str, str]] | None = ormar.JSON(nullable=True)
class Meta:
tablename = "game_results"
metadata = metadata
database = database
+3 -2
View File
@@ -20,9 +20,10 @@ from classquiz.db.models import (
ABCDQuizAnswer, ABCDQuizAnswer,
QuizQuestionType, QuizQuestionType,
VotingQuizAnswer, VotingQuizAnswer,
AnswerDataList,
) )
from classquiz.auth import check_api_key from classquiz.auth import check_api_key
from classquiz.socket_server import ReturnQuestion, sio, _AnswerDataList from classquiz.socket_server import ReturnQuestion, sio
settings = settings() settings = settings()
@@ -276,7 +277,7 @@ async def voting_results(game_pin: str, api_key: str, as_array: bool = False):
answer_data = await redis.get(f"game_session:{game_pin}:{game.current_question}") answer_data = await redis.get(f"game_session:{game_pin}:{game.current_question}")
if answer_data is None: if answer_data is None:
return return
answer_list = _AnswerDataList.parse_raw(answer_data) answer_list = AnswerDataList.parse_raw(answer_data)
answer_dict = {} answer_dict = {}
for answer in game.questions[game.current_question].answers: for answer in game.questions[game.current_question].answers:
answer_dict[answer.answer] = 0 answer_dict[answer.answer] = 0
+48 -19
View File
@@ -12,7 +12,17 @@ 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, VotingQuizAnswer from classquiz.db.models import (
PlayGame,
QuizQuestionType,
GameSession,
GamePlayer,
QuizQuestion,
VotingQuizAnswer,
AnswerDataList,
AnswerData,
GameResults,
)
from pydantic import BaseModel, ValidationError, validator from pydantic import BaseModel, ValidationError, validator
from datetime import datetime from datetime import datetime
@@ -272,19 +282,6 @@ class _SubmitAnswerData(BaseModel):
complex_answer: list[_SubmitAnswerDataOrderType] | None complex_answer: list[_SubmitAnswerDataOrderType] | None
class _AnswerData(BaseModel):
username: str
answer: str
right: bool
time_taken: float # In milliseconds
score: int
class _AnswerDataList(BaseModel):
# Just a method to make a top-level list
__root__: list[_AnswerData]
@sio.event @sio.event
async def submit_answer(sid: str, data: dict): async def submit_answer(sid: str, data: dict):
now = datetime.now() now = datetime.now()
@@ -360,9 +357,9 @@ async def submit_answer(sid: str, data: dict):
if answers is None: if answers is None:
await redis.set( await redis.set(
f"game_session:{session['game_pin']}:{data.question_index}", f"game_session:{session['game_pin']}:{data.question_index}",
_AnswerDataList( AnswerDataList(
__root__=[ __root__=[
_AnswerData( AnswerData(
username=session["username"], username=session["username"],
answer=data.answer, answer=data.answer,
right=answer_right, right=answer_right,
@@ -374,9 +371,9 @@ async def submit_answer(sid: str, data: dict):
ex=7200, ex=7200,
) )
else: else:
answers = _AnswerDataList.parse_raw(answers) answers = AnswerDataList.parse_raw(answers)
answers.__root__.append( answers.__root__.append(
_AnswerData( AnswerData(
username=session["username"], username=session["username"],
answer=data.answer, answer=data.answer,
right=answer_right, right=answer_right,
@@ -389,7 +386,7 @@ async def submit_answer(sid: str, data: dict):
answers.json(), answers.json(),
ex=7200, ex=7200,
) )
answers = _AnswerDataList.parse_raw(await redis.get(f"game_session:{session['game_pin']}:{data.question_index}")) answers = AnswerDataList.parse_raw(await redis.get(f"game_session:{session['game_pin']}:{data.question_index}"))
player_count = await redis.scard(f"game_session:{session['game_pin']}:players") player_count = await redis.scard(f"game_session:{session['game_pin']}:players")
if len(answers.__root__) == player_count: if len(answers.__root__) == player_count:
# await sio.emit( # await sio.emit(
@@ -510,3 +507,35 @@ async def set_control_visibility(sid: str, data: dict):
return return
session: dict = await sio.get_session(sid) session: dict = await sio.get_session(sid)
await sio.emit("control_visibility", {"visible": data.visible}, room=f"admin:{session['game_pin']}") await sio.emit("control_visibility", {"visible": data.visible}, room=f"admin:{session['game_pin']}")
@sio.event
async def save_quiz(sid: str):
session: dict = await sio.get_session(sid)
if not session["admin"]:
return
game_pin = session["game_pin"]
game = PlayGame.parse_raw(await redis.get(f"game:{game_pin}"))
player_count = await redis.scard(f"game_session:{game_pin}:players")
answers = []
for i in range(len(game.questions)):
print("hiqq", i)
redis_res = await redis.get(f"game_session:{game_pin}:{i}")
try:
answers.append(AnswerDataList.parse_raw(redis_res).dict())
except ValidationError:
answers.append([])
player_scores = await redis.hgetall(f"game_session:{game_pin}:player_scores")
custom_field_data = await redis.hgetall(f"game:{game_pin}:players:custom_fields")
print(custom_field_data)
data = GameResults(
id=game.game_id,
quiz_id=game.quiz_id,
user_id=game.user_id,
timestamp=datetime.now(),
player_count=player_count,
answers=json.dumps(answers),
player_scores=json.dumps(player_scores),
custom_field_data=json.dumps(custom_field_data),
)
await data.save()
+8
View File
@@ -110,6 +110,9 @@
const request_answer_export = async () => { const request_answer_export = async () => {
await socket.emit('get_export_token'); await socket.emit('get_export_token');
}; };
const save_quiz = async () => {
await socket.emit('save_quiz');
};
let darkMode = false; let darkMode = false;
if (browser) { if (browser) {
@@ -156,6 +159,11 @@
>{$t('admin_page.export_results')}</button >{$t('admin_page.export_results')}</button
> >
</div> </div>
<div class="w-screen flex justify-center">
<button on:click={save_quiz} class="admin-button"
>{$t('admin_page.save_results')}</button
>
</div>
{/if} {/if}
<FinalResults bind:data={player_scores} bind:show_final_results /> <FinalResults bind:data={player_scores} bind:show_final_results />
{/if} {/if}
+124
View File
@@ -0,0 +1,124 @@
## game_session:{GAME_PIN}:players:{PLAYER_NAME} [string]
Contains only the sid (socket.io session-id) of {PLAYER_NAME}
## game_session:{GAME_PIN}:{QUESTION_INDEX} [string]
Stores the answer per question which the players submitted
model: _AnswerDataList
example-data:
```json
[
{
"username": "Mawoka",
"answer": "a",
"right": false,
"time_taken": 4994.246999999999,
"score": 0
}
]
```
## game:{GAME_PIN} [string]
Stores the data for the game
model: PlayGame
example:
```json
{
"quiz_id": "be7089c6-ec97-4da9-bb3e-1aa9b67fb939",
"description": "asddsadas",
"user_id": "7cbabbc5-fdbb-4d8b-9a89-7005dfdb6f33",
"title": "Test",
"questions": [
{
"question": "sdadsadas",
"time": "20",
"type": "ABCD",
"answers": [
{
"right": false,
"answer": "a",
"color": null
},
{
"right": true,
"answer": "b",
"color": "null"
}
],
"image": null
}
],
"game_id": "7b572f2b-cf7b-47a9-ac0f-446dac22eab0",
"game_pin": "623490",
"started": true,
"captcha_enabled": false,
"cover_image": null,
"game_mode": "kahoot",
"current_question": 0,
"background_color": null,
"background_image": null,
"custom_field": null
}
```
## game_session:{GAME_PIN}:player_scores [hash]
Just stores the score the player has at any point of the game
data:
`{PLAYER_NAME} = {SCORE}`
## game_session:{GAME_PIN} [string]
Mostly unused, only used to check if an admin is registered. The `answers` never change.
model: GameSession
example:
```json
{
"admin": "qo1yt-rBG4HyX0YGAAAB",
"game_id": "7b572f2b-cf7b-47a9-ac0f-446dac22eab0",
"answers": []
}
```
## game_session:{GAME_PIN}:players [set]
A list of all current players, used to get the current number of players to check if everyone has answered
entry:
```json
{
"username": "Mawoka",
"sid": "VSprqk7xGKaH5QbwAAAD"
}
```
## game:{GAME_PIN}:current_time [string]
The time when the question was shown to measure the time the players needed to answer
## game_pin:{GAME_ID}:{QUIZ_ID} [string]
**Seems** to be unused
Returns the Game-pin
## game:{GAME_ID}:players:custom_fields [hash] [OPTIONAL]
Holds the custom-field data, but is only set if the custom-field is enabled.
data: `{PLAYER_NAME} = {CUSTOM_FIELD_VALUE}`
@@ -0,0 +1,43 @@
"""Added game_results column
Revision ID: ac98b64a5347
Revises: 7ad8502af419
Create Date: 2023-01-29 18:00:36.389980
"""
from alembic import op
import sqlalchemy as sa
import ormar
# revision identifiers, used by Alembic.
revision = "ac98b64a5347"
down_revision = "7ad8502af419"
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"game_results",
sa.Column("id", ormar.fields.sqlalchemy_uuid.CHAR(32), nullable=False),
sa.Column("quiz_id", ormar.fields.sqlalchemy_uuid.CHAR(32), nullable=True),
sa.Column("user_id", ormar.fields.sqlalchemy_uuid.CHAR(32), nullable=True),
sa.Column("timestamp", sa.DateTime(), nullable=False),
sa.Column("player_count", sa.Integer(), nullable=False),
sa.Column("note", sa.Text(), nullable=True),
sa.Column("answers", sa.JSON(), nullable=False),
sa.Column("player_scores", sa.JSON(none_as_null=True), nullable=True),
sa.Column("custom_field_data", sa.JSON(none_as_null=True), nullable=True),
sa.ForeignKeyConstraint(["quiz_id"], ["quiz.id"], name="fk_game_results_quiz_id_quiz_id"),
sa.ForeignKeyConstraint(["user_id"], ["users.id"], name="fk_game_results_users_id_user_id"),
sa.PrimaryKeyConstraint("id"),
)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("game_results")
# ### end Alembic commands ###