let a finished island quiz open the next island, and ground ask-AI in the student's book

The frontend plays node quizzes, but a node only completed once every one of its
8-13 lessons was completed separately, so students were stuck after the first
island of each unit. POST /api/nodes/{id}/quiz/complete now completes the node
(any score; score only sets stars/xp) and returns nextNodeId.

Also closes the other backend gaps found while auditing the frontend:
- /api/ai/ask without lessonId now picks the book from the user's grade instead
  of skipping retrieval, so the "ask Pi" tab is grounded in the textbook.
- guest sessions return a refreshToken; they used to go silently 401 after 1h.
- lesson_complete throttle 10/hour -> 120/hour (a lesson here is 1-2 questions).
- progressPercent is per grade, not over all 2,617 lessons of every grade.
- a one-year plan (299,000 toman) is created by migration when no plan exists.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-24 02:49:02 +03:30
parent 0c6b08c9ff
commit b32194497c
21 changed files with 803 additions and 12 deletions

View File

@@ -83,3 +83,24 @@ def resolve_from_lesson(lesson_id) -> dict:
unit_title, grade_title,
)
return {'gradeTitle': grade_title}
def resolve_from_grade(grade_id) -> dict:
"""
{'subject', 'gradeTitle'} برای وقتی که هیچ درسی در کار نیست.
صفحه‌ی «از پی بپرس» از نوارِ پایینِ اپ باز می‌شود، نه از داخلِ یک درس؛
پس نه lessonId دارد نه subject. بدونِ این تابع retrieval کلاً رد می‌شد و
همه‌ی جواب‌های آن صفحه عمومی بودند. هر پایه دقیقاً یک کتابِ ریاضی دارد،
پس پایه‌ی خودِ کاربر برای انتخابِ کتاب کافی‌ست؛ فصل معلوم نیست و جستجو
روی کلِ همان کتاب انجام می‌شود.
"""
from .config import resolve_grade_title
grade_title = resolve_grade_title(grade_id)
if not grade_title:
return {}
for subject, grade in MATH_CHAPTERS:
if grade == grade_title:
return {'subject': subject, 'gradeTitle': grade_title}
return {'gradeTitle': grade_title}