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:
22
content/migrations/0025_lesson_order.py
Normal file
22
content/migrations/0025_lesson_order.py
Normal 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),
|
||||
),
|
||||
]
|
||||
@@ -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
|
||||
|
||||
@@ -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 صحیح است."
|
||||
]
|
||||
},
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user