Files
MathApp/content/migrations/0008_grade_id_equals_number.py
Zareian 0c6b08c9ff 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>
2026-09-23 22:47:02 +03:30

106 lines
4.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
id هر پایه را برابرِ شماره‌اش می‌کند (پایه هشتم → 8) و همه‌ی ارجاع‌ها را همراهش.
تا پیش از این id ترتیبِ واردشدنِ کتاب‌ها بود (نهم=۱، ششم=۲، …، هشتم=۵)، پس
`gradeId: 5` یعنی هشتم و همه‌جا — ادمین، فیلترِ سوال‌ها، API — پایه‌ها نامرتب
بودند. شماره‌ی پایه از عنوان خوانده می‌شود، نه از id، پس روی هر دیتابیسی (با
هر ترتیبِ idِ قبلی) درست کار می‌کند.
ارجاع‌ها: FKِ `Unit.grade`، سه ارجاعِ سبکِ `grade_id` در accounts، ai_tutor و
rag، و `grade_id` داخلِ JSONِ `target_filter` کمپین‌های پیامکی. پایه‌ای که عنوانش شماره ندارد ([DEMO]/[SEED]) اگر idش در بازه‌ی ۱ تا ۱۲
باشد به بالای ۱۰۰ می‌رود تا جا برای پایه‌های واقعی باز شود.
برگشت‌ناپذیر به‌معنای «idهای قدیمی برنمی‌گردند»؛ برگشتِ migration فقط فیلد را
برمی‌دارد و idهای تازه می‌مانند، که برای همه‌ی کدها بی‌تفاوت است.
"""
from django.db import migrations
from content.models import grade_number_from_title
LOOSE_REFS = [('accounts', 'User'), ('ai_tutor', 'Conversation'), ('rag', 'LessonChunk')]
TEMP_OFFSET = 1_000_000
FIRST_UNNUMBERED_ID = 101
def forwards(apps, schema_editor):
Grade = apps.get_model('content', 'Grade')
Unit = apps.get_model('content', 'Unit')
refs = [apps.get_model(app, model) for app, model in LOOSE_REFS]
grades = list(Grade.objects.order_by('id'))
taken = set()
for grade in grades:
number = grade_number_from_title(grade.title)
if number and number not in taken:
taken.add(number)
Grade.objects.filter(pk=grade.pk).update(number=number)
grade.number = number
next_free = max([g.id for g in grades] + [FIRST_UNNUMBERED_ID - 1]) + 1
moves = {}
for grade in grades:
if grade.number:
target = grade.number
elif grade.id <= 12:
target, next_free = next_free, next_free + 1
else:
continue
if target != grade.id:
moves[grade.id] = target
def move(old, new):
Grade.objects.filter(id=old).update(id=new)
Unit.objects.filter(grade_id=old).update(grade_id=new)
for model in refs:
model.objects.filter(grade_id=old).update(grade_id=new)
# دو مرحله، چون مقصدِ یکی ممکن است idِ فعلیِ دیگری باشد (هشتم ۵→۸ درحالی‌که
# دوازدهم هنوز ۸ است).
for old in moves:
move(old, old + TEMP_OFFSET)
for old, new in moves.items():
move(old + TEMP_OFFSET, new)
# کمپینِ «همه‌ی کاربرانِ پایه‌ی X» idِ پایه را در JSON نگه می‌دارد
Campaign = apps.get_model('campaigns', 'MessageCampaign')
for campaign in Campaign.objects.all():
target = campaign.target_filter or {}
if target.get('type') == 'grade' and target.get('grade_id') in moves:
target['grade_id'] = moves[target['grade_id']]
Campaign.objects.filter(pk=campaign.pk).update(target_filter=target)
# پایه‌ی بی‌شماره‌ای که بعداً ساخته شود idِ خودکار می‌گیرد؛ شمارنده باید از
# بازه‌ی ۱ تا ۱۲ بالاتر باشد وگرنه با «پایه نهم» (id=9) برخورد می‌کند.
table = Grade._meta.db_table
floor = max([next_free - 1] + list(moves.values()) + [FIRST_UNNUMBERED_ID - 1])
with schema_editor.connection.cursor() as cursor:
vendor = schema_editor.connection.vendor
if vendor == 'sqlite':
cursor.execute('UPDATE sqlite_sequence SET seq = MAX(seq, %s) WHERE name = %s', [floor, table])
cursor.execute(
'INSERT INTO sqlite_sequence (name, seq) SELECT %s, %s '
'WHERE NOT EXISTS (SELECT 1 FROM sqlite_sequence WHERE name = %s)',
[table, floor, table],
)
elif vendor == 'postgresql':
cursor.execute(
f"SELECT setval(pg_get_serial_sequence('{table}', 'id'), "
f"GREATEST(%s, (SELECT COALESCE(MAX(id), 1) FROM {table})))",
[floor],
)
class Migration(migrations.Migration):
dependencies = [
('content', '0007_grade_number'),
('accounts', '0003_user_last_activity_at'),
('ai_tutor', '0005_message_chunk_count_message_completion_tokens_and_more'),
('rag', '0006_remove_lessonchunk_solution_agrees_and_more'),
('campaigns', '0003_campaigndelivery_one_active_per_user_channel'),
]
operations = [
migrations.RunPython(forwards, migrations.RunPython.noop),
]