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

27
content/testing.py Normal file
View File

@@ -0,0 +1,27 @@
"""کمک‌تابعِ تست‌ها: جوابِ ثبت‌شده مثلِ اپ (اپ هر سوال را جدا با /answer ثبت می‌کند و بعد پایان را می‌زند)."""
from .models import Choice, Question, UserAnswer
def record_answers(user_id, *, lesson=None, node=None, correct=True):
"""
به همه‌ی سوال‌های یک درس یا یک جزیره جوابِ ثبت‌شده می‌دهد (مستقیم در DB، بی‌قلب و بی‌throttle).
`correct=True` گزینه‌ی درست، `False` گزینه‌ی غلط را می‌زند؛ سوالی که آن گزینه را ندارد
گزینه‌اش ساخته می‌شود. سوالی که پیش‌تر جواب داده شده دست نمی‌خورد. تعدادِ سوال‌های جوابِ‌داده‌شده را برمی‌گرداند.
"""
questions = Question.objects.all()
if lesson is not None:
questions = questions.filter(lesson=lesson)
if node is not None:
questions = questions.filter(lesson__node=node)
count = 0
for question in questions.prefetch_related('choices'):
choice = next((c for c in question.choices.all() if c.is_correct == correct), None)
if choice is None:
choice = Choice.objects.create(
question=question, value='درست' if correct else 'غلط', is_correct=correct)
UserAnswer.objects.get_or_create(
user_id=user_id, question=question,
defaults={'choice': choice, 'correct': correct, 'xp_earned': 10 if correct else 0},
)
count += 1
return count