Merge pull request 'F3 UI: MVP host + player templates' (#31) from feature/f3-mvp-ui-templates into main
All checks were successful
CI / test-and-quality (push) Successful in 1m16s
All checks were successful
CI / test-and-quality (push) Successful in 1m16s
This commit was merged in pull request #31.
This commit is contained in:
34
lobby/templates/lobby/host_screen.html
Normal file
34
lobby/templates/lobby/host_screen.html
Normal file
@@ -0,0 +1,34 @@
|
||||
<!doctype html>
|
||||
<html lang="da"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>WPP Host</title></head>
|
||||
<body>
|
||||
<h1>Host panel (MVP)</h1>
|
||||
<p>Kræver login som host-bruger.</p>
|
||||
<button onclick="createSession()">1) Opret session</button>
|
||||
<input id="code" placeholder="Sessionkode">
|
||||
<select id="category">{% for c in categories %}<option value="{{ c.slug }}">{{ c.name }}</option>{% endfor %}</select>
|
||||
<button onclick="startRound()">2) Start runde</button>
|
||||
<button onclick="showQuestion()">3) Vis spørgsmål</button>
|
||||
<input id="roundQuestionId" placeholder="Round question id">
|
||||
<button onclick="mixAnswers()">4) Mix svar</button>
|
||||
<button onclick="calcScores()">5) Beregn score</button>
|
||||
<button onclick="showScoreboard()">6) Scoreboard</button>
|
||||
<button onclick="nextRound()">7) Næste runde</button>
|
||||
<button onclick="finishGame()">8) Afslut spil</button>
|
||||
<button onclick="sessionDetail()">Session-status</button>
|
||||
<pre id="out">Klar.</pre>
|
||||
<script>
|
||||
function csrf(){var m=document.cookie.match(/csrftoken=([^;]+)/);return m?m[1]:"";}
|
||||
function code(){return document.getElementById("code").value.trim().toUpperCase();}
|
||||
function rq(){return document.getElementById("roundQuestionId").value.trim();}
|
||||
async function api(path,method,payload){var o={method:method||"GET",headers:{"Accept":"application/json"}};if(payload!==null){o.headers["Content-Type"]="application/json";o.headers["X-CSRFToken"]=csrf();o.body=JSON.stringify(payload);}var r=await fetch(path,o);var d=await r.json().catch(function(){return {};});document.getElementById("out").textContent=JSON.stringify({status:r.status,data:d},null,2);if(d.session&&d.session.code){document.getElementById("code").value=d.session.code;}if(d.round_question&&d.round_question.id){document.getElementById("roundQuestionId").value=d.round_question.id;}return d;}
|
||||
function createSession(){return api("/lobby/sessions/create","POST",{});}
|
||||
function sessionDetail(){return api("/lobby/sessions/"+code(),"GET",null);}
|
||||
function startRound(){return api("/lobby/sessions/"+code()+"/rounds/start","POST",{category_slug:document.getElementById("category").value});}
|
||||
function showQuestion(){return api("/lobby/sessions/"+code()+"/questions/show","POST",{});}
|
||||
function mixAnswers(){return api("/lobby/sessions/"+code()+"/questions/"+rq()+"/answers/mix","POST",{});}
|
||||
function calcScores(){return api("/lobby/sessions/"+code()+"/questions/"+rq()+"/scores/calculate","POST",{});}
|
||||
function showScoreboard(){return api("/lobby/sessions/"+code()+"/scoreboard","GET",null);}
|
||||
function nextRound(){return api("/lobby/sessions/"+code()+"/rounds/next","POST",{});}
|
||||
function finishGame(){return api("/lobby/sessions/"+code()+"/finish","POST",{});}
|
||||
</script>
|
||||
</body></html>
|
||||
26
lobby/templates/lobby/player_screen.html
Normal file
26
lobby/templates/lobby/player_screen.html
Normal file
@@ -0,0 +1,26 @@
|
||||
<!doctype html>
|
||||
<html lang="da"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>WPP Player</title></head>
|
||||
<body>
|
||||
<h1>Player panel (MVP)</h1>
|
||||
<input id="code" placeholder="Sessionkode">
|
||||
<input id="nickname" placeholder="Nickname">
|
||||
<button onclick="joinSession()">1) Join</button>
|
||||
<input id="playerId" placeholder="Player id">
|
||||
<input id="roundQuestionId" placeholder="Round question id">
|
||||
<input id="lieText" placeholder="Din løgn">
|
||||
<button onclick="submitLie()">2) Submit løgn</button>
|
||||
<input id="guessText" placeholder="Dit gæt">
|
||||
<button onclick="submitGuess()">3) Submit gæt</button>
|
||||
<button onclick="sessionDetail()">Opdater session-status</button>
|
||||
<pre id="out">Klar.</pre>
|
||||
<script>
|
||||
function code(){return document.getElementById("code").value.trim().toUpperCase();}
|
||||
function pid(){return document.getElementById("playerId").value.trim();}
|
||||
function rq(){return document.getElementById("roundQuestionId").value.trim();}
|
||||
async function api(path,method,payload){var o={method:method||"GET",headers:{"Accept":"application/json"}};if(payload!==null){o.headers["Content-Type"]="application/json";o.body=JSON.stringify(payload);}var r=await fetch(path,o);var d=await r.json().catch(function(){return {};});document.getElementById("out").textContent=JSON.stringify({status:r.status,data:d},null,2);if(d.player&&d.player.id){document.getElementById("playerId").value=d.player.id;}if(d.round_question&&d.round_question.id){document.getElementById("roundQuestionId").value=d.round_question.id;}return d;}
|
||||
function joinSession(){return api("/lobby/sessions/join","POST",{code:code(),nickname:document.getElementById("nickname").value.trim()});}
|
||||
function sessionDetail(){return api("/lobby/sessions/"+code(),"GET",null);}
|
||||
function submitLie(){return api("/lobby/sessions/"+code()+"/questions/"+rq()+"/lies/submit","POST",{player_id:parseInt(pid(),10),text:document.getElementById("lieText").value});}
|
||||
function submitGuess(){return api("/lobby/sessions/"+code()+"/questions/"+rq()+"/guesses/submit","POST",{player_id:parseInt(pid(),10),selected_text:document.getElementById("guessText").value});}
|
||||
</script>
|
||||
</body></html>
|
||||
@@ -664,3 +664,50 @@ class RevealRoundFlowTests(TestCase):
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(response.json()["error"], "Next round can only start from reveal phase")
|
||||
|
||||
class UiScreenTests(TestCase):
|
||||
def setUp(self):
|
||||
self.host = User.objects.create_user(username="host_ui", password="secret123")
|
||||
|
||||
def test_host_screen_requires_login(self):
|
||||
response = self.client.get(reverse("lobby:host_screen"))
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
def test_host_screen_renders_for_logged_in_user(self):
|
||||
self.client.login(username="host_ui", password="secret123")
|
||||
response = self.client.get(reverse("lobby:host_screen"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "Host panel")
|
||||
|
||||
def test_player_screen_is_public(self):
|
||||
response = self.client.get(reverse("lobby:player_screen"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "Player panel")
|
||||
|
||||
|
||||
class SessionDetailRoundQuestionTests(TestCase):
|
||||
def setUp(self):
|
||||
self.host = User.objects.create_user(username="host_detail", password="secret123")
|
||||
self.session = GameSession.objects.create(host=self.host, code="ABCDE1", status=GameSession.Status.LIE)
|
||||
self.category = Category.objects.create(name="Historie", slug="historie-2", is_active=True)
|
||||
self.question = Question.objects.create(
|
||||
category=self.category,
|
||||
prompt="Hvem opfandt pæren?",
|
||||
correct_answer="Edison",
|
||||
is_active=True,
|
||||
)
|
||||
|
||||
def test_session_detail_includes_current_round_question_when_available(self):
|
||||
round_question = RoundQuestion.objects.create(
|
||||
session=self.session,
|
||||
round_number=1,
|
||||
question=self.question,
|
||||
correct_answer=self.question.correct_answer,
|
||||
)
|
||||
|
||||
response = self.client.get(reverse("lobby:session_detail", kwargs={"code": self.session.code}))
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
payload = response.json()
|
||||
self.assertEqual(payload["round_question"]["id"], round_question.id)
|
||||
self.assertEqual(payload["round_question"]["prompt"], self.question.prompt)
|
||||
|
||||
14
lobby/ui_views.py
Normal file
14
lobby/ui_views.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.shortcuts import render
|
||||
|
||||
from fupogfakta.models import Category
|
||||
|
||||
|
||||
@login_required
|
||||
def host_screen(request):
|
||||
categories = Category.objects.filter(is_active=True).order_by("name")
|
||||
return render(request, "lobby/host_screen.html", {"categories": categories})
|
||||
|
||||
|
||||
def player_screen(request):
|
||||
return render(request, "lobby/player_screen.html")
|
||||
@@ -1,10 +1,12 @@
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
from . import ui_views, views
|
||||
|
||||
app_name = "lobby"
|
||||
|
||||
urlpatterns = [
|
||||
path("ui/host", ui_views.host_screen, name="host_screen"),
|
||||
path("ui/player", ui_views.player_screen, name="player_screen"),
|
||||
path("sessions/create", views.create_session, name="create_session"),
|
||||
path("sessions/join", views.join_session, name="join_session"),
|
||||
path("sessions/<str:code>", views.session_detail, name="session_detail"),
|
||||
@@ -34,4 +36,3 @@ urlpatterns = [
|
||||
path("sessions/<str:code>/finish", views.finish_game, name="finish_game"),
|
||||
path("sessions/<str:code>/rounds/next", views.start_next_round, name="start_next_round"),
|
||||
]
|
||||
|
||||
|
||||
@@ -133,6 +133,22 @@ def session_detail(request: HttpRequest, code: str) -> JsonResponse:
|
||||
)
|
||||
)
|
||||
|
||||
current_round_question = (
|
||||
RoundQuestion.objects.filter(session=session, round_number=session.current_round)
|
||||
.select_related("question")
|
||||
.order_by("-id")
|
||||
.first()
|
||||
)
|
||||
|
||||
round_question_payload = None
|
||||
if current_round_question:
|
||||
round_question_payload = {
|
||||
"id": current_round_question.id,
|
||||
"round_number": current_round_question.round_number,
|
||||
"prompt": current_round_question.question.prompt,
|
||||
"shown_at": current_round_question.shown_at.isoformat(),
|
||||
}
|
||||
|
||||
return JsonResponse(
|
||||
{
|
||||
"session": {
|
||||
@@ -143,6 +159,7 @@ def session_detail(request: HttpRequest, code: str) -> JsonResponse:
|
||||
"players_count": len(players),
|
||||
},
|
||||
"players": players,
|
||||
"round_question": round_question_payload,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user