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 @@ + + + +
+
+
+ + + + + + + + {#each data.results as result} + + + + + + + {/each} +
Quiz TitleDate PlayedPlayer countNote
{result.quiz.title}{new Date(result.timestamp).toLocaleString()}{Object.keys(result.player_scores).length}{result.note}
+
+
+
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 @@ + + + +
+
+
+ +
+
+ +
+
+ +
+
+ {#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 @@ + + + +
+
+ + + + + {#if custom_field} + + {/if} + + {#each usernames as uname} + + + + {#if custom_field} + + {/if} + + {/each} +
Player namePlayer ScoreCustom field
{uname}{scores[uname]}{custom_field[uname]}
+
+
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 ###