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

@@ -15,6 +15,7 @@ from accounts.authentication import issue_access_token
from accounts.models import User
from .models import Choice, Grade, Lesson, Node, Question, Unit
from .testing import record_answers
class IslandProgressTests(TestCase):
@@ -29,9 +30,9 @@ class IslandProgressTests(TestCase):
self.b = self._lesson(self.first, 'ب', with_question=True)
self._lesson(self.second, 'ج', with_question=True)
user = User.objects.create(phone='09120000950', grade_id=grade.id)
self.user = User.objects.create(phone='09120000950', grade_id=grade.id)
self.client = APIClient()
self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {issue_access_token(user.id)}')
self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {issue_access_token(self.user.id)}')
def tearDown(self):
cache.clear()
@@ -52,6 +53,7 @@ class IslandProgressTests(TestCase):
return {n['title']: n for n in units[0]['nodes']}
def _complete(self, lesson):
record_answers(self.user.id, lesson=lesson) # مثلِ اپ: اول جواب، بعد پایان
response = self.client.post(f'/api/lessons/{lesson.id}/complete',
{'correctCount': 1, 'totalQuestions': 1}, format='json')
self.assertEqual(response.status_code, 200, response.content)