Files
MathApp/tools/figures/apply_fixups.py

172 lines
8.0 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
اصلاحِ محتوای از قبل منتشرشده. 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)