feat(lobby): start round with selected category
This commit is contained in:
2
TODO.md
2
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`
|
### 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
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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"),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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,
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user