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

@@ -103,7 +103,9 @@ class HeartsGateTests(_World, TestCase):
def test_unlimited_hearts_from_a_pack_are_never_blocked(self):
self.hearts(0)
HeartsPass.objects.create(user_id=self.user.id, expires_at=timezone.now() + timedelta(days=7))
self.assertEqual(self.answer(self.questions[0], correct=False).status_code, 200)
# جزیره دو سوال دارد و پایان بی‌جواب پذیرفته نمی‌شود؛ قلبِ نامحدود هر دو جواب را می‌گیرد.
for question in self.questions:
self.assertEqual(self.answer(question, correct=False).status_code, 200)
self.assertEqual(self.finish_island().status_code, 200)
def test_after_the_wait_a_heart_comes_back_and_play_continues(self):
@@ -115,6 +117,9 @@ class HeartsGateTests(_World, TestCase):
def test_hearts_off_changes_nothing(self):
self.hearts(0)
with override_settings(HEARTS_ENFORCED=False):
# با قلبِ خاموش، قلبِ صفر هم جواب‌دادن را نمی‌بندد؛ پایانِ جزیره هم پس از جواب‌ها می‌رسد.
for question in self.questions:
self.assertEqual(self.answer(question, correct=True).status_code, 200)
self.assertEqual(self.finish_island().status_code, 200)