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

@@ -218,3 +218,25 @@ class UnicodeInputsAreNormalisedOrRejectedTests(TestCase):
self.assertEqual(clean('علی رضایی'), 'علی رضایی')
class StaffCannotMintLoginsTests(TestCase):
"""کارمندِ غیرابرکاربر با اجازه‌ی add نمی‌تواند OTPCode یا Session دستی بسازد (= ورود به هر شماره)."""
def setUp(self):
from django.contrib.auth.models import Permission, User as AuthUser
self.staff = AuthUser.objects.create_user('staff1', 's@x.c', 'pw12345!', is_staff=True)
self.staff.user_permissions.set(Permission.objects.filter(
codename__in=['add_otpcode', 'change_otpcode', 'view_otpcode',
'add_session', 'change_session', 'view_session']))
self.root = AuthUser.objects.create_superuser('root1', 'r@x.c', 'pw12345!')
def test_staff_gets_403_on_add_pages(self):
self.client.force_login(self.staff)
for model in ('otpcode', 'session'):
self.assertEqual(self.client.get(f'/admin/accounts/{model}/add/').status_code, 403, model)
def test_superuser_can_still_add(self):
self.client.force_login(self.root)
for model in ('otpcode', 'session'):
self.assertEqual(self.client.get(f'/admin/accounts/{model}/add/').status_code, 200, model)