No LaTeX/markdown anywhere students read (seed, import, DB migration 0021, tutor history, frontend guard); quiz of an island is fixed per user again

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 12:15:28 +03:30
parent dd10ff83f4
commit e662914efc
11 changed files with 855 additions and 670 deletions

View File

@@ -0,0 +1,107 @@
"""
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)]