""" یک پایه چند کتاب دارد (دوازدهم: حسابان، هندسه، گسسته) و دانش‌آموز بینشان جابه‌جا می‌شود (۲۰۲۶-۱۰-۱۱). `/api/home?subject=` نقشه‌ی همان کتاب را می‌دهد؛ کتابِ ناموجود در پایه ۴۰۰ است؛ فصلِ اولِ هر کتاب رایگان است؛ و دستیار کتابِ درس را از خودِ فصل می‌گیرد. """ from django.core.cache import cache from django.test import TestCase from rest_framework.test import APIClient from accounts.authentication import issue_access_token from accounts.models import User from .models import Grade, Lesson, Node, Subject, Unit class SubjectHomeTests(TestCase): def setUp(self): cache.clear() self.grade = Grade.objects.create(title='پایه دوازدهم', icon='', color='') self.calc = Subject.objects.get(key='calculus') self.geo = Subject.objects.get(key='geometry') self.units = {} for subject, titles in ((self.calc, ['فصل ۱: تابع', 'فصل ۲: مثلثات']), (self.geo, ['فصل ۱: ماتریس', 'فصل ۲: مقاطع مخروطی'])): for order, title in enumerate(titles, start=1): unit = Unit.objects.create(grade=self.grade, subject=subject, title=title, order=order) node = Node.objects.create(unit=unit, title=f'جزیره {title}', order=1) Lesson.objects.create(node=node, title='درس') self.units[(subject.key, order)] = unit self.user = User.objects.create(phone='09120007911', grade_id=self.grade.id) self.client = APIClient() self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {issue_access_token(self.user.id)}') def test_default_is_the_first_subject_of_the_grade(self): body = self.client.get('/api/home').json() self.assertEqual(body['subject']['key'], 'calculus') self.assertEqual([s['key'] for s in body['subjects']], ['calculus', 'geometry']) self.assertEqual([u['title'] for u in body['units']], ['فصل ۱: تابع', 'فصل ۲: مثلثات']) def test_subject_param_switches_the_map(self): body = self.client.get('/api/home?subject=geometry').json() self.assertEqual(body['subject']['key'], 'geometry') self.assertEqual([u['title'] for u in body['units']], ['فصل ۱: ماتریس', 'فصل ۲: مقاطع مخروطی']) def test_a_subject_the_grade_does_not_have_is_a_400_not_a_silent_default(self): self.assertEqual(self.client.get('/api/home?subject=discrete').status_code, 400) self.assertEqual(self.client.get('/api/home?subject=nope').status_code, 400) def test_the_first_unit_of_each_subject_is_free(self): for key in ('calculus', 'geometry'): body = self.client.get(f'/api/home?subject={key}').json() self.assertEqual([u['requiresSubscription'] for u in body['units']], [False, True], key) def test_second_unit_of_other_subject_is_still_locked_by_the_paywall(self): node = Node.objects.get(unit=self.units[('geometry', 2)]) lesson = node.lessons.first() self.assertEqual(self.client.get(f'/api/lessons/{lesson.id}/questions').status_code, 402) def test_ordering_follows_subject_order_not_creation_order(self): # هندسه زودتر ساخته شود؛ نقشه باز هم حسابان را اول می‌دهد (Subject.order). cache.clear() Subject.objects.filter(pk=self.geo.pk).update(order=1) Subject.objects.filter(pk=self.calc.pk).update(order=9) cache.clear() self.assertEqual(self.client.get('/api/home').json()['subject']['key'], 'geometry') class LessonContextUsesTheUnitSubjectTests(TestCase): def test_a_geometry_lesson_resolves_to_the_geometry_book(self): from rag.services.lesson_context import resolve_from_lesson grade = Grade.objects.create(title='پایه دهم', icon='', color='') geo = Subject.objects.get(key='geometry') unit = Unit.objects.create(grade=grade, subject=geo, title='فصل ۲: قضیه‌ی تالس، تشابه و کاربردهای آن') lesson = Lesson.objects.create(node=Node.objects.create(unit=unit, title='n'), title='l') self.assertEqual( resolve_from_lesson(lesson.id), {'subject': 'هندسه', 'gradeTitle': 'دهم', 'chapter': 2}, ) def test_same_title_under_the_wrong_subject_does_not_match(self): from rag.services.lesson_context import resolve_from_lesson grade = Grade.objects.create(title='پایه دهم', icon='', color='') math = Subject.objects.get(key='math') # عنوانِ فصلِ هندسه، ولی زیرِ کتابِ ریاضی: نباید به collectionِ هندسه برود. unit = Unit.objects.create(grade=grade, subject=math, title='فصل ۲: قضیه‌ی تالس، تشابه و کاربردهای آن') lesson = Lesson.objects.create(node=Node.objects.create(unit=unit, title='n'), title='l') self.assertEqual(resolve_from_lesson(lesson.id), {'gradeTitle': 'دهم'}) class CurriculumFileNeedsASubjectTests(TestCase): def test_a_unit_without_subject_is_rejected(self): import json from .curriculum_io import CurriculumError, parse raw = json.dumps({'grades': [{'title': 'پایه ۱', 'units': [{'title': 'فصل ۱', 'nodes': []}]}]}) with self.assertRaises(CurriculumError): parse(raw.encode()) def test_an_unknown_subject_is_rejected_on_import(self): from .curriculum_io import CurriculumError, import_curriculum grades = [{'title': 'پایه ۱', 'icon': '', 'color': '', 'units': [ {'subject': 'فیزیک', 'title': 'فصل ۱', 'blurb': '', 'order': 1, 'nodes': []}]}] with self.assertRaises(CurriculumError): import_curriculum(grades) class NodeSplitTests(TestCase): """`content.node_split`: جزیره‌ی بلند → چند جزیره‌ی حداکثر ۶ درسی، بی‌آن‌که پیشرفتِ کسی گم یا دوبار شمرده شود.""" def setUp(self): from .models import NodeProgress, UserNodeCompletion self.grade = Grade.objects.create(title='پایه دوازدهم', icon='', color='') self.subject = Subject.objects.get(key='calculus') self.unit = Unit.objects.create(grade=self.grade, subject=self.subject, title='فصل ۱: تابع', order=1) self.long = Node.objects.create(unit=self.unit, title='تبدیل نمودار', order=1) self.short = Node.objects.create(unit=self.unit, title='کوتاه', order=2) self.lesson_ids = [Lesson.objects.create(node=self.long, title=f'درس {i}').id for i in range(25)] Lesson.objects.create(node=self.short, title='تک') UserNodeCompletion.objects.create(user_id=7, node=self.long, correct_count=3, total_questions=4, stars=2, xp_earned=50, coins_earned=10) NodeProgress.objects.create(user_id=7, node=self.long, status='completed') def _run(self, **kw): from .models import NodeProgress, UserNodeCompletion from .node_split import split_long_nodes return split_long_nodes(Node=Node, Lesson=Lesson, UserNodeCompletion=UserNodeCompletion, NodeProgress=NodeProgress, units=Unit.objects.all(), **kw) def test_splits_into_balanced_ordered_parts_and_keeps_lesson_ids(self): counts = self._run() self.assertEqual((counts['nodes_split'], counts['nodes_created']), (1, 4)) nodes = list(Node.objects.filter(unit=self.unit).order_by('order')) self.assertEqual([n.title for n in nodes], ['تبدیل نمودار (۱)', 'تبدیل نمودار (۲)', 'تبدیل نمودار (۳)', 'تبدیل نمودار (۴)', 'تبدیل نمودار (۵)', 'کوتاه']) self.assertEqual([n.order for n in nodes], [1, 2, 3, 4, 5, 6]) self.assertEqual(nodes[0].pk, self.long.pk) # بخشِ اول همان شناسه را نگه می‌دارد sizes = [Lesson.objects.filter(node=n).count() for n in nodes] self.assertEqual(sizes, [5, 5, 5, 5, 5, 1]) # ترتیبِ کتاب حفظ شده: درس‌های هر بخش پشتِ‌هم‌اند. first = list(Lesson.objects.filter(node=nodes[0]).order_by('id').values_list('id', flat=True)) self.assertEqual(first, self.lesson_ids[:5]) def test_a_finished_island_stays_finished_without_double_counting_xp(self): from .models import NodeProgress, UserNodeCompletion self._run() rows = UserNodeCompletion.objects.filter(user_id=7).order_by('node__order') self.assertEqual(rows.count(), 5) self.assertEqual(sum(r.xp_earned for r in rows), 50) self.assertEqual(sum(r.coins_earned for r in rows), 10) self.assertEqual(NodeProgress.objects.filter(user_id=7, status='completed').count(), 5) def test_second_run_changes_nothing(self): self._run() again = self._run() self.assertEqual(again, {'nodes_split': 0, 'nodes_created': 0, 'lessons_moved': 0, 'completions_cloned': 0}) self.assertEqual(Node.objects.filter(unit=self.unit).count(), 6) def test_dry_run_writes_nothing(self): counts = self._run(dry_run=True) self.assertEqual(counts['nodes_created'], 4) self.assertEqual(Node.objects.filter(unit=self.unit).count(), 2)