diff --git a/classquiz/__init__.py b/classquiz/__init__.py
index 45d9c15..3525a91 100644
--- a/classquiz/__init__.py
+++ b/classquiz/__init__.py
@@ -30,6 +30,7 @@ from classquiz.routers import (
remote,
community,
avatar,
+ results,
)
from classquiz.socket_server import sio
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)
+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(login.router, tags=["auth"], prefix="/api/v1/login", include_in_schema=True)
diff --git a/classquiz/db/models.py b/classquiz/db/models.py
index 10b3193..6e3ceab 100644
--- a/classquiz/db/models.py
+++ b/classquiz/db/models.py
@@ -300,12 +300,12 @@ class GameInLobby(BaseModel):
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)
+ 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[AnswerDataList] = ormar.JSON(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)
diff --git a/classquiz/routers/results.py b/classquiz/routers/results.py
new file mode 100644
index 0000000..8b0568e
--- /dev/null
+++ b/classquiz/routers/results.py
@@ -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()
diff --git a/classquiz/socket_server/__init__.py b/classquiz/socket_server/__init__.py
index df1d3f1..c5c95fb 100644
--- a/classquiz/socket_server/__init__.py
+++ b/classquiz/socket_server/__init__.py
@@ -519,19 +519,17 @@ async def save_quiz(sid: str):
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())
+ 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")
- print(custom_field_data)
data = GameResults(
id=game.game_id,
- quiz_id=game.quiz_id,
- user_id=game.user_id,
+ quiz=game.quiz_id,
+ user=game.user_id,
timestamp=datetime.now(),
player_count=player_count,
answers=json.dumps(answers),
diff --git a/frontend/src/routes/results/+page.svelte b/frontend/src/routes/results/+page.svelte
new file mode 100644
index 0000000..fcbac5a
--- /dev/null
+++ b/frontend/src/routes/results/+page.svelte
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+ Quiz Title
+ Date Played
+ Player count
+ Note
+
+ {#each data.results as result}
+
+ {result.quiz.title}
+ {new Date(result.timestamp).toLocaleString()}
+ {Object.keys(result.player_scores).length}
+ {result.note}
+
+ {/each}
+
+
+
+
diff --git a/frontend/src/routes/results/+page.ts b/frontend/src/routes/results/+page.ts
new file mode 100644
index 0000000..9f3e1de
--- /dev/null
+++ b/frontend/src/routes/results/+page.ts
@@ -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;
diff --git a/frontend/src/routes/results/[result_id]/+page.svelte b/frontend/src/routes/results/[result_id]/+page.svelte
new file mode 100644
index 0000000..ee85583
--- /dev/null
+++ b/frontend/src/routes/results/[result_id]/+page.svelte
@@ -0,0 +1,76 @@
+
+
+
+
+
+
+ {
+ selected_tab = SelectedTab.Overview;
+ }}
+ class="m-auto w-full h-full"
+ >Overview
+
+
+
+ {
+ selected_tab = SelectedTab.Players;
+ }}
+ class="m-auto w-full h-full"
+ >
+ Players
+
+
+
+ {
+ selected_tab = SelectedTab.Questions;
+ }}
+ class="m-auto w-full h-full"
+ >Questions
+
+
+
+ {#if selected_tab === SelectedTab.Overview}{:else if selected_tab === SelectedTab.Questions}{:else if selected_tab === SelectedTab.Players}
+
+ {/if}
+
diff --git a/frontend/src/routes/results/[result_id]/+page.ts b/frontend/src/routes/results/[result_id]/+page.ts
new file mode 100644
index 0000000..27726f8
--- /dev/null
+++ b/frontend/src/routes/results/[result_id]/+page.ts
@@ -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;
diff --git a/frontend/src/routes/results/[result_id]/player_overview.svelte b/frontend/src/routes/results/[result_id]/player_overview.svelte
new file mode 100644
index 0000000..71aad1a
--- /dev/null
+++ b/frontend/src/routes/results/[result_id]/player_overview.svelte
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+ Player name
+ Player Score
+ {#if custom_field}
+ Custom field
+ {/if}
+
+ {#each usernames as uname}
+
+ {uname}
+ {scores[uname]}
+ {#if custom_field}
+ {custom_field[uname]}
+ {/if}
+
+ {/each}
+
+
+
diff --git a/frontend/src/routes/results/[result_id]/question_overview.svelte b/frontend/src/routes/results/[result_id]/question_overview.svelte
new file mode 100644
index 0000000..e050045
--- /dev/null
+++ b/frontend/src/routes/results/[result_id]/question_overview.svelte
@@ -0,0 +1,17 @@
+
+
diff --git a/migrations/versions/ac98b64a5347_added_game_results_column.py b/migrations/versions/9a28d7a36ad1_added_game_results_column.py
similarity index 70%
rename from migrations/versions/ac98b64a5347_added_game_results_column.py
rename to migrations/versions/9a28d7a36ad1_added_game_results_column.py
index b2d60c9..cf7f847 100644
--- a/migrations/versions/ac98b64a5347_added_game_results_column.py
+++ b/migrations/versions/9a28d7a36ad1_added_game_results_column.py
@@ -1,8 +1,8 @@
"""Added game_results column
-Revision ID: ac98b64a5347
+Revision ID: 9a28d7a36ad1
Revises: 7ad8502af419
-Create Date: 2023-01-29 18:00:36.389980
+Create Date: 2023-01-30 15:42:42.293514
"""
from alembic import op
@@ -11,7 +11,7 @@ import ormar
# revision identifiers, used by Alembic.
-revision = "ac98b64a5347"
+revision = "9a28d7a36ad1"
down_revision = "7ad8502af419"
branch_labels = None
depends_on = None
@@ -22,16 +22,16 @@ def upgrade() -> None:
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("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.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.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 ###