Files
MathApp/content/serializers.py
Zareian ac1b1af398 open the API up to the frontend
سینیور خواست زودتر به فرانت‌اند وصل شویم تا از آنجا هم تست شود. این‌ها
سمتِ ما لازم بودند:

CORS با allowlist صریح (نه ALLOW_ALL): احرازِ هویتِ ما توکنِ Bearer در هدر
است، پس origin ناشناس نباید از مرورگرِ کاربر به API ما بزند.
CORS_ALLOW_CREDENTIALS عمداً خاموش می‌ماند — کوکی‌ای در کار نیست و روشن
کردنش فقط سطحِ حمله اضافه می‌کند. CorsMiddleware بالاتر از CommonMiddleware
گذاشته شد وگرنه ریدایرکتِ APPEND_SLASH و خطای ۴۰۰ هدرِ CORS نمی‌گیرند و در
مرورگر به‌شکلِ خطای مبهمِ شبکه دیده می‌شوند.

PATCH /api/user/profile اضافه شد؛ تا پیش از این هیچ راهی برای ثبتِ نامِ
کاربر نبود. GET /api/home بدونِ پارامتر حالا پایه‌ی ذخیره‌شده‌ی کاربر را
برمی‌گرداند و POST /api/ai/ask فقط با lessonId کار می‌کند — هر دو برای
اینکه فرانت لازم نباشد پایه را خودش نگه دارد. questionCount به
lessons/{id}/intro اضافه شد.

docs/frontend/api.ts و README.md کلاینتِ تایپ‌دار و راهنمای اتصال‌اند، از
روی OpenAPI واقعیِ همین سرویس تولید شده‌اند.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 19:01:06 +03:30

136 lines
5.2 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 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()
# تعدادِ سوال‌های آماده‌ی این درس. صفر یعنی محتوای آموزشی هست ولی آزمونش
# هنوز ساخته نشده؛ اپ باید «به‌زودی» نشان دهد نه یک کویزِ خالی. بدونِ این
# فیلد فرانت مجبور بود اول questions را بگیرد تا بفهمد چیزی هست یا نه، و
# `complete` هم totalQuestions=0 را رد می‌کند (درست است، ولی گیج‌کننده).
questionCount = serializers.IntegerField()
# ---- 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)