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:
@@ -79,6 +79,9 @@ class AnswerResponseSerializer(serializers.Serializer):
|
|||||||
correctOptionId = serializers.CharField()
|
correctOptionId = serializers.CharField()
|
||||||
xpEarned = serializers.IntegerField()
|
xpEarned = serializers.IntegerField()
|
||||||
heartsRemaining = serializers.IntegerField()
|
heartsRemaining = serializers.IntegerField()
|
||||||
|
# حالتِ انیمیشنِ ماسکوت («رُبو») برای فرانت: جوابِ درست → happy، غلط →
|
||||||
|
# encourage. همخوان با RobotMood در MathApp_frontend.
|
||||||
|
mascotMood = serializers.CharField()
|
||||||
|
|
||||||
|
|
||||||
# ---- Lesson completion ----
|
# ---- Lesson completion ----
|
||||||
|
|||||||
@@ -490,3 +490,54 @@ class LessonCompleteGuestThrottleBypassTests(TestCase):
|
|||||||
self.assertEqual(UserLessonCompletion.objects.filter(user_id=guest2.id).count(), 0)
|
self.assertEqual(UserLessonCompletion.objects.filter(user_id=guest2.id).count(), 0)
|
||||||
self.assertFalse(UserStats.objects.filter(user_id=guest2.id).exists())
|
self.assertFalse(UserStats.objects.filter(user_id=guest2.id).exists())
|
||||||
self.assertFalse(NodeProgress.objects.filter(user_id=guest2.id, node=self.node).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')
|
||||||
|
|||||||
@@ -199,6 +199,7 @@ class QuestionAnswerView(APIView):
|
|||||||
'correctOptionId': str(correct_choice.id) if correct_choice else '',
|
'correctOptionId': str(correct_choice.id) if correct_choice else '',
|
||||||
'xpEarned': existing.xp_earned,
|
'xpEarned': existing.xp_earned,
|
||||||
'heartsRemaining': hearts.current_hearts if hearts else 5,
|
'heartsRemaining': hearts.current_hearts if hearts else 5,
|
||||||
|
'mascotMood': 'happy' if existing.correct else 'encourage',
|
||||||
}
|
}
|
||||||
return Response(AnswerResponseSerializer(data).data)
|
return Response(AnswerResponseSerializer(data).data)
|
||||||
|
|
||||||
@@ -245,6 +246,7 @@ class QuestionAnswerView(APIView):
|
|||||||
'correctOptionId': str(correct_choice.id) if correct_choice else '',
|
'correctOptionId': str(correct_choice.id) if correct_choice else '',
|
||||||
'xpEarned': xp_earned,
|
'xpEarned': xp_earned,
|
||||||
'heartsRemaining': hearts_remaining if hearts_remaining is not None else 5,
|
'heartsRemaining': hearts_remaining if hearts_remaining is not None else 5,
|
||||||
|
'mascotMood': 'happy' if choice.is_correct else 'encourage',
|
||||||
}
|
}
|
||||||
return Response(AnswerResponseSerializer(data).data)
|
return Response(AnswerResponseSerializer(data).data)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user