return mascotMood on answer submission

The math-app frontend prototype drives a RobotMascot with a fixed set of
moods; this gives that animation an explicit backend signal. Complements
the existing Lesson.mascot_animation / LessonIntro.mascotAnimation, which
are unchanged.

serializers.py
  - AnswerResponseSerializer: new mascotMood field.
views.py
  - QuestionAnswerView: mascotMood = 'happy' for a correct answer else
    'encourage', on both the fresh response and the idempotent-replay
    response.
tests.py
  - QuestionAnswerMascotMoodTests: correct -> happy, wrong -> encourage,
    and the mood is preserved across an idempotent replay.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-29 02:30:50 +03:30
parent 5416fbb2ba
commit 149f1ad9ae
3 changed files with 56 additions and 0 deletions

View File

@@ -79,6 +79,9 @@ class AnswerResponseSerializer(serializers.Serializer):
correctOptionId = serializers.CharField()
xpEarned = serializers.IntegerField()
heartsRemaining = serializers.IntegerField()
# حالتِ انیمیشنِ ماسکوت («رُبو») برای فرانت: جوابِ درست → happy، غلط →
# encourage. هم‌خوان با RobotMood در MathApp_frontend.
mascotMood = serializers.CharField()
# ---- Lesson completion ----

View File

@@ -490,3 +490,54 @@ class LessonCompleteGuestThrottleBypassTests(TestCase):
self.assertEqual(UserLessonCompletion.objects.filter(user_id=guest2.id).count(), 0)
self.assertFalse(UserStats.objects.filter(user_id=guest2.id).exists())
self.assertFalse(NodeProgress.objects.filter(user_id=guest2.id, node=self.node).exists())
class QuestionAnswerMascotMoodTests(TestCase):
"""پاسخِ /answer باید mascotMood بده: درست → happy، غلط → encourage
(هم‌خوان با RobotMood در MathApp_frontend)."""
def setUp(self):
from django.core.cache import cache
cache.clear()
self.client = APIClient()
self.user = User.objects.create(device_id='device-mascot-1', is_guest=True, name='')
self.token = issue_access_token(self.user.id)
grade = Grade.objects.create(title='پایه اول', icon='i', color='#fff')
unit = Unit.objects.create(grade=grade, title='فصل ۱', order=1)
node = Node.objects.create(unit=unit, title='گره ۱', order=1)
lesson = Lesson.objects.create(node=node, title='درس ۱', intro_text='...')
self.question = Question.objects.create(lesson=lesson, type='multiple_choice', prompt='۱+۱؟')
self.correct = Choice.objects.create(question=self.question, value='۲', is_correct=True)
self.wrong = Choice.objects.create(question=self.question, value='۳', is_correct=False)
def _answer(self, choice_id):
return self.client.post(
f'/api/questions/{self.question.id}/answer',
{'selectedOptionId': str(choice_id)}, format='json',
HTTP_AUTHORIZATION=f'Bearer {self.token}',
)
def test_correct_answer_mood_happy(self):
r = self._answer(self.correct.id)
self.assertEqual(r.status_code, 200)
self.assertEqual(r.data['mascotMood'], 'happy')
def test_wrong_answer_mood_encourage(self):
r = self._answer(self.wrong.id)
self.assertEqual(r.status_code, 200)
self.assertEqual(r.data['mascotMood'], 'encourage')
def test_idempotent_replay_keeps_mood(self):
k = 'mascot-idem-1'
first = self.client.post(
f'/api/questions/{self.question.id}/answer',
{'selectedOptionId': str(self.wrong.id)}, format='json',
HTTP_AUTHORIZATION=f'Bearer {self.token}', HTTP_IDEMPOTENCY_KEY=k,
)
replay = self.client.post(
f'/api/questions/{self.question.id}/answer',
{'selectedOptionId': str(self.wrong.id)}, format='json',
HTTP_AUTHORIZATION=f'Bearer {self.token}', HTTP_IDEMPOTENCY_KEY=k,
)
self.assertEqual(first.data['mascotMood'], 'encourage')
self.assertEqual(replay.data['mascotMood'], 'encourage')

View File

@@ -199,6 +199,7 @@ class QuestionAnswerView(APIView):
'correctOptionId': str(correct_choice.id) if correct_choice else '',
'xpEarned': existing.xp_earned,
'heartsRemaining': hearts.current_hearts if hearts else 5,
'mascotMood': 'happy' if existing.correct else 'encourage',
}
return Response(AnswerResponseSerializer(data).data)
@@ -245,6 +246,7 @@ class QuestionAnswerView(APIView):
'correctOptionId': str(correct_choice.id) if correct_choice else '',
'xpEarned': xp_earned,
'heartsRemaining': hearts_remaining if hearts_remaining is not None else 5,
'mascotMood': 'happy' if choice.is_correct else 'encourage',
}
return Response(AnswerResponseSerializer(data).data)