Lesson.figures: fixed diagrams per lesson (stored once, served with the lesson); gen_lesson_figures; 3 calculus-12 lessons
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -95,6 +95,7 @@ def parse(raw: bytes) -> list[dict]:
|
|||||||
'intro_text': (lesson.get('introText') or ''),
|
'intro_text': (lesson.get('introText') or ''),
|
||||||
'example': str(lesson.get('example') or ''),
|
'example': str(lesson.get('example') or ''),
|
||||||
'steps': [str(x) for x in (lesson.get('steps') or []) if str(x).strip()][:12],
|
'steps': [str(x) for x in (lesson.get('steps') or []) if str(x).strip()][:12],
|
||||||
|
'figures': lesson.get('figures') or [],
|
||||||
}
|
}
|
||||||
for li, lesson in enumerate(lessons)
|
for li, lesson in enumerate(lessons)
|
||||||
],
|
],
|
||||||
@@ -170,6 +171,9 @@ def import_curriculum(grades: list[dict]) -> dict:
|
|||||||
if lesson_row.get('steps') and not lesson.steps:
|
if lesson_row.get('steps') and not lesson.steps:
|
||||||
lesson.steps = lesson_row['steps']
|
lesson.steps = lesson_row['steps']
|
||||||
changed.append('steps')
|
changed.append('steps')
|
||||||
|
if lesson_row.get('figures') and not lesson.figures:
|
||||||
|
lesson.figures = lesson_row['figures']
|
||||||
|
changed.append('figures')
|
||||||
if changed:
|
if changed:
|
||||||
lesson.save(update_fields=changed)
|
lesson.save(update_fields=changed)
|
||||||
if not created:
|
if not created:
|
||||||
@@ -200,6 +204,7 @@ def export_curriculum(grades_queryset) -> dict:
|
|||||||
'title': lesson.title, 'introText': lesson.intro_text,
|
'title': lesson.title, 'introText': lesson.intro_text,
|
||||||
**({'example': lesson.example} if lesson.example else {}),
|
**({'example': lesson.example} if lesson.example else {}),
|
||||||
**({'steps': lesson.steps} if lesson.steps else {}),
|
**({'steps': lesson.steps} if lesson.steps else {}),
|
||||||
|
**({'figures': lesson.figures} if lesson.figures else {}),
|
||||||
}
|
}
|
||||||
for lesson in node.lessons.all()
|
for lesson in node.lessons.all()
|
||||||
],
|
],
|
||||||
|
|||||||
93
content/management/commands/gen_lesson_figures.py
Normal file
93
content/management/commands/gen_lesson_figures.py
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
"""
|
||||||
|
شکلِ ثابتِ درسها را یک بار میسازد و در `Lesson.figures` مینویسد.
|
||||||
|
|
||||||
|
همان مسیرِ ارزانِ دستیار: همان مدلِ چت، همان کاتالوگِ <diagram>، همان
|
||||||
|
اعتبارسنجِ `rag.services.diagram.validate`. فرقش این است که نتیجه ذخیره میشود و
|
||||||
|
بعد از تأییدِ چشمی با `seed_content --export` به curriculum.json میرود.
|
||||||
|
فقط درسهایی که شکل لازم دارند شکل میگیرند؛ مدل میتواند `[]` بدهد.
|
||||||
|
آزموده نشده: درستیِ هندسی/ریاضیِ شکل؛ باید با چشم دید.
|
||||||
|
|
||||||
|
python manage.py gen_lesson_figures --grade 12 --limit 4 [--redo] [--dry-run]
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
|
||||||
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
|
|
||||||
|
from content.models import Lesson
|
||||||
|
from rag.services import clients, diagram as diagram_mod
|
||||||
|
from rag.services.generation import chat_model_for, completion_kwargs
|
||||||
|
|
||||||
|
PROMPT = (
|
||||||
|
'تو برای یک درسِ ریاضی (اپِ آموزشی) شکلهای آموزشی میسازی. متنِ درس: «توضیح»، «مثال» و '
|
||||||
|
'«قدمها» (شماره از صفر). فقط جاهایی شکل بگذار که دیدنش مفهوم را روشن میکند (نمودار، '
|
||||||
|
'انتقال، تقارن، محور، جدول، هندسه)؛ هر شکل باید دقیقاً همان چیزی را نشان دهد که آن بخش '
|
||||||
|
'میگوید، با همان اعداد. بخشِ بینیاز شکل نمیگیرد؛ اگر هیچجا لازم نبود []. حداکثر ۵ شکل.\n'
|
||||||
|
'خروجی فقط یک آرایهی JSON: [{"where":"intro"|"example"|"step","index":0,"diagram":{...}}] '
|
||||||
|
'(index فقط برای step). بدونِ توضیح و بدونِ ```.\n'
|
||||||
|
'مثالهای کاتالوگ فقط قالباند: نقطه، تابع یا آیتمی را که این درس لازم ندارد کپی نکن؛ فقط '
|
||||||
|
'چیزهایی بگذار که همین شکل نشان میدهد (برای نمودارِ تابع معمولاً فقط items با type=function). نمودارِ خطشکسته یا تکهای از نقطهها را '
|
||||||
|
'با type=segment بین نقطههای متوالی بکش، نه type=line (آن خطِ بیپایان است). نقطههایی که '
|
||||||
|
'در items نمیآیند تعریف نکن.\n\n'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = 'ساختِ شکلِ ثابتِ درسها (یک بار، ذخیره در Lesson.figures).'
|
||||||
|
|
||||||
|
def add_arguments(self, p):
|
||||||
|
p.add_argument('--grade', type=int, required=True)
|
||||||
|
p.add_argument('--limit', type=int, default=4)
|
||||||
|
p.add_argument('--lesson', type=int, help='فقط همین شناسه')
|
||||||
|
p.add_argument('--redo', action='store_true', help='درسهایی که شکل دارند را هم بساز')
|
||||||
|
p.add_argument('--dry-run', action='store_true')
|
||||||
|
|
||||||
|
def handle(self, *a, **o):
|
||||||
|
qs = Lesson.objects.filter(node__unit__grade__number=o['grade']).order_by(
|
||||||
|
'node__unit__order', 'node__order', 'id')
|
||||||
|
if o['lesson']:
|
||||||
|
qs = qs.filter(pk=o['lesson'])
|
||||||
|
if not o['redo']:
|
||||||
|
qs = qs.filter(figures=[])
|
||||||
|
lessons = list(qs[:o['limit']])
|
||||||
|
if not lessons:
|
||||||
|
raise CommandError('درسی پیدا نشد.')
|
||||||
|
model = chat_model_for('')
|
||||||
|
client = clients.get_openai_client()
|
||||||
|
spent = 0
|
||||||
|
for lesson in lessons:
|
||||||
|
body = (f'عنوان: {lesson.title}\nتوضیح: {lesson.intro_text}\nمثال: {lesson.example}\n'
|
||||||
|
+ '\n'.join(f'قدم {i}: {s}' for i, s in enumerate(lesson.steps or [])))
|
||||||
|
r = client.chat.completions.create(
|
||||||
|
model=model, **completion_kwargs(model),
|
||||||
|
messages=[{'role': 'system', 'content': PROMPT + diagram_mod.DIAGRAM_SPEC},
|
||||||
|
{'role': 'user', 'content': body}])
|
||||||
|
u = getattr(r, 'usage', None)
|
||||||
|
spent += (getattr(u, 'total_tokens', 0) or 0)
|
||||||
|
raw = re.sub(r'^```(?:json)?|```$', '', (r.choices[0].message.content or '').strip()).strip()
|
||||||
|
try:
|
||||||
|
start = raw.index('[')
|
||||||
|
items, _ = json.JSONDecoder().raw_decode(raw[start:])
|
||||||
|
if not isinstance(items, list):
|
||||||
|
raise ValueError('not a list')
|
||||||
|
except ValueError as e:
|
||||||
|
self.stderr.write(f'درس {lesson.id}: خروجیِ نامعتبر ({e}); ذخیره نشد.')
|
||||||
|
continue
|
||||||
|
figures, dropped = [], 0
|
||||||
|
for it in items:
|
||||||
|
where = it.get('where') if isinstance(it, dict) else None
|
||||||
|
spec = diagram_mod.validate(it.get('diagram')) if where else None
|
||||||
|
idx = it.get('index')
|
||||||
|
if (spec is None or where not in ('intro', 'example', 'step')
|
||||||
|
or (where == 'step' and not (isinstance(idx, int) and 0 <= idx < len(lesson.steps or [])))
|
||||||
|
or (where == 'example' and not lesson.example)):
|
||||||
|
dropped += 1
|
||||||
|
continue
|
||||||
|
figures.append({'where': where, **({'index': idx} if where == 'step' else {}), 'diagram': spec})
|
||||||
|
self.stdout.write(f'درس {lesson.id} «{lesson.title}»: {len(figures)} شکل، {dropped} ردشده')
|
||||||
|
for f in figures:
|
||||||
|
self.stdout.write(' ' + json.dumps(f, ensure_ascii=False))
|
||||||
|
if figures and not o['dry_run']:
|
||||||
|
lesson.figures = figures
|
||||||
|
lesson.save(update_fields=['figures'])
|
||||||
|
self.stdout.write(f'توکنِ مصرفی: {spent}')
|
||||||
18
content/migrations/0011_lesson_figures.py
Normal file
18
content/migrations/0011_lesson_figures.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.2.17 on 2026-10-07 12:45
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('content', '0010_seedrecord'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='lesson',
|
||||||
|
name='figures',
|
||||||
|
field=models.JSONField(blank=True, default=list),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -99,6 +99,11 @@ class Lesson(models.Model):
|
|||||||
intro_text = models.TextField()
|
intro_text = models.TextField()
|
||||||
example = models.TextField(blank=True)
|
example = models.TextField(blank=True)
|
||||||
steps = models.JSONField(default=list, blank=True)
|
steps = models.JSONField(default=list, blank=True)
|
||||||
|
# شکلهای ثابتِ درس: [{"where": "intro"|"example"|"step", "index": n, "diagram": {...}}].
|
||||||
|
# یک بار با `gen_lesson_figures` ساخته و با چشم تأیید میشود و بعد همینجا و در
|
||||||
|
# curriculum.json میماند؛ هنگامِ نمایش هیچ فراخوانیِ مدل و هزینهای نیست.
|
||||||
|
# `index` فقط برای where='step' است (از صفر). شکل با `rag.services.diagram.validate` گذشته است.
|
||||||
|
figures = models.JSONField(default=list, blank=True)
|
||||||
|
|
||||||
# فقط برای kind='video'
|
# فقط برای kind='video'
|
||||||
video_url = models.URLField(blank=True)
|
video_url = models.URLField(blank=True)
|
||||||
|
|||||||
@@ -20664,6 +20664,190 @@
|
|||||||
"به طور مثال، نقطه (-4,0) ابتدا به (-5,0) منتقل میشود و سپس به (-5,-3) میرسد.",
|
"به طور مثال، نقطه (-4,0) ابتدا به (-5,0) منتقل میشود و سپس به (-5,-3) میرسد.",
|
||||||
"همین کار برای سایر نقاط انجام میشود و نمودار نهایی به دست میآید.",
|
"همین کار برای سایر نقاط انجام میشود و نمودار نهایی به دست میآید.",
|
||||||
"✅ جواب: نقاط جدید: (-5,-3), (-3,-2), (-1,-2), (2,-3)"
|
"✅ جواب: نقاط جدید: (-5,-3), (-3,-2), (-1,-2), (2,-3)"
|
||||||
|
],
|
||||||
|
"figures": [
|
||||||
|
{
|
||||||
|
"where": "example",
|
||||||
|
"diagram": {
|
||||||
|
"kind": "plane",
|
||||||
|
"title": "نمودار تابع f",
|
||||||
|
"axes": true,
|
||||||
|
"points": {
|
||||||
|
"A": [
|
||||||
|
-4.0,
|
||||||
|
0.0
|
||||||
|
],
|
||||||
|
"B": [
|
||||||
|
-2.0,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"C": [
|
||||||
|
0.0,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"D": [
|
||||||
|
3.0,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "point",
|
||||||
|
"point": "A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "point",
|
||||||
|
"point": "B"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "point",
|
||||||
|
"point": "C"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "point",
|
||||||
|
"point": "D"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "segment",
|
||||||
|
"from": "A",
|
||||||
|
"to": "B"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "segment",
|
||||||
|
"from": "B",
|
||||||
|
"to": "C"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "segment",
|
||||||
|
"from": "C",
|
||||||
|
"to": "D"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"grid": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"where": "step",
|
||||||
|
"index": 0,
|
||||||
|
"diagram": {
|
||||||
|
"kind": "plane",
|
||||||
|
"title": "انتقال نمودار f به سمت چپ",
|
||||||
|
"axes": true,
|
||||||
|
"points": {
|
||||||
|
"A": [
|
||||||
|
-5.0,
|
||||||
|
0.0
|
||||||
|
],
|
||||||
|
"B": [
|
||||||
|
-3.0,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"C": [
|
||||||
|
-1.0,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"D": [
|
||||||
|
2.0,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "point",
|
||||||
|
"point": "A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "point",
|
||||||
|
"point": "B"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "point",
|
||||||
|
"point": "C"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "point",
|
||||||
|
"point": "D"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "segment",
|
||||||
|
"from": "A",
|
||||||
|
"to": "B"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "segment",
|
||||||
|
"from": "B",
|
||||||
|
"to": "C"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "segment",
|
||||||
|
"from": "C",
|
||||||
|
"to": "D"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"grid": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"where": "step",
|
||||||
|
"index": 1,
|
||||||
|
"diagram": {
|
||||||
|
"kind": "plane",
|
||||||
|
"title": "انتقال نمودار به سمت پایین",
|
||||||
|
"axes": true,
|
||||||
|
"points": {
|
||||||
|
"A": [
|
||||||
|
-5.0,
|
||||||
|
-3.0
|
||||||
|
],
|
||||||
|
"B": [
|
||||||
|
-3.0,
|
||||||
|
-2.0
|
||||||
|
],
|
||||||
|
"C": [
|
||||||
|
-1.0,
|
||||||
|
-2.0
|
||||||
|
],
|
||||||
|
"D": [
|
||||||
|
2.0,
|
||||||
|
-3.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "point",
|
||||||
|
"point": "A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "point",
|
||||||
|
"point": "B"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "point",
|
||||||
|
"point": "C"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "point",
|
||||||
|
"point": "D"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "segment",
|
||||||
|
"from": "A",
|
||||||
|
"to": "B"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "segment",
|
||||||
|
"from": "B",
|
||||||
|
"to": "C"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "segment",
|
||||||
|
"from": "C",
|
||||||
|
"to": "D"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"grid": true
|
||||||
|
}
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -20681,11 +20865,79 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "انتقال عمودی نمودار تابع",
|
"title": "انتقال عمودی نمودار تابع",
|
||||||
"introText": "برای انتقال عمودی نمودار تابع، اگر تابع اصلی y = f(x) باشد، تابع جدید را به صورت g(x) = f(x) + k تعریف میکنیم. این انتقال باعث میشود که هر نقطه (x, y) روی نمودار تابع اصلی به نقطه (x, y+k) روی نمودار تابع جدید منتقل شود. بنابراین نمودار تابع به اندازه k واحد به سمت بالا (اگر k مثبت باشد) یا به سمت پایین (اگر k منفی باشد) جابجا میشود."
|
"introText": "برای انتقال عمودی نمودار تابع، اگر تابع اصلی y = f(x) باشد، تابع جدید را به صورت g(x) = f(x) + k تعریف میکنیم. این انتقال باعث میشود که هر نقطه (x, y) روی نمودار تابع اصلی به نقطه (x, y+k) روی نمودار تابع جدید منتقل شود. بنابراین نمودار تابع به اندازه k واحد به سمت بالا (اگر k مثبت باشد) یا به سمت پایین (اگر k منفی باشد) جابجا میشود.",
|
||||||
|
"figures": [
|
||||||
|
{
|
||||||
|
"where": "intro",
|
||||||
|
"diagram": {
|
||||||
|
"kind": "plane",
|
||||||
|
"title": "انتقال عمودی نمودار تابع",
|
||||||
|
"axes": true,
|
||||||
|
"points": {},
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "function",
|
||||||
|
"expr": "x^2",
|
||||||
|
"from": -2.0,
|
||||||
|
"to": 2.0,
|
||||||
|
"marks": [
|
||||||
|
"vertex"
|
||||||
|
],
|
||||||
|
"label": "y = f(x)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "function",
|
||||||
|
"expr": "x^2 + 2",
|
||||||
|
"from": -2.0,
|
||||||
|
"to": 2.0,
|
||||||
|
"marks": [
|
||||||
|
"vertex"
|
||||||
|
],
|
||||||
|
"label": "g(x) = f(x) + 2"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"grid": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "انتقال افقی نمودار تابع",
|
"title": "انتقال افقی نمودار تابع",
|
||||||
"introText": "برای انتقال افقی نمودار تابع، اگر تابع اصلی y = f(x) باشد، تابع جدید را به صورت g(x) = f(x + c) تعریف میکنیم. این انتقال باعث میشود که هر نقطه (x, y) روی نمودار تابع اصلی به نقطه (x - c, y) روی نمودار تابع جدید منتقل شود. بنابراین نمودار تابع به اندازه c واحد به سمت چپ (اگر c مثبت باشد) یا به سمت راست (اگر c منفی باشد) جابجا میشود."
|
"introText": "برای انتقال افقی نمودار تابع، اگر تابع اصلی y = f(x) باشد، تابع جدید را به صورت g(x) = f(x + c) تعریف میکنیم. این انتقال باعث میشود که هر نقطه (x, y) روی نمودار تابع اصلی به نقطه (x - c, y) روی نمودار تابع جدید منتقل شود. بنابراین نمودار تابع به اندازه c واحد به سمت چپ (اگر c مثبت باشد) یا به سمت راست (اگر c منفی باشد) جابجا میشود.",
|
||||||
|
"figures": [
|
||||||
|
{
|
||||||
|
"where": "intro",
|
||||||
|
"diagram": {
|
||||||
|
"kind": "plane",
|
||||||
|
"title": "انتقال افقی تابع",
|
||||||
|
"axes": true,
|
||||||
|
"points": {},
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "function",
|
||||||
|
"expr": "x^2",
|
||||||
|
"from": -3.0,
|
||||||
|
"to": 3.0,
|
||||||
|
"marks": [
|
||||||
|
"vertex"
|
||||||
|
],
|
||||||
|
"label": "y = f(x)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "function",
|
||||||
|
"expr": "(x-2)^2",
|
||||||
|
"from": -3.0,
|
||||||
|
"to": 3.0,
|
||||||
|
"marks": [
|
||||||
|
"vertex"
|
||||||
|
],
|
||||||
|
"label": "y = g(x)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"grid": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "انتقال نمودار تابع به سمت بالا یا پایین",
|
"title": "انتقال نمودار تابع به سمت بالا یا پایین",
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ def _sync_lesson(node, path, row, ledger, counts):
|
|||||||
return
|
return
|
||||||
lesson = Lesson.objects.create(
|
lesson = Lesson.objects.create(
|
||||||
node=node, title=row['title'], intro_text=row['intro_text'],
|
node=node, title=row['title'], intro_text=row['intro_text'],
|
||||||
example=row['example'], steps=row['steps'],
|
example=row['example'], steps=row['steps'], figures=row.get('figures') or [],
|
||||||
)
|
)
|
||||||
counts['lessons'] += 1
|
counts['lessons'] += 1
|
||||||
ledger.record(_key('lesson', *path), 'lesson')
|
ledger.record(_key('lesson', *path), 'lesson')
|
||||||
@@ -154,6 +154,9 @@ def _sync_lesson(node, path, row, ledger, counts):
|
|||||||
if row.get('steps') and not lesson.steps:
|
if row.get('steps') and not lesson.steps:
|
||||||
lesson.steps = row['steps']
|
lesson.steps = row['steps']
|
||||||
changed.append('steps')
|
changed.append('steps')
|
||||||
|
if row.get('figures') and not lesson.figures:
|
||||||
|
lesson.figures = row['figures']
|
||||||
|
changed.append('figures')
|
||||||
if changed:
|
if changed:
|
||||||
lesson.save(update_fields=changed)
|
lesson.save(update_fields=changed)
|
||||||
counts['updated'] += 1
|
counts['updated'] += 1
|
||||||
|
|||||||
@@ -82,6 +82,8 @@ class LessonIntroResponseSerializer(serializers.Serializer):
|
|||||||
# متنِ درس سهتکه است: توضیح (همان introText)، مثال، و حلِ گامبهگام.
|
# متنِ درس سهتکه است: توضیح (همان introText)، مثال، و حلِ گامبهگام.
|
||||||
example = serializers.CharField(allow_blank=True)
|
example = serializers.CharField(allow_blank=True)
|
||||||
steps = serializers.ListField(child=serializers.CharField())
|
steps = serializers.ListField(child=serializers.CharField())
|
||||||
|
# شکلهای ثابتِ درس؛ فرانت با `diagram` همان رندرِ دستیار را میکشد.
|
||||||
|
figures = serializers.ListField(child=serializers.DictField(), default=list)
|
||||||
# فقط برای kind='video'
|
# فقط برای kind='video'
|
||||||
videoUrl = serializers.CharField(allow_blank=True)
|
videoUrl = serializers.CharField(allow_blank=True)
|
||||||
durationMinutes = serializers.IntegerField(allow_null=True)
|
durationMinutes = serializers.IntegerField(allow_null=True)
|
||||||
|
|||||||
17
content/tests_lesson_figures.py
Normal file
17
content/tests_lesson_figures.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
from rest_framework.test import APIClient
|
||||||
|
|
||||||
|
from content.models import Grade, Lesson, Node, Unit
|
||||||
|
|
||||||
|
|
||||||
|
class LessonFiguresTests(TestCase):
|
||||||
|
def test_intro_returns_stored_figures_and_empty_list_by_default(self):
|
||||||
|
grade = Grade.objects.create(title='دوازدهم', number=12)
|
||||||
|
unit = Unit.objects.create(grade=grade, title='u', order=1)
|
||||||
|
node = Node.objects.create(unit=unit, title='n', order=1)
|
||||||
|
fig = {'where': 'step', 'index': 0, 'diagram': {'kind': 'plane', 'title': 't'}}
|
||||||
|
with_fig = Lesson.objects.create(node=node, title='a', intro_text='x', steps=['s'], figures=[fig])
|
||||||
|
plain = Lesson.objects.create(node=node, title='b', intro_text='x')
|
||||||
|
c = APIClient()
|
||||||
|
self.assertEqual(c.get(f'/api/lessons/{with_fig.id}/intro').json()['figures'], [fig])
|
||||||
|
self.assertEqual(c.get(f'/api/lessons/{plain.id}/intro').json()['figures'], [])
|
||||||
@@ -441,6 +441,7 @@ class LessonIntroView(APIView):
|
|||||||
'introText': lesson.intro_text,
|
'introText': lesson.intro_text,
|
||||||
'example': lesson.example,
|
'example': lesson.example,
|
||||||
'steps': lesson.steps or [],
|
'steps': lesson.steps or [],
|
||||||
|
'figures': lesson.figures or [],
|
||||||
'videoUrl': lesson.video_url,
|
'videoUrl': lesson.video_url,
|
||||||
'durationMinutes': lesson.duration_minutes,
|
'durationMinutes': lesson.duration_minutes,
|
||||||
'mascotAnimation': lesson.mascot_animation,
|
'mascotAnimation': lesson.mascot_animation,
|
||||||
|
|||||||
Reference in New Issue
Block a user