feat(#275): harden django i18n locale negotiation and fallback #283
@@ -24,6 +24,15 @@ def lobby_i18n_error_messages() -> dict:
|
|||||||
return shared_i18n_catalog().get("backend", {}).get("errors", {})
|
return shared_i18n_catalog().get("backend", {}).get("errors", {})
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_error_key(code: str) -> str:
|
||||||
|
resolved = lobby_i18n_errors().get(code)
|
||||||
|
if isinstance(resolved, str) and resolved:
|
||||||
|
return resolved
|
||||||
|
|
||||||
|
LOGGER.warning("i18n error code missing in shared catalog", extra={"code": code})
|
||||||
|
return code
|
||||||
|
|
||||||
|
|
||||||
def _quality_value(language_candidate: str) -> float | None:
|
def _quality_value(language_candidate: str) -> float | None:
|
||||||
for parameter in language_candidate.split(";")[1:]:
|
for parameter in language_candidate.split(";")[1:]:
|
||||||
key, separator, value = parameter.partition("=")
|
key, separator, value = parameter.partition("=")
|
||||||
@@ -78,12 +87,13 @@ def resolve_error_message(*, key: str, locale: str) -> str:
|
|||||||
return key
|
return key
|
||||||
|
|
||||||
|
|
||||||
def api_error(request: HttpRequest, *, key: str, status: int) -> JsonResponse:
|
def api_error(request: HttpRequest, *, code: str, status: int) -> JsonResponse:
|
||||||
locale = resolve_locale(request)
|
locale = resolve_locale(request)
|
||||||
|
key = resolve_error_key(code)
|
||||||
return JsonResponse(
|
return JsonResponse(
|
||||||
{
|
{
|
||||||
"error": resolve_error_message(key=key, locale=locale),
|
"error": resolve_error_message(key=key, locale=locale),
|
||||||
"error_code": key,
|
"error_code": code,
|
||||||
"locale": locale,
|
"locale": locale,
|
||||||
},
|
},
|
||||||
status=status,
|
status=status,
|
||||||
|
|||||||
@@ -360,6 +360,8 @@ class LieSubmissionTests(TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 400)
|
self.assertEqual(response.status_code, 400)
|
||||||
|
self.assertEqual(response.json()["error_code"], "lie_submission_closed")
|
||||||
|
self.assertEqual(response.json()["locale"], "en")
|
||||||
self.assertEqual(response.json()["error"], "Lie submission window has closed")
|
self.assertEqual(response.json()["error"], "Lie submission window has closed")
|
||||||
|
|
||||||
def test_submit_lie_rejects_duplicate_submission(self):
|
def test_submit_lie_rejects_duplicate_submission(self):
|
||||||
@@ -381,6 +383,8 @@ class LieSubmissionTests(TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 409)
|
self.assertEqual(response.status_code, 409)
|
||||||
|
self.assertEqual(response.json()["error_code"], "lie_already_submitted")
|
||||||
|
self.assertEqual(response.json()["locale"], "en")
|
||||||
self.assertEqual(response.json()["error"], "Lie already submitted for this player")
|
self.assertEqual(response.json()["error"], "Lie already submitted for this player")
|
||||||
|
|
||||||
def test_submit_lie_requires_session_token(self):
|
def test_submit_lie_requires_session_token(self):
|
||||||
@@ -401,6 +405,8 @@ class LieSubmissionTests(TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 400)
|
self.assertEqual(response.status_code, 400)
|
||||||
|
self.assertEqual(response.json()["error_code"], "session_token_required")
|
||||||
|
self.assertEqual(response.json()["locale"], "en")
|
||||||
self.assertEqual(response.json()["error"], "session_token is required")
|
self.assertEqual(response.json()["error"], "session_token is required")
|
||||||
|
|
||||||
def test_submit_lie_rejects_invalid_session_token(self):
|
def test_submit_lie_rejects_invalid_session_token(self):
|
||||||
@@ -421,8 +427,33 @@ class LieSubmissionTests(TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 403)
|
self.assertEqual(response.status_code, 403)
|
||||||
|
self.assertEqual(response.json()["error_code"], "invalid_player_session_token")
|
||||||
|
self.assertEqual(response.json()["locale"], "en")
|
||||||
self.assertEqual(response.json()["error"], "Invalid player session token")
|
self.assertEqual(response.json()["error"], "Invalid player session token")
|
||||||
|
|
||||||
|
def test_submit_lie_uses_danish_locale_payload_from_accept_language(self):
|
||||||
|
round_question = RoundQuestion.objects.create(
|
||||||
|
session=self.session,
|
||||||
|
round_number=1,
|
||||||
|
question=self.question,
|
||||||
|
correct_answer=self.question.correct_answer,
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
reverse(
|
||||||
|
"lobby:submit_lie",
|
||||||
|
kwargs={"code": self.session.code, "round_question_id": round_question.id},
|
||||||
|
),
|
||||||
|
data={"player_id": self.player.id, "session_token": "invalid-token", "text": "Sydney"},
|
||||||
|
content_type="application/json",
|
||||||
|
HTTP_ACCEPT_LANGUAGE="da-DK,da;q=0.9,en;q=0.1",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
self.assertEqual(response.json()["error_code"], "invalid_player_session_token")
|
||||||
|
self.assertEqual(response.json()["locale"], "da")
|
||||||
|
self.assertEqual(response.json()["error"], "Ugyldigt spiller-session-token")
|
||||||
|
|
||||||
class MixAnswersTests(TestCase):
|
class MixAnswersTests(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.host = User.objects.create_user(username="host", password="secret123")
|
self.host = User.objects.create_user(username="host", password="secret123")
|
||||||
@@ -582,6 +613,8 @@ class GuessSubmissionTests(TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 400)
|
self.assertEqual(response.status_code, 400)
|
||||||
|
self.assertEqual(response.json()["error_code"], "guess_submission_invalid_phase")
|
||||||
|
self.assertEqual(response.json()["locale"], "en")
|
||||||
self.assertEqual(response.json()["error"], "Guess submission is only allowed in guess phase")
|
self.assertEqual(response.json()["error"], "Guess submission is only allowed in guess phase")
|
||||||
|
|
||||||
def test_submit_guess_rejects_unknown_answer(self):
|
def test_submit_guess_rejects_unknown_answer(self):
|
||||||
@@ -595,6 +628,8 @@ class GuessSubmissionTests(TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 400)
|
self.assertEqual(response.status_code, 400)
|
||||||
|
self.assertEqual(response.json()["error_code"], "selected_answer_invalid")
|
||||||
|
self.assertEqual(response.json()["locale"], "en")
|
||||||
self.assertEqual(response.json()["error"], "Selected answer is not part of this round")
|
self.assertEqual(response.json()["error"], "Selected answer is not part of this round")
|
||||||
|
|
||||||
def test_submit_guess_rejects_duplicate_submission(self):
|
def test_submit_guess_rejects_duplicate_submission(self):
|
||||||
@@ -610,6 +645,8 @@ class GuessSubmissionTests(TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 409)
|
self.assertEqual(response.status_code, 409)
|
||||||
|
self.assertEqual(response.json()["error_code"], "guess_already_submitted")
|
||||||
|
self.assertEqual(response.json()["locale"], "en")
|
||||||
self.assertEqual(response.json()["error"], "Guess already submitted for this player")
|
self.assertEqual(response.json()["error"], "Guess already submitted for this player")
|
||||||
|
|
||||||
def test_submit_guess_rejects_after_deadline(self):
|
def test_submit_guess_rejects_after_deadline(self):
|
||||||
@@ -626,6 +663,8 @@ class GuessSubmissionTests(TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 400)
|
self.assertEqual(response.status_code, 400)
|
||||||
|
self.assertEqual(response.json()["error_code"], "guess_submission_closed")
|
||||||
|
self.assertEqual(response.json()["locale"], "en")
|
||||||
self.assertEqual(response.json()["error"], "Guess submission window has closed")
|
self.assertEqual(response.json()["error"], "Guess submission window has closed")
|
||||||
|
|
||||||
|
|
||||||
@@ -641,6 +680,8 @@ class GuessSubmissionTests(TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 400)
|
self.assertEqual(response.status_code, 400)
|
||||||
|
self.assertEqual(response.json()["error_code"], "session_token_required")
|
||||||
|
self.assertEqual(response.json()["locale"], "en")
|
||||||
self.assertEqual(response.json()["error"], "session_token is required")
|
self.assertEqual(response.json()["error"], "session_token is required")
|
||||||
|
|
||||||
def test_submit_guess_rejects_invalid_session_token(self):
|
def test_submit_guess_rejects_invalid_session_token(self):
|
||||||
@@ -654,6 +695,8 @@ class GuessSubmissionTests(TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 403)
|
self.assertEqual(response.status_code, 403)
|
||||||
|
self.assertEqual(response.json()["error_code"], "invalid_player_session_token")
|
||||||
|
self.assertEqual(response.json()["locale"], "en")
|
||||||
self.assertEqual(response.json()["error"], "Invalid player session token")
|
self.assertEqual(response.json()["error"], "Invalid player session token")
|
||||||
|
|
||||||
|
|
||||||
@@ -735,6 +778,8 @@ class ScoreCalculationTests(TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 403)
|
self.assertEqual(response.status_code, 403)
|
||||||
|
self.assertEqual(response.json()["error_code"], "host_only_calculate_scores")
|
||||||
|
self.assertEqual(response.json()["locale"], "en")
|
||||||
self.assertEqual(response.json()["error"], "Only host can calculate scores")
|
self.assertEqual(response.json()["error"], "Only host can calculate scores")
|
||||||
|
|
||||||
def test_calculate_scores_rejects_duplicate_calculation(self):
|
def test_calculate_scores_rejects_duplicate_calculation(self):
|
||||||
@@ -756,6 +801,8 @@ class ScoreCalculationTests(TestCase):
|
|||||||
|
|
||||||
self.assertEqual(first.status_code, 200)
|
self.assertEqual(first.status_code, 200)
|
||||||
self.assertEqual(second.status_code, 409)
|
self.assertEqual(second.status_code, 409)
|
||||||
|
self.assertEqual(second.json()["error_code"], "scores_already_calculated")
|
||||||
|
self.assertEqual(second.json()["locale"], "en")
|
||||||
self.assertEqual(second.json()["error"], "Scores already calculated for this round question")
|
self.assertEqual(second.json()["error"], "Scores already calculated for this round question")
|
||||||
|
|
||||||
|
|
||||||
@@ -793,6 +840,8 @@ class RevealRoundFlowTests(TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 403)
|
self.assertEqual(response.status_code, 403)
|
||||||
|
self.assertEqual(response.json()["error_code"], "host_only_view_scoreboard")
|
||||||
|
self.assertEqual(response.json()["locale"], "en")
|
||||||
self.assertEqual(response.json()["error"], "Only host can view scoreboard")
|
self.assertEqual(response.json()["error"], "Only host can view scoreboard")
|
||||||
|
|
||||||
def test_host_can_finish_game_from_reveal(self):
|
def test_host_can_finish_game_from_reveal(self):
|
||||||
@@ -825,6 +874,8 @@ class RevealRoundFlowTests(TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 403)
|
self.assertEqual(response.status_code, 403)
|
||||||
|
self.assertEqual(response.json()["error_code"], "host_only_finish_game")
|
||||||
|
self.assertEqual(response.json()["locale"], "en")
|
||||||
self.assertEqual(response.json()["error"], "Only host can finish game")
|
self.assertEqual(response.json()["error"], "Only host can finish game")
|
||||||
|
|
||||||
def test_finish_game_rejects_wrong_phase(self):
|
def test_finish_game_rejects_wrong_phase(self):
|
||||||
@@ -840,6 +891,8 @@ class RevealRoundFlowTests(TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 400)
|
self.assertEqual(response.status_code, 400)
|
||||||
|
self.assertEqual(response.json()["error_code"], "finish_game_invalid_phase")
|
||||||
|
self.assertEqual(response.json()["locale"], "en")
|
||||||
self.assertEqual(response.json()["error"], "Game can only be finished from reveal phase")
|
self.assertEqual(response.json()["error"], "Game can only be finished from reveal phase")
|
||||||
|
|
||||||
def test_host_can_start_next_round_from_reveal(self):
|
def test_host_can_start_next_round_from_reveal(self):
|
||||||
@@ -874,8 +927,26 @@ class RevealRoundFlowTests(TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 400)
|
self.assertEqual(response.status_code, 400)
|
||||||
|
self.assertEqual(response.json()["error_code"], "next_round_invalid_phase")
|
||||||
|
self.assertEqual(response.json()["locale"], "en")
|
||||||
self.assertEqual(response.json()["error"], "Next round can only start from reveal phase")
|
self.assertEqual(response.json()["error"], "Next round can only start from reveal phase")
|
||||||
|
|
||||||
|
def test_reveal_scoreboard_unsupported_locale_falls_back_to_en_deterministically(self):
|
||||||
|
self.client.login(username="other_reveal", password="secret123")
|
||||||
|
|
||||||
|
response = self.client.get(
|
||||||
|
reverse(
|
||||||
|
"lobby:reveal_scoreboard",
|
||||||
|
kwargs={"code": self.session.code},
|
||||||
|
),
|
||||||
|
HTTP_ACCEPT_LANGUAGE="fr-FR,fr;q=0.9",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
self.assertEqual(response.json()["error_code"], "host_only_view_scoreboard")
|
||||||
|
self.assertEqual(response.json()["locale"], "en")
|
||||||
|
self.assertEqual(response.json()["error"], "Only host can view scoreboard")
|
||||||
|
|
||||||
class UiScreenTests(TestCase):
|
class UiScreenTests(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.host = User.objects.create_user(username="host_ui", password="secret123")
|
self.host = User.objects.create_user(username="host_ui", password="secret123")
|
||||||
@@ -1342,6 +1413,15 @@ class I18nResolverTests(TestCase):
|
|||||||
self.assertEqual(result, "missing_key")
|
self.assertEqual(result, "missing_key")
|
||||||
self.assertTrue(any("i18n key missing in shared catalog" in entry for entry in logs.output))
|
self.assertTrue(any("i18n key missing in shared catalog" in entry for entry in logs.output))
|
||||||
|
|
||||||
|
def test_missing_backend_error_code_is_logged_with_context(self):
|
||||||
|
from lobby.i18n import resolve_error_key
|
||||||
|
|
||||||
|
with self.assertLogs("lobby.i18n", level="WARNING") as logs:
|
||||||
|
result = resolve_error_key("missing_code")
|
||||||
|
|
||||||
|
self.assertEqual(result, "missing_code")
|
||||||
|
self.assertTrue(any("i18n error code missing in shared catalog" in entry for entry in logs.output))
|
||||||
|
|
||||||
def test_missing_locale_translation_falls_back_to_default_locale(self):
|
def test_missing_locale_translation_falls_back_to_default_locale(self):
|
||||||
with patch(
|
with patch(
|
||||||
"lobby.i18n.lobby_i18n_error_messages",
|
"lobby.i18n.lobby_i18n_error_messages",
|
||||||
|
|||||||
136
lobby/views.py
136
lobby/views.py
@@ -22,7 +22,7 @@ from fupogfakta.models import (
|
|||||||
|
|
||||||
from realtime.broadcast import sync_broadcast_phase_event
|
from realtime.broadcast import sync_broadcast_phase_event
|
||||||
|
|
||||||
from .i18n import api_error, lobby_i18n_errors
|
from .i18n import api_error
|
||||||
|
|
||||||
SESSION_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
|
SESSION_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
|
||||||
SESSION_CODE_LENGTH = 6
|
SESSION_CODE_LENGTH = 6
|
||||||
@@ -33,7 +33,7 @@ JOINABLE_STATUSES = {
|
|||||||
GameSession.Status.GUESS,
|
GameSession.Status.GUESS,
|
||||||
GameSession.Status.REVEAL,
|
GameSession.Status.REVEAL,
|
||||||
}
|
}
|
||||||
ERROR_CODES = lobby_i18n_errors()
|
|
||||||
|
|
||||||
|
|
||||||
def _json_body(request: HttpRequest) -> dict:
|
def _json_body(request: HttpRequest) -> dict:
|
||||||
@@ -131,14 +131,14 @@ def join_session(request: HttpRequest) -> JsonResponse:
|
|||||||
if not code:
|
if not code:
|
||||||
return api_error(
|
return api_error(
|
||||||
request,
|
request,
|
||||||
key=ERROR_CODES.get("session_code_required", "session_code_required"),
|
code="session_code_required",
|
||||||
status=400,
|
status=400,
|
||||||
)
|
)
|
||||||
|
|
||||||
if len(nickname) < 2 or len(nickname) > 40:
|
if len(nickname) < 2 or len(nickname) > 40:
|
||||||
return api_error(
|
return api_error(
|
||||||
request,
|
request,
|
||||||
key=ERROR_CODES.get("nickname_invalid", "nickname_invalid"),
|
code="nickname_invalid",
|
||||||
status=400,
|
status=400,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -147,21 +147,21 @@ def join_session(request: HttpRequest) -> JsonResponse:
|
|||||||
except GameSession.DoesNotExist:
|
except GameSession.DoesNotExist:
|
||||||
return api_error(
|
return api_error(
|
||||||
request,
|
request,
|
||||||
key=ERROR_CODES.get("session_not_found", "session_not_found"),
|
code="session_not_found",
|
||||||
status=404,
|
status=404,
|
||||||
)
|
)
|
||||||
|
|
||||||
if session.status not in JOINABLE_STATUSES:
|
if session.status not in JOINABLE_STATUSES:
|
||||||
return api_error(
|
return api_error(
|
||||||
request,
|
request,
|
||||||
key=ERROR_CODES.get("session_not_joinable", "session_not_joinable"),
|
code="session_not_joinable",
|
||||||
status=400,
|
status=400,
|
||||||
)
|
)
|
||||||
|
|
||||||
if Player.objects.filter(session=session, nickname__iexact=nickname).exists():
|
if Player.objects.filter(session=session, nickname__iexact=nickname).exists():
|
||||||
return api_error(
|
return api_error(
|
||||||
request,
|
request,
|
||||||
key=ERROR_CODES.get("nickname_taken", "nickname_taken"),
|
code="nickname_taken",
|
||||||
status=409,
|
status=409,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -193,7 +193,7 @@ def session_detail(request: HttpRequest, code: str) -> JsonResponse:
|
|||||||
except GameSession.DoesNotExist:
|
except GameSession.DoesNotExist:
|
||||||
return api_error(
|
return api_error(
|
||||||
request,
|
request,
|
||||||
key=ERROR_CODES.get("session_not_found", "session_not_found"),
|
code="session_not_found",
|
||||||
status=404,
|
status=404,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -254,7 +254,7 @@ def start_round(request: HttpRequest, code: str) -> JsonResponse:
|
|||||||
if not category_slug:
|
if not category_slug:
|
||||||
return api_error(
|
return api_error(
|
||||||
request,
|
request,
|
||||||
key=ERROR_CODES.get("category_slug_required", "category_slug_required"),
|
code="category_slug_required",
|
||||||
status=400,
|
status=400,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -265,21 +265,21 @@ def start_round(request: HttpRequest, code: str) -> JsonResponse:
|
|||||||
except GameSession.DoesNotExist:
|
except GameSession.DoesNotExist:
|
||||||
return api_error(
|
return api_error(
|
||||||
request,
|
request,
|
||||||
key=ERROR_CODES.get("session_not_found", "session_not_found"),
|
code="session_not_found",
|
||||||
status=404,
|
status=404,
|
||||||
)
|
)
|
||||||
|
|
||||||
if session.host_id != request.user.id:
|
if session.host_id != request.user.id:
|
||||||
return api_error(
|
return api_error(
|
||||||
request,
|
request,
|
||||||
key=ERROR_CODES.get("host_only_start_round", "host_only_start_round"),
|
code="host_only_start_round",
|
||||||
status=403,
|
status=403,
|
||||||
)
|
)
|
||||||
|
|
||||||
if session.status != GameSession.Status.LOBBY:
|
if session.status != GameSession.Status.LOBBY:
|
||||||
return api_error(
|
return api_error(
|
||||||
request,
|
request,
|
||||||
key=ERROR_CODES.get("round_start_invalid_phase", "round_start_invalid_phase"),
|
code="round_start_invalid_phase",
|
||||||
status=400,
|
status=400,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -288,14 +288,14 @@ def start_round(request: HttpRequest, code: str) -> JsonResponse:
|
|||||||
except Category.DoesNotExist:
|
except Category.DoesNotExist:
|
||||||
return api_error(
|
return api_error(
|
||||||
request,
|
request,
|
||||||
key=ERROR_CODES.get("category_not_found", "category_not_found"),
|
code="category_not_found",
|
||||||
status=404,
|
status=404,
|
||||||
)
|
)
|
||||||
|
|
||||||
if not Question.objects.filter(category=category, is_active=True).exists():
|
if not Question.objects.filter(category=category, is_active=True).exists():
|
||||||
return api_error(
|
return api_error(
|
||||||
request,
|
request,
|
||||||
key=ERROR_CODES.get("category_has_no_questions", "category_has_no_questions"),
|
code="category_has_no_questions",
|
||||||
status=400,
|
status=400,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -304,7 +304,7 @@ def start_round(request: HttpRequest, code: str) -> JsonResponse:
|
|||||||
if session.status != GameSession.Status.LOBBY:
|
if session.status != GameSession.Status.LOBBY:
|
||||||
return api_error(
|
return api_error(
|
||||||
request,
|
request,
|
||||||
key=ERROR_CODES.get("round_start_invalid_phase", "round_start_invalid_phase"),
|
code="round_start_invalid_phase",
|
||||||
status=400,
|
status=400,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -316,7 +316,7 @@ def start_round(request: HttpRequest, code: str) -> JsonResponse:
|
|||||||
if not created:
|
if not created:
|
||||||
return api_error(
|
return api_error(
|
||||||
request,
|
request,
|
||||||
key=ERROR_CODES.get("round_already_configured", "round_already_configured"),
|
code="round_already_configured",
|
||||||
status=409,
|
status=409,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -362,21 +362,21 @@ def show_question(request: HttpRequest, code: str) -> JsonResponse:
|
|||||||
except GameSession.DoesNotExist:
|
except GameSession.DoesNotExist:
|
||||||
return api_error(
|
return api_error(
|
||||||
request,
|
request,
|
||||||
key=ERROR_CODES.get("session_not_found", "session_not_found"),
|
code="session_not_found",
|
||||||
status=404,
|
status=404,
|
||||||
)
|
)
|
||||||
|
|
||||||
if session.host_id != request.user.id:
|
if session.host_id != request.user.id:
|
||||||
return api_error(
|
return api_error(
|
||||||
request,
|
request,
|
||||||
key=ERROR_CODES.get("host_only_show_question", "host_only_show_question"),
|
code="host_only_show_question",
|
||||||
status=403,
|
status=403,
|
||||||
)
|
)
|
||||||
|
|
||||||
if session.status != GameSession.Status.LIE:
|
if session.status != GameSession.Status.LIE:
|
||||||
return api_error(
|
return api_error(
|
||||||
request,
|
request,
|
||||||
key=ERROR_CODES.get("show_question_invalid_phase", "show_question_invalid_phase"),
|
code="show_question_invalid_phase",
|
||||||
status=400,
|
status=400,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -385,14 +385,14 @@ def show_question(request: HttpRequest, code: str) -> JsonResponse:
|
|||||||
except RoundConfig.DoesNotExist:
|
except RoundConfig.DoesNotExist:
|
||||||
return api_error(
|
return api_error(
|
||||||
request,
|
request,
|
||||||
key=ERROR_CODES.get("round_config_missing", "round_config_missing"),
|
code="round_config_missing",
|
||||||
status=400,
|
status=400,
|
||||||
)
|
)
|
||||||
|
|
||||||
if RoundQuestion.objects.filter(session=session, round_number=session.current_round).exists():
|
if RoundQuestion.objects.filter(session=session, round_number=session.current_round).exists():
|
||||||
return api_error(
|
return api_error(
|
||||||
request,
|
request,
|
||||||
key=ERROR_CODES.get("question_already_shown", "question_already_shown"),
|
code="question_already_shown",
|
||||||
status=409,
|
status=409,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -405,7 +405,7 @@ def show_question(request: HttpRequest, code: str) -> JsonResponse:
|
|||||||
if not available_questions.exists():
|
if not available_questions.exists():
|
||||||
return api_error(
|
return api_error(
|
||||||
request,
|
request,
|
||||||
key=ERROR_CODES.get("no_available_questions", "no_available_questions"),
|
code="no_available_questions",
|
||||||
status=400,
|
status=400,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -458,29 +458,29 @@ def submit_lie(request: HttpRequest, code: str, round_question_id: int) -> JsonR
|
|||||||
lie_text = str(payload.get("text", "")).strip()
|
lie_text = str(payload.get("text", "")).strip()
|
||||||
|
|
||||||
if not player_id:
|
if not player_id:
|
||||||
return JsonResponse({"error": "player_id is required"}, status=400)
|
return api_error(request, code="player_id_required", status=400)
|
||||||
|
|
||||||
if not session_token:
|
if not session_token:
|
||||||
return JsonResponse({"error": "session_token is required"}, status=400)
|
return api_error(request, code="session_token_required", status=400)
|
||||||
|
|
||||||
if not lie_text or len(lie_text) > 255:
|
if not lie_text or len(lie_text) > 255:
|
||||||
return JsonResponse({"error": "text must be between 1 and 255 characters"}, status=400)
|
return api_error(request, code="lie_text_invalid", status=400)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
session = GameSession.objects.get(code=session_code)
|
session = GameSession.objects.get(code=session_code)
|
||||||
except GameSession.DoesNotExist:
|
except GameSession.DoesNotExist:
|
||||||
return JsonResponse({"error": "Session not found"}, status=404)
|
return api_error(request, code="session_not_found", status=404)
|
||||||
|
|
||||||
if session.status != GameSession.Status.LIE:
|
if session.status != GameSession.Status.LIE:
|
||||||
return JsonResponse({"error": "Lie submission is only allowed in lie phase"}, status=400)
|
return api_error(request, code="lie_submission_invalid_phase", status=400)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
player = Player.objects.get(pk=player_id, session=session)
|
player = Player.objects.get(pk=player_id, session=session)
|
||||||
except Player.DoesNotExist:
|
except Player.DoesNotExist:
|
||||||
return JsonResponse({"error": "Player not found in session"}, status=404)
|
return api_error(request, code="player_not_found_in_session", status=404)
|
||||||
|
|
||||||
if player.session_token != session_token:
|
if player.session_token != session_token:
|
||||||
return JsonResponse({"error": "Invalid player session token"}, status=403)
|
return api_error(request, code="invalid_player_session_token", status=403)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
round_question = RoundQuestion.objects.get(
|
round_question = RoundQuestion.objects.get(
|
||||||
@@ -489,21 +489,21 @@ def submit_lie(request: HttpRequest, code: str, round_question_id: int) -> JsonR
|
|||||||
round_number=session.current_round,
|
round_number=session.current_round,
|
||||||
)
|
)
|
||||||
except RoundQuestion.DoesNotExist:
|
except RoundQuestion.DoesNotExist:
|
||||||
return JsonResponse({"error": "Round question not found"}, status=404)
|
return api_error(request, code="round_question_not_found", status=404)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
round_config = RoundConfig.objects.get(session=session, number=round_question.round_number)
|
round_config = RoundConfig.objects.get(session=session, number=round_question.round_number)
|
||||||
except RoundConfig.DoesNotExist:
|
except RoundConfig.DoesNotExist:
|
||||||
return JsonResponse({"error": "Round config missing"}, status=400)
|
return api_error(request, code="round_config_missing", status=400)
|
||||||
|
|
||||||
lie_deadline_at = round_question.shown_at + timedelta(seconds=round_config.lie_seconds)
|
lie_deadline_at = round_question.shown_at + timedelta(seconds=round_config.lie_seconds)
|
||||||
if timezone.now() > lie_deadline_at:
|
if timezone.now() > lie_deadline_at:
|
||||||
return JsonResponse({"error": "Lie submission window has closed"}, status=400)
|
return api_error(request, code="lie_submission_closed", status=400)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
lie = LieAnswer.objects.create(round_question=round_question, player=player, text=lie_text)
|
lie = LieAnswer.objects.create(round_question=round_question, player=player, text=lie_text)
|
||||||
except IntegrityError:
|
except IntegrityError:
|
||||||
return JsonResponse({"error": "Lie already submitted for this player"}, status=409)
|
return api_error(request, code="lie_already_submitted", status=409)
|
||||||
|
|
||||||
return JsonResponse(
|
return JsonResponse(
|
||||||
{
|
{
|
||||||
@@ -531,21 +531,21 @@ def mix_answers(request: HttpRequest, code: str, round_question_id: int) -> Json
|
|||||||
except GameSession.DoesNotExist:
|
except GameSession.DoesNotExist:
|
||||||
return api_error(
|
return api_error(
|
||||||
request,
|
request,
|
||||||
key=ERROR_CODES.get("session_not_found", "session_not_found"),
|
code="session_not_found",
|
||||||
status=404,
|
status=404,
|
||||||
)
|
)
|
||||||
|
|
||||||
if session.host_id != request.user.id:
|
if session.host_id != request.user.id:
|
||||||
return api_error(
|
return api_error(
|
||||||
request,
|
request,
|
||||||
key=ERROR_CODES.get("host_only_mix_answers", "host_only_mix_answers"),
|
code="host_only_mix_answers",
|
||||||
status=403,
|
status=403,
|
||||||
)
|
)
|
||||||
|
|
||||||
if session.status not in {GameSession.Status.LIE, GameSession.Status.GUESS}:
|
if session.status not in {GameSession.Status.LIE, GameSession.Status.GUESS}:
|
||||||
return api_error(
|
return api_error(
|
||||||
request,
|
request,
|
||||||
key=ERROR_CODES.get("mix_answers_invalid_phase", "mix_answers_invalid_phase"),
|
code="mix_answers_invalid_phase",
|
||||||
status=400,
|
status=400,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -558,7 +558,7 @@ def mix_answers(request: HttpRequest, code: str, round_question_id: int) -> Json
|
|||||||
except RoundQuestion.DoesNotExist:
|
except RoundQuestion.DoesNotExist:
|
||||||
return api_error(
|
return api_error(
|
||||||
request,
|
request,
|
||||||
key=ERROR_CODES.get("round_question_not_found", "round_question_not_found"),
|
code="round_question_not_found",
|
||||||
status=404,
|
status=404,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -567,7 +567,7 @@ def mix_answers(request: HttpRequest, code: str, round_question_id: int) -> Json
|
|||||||
if locked_session.status not in {GameSession.Status.LIE, GameSession.Status.GUESS}:
|
if locked_session.status not in {GameSession.Status.LIE, GameSession.Status.GUESS}:
|
||||||
return api_error(
|
return api_error(
|
||||||
request,
|
request,
|
||||||
key=ERROR_CODES.get("mix_answers_invalid_phase", "mix_answers_invalid_phase"),
|
code="mix_answers_invalid_phase",
|
||||||
status=400,
|
status=400,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -587,7 +587,7 @@ def mix_answers(request: HttpRequest, code: str, round_question_id: int) -> Json
|
|||||||
if len(deduped_answers) < 2:
|
if len(deduped_answers) < 2:
|
||||||
return api_error(
|
return api_error(
|
||||||
request,
|
request,
|
||||||
key=ERROR_CODES.get("not_enough_answers_to_mix", "not_enough_answers_to_mix"),
|
code="not_enough_answers_to_mix",
|
||||||
status=400,
|
status=400,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -641,29 +641,29 @@ def submit_guess(request: HttpRequest, code: str, round_question_id: int) -> Jso
|
|||||||
selected_text = str(payload.get("selected_text", "")).strip()
|
selected_text = str(payload.get("selected_text", "")).strip()
|
||||||
|
|
||||||
if not player_id:
|
if not player_id:
|
||||||
return JsonResponse({"error": "player_id is required"}, status=400)
|
return api_error(request, code="player_id_required", status=400)
|
||||||
|
|
||||||
if not session_token:
|
if not session_token:
|
||||||
return JsonResponse({"error": "session_token is required"}, status=400)
|
return api_error(request, code="session_token_required", status=400)
|
||||||
|
|
||||||
if not selected_text or len(selected_text) > 255:
|
if not selected_text or len(selected_text) > 255:
|
||||||
return JsonResponse({"error": "selected_text must be between 1 and 255 characters"}, status=400)
|
return api_error(request, code="selected_text_invalid", status=400)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
session = GameSession.objects.get(code=session_code)
|
session = GameSession.objects.get(code=session_code)
|
||||||
except GameSession.DoesNotExist:
|
except GameSession.DoesNotExist:
|
||||||
return JsonResponse({"error": "Session not found"}, status=404)
|
return api_error(request, code="session_not_found", status=404)
|
||||||
|
|
||||||
if session.status != GameSession.Status.GUESS:
|
if session.status != GameSession.Status.GUESS:
|
||||||
return JsonResponse({"error": "Guess submission is only allowed in guess phase"}, status=400)
|
return api_error(request, code="guess_submission_invalid_phase", status=400)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
player = Player.objects.get(pk=player_id, session=session)
|
player = Player.objects.get(pk=player_id, session=session)
|
||||||
except Player.DoesNotExist:
|
except Player.DoesNotExist:
|
||||||
return JsonResponse({"error": "Player not found in session"}, status=404)
|
return api_error(request, code="player_not_found_in_session", status=404)
|
||||||
|
|
||||||
if player.session_token != session_token:
|
if player.session_token != session_token:
|
||||||
return JsonResponse({"error": "Invalid player session token"}, status=403)
|
return api_error(request, code="invalid_player_session_token", status=403)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
round_question = RoundQuestion.objects.get(
|
round_question = RoundQuestion.objects.get(
|
||||||
@@ -672,18 +672,18 @@ def submit_guess(request: HttpRequest, code: str, round_question_id: int) -> Jso
|
|||||||
round_number=session.current_round,
|
round_number=session.current_round,
|
||||||
)
|
)
|
||||||
except RoundQuestion.DoesNotExist:
|
except RoundQuestion.DoesNotExist:
|
||||||
return JsonResponse({"error": "Round question not found"}, status=404)
|
return api_error(request, code="round_question_not_found", status=404)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
round_config = RoundConfig.objects.get(session=session, number=round_question.round_number)
|
round_config = RoundConfig.objects.get(session=session, number=round_question.round_number)
|
||||||
except RoundConfig.DoesNotExist:
|
except RoundConfig.DoesNotExist:
|
||||||
return JsonResponse({"error": "Round config missing"}, status=400)
|
return api_error(request, code="round_config_missing", status=400)
|
||||||
|
|
||||||
guess_deadline_at = round_question.shown_at + timedelta(
|
guess_deadline_at = round_question.shown_at + timedelta(
|
||||||
seconds=round_config.lie_seconds + round_config.guess_seconds
|
seconds=round_config.lie_seconds + round_config.guess_seconds
|
||||||
)
|
)
|
||||||
if timezone.now() > guess_deadline_at:
|
if timezone.now() > guess_deadline_at:
|
||||||
return JsonResponse({"error": "Guess submission window has closed"}, status=400)
|
return api_error(request, code="guess_submission_closed", status=400)
|
||||||
|
|
||||||
allowed_answers = {
|
allowed_answers = {
|
||||||
round_question.correct_answer.strip().casefold(),
|
round_question.correct_answer.strip().casefold(),
|
||||||
@@ -696,7 +696,7 @@ def submit_guess(request: HttpRequest, code: str, round_question_id: int) -> Jso
|
|||||||
|
|
||||||
selected_normalized = selected_text.casefold()
|
selected_normalized = selected_text.casefold()
|
||||||
if selected_normalized not in allowed_answers:
|
if selected_normalized not in allowed_answers:
|
||||||
return JsonResponse({"error": "Selected answer is not part of this round"}, status=400)
|
return api_error(request, code="selected_answer_invalid", status=400)
|
||||||
|
|
||||||
correct_normalized = round_question.correct_answer.strip().casefold()
|
correct_normalized = round_question.correct_answer.strip().casefold()
|
||||||
fooled_player_id = None
|
fooled_player_id = None
|
||||||
@@ -714,7 +714,7 @@ def submit_guess(request: HttpRequest, code: str, round_question_id: int) -> Jso
|
|||||||
fooled_player_id=fooled_player_id,
|
fooled_player_id=fooled_player_id,
|
||||||
)
|
)
|
||||||
except IntegrityError:
|
except IntegrityError:
|
||||||
return JsonResponse({"error": "Guess already submitted for this player"}, status=409)
|
return api_error(request, code="guess_already_submitted", status=409)
|
||||||
|
|
||||||
return JsonResponse(
|
return JsonResponse(
|
||||||
{
|
{
|
||||||
@@ -745,13 +745,13 @@ def reveal_scoreboard(request: HttpRequest, code: str) -> JsonResponse:
|
|||||||
try:
|
try:
|
||||||
session = GameSession.objects.get(code=session_code)
|
session = GameSession.objects.get(code=session_code)
|
||||||
except GameSession.DoesNotExist:
|
except GameSession.DoesNotExist:
|
||||||
return JsonResponse({"error": "Session not found"}, status=404)
|
return api_error(request, code="session_not_found", status=404)
|
||||||
|
|
||||||
if session.host_id != request.user.id:
|
if session.host_id != request.user.id:
|
||||||
return JsonResponse({"error": "Only host can view scoreboard"}, status=403)
|
return api_error(request, code="host_only_view_scoreboard", status=403)
|
||||||
|
|
||||||
if session.status != GameSession.Status.REVEAL:
|
if session.status != GameSession.Status.REVEAL:
|
||||||
return JsonResponse({"error": "Scoreboard is only available in reveal phase"}, status=400)
|
return api_error(request, code="scoreboard_invalid_phase", status=400)
|
||||||
|
|
||||||
leaderboard = list(
|
leaderboard = list(
|
||||||
Player.objects.filter(session=session)
|
Player.objects.filter(session=session)
|
||||||
@@ -785,15 +785,15 @@ def start_next_round(request: HttpRequest, code: str) -> JsonResponse:
|
|||||||
try:
|
try:
|
||||||
session = GameSession.objects.get(code=session_code)
|
session = GameSession.objects.get(code=session_code)
|
||||||
except GameSession.DoesNotExist:
|
except GameSession.DoesNotExist:
|
||||||
return JsonResponse({"error": "Session not found"}, status=404)
|
return api_error(request, code="session_not_found", status=404)
|
||||||
|
|
||||||
if session.host_id != request.user.id:
|
if session.host_id != request.user.id:
|
||||||
return JsonResponse({"error": "Only host can start next round"}, status=403)
|
return api_error(request, code="host_only_start_next_round", status=403)
|
||||||
|
|
||||||
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.REVEAL:
|
if locked_session.status != GameSession.Status.REVEAL:
|
||||||
return JsonResponse({"error": "Next round can only start from reveal phase"}, status=400)
|
return api_error(request, code="next_round_invalid_phase", status=400)
|
||||||
|
|
||||||
locked_session.current_round += 1
|
locked_session.current_round += 1
|
||||||
locked_session.status = GameSession.Status.LOBBY
|
locked_session.status = GameSession.Status.LOBBY
|
||||||
@@ -817,15 +817,15 @@ def finish_game(request: HttpRequest, code: str) -> JsonResponse:
|
|||||||
try:
|
try:
|
||||||
session = GameSession.objects.get(code=session_code)
|
session = GameSession.objects.get(code=session_code)
|
||||||
except GameSession.DoesNotExist:
|
except GameSession.DoesNotExist:
|
||||||
return JsonResponse({"error": "Session not found"}, status=404)
|
return api_error(request, code="session_not_found", status=404)
|
||||||
|
|
||||||
if session.host_id != request.user.id:
|
if session.host_id != request.user.id:
|
||||||
return JsonResponse({"error": "Only host can finish game"}, status=403)
|
return api_error(request, code="host_only_finish_game", status=403)
|
||||||
|
|
||||||
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.REVEAL:
|
if locked_session.status != GameSession.Status.REVEAL:
|
||||||
return JsonResponse({"error": "Game can only be finished from reveal phase"}, status=400)
|
return api_error(request, code="finish_game_invalid_phase", status=400)
|
||||||
|
|
||||||
locked_session.status = GameSession.Status.FINISHED
|
locked_session.status = GameSession.Status.FINISHED
|
||||||
locked_session.save(update_fields=["status"])
|
locked_session.save(update_fields=["status"])
|
||||||
@@ -865,20 +865,20 @@ def calculate_scores(request: HttpRequest, code: str, round_question_id: int) ->
|
|||||||
try:
|
try:
|
||||||
session = GameSession.objects.get(code=session_code)
|
session = GameSession.objects.get(code=session_code)
|
||||||
except GameSession.DoesNotExist:
|
except GameSession.DoesNotExist:
|
||||||
return JsonResponse({"error": "Session not found"}, status=404)
|
return api_error(request, code="session_not_found", status=404)
|
||||||
|
|
||||||
if session.host_id != request.user.id:
|
if session.host_id != request.user.id:
|
||||||
return JsonResponse({"error": "Only host can calculate scores"}, status=403)
|
return api_error(request, code="host_only_calculate_scores", status=403)
|
||||||
|
|
||||||
already_calculated = ScoreEvent.objects.filter(
|
already_calculated = ScoreEvent.objects.filter(
|
||||||
session=session,
|
session=session,
|
||||||
meta__round_question_id=round_question_id,
|
meta__round_question_id=round_question_id,
|
||||||
).exists()
|
).exists()
|
||||||
if already_calculated:
|
if already_calculated:
|
||||||
return JsonResponse({"error": "Scores already calculated for this round question"}, status=409)
|
return api_error(request, code="scores_already_calculated", status=409)
|
||||||
|
|
||||||
if session.status != GameSession.Status.GUESS:
|
if session.status != GameSession.Status.GUESS:
|
||||||
return JsonResponse({"error": "Scores can only be calculated in guess phase"}, status=400)
|
return api_error(request, code="calculate_scores_invalid_phase", status=400)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
round_question = RoundQuestion.objects.get(
|
round_question = RoundQuestion.objects.get(
|
||||||
@@ -887,16 +887,16 @@ def calculate_scores(request: HttpRequest, code: str, round_question_id: int) ->
|
|||||||
round_number=session.current_round,
|
round_number=session.current_round,
|
||||||
)
|
)
|
||||||
except RoundQuestion.DoesNotExist:
|
except RoundQuestion.DoesNotExist:
|
||||||
return JsonResponse({"error": "Round question not found"}, status=404)
|
return api_error(request, code="round_question_not_found", status=404)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
round_config = RoundConfig.objects.get(session=session, number=round_question.round_number)
|
round_config = RoundConfig.objects.get(session=session, number=round_question.round_number)
|
||||||
except RoundConfig.DoesNotExist:
|
except RoundConfig.DoesNotExist:
|
||||||
return JsonResponse({"error": "Round config missing"}, status=400)
|
return api_error(request, code="round_config_missing", status=400)
|
||||||
|
|
||||||
guesses = list(round_question.guesses.select_related("player"))
|
guesses = list(round_question.guesses.select_related("player"))
|
||||||
if not guesses:
|
if not guesses:
|
||||||
return JsonResponse({"error": "No guesses submitted for this round question"}, status=400)
|
return api_error(request, code="no_guesses_submitted", status=400)
|
||||||
|
|
||||||
bluff_counts = {}
|
bluff_counts = {}
|
||||||
for guess in guesses:
|
for guess in guesses:
|
||||||
@@ -906,7 +906,7 @@ def calculate_scores(request: HttpRequest, code: str, round_question_id: int) ->
|
|||||||
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.GUESS:
|
if locked_session.status != GameSession.Status.GUESS:
|
||||||
return JsonResponse({"error": "Scores can only be calculated in guess phase"}, status=400)
|
return api_error(request, code="calculate_scores_invalid_phase", status=400)
|
||||||
|
|
||||||
score_events = []
|
score_events = []
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
{
|
{
|
||||||
"locales": ["en", "da"],
|
"locales": [
|
||||||
|
"en",
|
||||||
|
"da"
|
||||||
|
],
|
||||||
"frontend_error_keys": [
|
"frontend_error_keys": [
|
||||||
"join_failed",
|
"join_failed",
|
||||||
"nickname_invalid",
|
"nickname_invalid",
|
||||||
@@ -11,50 +14,96 @@
|
|||||||
"unknown"
|
"unknown"
|
||||||
],
|
],
|
||||||
"backend_error_codes": [
|
"backend_error_codes": [
|
||||||
|
"calculate_scores_invalid_phase",
|
||||||
"category_has_no_questions",
|
"category_has_no_questions",
|
||||||
"category_not_found",
|
"category_not_found",
|
||||||
"category_slug_required",
|
"category_slug_required",
|
||||||
|
"finish_game_invalid_phase",
|
||||||
|
"guess_already_submitted",
|
||||||
|
"guess_submission_closed",
|
||||||
|
"guess_submission_invalid_phase",
|
||||||
|
"host_only_calculate_scores",
|
||||||
|
"host_only_finish_game",
|
||||||
"host_only_mix_answers",
|
"host_only_mix_answers",
|
||||||
"host_only_show_question",
|
"host_only_show_question",
|
||||||
|
"host_only_start_next_round",
|
||||||
"host_only_start_round",
|
"host_only_start_round",
|
||||||
|
"host_only_view_scoreboard",
|
||||||
|
"invalid_player_session_token",
|
||||||
|
"lie_already_submitted",
|
||||||
|
"lie_submission_closed",
|
||||||
|
"lie_submission_invalid_phase",
|
||||||
|
"lie_text_invalid",
|
||||||
"mix_answers_invalid_phase",
|
"mix_answers_invalid_phase",
|
||||||
|
"next_round_invalid_phase",
|
||||||
"nickname_invalid",
|
"nickname_invalid",
|
||||||
"nickname_taken",
|
"nickname_taken",
|
||||||
"no_available_questions",
|
"no_available_questions",
|
||||||
|
"no_guesses_submitted",
|
||||||
"not_enough_answers_to_mix",
|
"not_enough_answers_to_mix",
|
||||||
|
"player_id_required",
|
||||||
|
"player_not_found_in_session",
|
||||||
"question_already_shown",
|
"question_already_shown",
|
||||||
"round_already_configured",
|
"round_already_configured",
|
||||||
"round_config_missing",
|
"round_config_missing",
|
||||||
"round_question_not_found",
|
"round_question_not_found",
|
||||||
"round_start_invalid_phase",
|
"round_start_invalid_phase",
|
||||||
|
"scoreboard_invalid_phase",
|
||||||
|
"scores_already_calculated",
|
||||||
|
"selected_answer_invalid",
|
||||||
|
"selected_text_invalid",
|
||||||
"session_code_required",
|
"session_code_required",
|
||||||
"session_not_found",
|
"session_not_found",
|
||||||
"session_not_joinable",
|
"session_not_joinable",
|
||||||
|
"session_token_required",
|
||||||
"show_question_invalid_phase"
|
"show_question_invalid_phase"
|
||||||
],
|
],
|
||||||
"allowed_contract_only_backend_codes": [
|
"allowed_contract_only_backend_codes": [
|
||||||
"host_only_action"
|
"host_only_action"
|
||||||
],
|
],
|
||||||
"backend_error_keys": [
|
"backend_error_keys": [
|
||||||
|
"calculate_scores_invalid_phase",
|
||||||
"category_has_no_questions",
|
"category_has_no_questions",
|
||||||
"category_not_found",
|
"category_not_found",
|
||||||
"category_slug_required",
|
"category_slug_required",
|
||||||
|
"finish_game_invalid_phase",
|
||||||
|
"guess_already_submitted",
|
||||||
|
"guess_submission_closed",
|
||||||
|
"guess_submission_invalid_phase",
|
||||||
|
"host_only_calculate_scores",
|
||||||
|
"host_only_finish_game",
|
||||||
"host_only_mix_answers",
|
"host_only_mix_answers",
|
||||||
"host_only_show_question",
|
"host_only_show_question",
|
||||||
|
"host_only_start_next_round",
|
||||||
"host_only_start_round",
|
"host_only_start_round",
|
||||||
|
"host_only_view_scoreboard",
|
||||||
|
"invalid_player_session_token",
|
||||||
|
"lie_already_submitted",
|
||||||
|
"lie_submission_closed",
|
||||||
|
"lie_submission_invalid_phase",
|
||||||
|
"lie_text_invalid",
|
||||||
"mix_answers_invalid_phase",
|
"mix_answers_invalid_phase",
|
||||||
|
"next_round_invalid_phase",
|
||||||
"nickname_invalid",
|
"nickname_invalid",
|
||||||
"nickname_taken",
|
"nickname_taken",
|
||||||
"no_available_questions",
|
"no_available_questions",
|
||||||
|
"no_guesses_submitted",
|
||||||
"not_enough_answers_to_mix",
|
"not_enough_answers_to_mix",
|
||||||
|
"player_id_required",
|
||||||
|
"player_not_found_in_session",
|
||||||
"question_already_shown",
|
"question_already_shown",
|
||||||
"round_already_configured",
|
"round_already_configured",
|
||||||
"round_config_missing",
|
"round_config_missing",
|
||||||
"round_question_not_found",
|
"round_question_not_found",
|
||||||
"round_start_invalid_phase",
|
"round_start_invalid_phase",
|
||||||
|
"scoreboard_invalid_phase",
|
||||||
|
"scores_already_calculated",
|
||||||
|
"selected_answer_invalid",
|
||||||
|
"selected_text_invalid",
|
||||||
"session_code_required",
|
"session_code_required",
|
||||||
"session_not_found",
|
"session_not_found",
|
||||||
"session_not_joinable",
|
"session_not_joinable",
|
||||||
|
"session_token_required",
|
||||||
"show_question_invalid_phase"
|
"show_question_invalid_phase"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -296,7 +296,30 @@
|
|||||||
"not_enough_answers_to_mix": "not_enough_answers_to_mix",
|
"not_enough_answers_to_mix": "not_enough_answers_to_mix",
|
||||||
"host_only_start_round": "host_only_start_round",
|
"host_only_start_round": "host_only_start_round",
|
||||||
"host_only_show_question": "host_only_show_question",
|
"host_only_show_question": "host_only_show_question",
|
||||||
"host_only_mix_answers": "host_only_mix_answers"
|
"host_only_mix_answers": "host_only_mix_answers",
|
||||||
|
"player_id_required": "player_id_required",
|
||||||
|
"session_token_required": "session_token_required",
|
||||||
|
"lie_text_invalid": "lie_text_invalid",
|
||||||
|
"player_not_found_in_session": "player_not_found_in_session",
|
||||||
|
"invalid_player_session_token": "invalid_player_session_token",
|
||||||
|
"lie_submission_closed": "lie_submission_closed",
|
||||||
|
"lie_already_submitted": "lie_already_submitted",
|
||||||
|
"host_only_view_scoreboard": "host_only_view_scoreboard",
|
||||||
|
"scoreboard_invalid_phase": "scoreboard_invalid_phase",
|
||||||
|
"host_only_start_next_round": "host_only_start_next_round",
|
||||||
|
"next_round_invalid_phase": "next_round_invalid_phase",
|
||||||
|
"host_only_finish_game": "host_only_finish_game",
|
||||||
|
"finish_game_invalid_phase": "finish_game_invalid_phase",
|
||||||
|
"host_only_calculate_scores": "host_only_calculate_scores",
|
||||||
|
"scores_already_calculated": "scores_already_calculated",
|
||||||
|
"calculate_scores_invalid_phase": "calculate_scores_invalid_phase",
|
||||||
|
"no_guesses_submitted": "no_guesses_submitted",
|
||||||
|
"guess_submission_closed": "guess_submission_closed",
|
||||||
|
"selected_answer_invalid": "selected_answer_invalid",
|
||||||
|
"guess_already_submitted": "guess_already_submitted",
|
||||||
|
"lie_submission_invalid_phase": "lie_submission_invalid_phase",
|
||||||
|
"selected_text_invalid": "selected_text_invalid",
|
||||||
|
"guess_submission_invalid_phase": "guess_submission_invalid_phase"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"session_code_required": {
|
"session_code_required": {
|
||||||
@@ -378,6 +401,98 @@
|
|||||||
"host_only_mix_answers": {
|
"host_only_mix_answers": {
|
||||||
"en": "Only host can mix answers",
|
"en": "Only host can mix answers",
|
||||||
"da": "Kun værten kan blande svar"
|
"da": "Kun værten kan blande svar"
|
||||||
|
},
|
||||||
|
"player_id_required": {
|
||||||
|
"en": "player_id is required",
|
||||||
|
"da": "player_id er påkrævet"
|
||||||
|
},
|
||||||
|
"session_token_required": {
|
||||||
|
"en": "session_token is required",
|
||||||
|
"da": "session_token er påkrævet"
|
||||||
|
},
|
||||||
|
"lie_text_invalid": {
|
||||||
|
"en": "text must be between 1 and 255 characters",
|
||||||
|
"da": "text skal være mellem 1 og 255 tegn"
|
||||||
|
},
|
||||||
|
"player_not_found_in_session": {
|
||||||
|
"en": "Player not found in session",
|
||||||
|
"da": "Spiller blev ikke fundet i sessionen"
|
||||||
|
},
|
||||||
|
"invalid_player_session_token": {
|
||||||
|
"en": "Invalid player session token",
|
||||||
|
"da": "Ugyldigt spiller-session-token"
|
||||||
|
},
|
||||||
|
"lie_submission_closed": {
|
||||||
|
"en": "Lie submission window has closed",
|
||||||
|
"da": "Vinduet for løgn-indsendelse er lukket"
|
||||||
|
},
|
||||||
|
"lie_already_submitted": {
|
||||||
|
"en": "Lie already submitted for this player",
|
||||||
|
"da": "Løgnen er allerede indsendt for denne spiller"
|
||||||
|
},
|
||||||
|
"host_only_view_scoreboard": {
|
||||||
|
"en": "Only host can view scoreboard",
|
||||||
|
"da": "Kun værten kan se scoreboard"
|
||||||
|
},
|
||||||
|
"scoreboard_invalid_phase": {
|
||||||
|
"en": "Scoreboard is only available in reveal phase",
|
||||||
|
"da": "Scoreboard er kun tilgængeligt i reveal-fasen"
|
||||||
|
},
|
||||||
|
"host_only_start_next_round": {
|
||||||
|
"en": "Only host can start next round",
|
||||||
|
"da": "Kun værten kan starte næste runde"
|
||||||
|
},
|
||||||
|
"next_round_invalid_phase": {
|
||||||
|
"en": "Next round can only start from reveal phase",
|
||||||
|
"da": "Næste runde kan kun startes fra reveal-fasen"
|
||||||
|
},
|
||||||
|
"host_only_finish_game": {
|
||||||
|
"en": "Only host can finish game",
|
||||||
|
"da": "Kun værten kan afslutte spillet"
|
||||||
|
},
|
||||||
|
"finish_game_invalid_phase": {
|
||||||
|
"en": "Game can only be finished from reveal phase",
|
||||||
|
"da": "Spillet kan kun afsluttes fra reveal-fasen"
|
||||||
|
},
|
||||||
|
"host_only_calculate_scores": {
|
||||||
|
"en": "Only host can calculate scores",
|
||||||
|
"da": "Kun værten kan udregne score"
|
||||||
|
},
|
||||||
|
"scores_already_calculated": {
|
||||||
|
"en": "Scores already calculated for this round question",
|
||||||
|
"da": "Score er allerede udregnet for dette rundespørgsmål"
|
||||||
|
},
|
||||||
|
"calculate_scores_invalid_phase": {
|
||||||
|
"en": "Scores can only be calculated in guess phase",
|
||||||
|
"da": "Score kan kun udregnes i gættefasen"
|
||||||
|
},
|
||||||
|
"no_guesses_submitted": {
|
||||||
|
"en": "No guesses submitted for this round question",
|
||||||
|
"da": "Ingen gæt er indsendt for dette rundespørgsmål"
|
||||||
|
},
|
||||||
|
"guess_submission_closed": {
|
||||||
|
"en": "Guess submission window has closed",
|
||||||
|
"da": "Vinduet for gæt-indsendelse er lukket"
|
||||||
|
},
|
||||||
|
"selected_answer_invalid": {
|
||||||
|
"en": "Selected answer is not part of this round",
|
||||||
|
"da": "Det valgte svar er ikke en del af denne runde"
|
||||||
|
},
|
||||||
|
"guess_already_submitted": {
|
||||||
|
"en": "Guess already submitted for this player",
|
||||||
|
"da": "Gættet er allerede indsendt for denne spiller"
|
||||||
|
},
|
||||||
|
"lie_submission_invalid_phase": {
|
||||||
|
"en": "Lie submission is only allowed in lie phase",
|
||||||
|
"da": "Løgn kan kun indsendes i løgnefasen"
|
||||||
|
},
|
||||||
|
"selected_text_invalid": {
|
||||||
|
"en": "selected_text must be between 1 and 255 characters",
|
||||||
|
"da": "selected_text skal være mellem 1 og 255 tegn"
|
||||||
|
},
|
||||||
|
"guess_submission_invalid_phase": {
|
||||||
|
"en": "Guess submission is only allowed in guess phase",
|
||||||
|
"da": "Gæt kan kun indsendes i gættefasen"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -416,7 +531,30 @@
|
|||||||
"no_available_questions": "start_round_failed",
|
"no_available_questions": "start_round_failed",
|
||||||
"mix_answers_invalid_phase": "start_round_failed",
|
"mix_answers_invalid_phase": "start_round_failed",
|
||||||
"round_question_not_found": "start_round_failed",
|
"round_question_not_found": "start_round_failed",
|
||||||
"not_enough_answers_to_mix": "start_round_failed"
|
"not_enough_answers_to_mix": "start_round_failed",
|
||||||
|
"player_id_required": "unknown",
|
||||||
|
"session_token_required": "unknown",
|
||||||
|
"lie_text_invalid": "unknown",
|
||||||
|
"lie_submission_invalid_phase": "unknown",
|
||||||
|
"player_not_found_in_session": "unknown",
|
||||||
|
"invalid_player_session_token": "unknown",
|
||||||
|
"lie_submission_closed": "unknown",
|
||||||
|
"lie_already_submitted": "unknown",
|
||||||
|
"selected_text_invalid": "unknown",
|
||||||
|
"guess_submission_invalid_phase": "unknown",
|
||||||
|
"guess_submission_closed": "unknown",
|
||||||
|
"selected_answer_invalid": "unknown",
|
||||||
|
"guess_already_submitted": "unknown",
|
||||||
|
"host_only_view_scoreboard": "unknown",
|
||||||
|
"scoreboard_invalid_phase": "unknown",
|
||||||
|
"host_only_start_next_round": "unknown",
|
||||||
|
"next_round_invalid_phase": "unknown",
|
||||||
|
"host_only_finish_game": "unknown",
|
||||||
|
"finish_game_invalid_phase": "unknown",
|
||||||
|
"host_only_calculate_scores": "unknown",
|
||||||
|
"scores_already_calculated": "unknown",
|
||||||
|
"calculate_scores_invalid_phase": "unknown",
|
||||||
|
"no_guesses_submitted": "unknown"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user