diagrams for every grade, sectioned prompt, repair of real model slips; fill lesson texts and thin islands

Diagrams: 14 kinds drawn by the app (plane with constructed points, graphs with
computed roots/extrema/tangents, number lines with intervals, solids, charts,
tables, Venn, mapping, trees, fractions, grids, groups, clock). The catalog is
split by topic and only the matching sections go next to a question (200-1073
tokens instead of 1854). parse() repairs slips seen on 96 real book problems:
Persian digits in JSON numbers, 2*pi, // comments, bare items, Venn with
Persian set names. Measured: 45/48 valid on the second round, 0/8 on plain
arithmetic; visual review of 83: 64 right, 3 weak, 16 wrong.

Content: 82 new questions so no island has fewer than four; fill_lesson_texts
writes Unit.blurb (58) and Lesson.example/steps (590) from verified book
problems, never the lesson's own quiz problem. seed_content only fills empty
fields, so admin edits survive a deploy. Tedi unlocks at 500 xp.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-28 23:22:13 +03:30
parent 33d8f43d74
commit 0917ab487f
10 changed files with 7434 additions and 745 deletions

View File

@@ -0,0 +1,93 @@
"""متن‌های درس و فصل: پر شدن از کتاب، و اینکه دیپلوی ویرایشِ ادمین را بازنویسی نکند."""
import json
from django.core.management import call_command
from django.test import TestCase
from content.curriculum_io import export_curriculum, import_curriculum, parse
from content.management.commands.fill_lesson_texts import related, split_steps, unit_blurb
from content.models import Choice, Grade, Lesson, Node, Question, Unit
from rag.models import LessonChunk
def _curriculum(blurb='', example='', steps=None):
return json.dumps({'grades': [{'title': 'پایه آزمون', 'units': [{
'title': 'فصل ۱', 'blurb': blurb, 'nodes': [{'title': 'جزیره', 'lessons': [
{'title': 'درس', 'introText': 'توضیح', 'example': example, 'steps': steps or []},
]}],
}]}]}).encode()
class SeedKeepsAdminEditsTests(TestCase):
def test_empty_fields_are_filled_from_the_file(self):
import_curriculum(parse(_curriculum()))
import_curriculum(parse(_curriculum('در این فصل: جزیره.', 'مثال', ['قدم ۱'])))
lesson = Lesson.objects.get(title='درس')
self.assertEqual((lesson.example, lesson.steps), ('مثال', ['قدم ۱']))
self.assertEqual(Unit.objects.get(title='فصل ۱').blurb, 'در این فصل: جزیره.')
def test_text_written_in_admin_survives_the_next_deploy(self):
import_curriculum(parse(_curriculum()))
Lesson.objects.filter(title='درس').update(example='نوشته‌ی ادمین', steps=['قدمِ ادمین'])
Unit.objects.filter(title='فصل ۱').update(blurb='توضیحِ ادمین')
import_curriculum(parse(_curriculum('از فایل', 'از فایل', ['از فایل'])))
lesson = Lesson.objects.get(title='درس')
self.assertEqual((lesson.example, lesson.steps), ('نوشته‌ی ادمین', ['قدمِ ادمین']))
self.assertEqual(Unit.objects.get(title='فصل ۱').blurb, 'توضیحِ ادمین')
def test_export_round_trips(self):
import_curriculum(parse(_curriculum('توضیح', 'مثال', ['الف', 'ب'])))
out = export_curriculum(Grade.objects.filter(title='پایه آزمون'))
unit = out['grades'][0]['units'][0]
self.assertEqual(unit['blurb'], 'توضیح')
self.assertEqual(unit['nodes'][0]['lessons'][0]['steps'], ['الف', 'ب'])
class HelpersTests(TestCase):
def test_blurb_lists_islands_and_stays_short(self):
grade = Grade.objects.create(title='پایه تست')
unit = Unit.objects.create(grade=grade, title='فصل', order=1)
for i, t in enumerate(['عددهای صحیح', 'عددهای گویا', 'جمع و تفریق']):
Node.objects.create(unit=unit, title=t, order=i)
self.assertEqual(unit_blurb(unit), 'در این فصل: عددهای صحیح، عددهای گویا و جمع و تفریق.')
for i in range(20):
Node.objects.create(unit=unit, title=f'جزیره‌ی بسیار طولانیِ شماره‌ی {i}', order=10 + i)
self.assertLessEqual(len(unit_blurb(unit)), 120)
def test_steps_drop_their_own_numbering(self):
self.assertEqual(split_steps('گام 1: ضرایب\nگام 2: توان‌ها\n3) نتیجه'), ['ضرایب', 'توان‌ها', 'نتیجه'])
def test_relevance_needs_two_real_words(self):
chunk = LessonChunk(problem_type='معادله‌ی خط', problem='خط y=2x+3 را رسم کن و شیب خط را بگو')
self.assertTrue(related('رسم خط با معادله و شیب', chunk))
self.assertFalse(related('محاسبه تعداد مثلث‌های ممکن با شرایط داده شده', chunk))
class FillCommandTests(TestCase):
"""مثالِ درس از دومین مسئله‌ی تأییدشده، هرگز همان مسئله‌ی آزمون."""
def setUp(self):
grade = Grade.objects.create(title='پایه هشتم', number=8)
unit = Unit.objects.create(grade=grade, title='فصل ۱', order=1)
node = Node.objects.create(unit=unit, title='جزیره', order=1)
self.lesson = Lesson.objects.create(node=node, title='جمع کسرها', intro_text='x')
q = Question.objects.create(lesson=self.lesson, type='multiple_choice', prompt='۱/۲ + ۱/۴ = ?')
Choice.objects.create(question=q, value='3/4', is_correct=True)
common = {'collection': 'ریاضی هشتم', 'problem_type': 'جمع کسرها', 'solution_verdict': 'agrees',
'grade_title': 'هشتم', 'lesson_number': 1, 'chapter': 1, 'subject': 'ریاضی'}
LessonChunk.objects.create(order=1, problem='اولی: ۱/۲ + ۱/۴', solution='مخرج مشترک ۴\nجواب ۳/۴', checked_answer='3/4', **common)
LessonChunk.objects.create(order=2, problem='دومی با همان جواب', solution='...\n۳/۴', checked_answer='3 / 4', **common)
LessonChunk.objects.create(order=3, problem='سومی: ۱/۳ + ۱/۶', solution='مخرج مشترک ۶\n۲/۶ + ۱/۶ = ۳/۶', checked_answer='1/2', **common)
def test_example_skips_the_quiz_problem_and_its_answer(self):
call_command('fill_lesson_texts', verbosity=0)
self.lesson.refresh_from_db()
self.assertTrue(self.lesson.example.startswith('سومی'))
self.assertEqual(self.lesson.steps[-1], '✅ جواب: 1/2')
def test_second_run_changes_nothing(self):
call_command('fill_lesson_texts', verbosity=0)
Lesson.objects.filter(pk=self.lesson.pk).update(example='ادمین')
call_command('fill_lesson_texts', verbosity=0)
self.lesson.refresh_from_db()
self.assertEqual(self.lesson.example, 'ادمین')