""" LaTeX و نشانه‌گذاریِ خام (`^(−۴)`، `**…**`، `\\vec`) را از همه‌ی متن‌هایی که دانش‌آموز می‌بیند پاک می‌کند: سوال‌ها و گزینه‌ها، متنِ درس‌ها و گفتگوهای دستیار. فایل‌های seed همین تبدیل را دارند؛ بدونِ این migration `seed_content` سوالِ تمیز را «تازه» می‌دید (صورتش با دیتابیس فرق دارد) و کنارِ نسخه‌ی قدیمی دوباره می‌ساخت. تبدیل `content/plain_math.py::convert` است (idempotent). معکوس ندارد. پاسخِ دستیار با `rag.services.mathtext.to_plain` (همان مسیرِ زنده‌ی پاسخ‌ها). """ import hashlib from django.db import migrations from content.plain_math import convert, convert_figures def _h(text): return hashlib.sha1((text or '').encode('utf-8')).hexdigest() def _key(*parts): return hashlib.sha1('\x1f'.join(('intro', *map(str, parts))).encode('utf-8')).hexdigest() def forwards(apps, schema_editor): Question = apps.get_model('content', 'Question') Choice = apps.get_model('content', 'Choice') Lesson = apps.get_model('content', 'Lesson') SeedRecord = apps.get_model('content', 'SeedRecord') batch = [] for q in Question.objects.all().iterator(chunk_size=1000): new = convert(q.prompt) if new != q.prompt: q.prompt = new batch.append(q) if len(batch) >= 500: Question.objects.bulk_update(batch, ['prompt']) batch = [] if batch: Question.objects.bulk_update(batch, ['prompt']) batch = [] for c in Choice.objects.all().iterator(chunk_size=2000): new = convert(c.value) if new != c.value: c.value = new batch.append(c) if len(batch) >= 500: Choice.objects.bulk_update(batch, ['value']) batch = [] if batch: Choice.objects.bulk_update(batch, ['value']) # بعد از تبدیل ممکن است دو سوالِ هم‌صورت در یک درس بماند: نسخه‌ی قدیمیِ LaTeXدار و نسخه‌ای که seed # پیش از این migration با متنِ تمیز ساخته بود. یکی نگه داشته می‌شود: آن‌که پاسخِ بیشتری دارد، وگرنه قدیمی‌تر. from django.db.models import Count UserAnswer = apps.get_model('content', 'UserAnswer') groups = {} for qid, lid, prompt in Question.objects.values_list('id', 'lesson_id', 'prompt').iterator(chunk_size=2000): groups.setdefault((lid, prompt), []).append(qid) drop = [] for ids in groups.values(): if len(ids) > 1: answered = dict(UserAnswer.objects.filter(question_id__in=ids).values_list('question_id').annotate(n=Count('id'))) keep = max(ids, key=lambda i: (answered.get(i, 0), -i)) drop.extend(i for i in ids if i != keep) if drop: Question.objects.filter(id__in=drop).delete() for lesson in Lesson.objects.select_related('node__unit__grade').iterator(chunk_size=500): new = { 'title': convert(lesson.title), 'intro_text': convert(lesson.intro_text), 'example': convert(lesson.example), 'steps': [convert(s) for s in (lesson.steps or [])], 'figures': convert_figures(lesson.figures or []), } changed = [f for f, v in new.items() if v != getattr(lesson, f)] if not changed: continue old_intro, old_title = lesson.intro_text, lesson.title for f in changed: setattr(lesson, f, new[f]) lesson.save(update_fields=changed) if 'intro_text' in changed: n = lesson.node key = _key(n.unit.grade.title, n.unit.title, n.title, old_title) SeedRecord.objects.filter(key=key, value=_h(old_intro)).update(value=_h(new['intro_text'])) try: Message = apps.get_model('ai_tutor', 'Message') except LookupError: return from rag.services.mathtext import to_plain batch = [] for m in Message.objects.all().iterator(chunk_size=500): new = to_plain(m.text) if m.role == 'assistant' else convert(m.text) if new != m.text: m.text = new batch.append(m) if batch: Message.objects.bulk_update(batch, ['text']) class Migration(migrations.Migration): dependencies = [('content', '0020_plain_math_lesson_texts'), ('ai_tutor', '0008_alter_message_created_at')] operations = [migrations.RunPython(forwards, migrations.RunPython.noop)]