Subjects per grade (math, calculus, geometry, stats, discrete), 5 new textbooks, 202 new islands, long islands split, hand-written figures and lesson texts for the new books

- content.Subject + Unit.subject, /api/home?subject=, tutor grounded in the chosen book
- 5 new books ingested; ~1150 hand-written questions, islands named by hand
- node_split: islands over 7 lessons split into parts of <=6 (lesson ids and progress kept)
- Lesson.figures hand-authored and visually checked for 653 lessons of the new books;
  gen_lesson_figures got --subject and survives network errors
- seed files re-exported

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 05:35:18 +03:30
parent 3d51b3b83a
commit 8cb1ecb85e
32 changed files with 81218 additions and 4486 deletions

View File

@@ -0,0 +1,23 @@
from django.db import migrations, models
class Migration(migrations.Migration):
"""مرحله‌ی ۱ از ۴ (جدولِ کتاب‌ها). چهار مرحله‌ی جدا چون PostgreSQL اجازه نمی‌دهد در یک تراکنش
هم داده نوشت و هم همان جدول را ALTER کرد (pending trigger events)."""
dependencies = [('content', '0013_alter_userlessoncompletion_created_at_and_more')]
operations = [
migrations.CreateModel(
name='Subject',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('key', models.SlugField(max_length=30, unique=True)),
('title', models.CharField(max_length=50, unique=True)),
('icon', models.CharField(blank=True, max_length=100)),
('color', models.CharField(blank=True, max_length=10)),
('order', models.PositiveSmallIntegerField(default=0)),
],
options={'ordering': ['order', 'id']},
),
]

View File

@@ -0,0 +1,24 @@
from django.db import migrations
# رنگ‌ها جدا از رنگِ پایه‌ها (Grade.color) و از هم‌اند تا دکمه‌های نوارِ کتاب‌ها در نقشه
# به‌چشم یک مسیرِ جدا بیایند. آیکون‌ها نامِ lucide‌اند (همان فهرستی که نقشه می‌شناسد).
SUBJECTS = [
('math', 'ریاضی', 'Sigma', '#ED6956', 1),
('calculus', 'حسابان', 'LineChart', '#00BCD4', 2),
('geometry', 'هندسه', 'Compass', '#9B7BFF', 3),
('stats', 'آمار و احتمال', 'BarChart3', '#FF7A3D', 4),
('discrete', 'گسسته', 'Network', '#5FBF8F', 5),
]
def create_subjects(apps, schema_editor):
Subject = apps.get_model('content', 'Subject')
for key, title, icon, color, order in SUBJECTS:
Subject.objects.get_or_create(
key=key, defaults={'title': title, 'icon': icon, 'color': color, 'order': order},
)
class Migration(migrations.Migration):
dependencies = [('content', '0014_subject')]
operations = [migrations.RunPython(create_subjects, migrations.RunPython.noop)]

View File

@@ -0,0 +1,20 @@
import django.db.models.deletion
from django.db import migrations, models
import content.models
class Migration(migrations.Migration):
dependencies = [('content', '0015_seed_subjects')]
operations = [
migrations.AddField(
model_name='unit',
name='subject',
field=models.ForeignKey(
default=content.models.default_subject_pk,
on_delete=django.db.models.deletion.PROTECT,
related_name='units', to='content.subject',
),
),
]

View File

@@ -0,0 +1,19 @@
from django.db import migrations
def mark_calculus(apps, schema_editor):
"""
فصل‌های پایه‌ی یازدهم و دوازدهمِ موجود از کتابِ «حسابان»اند (`CALCULUS_11/12`)، نه ریاضیِ عمومی.
۰۰۱۶ همه را «ریاضی» گذاشت؛ اینجا درست می‌شود. پایه از `number` شناخته می‌شود، نه از id.
بررسی‌نشده: دیتابیسی که در آن فصلِ یازدهم/دوازدهم از کتابی جز حسابان آمده باشد (روی سرور و
لپ‌تاپ نیست؛ هیچ کتابِ دیگری برای آن پایه‌ها پیش از این migration ingest نشده بود).
"""
Subject = apps.get_model('content', 'Subject')
Unit = apps.get_model('content', 'Unit')
calculus = Subject.objects.get(key='calculus')
Unit.objects.filter(grade__number__in=(11, 12)).update(subject=calculus)
class Migration(migrations.Migration):
dependencies = [('content', '0016_unit_subject')]
operations = [migrations.RunPython(mark_calculus, migrations.RunPython.noop)]

View File

@@ -0,0 +1,25 @@
from django.db import migrations
from content.node_split import split_long_nodes
def split(apps, schema_editor):
"""
جزیره‌های بلندِ (بیش از ۷ درس) پایه‌های سوم تا دوازدهم را به جزیره‌های حداکثر ۶ درسی می‌شکند (`content/node_split.py`).
روی دیتابیسِ خالی (تست، سرورِ تازه) هیچ کاری نمی‌کند؛ آنجا `seed_content` از فایلِ بازساخته‌شده می‌سازد.
"""
Unit = apps.get_model('content', 'Unit')
stats = split_long_nodes(
Node=apps.get_model('content', 'Node'),
Lesson=apps.get_model('content', 'Lesson'),
UserNodeCompletion=apps.get_model('content', 'UserNodeCompletion'),
NodeProgress=apps.get_model('content', 'NodeProgress'),
units=Unit.objects.filter(grade__number__in=range(3, 13)).order_by('id'),
)
if stats['nodes_split']:
print(f'\n split_long_nodes: {stats}')
class Migration(migrations.Migration):
dependencies = [('content', '0017_calculus_units')]
operations = [migrations.RunPython(split, migrations.RunPython.noop)]