Merge pull request #203 from mawoka-myblock/save-game-results
This commit is contained in:
@@ -30,6 +30,7 @@ from classquiz.routers import (
|
|||||||
remote,
|
remote,
|
||||||
community,
|
community,
|
||||||
avatar,
|
avatar,
|
||||||
|
results,
|
||||||
)
|
)
|
||||||
from classquiz.socket_server import sio
|
from classquiz.socket_server import sio
|
||||||
from classquiz.helpers import meilisearch_init, telemetry_ping, bg_tasks
|
from classquiz.helpers import meilisearch_init, telemetry_ping, bg_tasks
|
||||||
@@ -83,6 +84,7 @@ async def auth_middleware_wrapper(request: Request, call_next):
|
|||||||
return await rememberme_middleware(request, call_next)
|
return await rememberme_middleware(request, call_next)
|
||||||
|
|
||||||
|
|
||||||
|
app.include_router(results.router, tags=["results"], prefix="/api/v1/results", include_in_schema=True)
|
||||||
app.include_router(remote.router, tags=["remote"], prefix="/api/v1/remote", include_in_schema=True)
|
app.include_router(remote.router, tags=["remote"], prefix="/api/v1/remote", include_in_schema=True)
|
||||||
app.include_router(login.router, tags=["auth"], prefix="/api/v1/login", include_in_schema=True)
|
app.include_router(login.router, tags=["auth"], prefix="/api/v1/login", include_in_schema=True)
|
||||||
|
|
||||||
|
|||||||
@@ -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,23 @@ 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: uuid.UUID | Quiz = ormar.ForeignKey(Quiz)
|
||||||
|
user: uuid.UUID | User = 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[list[AnswerData]] = 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)
|
||||||
|
title: str = ormar.Text(nullable=False)
|
||||||
|
description: str = ormar.Text(nullable=False)
|
||||||
|
questions: Json[list[QuizQuestion]] = ormar.JSON(nullable=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
tablename = "game_results"
|
||||||
|
metadata = metadata
|
||||||
|
database = database
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ async def generate_spreadsheet(quiz_results: dict, quiz: Quiz, player_fields: di
|
|||||||
worksheet.write(0, 5, "Wrong answers")
|
worksheet.write(0, 5, "Wrong answers")
|
||||||
for i, _ in enumerate(quiz_results):
|
for i, _ in enumerate(quiz_results):
|
||||||
question = quiz.questions[i]
|
question = quiz.questions[i]
|
||||||
print(quiz_results)
|
# print(quiz_results)
|
||||||
try:
|
try:
|
||||||
answer_data = quiz_results[str(i)]
|
answer_data = quiz_results[str(i)]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
# 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/.
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from classquiz.auth import get_current_user
|
||||||
|
from classquiz.db.models import User, GameResults
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/list", response_model=list[GameResults])
|
||||||
|
async def list_game_results(user: User = Depends(get_current_user)):
|
||||||
|
results = await GameResults.objects.all(user=user.id)
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/list/{quiz_id}", response_model=list[GameResults])
|
||||||
|
async def get_results_by_quiz(quiz_id: UUID, user: User = Depends(get_current_user)):
|
||||||
|
res = await GameResults.objects.all(user=user.id, quiz=quiz_id)
|
||||||
|
if res is None:
|
||||||
|
raise HTTPException(status_code=404, detail="Game Result not found")
|
||||||
|
else:
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{game_id}", response_model=GameResults)
|
||||||
|
async def get_game_result(game_id: UUID, user: User = Depends(get_current_user)):
|
||||||
|
res = await GameResults.objects.get_or_none(user=user.id, id=game_id)
|
||||||
|
if res is None:
|
||||||
|
raise HTTPException(status_code=404, detail="Game Result not found")
|
||||||
|
else:
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
class _SetNoteInput(BaseModel):
|
||||||
|
note: str
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/set_note", response_model=GameResults)
|
||||||
|
async def set_note(id: UUID, data: _SetNoteInput, user: User = Depends(get_current_user)):
|
||||||
|
res = await GameResults.objects.get_or_none(user=user.id, id=id)
|
||||||
|
if res is None:
|
||||||
|
raise HTTPException(status_code=404, detail="Game Result not found")
|
||||||
|
res.note = data.note
|
||||||
|
return await res.update()
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
@router.get("/export/{result_id}", response_class=StreamingResponse)
|
||||||
|
async def export_result(result_id: UUID, user: User = Depends(get_current_user)):
|
||||||
|
res = await GameResults.objects.get_or_none(user=user.id, id=result_id)
|
||||||
|
if res is None:
|
||||||
|
raise HTTPException(status_code=404, detail="Game Result not found")
|
||||||
|
quiz = Quiz(title=res.title, questions=res.questions)
|
||||||
|
spreadsheet = await generate_spreadsheet(
|
||||||
|
quiz=quiz, quiz_results=data, player_fields=player_fields, player_scores=score_data
|
||||||
|
)
|
||||||
|
|
||||||
|
def iter_file():
|
||||||
|
yield from spreadsheet
|
||||||
|
|
||||||
|
return StreamingResponse(
|
||||||
|
iter_file(),
|
||||||
|
media_type="application/vnd.ms-excel",
|
||||||
|
headers={
|
||||||
|
"Content-Disposition": f"attachment;filename=ClassQuiz-{urllib.parse.quote(quiz.title)}-{datetime.strftime('%m-%d-%Y')}.xlsx"
|
||||||
|
# noqa: E501
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
"""
|
||||||
@@ -12,10 +12,21 @@ 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,
|
||||||
|
)
|
||||||
from pydantic import BaseModel, ValidationError, validator
|
from pydantic import BaseModel, ValidationError, validator
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
from classquiz.socket_server.export_helpers import save_quiz_to_storage
|
||||||
|
|
||||||
sio = socketio.AsyncServer(async_mode="asgi", cors_allowed_origins=[])
|
sio = socketio.AsyncServer(async_mode="asgi", cors_allowed_origins=[])
|
||||||
settings = settings()
|
settings = settings()
|
||||||
|
|
||||||
@@ -272,19 +283,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 +358,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 +372,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 +387,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 +508,12 @@ 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
|
||||||
|
await save_quiz_to_storage(session["game_pin"])
|
||||||
|
await sio.emit("results_saved_successfully")
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# 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/.
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from pydantic import ValidationError
|
||||||
|
|
||||||
|
from classquiz.config import redis
|
||||||
|
from classquiz.db.models import PlayGame, GameResults
|
||||||
|
|
||||||
|
|
||||||
|
async def save_quiz_to_storage(game_pin: str):
|
||||||
|
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)):
|
||||||
|
redis_res = await redis.get(f"game_session:{game_pin}:{i}")
|
||||||
|
try:
|
||||||
|
answers.append(json.loads(redis_res))
|
||||||
|
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")
|
||||||
|
q_return = []
|
||||||
|
for q in game.questions:
|
||||||
|
q_return.append(q.dict())
|
||||||
|
data = GameResults(
|
||||||
|
id=game.game_id,
|
||||||
|
quiz=game.quiz_id,
|
||||||
|
user=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),
|
||||||
|
title=game.title,
|
||||||
|
description=game.description,
|
||||||
|
questions=json.dumps(q_return),
|
||||||
|
)
|
||||||
|
await data.save()
|
||||||
@@ -157,7 +157,11 @@
|
|||||||
"backup_code": "Backup-code",
|
"backup_code": "Backup-code",
|
||||||
"totp": "Totp",
|
"totp": "Totp",
|
||||||
"text": "Text",
|
"text": "Text",
|
||||||
"order": "order"
|
"order": "order",
|
||||||
|
"results": "Results",
|
||||||
|
"note": "Note",
|
||||||
|
"player_plural": "Players",
|
||||||
|
"score": "Score"
|
||||||
},
|
},
|
||||||
"editor": {
|
"editor": {
|
||||||
"time_in_seconds": "Time in seconds",
|
"time_in_seconds": "Time in seconds",
|
||||||
@@ -189,7 +193,8 @@
|
|||||||
"show_next_question": "Show next question",
|
"show_next_question": "Show next question",
|
||||||
"start_by_showing_first_question": "Start by showing the first question.",
|
"start_by_showing_first_question": "Start by showing the first question.",
|
||||||
"no_answers": "No answers!",
|
"no_answers": "No answers!",
|
||||||
"stop_time": "Stop time"
|
"stop_time": "Stop time",
|
||||||
|
"save_results": "Save results"
|
||||||
},
|
},
|
||||||
"password_reset_page": {
|
"password_reset_page": {
|
||||||
"reset_password": "Reset password"
|
"reset_password": "Reset password"
|
||||||
@@ -253,5 +258,19 @@
|
|||||||
"clothe_graphic_type": "Graphic",
|
"clothe_graphic_type": "Graphic",
|
||||||
"thats_you": "That's You!",
|
"thats_you": "That's You!",
|
||||||
"start_over": "Start over"
|
"start_over": "Start over"
|
||||||
|
},
|
||||||
|
"results_page": {
|
||||||
|
"no_results_so_far": "No results saved so far...",
|
||||||
|
"quiz_title": "Quiz Title",
|
||||||
|
"date_played": "Date Played",
|
||||||
|
"player_count": "Player count"
|
||||||
|
},
|
||||||
|
"result_page": {
|
||||||
|
"player_name": "Player name",
|
||||||
|
"custom_field": "Custom field",
|
||||||
|
"average_score": "Average score: {{average_score}}",
|
||||||
|
"correct_answer": "{{count}} correct answer",
|
||||||
|
"correct_answer_plural": "{{count}} correct answers",
|
||||||
|
"time_taken": "Time taken"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import FinalResults from '$lib/play/admin/final_results.svelte';
|
import FinalResults from '$lib/play/admin/final_results.svelte';
|
||||||
|
import GrayButton from '$lib/components/buttons/gray.svelte';
|
||||||
|
|
||||||
navbarVisible.set(false);
|
navbarVisible.set(false);
|
||||||
|
|
||||||
@@ -98,6 +99,10 @@
|
|||||||
}, 200);
|
}, 200);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
socket.on('results_saved_successfully', (_) => {
|
||||||
|
results_saved = true;
|
||||||
|
});
|
||||||
|
|
||||||
const confirmUnload = () => {
|
const confirmUnload = () => {
|
||||||
if (warnToLeave) {
|
if (warnToLeave) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -110,6 +115,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) {
|
||||||
@@ -132,6 +140,7 @@
|
|||||||
};
|
};
|
||||||
let bg_color;
|
let bg_color;
|
||||||
let bg_image;
|
let bg_image;
|
||||||
|
let results_saved = false;
|
||||||
$: bg_color = quiz_data ? quiz_data.background_color : undefined;
|
$: bg_color = quiz_data ? quiz_data.background_color : undefined;
|
||||||
$: bg_image = quiz_data ? quiz_data.background_image : undefined;
|
$: bg_image = quiz_data ? quiz_data.background_image : undefined;
|
||||||
let show_final_results = false;
|
let show_final_results = false;
|
||||||
@@ -152,10 +161,35 @@
|
|||||||
{#if JSON.stringify(final_results) !== JSON.stringify([null])}
|
{#if JSON.stringify(final_results) !== JSON.stringify([null])}
|
||||||
{#if control_visible}
|
{#if control_visible}
|
||||||
<div class="w-screen flex justify-center mt-16">
|
<div class="w-screen flex justify-center mt-16">
|
||||||
<button on:click={request_answer_export} class="admin-button"
|
<div class="w-fit">
|
||||||
>{$t('admin_page.export_results')}</button
|
<GrayButton on:click={request_answer_export}
|
||||||
|
>{$t('admin_page.export_results')}</GrayButton
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="w-screen flex justify-center mt-2">
|
||||||
|
<div class="w-fit">
|
||||||
|
<GrayButton on:click={save_quiz} flex={true} disabled={results_saved}>
|
||||||
|
{#if results_saved}
|
||||||
|
<svg
|
||||||
|
class="w-4 h-4"
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M5 13l4 4L19 7"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{:else}{$t('admin_page.save_results')}{/if}
|
||||||
|
</GrayButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<FinalResults bind:data={player_scores} bind:show_final_results />
|
<FinalResults bind:data={player_scores} bind:show_final_results />
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -103,7 +103,7 @@
|
|||||||
<div class="w-full grid grid-cols-4 gap-2">
|
<div class="w-full grid grid-cols-4 gap-2">
|
||||||
<BrownButton href="/create">{$t('words.create')}</BrownButton>
|
<BrownButton href="/create">{$t('words.create')}</BrownButton>
|
||||||
<BrownButton href="/import">{$t('words.import')}</BrownButton>
|
<BrownButton href="/import">{$t('words.import')}</BrownButton>
|
||||||
<BrownButton href="/api/v1/users/logout">{$t('words.logout')}</BrownButton>
|
<BrownButton href="/results">{$t('words.results')}</BrownButton>
|
||||||
<BrownButton href="/account/settings">
|
<BrownButton href="/account/settings">
|
||||||
{$t('words.settings')}
|
{$t('words.settings')}
|
||||||
</BrownButton>
|
</BrownButton>
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
<!--
|
||||||
|
- 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 { PageData } from './$types';
|
||||||
|
import { getLocalization } from '$lib/i18n';
|
||||||
|
|
||||||
|
const { t } = getLocalization();
|
||||||
|
|
||||||
|
export let data: PageData;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="w-full">
|
||||||
|
<div class="flex justify-center w-full">
|
||||||
|
<div class="w-11/12 m-auto">
|
||||||
|
{#if data.results.length === 0}
|
||||||
|
<p class="text-center text-3xl mt-8">{$t('results_page.no_results_so_far')}</p>
|
||||||
|
{:else}
|
||||||
|
<table class="w-full">
|
||||||
|
<tr class="border-b-2 dark:border-gray-500 text-left border-gray-300">
|
||||||
|
<th class="border-r dark:border-gray-500 p-1 mx-auto border-gray-300"
|
||||||
|
>{$t('results_page.quiz_title')}
|
||||||
|
</th>
|
||||||
|
<th class="border-r dark:border-gray-500 p-1 mx-auto border-gray-300"
|
||||||
|
>{$t('results_page.date_played')}
|
||||||
|
</th>
|
||||||
|
<th class="border-r dark:border-gray-500 p-1 mx-auto border-gray-300"
|
||||||
|
>{$t('results_page.player_count')}
|
||||||
|
</th>
|
||||||
|
<th class="mx-auto p-1">{$t('words.note')}</th>
|
||||||
|
</tr>
|
||||||
|
{#each data.results as result}
|
||||||
|
<tr class="text-left">
|
||||||
|
<td class="border-r dark:border-gray-500 p-1 border-gray-300"
|
||||||
|
><a href="/results/{result.id}" class="underline text-lg"
|
||||||
|
>{result.title}</a
|
||||||
|
></td
|
||||||
|
>
|
||||||
|
<td class="border-r dark:border-gray-500 p-1 border-gray-300"
|
||||||
|
>{new Date(result.timestamp).toLocaleString()}</td
|
||||||
|
>
|
||||||
|
<td class="border-r dark:border-gray-500 p-1 border-gray-300"
|
||||||
|
>{Object.keys(result.player_scores).length}</td
|
||||||
|
>
|
||||||
|
<td class:p-1={result.note}>
|
||||||
|
{#if result.note}
|
||||||
|
{result.note}
|
||||||
|
{/if}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</table>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
/*
|
||||||
|
* 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/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { PageLoad } from './$types';
|
||||||
|
|
||||||
|
export const load = (async ({ fetch }) => {
|
||||||
|
const res = await fetch('/api/v1/results/list?include_quiz=true');
|
||||||
|
let json;
|
||||||
|
if (res.ok) {
|
||||||
|
json = await res.json();
|
||||||
|
} else {
|
||||||
|
json = [];
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
results: json
|
||||||
|
};
|
||||||
|
}) satisfies PageLoad;
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
<!--
|
||||||
|
- 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 { PageData } from './$types';
|
||||||
|
import PlayerOverview from './player_overview.svelte';
|
||||||
|
import QuestionOverview from './question_overview.svelte';
|
||||||
|
import GeneralOverview from './general_overview.svelte';
|
||||||
|
import { fade } from 'svelte/transition';
|
||||||
|
import { getLocalization } from '$lib/i18n';
|
||||||
|
|
||||||
|
const { t } = getLocalization();
|
||||||
|
|
||||||
|
export let data: PageData;
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
enum SelectedTab {
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
Overview,
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
Players,
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
Questions
|
||||||
|
}
|
||||||
|
|
||||||
|
let selected_tab: SelectedTab = SelectedTab.Overview;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="w-full">
|
||||||
|
<div class="flex flex-row w-full justify-around border-b-2 border-gray-500 mb-4">
|
||||||
|
<div
|
||||||
|
class="w-full py-2 flex transition-all hover:opacity-100"
|
||||||
|
class:text-lg={selected_tab === SelectedTab.Overview}
|
||||||
|
class:opacity-60={selected_tab !== SelectedTab.Overview}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
on:click={() => {
|
||||||
|
selected_tab = SelectedTab.Overview;
|
||||||
|
}}
|
||||||
|
class="m-auto w-full h-full"
|
||||||
|
>{$t('words.overview')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="w-full py-2 flex border-x-2 border-gray-500 transition-all hover:opacity-100"
|
||||||
|
class:text-lg={selected_tab === SelectedTab.Players}
|
||||||
|
class:opacity-60={selected_tab !== SelectedTab.Players}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
on:click={() => {
|
||||||
|
selected_tab = SelectedTab.Players;
|
||||||
|
}}
|
||||||
|
class="m-auto w-full h-full"
|
||||||
|
>
|
||||||
|
{$t('words.player', { count: 2 })}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="w-full py-2 flex transition-all hover:opacity-100"
|
||||||
|
class:text-lg={selected_tab === SelectedTab.Questions}
|
||||||
|
class:opacity-60={selected_tab !== SelectedTab.Questions}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
on:click={() => {
|
||||||
|
selected_tab = SelectedTab.Questions;
|
||||||
|
}}
|
||||||
|
class="m-auto w-full h-full"
|
||||||
|
>{$t('words.question', { count: 2 })}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{#if selected_tab === SelectedTab.Overview}
|
||||||
|
<div in:fade={{ duration: 150 }}>
|
||||||
|
<GeneralOverview
|
||||||
|
questions={data.results.questions}
|
||||||
|
answers={data.results.answers}
|
||||||
|
scores={data.results.player_scores}
|
||||||
|
title={data.results.title}
|
||||||
|
timestamp={data.results.timestamp}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{:else if selected_tab === SelectedTab.Questions}
|
||||||
|
<div in:fade={{ duration: 150 }}>
|
||||||
|
<QuestionOverview questions={data.results.questions} answers={data.results.answers} />
|
||||||
|
</div>
|
||||||
|
{:else if selected_tab === SelectedTab.Players}
|
||||||
|
<div in:fade={{ duration: 150 }}>
|
||||||
|
<PlayerOverview
|
||||||
|
custom_field={data.results.custom_field_data}
|
||||||
|
scores={data.results.player_scores}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
/*
|
||||||
|
* 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/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// import type { PageLoad } from './$types';
|
||||||
|
|
||||||
|
export const load = async ({ params, fetch }) => {
|
||||||
|
const res = await fetch(`/api/v1/results/${params.result_id}?include_quiz=true`);
|
||||||
|
let json;
|
||||||
|
if (res.ok) {
|
||||||
|
json = await res.json();
|
||||||
|
} else {
|
||||||
|
json = undefined;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
results: json
|
||||||
|
};
|
||||||
|
}; //satisfies PageLoad;
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<!--
|
||||||
|
- 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 questions: Question[];
|
||||||
|
export let answers: {
|
||||||
|
username: string;
|
||||||
|
answer: string;
|
||||||
|
right: boolean;
|
||||||
|
tike_taken: number;
|
||||||
|
score: number;
|
||||||
|
}[][];
|
||||||
|
|
||||||
|
export let scores: {
|
||||||
|
[key: string]: string;
|
||||||
|
};
|
||||||
|
export let title: string;
|
||||||
|
export let timestamp: string;
|
||||||
|
|
||||||
|
const usernames = Object.keys(scores);
|
||||||
|
|
||||||
|
const get_average_final_score = () => {
|
||||||
|
let score_data = 0;
|
||||||
|
for (const username of usernames) {
|
||||||
|
score_data += parseInt(scores[username]);
|
||||||
|
}
|
||||||
|
return score_data / usernames.length;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
- 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/.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<div class="w-full">
|
||||||
|
<div class="flex justify-center w-full">
|
||||||
|
<p class="text-3xl w-5/6 text-center">
|
||||||
|
The quiz with the title '<strong>{title}</strong>' was played on
|
||||||
|
<strong>{new Date(timestamp).toLocaleString()}</strong>
|
||||||
|
with <strong>{usernames.length}</strong> players. The players achieved an average score
|
||||||
|
of <strong>{get_average_final_score()}</strong>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
underline {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<!--
|
||||||
|
- 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 { getLocalization } from '$lib/i18n';
|
||||||
|
|
||||||
|
const { t } = getLocalization();
|
||||||
|
export let scores: {
|
||||||
|
[key: string]: string;
|
||||||
|
};
|
||||||
|
export let custom_field: {
|
||||||
|
[key: string]: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
let usernames = Object.keys(scores);
|
||||||
|
console.log(custom_field);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="w-full">
|
||||||
|
<div class="flex justify-center w-full">
|
||||||
|
<table class="w-11/12 m-auto">
|
||||||
|
<tr class="border-b-2 dark:border-gray-500 text-left border-gray-300">
|
||||||
|
<th class="border-r dark:border-gray-500 p-1 mx-auto border-gray-300"
|
||||||
|
>{$t('result_page.player_name')}
|
||||||
|
</th>
|
||||||
|
<th class="p-1 mx-auto">Player Score</th>
|
||||||
|
{#if Object.keys(custom_field).length !== 0}
|
||||||
|
<th class="border-l dark:border-gray-500 p-1 mx-auto border-gray-300"
|
||||||
|
>{$t('result_page.custom_field')}
|
||||||
|
</th>
|
||||||
|
{/if}
|
||||||
|
</tr>
|
||||||
|
{#each usernames as uname}
|
||||||
|
<tr class="text-left">
|
||||||
|
<td class="border-r dark:border-gray-500 p-1 border-gray-300">{uname}</td>
|
||||||
|
<td class="p-1">{scores[uname]}</td>
|
||||||
|
{#if custom_field[uname]}
|
||||||
|
<td class="border-l dark:border-gray-500 p-1 border-gray-300"
|
||||||
|
>{custom_field[uname]}</td
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
<!--
|
||||||
|
- 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';
|
||||||
|
import { fly } from 'svelte/transition';
|
||||||
|
import QuestionTab from './question_tab_thing.svelte';
|
||||||
|
import { QuizQuestionType } from '$lib/quiz_types';
|
||||||
|
import { getLocalization } from '$lib/i18n';
|
||||||
|
|
||||||
|
const { t } = getLocalization();
|
||||||
|
|
||||||
|
export let questions: Question[];
|
||||||
|
export let answers: {
|
||||||
|
username: string;
|
||||||
|
answer: string;
|
||||||
|
right: boolean;
|
||||||
|
tike_taken: number;
|
||||||
|
score: number;
|
||||||
|
}[][];
|
||||||
|
|
||||||
|
const get_average_score = (q_index: number): number => {
|
||||||
|
const q_answers = answers[q_index];
|
||||||
|
let summed_up_scores = 0;
|
||||||
|
for (const answer of q_answers) {
|
||||||
|
summed_up_scores = summed_up_scores + answer.score;
|
||||||
|
}
|
||||||
|
return summed_up_scores / q_answers.length;
|
||||||
|
};
|
||||||
|
|
||||||
|
const get_number_of_correct_answers = (q_index: number): number => {
|
||||||
|
const q_answers = answers[q_index];
|
||||||
|
let correct_answer = 0;
|
||||||
|
for (const answer of q_answers) {
|
||||||
|
if (answer.right) {
|
||||||
|
correct_answer++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return correct_answer;
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggle_dropdown = (q_index: number) => {
|
||||||
|
if (question_open === q_index) {
|
||||||
|
question_open = false;
|
||||||
|
} else {
|
||||||
|
question_open = q_index;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let question_open: number | boolean = false;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="w-full flex justify-center">
|
||||||
|
<div class="w-11/12 flex flex-col w-full gap-4">
|
||||||
|
{#each questions as question, i}
|
||||||
|
<div class="transition-all">
|
||||||
|
<div class="w-full bg-white bg-opacity-60 p-2 rounded grid grid-cols-3 z-40">
|
||||||
|
<button
|
||||||
|
class="text-center underline text-xl"
|
||||||
|
on:click={() => {
|
||||||
|
toggle_dropdown(i);
|
||||||
|
}}>{@html question.question}</button
|
||||||
|
>
|
||||||
|
{#if question.type !== QuizQuestionType.VOTING}
|
||||||
|
{@const correct_answers = get_number_of_correct_answers(i)}
|
||||||
|
<p class="text-center text-sm my-auto">
|
||||||
|
{$t('result_page.average_score', {
|
||||||
|
average_score: get_average_score(i)
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
<p class="text-center text-sm my-auto">
|
||||||
|
{$t('result_page.correct_answer', { count: correct_answers })}
|
||||||
|
<!-- {correct_answers} correct
|
||||||
|
{#if correct_answers === 1}Answer{:else}Answers{/if}-->
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{#if question_open === i}
|
||||||
|
<div in:fly|local={{ y: -10 }}>
|
||||||
|
<QuestionTab {question} answers={answers[i]} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
<!--
|
||||||
|
- 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 { QuizQuestionType } from '$lib/quiz_types';
|
||||||
|
import { getLocalization } from '$lib/i18n';
|
||||||
|
|
||||||
|
const { t } = getLocalization();
|
||||||
|
|
||||||
|
export let question;
|
||||||
|
|
||||||
|
interface Answer {
|
||||||
|
username: string;
|
||||||
|
answer: string;
|
||||||
|
right: boolean;
|
||||||
|
time_taken: number;
|
||||||
|
score: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export let answers: Answer[];
|
||||||
|
// console.log(question);
|
||||||
|
|
||||||
|
const get_answer_count_for_answer = (answer: string): number => {
|
||||||
|
let count = 0;
|
||||||
|
for (const a of answers) {
|
||||||
|
if (a.answer === answer) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<div class="bg-white p-2 -z-10 w-10/12 rounded">
|
||||||
|
{#if question.type !== QuizQuestionType.ORDER && question.type !== QuizQuestionType.RANGE}
|
||||||
|
<div class="flex flex-col mb-4">
|
||||||
|
{#each question.answers as answer}
|
||||||
|
<div class="grid grid-cols-4">
|
||||||
|
<p>{answer.answer}</p>
|
||||||
|
<div
|
||||||
|
class="col-span-3 flex w-full border-l border-gray-300 px-1 dark:border-gray-500"
|
||||||
|
>
|
||||||
|
<div class="my-auto w-full mr-1">
|
||||||
|
<span
|
||||||
|
class="h-1 block bg-green-600 my-auto"
|
||||||
|
style="width: {(get_answer_count_for_answer(answer.answer) /
|
||||||
|
answers.length) *
|
||||||
|
100}%"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p>{get_answer_count_for_answer(answer.answer)}</p>
|
||||||
|
{#if question.type !== QuizQuestionType.VOTING && question.type !== QuizQuestionType.TEXT}
|
||||||
|
<p class="ml-1">
|
||||||
|
{#if answer.right}✅{:else}❌{/if}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div>
|
||||||
|
<table class="w-full text-left">
|
||||||
|
<tr class="border-b-2 dark:border-gray-500 text-left border-gray-300">
|
||||||
|
<th class="border-r dark:border-gray-500 p-1 mx-auto border-gray-300"
|
||||||
|
>{$t('result_page.player_name')}
|
||||||
|
</th>
|
||||||
|
{#if question.type !== QuizQuestionType.VOTING}
|
||||||
|
<th class="border-r dark:border-gray-500 p-1 mx-auto border-gray-300"
|
||||||
|
>{$t('words.score')}</th
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
<th class="border-r dark:border-gray-500 p-1 mx-auto border-gray-300"
|
||||||
|
>{$t('result_page.time_taken')}
|
||||||
|
</th>
|
||||||
|
<th class="p-1 mx-auto">{$t('words.answer')} </th>
|
||||||
|
{#if question.type !== QuizQuestionType.VOTING}
|
||||||
|
<th class="border-l dark:border-gray-500 p-1 mx-auto border-gray-300"
|
||||||
|
>{$t('words.correct')}?</th
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
</tr>
|
||||||
|
{#each answers as answer}
|
||||||
|
<tr>
|
||||||
|
<td class="border-r dark:border-gray-500 p-1 border-gray-300"
|
||||||
|
>{answer.username}</td
|
||||||
|
>
|
||||||
|
{#if question.type !== QuizQuestionType.VOTING}
|
||||||
|
<td class="border-r dark:border-gray-500 p-1 border-gray-300"
|
||||||
|
>{answer.score}</td
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
<td class="border-r dark:border-gray-500 p-1 border-gray-300"
|
||||||
|
>{(answer.time_taken / 1000).toFixed(3)}s
|
||||||
|
</td>
|
||||||
|
<td class="p-1">{answer.answer}</td>
|
||||||
|
{#if question.type !== QuizQuestionType.VOTING}
|
||||||
|
<td class="p-1 border-l dark:border-gray-500 border-gray-300">
|
||||||
|
{#if answer.right}✅{:else}❌{/if}
|
||||||
|
</td>
|
||||||
|
{/if}
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -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,46 @@
|
|||||||
|
"""Added game_results
|
||||||
|
|
||||||
|
Revision ID: 438516c09cf3
|
||||||
|
Revises: 7ad8502af419
|
||||||
|
Create Date: 2023-02-13 17:34:05.358665
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
import ormar
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = "438516c09cf3"
|
||||||
|
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", ormar.fields.sqlalchemy_uuid.CHAR(32), nullable=True),
|
||||||
|
sa.Column("user", 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.Column("title", sa.Text(), nullable=False),
|
||||||
|
sa.Column("description", sa.Text(), nullable=False),
|
||||||
|
sa.Column("questions", sa.JSON(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(["quiz"], ["quiz.id"], name="fk_game_results_quiz_id_quiz"),
|
||||||
|
sa.ForeignKeyConstraint(["user"], ["users.id"], name="fk_game_results_users_id_user"),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_table("game_results")
|
||||||
|
# ### end Alembic commands ###
|
||||||
Reference in New Issue
Block a user