Merge pull request 'F3: Reveal scoreboard + next-round transition' (#12) from feature/f3-scoreboard-next-round into main
All checks were successful
CI / test-and-quality (push) Successful in 1m1s
All checks were successful
CI / test-and-quality (push) Successful in 1m1s
This commit was merged in pull request #12.
This commit is contained in:
2
TODO.md
2
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] System blander korrekt svar + løgne
|
||||||
- [x] Guessfase: alle gætter inden Z sek
|
- [x] Guessfase: alle gætter inden Z sek
|
||||||
- [x] Pointudregning (konfigurerbar pr. runde)
|
- [x] Pointudregning (konfigurerbar pr. runde)
|
||||||
- [ ] Scoreboard + næste spørgsmål/runde
|
- [x] Scoreboard + næste spørgsmål/runde
|
||||||
- [ ] Slutresultat
|
- [ ] Slutresultat
|
||||||
|
|
||||||
### Fase 4 — Voice-acting (platformkrav)
|
### Fase 4 — Voice-acting (platformkrav)
|
||||||
|
|||||||
@@ -546,3 +546,74 @@ class ScoreCalculationTests(TestCase):
|
|||||||
self.assertEqual(first.status_code, 200)
|
self.assertEqual(first.status_code, 200)
|
||||||
self.assertEqual(second.status_code, 409)
|
self.assertEqual(second.status_code, 409)
|
||||||
self.assertEqual(second.json()["error"], "Scores already calculated for this round question")
|
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")
|
||||||
|
|||||||
@@ -30,5 +30,7 @@ urlpatterns = [
|
|||||||
views.calculate_scores,
|
views.calculate_scores,
|
||||||
name="calculate_scores",
|
name="calculate_scores",
|
||||||
),
|
),
|
||||||
|
path("sessions/<str:code>/scoreboard", views.reveal_scoreboard, name="reveal_scoreboard"),
|
||||||
|
path("sessions/<str:code>/rounds/next", views.start_next_round, name="start_next_round"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
@require_POST
|
||||||
@login_required
|
@login_required
|
||||||
def calculate_scores(request: HttpRequest, code: str, round_question_id: int) -> JsonResponse:
|
def calculate_scores(request: HttpRequest, code: str, round_question_id: int) -> JsonResponse:
|
||||||
|
|||||||
Reference in New Issue
Block a user