From 03100c99cdff1bb15b176ff6cd61c7ffbef283c6 Mon Sep 17 00:00:00 2001 From: Asger Geel Weirsoee Date: Fri, 27 Feb 2026 14:14:40 +0100 Subject: [PATCH] feat(lobby): start round with selected category --- TODO.md | 2 +- lobby/tests.py | 81 +++++++++++++++++++++++++++++++++++++++++++++++++- lobby/urls.py | 1 + lobby/views.py | 68 +++++++++++++++++++++++++++++++++++++++++- 4 files changed, 149 insertions(+), 3 deletions(-) diff --git a/TODO.md b/TODO.md index 831bbe5..0557ada 100644 --- a/TODO.md +++ b/TODO.md @@ -54,7 +54,7 @@ Byg **Weirsøe Party Protocol**: en dansk party-webapp platform ala Jackbox, hvo ### Fase 3 — Spilflow `Fup og Fakta` - [x] Lobby: host opretter session, spillere joiner via kode -- [ ] Runde starter med kategori +- [x] Runde starter med kategori - [ ] Spørgsmål vises -> alle skriver løgn inden X sek - [ ] System blander korrekt svar + løgne - [ ] Guessfase: alle gætter inden Z sek diff --git a/lobby/tests.py b/lobby/tests.py index 5dc6733..ca49efa 100644 --- a/lobby/tests.py +++ b/lobby/tests.py @@ -2,7 +2,7 @@ from django.contrib.auth import get_user_model from django.test import TestCase from django.urls import reverse -from fupogfakta.models import GameSession, Player +from fupogfakta.models import Category, GameSession, Player, Question, RoundConfig User = get_user_model() @@ -81,3 +81,82 @@ class LobbyFlowTests(TestCase): payload = response.json() self.assertEqual(payload["session"]["players_count"], 2) self.assertEqual([p["nickname"] for p in payload["players"]], ["Bo", "Mia"]) + + +class StartRoundTests(TestCase): + def setUp(self): + self.host = User.objects.create_user(username="host", password="secret123") + self.other_user = User.objects.create_user(username="other", password="secret123") + self.session = GameSession.objects.create(host=self.host, code="ABCD23") + self.category = Category.objects.create(name="Historie", slug="historie", is_active=True) + Question.objects.create( + category=self.category, + prompt="Hvilket år faldt muren?", + correct_answer="1989", + is_active=True, + ) + + def test_host_can_start_round_with_selected_category(self): + self.client.login(username="host", password="secret123") + + response = self.client.post( + reverse("lobby:start_round", kwargs={"code": self.session.code}), + data={"category_slug": self.category.slug}, + content_type="application/json", + ) + + self.assertEqual(response.status_code, 201) + body = response.json() + self.assertEqual(body["session"]["status"], GameSession.Status.LIE) + self.assertEqual(body["round"]["number"], 1) + self.assertEqual(body["round"]["category"]["slug"], self.category.slug) + + self.session.refresh_from_db() + self.assertEqual(self.session.status, GameSession.Status.LIE) + round_config = RoundConfig.objects.get(session=self.session, number=1) + self.assertEqual(round_config.category, self.category) + + def test_start_round_requires_host(self): + self.client.login(username="other", password="secret123") + + response = self.client.post( + reverse("lobby:start_round", kwargs={"code": self.session.code}), + data={"category_slug": self.category.slug}, + content_type="application/json", + ) + + self.assertEqual(response.status_code, 403) + self.assertEqual(response.json()["error"], "Only host can start round") + + def test_start_round_requires_existing_active_category_with_questions(self): + self.client.login(username="host", password="secret123") + + response = self.client.post( + reverse("lobby:start_round", kwargs={"code": self.session.code}), + data={"category_slug": "ukendt"}, + content_type="application/json", + ) + self.assertEqual(response.status_code, 404) + + empty_category = Category.objects.create(name="Sport", slug="sport", is_active=True) + response = self.client.post( + reverse("lobby:start_round", kwargs={"code": self.session.code}), + data={"category_slug": empty_category.slug}, + content_type="application/json", + ) + self.assertEqual(response.status_code, 400) + self.assertEqual(response.json()["error"], "Category has no active questions") + + def test_start_round_rejects_non_lobby_session(self): + self.client.login(username="host", password="secret123") + self.session.status = GameSession.Status.GUESS + self.session.save(update_fields=["status"]) + + response = self.client.post( + reverse("lobby:start_round", kwargs={"code": self.session.code}), + data={"category_slug": self.category.slug}, + content_type="application/json", + ) + + self.assertEqual(response.status_code, 400) + self.assertEqual(response.json()["error"], "Round can only be started from lobby") diff --git a/lobby/urls.py b/lobby/urls.py index b63dc6e..a2b1c31 100644 --- a/lobby/urls.py +++ b/lobby/urls.py @@ -8,4 +8,5 @@ urlpatterns = [ path("sessions/create", views.create_session, name="create_session"), path("sessions/join", views.join_session, name="join_session"), path("sessions/", views.session_detail, name="session_detail"), + path("sessions//rounds/start", views.start_round, name="start_round"), ] diff --git a/lobby/views.py b/lobby/views.py index 127417a..aab0a6a 100644 --- a/lobby/views.py +++ b/lobby/views.py @@ -2,10 +2,11 @@ import json import random from django.contrib.auth.decorators import login_required +from django.db import transaction from django.http import HttpRequest, JsonResponse from django.views.decorators.http import require_GET, require_POST -from fupogfakta.models import GameSession, Player +from fupogfakta.models import Category, GameSession, Player, Question, RoundConfig SESSION_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789" SESSION_CODE_LENGTH = 6 @@ -132,3 +133,68 @@ def session_detail(request: HttpRequest, code: str) -> JsonResponse: "players": players, } ) + + +@require_POST +@login_required +def start_round(request: HttpRequest, code: str) -> JsonResponse: + payload = _json_body(request) + category_slug = str(payload.get("category_slug", "")).strip() + + if not category_slug: + return JsonResponse({"error": "category_slug is required"}, status=400) + + 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 round"}, status=403) + + if session.status != GameSession.Status.LOBBY: + return JsonResponse({"error": "Round can only be started from lobby"}, status=400) + + try: + category = Category.objects.get(slug=category_slug, is_active=True) + except Category.DoesNotExist: + return JsonResponse({"error": "Category not found"}, status=404) + + if not Question.objects.filter(category=category, is_active=True).exists(): + return JsonResponse({"error": "Category has no active questions"}, status=400) + + with transaction.atomic(): + session = GameSession.objects.select_for_update().get(pk=session.pk) + if session.status != GameSession.Status.LOBBY: + return JsonResponse({"error": "Round can only be started from lobby"}, status=400) + + round_config, created = RoundConfig.objects.get_or_create( + session=session, + number=session.current_round, + defaults={"category": category}, + ) + if not created: + return JsonResponse({"error": "Round already configured"}, status=409) + + session.status = GameSession.Status.LIE + session.save(update_fields=["status"]) + + return JsonResponse( + { + "session": { + "code": session.code, + "status": session.status, + "current_round": session.current_round, + }, + "round": { + "number": round_config.number, + "category": { + "slug": round_config.category.slug, + "name": round_config.category.name, + }, + }, + }, + status=201, + )