anchor a question to its lesson by page, not by a title that keeps changing

chunkها با تطبیقِ دقیقِ رشته به درس وصل می‌شدند (problem_type == lesson.title).
عنوانِ درس از یک استخراجِ قدیمی آمده بود و هر بازخوانیِ کتاب عنوانِ تازه می‌سازد،
پس بعد از بازخوانیِ امروز فقط ۵ تا ۸ درصدِ عنوان‌ها می‌خوردند — یعنی ۹۵٪ درس‌ها
بی‌هیچ خطایی هیچ نامزدی پیدا نمی‌کردند و ساختِ سوال صفر برمی‌گرداند.

لنگرِ پایدار صفحه است: هر Node دقیقاً یک بخشِ فهرستِ کتاب با بازه‌ی صفحه‌ی مشخص
است (تطبیق ۱۰۰٪: ۳۴/۳۴، ۲۸/۲۸، ۲۳/۲۳) و هر chunk هم source_page دارد. وقتی
عنوان نخورد، از روی بازه‌ی صفحه سراغ chunk می‌رویم — بدونِ هیچ فراخوانیِ مدلی.

نتیجه: هشتم ۰→۲۰۱، نهم ۰→۴۹، دهم ۰→۱۳۴.

رفتارِ قدیم با --strict-title حفظ شد، و یک محافظ گذاشته شد که یک chunk به دو
درس داده نشود.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-20 12:36:45 +03:30
parent 96bda905db
commit d15a28799a
2 changed files with 125 additions and 0 deletions

View File

@@ -270,3 +270,79 @@ class HealthTrafficTests(TestCase):
self.assertNotIn('09121234567', raw)
self.assertNotIn('علی', raw)
class QuestionGenerationPageBridgeTests(TestCase):
"""
پلِ بینِ chunkهای کتاب و درس‌های مسیرِ بازی.
تطبیق از روی عنوان شکننده است: عنوانِ درس از یک استخراجِ قدیمی می‌آید و هر
بازخوانیِ کتاب عنوانِ تازه می‌سازد. بعد از بازخوانیِ ۱۴۰۵/۰۶/۲۹ فقط ۵ تا ۸
درصدِ عنوان‌ها می‌خوردند و ۹۵٪ درس‌ها **بی‌هیچ خطایی** بدونِ نامزد می‌ماندند.
لنگرِ پایدار شماره‌ی صفحه است: هر Node یک بخشِ فهرستِ کتاب با بازه‌ی مشخص.
"""
def setUp(self):
from content.models import Grade, Lesson, Node, Unit
from rag.models import LessonChunk
from rag.services.textbooks import TEXTBOOKS
self.book = TEXTBOOKS['ریاضی نهم']
chapter = self.book.chapters[0]
section = chapter.resolved_sections()[0]
self.section = section
grade = Grade.objects.create(title='پایه نهم')
unit = Unit.objects.create(grade=grade, title=chapter.title, order=chapter.number)
node = Node.objects.create(unit=unit, title=section.title, order=1)
# عنوانِ درس عمداً با هیچ chunkی یکی نیست — همان حالتی که بعد از
# بازخوانی پیش آمد.
self.lesson = Lesson.objects.create(
node=node, title='عنوانی که دیگر وجود ندارد', intro_text='x')
self.chunk = LessonChunk.objects.create(
collection='ریاضی نهم', subject='ریاضی', lesson_number=1,
chapter=chapter.number, chapter_title=chapter.title, order=1,
title=chapter.title, original_text='x', summary='x',
problem_type='نوعِ تازه‌ی استخراج‌شده',
problem='۲ + ۲ چند است؟', solution='۴',
source_page=section.first_page + self.book.page_offset,
)
def _targets(self, **over):
from content.management.commands.generate_questions_from_chunks import Command
from content.models import Grade
opts = {'chapters': None, 'per_lesson': 1, 'limit': None,
'verified_only': False, 'by_page': True}
opts.update(over)
cmd = Command()
cmd._book = self.book
return cmd._targets(Grade.objects.get(title='پایه نهم'), 'ریاضی نهم', opts)
def test_a_chunk_in_the_nodes_page_range_is_used_even_if_the_title_differs(self):
targets = self._targets()
self.assertEqual([(l.pk, c.pk) for l, c in targets],
[(self.lesson.pk, self.chunk.pk)])
def test_strict_title_mode_finds_nothing(self):
# رفتارِ قدیم، نگه داشته شده تا بشود مقایسه کرد.
self.assertEqual(self._targets(by_page=False), [])
def test_a_chunk_outside_the_page_range_is_not_borrowed(self):
self.chunk.source_page = self.section.last_page + self.book.page_offset + 50
self.chunk.save(update_fields=['source_page'])
self.assertEqual(self._targets(), [])
def test_unverified_chunks_are_skipped_by_default(self):
# ساختنِ سوال از راه‌حلی که تأیید نشده، خطای کتاب را به دانش‌آموز می‌برد.
self.assertEqual(self._targets(verified_only=True), [])
def test_one_chunk_is_never_given_to_two_lessons(self):
from content.models import Lesson
second = Lesson.objects.create(
node=self.lesson.node, title='درسِ دوم', intro_text='x')
targets = self._targets()
self.assertEqual(len(targets), 1)
self.assertNotIn(second.pk, [l.pk for l, _ in targets])