From 4a3339b9b8435cdcbf4c9a9f77b114c0e712d860 Mon Sep 17 00:00:00 2001 From: Mawoka Date: Fri, 30 Jun 2023 01:05:13 +0200 Subject: [PATCH] :sparkles: Improved ClassQuizController --- classquiz/db/models.py | 5 +- .../box_controller/embedded/__init__.py | 19 ++- .../routers/box_controller/embedded/socket.py | 5 +- classquiz/routers/box_controller/web.py | 26 +++- frontend/src/lib/i18n/locales/en.json | 18 ++- .../routes/account/controllers/+page.svelte | 41 +++-- .../controllers/[controller_id]/+page.svelte | 145 ++++++++++++++++++ .../controllers/[controller_id]/+page.ts | 26 ++++ .../[controller_id]/SaveIndicator.svelte | 37 +++++ .../controllers/[controller_id]/commons.ts | 15 ++ .../account/controllers/add/wait/+page.svelte | 1 - ...> 4916d7b204cb_added_controllers_table.py} | 15 +- 12 files changed, 316 insertions(+), 37 deletions(-) create mode 100644 frontend/src/routes/account/controllers/[controller_id]/+page.ts create mode 100644 frontend/src/routes/account/controllers/[controller_id]/SaveIndicator.svelte create mode 100644 frontend/src/routes/account/controllers/[controller_id]/commons.ts rename migrations/versions/{b9ca06dfa179_added_controllers_table.py => 4916d7b204cb_added_controllers_table.py} (80%) diff --git a/classquiz/db/models.py b/classquiz/db/models.py index 889ce94..cc395b9 100644 --- a/classquiz/db/models.py +++ b/classquiz/db/models.py @@ -320,7 +320,7 @@ class GameResults(ormar.Model): database = database -class Controllers(ormar.Model): +class Controller(ormar.Model): id: uuid.UUID = ormar.UUID(primary_key=True) user: uuid.UUID | User = ormar.ForeignKey(User) secret_key: str = ormar.String(nullable=False, max_length=24, min_length=24) @@ -329,8 +329,9 @@ class Controllers(ormar.Model): first_seen: datetime | None = ormar.DateTime(nullable=True) name: str = ormar.Text(nullable=False) os_version: str | None = ormar.Text(nullable=True) + wanted_os_version: str = ormar.Text(nullable=True, default=None) class Meta: - tablename = "controllers" + tablename = "controller" metadata = metadata database = database diff --git a/classquiz/routers/box_controller/embedded/__init__.py b/classquiz/routers/box_controller/embedded/__init__.py index b97a314..77a8f97 100644 --- a/classquiz/routers/box_controller/embedded/__init__.py +++ b/classquiz/routers/box_controller/embedded/__init__.py @@ -10,9 +10,10 @@ import uuid from datetime import datetime from fastapi import APIRouter, HTTPException +from fastapi.responses import PlainTextResponse from pydantic import BaseModel from classquiz.config import redis -from classquiz.db.models import PlayGame, Controllers +from classquiz.db.models import PlayGame, Controller from classquiz.routers.box_controller.embedded.socket import router as socket_router router = APIRouter() @@ -31,7 +32,7 @@ class JoinGameResponse(BaseModel): @router.post("/join") async def join_game(data: JoinGameInput) -> JoinGameResponse: - controller = await Controllers.objects.get_or_none(id=data.id, secret_key=data.secret_key) + controller = await Controller.objects.get_or_none(id=data.id, secret_key=data.secret_key) game_pin = await redis.get(f"game:cqc:code:{data.code}") if game_pin is None: raise HTTPException(status_code=404, detail="Game not found") @@ -65,7 +66,7 @@ async def register_with_code(data: RegisterWithCodeInput) -> RegisterWithCodeRes raise HTTPException(status_code=404, detail="Code not found") await redis.delete(f"controller_setup:{data.code}") c_id = uuid.UUID(c_id) - controller = await Controllers.objects.get(id=c_id) + controller = await Controller.objects.get(id=c_id) controller.first_seen = datetime.now() controller.last_seen = datetime.now() await controller.update() @@ -74,9 +75,19 @@ async def register_with_code(data: RegisterWithCodeInput) -> RegisterWithCodeRes @router.get("/ping") async def ping_server(id: uuid.UUID, secret_key: str, version: str): - controller = await Controllers.objects.get_or_none(id=id, secret_key=secret_key) + controller = await Controller.objects.get_or_none(id=id, secret_key=secret_key) if controller is None: raise HTTPException(status_code=404, detail="Key and/or id invalid") controller.last_seen = datetime.now() controller.os_version = version await controller.update() + + +@router.get("/update") +async def get_firmware_version(id: uuid.UUID, secret_key: str) -> PlainTextResponse: + controller = await Controller.objects.get_or_none(id=id, secret_key=secret_key) + if controller is None: + return PlainTextResponse(status_code=404, content="Key and/or id invalid") + if controller.wanted_os_version is None: + return PlainTextResponse(status_code=400, content="No update needed") + return PlainTextResponse(status_code=200, content=controller.wanted_os_version) diff --git a/classquiz/routers/box_controller/embedded/socket.py b/classquiz/routers/box_controller/embedded/socket.py index 5b12d9a..4c2ff03 100644 --- a/classquiz/routers/box_controller/embedded/socket.py +++ b/classquiz/routers/box_controller/embedded/socket.py @@ -62,7 +62,7 @@ async def submit_answer_fn(data_answer: int, game_pin: str, player_id: str, now: await sio.emit("everyone_answered", {}) -button_to_index_map = {"b": 0, "g": 2, "y": 1, "r": 3} +button_to_index_map = {"y": 0, "r": 2, "g": 1, "b": 3} class WebSocketTypes(enum.Enum): @@ -85,7 +85,6 @@ async def websocket_endpoint(ws: WebSocket, game_id: str): await ws.close(code=status.WS_1001_GOING_AWAY) print("Client {} already exists.".format(game_id)) return - print("hI!") await ws.accept() wss_clients[game_id] = ws @@ -119,9 +118,7 @@ async def websocket_endpoint(ws: WebSocket, game_id: str): except (KeyError, AttributeError): await ws.send_text(WebSocketRequest(type=WebSocketTypes.Error, data="InvalidButton").json()) continue - print(data) await submit_answer_fn(answer_index, game_pin, player_id, now) - print("Data from client {}: {}".format(game_id, data)) except WebSocketDisconnect as ex: print("Client {} is disconnected: {}".format(game_id, ex)) diff --git a/classquiz/routers/box_controller/web.py b/classquiz/routers/box_controller/web.py index be641fb..92a0f8a 100644 --- a/classquiz/routers/box_controller/web.py +++ b/classquiz/routers/box_controller/web.py @@ -8,7 +8,7 @@ from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel from classquiz.auth import get_current_user -from classquiz.db.models import User, Controllers +from classquiz.db.models import User, Controller from classquiz.config import redis from classquiz.helpers.box_controller import generate_code @@ -32,7 +32,7 @@ async def set_controller_up( code = generate_code(10) if input_data.player_name is None: input_data.player_name = user.username - data = Controllers( + data = Controller( id=uuid.uuid4(), user=user, secret_key=os.urandom(12).hex(), @@ -47,12 +47,12 @@ async def set_controller_up( return SetControllerUpResponse(code=code, id=data.id) -GetControllerResponse = Controllers.get_pydantic(exclude={"secret_key", "user"}) +GetControllerResponse = Controller.get_pydantic(exclude={"secret_key", "user"}) @router.get("/controller") async def get_controller(id: uuid.UUID, user: User = Depends(get_current_user)) -> GetControllerResponse: - controller = await Controllers.objects.get_or_none(id=id, user=user.id) + controller = await Controller.objects.get_or_none(id=id, user=user.id) if controller is None: raise HTTPException(status_code=404, detail="Controller not found") return GetControllerResponse(**controller.dict()) @@ -68,7 +68,7 @@ class ModifyControllerInput(BaseModel): async def modify_controller( data: ModifyControllerInput, user: User = Depends(get_current_user) ) -> GetControllerResponse: - controller = await Controllers.objects.get_or_none(id=data.id, user=user.id) + controller = await Controller.objects.get_or_none(id=data.id, user=user.id) if controller is None: raise HTTPException(status_code=404, detail="Controller not found") controller.player_name = data.player_name @@ -79,10 +79,24 @@ async def modify_controller( @router.get("/list") async def get_all_controllers(user: User = Depends(get_current_user)) -> list[GetControllerResponse]: - controllers = await Controllers.objects.all(user=user.id) + controllers = await Controller.objects.all(user=user.id) if len(controllers) == 0: return [] return_list = [] for controller in controllers: return_list.append(GetControllerResponse(**controller.dict())) return return_list + + +class SetVersionToBeFlashedInput(BaseModel): + id: uuid.UUID + version: str + + +@router.post("/set_update") +async def set_version_to_be_flashed(data: SetVersionToBeFlashedInput, user: User = Depends(get_current_user)): + controller = await Controller.objects.get_or_none(id=data.id, user=user.id) + if controller is None: + raise HTTPException(status_code=404, detail="Controller not found") + controller.wanted_os_version = data.version + await controller.update() diff --git a/frontend/src/lib/i18n/locales/en.json b/frontend/src/lib/i18n/locales/en.json index c6ee4d3..a4a75af 100644 --- a/frontend/src/lib/i18n/locales/en.json +++ b/frontend/src/lib/i18n/locales/en.json @@ -161,7 +161,12 @@ "results": "Results", "note": "Note", "player_plural": "Players", - "score": "Score" + "score": "Score", + "version": "Version", + "never": "Never", + "unknown": "Unknown", + "name": "Name", + "update": "Update" }, "editor": { "time_in_seconds": "Time in seconds", @@ -272,5 +277,16 @@ "correct_answer": "{{count}} correct answer", "correct_answer_plural": "{{count}} correct answers", "time_taken": "Time taken" + }, + "controllers": { + "add_new_controller": "Add new controller", + "player_name": "Player name", + "first_seen": "First seen", + "last_seen": "Last seen", + "controller_name": "Controller Name", + "version_overview": "The newest version available is {{newest_version}} and version {{current_version}} is installed.", + "already_latest_version": "You're already on the latest version", + "cancel_update": "Cancel Update!", + "update_from_to": "Update from {{current_version}} to {{newest_version}}" } } diff --git a/frontend/src/routes/account/controllers/+page.svelte b/frontend/src/routes/account/controllers/+page.svelte index b5eb783..a5898d1 100644 --- a/frontend/src/routes/account/controllers/+page.svelte +++ b/frontend/src/routes/account/controllers/+page.svelte @@ -1,7 +1,13 @@ + + +
+
+

{$t('controllers.controller_name')}

+
+ + +
+
+
+

{$t('controllers.player_name')}

+
+ + +
+
+
+

{$t('words.update')}

+ {#await get_latest_version()} + + {:then _} +

+ {$t('controllers.version_overview', { + newest_version, + current_version: controller.os_version + })} +

+ {/await} + {#if newest_version && controller.os_version} +
+ + {#if newest_version === controller.os_version} + {$t('controllers.already_latest_version')} + {:else if newest_version === controller.wanted_os_version} + {$t('controllers.cancel_update')} + {:else} + {$t('controllers.update_from_to', { + newest_version, + current_version: controller.os_version + })} + {/if} + +
+ {/if} +
+
diff --git a/frontend/src/routes/account/controllers/[controller_id]/+page.ts b/frontend/src/routes/account/controllers/[controller_id]/+page.ts new file mode 100644 index 0000000..6c3dc58 --- /dev/null +++ b/frontend/src/routes/account/controllers/[controller_id]/+page.ts @@ -0,0 +1,26 @@ +/* + * 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'; +import { error } from '@sveltejs/kit'; + +export const load = (async ({ fetch, params }) => { + const res = await fetch(`/api/v1/box-controller/web/controller?id=${params.controller_id}`); + if (res.status !== 200) { + throw error(res.status, await res.text()); + } + const json: { + id: string; + player_name: string; + last_seen?: string; + first_seen?: string; + name: string; + os_version?: string; + wanted_os_version?: string; + } = await res.json(); + return { + controller: json + }; +}) satisfies PageLoad; diff --git a/frontend/src/routes/account/controllers/[controller_id]/SaveIndicator.svelte b/frontend/src/routes/account/controllers/[controller_id]/SaveIndicator.svelte new file mode 100644 index 0000000..6fb3c16 --- /dev/null +++ b/frontend/src/routes/account/controllers/[controller_id]/SaveIndicator.svelte @@ -0,0 +1,37 @@ + + + +
+ {#if status === SaveStatus.Saved} + + {:else if status === SaveStatus.Saving} + + + + + {/if} +
diff --git a/frontend/src/routes/account/controllers/[controller_id]/commons.ts b/frontend/src/routes/account/controllers/[controller_id]/commons.ts new file mode 100644 index 0000000..c9616de --- /dev/null +++ b/frontend/src/routes/account/controllers/[controller_id]/commons.ts @@ -0,0 +1,15 @@ +/* + * 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/. + */ + +/* eslint-disable no-unused-vars */ +export enum SaveStatus { + Unchanged, + Saved, + Saving, + Error +} + +/* eslint-enable */ diff --git a/frontend/src/routes/account/controllers/add/wait/+page.svelte b/frontend/src/routes/account/controllers/add/wait/+page.svelte index 417a060..a84038f 100644 --- a/frontend/src/routes/account/controllers/add/wait/+page.svelte +++ b/frontend/src/routes/account/controllers/add/wait/+page.svelte @@ -3,7 +3,6 @@ import CodeDisplay from '$lib/components/controller/code.svelte'; import Spinner from '$lib/Spinner.svelte'; import { onMount } from 'svelte'; - import { DateTime } from 'luxon'; export let data: PageData; let controller_seen = false; diff --git a/migrations/versions/b9ca06dfa179_added_controllers_table.py b/migrations/versions/4916d7b204cb_added_controllers_table.py similarity index 80% rename from migrations/versions/b9ca06dfa179_added_controllers_table.py rename to migrations/versions/4916d7b204cb_added_controllers_table.py index c0c2779..2406311 100644 --- a/migrations/versions/b9ca06dfa179_added_controllers_table.py +++ b/migrations/versions/4916d7b204cb_added_controllers_table.py @@ -1,8 +1,8 @@ -"""Added controllers table +"""Added Controllers Table -Revision ID: b9ca06dfa179 +Revision ID: 4916d7b204cb Revises: 7afe98d04169 -Create Date: 2023-04-03 13:16:49.585163 +Create Date: 2023-06-29 19:01:18.331367 """ from alembic import op @@ -11,7 +11,7 @@ import ormar # revision identifiers, used by Alembic. -revision = "b9ca06dfa179" +revision = "4916d7b204cb" down_revision = "7afe98d04169" branch_labels = None depends_on = None @@ -20,7 +20,7 @@ depends_on = None def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### op.create_table( - "controllers", + "controller", sa.Column("id", ormar.fields.sqlalchemy_uuid.CHAR(32), nullable=False), sa.Column("user", ormar.fields.sqlalchemy_uuid.CHAR(32), nullable=True), sa.Column("secret_key", sa.String(length=24), nullable=False), @@ -29,7 +29,8 @@ def upgrade() -> None: sa.Column("first_seen", sa.DateTime(), nullable=True), sa.Column("name", sa.Text(), nullable=False), sa.Column("os_version", sa.Text(), nullable=True), - sa.ForeignKeyConstraint(["user"], ["users.id"], name="fk_controllers_users_id_user"), + sa.Column("wanted_os_version", sa.Text(), nullable=True), + sa.ForeignKeyConstraint(["user"], ["users.id"], name="fk_controller_users_id_user"), sa.PrimaryKeyConstraint("id"), ) # ### end Alembic commands ### @@ -37,5 +38,5 @@ def upgrade() -> None: def downgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### - op.drop_table("controllers") + op.drop_table("controller") # ### end Alembic commands ###