Files
MathApp/content/serializers.py

266 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from rest_framework import serializers
from .models import Grade, LessonReport, QuestionReport
# ---- Grades ----
class GradeSerializer(serializers.ModelSerializer):
# بدونِ این، کاربر پایه‌ای را انتخاب می‌کند که هنوز هیچ محتوایی ندارد و
# صفحه‌ی خالی می‌بیند. فرانت با همین می‌تواند آن گزینه را غیرفعال کند.
lessonCount = serializers.IntegerField(read_only=True)
class Meta:
model = Grade
fields = ['id', 'number', 'title', 'icon', 'color', 'lessonCount']
class GradesListResponseSerializer(serializers.Serializer):
grades = GradeSerializer(many=True)
# ---- Home (skill map + stats) ----
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 کار می‌کنن).
# ⚠️ عضوِ اول همیشه nextLessonId است؛ بقیه به ترتیبِ کتاب. فرانتِ فعلی با
# کلیک روی جزیره lessonIds[0] را باز می‌کند، پس با همین ترتیب هر بار درسِ
# بعدی باز می‌شود — پیش‌تر همیشه درسِ اول باز می‌شد و جزیره هرگز تمام نمی‌شد.
lessonIds = serializers.ListField(
child=serializers.IntegerField(),
help_text='عضوِ اول = nextLessonId؛ بقیه به ترتیبِ کتاب.',
)
nextLessonId = serializers.IntegerField(
allow_null=True,
help_text=(
'درسی که با کلیک روی این جزیره باید باز شود: اولین درسِ تمام‌نشده‌ای '
'که سوال دارد؛ اگر همه تمام شده‌اند، اولین درس (برای مرور).'
),
)
class UnitWithNodesSerializer(serializers.Serializer):
id = serializers.CharField()
title = serializers.CharField()
blurb = serializers.CharField(allow_blank=True)
requiresSubscription = serializers.BooleanField(help_text='این کاربر برای بازیِ این فصل اشتراک لازم دارد (فقط فصلِ اولِ هر پایه رایگان است).')
nodes = NodeStatusSerializer(many=True)
class StatsSerializer(serializers.Serializer):
progressPercent = serializers.FloatField()
xp = serializers.IntegerField()
coins = serializers.IntegerField()
class SubjectSerializer(serializers.Serializer):
key = serializers.CharField(help_text='همان که به `?subject=` داده می‌شود.')
title = serializers.CharField()
icon = serializers.CharField(allow_blank=True, help_text='نامِ آیکنِ lucide.')
color = serializers.CharField(allow_blank=True)
order = serializers.IntegerField()
class HomeResponseSerializer(serializers.Serializer):
subject = SubjectSerializer(
allow_null=True,
help_text='کتابی که `units` از آن است؛ null فقط وقتی پایه هیچ فصلی ندارد.',
)
subjects = SubjectSerializer(
many=True,
help_text='همه‌ی کتاب‌های این پایه. فرانت نوارِ جابه‌جاییِ کتاب را فقط وقتی نشان می‌دهد '
'که بیش از یکی باشد.',
)
units = UnitWithNodesSerializer(many=True)
stats = StatsSerializer()
# ---- Lesson intro ----
class LessonIntroResponseSerializer(serializers.Serializer):
lessonId = serializers.CharField()
title = serializers.CharField()
introText = serializers.CharField()
mascotAnimation = serializers.CharField()
# تعدادِ سوال‌های آماده‌ی این درس. صفر یعنی محتوای آموزشی هست ولی آزمونش
# هنوز ساخته نشده؛ اپ باید «به‌زودی» نشان دهد نه یک کویزِ خالی. بدونِ این
# فیلد فرانت مجبور بود اول 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())
# شکل‌های ثابتِ درس؛ فرانت با `diagram` همان رندرِ دستیار را می‌کشد.
figures = serializers.ListField(child=serializers.DictField(), default=list)
# فقط برای 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 ----
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)
# شکلِ سوال (توصیفِ JSON مثلِ `LessonFigure.diagram`)؛ null یعنی شکل ندارد.
figure = serializers.JSONField(allow_null=True, required=False)
class QuizQuestionSerializer(serializers.Serializer):
"""
مثلِ QuestionSerializer است به‌علاوه‌ی اینکه سوال از کدام درس آمده.
چرا lessonId و lessonTitle: آزمونِ مبحث از چند درس نمونه برمی‌دارد، و
وقتی دانش‌آموز غلط جواب داد باید بشود گفت «این مهارت را مرور کن». بدونِ
این دو فیلد، فرانت فقط می‌داند «یک سوال غلط شد» و نمی‌داند کدام مبحثِ ریز.
"""
id = serializers.CharField()
lessonId = serializers.CharField()
lessonTitle = serializers.CharField()
type = serializers.CharField()
prompt = serializers.CharField()
options = ChoiceOptionSerializer(many=True)
figure = serializers.JSONField(allow_null=True, required=False)
class NodeQuizResponseSerializer(serializers.Serializer):
nodeId = serializers.CharField()
nodeTitle = serializers.CharField()
unitTitle = serializers.CharField()
# questionCount ممکن است از requested کمتر باشد؛ مبحث‌هایی هستند که هنوز
# سوالِ کافی ندارند و فرانت باید بتواند این را ببیند نه اینکه حدس بزند.
questionCount = serializers.IntegerField()
requested = serializers.IntegerField()
questions = QuizQuestionSerializer(many=True)
class NodeQuizCompleteRequestSerializer(serializers.Serializer):
# همان شکلِ lessons/{id}/complete تا فرانت یک الگو یاد بگیرد. سقفِ
# correctCount در view با تعدادِ واقعیِ سوال‌های مبحث چک می‌شود.
correctCount = serializers.IntegerField(min_value=0)
totalQuestions = serializers.IntegerField(min_value=1)
class NodeQuizCompleteResponseSerializer(serializers.Serializer):
nodeId = serializers.CharField()
stars = serializers.IntegerField()
xpEarned = serializers.IntegerField()
coinsEarned = serializers.IntegerField()
# جزیره‌ی بعدی که حالا باز است؛ null یعنی این آخرین جزیره‌ی پایه بود.
nextNodeId = serializers.CharField(allow_null=True)
stats = StatsSerializer()
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.IntegerField(source='lessons_total')
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)
class NodeProgressListResponseSerializer(serializers.Serializer):
progress = NodeProgressSerializer(many=True)
class QuestionReportRequestSerializer(serializers.Serializer):
"""دلیل اجباری‌ست تا گزارش‌ها قابلِ دسته‌بندی باشند؛ توضیح اختیاری."""
reason = serializers.ChoiceField(choices=QuestionReport.REASON_CHOICES)
note = serializers.CharField(required=False, allow_blank=True, max_length=1000)
class QuestionReportResponseSerializer(serializers.Serializer):
success = serializers.BooleanField()
reportId = serializers.IntegerField()
alreadyReported = serializers.BooleanField()
class LessonReportRequestSerializer(serializers.Serializer):
reason = serializers.ChoiceField(choices=LessonReport.REASON_CHOICES)
note = serializers.CharField(required=False, allow_blank=True, max_length=1000)