پیدا کردنِ «سوالهای فلان مبحث» در فهرستِ پیشفرضِ جنگو عملاً ممکن نبود: باید
بینِ چند صفحهی جدا با دراپداونهای هزارتایی بالا و پایین میرفتی.
/admin/content/question/browse/ چهار ستونِ آبشاری دارد — پایه → فصل → درس →
مبحث — و هر ستون فقط وقتی پر میشود که قبلی انتخاب شده باشد، پس هیچوقت
فهرستِ هزارتایی جلوی چشم نیست. تهاش سوالهای همان مبحث با گزینههایشان
میآیند و از همانجا ویرایش یا اضافه میشوند.
کنارِ هر سوال دو عددِ تصمیمساز: گزارشهای بازِ کاربران، و درصدِ پاسخِ غلط.
درصد زیرِ ۵ پاسخ نشان داده نمیشود — یک نمونهی کوچک سوالِ سالم را
«مشکلدار» جلوه میدهد. همین دو ستون به فهرستِ سوالها هم اضافه شدند.
content.QuestionReport و POST /api/questions/{id}/report تازهاند: تنها
بازخوردی که تا امروز جمع میشد مالِ چتِ دستیار بود، و روی سوالهای مسیرِ
آموزشی هیچ راهی نبود که کاربر بگوید گزینهی درست غلط است. گزارشِ بازِ تکراریِ
همان کاربر ردیفِ تازه نمیسازد تا دابلکلیک آمار را بههم نزند.
وابستگیِ تازهای اضافه نشد: چند اندپوینتِ کوچکِ JSON و JavaScript ساده.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
149 lines
5.8 KiB
Python
149 lines
5.8 KiB
Python
from drf_spectacular.utils import extend_schema_field
|
||
from rest_framework import serializers
|
||
|
||
from .models import Grade, QuestionReport
|
||
|
||
|
||
# ---- 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)
|
||
|
||
|
||
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()
|