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:
105
content/migrations/0008_grade_id_equals_number.py
Normal file
105
content/migrations/0008_grade_id_equals_number.py
Normal file
@@ -0,0 +1,105 @@
|
||||
"""
|
||||
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),
|
||||
]
|
||||
Reference in New Issue
Block a user