Files
MathApp/docs/content-authoring.md
Zareian 5492788bf8 make the README an entry point and move procedures into docs/
The README had grown into a 295-line mixture of onboarding, an ops runbook, a
content-authoring workflow, bulk question generation and a Docker log stack. A
reader arriving at the repo could not tell in ten seconds what this service is,
whether it is live, or where to start, and five of its claims no longer matched
the code:

- it said to export .env values by hand; settings.py has loaded .env through
  python-dotenv for a while now
- it listed six variables as "everything the app actually reads"; .env.example
  has carried many more than that for some time
- it called DJANGO_SECRET_KEY, DJANGO_ALLOWED_HOSTS and CACHE_URL required in
  production; all three now have working defaults, and the app boots without
  them on purpose so a partial .env cannot take the service down
- it claimed there is no insecure SECRET_KEY fallback; there is one, under DEBUG
- it required Python 3.12 while the project runs on 3.11 (Docker uses 3.12)

The README now states what the service is, where it runs, how healthy it is and
how to get it running in six commands, then links onward. Procedures moved to
docs/operations.md, docs/content-authoring.md, docs/architecture.md and
docs/docker.md, which is where someone follows a procedure rather than skims it.
Nothing was dropped; the deploy one-liner now includes the separate sudo restart
that deploy.sh cannot perform itself, and backup/restore is written down for the
first time.

Every number in the README was measured rather than quoted: content counts from
the live /api/health and content/seed/curriculum.json, 55 endpoints from the
generated OpenAPI schema, 481 tests from a full run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 01:12:04 +03:30

8.0 KiB
Raw Blame History

ساختن و ویرایشِ محتوا

هدفِ این مسیر یک جمله است: محتوا باید بدونِ توسعه‌دهنده قابلِ ویرایش باشد. اجرای یک management command روی SSH آخرین راهِ چاره است، نه روالِ کار.

محتوا کجا زندگی می‌کند

content/seed/ کلِ برنامه‌ی درسی و بانکِ سوال را به‌صورتِ JSONِ کامیت‌شده نگه می‌دارد:

فایل محتوا
curriculum.json ۸ پایه، ۵۸ فصل، ۲۶۳ نود، ۲٬۶۱۷ درس
questions.json ۲٬۴۶۳ سوالِ چندگزینه‌ای

deploy.sh در هر انتشار seed_content را اجرا می‌کند، پس یک سرورِ تازه خودش را با یک فرمان پر می‌کند — بدونِ Qdrant، بدونِ اعتبارِ API، بدونِ نشستِ SSH.

کد و محتوا دو مسیرِ جدا دارند. db.sqlite3 در gitignore است. سوالی که فقط در دیتابیسِ محلیِ شماست به سرور نمی‌رسد مگر از راهِ seed_content --export --only questions و کامیتِ content/seed/questions.json. پوش کردنِ کد به‌تنهایی محتوا را نمی‌برد.

دو رفتارِ seed_content که دانستنشان لازم است:

  • idempotent است و هرگز چیزی پاک نمی‌کند. هر چه در فایل هست ساخته یا به‌روز می‌شود؛ هر چه نیست دست‌نخورده می‌ماند. پس یک فایلِ ناقص محتوای سرور را از بین نمی‌برد.
  • سوال‌ها با مسیرِ عنوان به درس وصل می‌شوند (پایه › فصل › نود › درس)، نه با کلیدِ اصلی. شناسه‌ها بینِ هر دو دیتابیس فرق دارند و اتصال با id، سوال‌ها را بی‌صدا به درسِ اشتباه می‌چسباند.

seed_content --export فایل‌ها را از دیتابیسِ محلی بازنویسی می‌کند. اگر دیتابیس سوالی نداشته باشد از بازنویسیِ یک questions.json غیرخالی خودداری می‌کند (این قبلاً فایل را بی‌صدا پاک می‌کرد)؛ وقتی فقط برنامه‌ی درسی را می‌خواهید --export --only curriculum بزنید.

از پنلِ ادمین (روالِ عادی)

کار کجا
افزودن یا ویرایشِ یک سوال /admin/content/question/ → «افزودن» (انتخابِ درس با جستجو)
وارد کردنِ فله‌ای سوال همان صفحه → «وارد کردنِ سوال‌ها از فایل»
گرفتنِ خروجیِ سوال‌ها چند سوال را انتخاب کنید → اکشنِ «دریافتِ فایل»
وارد کردنِ برنامه‌ی درسی /admin/content/grade/ → «وارد کردنِ برنامه‌ی درسی از فایل»
گرفتنِ خروجیِ برنامه‌ی درسی پایه‌ها را انتخاب کنید → اکشنِ «دریافتِ فایلِ برنامه‌ی درسی»

خروجیِ هر کدام دقیقاً همان قالبی‌ست که ورودی‌اش می‌پذیرد، پس چرخه‌ی «بگیر → ویرایش کن → برگردان» بسته است و برای دیدنِ یک نمونه لازم نیست کسی مستندات بخواند.

دو تضمین:

  • سوال‌ها: فایلِ نامعتبر هیچ چیزی نمی‌نویسد — یا همه وارد می‌شوند یا هیچ‌کدام.
  • برنامه‌ی درسی: چیزی پاک نمی‌شود، به همان دلیلِ بالا.

ساختِ انبوهِ سوال از یک کتابِ تازه

وقتی کتابی تازه روی Qdrant آمده، مسیرِ رایگان این است:

# اسکلتِ برنامه‌ی درسی از فهرستِ خودِ کتاب (بدونِ هزینه)
python manage.py seed_curriculum_from_textbook --subject ریاضی --grade نهم --dry-run
python manage.py seed_curriculum_from_textbook --subject ریاضی --grade نهم

# جفت‌های (درس، مسئله‌ی تأییدشده) را برای سوال‌نویسی بیرون بده (بدونِ هزینه)
python manage.py generate_questions_from_chunks --subject ریاضی --grade نهم \
  --export /tmp/batches

--dry-run روی هر دو کار می‌کند و پیش از هر نوشتنی نشان می‌دهد چه ساخته خواهد شد.

خروجی چند فایلِ JSON است به‌همراهِ یک *-RULES.txt که عینِ همان پرامپتی را دارد که به مدلِ پولی می‌رفت — تا کیفیتِ دو مسیر از هم جدا نیفتد. سوال‌ها را با آن قواعد بنویسید و از «وارد کردنِ فله‌ای سوال» در جدولِ بالا واردشان کنید.

همان فرمان بدونِ --export کار را به مدلِ پولی می‌سپارد. این کار را نکنید مگر دلیلِ خاصی داشته باشید: اندازه‌گیری‌شده، گران‌تر است و کیفیتِ بهتری نمی‌دهد. جزئیات و هزینه‌های واقعی در extraction-playbook.md.

کیفیتِ محتوا

کتاب‌ها با یک مدلِ ویژن از تصویرِ صفحه خوانده می‌شوند، پس خطای استخراج از همان‌جا وارد می‌شود: رقمِ بدخوانده‌شده، جداکننده‌ی اعشارِ افتاده، راه‌حلی که به مسئله‌ی بغلی چسبیده. این‌ها در بازیابی دیده نمی‌شوند و فقط وقتی لو می‌روند که چیزی به متن اعتماد کند.

rag_verify_solutions هر مسئله‌ی استخراج‌شده را مستقلاً دوباره حل می‌کند و نتیجه را مقایسه می‌کند. LessonChunk.solution_verdict سه‌حالته است — agrees / disagrees / unjudgeable. ساختِ سوال فقط از chunkهای agrees می‌خواند.

دو درسی که گران تمام شدند و حالا در پرامپت‌ها کدگذاری شده‌اند:

  • رأیِ دوحالته غلط است. با دو حالت، هر «نمی‌توانم قضاوت کنم» تبدیل به «غلط» می‌شود و نرخِ خطا را حدودِ سه برابرِ واقعیت گزارش می‌کند. پیش از اعتماد به هر اجرای انبوه، چند نمونه را با چشم بخوانید.
  • ۳/۱۰۴ در این کتاب‌ها عددِ اعشاریِ ۳٫۱۰۴ است، نه کسرِ ۳ تقسیم بر ۱۰۴.

خودِ solution هرگز بازنویسی نمی‌شود — متنِ استخراج‌شده به‌عنوانِ سند می‌ماند و پاسخِ مستقل جداگانه در checked_answer ذخیره می‌شود.

solution_verdict='agrees' تضمینِ کامل نیست، فقط فیلترِ اول: چند بار خطای حسابِ خودِ کتاب در همین مرحله گرفته شده است.

داده‌ی ساختگی برای توسعه

seed_test_data یک دیتاستِ کوچک و idempotent می‌سازد که همه‌ی اپ‌ها را پوشش می‌دهد. هرگز روی سروری که کاربرِ واقعی دارد اجرا نشود — ردیف‌هایی با پیشوندِ [SEED]/[DEMO] می‌سازد که در API دیده می‌شوند. محتوای واقعی از مسیرِ بالا می‌رود، نه از این فرمان.

python manage.py seed_test_data              # فقط با DEBUG=True اجرا می‌شود
python manage.py seed_test_data --force      # staging، با DEBUG=False
python manage.py seed_test_data --reset      # پاک کردنِ داده‌ی seedِ قبلی و ساختِ دوباره