صفحههای فرانت خوانده شدند و با API مقایسه شدند. ساختارِ ما از کتابِ واقعی
درآمده و درستتر است، پس مدل عوض نشد — ولی چند چیز کم داشت.
مهمترینش یک باگ بود: /api/home پارامتر را `grade` میخواند و کلاینتی که به
فرانت فرستادیم `gradeId` میفرستد. پارامترِ ناشناخته بیصدا نادیده گرفته
میشد، یعنی کاربر پایهای را انتخاب میکرد و نقشهی پایهی دیگری میدید
بدونِ هیچ خطایی. حالا هر دو نام پذیرفته میشوند (`gradeId` نامِ درست است،
با بقیهی API یکی). تستِ سرتاسریِ قبلی این را نگرفته بود چون home را بدونِ
آرگومان صدا زده بود.
/api/grades حالا lessonCount میدهد. فهرستِ پایههای فرانت تا دوازدهم است
ولی محتوای ما تا نهم؛ بدونِ این عدد، کاربر پایهی خالی را انتخاب میکند و
صفحهی خالی میبیند بیآنکه بفهمد چرا.
GET /api/nodes/{id}/lessons اضافه شد. /api/home فقط شناسهی درسها را
میدهد، ولی نقشهی مهارت برای هر قدم عنوان، نوع، زیرنویس و «انجام شد» لازم
دارد — بدونش فرانت باید برای هر درس یک intro جدا میگرفت.
Lesson حالا kind (آزمون/ویدیو) و متنِ سهتکه دارد: توضیح، مثال، حلِ
گامبهگام. ویدیو از قبل سوالِ بازِ محصول بود و فرانت آیتمش را دارد؛ حالا
API میتواند بیانش کند، حتی اگر محتوایش بعداً بیاید. Unit یک blurb گرفت و
Node آیکون و رنگ، تا فرانت رنگها را هاردکد نکند. هیچکدام اجباری نیست، پس
محتوای موجود دستنخورده کار میکند.
PATCH /api/user/profile حالا age را هم میپذیرد (صفحهی ثبتنامِ فرانت سن
میپرسد). ذخیرهسازی همچنان birthYear است چون کهنه نمیشود؛ این فقط ورودی
را ترجمه میکند تا دو طرف حسابِ متفاوت درنیاورند.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
پیدا کردنِ «سوالهای فلان مبحث» در فهرستِ پیشفرضِ جنگو عملاً ممکن نبود: باید
بینِ چند صفحهی جدا با دراپداونهای هزارتایی بالا و پایین میرفتی.
/admin/content/question/browse/ چهار ستونِ آبشاری دارد — پایه → فصل → درس →
مبحث — و هر ستون فقط وقتی پر میشود که قبلی انتخاب شده باشد، پس هیچوقت
فهرستِ هزارتایی جلوی چشم نیست. تهاش سوالهای همان مبحث با گزینههایشان
میآیند و از همانجا ویرایش یا اضافه میشوند.
کنارِ هر سوال دو عددِ تصمیمساز: گزارشهای بازِ کاربران، و درصدِ پاسخِ غلط.
درصد زیرِ ۵ پاسخ نشان داده نمیشود — یک نمونهی کوچک سوالِ سالم را
«مشکلدار» جلوه میدهد. همین دو ستون به فهرستِ سوالها هم اضافه شدند.
content.QuestionReport و POST /api/questions/{id}/report تازهاند: تنها
بازخوردی که تا امروز جمع میشد مالِ چتِ دستیار بود، و روی سوالهای مسیرِ
آموزشی هیچ راهی نبود که کاربر بگوید گزینهی درست غلط است. گزارشِ بازِ تکراریِ
همان کاربر ردیفِ تازه نمیسازد تا دابلکلیک آمار را بههم نزند.
وابستگیِ تازهای اضافه نشد: چند اندپوینتِ کوچکِ JSON و JavaScript ساده.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
سینیور خواست زودتر به فرانتاند وصل شویم تا از آنجا هم تست شود. اینها
سمتِ ما لازم بودند:
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>
next_refill_at was set on UserHearts but never read or recalculated,
so a heart lost to zero never came back. UserHearts.apply_regen() now
catches up missed 24h intervals on read/deduct, and lose_heart() starts
the timer only when it isn't already running.
Any user with an active (non-expired) subscription now skips heart
deduction entirely, regardless of which plan they're on — good enough
for now until per-plan perks are needed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The skill-map response gave units -> nodes with id/title/status but no way
to map a node to its lessons, and no other endpoint fills that gap
(/api/progress/nodes doesn't either). Any client rendering the map needs
this to know which lesson to open when a node is tapped.
The ids are already computed in the cached _grade_tree; this just surfaces
them. Additive and backward-compatible; content tests pass.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
GET /api/home is the most-hit authenticated endpoint (every app open) and
was running one COUNT per node. Now it is a flat number of queries
regardless of grade size, and the shared curriculum skeleton is served
from cache; per-user progress/stats stay live.
content/views.py
- _node_status(): takes a precomputed set of completed lesson_ids and a
node's lesson_ids instead of firing its own COUNT query.
- HomeView.get(): one query for all completed lesson_ids of the user in
the grade, status computed in Python; grade tree (units -> nodes ->
lesson_ids, no per-user data) read from cache
(content:tree:grade:{id}:v{N}); missing grade -> 404 (was an
unhandled 500); grade with no units -> empty units, not 404.
- GradesListView: served from cache (content:grades:v{N}).
- curriculum_version() / bump_curriculum_version(): one shared version
key; bumping it invalidates every content cache key at once.
- both GET views get throttle_scope='read' + GuestAwareContentThrottle.
content/receivers.py
- post_save/post_delete on Grade/Unit/Node/Lesson -> bump_curriculum_version().
content/tests.py
- HomeViewScalabilityTests: query count flat for 2 vs 20 nodes; 2nd call
hits cache (no content_unit/node SQL); a new Node invalidates the tree;
missing grade 404; empty grade 200.
- GradesListCacheTests: list cached, invalidated on new Grade.
- ReadThrottleTests: /api/home returns 429 past the limit.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The math-app frontend prototype drives a RobotMascot with a fixed set of
moods; this gives that animation an explicit backend signal. Complements
the existing Lesson.mascot_animation / LessonIntro.mascotAnimation, which
are unchanged.
serializers.py
- AnswerResponseSerializer: new mascotMood field.
views.py
- QuestionAnswerView: mascotMood = 'happy' for a correct answer else
'encourage', on both the fresh response and the idempotent-replay
response.
tests.py
- QuestionAnswerMascotMoodTests: correct -> happy, wrong -> encourage,
and the mood is preserved across an idempotent replay.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>