Lesson.order from the book (seed sets it, also re-syncs unit/node order); matrix lesson rows as one multi-line step

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-15 13:53:30 +03:30
parent 3f68abe741
commit 8a2bf26b16
6 changed files with 65 additions and 11 deletions

View File

@@ -0,0 +1,22 @@
# Generated by Django 5.2.17 on 2026-10-15 10:16
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('content', '0024_explicit_ordering'),
]
operations = [
migrations.AlterModelOptions(
name='lesson',
options={'ordering': ['order', 'id']},
),
migrations.AddField(
model_name='lesson',
name='order',
field=models.PositiveIntegerField(default=10000),
),
]

View File

@@ -156,11 +156,15 @@ class Lesson(models.Model):
mascot_animation = models.CharField(max_length=50, default='explain')
# جایگاهِ درس در جزیره (از ۱). ۱۰۰۰۰ برای درسی که ادمین ساخته و در فایلِ کتاب نیست: ته‌ِ جزیره می‌نشیند.
order = models.PositiveIntegerField(default=10000)
class Meta:
# بدونِ ORDER BY، Postgres سطرها را به ترتیبِ فیزیکی برمی‌گرداند و هر UPDATE (ویرایشِ ادمین،
# seed_content) آن را جابه‌جا می‌کند: درسِ اول یک جزیره عوض می‌شد (گزارشِ مالک ۲۰۲۶-۱۰-۱۵).
# seed درس‌ها را به ترتیبِ فایل می‌سازد، پس ترتیبِ id همان ترتیبِ کتاب است.
ordering = ['id']
# `order` جایگاهِ درس در کتاب است و seed_content آن را از فایل می‌گذارد؛ درسی که بعداً در وسطِ
# جزیره اضافه شده id بزرگ‌تر دارد ولی جایش وسط است. `id` فقط برای برابری است.
ordering = ['order', 'id']
def __str__(self):
return self.title

View File

@@ -173252,10 +173252,7 @@
"example": "ماتریس B به ابعاد m×n با درایه‌های bᵢⱼ که i=1 تا m و j=1 تا n تغییر می‌کند، نمایش داده شده است. ماتریس B را به صورت درایه‌ای نمایش دهید.",
"steps": [
"ماتریس B به صورت زیر نمایش داده می‌شود:",
"B(m×n) = b₁₁ b₁₂ ⋯ b(1j) ⋯ b(1n)",
"b₂₁ b₂₂ ⋯ b(2j) ⋯ b(2n)",
"⋮ ⋮ ⋱ ⋮ ⋱ ⋮",
"b(i1) b(i2) ⋯ b(ij) ⋯ b(in)",
"B(m×n) = b₁₁ b₁₂ ⋯ b(1j) ⋯ b(1n)\nb₂₁ b₂₂ ⋯ b(2j) ⋯ b(2n)\n⋮ ⋮ ⋱ ⋮ ⋱ ⋮\nb(i1) b(i2) ⋯ b(ij) ⋯ b(in)",
"✅ جواب: نمایش درایه‌ای ماتریس B با b(ij) برای سطر i و ستون j صحیح است."
]
},

View File

@@ -71,7 +71,7 @@ class _Ledger:
def sync(grades: list[dict], question_rows: list[dict]) -> dict:
"""grades: خروجیِ curriculum_io.parse؛ question_rows: ردیف‌های مسیردارِ questions.json."""
ledger = _Ledger(active=Grade.objects.exists())
counts = {'grades': 0, 'units': 0, 'nodes': 0, 'lessons': 0, 'updated': 0,
counts = {'grades': 0, 'units': 0, 'nodes': 0, 'lessons': 0, 'updated': 0, 'reordered': 0,
'kept_admin_text': 0, 'retired': 0, 'created': 0, 'skipped': 0}
for g in grades:
@@ -98,6 +98,11 @@ def sync(grades: list[dict], question_rows: list[dict]) -> dict:
)
counts['units'] += 1
ledger.record(_key('unit', *upath), 'unit')
if unit.order != u['order']:
# ترتیبِ فصل‌ها ترتیبِ کتاب است، نه سلیقه‌ی ادمین؛ دوباره از فایل گذاشته می‌شود.
unit.order = u['order']
unit.save(update_fields=['order'])
counts['reordered'] += 1
if u.get('blurb') and not unit.blurb:
unit.blurb = u['blurb']
unit.save(update_fields=['blurb'])
@@ -112,16 +117,20 @@ def sync(grades: list[dict], question_rows: list[dict]) -> dict:
node = Node.objects.create(unit=unit, title=n['title'], order=n['order'])
counts['nodes'] += 1
ledger.record(_key('node', *npath), 'node')
if node.order != n['order']:
node.order = n['order']
node.save(update_fields=['order'])
counts['reordered'] += 1
for row in n['lessons']:
_sync_lesson(node, npath + (row['title'],), row, ledger, counts)
for position, row in enumerate(n['lessons'], start=1):
_sync_lesson(node, npath + (row['title'],), row, ledger, counts, position)
_sync_questions(question_rows, ledger, counts)
ledger.save()
return counts
def _sync_lesson(node, path, row, ledger, counts):
def _sync_lesson(node, path, row, ledger, counts, position):
intro_key = _key('intro', *path)
lesson = Lesson.objects.filter(node=node, title=row['title']).first()
if lesson is None:
@@ -131,6 +140,7 @@ def _sync_lesson(node, path, row, ledger, counts):
lesson = Lesson.objects.create(
node=node, title=row['title'], intro_text=row['intro_text'],
example=row['example'], steps=row['steps'], figures=row.get('figures') or [],
order=position,
)
counts['lessons'] += 1
ledger.record(_key('lesson', *path), 'lesson')
@@ -139,6 +149,10 @@ def _sync_lesson(node, path, row, ledger, counts):
ledger.record(_key('lesson', *path), 'lesson')
changed = []
if lesson.order != position:
lesson.order = position
changed.append('order')
counts['reordered'] += 1
file_text, db_text = row['intro_text'], lesson.intro_text
last_seeded = ledger.value(intro_key)
if file_text and db_text != file_text:

View File

@@ -11,7 +11,7 @@ class StableOrderTests(TestCase):
def test_content_querysets_have_explicit_order(self):
for qs in (Lesson.objects.all(), Question.objects.all(), Choice.objects.all()):
self.assertIn('ORDER BY', str(qs.query), qs.model.__name__)
self.assertEqual(qs.query.order_by or qs.model._meta.ordering, ['id'])
self.assertEqual(qs.model._meta.ordering[-1], 'id')
def test_lessons_come_back_in_creation_order_after_updates(self):
grade = Grade.objects.create(title='پایه', number=99)

View File

@@ -76,6 +76,23 @@ class SeedContentTests(TestCase):
self.assertEqual(question.choices.count(), 3)
self.assertEqual(question.choices.get(is_correct=True).value, '۴')
def test_book_order_wins_over_creation_order(self):
"""درسی که بعداً در وسطِ جزیره اضافه می‌شود id بزرگ‌تر دارد؛ ترتیبِ نمایش باید ترتیبِ کتاب باشد،
و ترتیبِ جابه‌جاشده‌ی فصل و جزیره هم با اجرای بعدیِ seed به کتاب برمی‌گردد."""
call_command('seed_content')
lessons = CURRICULUM['grades'][0]['units'][0]['nodes'][0]['lessons']
lessons.insert(1, {'title': 'درسِ تازه در وسط', 'introText': 'x'})
Unit.objects.update(order=99)
Node.objects.update(order=99)
self._write(CURRICULUM, QUESTIONS)
call_command('seed_content')
node = Node.objects.get()
self.assertEqual([l.title for l in node.lessons.all()],
['ضرب توان‌ها', 'درسِ تازه در وسط', 'تقسیم توان‌ها'])
self.assertEqual((Unit.objects.get().order, node.order), (4, 1))
lessons.pop(1) # فایلِ مشترکِ تست را برای تست‌های بعدی برمی‌گرداند
def test_second_run_changes_nothing(self):
"""دیپلویِ دوم نباید چیزی را تکراری کند — این کلِ ایمنیِ خودکار بودنش است."""
call_command('seed_content')