Files
MathApp/content/tests_node_completion.py

268 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
پایانِ آزمونِ جزیره → جزیره‌ی بعدی باز می‌شود.
شکافی که این تست‌ها می‌بندند: اپ با آزمونِ چهارسوالیِ هر جزیره بازی
می‌کند، ولی جزیره فقط وقتی «تمام» حساب می‌شد که تک‌تکِ درس‌هایش (۸ تا ۱۳
درس) جدا تمام شده باشند؛ دانش‌آموز بعد از جزیره‌ی اولِ هر فصل گیر می‌کرد.
"""
from subscription.models import Subscription
from django.core.cache import cache
from django.test import TestCase
from accounts.authentication import issue_access_token
from accounts.models import User
from content.models import (
Choice, Grade, Lesson, Node, NodeProgress, Question, Unit, UserLessonCompletion, UserNodeCompletion,
)
from content.testing import record_answers
from content.views import NodeQuizView
from notifications.models import Notification
from progress.models import UserStats
class NodeQuizCompleteTests(TestCase):
def setUp(self):
cache.clear()
self.grade = Grade.objects.create(title='پایه نهم', icon='', color='')
self.unit = Unit.objects.create(grade=self.grade, title='فصل ۱', order=1)
self.next_unit = Unit.objects.create(grade=self.grade, title='فصل ۲', order=2)
self.first = self._node(self.unit, 'اول', 1, lessons=5)
self.second = self._node(self.unit, 'دوم', 2, lessons=3)
self.other_unit_first = self._node(self.next_unit, 'فصلِ بعد', 1, lessons=2)
self.user = User.objects.create(phone='09120000009', grade_id=self.grade.id)
# فصلِ دوم اشتراک می‌خواهد (content.views.unit_is_free)؛ این تست ترتیبِ باز شدن را می‌سنجد، نه پرداخت.
Subscription.objects.create(user_id=self.user.id, status='active', expires_at=None)
self.auth = {'HTTP_AUTHORIZATION': f'Bearer {issue_access_token(self.user.id)}'}
def _node(self, unit, title, order, lessons):
node = Node.objects.create(unit=unit, title=title, order=order)
for i in range(lessons):
lesson = Lesson.objects.create(node=node, title=f'{title} {i}')
question = Question.objects.create(
lesson=lesson, type='multiple_choice', prompt=f'سوال {i}')
Choice.objects.create(question=question, value='الف', is_correct=True)
Choice.objects.create(question=question, value='ب', is_correct=False)
return node
def _answer(self, node, correct, wrong=0):
"""مثلِ اپ: اول جواب‌ها ثبت می‌شوند (`correct` تای اول درست، `wrong` تای بعدی غلط)، بعد پایانِ آزمون."""
questions = list(Question.objects.filter(lesson__node=node).order_by('id')[:correct + wrong])
for index, question in enumerate(questions):
choice = question.choices.get(is_correct=index < correct)
self.client.post(f'/api/questions/{question.pk}/answer', {'selectedOptionId': choice.pk},
content_type='application/json', **self.auth)
def _complete(self, node, correct=3, total=4, answer=True, **headers):
if answer:
# سرور همه‌ی سوال‌های آزمون را جواب‌داده می‌خواهد (دستِ‌کم آزمونِ استانداردِ ۴ تایی).
pool = Question.objects.filter(lesson__node=node).count()
needed = max(total, min(NodeQuizView.DEFAULT_SIZE, pool))
self._answer(node, correct, wrong=max(needed - correct, 0))
return self.client.post(
f'/api/nodes/{node.pk}/quiz/complete',
{'correctCount': correct, 'totalQuestions': total},
content_type='application/json', **self.auth, **headers,
)
def _statuses(self):
units = self.client.get('/api/home', **self.auth).json()['units']
return {n['title']: n['status'] for u in units for n in u['nodes']}
def test_finishing_the_quiz_opens_the_next_island(self):
self.assertEqual(self._statuses()['دوم'], 'locked')
response = self._complete(self.first)
self.assertEqual(response.status_code, 200)
statuses = self._statuses()
self.assertEqual(statuses['اول'], 'completed')
self.assertEqual(statuses['دوم'], 'unlocked')
def test_a_low_score_still_moves_on(self):
"""قاعده عمداً ساده است: آزمون که تمام شد، جزیره تمام است."""
self._complete(self.first, correct=0)
self.assertEqual(self._statuses()['دوم'], 'unlocked')
def test_it_names_the_next_island(self):
body = self._complete(self.first).json()
self.assertEqual(body['nextNodeId'], str(self.second.pk))
# آخرین جزیره‌ی فصل → اولین جزیره‌ی فصلِ بعد
body = self._complete(self.second, total=3).json()
self.assertEqual(body['nextNodeId'], str(self.other_unit_first.pk))
# آخرین جزیره‌ی پایه
body = self._complete(self.other_unit_first, correct=2, total=2).json()
self.assertIsNone(body['nextNodeId'])
def test_rewards_only_the_first_time(self):
first = self._complete(self.first, correct=4).json()
again = self._complete(self.first, correct=4).json()
self.assertEqual(first['xpEarned'], 40)
self.assertEqual(first['stars'], 3)
self.assertEqual(first['coinsEarned'], 15)
self.assertEqual(again['xpEarned'], 0)
self.assertEqual(again['coinsEarned'], 0)
self.assertEqual(UserStats.objects.get(user_id=self.user.id).xp, 40)
def test_answers_that_were_never_given_earn_nothing(self):
# پیش‌تر correctCount همان‌طور که کلاینت فرستاده بود پذیرفته می‌شد؛ بعد (۲۰۲۶-۱۰-۰۷) بی‌جواب
# اصلاً تمام نمی‌شود: ۴۰۹، و نه جزیره تمام می‌شود نه امتیازی ثبت.
response = self._complete(self.first, correct=4, answer=False)
self.assertEqual((response.status_code, response.json()['code']), (409, 'answers_missing'))
self.assertFalse(UserNodeCompletion.objects.filter(user_id=self.user.id).exists())
self.assertEqual(self._statuses()['دوم'], 'locked')
# چهار جواب ثبت شده ولی فقط دو تا درست؛ ادعای ۴ درست فقط برای همان دو امتیاز می‌دهد.
self._answer(self.first, correct=2, wrong=2)
body = self._complete(self.first, correct=4, total=4, answer=False).json()
self.assertEqual(body['xpEarned'], 20)
def test_understating_the_question_count_does_not_buy_stars(self):
"""با totalQuestions=1 و یک جوابِ درست ۳ ستاره می‌شد؛ آزمونِ استانداردِ سرور ۴ سوال است."""
self._answer(self.first, correct=1, wrong=3)
body = self._complete(self.first, correct=1, total=1, answer=False).json()
self.assertEqual(body['stars'], 1) # ۱ از ۴
self.assertEqual(UserNodeCompletion.objects.get(user_id=self.user.id).total_questions, 4)
def test_answering_fewer_questions_than_the_quiz_has_is_refused(self):
self._answer(self.first, correct=3)
response = self._complete(self.first, correct=3, total=4, answer=False)
self.assertEqual(response.status_code, 409)
self.assertEqual((response.json()['answered'], response.json()['required']), (3, 4))
def test_a_locked_island_cannot_be_finished(self):
"""جزیره‌ی دوم تا اولی تمام نشده قفل است؛ حتی با جوابِ همه‌ی سوال‌هایش."""
self._answer(self.second, correct=3)
response = self._complete(self.second, correct=3, total=3, answer=False)
self.assertEqual((response.status_code, response.json()['code']), (403, 'node_locked'))
self.assertFalse(UserNodeCompletion.objects.filter(user_id=self.user.id).exists())
self.assertEqual(self._statuses()['دوم'], 'locked')
def test_the_first_island_of_every_unit_is_open(self):
"""همان‌طور که نقشه نشان می‌دهد: اولین جزیره‌ی فصل‌ها بی‌قید باز است."""
self.assertEqual(self._statuses()['فصلِ بعد'], 'unlocked')
self.assertEqual(self._complete(self.other_unit_first, correct=2, total=2).status_code, 200)
def test_an_open_island_without_questions_passes_without_answers_and_earns_nothing(self):
"""چیزی برای جواب‌دادن نیست؛ رد می‌شود تا مسیرِ بقیه را نبندد (بی‌ستاره، بی‌امتیاز)."""
empty = Node.objects.create(unit=self.next_unit, title='خالی', order=0) # اولین جزیره‌ی فصل: باز
Lesson.objects.create(node=empty, title='درسِ بی‌سوال')
response = self.client.post(
f'/api/nodes/{empty.pk}/quiz/complete', {'correctCount': 0, 'totalQuestions': 1},
content_type='application/json', **self.auth)
self.assertEqual(response.status_code, 200, response.content)
self.assertEqual((response.json()['xpEarned'], response.json()['stars']), (0, 0))
self.assertEqual(response.json()['nextNodeId'], str(self.other_unit_first.pk))
def test_a_locked_island_without_questions_is_still_locked(self):
empty = Node.objects.create(unit=self.unit, title='خالیِ قفل', order=9) # بعد از دو جزیره‌ی تمام‌نشده
Lesson.objects.create(node=empty, title='درسِ بی‌سوال')
response = self.client.post(
f'/api/nodes/{empty.pk}/quiz/complete', {'correctCount': 0, 'totalQuestions': 1},
content_type='application/json', **self.auth)
self.assertEqual((response.status_code, response.json()['code']), (403, 'node_locked'))
def test_same_idempotency_key_does_not_double_count(self):
self._complete(self.first, correct=4, HTTP_IDEMPOTENCY_KEY='k1')
body = self._complete(self.first, correct=4, HTTP_IDEMPOTENCY_KEY='k1').json()
self.assertEqual(body['xpEarned'], 40) # همان نتیجه‌ی قبلی
self.assertEqual(UserStats.objects.get(user_id=self.user.id).xp, 40)
def test_cannot_claim_more_questions_than_the_island_has(self):
response = self._complete(self.first, correct=9, total=9)
self.assertEqual(response.status_code, 400)
def test_cannot_claim_more_correct_than_asked(self):
response = self._complete(self.first, correct=5, total=4)
self.assertEqual(response.status_code, 400)
def test_requires_sign_in(self):
response = self.client.post(
f'/api/nodes/{self.first.pk}/quiz/complete',
{'correctCount': 1, 'totalQuestions': 4}, content_type='application/json',
)
self.assertEqual(response.status_code, 401)
def test_it_sends_a_notification_once(self):
self._complete(self.first)
self._complete(self.first)
notes = Notification.objects.filter(user_id=self.user.id, type='node_completed')
self.assertEqual(notes.count(), 1)
self.assertIn('اول', notes.first().body)
def test_node_progress_is_completed_and_stays_completed(self):
"""درسِ تازه در همان جزیره نباید آن را به in_progress برگرداند."""
self._complete(self.first)
lesson = self.first.lessons.first()
record_answers(self.user.id, lesson=lesson)
self.client.post(
f'/api/lessons/{lesson.pk}/complete',
{'correctCount': 1, 'totalQuestions': 1},
content_type='application/json', **self.auth,
)
progress = NodeProgress.objects.get(user_id=self.user.id, node=self.first)
self.assertEqual(progress.status, 'completed')
def test_progress_percent_counts_the_islands_lessons(self):
"""جزیره‌ی ۵ درسی از ۱۰ درسِ پایه → ۵۰٪، و فقط در پایه‌ی خودِ کاربر."""
Grade.objects.create(title='پایه دهم', icon='', color='') # مخرج را بزرگ نکند
body = self._complete(self.first).json()
self.assertEqual(body['stats']['progressPercent'], 50.0)
home = self.client.get('/api/home', **self.auth).json()
self.assertEqual(home['stats']['progressPercent'], 50.0)
def test_lesson_completion_progress_is_per_grade_too(self):
# درسی از جزیره‌ی باز (جزیره‌ی دوم تا اولی تمام نشده قفل است و درسش را نمی‌شود تمام کرد).
lesson = self.first.lessons.first()
record_answers(self.user.id, lesson=lesson)
body = self.client.post(
f'/api/lessons/{lesson.pk}/complete',
{'correctCount': 1, 'totalQuestions': 1},
content_type='application/json', **self.auth,
).json()
self.assertEqual(body['stats']['progressPercent'], 10.0) # ۱ از ۱۰ درس
self.assertTrue(UserLessonCompletion.objects.filter(user_id=self.user.id).exists())
class NodeQuizRotationTests(TestCase):
"""دوباره‌بازیِ یک جزیره آزمونِ تازه می‌دهد؛ رفرش تا پایانِ آزمون همان را می‌دهد."""
def setUp(self):
cache.clear()
grade = Grade.objects.create(title='پایه نهم', icon='', color='')
unit = Unit.objects.create(grade=grade, title='فصل ۱', order=1)
self.node = Node.objects.create(unit=unit, title='اول', order=1)
for i in range(4):
lesson = Lesson.objects.create(node=self.node, title=f'درس {i}')
for j in range(12):
question = Question.objects.create(
lesson=lesson, type='multiple_choice', prompt=f'سوال {i}-{j}')
Choice.objects.create(question=question, value='الف', is_correct=True)
Choice.objects.create(question=question, value='ب', is_correct=False)
self.user = User.objects.create(phone='09120000019', grade_id=grade.id)
self.auth = {'HTTP_AUTHORIZATION': f'Bearer {issue_access_token(self.user.id)}'}
def _ids(self):
body = self.client.get(f'/api/nodes/{self.node.pk}/quiz', **self.auth).json()
return [q['id'] for q in body['questions']]
def test_refresh_keeps_the_quiz_until_it_is_completed(self):
self.assertEqual(self._ids(), self._ids())
def test_each_completed_round_draws_a_fresh_quiz(self):
first = self._ids()
UserNodeCompletion.objects.create(
user_id=self.user.id, node=self.node, correct_count=4, total_questions=4, stars=3)
second = self._ids()
self.assertNotEqual(first, second)
self.assertEqual(second, self._ids())