From 102c8b91ec67829272e9679c69d256d2c9cd80b8 Mon Sep 17 00:00:00 2001 From: Asger Geel Weirsoee Date: Fri, 27 Feb 2026 17:20:57 +0100 Subject: [PATCH] feat(f3): add reveal scoreboard and next-round transition --- TODO.md | 2 +- lobby/tests.py | 71 ++++++++++++++++++++++++++++++++++++++++++++++++++ lobby/urls.py | 2 ++ lobby/views.py | 68 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 142 insertions(+), 1 deletion(-) diff --git a/TODO.md b/TODO.md index b7c6b68..2e90931 100644 --- a/TODO.md +++ b/TODO.md @@ -59,7 +59,7 @@ Byg **Weirsøe Party Protocol**: en dansk party-webapp platform ala Jackbox, hvo - [x] System blander korrekt svar + løgne - [x] Guessfase: alle gætter inden Z sek - [x] Pointudregning (konfigurerbar pr. runde) -- [ ] Scoreboard + næste spørgsmål/runde +- [x] Scoreboard + næste spørgsmål/runde - [ ] Slutresultat ### Fase 4 — Voice-acting (platformkrav) diff --git a/lobby/tests.py b/lobby/tests.py index 9a1980e..7eb1d32 100644 --- a/lobby/tests.py +++ b/lobby/tests.py @@ -546,3 +546,74 @@ class ScoreCalculationTests(TestCase): self.assertEqual(first.status_code, 200) self.assertEqual(second.status_code, 409) self.assertEqual(second.json()["error"], "Scores already calculated for this round question") + + +class RevealRoundFlowTests(TestCase): + def setUp(self): + self.host = User.objects.create_user(username="host_reveal", password="secret123") + self.other_user = User.objects.create_user(username="other_reveal", password="secret123") + self.session = GameSession.objects.create(host=self.host, code="RVL123", status=GameSession.Status.REVEAL) + self.player_one = Player.objects.create(session=self.session, nickname="Luna", score=9) + self.player_two = Player.objects.create(session=self.session, nickname="Mads", score=3) + + def test_host_can_get_reveal_scoreboard(self): + self.client.login(username="host_reveal", password="secret123") + + response = self.client.get( + reverse( + "lobby:reveal_scoreboard", + kwargs={"code": self.session.code}, + ) + ) + + self.assertEqual(response.status_code, 200) + payload = response.json() + self.assertEqual(payload["session"]["status"], GameSession.Status.REVEAL) + self.assertEqual([item["nickname"] for item in payload["leaderboard"]], ["Luna", "Mads"]) + + def test_reveal_scoreboard_requires_host(self): + self.client.login(username="other_reveal", password="secret123") + + response = self.client.get( + reverse( + "lobby:reveal_scoreboard", + kwargs={"code": self.session.code}, + ) + ) + + self.assertEqual(response.status_code, 403) + self.assertEqual(response.json()["error"], "Only host can view scoreboard") + + def test_host_can_start_next_round_from_reveal(self): + self.client.login(username="host_reveal", password="secret123") + + response = self.client.post( + reverse( + "lobby:start_next_round", + kwargs={"code": self.session.code}, + ) + ) + + self.assertEqual(response.status_code, 200) + payload = response.json() + self.assertEqual(payload["session"]["status"], GameSession.Status.LOBBY) + self.assertEqual(payload["session"]["current_round"], 2) + + self.session.refresh_from_db() + self.assertEqual(self.session.status, GameSession.Status.LOBBY) + self.assertEqual(self.session.current_round, 2) + + def test_start_next_round_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:start_next_round", + kwargs={"code": self.session.code}, + ) + ) + + self.assertEqual(response.status_code, 400) + self.assertEqual(response.json()["error"], "Next round can only start from reveal phase") diff --git a/lobby/urls.py b/lobby/urls.py index 5ebead3..c9fca14 100644 --- a/lobby/urls.py +++ b/lobby/urls.py @@ -30,5 +30,7 @@ urlpatterns = [ views.calculate_scores, name="calculate_scores", ), + path("sessions//scoreboard", views.reveal_scoreboard, name="reveal_scoreboard"), + path("sessions//rounds/next", views.start_next_round, name="start_next_round"), ] diff --git a/lobby/views.py b/lobby/views.py index 329083c..2fa69d9 100644 --- a/lobby/views.py +++ b/lobby/views.py @@ -499,6 +499,74 @@ def submit_guess(request: HttpRequest, code: str, round_question_id: int) -> Jso ) + + +@require_GET +@login_required +def reveal_scoreboard(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 view scoreboard"}, status=403) + + if session.status != GameSession.Status.REVEAL: + return JsonResponse({"error": "Scoreboard is only available in reveal phase"}, status=400) + + leaderboard = list( + Player.objects.filter(session=session) + .order_by("-score", "nickname") + .values("id", "nickname", "score") + ) + + return JsonResponse( + { + "session": { + "code": session.code, + "status": session.status, + "current_round": session.current_round, + }, + "leaderboard": leaderboard, + } + ) + + +@require_POST +@login_required +def start_next_round(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 start next round"}, 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": "Next round can only start from reveal phase"}, status=400) + + locked_session.current_round += 1 + locked_session.status = GameSession.Status.LOBBY + locked_session.save(update_fields=["current_round", "status"]) + + return JsonResponse( + { + "session": { + "code": session.code, + "status": GameSession.Status.LOBBY, + "current_round": locked_session.current_round, + } + } + ) + @require_POST @login_required def calculate_scores(request: HttpRequest, code: str, round_question_id: int) -> JsonResponse: -- 2.39.5