Compare commits
12 Commits
82fec6bf75
...
670513b462
| Author | SHA1 | Date | |
|---|---|---|---|
| 670513b462 | |||
| 133233144e | |||
| a5a93ef6ba | |||
| 5f963e2898 | |||
| 7bd7c637b4 | |||
| ac96f93abc | |||
| 9903e0ec91 | |||
| 759bb0d575 | |||
| f8a93a37c9 | |||
| 9eae99340d | |||
| f57deb9c1d | |||
| 5fbbd371fe |
@@ -300,7 +300,7 @@ has been done before.
|
|||||||
### Content
|
### Content
|
||||||
|
|
||||||
- `content/seed/` is the whole curriculum and question bank as committed JSON: 10 grades (3–12), 5 subjects,
|
- `content/seed/` is the whole curriculum and question bank as committed JSON: 10 grades (3–12), 5 subjects,
|
||||||
89 units, 1,145 islands, 5,406 lessons, 18,522 questions. `deploy.sh` runs `seed_content` on every release.
|
89 units, 1,145 islands, 5,406 lessons (every one has at least one question), 39,156 questions. `deploy.sh` runs `seed_content` on every release.
|
||||||
- `seed_content` is idempotent, never deletes, and does not undo admin work: the `SeedRecord` ledger remembers
|
- `seed_content` is idempotent, never deletes, and does not undo admin work: the `SeedRecord` ledger remembers
|
||||||
what it delivered, so anything the admin deleted or renamed is not recreated; lesson text, `example`,
|
what it delivered, so anything the admin deleted or renamed is not recreated; lesson text, `example`,
|
||||||
`steps` and `figures` are only filled when empty. Questions bind to lessons **by title path**
|
`steps` and `figures` are only filled when empty. Questions bind to lessons **by title path**
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
| **سرویسِ زنده** | `https://mathapi.tadrisino2.ir` · اپ: `https://math.tadrisino2.ir` |
|
| **سرویسِ زنده** | `https://mathapi.tadrisino2.ir` · اپ: `https://math.tadrisino2.ir` |
|
||||||
| **فناوری** | Python 3.11 · Django 5.2 · Django REST Framework · PostgreSQL 14 · Redis · Qdrant · gunicorn پشتِ nginx و ArvanCloud |
|
| **فناوری** | Python 3.11 · Django 5.2 · Django REST Framework · PostgreSQL 14 · Redis · Qdrant · gunicorn پشتِ nginx و ArvanCloud |
|
||||||
| **API** | ۶۱ عملیات روی ۵۹ مسیر، مستند با OpenAPI (`/api/docs/`) |
|
| **API** | ۶۱ عملیات روی ۵۹ مسیر، مستند با OpenAPI (`/api/docs/`) |
|
||||||
| **محتوا** | ۱۰ پایه · ۵ کتاب · ۸۹ فصل · ۱٬۱۴۵ جزیره · ۵٬۴۰۶ درس · ۱۸٬۵۲۲ سوال |
|
| **محتوا** | ۱۰ پایه · ۵ کتاب · ۸۹ فصل · ۱٬۱۴۵ جزیره · ۵٬۴۰۶ درس · ۳۹٬۱۵۶ سوال |
|
||||||
| **تست** | ۹۷۳ تستِ خودکار، همه سبز |
|
| **تست** | ۹۷۳ تستِ خودکار، همه سبز |
|
||||||
| **سرویسهای بیرونی** | GapGPT (مدلِ زبانی) · Qdrant (جستجوی کتاب) · کاوهنگار (پیامک) · زرینپال و کافهبازار (پرداخت) |
|
| **سرویسهای بیرونی** | GapGPT (مدلِ زبانی) · Qdrant (جستجوی کتاب) · کاوهنگار (پیامک) · زرینپال و کافهبازار (پرداخت) |
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
356308
content/seed/questions.json
356308
content/seed/questions.json
File diff suppressed because it is too large
Load Diff
@@ -57,7 +57,11 @@ class _Ledger:
|
|||||||
self.write[key] = (kind, value)
|
self.write[key] = (kind, value)
|
||||||
|
|
||||||
def save(self) -> None:
|
def save(self) -> None:
|
||||||
existing = set(SeedRecord.objects.filter(key__in=list(self.write)).values_list('key', flat=True))
|
# دستهدسته: SQLite بیش از ۳۲٬۷۶۶ متغیر در یک پرسوجو نمیپذیرد و دفترِ یک seedِ تازه از آن بیشتر است.
|
||||||
|
keys = list(self.write)
|
||||||
|
existing = set()
|
||||||
|
for i in range(0, len(keys), 500):
|
||||||
|
existing.update(SeedRecord.objects.filter(key__in=keys[i:i + 500]).values_list('key', flat=True))
|
||||||
SeedRecord.objects.bulk_create([
|
SeedRecord.objects.bulk_create([
|
||||||
SeedRecord(key=k, kind=kind, value=value)
|
SeedRecord(key=k, kind=kind, value=value)
|
||||||
for k, (kind, value) in self.write.items() if k not in existing
|
for k, (kind, value) in self.write.items() if k not in existing
|
||||||
|
|||||||
61
content/tests_apply_fixups.py
Normal file
61
content/tests_apply_fixups.py
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
"""tools/figures/apply_fixups.py --db: اصلاحِ محتوای منتشرشده روی دیتابیس، بدون از دست رفتنِ پاسخهای ثبتشده."""
|
||||||
|
import importlib.util
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from content.models import Choice, Grade, Lesson, Node, Question, Unit, UserAnswer
|
||||||
|
|
||||||
|
_spec = importlib.util.spec_from_file_location(
|
||||||
|
'apply_fixups', os.path.join(settings.BASE_DIR, 'tools', 'figures', 'apply_fixups.py'))
|
||||||
|
apply_fixups = importlib.util.module_from_spec(_spec)
|
||||||
|
_spec.loader.exec_module(apply_fixups)
|
||||||
|
|
||||||
|
PATH = ['پایه سوم', 'فصل ۱', 'جزیره', 'درس']
|
||||||
|
|
||||||
|
|
||||||
|
class ApplyFixupsDbTests(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
g = Grade.objects.create(title='پایه سوم', icon='', color='')
|
||||||
|
u = Unit.objects.create(grade=g, title='فصل ۱', order=1)
|
||||||
|
n = Node.objects.create(unit=u, title='جزیره', order=1)
|
||||||
|
self.lesson = Lesson.objects.create(node=n, title='درس', intro_text='متن', example='مثالِ غلط',
|
||||||
|
steps=['گام'], figures=[{'where': 'example', 'diagram': {}}])
|
||||||
|
self.q = Question.objects.create(lesson=self.lesson, type='multiple_choice', prompt='(۱)/(۲) کدام است؟')
|
||||||
|
self.c = [Choice.objects.create(question=self.q, value=v, is_correct=(i == 0))
|
||||||
|
for i, v in enumerate(['(۱)/(۲)', '۲', '۳', '۴'])]
|
||||||
|
self.answer = UserAnswer.objects.create(user_id=7, question=self.q, choice=self.c[0], correct=True)
|
||||||
|
self.dup = Question.objects.create(lesson=self.lesson, type='multiple_choice', prompt='تکراری')
|
||||||
|
|
||||||
|
def test_question_set_keeps_rows_and_answers(self):
|
||||||
|
problems = apply_fixups.apply_db({'question_set': [[PATH, '(۱)/(۲) کدام است؟', {
|
||||||
|
'prompt': '۱/۲ کدام است؟', 'choices': ['۱/۲', '۲', '۳', '۴'], 'correct': 0}]]})
|
||||||
|
self.assertEqual(problems, [])
|
||||||
|
self.q.refresh_from_db()
|
||||||
|
self.assertEqual(self.q.prompt, '۱/۲ کدام است؟')
|
||||||
|
self.assertEqual([c.value for c in self.q.choices.order_by('id')], ['۱/۲', '۲', '۳', '۴'])
|
||||||
|
self.assertEqual([c.pk for c in self.q.choices.order_by('id')], [c.pk for c in self.c])
|
||||||
|
self.assertTrue(UserAnswer.objects.filter(pk=self.answer.pk, choice=self.c[0]).exists())
|
||||||
|
|
||||||
|
def test_choice_count_mismatch_is_reported_not_applied(self):
|
||||||
|
problems = apply_fixups.apply_db({'question_set': [[PATH, '(۱)/(۲) کدام است؟', {'choices': ['۱', '۲']}]]})
|
||||||
|
self.assertEqual(len(problems), 1)
|
||||||
|
self.assertEqual(self.c[0].__class__.objects.get(pk=self.c[0].pk).value, '(۱)/(۲)')
|
||||||
|
|
||||||
|
def test_lesson_fields_and_delete(self):
|
||||||
|
problems = apply_fixups.apply_db({
|
||||||
|
'lesson': [[PATH, {'example': 'مثالِ درست', 'figures': []}]],
|
||||||
|
'delete': [[PATH, 'تکراری']]})
|
||||||
|
self.assertEqual(problems, [])
|
||||||
|
self.lesson.refresh_from_db()
|
||||||
|
self.assertEqual((self.lesson.example, self.lesson.figures, self.lesson.steps), ('مثالِ درست', [], ['گام']))
|
||||||
|
self.assertFalse(Question.objects.filter(pk=self.dup.pk).exists())
|
||||||
|
|
||||||
|
def test_missing_targets_are_reported(self):
|
||||||
|
problems = apply_fixups.apply_db({'delete': [[PATH, 'نیست']], 'lesson': [[['x', 'y', 'z', 'w'], {'steps': []}]]})
|
||||||
|
self.assertEqual(len(problems), 2)
|
||||||
|
|
||||||
|
def test_unknown_field_stops(self):
|
||||||
|
with self.assertRaises(SystemExit):
|
||||||
|
apply_fixups.apply_db({'lesson': [[PATH, {'title': 'نه'}]]})
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
| فایل | محتوا |
|
| فایل | محتوا |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `curriculum.json` | ۱۰ پایه (سوم تا دوازدهم)، ۵ کتاب، ۸۹ فصل، ۱٬۱۴۵ جزیره، ۵٬۴۰۶ درس؛ با `subject`، متنِ درس، مثال، قدمها و شکلِ درس (`figures`) |
|
| `curriculum.json` | ۱۰ پایه (سوم تا دوازدهم)، ۵ کتاب، ۸۹ فصل، ۱٬۱۴۵ جزیره، ۵٬۴۰۶ درس؛ با `subject`، متنِ درس، مثال، قدمها و شکلِ درس (`figures`) |
|
||||||
| `questions.json` | ۱۸٬۵۲۲ سوالِ چهارگزینهای، بعضی با شکل (`figure`) |
|
| `questions.json` | ۳۹٬۱۵۶ سوالِ چهارگزینهای، بعضی با شکل (`figure`) |
|
||||||
|
|
||||||
هر فصل باید `subject` داشته باشد (ریاضی، حسابان، هندسه، آمار و احتمال، گسسته)؛ فایلِ بیآن رد میشود. شکلها
|
هر فصل باید `subject` داشته باشد (ریاضی، حسابان، هندسه، آمار و احتمال، گسسته)؛ فایلِ بیآن رد میشود. شکلها
|
||||||
(`Lesson.figures`، `Question.figure`) با `rag.services.diagram.validate` در ورود اعتبارسنجی میشوند و شکلِ نامعتبر کلِ
|
(`Lesson.figures`، `Question.figure`) با `rag.services.diagram.validate` در ورود اعتبارسنجی میشوند و شکلِ نامعتبر کلِ
|
||||||
@@ -180,3 +180,5 @@ python tools/figures/batch.py merge batch.json # افزود
|
|||||||
|
|
||||||
اصلاحِ سوالِ منتشرشده: `tools/figures/apply_fixups.py` (حذف و خالیکردنِ عنوان، هم روی seed هم با `--db` روی سرور،
|
اصلاحِ سوالِ منتشرشده: `tools/figures/apply_fixups.py` (حذف و خالیکردنِ عنوان، هم روی seed هم با `--db` روی سرور،
|
||||||
پس از `pg_dump`)؛ seed خودش چیزی را بازنویسی نمیکند.
|
پس از `pg_dump`)؛ seed خودش چیزی را بازنویسی نمیکند.
|
||||||
|
فایل `tools/figures/server_fixups.json` اصلاحهای سوالها و درسهایی است که پیش از بازسازیِ پایههای ۳ تا ۱۲ منتشر شده بودند
|
||||||
|
(۹۳ حذف، ۵۸۵ اصلاحِ سوال، ۲۷۳۸ اصلاحِ درس)؛ پس از هر استقرارِ seed با `apply_fixups.py --db` روی سرور اجرا میشود.
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
|---|---|
|
|---|---|
|
||||||
| پایههای سوم تا دوازدهم | **۱۰ پایه** |
|
| پایههای سوم تا دوازدهم | **۱۰ پایه** |
|
||||||
| کتاب (ریاضی، حسابان، هندسه، آمار و احتمال، گسسته) | **۱۵ کتاب در ۵ درس** |
|
| کتاب (ریاضی، حسابان، هندسه، آمار و احتمال، گسسته) | **۱۵ کتاب در ۵ درس** |
|
||||||
| سؤالِ چهارگزینهای | **۱۸٬۵۲۲** |
|
| سؤالِ چهارگزینهای | **۲۲٬۶۰۲** |
|
||||||
| عملیاتِ API، مستند در Swagger | **۶۱** |
|
| عملیاتِ API، مستند در Swagger | **۶۱** |
|
||||||
| تستِ خودکار، همه سبز | **۹۷۳** |
|
| تستِ خودکار، همه سبز | **۹۷۳** |
|
||||||
| اپِ جنگو، هرکدام یک مسئولیت | **۱۴** |
|
| اپِ جنگو، هرکدام یک مسئولیت | **۱۴** |
|
||||||
@@ -55,7 +55,7 @@
|
|||||||
|
|
||||||
### واحدهای محتوا (این واژهها در کلِ سند تکرار میشوند)
|
### واحدهای محتوا (این واژهها در کلِ سند تکرار میشوند)
|
||||||
|
|
||||||
پایه Grade ۱۰ ← کتاب Subject ۵ ← فصل Unit ۸۹ ← جزیره Node ۱٬۱۴۵ ← درس Lesson ۵٬۴۰۶ ← سؤال Question ۱۸٬۵۲۲ ← گزینه Choice
|
پایه Grade ۱۰ ← کتاب Subject ۵ ← فصل Unit ۸۹ ← جزیره Node ۱٬۱۴۵ ← درس Lesson ۵٬۴۰۶ ← سؤال Question ۲۲٬۶۰۲ ← گزینه Choice
|
||||||
|
|
||||||
**«درس» در این پروژه یعنی یک «تیپمسئله»ی کتاب**، نه یک جلسهی کلاس. برای همین درس خیلی ریز است و اپ در سطحِ **جزیره** (Node) بازی میشود: آزمونِ هر جزیره از درسهایش سؤال برمیدارد.
|
**«درس» در این پروژه یعنی یک «تیپمسئله»ی کتاب**، نه یک جلسهی کلاس. برای همین درس خیلی ریز است و اپ در سطحِ **جزیره** (Node) بازی میشود: آزمونِ هر جزیره از درسهایش سؤال برمیدارد.
|
||||||
|
|
||||||
@@ -732,7 +732,7 @@ if (Math.sign(y) !== Math.sign(py)) { const r = bisect(f, px, x); if (Math.abs(f
|
|||||||
| فایل | محتوا |
|
| فایل | محتوا |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `content/seed/curriculum.json` | ۱۰ پایه، ۵ کتاب، ۸۹ فصل، ۱٬۱۴۵ جزیره، ۵٬۴۰۶ درس، با مثال، قدمها و شکلِ درس |
|
| `content/seed/curriculum.json` | ۱۰ پایه، ۵ کتاب، ۸۹ فصل، ۱٬۱۴۵ جزیره، ۵٬۴۰۶ درس، با مثال، قدمها و شکلِ درس |
|
||||||
| `content/seed/questions.json` | ۱۸٬۵۲۲ سؤالِ چهارگزینهای، بعضی با شکل |
|
| `content/seed/questions.json` | ۲۲٬۶۰۲ سؤالِ چهارگزینهای، بعضی با شکل |
|
||||||
|
|
||||||
- `seed_content` در هر انتشار اجرا میشود؛ **هیچوقت چیزی پاک نمیکند** و اجرای دوبارهاش بیخطر است.
|
- `seed_content` در هر انتشار اجرا میشود؛ **هیچوقت چیزی پاک نمیکند** و اجرای دوبارهاش بیخطر است.
|
||||||
- سؤال با **مسیرِ عنوان** (پایه › فصل › جزیره › درس) به درس وصل میشود، نه با شناسه — چون شناسهها بینِ دو دیتابیس فرق دارند.
|
- سؤال با **مسیرِ عنوان** (پایه › فصل › جزیره › درس) به درس وصل میشود، نه با شناسه — چون شناسهها بینِ دو دیتابیس فرق دارند.
|
||||||
|
|||||||
@@ -1,13 +1,21 @@
|
|||||||
"""
|
"""
|
||||||
اصلاحِ سوالهای شکلدارِ از قبل منتشرشده (seed هیچوقت حذف یا بازنویسی نمیکند، پس اصلاح یک ابزارِ جدا میخواهد).
|
اصلاحِ محتوای از قبل منتشرشده. seed هیچوقت سوال را حذف یا بازنویسی نمیکند و مثال، گامها و شکلهای درس را
|
||||||
|
فقط وقتی خالیاند پر میکند؛ پس اصلاحِ چیزی که روی سرور هست این ابزارِ جدا را میخواهد.
|
||||||
|
|
||||||
python tools/figures/apply_fixups.py fixups.json # فقط فایلهای seed محلی
|
python tools/figures/apply_fixups.py fixups.json # فقط فایلهای seed محلی
|
||||||
venv/bin/python tools/figures/apply_fixups.py --db fixups.json # دیتابیسِ همین ماشین (روی سرور، پس از pg_dump)
|
venv/bin/python tools/figures/apply_fixups.py --db fixups.json # دیتابیسِ همین ماشین (روی سرور، پس از pg_dump)
|
||||||
|
|
||||||
fixups.json: {"delete": [[path, prompt], ...], "blank_title": [[path, prompt], ...]}
|
fixups.json (هر کلید اختیاری است):
|
||||||
delete سوال را کامل پاک میکند (پاسخهای ثبتشدهاش هم میرود؛ فقط برای سوالِ تازهمنتشرشده)
|
"delete": [[path, prompt], ...] سوال را کامل پاک میکند. پاسخهای ثبتشدهاش هم پاک میشود (CASCADE)،
|
||||||
blank_title عنوانِ زیرِ شکل را خالی میکند (عنوان گاهی جوابِ سوال را لو میداد)
|
پس فقط برای سوالِ تکراری یا غلط، و پیش از کاربرِ واقعی.
|
||||||
سوال با (path, prompt) شناخته میشود؛ هر کدام پیدا نشود فهرست میشود، بیصدا رد نمیشود.
|
"blank_title": [[path, prompt], ...] عنوانِ زیرِ شکل را خالی میکند (عنوان گاهی جواب را لو میداد).
|
||||||
|
"lesson": [[path, {"intro"|"example"|"steps"|"figures": مقدار}], ...]
|
||||||
|
همان فیلدهای دادهشدهی درس را بازنویسی میکند.
|
||||||
|
"question_set": [[path, prompt, {"prompt"|"choices"|"correct"|"figure": مقدار}], ...]
|
||||||
|
سوال را در جای خودش اصلاح میکند. گزینهها به ترتیبِ id عوض میشوند و
|
||||||
|
ردیفشان میماند، چون پاسخهای ثبتشده به ردیفِ گزینه وصلاند؛ تعدادِ
|
||||||
|
گزینهها باید با سوالِ فعلی یکی باشد.
|
||||||
|
path برای سوال و درس [پایه، فصل، جزیره، درس] است. هر موردی پیدا نشود یا نخواند فهرست میشود، بیصدا رد نمیشود.
|
||||||
"""
|
"""
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
@@ -16,64 +24,148 @@ import sys
|
|||||||
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
sys.path.insert(0, ROOT)
|
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):
|
def key(path, prompt):
|
||||||
return (tuple(path), prompt.strip())
|
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):
|
def apply_seed(fx):
|
||||||
p = os.path.join(ROOT, 'content', 'seed', 'questions.json')
|
p = os.path.join(ROOT, 'content', 'seed', 'questions.json')
|
||||||
data = json.load(open(p))
|
data = json.load(open(p))
|
||||||
dele = {key(*x) for x in fx.get('delete', [])}
|
dele = {key(*x) for x in fx.get('delete', [])}
|
||||||
blank = {key(*x) for x in fx.get('blank_title', [])}
|
blank = {key(*x) for x in fx.get('blank_title', [])}
|
||||||
seen_d, seen_b, out = set(), set(), []
|
qset = {key(path, prompt): f for path, prompt, f in fx.get('question_set', [])}
|
||||||
|
seen, out, problems = set(), [], []
|
||||||
for q in data['questions']:
|
for q in data['questions']:
|
||||||
k = key(q['path'], q['prompt'])
|
k = key(q['path'], q['prompt'])
|
||||||
if k in dele:
|
if k in dele:
|
||||||
seen_d.add(k)
|
seen.add(('delete', k))
|
||||||
continue
|
continue
|
||||||
if k in blank and q.get('figure'):
|
if k in blank and q.get('figure'):
|
||||||
q['figure']['title'] = ''
|
q['figure']['title'] = ''
|
||||||
seen_b.add(k)
|
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)
|
out.append(q)
|
||||||
data['questions'] = out
|
data['questions'] = out
|
||||||
json.dump(data, open(p, 'w'), ensure_ascii=False, indent=1)
|
json.dump(data, open(p, 'w'), ensure_ascii=False, indent=1)
|
||||||
return dele - seen_d, blank - seen_b
|
|
||||||
|
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):
|
def apply_db(fx):
|
||||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||||
import django
|
import django
|
||||||
django.setup()
|
django.setup()
|
||||||
from content.models import Question
|
from django.db import transaction
|
||||||
|
from content.models import Lesson, Question
|
||||||
lessons = {}
|
lessons = {}
|
||||||
from content.models import Lesson
|
|
||||||
for l in Lesson.objects.select_related('node__unit__grade'):
|
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.pk
|
lessons[(l.node.unit.grade.title, l.node.unit.title, l.node.title, l.title)] = l
|
||||||
miss_d, miss_b = set(), set()
|
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', []):
|
for path, prompt in fx.get('delete', []):
|
||||||
lid = lessons.get(tuple(path))
|
qs = find(path, prompt)
|
||||||
qs = Question.objects.filter(lesson_id=lid, prompt=prompt.strip()) if lid else Question.objects.none()
|
|
||||||
if not qs.exists():
|
|
||||||
miss_d.add(key(path, prompt))
|
|
||||||
else:
|
|
||||||
qs.delete()
|
|
||||||
for path, prompt in fx.get('blank_title', []):
|
|
||||||
lid = lessons.get(tuple(path))
|
|
||||||
qs = list(Question.objects.filter(lesson_id=lid, prompt=prompt.strip())) if lid else []
|
|
||||||
if not qs:
|
if not qs:
|
||||||
miss_b.add(key(path, prompt))
|
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:
|
for q in qs:
|
||||||
if q.figure:
|
if q.figure:
|
||||||
q.figure = {**q.figure, 'title': ''}
|
q.figure = {**q.figure, 'title': ''}
|
||||||
q.save(update_fields=['figure'])
|
q.save(update_fields=['figure'])
|
||||||
return miss_d, miss_b
|
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__':
|
if __name__ == '__main__':
|
||||||
args = [a for a in sys.argv[1:] if a != '--db']
|
args = [a for a in sys.argv[1:] if a != '--db']
|
||||||
fx = json.load(open(args[0]))
|
fx = json.load(open(args[0]))
|
||||||
miss_d, miss_b = (apply_db if '--db' in sys.argv else apply_seed)(fx)
|
problems = (apply_db if '--db' in sys.argv else apply_seed)(fx)
|
||||||
print(f'deleted/blanked ok; not found: delete={len(miss_d)} blank_title={len(miss_b)}')
|
counts = {k: len(v) for k, v in fx.items()}
|
||||||
for k in list(miss_d)[:5] + list(miss_b)[:5]:
|
print(f'applied {counts}; problems: {len(problems)}')
|
||||||
print(' missing:', k[1][:60])
|
for what, k in problems[:20]:
|
||||||
|
print(f' {what}: {" › ".join(k[0])} | {k[1][:60]}')
|
||||||
|
sys.exit(1 if problems else 0)
|
||||||
|
|||||||
1
tools/figures/server_fixups.json
Normal file
1
tools/figures/server_fixups.json
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user