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 <noreply@anthropic.com>
This commit is contained in:
2026-10-15 13:39:42 +03:30
parent 7973bf4100
commit 3f68abe741
3 changed files with 69 additions and 2 deletions

View File

@@ -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']},
),
]

View File

@@ -101,7 +101,7 @@ class Unit(models.Model):
order = models.PositiveIntegerField(default=0) order = models.PositiveIntegerField(default=0)
class Meta: class Meta:
ordering = ['order'] ordering = ['order', 'id']
def __str__(self): def __str__(self):
return self.title return self.title
@@ -117,7 +117,7 @@ class Node(models.Model):
order = models.PositiveIntegerField(default=0) order = models.PositiveIntegerField(default=0)
class Meta: class Meta:
ordering = ['order'] ordering = ['order', 'id']
def __str__(self): def __str__(self):
return self.title return self.title
@@ -156,6 +156,12 @@ class Lesson(models.Model):
mascot_animation = models.CharField(max_length=50, default='explain') mascot_animation = models.CharField(max_length=50, default='explain')
class Meta:
# بدونِ ORDER BY، Postgres سطرها را به ترتیبِ فیزیکی برمی‌گرداند و هر UPDATE (ویرایشِ ادمین،
# seed_content) آن را جابه‌جا می‌کند: درسِ اول یک جزیره عوض می‌شد (گزارشِ مالک ۲۰۲۶-۱۰-۱۵).
# seed درس‌ها را به ترتیبِ فایل می‌سازد، پس ترتیبِ id همان ترتیبِ کتاب است.
ordering = ['id']
def __str__(self): def __str__(self):
return self.title return self.title
@@ -171,6 +177,9 @@ class Question(models.Model):
# null یعنی سوال شکل ندارد. هنگامِ نمایش هیچ فراخوانیِ مدل نیست؛ فرانت فقط می‌کشد. # null یعنی سوال شکل ندارد. هنگامِ نمایش هیچ فراخوانیِ مدل نیست؛ فرانت فقط می‌کشد.
figure = models.JSONField(null=True, blank=True) figure = models.JSONField(null=True, blank=True)
class Meta:
ordering = ['id']
def __str__(self): def __str__(self):
return self.prompt[:50] return self.prompt[:50]
@@ -181,6 +190,9 @@ class Choice(models.Model):
value = models.CharField(max_length=300) value = models.CharField(max_length=300)
is_correct = models.BooleanField(default=False) is_correct = models.BooleanField(default=False)
class Meta:
ordering = ['id']
def __str__(self): def __str__(self):
return self.value return self.value

22
content/tests_ordering.py Normal file
View File

@@ -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)