✨ Started working on results-UI
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)
|
||||||
|
|
||||||
|
|||||||
@@ -300,12 +300,12 @@ class GameInLobby(BaseModel):
|
|||||||
|
|
||||||
class GameResults(ormar.Model):
|
class GameResults(ormar.Model):
|
||||||
id: uuid.UUID = ormar.UUID(primary_key=True)
|
id: uuid.UUID = ormar.UUID(primary_key=True)
|
||||||
quiz_id: uuid.UUID = ormar.ForeignKey(Quiz)
|
quiz: uuid.UUID | Quiz = ormar.ForeignKey(Quiz)
|
||||||
user_id: uuid.UUID = ormar.ForeignKey(User)
|
user: uuid.UUID | User = ormar.ForeignKey(User)
|
||||||
timestamp: datetime = ormar.DateTime(default=datetime.now(), nullable=False)
|
timestamp: datetime = ormar.DateTime(default=datetime.now(), nullable=False)
|
||||||
player_count: int = ormar.Integer(nullable=False, default=0)
|
player_count: int = ormar.Integer(nullable=False, default=0)
|
||||||
note: str | None = ormar.Text(nullable=True)
|
note: str | None = ormar.Text(nullable=True)
|
||||||
answers: Json[AnswerDataList] = 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)
|
||||||
|
|
||||||
|
|||||||
@@ -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/.
|
||||||
|
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(include_quiz: bool = False, user: User = Depends(get_current_user)):
|
||||||
|
if include_quiz is True:
|
||||||
|
results = await GameResults.objects.select_related("quiz").all(user=user.id)
|
||||||
|
else:
|
||||||
|
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, include_quiz: bool = False, user: User = Depends(get_current_user)):
|
||||||
|
if include_quiz is True:
|
||||||
|
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:
|
||||||
|
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, include_quiz: bool = False, user: User = Depends(get_current_user)):
|
||||||
|
if include_quiz:
|
||||||
|
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:
|
||||||
|
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()
|
||||||
@@ -519,19 +519,17 @@ async def save_quiz(sid: str):
|
|||||||
player_count = await redis.scard(f"game_session:{game_pin}:players")
|
player_count = await redis.scard(f"game_session:{game_pin}:players")
|
||||||
answers = []
|
answers = []
|
||||||
for i in range(len(game.questions)):
|
for i in range(len(game.questions)):
|
||||||
print("hiqq", i)
|
|
||||||
redis_res = await redis.get(f"game_session:{game_pin}:{i}")
|
redis_res = await redis.get(f"game_session:{game_pin}:{i}")
|
||||||
try:
|
try:
|
||||||
answers.append(AnswerDataList.parse_raw(redis_res).dict())
|
answers.append(json.loads(redis_res))
|
||||||
except ValidationError:
|
except ValidationError:
|
||||||
answers.append([])
|
answers.append([])
|
||||||
player_scores = await redis.hgetall(f"game_session:{game_pin}:player_scores")
|
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")
|
custom_field_data = await redis.hgetall(f"game:{game_pin}:players:custom_fields")
|
||||||
print(custom_field_data)
|
|
||||||
data = GameResults(
|
data = GameResults(
|
||||||
id=game.game_id,
|
id=game.game_id,
|
||||||
quiz_id=game.quiz_id,
|
quiz=game.quiz_id,
|
||||||
user_id=game.user_id,
|
user=game.user_id,
|
||||||
timestamp=datetime.now(),
|
timestamp=datetime.now(),
|
||||||
player_count=player_count,
|
player_count=player_count,
|
||||||
answers=json.dumps(answers),
|
answers=json.dumps(answers),
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
<!--
|
||||||
|
- 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';
|
||||||
|
|
||||||
|
export let data: PageData;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="w-full">
|
||||||
|
<div class="flex justify-center w-full">
|
||||||
|
<div class="w-11/12 m-auto">
|
||||||
|
<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"
|
||||||
|
>Quiz Title</th
|
||||||
|
>
|
||||||
|
<th class="border-r dark:border-gray-500 p-1 mx-auto border-gray-300"
|
||||||
|
>Date Played</th
|
||||||
|
>
|
||||||
|
<th class="border-r dark:border-gray-500 p-1 mx-auto border-gray-300"
|
||||||
|
>Player count</th
|
||||||
|
>
|
||||||
|
<th>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.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>{result.note}</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</table>
|
||||||
|
</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,76 @@
|
|||||||
|
<!--
|
||||||
|
- 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';
|
||||||
|
|
||||||
|
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"
|
||||||
|
>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"
|
||||||
|
>
|
||||||
|
Players
|
||||||
|
</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"
|
||||||
|
>Questions
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{#if selected_tab === SelectedTab.Overview}{:else if selected_tab === SelectedTab.Questions}{:else if selected_tab === SelectedTab.Players}
|
||||||
|
<div>
|
||||||
|
<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,44 @@
|
|||||||
|
<!--
|
||||||
|
- 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">
|
||||||
|
export let scores: {
|
||||||
|
[key: string]: string;
|
||||||
|
};
|
||||||
|
export let custom_field: {
|
||||||
|
[key: string]: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
let usernames = Object.keys(scores);
|
||||||
|
</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"
|
||||||
|
>Player name</th
|
||||||
|
>
|
||||||
|
<th class="p-1 mx-auto">Player Score</th>
|
||||||
|
{#if custom_field}
|
||||||
|
<th class="border-l dark:border-gray-500 p-1 mx-auto border-gray-300"
|
||||||
|
>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}
|
||||||
|
<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,17 @@
|
|||||||
|
<!--
|
||||||
|
- 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 { QuizData } from '$lib/quiz_types';
|
||||||
|
|
||||||
|
export let quiz: QuizData;
|
||||||
|
export let answers: {
|
||||||
|
username: string;
|
||||||
|
answer: string;
|
||||||
|
right: boolean;
|
||||||
|
tike_taken: number;
|
||||||
|
score: number;
|
||||||
|
}[];
|
||||||
|
</script>
|
||||||
+7
-7
@@ -1,8 +1,8 @@
|
|||||||
"""Added game_results column
|
"""Added game_results column
|
||||||
|
|
||||||
Revision ID: ac98b64a5347
|
Revision ID: 9a28d7a36ad1
|
||||||
Revises: 7ad8502af419
|
Revises: 7ad8502af419
|
||||||
Create Date: 2023-01-29 18:00:36.389980
|
Create Date: 2023-01-30 15:42:42.293514
|
||||||
|
|
||||||
"""
|
"""
|
||||||
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 = "ac98b64a5347"
|
revision = "9a28d7a36ad1"
|
||||||
down_revision = "7ad8502af419"
|
down_revision = "7ad8502af419"
|
||||||
branch_labels = None
|
branch_labels = None
|
||||||
depends_on = None
|
depends_on = None
|
||||||
@@ -22,16 +22,16 @@ def upgrade() -> None:
|
|||||||
op.create_table(
|
op.create_table(
|
||||||
"game_results",
|
"game_results",
|
||||||
sa.Column("id", ormar.fields.sqlalchemy_uuid.CHAR(32), nullable=False),
|
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("quiz", ormar.fields.sqlalchemy_uuid.CHAR(32), nullable=True),
|
||||||
sa.Column("user_id", 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("timestamp", sa.DateTime(), nullable=False),
|
||||||
sa.Column("player_count", sa.Integer(), nullable=False),
|
sa.Column("player_count", sa.Integer(), nullable=False),
|
||||||
sa.Column("note", sa.Text(), nullable=True),
|
sa.Column("note", sa.Text(), nullable=True),
|
||||||
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.ForeignKeyConstraint(["quiz_id"], ["quiz.id"], name="fk_game_results_quiz_id_quiz_id"),
|
sa.ForeignKeyConstraint(["quiz"], ["quiz.id"], name="fk_game_results_quiz_id_quiz"),
|
||||||
sa.ForeignKeyConstraint(["user_id"], ["users.id"], name="fk_game_results_users_id_user_id"),
|
sa.ForeignKeyConstraint(["user"], ["users.id"], name="fk_game_results_users_id_user"),
|
||||||
sa.PrimaryKeyConstraint("id"),
|
sa.PrimaryKeyConstraint("id"),
|
||||||
)
|
)
|
||||||
# ### end Alembic commands ###
|
# ### end Alembic commands ###
|
||||||
Reference in New Issue
Block a user