Finished game-results

This commit is contained in:
Mawoka
2023-02-13 17:37:05 +01:00
parent f0c2c4250f
commit 3646fcf90b
16 changed files with 355 additions and 154 deletions
+3
View File
@@ -308,6 +308,9 @@ class GameResults(ormar.Model):
answers: Json[list[AnswerData]] = ormar.JSON(True) answers: Json[list[AnswerData]] = ormar.JSON(True)
player_scores: Json[dict[str, str]] = ormar.JSON(nullable=True) player_scores: Json[dict[str, str]] = ormar.JSON(nullable=True)
custom_field_data: Json[dict[str, str]] | None = 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: class Meta:
tablename = "game_results" tablename = "game_results"
+1 -1
View File
@@ -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:
+32 -15
View File
@@ -13,20 +13,14 @@ router = APIRouter()
@router.get("/list", response_model=list[GameResults]) @router.get("/list", response_model=list[GameResults])
async def list_game_results(include_quiz: bool = False, user: User = Depends(get_current_user)): async def list_game_results(user: User = Depends(get_current_user)):
if include_quiz is True: results = await GameResults.objects.all(user=user.id)
results = await GameResults.objects.select_related("quiz").all(user=user.id)
else:
results = await GameResults.objects.all(user=user.id)
return results return results
@router.get("/list/{quiz_id}", response_model=list[GameResults]) @router.get("/list/{quiz_id}", response_model=list[GameResults])
async def get_results_by_quiz(quiz_id: UUID, include_quiz: bool = False, user: User = Depends(get_current_user)): async def get_results_by_quiz(quiz_id: UUID, user: User = Depends(get_current_user)):
if include_quiz is True: res = await GameResults.objects.all(user=user.id, quiz=quiz_id)
res = await GameResults.objects.select_related("quiz").all(user=user.id, quiz=quiz_id)
else:
res = await GameResults.objects.all(user=user.id, quiz=quiz_id)
if res is None: if res is None:
raise HTTPException(status_code=404, detail="Game Result not found") raise HTTPException(status_code=404, detail="Game Result not found")
else: else:
@@ -34,11 +28,8 @@ async def get_results_by_quiz(quiz_id: UUID, include_quiz: bool = False, user: U
@router.get("/{game_id}", response_model=GameResults) @router.get("/{game_id}", response_model=GameResults)
async def get_game_result(game_id: UUID, include_quiz: bool = False, user: User = Depends(get_current_user)): async def get_game_result(game_id: UUID, user: User = Depends(get_current_user)):
if include_quiz: res = await GameResults.objects.get_or_none(user=user.id, id=game_id)
res = await GameResults.objects.select_related("quiz").get_or_none(user=user.id, id=game_id)
else:
res = await GameResults.objects.get_or_none(user=user.id, id=game_id)
if res is None: if res is None:
raise HTTPException(status_code=404, detail="Game Result not found") raise HTTPException(status_code=404, detail="Game Result not found")
else: else:
@@ -56,3 +47,29 @@ async def set_note(id: UUID, data: _SetNoteInput, user: User = Depends(get_curre
raise HTTPException(status_code=404, detail="Game Result not found") raise HTTPException(status_code=404, detail="Game Result not found")
res.note = data.note res.note = data.note
return await res.update() 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
},
)
"""
+4 -24
View File
@@ -21,11 +21,12 @@ from classquiz.db.models import (
VotingQuizAnswer, VotingQuizAnswer,
AnswerDataList, AnswerDataList,
AnswerData, AnswerData,
GameResults,
) )
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()
@@ -514,26 +515,5 @@ async def save_quiz(sid: str):
session: dict = await sio.get_session(sid) session: dict = await sio.get_session(sid)
if not session["admin"]: if not session["admin"]:
return return
game_pin = session["game_pin"] await save_quiz_to_storage(session["game_pin"])
game = PlayGame.parse_raw(await redis.get(f"game:{game_pin}")) await sio.emit("results_saved_successfully")
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")
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),
)
await data.save()
+41
View File
@@ -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()
+21 -2
View File
@@ -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"
} }
} }
+33 -7
View File
@@ -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();
@@ -135,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;
@@ -155,14 +161,34 @@
{#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"> <div class="w-screen flex justify-center mt-2">
<button on:click={save_quiz} class="admin-button" <div class="w-fit">
>{$t('admin_page.save_results')}</button <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> </div>
{/if} {/if}
<FinalResults bind:data={player_scores} bind:show_final_results /> <FinalResults bind:data={player_scores} bind:show_final_results />
+1 -1
View File
@@ -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>
+40 -33
View File
@@ -5,6 +5,9 @@
--> -->
<script lang="ts"> <script lang="ts">
import type { PageData } from './$types'; import type { PageData } from './$types';
import { getLocalization } from '$lib/i18n';
const { t } = getLocalization();
export let data: PageData; export let data: PageData;
</script> </script>
@@ -12,40 +15,44 @@
<div class="w-full"> <div class="w-full">
<div class="flex justify-center w-full"> <div class="flex justify-center w-full">
<div class="w-11/12 m-auto"> <div class="w-11/12 m-auto">
<table class="w-full"> {#if data.results.length === 0}
<tr class="border-b-2 dark:border-gray-500 text-left border-gray-300"> <p class="text-center text-3xl mt-8">{$t('results_page.no_results_so_far')}</p>
<th class="border-r dark:border-gray-500 p-1 mx-auto border-gray-300" {:else}
>Quiz Title <table class="w-full">
</th> <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" <th class="border-r dark:border-gray-500 p-1 mx-auto border-gray-300"
>Date Played >{$t('results_page.quiz_title')}
</th> </th>
<th class="border-r dark:border-gray-500 p-1 mx-auto border-gray-300" <th class="border-r dark:border-gray-500 p-1 mx-auto border-gray-300"
>Player count >{$t('results_page.date_played')}
</th> </th>
<th class="mx-auto p-1">Note</th> <th class="border-r dark:border-gray-500 p-1 mx-auto border-gray-300"
</tr> >{$t('results_page.player_count')}
{#each data.results as result} </th>
<tr class="text-left"> <th class="mx-auto p-1">{$t('words.note')}</th>
<td class="border-r dark:border-gray-500 p-1 border-gray-300"
><a href="/results/{result.id}" class="underline text-lg"
>{result.quiz.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> </tr>
{/each} {#each data.results as result}
</table> <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> </div>
</div> </div>
@@ -7,6 +7,11 @@
import type { PageData } from './$types'; import type { PageData } from './$types';
import PlayerOverview from './player_overview.svelte'; import PlayerOverview from './player_overview.svelte';
import QuestionOverview from './question_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; export let data: PageData;
@@ -35,7 +40,7 @@
selected_tab = SelectedTab.Overview; selected_tab = SelectedTab.Overview;
}} }}
class="m-auto w-full h-full" class="m-auto w-full h-full"
>Overview >{$t('words.overview')}
</button> </button>
</div> </div>
<div <div
@@ -49,7 +54,7 @@
}} }}
class="m-auto w-full h-full" class="m-auto w-full h-full"
> >
Players {$t('words.player', { count: 2 })}
</button> </button>
</div> </div>
<div <div
@@ -62,16 +67,26 @@
selected_tab = SelectedTab.Questions; selected_tab = SelectedTab.Questions;
}} }}
class="m-auto w-full h-full" class="m-auto w-full h-full"
>Questions >{$t('words.question', { count: 2 })}
</button> </button>
</div> </div>
</div> </div>
{#if selected_tab === SelectedTab.Overview}{:else if selected_tab === SelectedTab.Questions} {#if selected_tab === SelectedTab.Overview}
<div> <div in:fade={{ duration: 150 }}>
<QuestionOverview quiz={data.results.quiz} answers={data.results.answers} /> <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> </div>
{:else if selected_tab === SelectedTab.Players} {:else if selected_tab === SelectedTab.Players}
<div> <div in:fade={{ duration: 150 }}>
<PlayerOverview <PlayerOverview
custom_field={data.results.custom_field_data} custom_field={data.results.custom_field_data}
scores={data.results.player_scores} scores={data.results.player_scores}
@@ -4,9 +4,9 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/ */
import type { PageLoad } from './$types'; // import type { PageLoad } from './$types';
export const load = (async ({ params, fetch }) => { export const load = async ({ params, fetch }) => {
const res = await fetch(`/api/v1/results/${params.result_id}?include_quiz=true`); const res = await fetch(`/api/v1/results/${params.result_id}?include_quiz=true`);
let json; let json;
if (res.ok) { if (res.ok) {
@@ -17,4 +17,4 @@ export const load = (async ({ params, fetch }) => {
return { return {
results: json results: json
}; };
}) satisfies PageLoad; }; //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>
@@ -4,6 +4,9 @@
- file, You can obtain one at https://mozilla.org/MPL/2.0/. - file, You can obtain one at https://mozilla.org/MPL/2.0/.
--> -->
<script lang="ts"> <script lang="ts">
import { getLocalization } from '$lib/i18n';
const { t } = getLocalization();
export let scores: { export let scores: {
[key: string]: string; [key: string]: string;
}; };
@@ -20,13 +23,13 @@
<table class="w-11/12 m-auto"> <table class="w-11/12 m-auto">
<tr class="border-b-2 dark:border-gray-500 text-left border-gray-300"> <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" <th class="border-r dark:border-gray-500 p-1 mx-auto border-gray-300"
>Player name</th >{$t('result_page.player_name')}
> </th>
<th class="p-1 mx-auto">Player Score</th> <th class="p-1 mx-auto">Player Score</th>
{#if Object.keys(custom_field).length !== 0} {#if Object.keys(custom_field).length !== 0}
<th class="border-l dark:border-gray-500 p-1 mx-auto border-gray-300" <th class="border-l dark:border-gray-500 p-1 mx-auto border-gray-300"
>Custom field</th >{$t('result_page.custom_field')}
> </th>
{/if} {/if}
</tr> </tr>
{#each usernames as uname} {#each usernames as uname}
@@ -4,11 +4,15 @@
- file, You can obtain one at https://mozilla.org/MPL/2.0/. - file, You can obtain one at https://mozilla.org/MPL/2.0/.
--> -->
<script lang="ts"> <script lang="ts">
import type { QuizData } from '$lib/quiz_types'; import type { Question } from '$lib/quiz_types';
import { fly } from 'svelte/transition'; import { fly } from 'svelte/transition';
import QuestionTab from './question_tab_thing.svelte'; import QuestionTab from './question_tab_thing.svelte';
import { QuizQuestionType } from '$lib/quiz_types';
import { getLocalization } from '$lib/i18n';
export let quiz: QuizData; const { t } = getLocalization();
export let questions: Question[];
export let answers: { export let answers: {
username: string; username: string;
answer: string; answer: string;
@@ -48,23 +52,32 @@
</script> </script>
<div class="w-full flex justify-center"> <div class="w-full flex justify-center">
<div class="w-11/12 flex flex-col w-full"> <div class="w-11/12 flex flex-col w-full gap-4">
{#each quiz.questions as question, i} {#each questions as question, i}
<div> <div class="transition-all">
<div class="w-full bg-white bg-opacity-60 p-2 rounded grid grid-cols-3 z-40"> <div class="w-full bg-white bg-opacity-60 p-2 rounded grid grid-cols-3 z-40">
<button <button
class="text-center underline text-xl" class="text-center underline text-xl"
on:click={() => { on:click={() => {
toggle_dropdown(i); toggle_dropdown(i);
}}>{question.question}</button }}>{@html question.question}</button
> >
<p class="text-center text-sm my-auto">Average Score: {get_average_score(i)}</p> {#if question.type !== QuizQuestionType.VOTING}
<p class="text-center text-sm my-auto"> {@const correct_answers = get_number_of_correct_answers(i)}
{get_number_of_correct_answers(i)} correct Answer(s) <p class="text-center text-sm my-auto">
</p> {$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> </div>
{#if question_open === i} {#if question_open === i}
<div transition:fly|local={{ y: -10 }}> <div in:fly|local={{ y: -10 }}>
<QuestionTab {question} answers={answers[i]} /> <QuestionTab {question} answers={answers[i]} />
</div> </div>
{/if} {/if}
@@ -4,6 +4,11 @@
- file, You can obtain one at https://mozilla.org/MPL/2.0/. - file, You can obtain one at https://mozilla.org/MPL/2.0/.
--> -->
<script lang="ts"> <script lang="ts">
import { QuizQuestionType } from '$lib/quiz_types';
import { getLocalization } from '$lib/i18n';
const { t } = getLocalization();
export let question; export let question;
interface Answer { interface Answer {
@@ -15,7 +20,7 @@
} }
export let answers: Answer[]; export let answers: Answer[];
console.log(question); // console.log(question);
const get_answer_count_for_answer = (answer: string): number => { const get_answer_count_for_answer = (answer: string): number => {
let count = 0; let count = 0;
@@ -30,60 +35,73 @@
<div class="flex justify-center"> <div class="flex justify-center">
<div class="bg-white p-2 -z-10 w-10/12 rounded"> <div class="bg-white p-2 -z-10 w-10/12 rounded">
<div class="flex flex-col"> {#if question.type !== QuizQuestionType.ORDER && question.type !== QuizQuestionType.RANGE}
{#each question.answers as answer} <div class="flex flex-col mb-4">
<div class="grid grid-cols-4"> {#each question.answers as answer}
<p>{answer.answer}</p> <div class="grid grid-cols-4">
<div <p>{answer.answer}</p>
class="col-span-3 flex w-full border-l border-gray-300 px-1 dark:border-gray-500" <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 <div class="my-auto w-full mr-1">
class="h-1 block bg-green-600 my-auto" <span
style="width: {(get_answer_count_for_answer(answer.answer) / class="h-1 block bg-green-600 my-auto"
answers.length) * style="width: {(get_answer_count_for_answer(answer.answer) /
100}%" 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>
<p>{get_answer_count_for_answer(answer.answer)}</p>
<p class="ml-1">
{#if answer.right}{:else}{/if}
</p>
</div> </div>
</div> {/each}
{/each} </div>
</div> {/if}
<div class="mt-4"> <div>
<table class="w-full text-left"> <table class="w-full text-left">
<tr class="border-b-2 dark:border-gray-500 text-left border-gray-300"> <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" <th class="border-r dark:border-gray-500 p-1 mx-auto border-gray-300"
>Player Name</th >{$t('result_page.player_name')}
> </th>
<th class="border-r dark:border-gray-500 p-1 mx-auto border-gray-300">Score</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" <th class="border-r dark:border-gray-500 p-1 mx-auto border-gray-300"
>Time Taken</th >{$t('result_page.time_taken')}
> </th>
<th class="border-r dark:border-gray-500 p-1 mx-auto border-gray-300">Answer</th <th class="p-1 mx-auto">{$t('words.answer')} </th>
> {#if question.type !== QuizQuestionType.VOTING}
<th class="p-1">Correct?</th> <th class="border-l dark:border-gray-500 p-1 mx-auto border-gray-300"
>{$t('words.correct')}?</th
>
{/if}
</tr> </tr>
{#each answers as answer} {#each answers as answer}
<tr> <tr>
<td class="border-r dark:border-gray-500 p-1 border-gray-300" <td class="border-r dark:border-gray-500 p-1 border-gray-300"
>{answer.username}</td >{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" <td class="border-r dark:border-gray-500 p-1 border-gray-300"
>{answer.score}</td >{(answer.time_taken / 1000).toFixed(3)}s
> </td>
<td class="border-r dark:border-gray-500 p-1 border-gray-300" <td class="p-1">{answer.answer}</td>
>{(answer.time_taken / 1000).toFixed(3)}s</td {#if question.type !== QuizQuestionType.VOTING}
> <td class="p-1 border-l dark:border-gray-500 border-gray-300">
<td class="border-r dark:border-gray-500 p-1 border-gray-300" {#if answer.right}{:else}{/if}
>{answer.answer}</td </td>
> {/if}
<td class="p-1"
>{#if answer.right}{:else}{/if}</td
>
</tr> </tr>
{/each} {/each}
</table> </table>
@@ -1,8 +1,8 @@
"""Added game_results column """Added game_results
Revision ID: 9a28d7a36ad1 Revision ID: 438516c09cf3
Revises: 7ad8502af419 Revises: 7ad8502af419
Create Date: 2023-01-30 15:42:42.293514 Create Date: 2023-02-13 17:34:05.358665
""" """
from alembic import op from alembic import op
@@ -11,7 +11,7 @@ import ormar
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision = "9a28d7a36ad1" revision = "438516c09cf3"
down_revision = "7ad8502af419" down_revision = "7ad8502af419"
branch_labels = None branch_labels = None
depends_on = None depends_on = None
@@ -30,6 +30,9 @@ def upgrade() -> None:
sa.Column("answers", sa.JSON(), nullable=False), sa.Column("answers", sa.JSON(), nullable=False),
sa.Column("player_scores", sa.JSON(none_as_null=True), nullable=True), 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("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(["quiz"], ["quiz.id"], name="fk_game_results_quiz_id_quiz"),
sa.ForeignKeyConstraint(["user"], ["users.id"], name="fk_game_results_users_id_user"), sa.ForeignKeyConstraint(["user"], ["users.id"], name="fk_game_results_users_id_user"),
sa.PrimaryKeyConstraint("id"), sa.PrimaryKeyConstraint("id"),