From 3f68abe7419b8c7703a9bca8485e47d68efe7344 Mon Sep 17 00:00:00 2001 From: Zareian Date: Thu, 15 Oct 2026 13:39:42 +0330 Subject: [PATCH] Explicit ordering for Lesson, Question, Choice (+id tie-break on Unit/Node): lessons no longer reshuffle after row updates Co-Authored-By: Claude Sonnet 5.5 --- content/migrations/0024_explicit_ordering.py | 33 ++++++++++++++++++++ content/models.py | 16 ++++++++-- content/tests_ordering.py | 22 +++++++++++++ 3 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 content/migrations/0024_explicit_ordering.py create mode 100644 content/tests_ordering.py diff --git a/content/migrations/0024_explicit_ordering.py b/content/migrations/0024_explicit_ordering.py new file mode 100644 index 0000000..0b4b8fa --- /dev/null +++ b/content/migrations/0024_explicit_ordering.py @@ -0,0 +1,33 @@ +# Generated by Django 5.2.17 on 2026-10-15 10:07 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('content', '0023_lesson_report_content_vote'), + ] + + operations = [ + migrations.AlterModelOptions( + name='choice', + options={'ordering': ['id']}, + ), + migrations.AlterModelOptions( + name='lesson', + options={'ordering': ['id']}, + ), + migrations.AlterModelOptions( + name='node', + options={'ordering': ['order', 'id']}, + ), + migrations.AlterModelOptions( + name='question', + options={'ordering': ['id']}, + ), + migrations.AlterModelOptions( + name='unit', + options={'ordering': ['order', 'id']}, + ), + ] diff --git a/content/models.py b/content/models.py index fa19781..3824fcb 100644 --- a/content/models.py +++ b/content/models.py @@ -101,7 +101,7 @@ class Unit(models.Model): order = models.PositiveIntegerField(default=0) class Meta: - ordering = ['order'] + ordering = ['order', 'id'] def __str__(self): return self.title @@ -117,7 +117,7 @@ class Node(models.Model): order = models.PositiveIntegerField(default=0) class Meta: - ordering = ['order'] + ordering = ['order', 'id'] def __str__(self): return self.title @@ -156,6 +156,12 @@ class Lesson(models.Model): mascot_animation = models.CharField(max_length=50, default='explain') + class Meta: + # بدونِ ORDER BY، Postgres سطرها را به ترتیبِ فیزیکی برمی‌گرداند و هر UPDATE (ویرایشِ ادمین، + # seed_content) آن را جابه‌جا می‌کند: درسِ اول یک جزیره عوض می‌شد (گزارشِ مالک ۲۰۲۶-۱۰-۱۵). + # seed درس‌ها را به ترتیبِ فایل می‌سازد، پس ترتیبِ id همان ترتیبِ کتاب است. + ordering = ['id'] + def __str__(self): return self.title @@ -171,6 +177,9 @@ class Question(models.Model): # null یعنی سوال شکل ندارد. هنگامِ نمایش هیچ فراخوانیِ مدل نیست؛ فرانت فقط می‌کشد. figure = models.JSONField(null=True, blank=True) + class Meta: + ordering = ['id'] + def __str__(self): return self.prompt[:50] @@ -181,6 +190,9 @@ class Choice(models.Model): value = models.CharField(max_length=300) is_correct = models.BooleanField(default=False) + class Meta: + ordering = ['id'] + def __str__(self): return self.value diff --git a/content/tests_ordering.py b/content/tests_ordering.py new file mode 100644 index 0000000..32b76eb --- /dev/null +++ b/content/tests_ordering.py @@ -0,0 +1,22 @@ +from django.test import TestCase + +from content.models import Choice, Grade, Lesson, Node, Question, Unit + + +class StableOrderTests(TestCase): + """گزارشِ مالک ۲۰۲۶-۱۰-۱۵: درسِ اولِ جزیره بعد از دیپلوی عوض شد، چون Lesson ترتیبِ صریح نداشت + و Postgres سطرهای ویرایش‌شده را جابه‌جا برمی‌گرداند. SQLite سطرها را به ترتیبِ id برمی‌گرداند و + این باگ را نشان نمی‌دهد؛ پس خودِ ORDER BY را بررسی می‌کنیم.""" + + 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']) + + def test_lessons_come_back_in_creation_order_after_updates(self): + grade = Grade.objects.create(title='پایه', number=99) + unit = Unit.objects.create(grade=grade, title='فصل', order=1) + node = Node.objects.create(unit=unit, title='جزیره', order=1) + ids = [Lesson.objects.create(node=node, title=f'درس {i}').id for i in range(5)] + Lesson.objects.filter(pk=ids[0]).update(title='ویرایش‌شده') + self.assertEqual([l.id for l in node.lessons.all()], ids)