build the real curriculum from the textbook structure

تا پیش از این کلِ محتوای دیتابیس ردیف‌های [DEMO] و [SEED] بود — دانش‌آموزِ
پایه نهم در دمو «جمع تا ۱۰» می‌دید. سینیور «یک بیسیکِ کامل با دیتای واقعی»
خواسته بود.

seed_curriculum_from_textbook (رایگان، هیچ فراخوانیِ پولی ندارد):
    Grade  ← پایه‌ی کتاب
    Unit   ← فصلِ کتاب
    Node   ← درسِ داخلِ فصل، از فهرستِ خودِ کتاب
    Lesson ← نوعِ مسئله‌ی استخراج‌شده در بازه‌ی صفحه‌ی همان درس
intro_text مستقیم از generalSolution کتاب می‌آید.

generate_questions_from_chunks سوالِ چهارگزینه‌ای با گزینه‌های انحرافیِ
باورپذیر می‌سازد (هزینه‌دار، اعتبارسنجیِ سخت‌گیرانه). سوال اینجا جدا شد
چون ساختنِ گزینه‌ی انحرافیِ درست به مدل نیاز دارد.

purge_demo_data ردیف‌های [SEED]/[DEMO] را گزارش و — فقط با --apply — پاک
می‌کند؛ بدونِ آن این ردیف‌ها در دمو داخلِ API دیده می‌شوند.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-13 19:00:10 +03:30
parent c79be0a336
commit 78130e7194
3 changed files with 500 additions and 0 deletions

View File

@@ -0,0 +1,87 @@
"""
پاک‌کردنِ ردیف‌های نمونه‌ی «[SEED]» و «[DEMO]» از دیتابیسِ محلی.
چرا لازم است: این ردیف‌ها در APIهای واقعی دیده می‌شوند — «[DEMO] پایهٔ نمونه»
در `/api/grades`، «[SEED] ماهانه» در پلن‌ها، «[SEED] کاربر اصلی» در لیدربورد —
و در دمو کنارِ محتوای واقعیِ کتاب بد به نظر می‌رسند.
برگشت‌پذیر است: `python manage.py seed_test_data` دوباره می‌سازدشان.
python manage.py purge_demo_data # فقط نشان می‌دهد
python manage.py purge_demo_data --apply # واقعاً پاک می‌کند
"""
from django.core.management.base import BaseCommand
from django.db import transaction
PREFIXES = ('[SEED]', '[DEMO]')
class Command(BaseCommand):
help = 'پاک‌کردنِ ردیف‌های نمونه‌ی [SEED]/[DEMO] (برگشت‌پذیر با seed_test_data).'
def add_arguments(self, parser):
parser.add_argument('--apply', action='store_true',
help='بدونِ این فقط گزارش می‌دهد و چیزی پاک نمی‌کند.')
def handle(self, *args, **opts):
from accounts.models import User
from content.models import Grade
from progress.models import Character
from subscription.models import Plan
# (مدل، نامِ فیلدِ متنی) — همه‌ی جاهایی که seed_test_data برچسب می‌زند.
targets = [
(Grade, 'title'), (Plan, 'title'), (Character, 'name'), (User, 'name'),
]
found = []
for model, field in targets:
queryset = model.objects.none()
for prefix in PREFIXES:
queryset = queryset | model.objects.filter(**{f'{field}__startswith': prefix})
rows = list(queryset.distinct())
if rows:
found.append((model, field, rows))
if not found:
self.stdout.write(self.style.SUCCESS('هیچ ردیفِ نمونه‌ای پیدا نشد.'))
return
self.stdout.write(self.style.MIGRATE_HEADING('\nردیف‌های نمونه\n'))
total = 0
for model, field, rows in found:
self.stdout.write(f' {model.__name__} — {len(rows)} ردیف')
for row in rows[:8]:
self.stdout.write(f' #{row.pk} {getattr(row, field)}')
if len(rows) > 8:
self.stdout.write(f' … و {len(rows) - 8} مورد دیگر')
total += len(rows)
# هشدارِ لازم: بعضی از این‌ها تنها دادهٔ آن endpoint هستند.
empties = [
name for model, name in ((Plan, 'subscription/plans'), (Character, 'characters'))
if model.objects.count() == len([r for m, _f, rows in found if m is model for r in rows])
]
if empties:
self.stdout.write(self.style.WARNING(
'\n⚠ بعد از پاک‌کردن این endpointها خالی می‌شوند: ' + '، '.join(empties)
+ '\n (پلن و کاراکتر را ادمین در جنگو-ادمین می‌سازد؛ اگر برای دمو '
'لازم‌اند، اول واقعی‌شان را بساز بعد این را اجرا کن.)'
))
if not opts['apply']:
self.stdout.write(self.style.WARNING(
f'\n{total} ردیف. چیزی پاک نشد — برای پاک‌کردن --apply بدهید.'
))
return
# حذفِ آبشاری: Grade کلِ زیردرختِ Unit/Node/Lesson/Question را می‌برد،
# و User سشن‌ها و پاسخ‌هایش را. همه‌شان دادهٔ نمونه‌اند.
with transaction.atomic():
for model, _field, rows in found:
model.objects.filter(pk__in=[r.pk for r in rows]).delete()
self.stdout.write(self.style.SUCCESS(f'\n{total} ردیفِ نمونه پاک شد.'))
self.stdout.write(self.style.HTTP_INFO(
'برای برگرداندن: python manage.py seed_test_data'
))