make every grade's id its class number and sort grades by it everywhere

Grade ids were the order the books were imported in (ninth=1, eighth=5), so
gradeId: 5 meant eighth grade and the admin, the question filter and the API
all listed grades out of order. Grade.number (1-12) is now read from the title,
new grades take it as their id, and migration 0008 moves existing ids plus every
reference to them: Unit, the loose grade_id in accounts/ai_tutor/rag, and the
grade target inside campaign JSON. Unnumbered demo grades move above 100 and the
id sequence is raised past 12 so they never collide with a real grade.

The frontend needs no change: it always reads gradeId from the server.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-23 22:47:02 +03:30
parent d70737dddf
commit 0c6b08c9ff
13 changed files with 292 additions and 12 deletions

View File

@@ -0,0 +1,82 @@
import importlib
from types import SimpleNamespace
from django.apps import apps
from django.db import connection
from django.test import TestCase
from accounts.models import User
from ai_tutor.models import Conversation
from campaigns.models import MessageCampaign
from content.models import Grade, Unit, grade_number_from_title
migration = importlib.import_module('content.migrations.0008_grade_id_equals_number')
class GradeNumberTests(TestCase):
def test_number_is_read_from_title(self):
self.assertEqual(grade_number_from_title('پایه هشتم'), 8)
self.assertEqual(grade_number_from_title('پایه دوازدهم'), 12)
self.assertEqual(grade_number_from_title('پایهٔ یازدهم'), 11)
self.assertIsNone(grade_number_from_title('[SEED] پایه سوم'))
self.assertIsNone(grade_number_from_title('ریاضی'))
def test_new_grade_takes_its_number_as_id(self):
# بدونِ این، «پایه چهارم»ی که بعداً اضافه شود id=13 می‌گرفت.
grade = Grade.objects.create(title='پایه چهارم', icon='g', color='#000')
self.assertEqual((grade.number, grade.pk), (4, 4))
def test_unnumbered_grade_stays_out_of_one_to_twelve(self):
grade = Grade.objects.create(title='[DEMO] نمونه', icon='g', color='#000')
self.assertIsNone(grade.number)
self.assertGreater(grade.pk, 12)
# و بعدش هنوز جا برای پایه‌ی واقعی هست
self.assertEqual(Grade.objects.create(title='پایه نهم', icon='g', color='#000').pk, 9)
def test_duplicate_title_does_not_break_save(self):
Grade.objects.create(title='پایه دهم', icon='g', color='#000')
second = Grade.objects.create(title='پایه دهم', icon='g', color='#000')
self.assertIsNone(second.number)
def test_grades_api_is_ordered_by_number(self):
for title in ('پایه نهم', 'پایه سوم', 'پایه دوازدهم', 'پایه هشتم'):
Grade.objects.create(title=title, icon='g', color='#000')
rows = self.client.get('/api/grades').json()['grades']
self.assertEqual([r['number'] for r in rows], [3, 8, 9, 12])
self.assertEqual([r['id'] for r in rows], [3, 8, 9, 12])
class GradeRenumberMigrationTests(TestCase):
"""همان وضعیتِ سرور پیش از migration: idها به ترتیبِ واردشدنِ کتاب‌ها."""
def _grade(self, pk, title):
# save() را دور می‌زنیم تا idِ قدیمی و بی‌شماره بسازیم، مثلِ دیتای واقعی
Grade.objects.bulk_create([Grade(pk=pk, title=title, icon='g', color='#000')])
return pk
def test_ids_become_numbers_and_every_reference_follows(self):
old = { # id قدیمی → عنوان (ترتیبِ سرورِ واقعی)
1: 'پایه نهم', 2: 'پایه ششم', 5: 'پایه هشتم', 8: 'پایه دوازدهم', 9: '[DEMO] نمونه',
}
for pk, title in old.items():
self._grade(pk, title)
Unit.objects.create(grade_id=pk, title=f'فصلِ {title}')
student = User.objects.create(grade_id=5) # هشتم
conversation = Conversation.objects.create(user_id=student.id, grade_id=8) # دوازدهم
campaign = MessageCampaign.objects.create(
title='t', body='b', channel='sms', target_filter={'type': 'grade', 'grade_id': 1}) # نهم
migration.forwards(apps, SimpleNamespace(connection=connection))
by_title = {g.title: g for g in Grade.objects.all()}
self.assertEqual({t: (g.pk, g.number) for t, g in by_title.items() if g.number}, {
'پایه نهم': (9, 9), 'پایه ششم': (6, 6), 'پایه هشتم': (8, 8), 'پایه دوازدهم': (12, 12),
})
self.assertGreater(by_title['[DEMO] نمونه'].pk, 12)
for grade in by_title.values():
self.assertEqual(list(grade.units.values_list('title', flat=True)), [f'فصلِ {grade.title}'])
student.refresh_from_db()
conversation.refresh_from_db()
campaign.refresh_from_db()
self.assertEqual((student.grade_id, conversation.grade_id), (8, 12))
self.assertEqual(campaign.target_filter, {'type': 'grade', 'grade_id': 9})