Merge pull request '[MVP][READY] #225 Backend i18n baseline (resolver + fallback)' (#232) from feat/issue-225-backend-i18n-baseline into main
All checks were successful
CI / test-and-quality (push) Successful in 3m59s
All checks were successful
CI / test-and-quality (push) Successful in 3m59s
This commit was merged in pull request #232.
This commit is contained in:
@@ -209,6 +209,8 @@ class StartRoundTests(TestCase):
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 403)
|
||||
self.assertEqual(response.json()["error_code"], "host_only_start_round")
|
||||
self.assertEqual(response.json()["locale"], "en")
|
||||
self.assertEqual(response.json()["error"], "Only host can start round")
|
||||
|
||||
def test_start_round_requires_existing_active_category_with_questions(self):
|
||||
@@ -228,6 +230,8 @@ class StartRoundTests(TestCase):
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(response.json()["error_code"], "category_has_no_questions")
|
||||
self.assertEqual(response.json()["locale"], "en")
|
||||
self.assertEqual(response.json()["error"], "Category has no active questions")
|
||||
|
||||
def test_start_round_rejects_non_lobby_session(self):
|
||||
@@ -244,6 +248,36 @@ class StartRoundTests(TestCase):
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(response.json()["error"], "Round can only be started from lobby")
|
||||
|
||||
def test_start_round_error_localizes_to_danish(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",
|
||||
HTTP_ACCEPT_LANGUAGE="da",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 403)
|
||||
self.assertEqual(response.json()["error_code"], "host_only_start_round")
|
||||
self.assertEqual(response.json()["locale"], "da")
|
||||
self.assertEqual(response.json()["error"], "Kun værten kan starte runden")
|
||||
|
||||
def test_start_round_error_falls_back_to_english_for_unsupported_locale(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",
|
||||
HTTP_ACCEPT_LANGUAGE="fr",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 403)
|
||||
self.assertEqual(response.json()["error_code"], "host_only_start_round")
|
||||
self.assertEqual(response.json()["locale"], "en")
|
||||
self.assertEqual(response.json()["error"], "Only host can start round")
|
||||
|
||||
|
||||
class LieSubmissionTests(TestCase):
|
||||
def setUp(self):
|
||||
@@ -434,6 +468,8 @@ class MixAnswersTests(TestCase):
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 403)
|
||||
self.assertEqual(response.json()["error_code"], "host_only_mix_answers")
|
||||
self.assertEqual(response.json()["locale"], "en")
|
||||
self.assertEqual(response.json()["error"], "Only host can mix answers")
|
||||
|
||||
def test_mix_answers_deduplicates_case_insensitive_lies(self):
|
||||
|
||||
@@ -268,7 +268,11 @@ def start_round(request: HttpRequest, code: str) -> JsonResponse:
|
||||
)
|
||||
|
||||
if session.host_id != request.user.id:
|
||||
return JsonResponse({"error": "Only host can start round"}, status=403)
|
||||
return api_error(
|
||||
request,
|
||||
key=ERROR_CODES.get("host_only_start_round", "host_only_start_round"),
|
||||
status=403,
|
||||
)
|
||||
|
||||
if session.status != GameSession.Status.LOBBY:
|
||||
return api_error(
|
||||
@@ -287,7 +291,11 @@ def start_round(request: HttpRequest, code: str) -> JsonResponse:
|
||||
)
|
||||
|
||||
if not Question.objects.filter(category=category, is_active=True).exists():
|
||||
return JsonResponse({"error": "Category has no active questions"}, status=400)
|
||||
return api_error(
|
||||
request,
|
||||
key=ERROR_CODES.get("category_has_no_questions", "category_has_no_questions"),
|
||||
status=400,
|
||||
)
|
||||
|
||||
with transaction.atomic():
|
||||
session = GameSession.objects.select_for_update().get(pk=session.pk)
|
||||
@@ -340,21 +348,41 @@ def show_question(request: HttpRequest, code: str) -> JsonResponse:
|
||||
try:
|
||||
session = GameSession.objects.get(code=session_code)
|
||||
except GameSession.DoesNotExist:
|
||||
return JsonResponse({"error": "Session not found"}, status=404)
|
||||
return api_error(
|
||||
request,
|
||||
key=ERROR_CODES.get("session_not_found", "session_not_found"),
|
||||
status=404,
|
||||
)
|
||||
|
||||
if session.host_id != request.user.id:
|
||||
return JsonResponse({"error": "Only host can show question"}, status=403)
|
||||
return api_error(
|
||||
request,
|
||||
key=ERROR_CODES.get("host_only_show_question", "host_only_show_question"),
|
||||
status=403,
|
||||
)
|
||||
|
||||
if session.status != GameSession.Status.LIE:
|
||||
return JsonResponse({"error": "Question can only be shown in lie phase"}, status=400)
|
||||
return api_error(
|
||||
request,
|
||||
key=ERROR_CODES.get("show_question_invalid_phase", "show_question_invalid_phase"),
|
||||
status=400,
|
||||
)
|
||||
|
||||
try:
|
||||
round_config = RoundConfig.objects.get(session=session, number=session.current_round)
|
||||
except RoundConfig.DoesNotExist:
|
||||
return JsonResponse({"error": "Round config missing"}, status=400)
|
||||
return api_error(
|
||||
request,
|
||||
key=ERROR_CODES.get("round_config_missing", "round_config_missing"),
|
||||
status=400,
|
||||
)
|
||||
|
||||
if RoundQuestion.objects.filter(session=session, round_number=session.current_round).exists():
|
||||
return JsonResponse({"error": "Question already shown for this round"}, status=409)
|
||||
return api_error(
|
||||
request,
|
||||
key=ERROR_CODES.get("question_already_shown", "question_already_shown"),
|
||||
status=409,
|
||||
)
|
||||
|
||||
used_question_ids = RoundQuestion.objects.filter(session=session).values_list("question_id", flat=True)
|
||||
available_questions = Question.objects.filter(
|
||||
@@ -363,7 +391,11 @@ def show_question(request: HttpRequest, code: str) -> JsonResponse:
|
||||
).exclude(pk__in=used_question_ids)
|
||||
|
||||
if not available_questions.exists():
|
||||
return JsonResponse({"error": "No available questions in category"}, status=400)
|
||||
return api_error(
|
||||
request,
|
||||
key=ERROR_CODES.get("no_available_questions", "no_available_questions"),
|
||||
status=400,
|
||||
)
|
||||
|
||||
question = random.choice(list(available_questions))
|
||||
round_question = RoundQuestion.objects.create(
|
||||
@@ -473,13 +505,25 @@ def mix_answers(request: HttpRequest, code: str, round_question_id: int) -> Json
|
||||
try:
|
||||
session = GameSession.objects.get(code=session_code)
|
||||
except GameSession.DoesNotExist:
|
||||
return JsonResponse({"error": "Session not found"}, status=404)
|
||||
return api_error(
|
||||
request,
|
||||
key=ERROR_CODES.get("session_not_found", "session_not_found"),
|
||||
status=404,
|
||||
)
|
||||
|
||||
if session.host_id != request.user.id:
|
||||
return JsonResponse({"error": "Only host can mix answers"}, status=403)
|
||||
return api_error(
|
||||
request,
|
||||
key=ERROR_CODES.get("host_only_mix_answers", "host_only_mix_answers"),
|
||||
status=403,
|
||||
)
|
||||
|
||||
if session.status not in {GameSession.Status.LIE, GameSession.Status.GUESS}:
|
||||
return JsonResponse({"error": "Answers can only be mixed in lie or guess phase"}, status=400)
|
||||
return api_error(
|
||||
request,
|
||||
key=ERROR_CODES.get("mix_answers_invalid_phase", "mix_answers_invalid_phase"),
|
||||
status=400,
|
||||
)
|
||||
|
||||
try:
|
||||
round_question = RoundQuestion.objects.get(
|
||||
@@ -488,12 +532,20 @@ def mix_answers(request: HttpRequest, code: str, round_question_id: int) -> Json
|
||||
round_number=session.current_round,
|
||||
)
|
||||
except RoundQuestion.DoesNotExist:
|
||||
return JsonResponse({"error": "Round question not found"}, status=404)
|
||||
return api_error(
|
||||
request,
|
||||
key=ERROR_CODES.get("round_question_not_found", "round_question_not_found"),
|
||||
status=404,
|
||||
)
|
||||
|
||||
with transaction.atomic():
|
||||
locked_session = GameSession.objects.select_for_update().get(pk=session.pk)
|
||||
if locked_session.status not in {GameSession.Status.LIE, GameSession.Status.GUESS}:
|
||||
return JsonResponse({"error": "Answers can only be mixed in lie or guess phase"}, status=400)
|
||||
return api_error(
|
||||
request,
|
||||
key=ERROR_CODES.get("mix_answers_invalid_phase", "mix_answers_invalid_phase"),
|
||||
status=400,
|
||||
)
|
||||
|
||||
locked_round_question = RoundQuestion.objects.select_for_update().get(pk=round_question.pk)
|
||||
|
||||
@@ -509,7 +561,11 @@ def mix_answers(request: HttpRequest, code: str, round_question_id: int) -> Json
|
||||
deduped_answers.append(text.strip())
|
||||
|
||||
if len(deduped_answers) < 2:
|
||||
return JsonResponse({"error": "Not enough answers to mix"}, status=400)
|
||||
return api_error(
|
||||
request,
|
||||
key=ERROR_CODES.get("not_enough_answers_to_mix", "not_enough_answers_to_mix"),
|
||||
status=400,
|
||||
)
|
||||
|
||||
random.shuffle(deduped_answers)
|
||||
locked_round_question.mixed_answers = deduped_answers
|
||||
|
||||
@@ -281,7 +281,18 @@
|
||||
"category_slug_required": "category_slug_required",
|
||||
"category_not_found": "category_not_found",
|
||||
"round_start_invalid_phase": "round_start_invalid_phase",
|
||||
"round_already_configured": "round_already_configured"
|
||||
"round_already_configured": "round_already_configured",
|
||||
"category_has_no_questions": "category_has_no_questions",
|
||||
"show_question_invalid_phase": "show_question_invalid_phase",
|
||||
"round_config_missing": "round_config_missing",
|
||||
"question_already_shown": "question_already_shown",
|
||||
"no_available_questions": "no_available_questions",
|
||||
"mix_answers_invalid_phase": "mix_answers_invalid_phase",
|
||||
"round_question_not_found": "round_question_not_found",
|
||||
"not_enough_answers_to_mix": "not_enough_answers_to_mix",
|
||||
"host_only_start_round": "host_only_start_round",
|
||||
"host_only_show_question": "host_only_show_question",
|
||||
"host_only_mix_answers": "host_only_mix_answers"
|
||||
},
|
||||
"errors": {
|
||||
"session_code_required": {
|
||||
@@ -319,6 +330,50 @@
|
||||
"round_already_configured": {
|
||||
"en": "Round already configured",
|
||||
"da": "Runden er allerede konfigureret"
|
||||
},
|
||||
"category_has_no_questions": {
|
||||
"en": "Category has no active questions",
|
||||
"da": "Kategorien har ingen aktive spørgsmål"
|
||||
},
|
||||
"show_question_invalid_phase": {
|
||||
"en": "Question can only be shown in lie phase",
|
||||
"da": "Spørgsmålet kan kun vises i løgnefasen"
|
||||
},
|
||||
"round_config_missing": {
|
||||
"en": "Round config missing",
|
||||
"da": "Rundekonfiguration mangler"
|
||||
},
|
||||
"question_already_shown": {
|
||||
"en": "Question already shown for this round",
|
||||
"da": "Spørgsmålet er allerede vist for denne runde"
|
||||
},
|
||||
"no_available_questions": {
|
||||
"en": "No available questions in category",
|
||||
"da": "Ingen tilgængelige spørgsmål i kategorien"
|
||||
},
|
||||
"mix_answers_invalid_phase": {
|
||||
"en": "Answers can only be mixed in lie or guess phase",
|
||||
"da": "Svar kan kun blandes i løgne- eller gættefasen"
|
||||
},
|
||||
"round_question_not_found": {
|
||||
"en": "Round question not found",
|
||||
"da": "Rundespørgsmål blev ikke fundet"
|
||||
},
|
||||
"not_enough_answers_to_mix": {
|
||||
"en": "Not enough answers to mix",
|
||||
"da": "Ikke nok svar at blande"
|
||||
},
|
||||
"host_only_start_round": {
|
||||
"en": "Only host can start round",
|
||||
"da": "Kun værten kan starte runden"
|
||||
},
|
||||
"host_only_show_question": {
|
||||
"en": "Only host can show question",
|
||||
"da": "Kun værten kan vise spørgsmålet"
|
||||
},
|
||||
"host_only_mix_answers": {
|
||||
"en": "Only host can mix answers",
|
||||
"da": "Kun værten kan blande svar"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -345,7 +400,18 @@
|
||||
"category_slug_required": "start_round_failed",
|
||||
"category_not_found": "start_round_failed",
|
||||
"round_start_invalid_phase": "start_round_failed",
|
||||
"round_already_configured": "start_round_failed"
|
||||
"round_already_configured": "start_round_failed",
|
||||
"host_only_start_round": "start_round_failed",
|
||||
"host_only_show_question": "start_round_failed",
|
||||
"host_only_mix_answers": "start_round_failed",
|
||||
"category_has_no_questions": "start_round_failed",
|
||||
"show_question_invalid_phase": "start_round_failed",
|
||||
"round_config_missing": "start_round_failed",
|
||||
"question_already_shown": "start_round_failed",
|
||||
"no_available_questions": "start_round_failed",
|
||||
"mix_answers_invalid_phase": "start_round_failed",
|
||||
"round_question_not_found": "start_round_failed",
|
||||
"not_enough_answers_to_mix": "start_round_failed"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user