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)