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>
This commit is contained in:
2026-09-13 19:01:06 +03:30
parent fa56308a35
commit ac1b1af398
8 changed files with 697 additions and 7 deletions

View File

@@ -51,6 +51,11 @@ class LessonIntroResponseSerializer(serializers.Serializer):
title = serializers.CharField()
introText = serializers.CharField()
mascotAnimation = serializers.CharField()
# تعدادِ سوال‌های آماده‌ی این درس. صفر یعنی محتوای آموزشی هست ولی آزمونش
# هنوز ساخته نشده؛ اپ باید «به‌زودی» نشان دهد نه یک کویزِ خالی. بدونِ این
# فیلد فرانت مجبور بود اول questions را بگیرد تا بفهمد چیزی هست یا نه، و
# `complete` هم totalQuestions=0 را رد می‌کند (درست است، ولی گیج‌کننده).
questionCount = serializers.IntegerField()
# ---- Lesson questions ----

View File

@@ -153,17 +153,31 @@ class HomeView(APIView):
tags=['Curriculum (content)'],
)
def get(self, request):
grade_id = request.query_params.get('grade')
user_id = request.user.id
# ⚠️ پیش‌تر فقط پارامترِ صریحِ ?grade= خوانده می‌شد و پایه‌ی ذخیره‌شده‌ی
# خودِ کاربر نادیده می‌ماند؛ نتیجه این بود که اپ در «آشنایی» پایه را ثبت
# می‌کرد و بعد صفحه‌ی خانه خالی برمی‌گشت. حالا پارامتر اولویت دارد (برای
# مرورِ پایه‌ی دیگر) و در نبودش پایه‌ی خودِ کاربر ملاک است.
grade_param = request.query_params.get('grade')
if grade_param:
try:
grade_id = int(grade_param)
except (TypeError, ValueError):
raise Http404
else:
grade_id = request.user.grade_id
tree = []
if grade_id:
try:
tree = _grade_tree(int(grade_id))
except (TypeError, ValueError):
raise Http404
tree = _grade_tree(grade_id)
if tree is None:
raise Http404
# پایه‌ای که کاربر ذخیره کرده ممکن است بعداً حذف شده باشد؛ آن
# حالت خطا نیست، فقط نقشه‌ی خالی است. ولی ?grade=ی که کاربر
# صریحاً خواسته و وجود ندارد، ۴۰۴ است.
if grade_param:
raise Http404
tree = []
# همه‌ی lesson_idهای کامل‌شده‌ی این کاربر در همین گرید — یک کوئری،
# مستقل از تعدادِ node (رفعِ N+1).
@@ -214,6 +228,7 @@ class LessonIntroView(APIView):
'title': lesson.title,
'introText': lesson.intro_text,
'mascotAnimation': lesson.mascot_animation,
'questionCount': lesson.questions.count(),
}
return Response(LessonIntroResponseSerializer(data).data)