172 lines
8.0 KiB
Python
172 lines
8.0 KiB
Python
"""
|
||
اصلاحِ محتوای از قبل منتشرشده. 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)
|