From adce99b82ba5b6884e8362a9758f3f0d2910b6a3 Mon Sep 17 00:00:00 2001 From: Asger Geel Weirsoee Date: Fri, 27 Feb 2026 18:02:13 +0100 Subject: [PATCH] feat(f3): add final result endpoint to finish game --- TODO.md | 2 +- lobby/tests.py | 47 +++++++++++++++++++++++++++++++++++++++++++++++ lobby/urls.py | 1 + lobby/views.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 91 insertions(+), 1 deletion(-) diff --git a/TODO.md b/TODO.md index 2e90931..4d39dc0 100644 --- a/TODO.md +++ b/TODO.md @@ -60,7 +60,7 @@ Byg **Weirsøe Party Protocol**: en dansk party-webapp platform ala Jackbox, hvo - [x] Guessfase: alle gætter inden Z sek - [x] Pointudregning (konfigurerbar pr. runde) - [x] Scoreboard + næste spørgsmål/runde -- [ ] Slutresultat +- [x] Slutresultat ### Fase 4 — Voice-acting (platformkrav) - [ ] Definér TTS provider-interface diff --git a/lobby/tests.py b/lobby/tests.py index 7eb1d32..16d75c2 100644 --- a/lobby/tests.py +++ b/lobby/tests.py @@ -584,6 +584,53 @@ class RevealRoundFlowTests(TestCase): self.assertEqual(response.status_code, 403) self.assertEqual(response.json()["error"], "Only host can view scoreboard") + def test_host_can_finish_game_from_reveal(self): + self.client.login(username="host_reveal", password="secret123") + + response = self.client.post( + reverse( + "lobby:finish_game", + kwargs={"code": self.session.code}, + ) + ) + + self.assertEqual(response.status_code, 200) + payload = response.json() + self.assertEqual(payload["session"]["status"], GameSession.Status.FINISHED) + self.assertEqual(payload["winner"]["nickname"], "Luna") + self.assertEqual([item["nickname"] for item in payload["leaderboard"]], ["Luna", "Mads"]) + + self.session.refresh_from_db() + self.assertEqual(self.session.status, GameSession.Status.FINISHED) + + def test_finish_game_requires_host(self): + self.client.login(username="other_reveal", password="secret123") + + response = self.client.post( + reverse( + "lobby:finish_game", + kwargs={"code": self.session.code}, + ) + ) + + self.assertEqual(response.status_code, 403) + self.assertEqual(response.json()["error"], "Only host can finish game") + + def test_finish_game_rejects_wrong_phase(self): + self.client.login(username="host_reveal", password="secret123") + self.session.status = GameSession.Status.GUESS + self.session.save(update_fields=["status"]) + + response = self.client.post( + reverse( + "lobby:finish_game", + kwargs={"code": self.session.code}, + ) + ) + + self.assertEqual(response.status_code, 400) + self.assertEqual(response.json()["error"], "Game can only be finished from reveal phase") + def test_host_can_start_next_round_from_reveal(self): self.client.login(username="host_reveal", password="secret123") diff --git a/lobby/urls.py b/lobby/urls.py index c9fca14..558b9f1 100644 --- a/lobby/urls.py +++ b/lobby/urls.py @@ -31,6 +31,7 @@ urlpatterns = [ name="calculate_scores", ), path("sessions//scoreboard", views.reveal_scoreboard, name="reveal_scoreboard"), + path("sessions//finish", views.finish_game, name="finish_game"), path("sessions//rounds/next", views.start_next_round, name="start_next_round"), ] diff --git a/lobby/views.py b/lobby/views.py index 2fa69d9..4d3cf8b 100644 --- a/lobby/views.py +++ b/lobby/views.py @@ -567,6 +567,48 @@ def start_next_round(request: HttpRequest, code: str) -> JsonResponse: } ) +@require_POST +@login_required +def finish_game(request: HttpRequest, code: str) -> JsonResponse: + session_code = code.strip().upper() + + try: + session = GameSession.objects.get(code=session_code) + except GameSession.DoesNotExist: + return JsonResponse({"error": "Session not found"}, status=404) + + if session.host_id != request.user.id: + return JsonResponse({"error": "Only host can finish game"}, status=403) + + with transaction.atomic(): + locked_session = GameSession.objects.select_for_update().get(pk=session.pk) + if locked_session.status != GameSession.Status.REVEAL: + return JsonResponse({"error": "Game can only be finished from reveal phase"}, status=400) + + locked_session.status = GameSession.Status.FINISHED + locked_session.save(update_fields=["status"]) + + leaderboard = list( + Player.objects.filter(session=session) + .order_by("-score", "nickname") + .values("id", "nickname", "score") + ) + + winner = leaderboard[0] if leaderboard else None + + return JsonResponse( + { + "session": { + "code": session.code, + "status": GameSession.Status.FINISHED, + "current_round": session.current_round, + }, + "winner": winner, + "leaderboard": leaderboard, + } + ) + + @require_POST @login_required def calculate_scores(request: HttpRequest, code: str, round_question_id: int) -> JsonResponse: