""" اصلاحِ محتوای از قبل منتشرشده. seed هیچ‌وقت سوال را حذف یا بازنویسی نمی‌کند و مثال، گام‌ها و شکل‌های درس را فقط وقتی خالی‌اند پر می‌کند؛ پس اصلاحِ چیزی که روی سرور هست این ابزارِ جدا را می‌خواهد. python tools/figures/apply_fixups.py fixups.json # فقط فایل‌های seed محلی venv/bin/python tools/figures/apply_fixups.py --db fixups.json # دیتابیسِ همین ماشین (روی سرور، پس از pg_dump) fixups.json (هر کلید اختیاری است): "delete": [[path, prompt], ...] سوال را کامل پاک می‌کند. پاسخ‌های ثبت‌شده‌اش هم پاک می‌شود (CASCADE)، پس فقط برای سوالِ تکراری یا غلط، و پیش از کاربرِ واقعی. "blank_title": [[path, prompt], ...] عنوانِ زیرِ شکل را خالی می‌کند (عنوان گاهی جواب را لو می‌داد). "lesson": [[path, {"intro"|"example"|"steps"|"figures": مقدار}], ...] همان فیلدهای داده‌شده‌ی درس را بازنویسی می‌کند. "question_set": [[path, prompt, {"prompt"|"choices"|"correct"|"figure": مقدار}], ...] سوال را در جای خودش اصلاح می‌کند. گزینه‌ها به ترتیبِ id عوض می‌شوند و ردیفشان می‌ماند، چون پاسخ‌های ثبت‌شده به ردیفِ گزینه وصل‌اند؛ تعدادِ گزینه‌ها باید با سوالِ فعلی یکی باشد. path برای سوال و درس [پایه، فصل، جزیره، درس] است. هر موردی پیدا نشود یا نخواند فهرست می‌شود، بی‌صدا رد نمی‌شود. """ import json import os import sys ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) sys.path.insert(0, ROOT) LESSON_FIELDS = {'intro': ('introText', 'intro_text'), 'example': ('example', 'example'), 'steps': ('steps', 'steps'), 'figures': ('figures', 'figures')} QUESTION_FIELDS = {'prompt', 'choices', 'correct', 'figure'} def key(path, prompt): return (tuple(path), prompt.strip()) def _check_fields(fields, allowed, what): bad = set(fields) - set(allowed) if bad: raise SystemExit(f'{what}: فیلدِ ناشناخته {sorted(bad)}') def apply_seed(fx): p = os.path.join(ROOT, 'content', 'seed', 'questions.json') data = json.load(open(p)) dele = {key(*x) for x in fx.get('delete', [])} blank = {key(*x) for x in fx.get('blank_title', [])} qset = {key(path, prompt): f for path, prompt, f in fx.get('question_set', [])} seen, out, problems = set(), [], [] for q in data['questions']: k = key(q['path'], q['prompt']) if k in dele: seen.add(('delete', k)) continue if k in blank and q.get('figure'): q['figure']['title'] = '' seen.add(('blank_title', k)) if k in qset: f = qset[k] _check_fields(f, QUESTION_FIELDS, 'question_set') if 'choices' in f and len(f['choices']) != len(q['choices']): problems.append(('question_set: تعدادِ گزینه فرق دارد', k)) else: q.update(f) seen.add(('question_set', k)) out.append(q) data['questions'] = out json.dump(data, open(p, 'w'), ensure_ascii=False, indent=1) cp = os.path.join(ROOT, 'content', 'seed', 'curriculum.json') cur = json.load(open(cp)) lessons = {} for g in cur['grades']: for u in g['units']: for n in u['nodes']: for l in n['lessons']: lessons[(g['title'], u['title'], n['title'], l['title'])] = l for path, f in fx.get('lesson', []): _check_fields(f, LESSON_FIELDS, 'lesson') l = lessons.get(tuple(path)) if l is None: problems.append(('lesson: پیدا نشد', (tuple(path), ''))) continue for name, value in f.items(): l[LESSON_FIELDS[name][0]] = value seen.add(('lesson', tuple(path))) json.dump(cur, open(cp, 'w'), ensure_ascii=False, indent=1) return _missing(fx, seen) + problems def _missing(fx, seen): miss = [] for op in ('delete', 'blank_title'): miss += [(f'{op}: پیدا نشد', key(*x)) for x in fx.get(op, []) if (op, key(*x)) not in seen] miss += [('question_set: پیدا نشد', key(p, pr)) for p, pr, _ in fx.get('question_set', []) if ('question_set', key(p, pr)) not in seen] return miss def apply_db(fx): os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') import django django.setup() from django.db import transaction from content.models import Lesson, Question lessons = {} for l in Lesson.objects.select_related('node__unit__grade'): lessons[(l.node.unit.grade.title, l.node.unit.title, l.node.title, l.title)] = l problems = [] def find(path, prompt): l = lessons.get(tuple(path)) return list(Question.objects.filter(lesson=l, prompt=prompt.strip())) if l else [] with transaction.atomic(): for path, prompt in fx.get('delete', []): qs = find(path, prompt) if not qs: problems.append(('delete: پیدا نشد', key(path, prompt))) for q in qs: q.delete() for path, prompt in fx.get('blank_title', []): qs = find(path, prompt) if not qs: problems.append(('blank_title: پیدا نشد', key(path, prompt))) for q in qs: if q.figure: q.figure = {**q.figure, 'title': ''} q.save(update_fields=['figure']) for path, prompt, f in fx.get('question_set', []): _check_fields(f, QUESTION_FIELDS, 'question_set') qs = find(path, prompt) if not qs: problems.append(('question_set: پیدا نشد', key(path, prompt))) for q in qs: choices = list(q.choices.order_by('id')) if 'choices' in f and len(f['choices']) != len(choices): problems.append(('question_set: تعدادِ گزینه فرق دارد', key(path, prompt))) continue correct = f.get('correct') for i, c in enumerate(choices): if 'choices' in f: c.value = f['choices'][i] if correct is not None: c.is_correct = (i == correct) c.save(update_fields=['value', 'is_correct']) if 'prompt' in f: q.prompt = f['prompt'] if 'figure' in f: q.figure = f['figure'] q.save(update_fields=['prompt', 'figure']) for path, f in fx.get('lesson', []): _check_fields(f, LESSON_FIELDS, 'lesson') l = lessons.get(tuple(path)) if l is None: problems.append(('lesson: پیدا نشد', (tuple(path), ''))) continue for name, value in f.items(): setattr(l, LESSON_FIELDS[name][1], value) l.save(update_fields=[LESSON_FIELDS[n][1] for n in f]) return problems if __name__ == '__main__': args = [a for a in sys.argv[1:] if a != '--db'] fx = json.load(open(args[0])) problems = (apply_db if '--db' in sys.argv else apply_seed)(fx) counts = {k: len(v) for k, v in fx.items()} print(f'applied {counts}; problems: {len(problems)}') for what, k in problems[:20]: print(f' {what}: {" › ".join(k[0])} | {k[1][:60]}') sys.exit(1 if problems else 0)