F3/UI: Persistér mixed svarrækkefølge for reconnect #34
18
fupogfakta/migrations/0003_roundquestion_mixed_answers.py
Normal file
18
fupogfakta/migrations/0003_roundquestion_mixed_answers.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 6.0.2 on 2026-02-27 21:57
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('fupogfakta', '0002_roundquestion_shown_at'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='roundquestion',
|
||||||
|
name='mixed_answers',
|
||||||
|
field=models.JSONField(blank=True, default=list),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -87,6 +87,7 @@ class RoundQuestion(models.Model):
|
|||||||
question = models.ForeignKey(Question, on_delete=models.PROTECT)
|
question = models.ForeignKey(Question, on_delete=models.PROTECT)
|
||||||
correct_answer = models.CharField(max_length=255)
|
correct_answer = models.CharField(max_length=255)
|
||||||
shown_at = models.DateTimeField(default=timezone.now)
|
shown_at = models.DateTimeField(default=timezone.now)
|
||||||
|
mixed_answers = models.JSONField(default=list, blank=True)
|
||||||
|
|
||||||
|
|
||||||
class LieAnswer(models.Model):
|
class LieAnswer(models.Model):
|
||||||
|
|||||||
@@ -1,3 +1,2 @@
|
|||||||
from django.test import TestCase
|
|
||||||
|
|
||||||
# Create your tests here.
|
# Create your tests here.
|
||||||
|
|||||||
@@ -1,3 +1,2 @@
|
|||||||
from django.shortcuts import render
|
|
||||||
|
|
||||||
# Create your views here.
|
# Create your views here.
|
||||||
|
|||||||
@@ -308,7 +308,9 @@ class MixAnswersTests(TestCase):
|
|||||||
self.assertEqual(payload["session"]["status"], GameSession.Status.GUESS)
|
self.assertEqual(payload["session"]["status"], GameSession.Status.GUESS)
|
||||||
|
|
||||||
self.session.refresh_from_db()
|
self.session.refresh_from_db()
|
||||||
|
self.round_question.refresh_from_db()
|
||||||
self.assertEqual(self.session.status, GameSession.Status.GUESS)
|
self.assertEqual(self.session.status, GameSession.Status.GUESS)
|
||||||
|
self.assertEqual(self.round_question.mixed_answers, answer_texts)
|
||||||
|
|
||||||
def test_mix_answers_requires_host(self):
|
def test_mix_answers_requires_host(self):
|
||||||
self.client.login(username="other", password="secret123")
|
self.client.login(username="other", password="secret123")
|
||||||
@@ -339,6 +341,30 @@ class MixAnswersTests(TestCase):
|
|||||||
answer_texts = [entry["text"] for entry in response.json()["answers"]]
|
answer_texts = [entry["text"] for entry in response.json()["answers"]]
|
||||||
self.assertEqual(set(answer_texts), {"København", "Aarhus"})
|
self.assertEqual(set(answer_texts), {"København", "Aarhus"})
|
||||||
|
|
||||||
|
def test_mix_answers_is_idempotent_after_transition_to_guess(self):
|
||||||
|
LieAnswer.objects.create(round_question=self.round_question, player=self.player_one, text="Aarhus")
|
||||||
|
LieAnswer.objects.create(round_question=self.round_question, player=self.player_two, text="Odense")
|
||||||
|
|
||||||
|
self.client.login(username="host", password="secret123")
|
||||||
|
first = self.client.post(reverse("lobby:mix_answers", kwargs={"code": self.session.code, "round_question_id": self.round_question.id}))
|
||||||
|
second = self.client.post(reverse("lobby:mix_answers", kwargs={"code": self.session.code, "round_question_id": self.round_question.id}))
|
||||||
|
|
||||||
|
self.assertEqual(first.status_code, 200)
|
||||||
|
self.assertEqual(second.status_code, 200)
|
||||||
|
self.assertEqual([entry["text"] for entry in first.json()["answers"]], [entry["text"] for entry in second.json()["answers"]])
|
||||||
|
|
||||||
|
def test_session_detail_returns_persisted_mixed_answers_for_reconnect(self):
|
||||||
|
LieAnswer.objects.create(round_question=self.round_question, player=self.player_one, text="Aarhus")
|
||||||
|
LieAnswer.objects.create(round_question=self.round_question, player=self.player_two, text="Odense")
|
||||||
|
|
||||||
|
self.client.login(username="host", password="secret123")
|
||||||
|
mix_response = self.client.post(reverse("lobby:mix_answers", kwargs={"code": self.session.code, "round_question_id": self.round_question.id}))
|
||||||
|
detail_response = self.client.get(reverse("lobby:session_detail", kwargs={"code": self.session.code}))
|
||||||
|
|
||||||
|
self.assertEqual(mix_response.status_code, 200)
|
||||||
|
self.assertEqual(detail_response.status_code, 200)
|
||||||
|
self.assertEqual([entry["text"] for entry in mix_response.json()["answers"]], [entry["text"] for entry in detail_response.json()["round_question"]["answers"]])
|
||||||
|
|
||||||
|
|
||||||
class GuessSubmissionTests(TestCase):
|
class GuessSubmissionTests(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|||||||
@@ -147,6 +147,7 @@ def session_detail(request: HttpRequest, code: str) -> JsonResponse:
|
|||||||
"round_number": current_round_question.round_number,
|
"round_number": current_round_question.round_number,
|
||||||
"prompt": current_round_question.question.prompt,
|
"prompt": current_round_question.question.prompt,
|
||||||
"shown_at": current_round_question.shown_at.isoformat(),
|
"shown_at": current_round_question.shown_at.isoformat(),
|
||||||
|
"answers": [{"text": text} for text in (current_round_question.mixed_answers or [])],
|
||||||
}
|
}
|
||||||
|
|
||||||
return JsonResponse(
|
return JsonResponse(
|
||||||
@@ -368,8 +369,8 @@ def mix_answers(request: HttpRequest, code: str, round_question_id: int) -> Json
|
|||||||
if session.host_id != request.user.id:
|
if session.host_id != request.user.id:
|
||||||
return JsonResponse({"error": "Only host can mix answers"}, status=403)
|
return JsonResponse({"error": "Only host can mix answers"}, status=403)
|
||||||
|
|
||||||
if session.status != GameSession.Status.LIE:
|
if session.status not in {GameSession.Status.LIE, GameSession.Status.GUESS}:
|
||||||
return JsonResponse({"error": "Answers can only be mixed in lie phase"}, status=400)
|
return JsonResponse({"error": "Answers can only be mixed in lie or guess phase"}, status=400)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
round_question = RoundQuestion.objects.get(
|
round_question = RoundQuestion.objects.get(
|
||||||
@@ -380,28 +381,34 @@ def mix_answers(request: HttpRequest, code: str, round_question_id: int) -> Json
|
|||||||
except RoundQuestion.DoesNotExist:
|
except RoundQuestion.DoesNotExist:
|
||||||
return JsonResponse({"error": "Round question not found"}, status=404)
|
return JsonResponse({"error": "Round question not found"}, status=404)
|
||||||
|
|
||||||
lie_texts = list(round_question.lies.values_list("text", flat=True))
|
|
||||||
|
|
||||||
deduped_answers = []
|
|
||||||
seen = set()
|
|
||||||
for text in [round_question.correct_answer, *lie_texts]:
|
|
||||||
normalized = text.strip().casefold()
|
|
||||||
if not normalized or normalized in seen:
|
|
||||||
continue
|
|
||||||
seen.add(normalized)
|
|
||||||
deduped_answers.append(text.strip())
|
|
||||||
|
|
||||||
if len(deduped_answers) < 2:
|
|
||||||
return JsonResponse({"error": "Not enough answers to mix"}, status=400)
|
|
||||||
|
|
||||||
random.shuffle(deduped_answers)
|
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
locked_session = GameSession.objects.select_for_update().get(pk=session.pk)
|
locked_session = GameSession.objects.select_for_update().get(pk=session.pk)
|
||||||
if locked_session.status != GameSession.Status.LIE:
|
if locked_session.status not in {GameSession.Status.LIE, GameSession.Status.GUESS}:
|
||||||
return JsonResponse({"error": "Answers can only be mixed in lie phase"}, status=400)
|
return JsonResponse({"error": "Answers can only be mixed in lie or guess phase"}, status=400)
|
||||||
locked_session.status = GameSession.Status.GUESS
|
|
||||||
locked_session.save(update_fields=["status"])
|
locked_round_question = RoundQuestion.objects.select_for_update().get(pk=round_question.pk)
|
||||||
|
|
||||||
|
deduped_answers = list(locked_round_question.mixed_answers or [])
|
||||||
|
if not deduped_answers:
|
||||||
|
lie_texts = list(locked_round_question.lies.values_list("text", flat=True))
|
||||||
|
seen = set()
|
||||||
|
for text in [locked_round_question.correct_answer, *lie_texts]:
|
||||||
|
normalized = text.strip().casefold()
|
||||||
|
if not normalized or normalized in seen:
|
||||||
|
continue
|
||||||
|
seen.add(normalized)
|
||||||
|
deduped_answers.append(text.strip())
|
||||||
|
|
||||||
|
if len(deduped_answers) < 2:
|
||||||
|
return JsonResponse({"error": "Not enough answers to mix"}, status=400)
|
||||||
|
|
||||||
|
random.shuffle(deduped_answers)
|
||||||
|
locked_round_question.mixed_answers = deduped_answers
|
||||||
|
locked_round_question.save(update_fields=["mixed_answers"])
|
||||||
|
|
||||||
|
if locked_session.status == GameSession.Status.LIE:
|
||||||
|
locked_session.status = GameSession.Status.GUESS
|
||||||
|
locked_session.save(update_fields=["status"])
|
||||||
|
|
||||||
return JsonResponse(
|
return JsonResponse(
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user