Files
MathApp/content/tests_finish_requires_answers.py

210 lines
12 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.
"""
پایانِ درس و جزیره بی‌جواب و روی جزیره‌ی قفل پذیرفته نمی‌شود (۲۰۲۶-۱۰-۰۷).
تا پیش از این: کسی که قلب داشت و هیچ سوالی جواب نداده بود جزیره را تمام می‌کرد، حتی جزیره‌ی قفل را
(با correctCount=0)؛ نقشه هم وضعیتِ «باز» را از روی درس‌های تمام‌شده حساب می‌کرد، پس تمام‌کردنِ
درسِ جزیره‌ی قفل آن را باز می‌کرد. فقط قلبِ صفر جلوی بی‌جواب‌تمام‌کردن را می‌گرفت.
"""
from subscription.models import Subscription
from django.core.cache import cache
from django.test import TestCase, override_settings
from rest_framework.test import APIClient
from accounts.authentication import issue_access_token
from accounts.models import User
from progress.models import UserHearts, UserStats
from .models import (
Choice, Grade, Lesson, Node, NodeProgress, Question, Unit, UserLessonCompletion, UserNodeCompletion,
)
from .testing import record_answers
from .views import node_is_unlocked
class _World:
def build(self, phone):
cache.clear()
self.grade = Grade.objects.create(title='پایه نهم', icon='', color='')
self.unit = Unit.objects.create(grade=self.grade, title='فصل ۱', order=1)
self.other_unit = Unit.objects.create(grade=self.grade, title='فصل ۲', order=2)
self.n1 = self.node(self.unit, 'یک', 1)
self.n2 = self.node(self.unit, 'دو', 2)
self.n3 = self.node(self.unit, 'سه', 3)
self.m1 = self.node(self.other_unit, 'الف', 1)
self.user = User.objects.create(phone=phone, grade_id=self.grade.id)
self.client = APIClient()
self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {issue_access_token(self.user.id)}')
def node(self, unit, title, order, lessons=2):
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}')
q = Question.objects.create(lesson=lesson, type='multiple_choice', prompt='؟')
Choice.objects.create(question=q, value='درست', is_correct=True)
Choice.objects.create(question=q, value='غلط', is_correct=False)
return node
def finish_node(self, node, correct=0, total=2):
return self.client.post(f'/api/nodes/{node.id}/quiz/complete',
{'correctCount': correct, 'totalQuestions': total}, format='json')
def finish_lesson(self, lesson):
return self.client.post(f'/api/lessons/{lesson.id}/complete',
{'correctCount': 0, 'totalQuestions': 1}, format='json')
def statuses(self):
units = self.client.get('/api/home').json()['units']
return {n['title']: n['status'] for u in units for n in u['nodes']}
class FinishWithoutAnswersTests(_World, TestCase):
def setUp(self):
self.build('09120007801')
def tearDown(self):
cache.clear()
def test_a_user_with_hearts_and_no_answers_cannot_finish_the_island(self):
"""همان جمله‌ای که باز مانده بود: قلب دارد، هیچ سوالی جواب نداده."""
self.assertEqual(UserHearts.objects.filter(user_id=self.user.id).count(), 0) # قلبِ پیش‌فرضِ پر
response = self.finish_node(self.n1)
self.assertEqual((response.status_code, response.json()['code']), (409, 'answers_missing'))
self.assertEqual((response.json()['answered'], response.json()['required']), (0, 2))
self.assertFalse(UserNodeCompletion.objects.exists())
self.assertFalse(NodeProgress.objects.filter(status='completed').exists())
self.assertEqual(self.statuses()['دو'], 'locked')
def test_the_locked_island_cannot_be_finished_even_after_answering_everything(self):
record_answers(self.user.id, node=self.n2)
response = self.finish_node(self.n2, correct=2)
self.assertEqual((response.status_code, response.json()['code']), (403, 'node_locked'))
self.assertFalse(UserNodeCompletion.objects.exists())
self.assertFalse(UserStats.objects.filter(user_id=self.user.id, xp__gt=0).exists())
def test_the_locked_island_cannot_be_finished_with_zero_answers_either(self):
response = self.finish_node(self.n3)
self.assertEqual((response.status_code, response.json()['code']), (403, 'node_locked'))
def test_finishing_the_lessons_of_a_locked_island_does_not_unlock_it(self):
"""بدونِ قفلِ درس، تمام‌کردنِ یک درسِ جزیره‌ی قفل وضعیتِ آن را «باز» می‌کرد."""
lesson = self.n2.lessons.first()
record_answers(self.user.id, lesson=lesson)
response = self.finish_lesson(lesson)
self.assertEqual((response.status_code, response.json()['code']), (403, 'node_locked'))
self.assertFalse(UserLessonCompletion.objects.exists())
self.assertEqual(self.statuses()['دو'], 'locked')
def test_a_lesson_cannot_be_finished_without_answers(self):
lesson = self.n1.lessons.first()
response = self.finish_lesson(lesson)
self.assertEqual((response.status_code, response.json()['code']), (409, 'answers_missing'))
self.assertFalse(UserLessonCompletion.objects.exists())
def test_a_lesson_with_some_answers_missing_is_refused(self):
lesson = self.n1.lessons.first()
second = Question.objects.create(lesson=lesson, type='multiple_choice', prompt='دوم؟')
Choice.objects.create(question=second, value='x', is_correct=True)
first = lesson.questions.exclude(pk=second.pk).get()
record_answers(self.user.id, lesson=lesson)
from .models import UserAnswer
UserAnswer.objects.filter(question=second).delete() # فقط سوالِ اول جواب داده شده
response = self.finish_lesson(lesson)
self.assertEqual((response.status_code, response.json()['answered']), (409, 1))
record_answers(self.user.id, lesson=lesson)
self.assertEqual(self.finish_lesson(lesson).status_code, 200)
self.assertTrue(first.answers.exists())
def test_a_lesson_without_questions_in_an_open_island_still_passes_and_earns_nothing(self):
empty = Lesson.objects.create(node=self.n1, title='بی‌سوال')
response = self.finish_lesson(empty)
self.assertEqual(response.status_code, 200, response.content)
self.assertEqual((response.json()['xpEarned'], response.json()['stars']), (0, 0))
def test_a_lesson_without_questions_in_a_locked_island_is_still_locked(self):
empty = Lesson.objects.create(node=self.n3, title='بی‌سوالِ قفل')
response = self.finish_lesson(empty)
self.assertEqual((response.status_code, response.json()['code']), (403, 'node_locked'))
self.assertFalse(UserLessonCompletion.objects.exists())
def test_the_normal_path_still_works_in_order(self):
"""جزیره‌ی اول، بعد دوم، بعد سوم؛ و اولین جزیره‌ی فصلِ دیگر از اول باز است."""
# فصلِ دوم اشتراک می‌خواهد (content.views.unit_is_free)؛ این تست ترتیبِ باز شدن را می‌سنجد، نه پرداخت.
Subscription.objects.create(user_id=self.user.id, status='active', expires_at=None)
self.assertEqual(self.statuses(), {'یک': 'unlocked', 'دو': 'locked', 'سه': 'locked', 'الف': 'unlocked'})
for node in (self.n1, self.n2, self.n3, self.m1):
record_answers(self.user.id, node=node)
response = self.finish_node(node, correct=2)
self.assertEqual(response.status_code, 200, (node.title, response.content))
self.assertEqual(set(self.statuses().values()), {'completed'})
def test_replaying_a_finished_island_still_works(self):
record_answers(self.user.id, node=self.n1)
self.assertEqual(self.finish_node(self.n1, correct=2).status_code, 200)
self.assertEqual(self.finish_node(self.n1, correct=2).status_code, 200)
def test_an_idempotent_retry_of_a_finished_island_returns_the_first_result(self):
record_answers(self.user.id, node=self.n1)
kwargs = {'HTTP_IDEMPOTENCY_KEY': 'k-1'}
first = self.client.post(f'/api/nodes/{self.n1.id}/quiz/complete',
{'correctCount': 2, 'totalQuestions': 2}, format='json', **kwargs)
again = self.client.post(f'/api/nodes/{self.n1.id}/quiz/complete',
{'correctCount': 2, 'totalQuestions': 2}, format='json', **kwargs)
self.assertEqual((first.status_code, again.status_code), (200, 200))
self.assertEqual(first.json()['xpEarned'], again.json()['xpEarned'])
def test_unanswered_with_zero_hearts_still_gets_the_hearts_page_not_a_generic_error(self):
from datetime import timedelta
from django.utils import timezone
now = timezone.now()
UserHearts.objects.create(user_id=self.user.id, current_hearts=0,
next_refill_at=now + timedelta(hours=4), full_refill_at=now + timedelta(hours=20))
with override_settings(HEARTS_ENFORCED=True):
response = self.finish_node(self.n1)
self.assertEqual((response.status_code, response.json()['code']), (403, 'no_hearts'))
class NodeIsUnlockedMatchesHomeTests(_World, TestCase):
"""قاعده‌ی قفلِ سرور باید دقیقاً همانی باشد که نقشه (`/api/home`) نشان می‌دهد."""
def setUp(self):
self.build('09120007802')
def _assert_matches_home(self, label):
home = self.statuses()
for node in (self.n1, self.n2, self.n3, self.m1):
self.assertEqual(
node_is_unlocked(self.user.id, node), home[node.title] != 'locked',
f'{label}: {node.title} (نقشه: {home[node.title]})',
)
def test_matches_at_every_step(self):
self._assert_matches_home('شروع')
record_answers(self.user.id, node=self.n1)
self.assertEqual(self.finish_node(self.n1, correct=2).status_code, 200)
self._assert_matches_home('بعد از جزیره‌ی یک')
# تمام‌کردنِ درس‌های جزیره‌ی دو (از راهِ درس، نه آزمون)
for lesson in self.n2.lessons.all():
record_answers(self.user.id, lesson=lesson)
self.assertEqual(self.finish_lesson(lesson).status_code, 200)
self._assert_matches_home('بعد از درس‌های جزیره‌ی دو')
self.assertEqual(self.statuses()['سه'], 'unlocked')
def test_tester_phone_has_every_node_unlocked_on_map_and_server(self):
"""UNLOCK_ALL_PHONES: نقشه و قفلِ سرور هر دو باز؛ کاربرِ دیگر همان قفلِ عادی را دارد.
فقط قفلِ ترتیب را سنجیده؛ اشتراکِ فصل‌ها دست نمی‌خورد (تستش جای دیگر است)."""
from django.test import override_settings
self.assertEqual(self.statuses()['سه'], 'locked')
self.assertFalse(node_is_unlocked(self.user.id, self.n3))
with override_settings(UNLOCK_ALL_PHONES=frozenset({self.user.phone})):
self.assertNotIn('locked', self.statuses().values())
self.assertTrue(node_is_unlocked(self.user.id, self.n3))
with override_settings(UNLOCK_ALL_PHONES=frozenset({'09129999999'})):
self.assertEqual(self.statuses()['سه'], 'locked')
self.assertFalse(node_is_unlocked(self.user.id, self.n3))
def test_a_node_missing_from_the_tree_is_locked_and_logged(self):
ghost = Node(id=999999, unit=self.unit, title='شبح', order=9)
with self.assertLogs('content.views', level='WARNING'):
self.assertFalse(node_is_unlocked(self.user.id, ghost))