serve what the frontend screens actually need

صفحه‌های فرانت خوانده شدند و با API مقایسه شدند. ساختارِ ما از کتابِ واقعی
درآمده و درست‌تر است، پس مدل عوض نشد — ولی چند چیز کم داشت.

مهم‌ترینش یک باگ بود: /api/home پارامتر را `grade` می‌خواند و کلاینتی که به
فرانت فرستادیم `gradeId` می‌فرستد. پارامترِ ناشناخته بی‌صدا نادیده گرفته
می‌شد، یعنی کاربر پایه‌ای را انتخاب می‌کرد و نقشه‌ی پایه‌ی دیگری می‌دید
بدونِ هیچ خطایی. حالا هر دو نام پذیرفته می‌شوند (`gradeId` نامِ درست است،
با بقیه‌ی API یکی). تستِ سرتاسریِ قبلی این را نگرفته بود چون home را بدونِ
آرگومان صدا زده بود.

/api/grades حالا lessonCount می‌دهد. فهرستِ پایه‌های فرانت تا دوازدهم است
ولی محتوای ما تا نهم؛ بدونِ این عدد، کاربر پایه‌ی خالی را انتخاب می‌کند و
صفحه‌ی خالی می‌بیند بی‌آنکه بفهمد چرا.

GET /api/nodes/{id}/lessons اضافه شد. /api/home فقط شناسه‌ی درس‌ها را
می‌دهد، ولی نقشه‌ی مهارت برای هر قدم عنوان، نوع، زیرنویس و «انجام شد» لازم
دارد — بدونش فرانت باید برای هر درس یک intro جدا می‌گرفت.

Lesson حالا kind (آزمون/ویدیو) و متنِ سه‌تکه دارد: توضیح، مثال، حلِ
گام‌به‌گام. ویدیو از قبل سوالِ بازِ محصول بود و فرانت آیتمش را دارد؛ حالا
API می‌تواند بیانش کند، حتی اگر محتوایش بعداً بیاید. Unit یک blurb گرفت و
Node آیکون و رنگ، تا فرانت رنگ‌ها را هاردکد نکند. هیچ‌کدام اجباری نیست، پس
محتوای موجود دست‌نخورده کار می‌کند.

PATCH /api/user/profile حالا age را هم می‌پذیرد (صفحه‌ی ثبت‌نامِ فرانت سن
می‌پرسد). ذخیره‌سازی همچنان birthYear است چون کهنه نمی‌شود؛ این فقط ورودی
را ترجمه می‌کند تا دو طرف حسابِ متفاوت درنیاورند.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-14 01:42:24 +03:30
parent 8effe5b60b
commit 99a0cafe24
7 changed files with 386 additions and 7 deletions

View File

@@ -7,9 +7,13 @@ from .models import Grade, QuestionReport
# ---- Grades ----
class GradeSerializer(serializers.ModelSerializer):
# بدونِ این، کاربر پایه‌ای را انتخاب می‌کند که هنوز هیچ محتوایی ندارد و
# صفحه‌ی خالی می‌بیند. فرانت با همین می‌تواند آن گزینه را غیرفعال کند.
lessonCount = serializers.IntegerField(read_only=True)
class Meta:
model = Grade
fields = ['id', 'title', 'icon', 'color']
fields = ['id', 'title', 'icon', 'color', 'lessonCount']
class GradesListResponseSerializer(serializers.Serializer):
@@ -21,6 +25,10 @@ class GradesListResponseSerializer(serializers.Serializer):
class NodeStatusSerializer(serializers.Serializer):
id = serializers.CharField()
title = serializers.CharField()
# کارتِ مبحث در صفحه‌ی «مباحث» رنگ و آیکون دارد؛ با این دو، فرانت لازم
# نیست رنگ‌ها را خودش هاردکد کند.
icon = serializers.CharField(allow_blank=True)
color = serializers.CharField(allow_blank=True)
status = serializers.ChoiceField(choices=['locked', 'unlocked', 'completed'])
# شناسه‌ی درس‌های همین گره تا کلاینت بدونه با کلیک روی گره کدوم درس رو
# باز کنه (endpointهای lessons/{id}/... مستقیم با همین id کار می‌کنن).
@@ -30,6 +38,7 @@ class NodeStatusSerializer(serializers.Serializer):
class UnitWithNodesSerializer(serializers.Serializer):
id = serializers.CharField()
title = serializers.CharField()
blurb = serializers.CharField(allow_blank=True)
nodes = NodeStatusSerializer(many=True)
@@ -56,6 +65,33 @@ class LessonIntroResponseSerializer(serializers.Serializer):
# فیلد فرانت مجبور بود اول questions را بگیرد تا بفهمد چیزی هست یا نه، و
# `complete` هم totalQuestions=0 را رد می‌کند (درست است، ولی گیج‌کننده).
questionCount = serializers.IntegerField()
# نوعِ قدم در مسیر: آزمون یا ویدیو.
kind = serializers.ChoiceField(choices=['quiz', 'video'])
# متنِ درس سه‌تکه است: توضیح (همان introText)، مثال، و حلِ گام‌به‌گام.
example = serializers.CharField(allow_blank=True)
steps = serializers.ListField(child=serializers.CharField())
# فقط برای kind='video'
videoUrl = serializers.CharField(allow_blank=True)
durationMinutes = serializers.IntegerField(allow_null=True)
# ---- Node lessons (نقشه‌ی مهارت) ----
class NodeLessonSerializer(serializers.Serializer):
"""یک قدم در نقشه‌ی مهارت — همان چیزی که کارتِ درس نشان می‌دهد."""
id = serializers.CharField()
kind = serializers.ChoiceField(choices=['quiz', 'video'])
title = serializers.CharField()
# زیرنویسِ کارت: «۵ سوال» برای آزمون، «۱۲ دقیقه» برای ویدیو.
meta = serializers.CharField(allow_blank=True)
done = serializers.BooleanField()
class NodeLessonsResponseSerializer(serializers.Serializer):
nodeId = serializers.CharField()
title = serializers.CharField()
lessons = NodeLessonSerializer(many=True)
# ---- Lesson questions ----