The skill-map response gave units -> nodes with id/title/status but no way to map a node to its lessons, and no other endpoint fills that gap (/api/progress/nodes doesn't either). Any client rendering the map needs this to know which lesson to open when a node is tapped. The ids are already computed in the cached _grade_tree; this just surfaces them. Additive and backward-compatible; content tests pass. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
131 lines
4.7 KiB
Python
131 lines
4.7 KiB
Python
from drf_spectacular.utils import extend_schema_field
|
||
from rest_framework import serializers
|
||
|
||
from .models import Grade
|
||
|
||
|
||
# ---- Grades ----
|
||
|
||
class GradeSerializer(serializers.ModelSerializer):
|
||
class Meta:
|
||
model = Grade
|
||
fields = ['id', 'title', 'icon', 'color']
|
||
|
||
|
||
class GradesListResponseSerializer(serializers.Serializer):
|
||
grades = GradeSerializer(many=True)
|
||
|
||
|
||
# ---- Home (skill map + stats) ----
|
||
|
||
class NodeStatusSerializer(serializers.Serializer):
|
||
id = serializers.CharField()
|
||
title = serializers.CharField()
|
||
status = serializers.ChoiceField(choices=['locked', 'unlocked', 'completed'])
|
||
# شناسهی درسهای همین گره تا کلاینت بدونه با کلیک روی گره کدوم درس رو
|
||
# باز کنه (endpointهای lessons/{id}/... مستقیم با همین id کار میکنن).
|
||
lessonIds = serializers.ListField(child=serializers.IntegerField())
|
||
|
||
|
||
class UnitWithNodesSerializer(serializers.Serializer):
|
||
id = serializers.CharField()
|
||
title = serializers.CharField()
|
||
nodes = NodeStatusSerializer(many=True)
|
||
|
||
|
||
class StatsSerializer(serializers.Serializer):
|
||
progressPercent = serializers.FloatField()
|
||
xp = serializers.IntegerField()
|
||
coins = serializers.IntegerField()
|
||
|
||
|
||
class HomeResponseSerializer(serializers.Serializer):
|
||
units = UnitWithNodesSerializer(many=True)
|
||
stats = StatsSerializer()
|
||
|
||
|
||
# ---- Lesson intro ----
|
||
|
||
class LessonIntroResponseSerializer(serializers.Serializer):
|
||
lessonId = serializers.CharField()
|
||
title = serializers.CharField()
|
||
introText = serializers.CharField()
|
||
mascotAnimation = serializers.CharField()
|
||
|
||
|
||
# ---- Lesson questions ----
|
||
|
||
class ChoiceOptionSerializer(serializers.Serializer):
|
||
id = serializers.CharField()
|
||
value = serializers.CharField()
|
||
|
||
|
||
class QuestionSerializer(serializers.Serializer):
|
||
id = serializers.CharField()
|
||
type = serializers.CharField()
|
||
prompt = serializers.CharField()
|
||
options = ChoiceOptionSerializer(many=True)
|
||
|
||
|
||
class LessonQuestionsResponseSerializer(serializers.Serializer):
|
||
questions = QuestionSerializer(many=True)
|
||
|
||
|
||
# ---- Answer submission ----
|
||
|
||
class AnswerRequestSerializer(serializers.Serializer):
|
||
selectedOptionId = serializers.IntegerField()
|
||
|
||
|
||
class AnswerResponseSerializer(serializers.Serializer):
|
||
correct = serializers.BooleanField()
|
||
correctOptionId = serializers.CharField()
|
||
xpEarned = serializers.IntegerField()
|
||
heartsRemaining = serializers.IntegerField()
|
||
# حالتِ انیمیشنِ ماسکوت («رُبو») برای فرانت: جوابِ درست → happy، غلط →
|
||
# encourage. همخوان با RobotMood در MathApp_frontend.
|
||
mascotMood = serializers.CharField()
|
||
|
||
|
||
# ---- Lesson completion ----
|
||
|
||
class LessonCompleteRequestSerializer(serializers.Serializer):
|
||
# 🔒 اصلاح امنیتی: قبلاً هیچ محدودیتی روی این دو فیلد نبود؛ کلاینت
|
||
# میتونست correctCount منفی یا totalQuestions صفر/منفی بفرسته.
|
||
# min_value ها فقط شرطِ سادهی «غیرمنفی/مثبت» رو تضمین میکنن؛ چکِ
|
||
# اصلیِ «correctCount نباید از تعدادِ واقعیِ سوالهای درس بیشتر باشه»
|
||
# در LessonCompleteView (جایی که به Lesson دسترسی داریم) انجام میشه.
|
||
correctCount = serializers.IntegerField(min_value=0)
|
||
totalQuestions = serializers.IntegerField(min_value=1)
|
||
|
||
|
||
class LessonCompleteResponseSerializer(serializers.Serializer):
|
||
stars = serializers.IntegerField()
|
||
xpEarned = serializers.IntegerField()
|
||
coinsEarned = serializers.IntegerField()
|
||
stats = StatsSerializer()
|
||
|
||
|
||
# ---- Node progress (تکمیلشده / نصفهرها / در حال انجام) ----
|
||
|
||
class NodeProgressSerializer(serializers.Serializer):
|
||
nodeId = serializers.CharField(source='node_id')
|
||
nodeTitle = serializers.CharField(source='node.title')
|
||
unitId = serializers.CharField(source='node.unit_id')
|
||
status = serializers.ChoiceField(
|
||
choices=['not_started', 'in_progress', 'completed', 'abandoned'],
|
||
)
|
||
lessonsCompleted = serializers.IntegerField(source='lessons_completed_count')
|
||
lessonsTotal = serializers.SerializerMethodField()
|
||
startedAt = serializers.DateTimeField(source='started_at', allow_null=True)
|
||
completedAt = serializers.DateTimeField(source='completed_at', allow_null=True)
|
||
lastActivityAt = serializers.DateTimeField(source='last_activity_at', allow_null=True)
|
||
|
||
@extend_schema_field(serializers.IntegerField)
|
||
def get_lessonsTotal(self, obj):
|
||
return obj.node.lessons.count()
|
||
|
||
|
||
class NodeProgressListResponseSerializer(serializers.Serializer):
|
||
progress = NodeProgressSerializer(many=True)
|