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:
33
content/migrations/0024_explicit_ordering.py
Normal file
33
content/migrations/0024_explicit_ordering.py
Normal 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']},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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
22
content/tests_ordering.py
Normal 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)
|
||||||
Reference in New Issue
Block a user