""" 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), ]