Finishing a lesson or island needs recorded answers and an unlocked island; auto DB backup before migrate; throttles on 15 more routes

- content: POST nodes/{id}/quiz/complete and lessons/{id}/complete refuse a locked island
  (403 node_locked, same rule as /api/home) and a user who has not answered every question
  (409 answers_missing). Before, only a user with zero hearts and no answers was refused, so
  anyone else finished any island, locked or not, and finishing a locked island's lessons
  unlocked it. A node or lesson with no questions still passes when unlocked, with no XP.
  The client can no longer understate totalQuestions to buy stars.
- throttle_scope on 15 routes that had none (question bank without login, intro, hearts,
  notifications, tickets, plans, subscription status, orders, offers, affiliate, assistant
  history and feedback, logout).
- admin: OTPCode and Session can only be added/changed by a superuser.
- deploy.sh: integrity-checked DB backup before migrate (keeps the last 2); no migrate without it.
- docs and CLAUDE.md updated to say what is verified and what is not.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-07 13:30:24 +03:30
parent 7e3854098f
commit 7ecb337263
26 changed files with 688 additions and 69 deletions

View File

@@ -298,7 +298,12 @@ grades 8–12 does; the 39 that fall short are all in grades 3/6/7.
Finishing the quiz finishes the island: `POST /api/nodes/{id}/quiz/complete` marks the node
completed whatever the score (the owner's call on 2026-09-24: keep it simple) and returns
`nextNodeId`. Before it existed, a node only completed when every one of its 8–13 lessons
`nextNodeId`. Since 2026-10-07 it refuses (in code; deployed only after the next release) a node
that is locked on the map (403 `node_locked`, same rule as `/api/home`, `content.views.node_is_unlocked`),
and a user who has not answered every quiz question (409 `answers_missing`;
the required count is `max(client totalQuestions, min(4, questions in node))`, so the client cannot
understate it). A node or lesson with no questions has nothing to answer: it passes when unlocked (no stars, no XP) so it does not block the chain, and is still refused when locked. `POST /api/lessons/{id}/complete` has the same lock and answer rules. Before that,
only a user with zero hearts and no answers was refused; anyone else finished any island, locked or not. Before it existed, a node only completed when every one of its 8–13 lessons
was completed one by one, so a student playing node quizzes was stuck after the first
island of every unit. Details in `docs/architecture.md`.
@@ -821,10 +826,10 @@ migrationها اعمال شدند، سه‌یار و `nextLessonId` از بیر
(`templates/tadrisino/`) تا متنِ فارسی راست‌به‌چپ رندر شود. قالبِ drf-spectacular
دست‌نخورده است و با `{{ block.super }}` گسترش داده شده.
**بکاپ هنوز خودکار نیست.** `deploy.sh` پیش از `migrate` بکاپ نمی‌گیرد؛ روشِ دستی در
`docs/operations.md` است و در سه دیپلویِ آن روز دستی گرفته شد. خودکار کردنش
ارزشمندترین کارِ بعدی‌ست. `sqlite3` روی سرور نصب نیست — با `venv/bin/python` و
`sqlite3.Connection.backup()` بکاپ بگیر، نه با `cp`.
**بکاپ:** `deploy.sh` پیش از `migrate` خودش بکاپ می‌گیرد (`db-before-deploy-*`، ۲ تای آخر،
بی‌بکاپ migrate نمی‌شود)؛ فقط روی لپ‌تاپ آزموده شد و روی سرور هنوز اجرا نشده. بیرون از ماشین بکاپی
نیست. `sqlite3` روی سرور نصب نیست — برای بکاپِ دستی با `venv/bin/python` و
`sqlite3.Connection.backup()` بگیر، نه با `cp`.
بخش‌های تاریخیِ زیر به‌ترتیبِ زمان نگه داشته شده‌اند چون درس‌هایشان هنوز معتبر است؛
**اعدادِ محتوایی‌شان کهنه‌اند و ملاک همین بخش است.**