feat(lobby): start round with selected category

This commit is contained in:
2026-02-27 14:14:40 +01:00
parent 9995203add
commit 03100c99cd
4 changed files with 149 additions and 3 deletions

View File

@@ -54,7 +54,7 @@ Byg **Weirsøe Party Protocol**: en dansk party-webapp platform ala Jackbox, hvo
### Fase 3 — Spilflow `Fup og Fakta` ### Fase 3 — Spilflow `Fup og Fakta`
- [x] Lobby: host opretter session, spillere joiner via kode - [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 - [ ] Spørgsmål vises -> alle skriver løgn inden X sek
- [ ] System blander korrekt svar + løgne - [ ] System blander korrekt svar + løgne
- [ ] Guessfase: alle gætter inden Z sek - [ ] Guessfase: alle gætter inden Z sek

View File

@@ -2,7 +2,7 @@ from django.contrib.auth import get_user_model
from django.test import TestCase from django.test import TestCase
from django.urls import reverse from django.urls import reverse
from fupogfakta.models import GameSession, Player from fupogfakta.models import Category, GameSession, Player, Question, RoundConfig
User = get_user_model() User = get_user_model()
@@ -81,3 +81,82 @@ class LobbyFlowTests(TestCase):
payload = response.json() payload = response.json()
self.assertEqual(payload["session"]["players_count"], 2) self.assertEqual(payload["session"]["players_count"], 2)
self.assertEqual([p["nickname"] for p in payload["players"]], ["Bo", "Mia"]) 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")

View File

@@ -8,4 +8,5 @@ urlpatterns = [
path("sessions/create", views.create_session, name="create_session"), path("sessions/create", views.create_session, name="create_session"),
path("sessions/join", views.join_session, name="join_session"), path("sessions/join", views.join_session, name="join_session"),
path("sessions/<str:code>", views.session_detail, name="session_detail"), path("sessions/<str:code>", views.session_detail, name="session_detail"),
path("sessions/<str:code>/rounds/start", views.start_round, name="start_round"),
] ]

View File

@@ -2,10 +2,11 @@ import json
import random import random
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.db import transaction
from django.http import HttpRequest, JsonResponse from django.http import HttpRequest, JsonResponse
from django.views.decorators.http import require_GET, require_POST 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_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
SESSION_CODE_LENGTH = 6 SESSION_CODE_LENGTH = 6
@@ -132,3 +133,68 @@ def session_detail(request: HttpRequest, code: str) -> JsonResponse:
"players": players, "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,
)