Remove the guest system entirely: no is_guest/device_id, no guest throttles, phone login is the only way in

accounts.0008 drops the columns and refuses to run while guest rows exist.
Docs describe the current state; tests that only proved guest behaviour are gone.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-05 18:20:10 +03:30
parent 8835e01595
commit 663252a260
61 changed files with 357 additions and 1518 deletions

View File

@@ -20,7 +20,7 @@ class LessonCompleteImpossibleCountTests(TestCase):
def setUp(self):
self.client = APIClient()
self.user = User.objects.create(device_id='device-complete-1', is_guest=True, name='')
self.user = User.objects.create(phone='09000000011', name='')
self.token = issue_access_token(self.user.id)
grade = Grade.objects.create(title='پایه اول', icon='icon', color='#fff')
@@ -79,7 +79,7 @@ class LessonCompleteRatioIgnoresClientTotalQuestionsTests(TestCase):
def setUp(self):
self.client = APIClient()
self.user = User.objects.create(device_id='device-complete-2', is_guest=True, name='')
self.user = User.objects.create(phone='09000000012', name='')
self.token = issue_access_token(self.user.id)
grade = Grade.objects.create(title='پایه دوم', icon='icon', color='#fff')
@@ -130,7 +130,7 @@ class QuestionAnswerThrottleTests(TestCase):
from django.core.cache import cache
cache.clear()
self.client = APIClient()
self.user = User.objects.create(device_id='device-answer-throttle-1', is_guest=True, name='')
self.user = User.objects.create(phone='09000000013', name='')
self.token = issue_access_token(self.user.id)
grade = Grade.objects.create(title='پایه اول', icon='icon', color='#fff')
@@ -173,13 +173,8 @@ class QuestionAnswerThrottleTests(TestCase):
self.assertEqual(blocked_response.status_code, 429)
def test_changing_client_ip_does_not_bypass_the_user_limit(self):
# ⚠️ توجه: از یک کاربرِ *غیرمهمان* استفاده می‌شه (نه self.user که
# مهمان است)، چون طبقِ فیکسِ GuestAwareContentThrottle حالا کلیدِ
# bucket برای کاربرهای مهمان عمداً بر اساسِ IP است (نگاه کنید به
# QuestionAnswerGuestThrottleBypassTests پایین‌تر) — این تست فقط
# رفتارِ per-user (نه IP) برای کاربرهایِ ثبت‌نام‌شده رو تضمین
# می‌کنه که باید دقیقاً مثلِ قبل دست‌نخورده بمونه.
registered_user = User.objects.create(is_guest=False, name='', phone='+989120000701')
# کلیدِ throttle شناسه‌ی کاربر است، نه IP؛ تغییرِ IP سطلِ تازه نمی‌دهد.
registered_user = User.objects.create(name='', phone='+989120000701')
registered_token = issue_access_token(registered_user.id)
def _answer_as(remote_addr):
@@ -253,7 +248,7 @@ class LessonCompleteThrottleTests(TestCase):
from django.core.cache import cache
cache.clear()
self.client = APIClient()
self.user = User.objects.create(device_id='device-complete-throttle-1', is_guest=True, name='')
self.user = User.objects.create(phone='09000000014', name='')
self.token = issue_access_token(self.user.id)
grade = Grade.objects.create(title='پایه اول', icon='icon', color='#fff')
@@ -294,13 +289,8 @@ class LessonCompleteThrottleTests(TestCase):
self.assertEqual(blocked_response.status_code, 429)
def test_changing_client_ip_does_not_bypass_the_user_limit(self):
# ⚠️ توجه: از یک کاربرِ *غیرمهمان* استفاده می‌شه (نه self.user که
# مهمان است)، چون طبقِ فیکسِ GuestAwareContentThrottle حالا کلیدِ
# bucket برای کاربرهای مهمان عمداً بر اساسِ IP است (نگاه کنید به
# LessonCompleteGuestThrottleBypassTests پایین‌تر) — این تست فقط
# رفتارِ per-user (نه IP) برای کاربرهایِ ثبت‌نام‌شده رو تضمین
# می‌کنه که باید دقیقاً مثلِ قبل دست‌نخورده بمونه.
registered_user = User.objects.create(is_guest=False, name='', phone='+989120000702')
# کلیدِ throttle شناسه‌ی کاربر است، نه IP؛ تغییرِ IP سطلِ تازه نمی‌دهد.
registered_user = User.objects.create(name='', phone='+989120000702')
registered_token = issue_access_token(registered_user.id)
def _complete_as(remote_addr):
@@ -358,10 +348,8 @@ class LessonCompleteThrottleTests(TestCase):
self.assertEqual(progress_after.last_activity_at, progress_snapshot['last_activity_at'])
class QuestionAnswerGuestThrottleBypassTests(TestCase):
"""اثباتِ فیکسِ throttle: چرخشِ حساب مهمان (هر deviceId یه pk تازه) نباید
bucket تازه‌ی question_answer بده — GuestAwareContentThrottle برای
مهمان‌ها کلید رو بر اساسِ IP می‌سازه، نه user.pk."""
class QuestionAnswerPerUserThrottleTests(TestCase):
"""هر کاربرِ ثبت‌نام‌شده سطلِ throttleِ جدای خودش را دارد؛ مصرفِ حسابِ دیگر به او نمی‌رسد."""
def setUp(self):
from django.core.cache import cache
@@ -387,58 +375,23 @@ class QuestionAnswerGuestThrottleBypassTests(TestCase):
HTTP_AUTHORIZATION=f'Bearer {token}',
)
def test_rotating_guest_accounts_share_the_same_throttle_bucket(self):
# سه حساب مهمانِ متفاوت (شبیه‌سازیِ چرخشِ حساب برای دورزدنِ throttle)،
# همه از همون کلاینت/IP — باید همه یک bucket مشترک رو مصرف کنند.
with patch.dict(ScopedRateThrottle.THROTTLE_RATES, {'question_answer': '2/min'}):
guest1 = User.objects.create(device_id='device-answer-guest-rot-1', is_guest=True, name='')
response1 = self._answer(issue_access_token(guest1.id))
self.assertEqual(response1.status_code, 200)
guest2 = User.objects.create(device_id='device-answer-guest-rot-2', is_guest=True, name='')
response2 = self._answer(issue_access_token(guest2.id))
self.assertEqual(response2.status_code, 200)
# سومین حسابِ مهمانِ تازه (pk جدید) باید throttle بشه، چون
# bucket بر اساسِ IP به‌اشتراک گذاشته شده، نه user.pk.
guest3 = User.objects.create(device_id='device-answer-guest-rot-3', is_guest=True, name='')
response3 = self._answer(issue_access_token(guest3.id))
self.assertEqual(response3.status_code, 429)
def test_registered_users_keep_independent_per_user_buckets(self):
# عدمِ رگرسیون: کاربرهای غیرمهمان (ثبت‌نام‌شده با شماره) باید مثلِ
# قبل هرکدوم bucket جداگانه‌ی خودشون رو داشته باشن.
with patch.dict(ScopedRateThrottle.THROTTLE_RATES, {'question_answer': '1/min'}):
user_a = User.objects.create(is_guest=False, name='', phone='+989120000711')
user_a = User.objects.create(name='', phone='+989120000711')
response_a = self._answer(issue_access_token(user_a.id))
self.assertEqual(response_a.status_code, 200)
user_b = User.objects.create(is_guest=False, name='', phone='+989120000712')
user_b = User.objects.create(name='', phone='+989120000712')
response_b = self._answer(issue_access_token(user_b.id))
self.assertEqual(response_b.status_code, 200)
def test_blocked_guest_request_creates_no_answer_or_hearts_change(self):
# درخواستِ بلاک‌شده (از حسابِ مهمانِ چرخشی) نباید UserAnswer بسازه
# یا هارت/progress رو تغییر بده.
with patch.dict(ScopedRateThrottle.THROTTLE_RATES, {'question_answer': '1/min'}):
guest1 = User.objects.create(device_id='device-answer-guest-rot-4', is_guest=True, name='')
first_response = self._answer(issue_access_token(guest1.id))
self.assertEqual(first_response.status_code, 200)
guest2 = User.objects.create(device_id='device-answer-guest-rot-5', is_guest=True, name='')
blocked_response = self._answer(issue_access_token(guest2.id))
self.assertEqual(blocked_response.status_code, 429)
self.assertEqual(UserAnswer.objects.filter(user_id=guest2.id).count(), 0)
self.assertFalse(UserHearts.objects.filter(user_id=guest2.id).exists())
class LessonCompleteGuestThrottleBypassTests(TestCase):
"""اثباتِ فیکسِ throttle: چرخشِ حساب مهمان نباید bucket تازه‌ی
lesson_complete بده — GuestAwareContentThrottle برای مهمان‌ها کلید رو
بر اساسِ IP می‌سازه، نه user.pk."""
class LessonCompletePerUserThrottleTests(TestCase):
"""هر کاربرِ ثبت‌نام‌شده سطلِ throttleِ جدای خودش را دارد؛ مصرفِ حسابِ دیگر به او نمی‌رسد."""
def setUp(self):
from django.core.cache import cache
@@ -462,54 +415,20 @@ class LessonCompleteGuestThrottleBypassTests(TestCase):
HTTP_AUTHORIZATION=f'Bearer {token}',
)
def test_rotating_guest_accounts_share_the_same_throttle_bucket(self):
# سه حساب مهمانِ متفاوت (شبیه‌سازیِ چرخشِ حساب برای دورزدنِ throttle)،
# همه از همون کلاینت/IP — باید همه یک bucket مشترک رو مصرف کنند.
with patch.dict(ScopedRateThrottle.THROTTLE_RATES, {'lesson_complete': '2/hour'}):
guest1 = User.objects.create(device_id='device-complete-guest-rot-1', is_guest=True, name='')
response1 = self._complete(issue_access_token(guest1.id))
self.assertEqual(response1.status_code, 200)
guest2 = User.objects.create(device_id='device-complete-guest-rot-2', is_guest=True, name='')
response2 = self._complete(issue_access_token(guest2.id))
self.assertEqual(response2.status_code, 200)
# سومین حسابِ مهمانِ تازه (pk جدید) باید throttle بشه، چون
# bucket بر اساسِ IP به‌اشتراک گذاشته شده، نه user.pk.
guest3 = User.objects.create(device_id='device-complete-guest-rot-3', is_guest=True, name='')
response3 = self._complete(issue_access_token(guest3.id))
self.assertEqual(response3.status_code, 429)
def test_registered_users_keep_independent_per_user_buckets(self):
# عدمِ رگرسیون: کاربرهای غیرمهمان (ثبت‌نام‌شده با شماره) باید مثلِ
# قبل هرکدوم bucket جداگانه‌ی خودشون رو داشته باشن.
with patch.dict(ScopedRateThrottle.THROTTLE_RATES, {'lesson_complete': '1/hour'}):
user_a = User.objects.create(is_guest=False, name='', phone='+989120000721')
user_a = User.objects.create(name='', phone='+989120000721')
response_a = self._complete(issue_access_token(user_a.id))
self.assertEqual(response_a.status_code, 200)
user_b = User.objects.create(is_guest=False, name='', phone='+989120000722')
user_b = User.objects.create(name='', phone='+989120000722')
response_b = self._complete(issue_access_token(user_b.id))
self.assertEqual(response_b.status_code, 200)
def test_blocked_guest_request_creates_no_completion_or_reward_side_effects(self):
# درخواستِ بلاک‌شده (از حسابِ مهمانِ چرخشی) نباید UserLessonCompletion
# بسازه یا UserStats/NodeProgress رو تغییر بده.
with patch.dict(ScopedRateThrottle.THROTTLE_RATES, {'lesson_complete': '1/hour'}):
guest1 = User.objects.create(device_id='device-complete-guest-rot-4', is_guest=True, name='')
first_response = self._complete(issue_access_token(guest1.id))
self.assertEqual(first_response.status_code, 200)
guest2 = User.objects.create(device_id='device-complete-guest-rot-5', is_guest=True, name='')
blocked_response = self._complete(issue_access_token(guest2.id))
self.assertEqual(blocked_response.status_code, 429)
self.assertEqual(UserLessonCompletion.objects.filter(user_id=guest2.id).count(), 0)
self.assertFalse(UserStats.objects.filter(user_id=guest2.id).exists())
self.assertFalse(NodeProgress.objects.filter(user_id=guest2.id, node=self.node).exists())
class QuestionAnswerMascotMoodTests(TestCase):
"""پاسخِ /answer باید mascotMood بده: درست → happy، غلط → encourage
@@ -519,7 +438,7 @@ class QuestionAnswerMascotMoodTests(TestCase):
from django.core.cache import cache
cache.clear()
self.client = APIClient()
self.user = User.objects.create(device_id='device-mascot-1', is_guest=True, name='')
self.user = User.objects.create(phone='09000000015', name='')
self.token = issue_access_token(self.user.id)
grade = Grade.objects.create(title='پایه اول', icon='i', color='#fff')
unit = Unit.objects.create(grade=grade, title='فصل ۱', order=1)
@@ -574,7 +493,7 @@ class HomeViewScalabilityTests(TestCase):
def setUp(self):
_django_cache.clear()
self.client = APIClient()
self.user = User.objects.create(device_id='dev-home-scale', is_guest=True, name='')
self.user = User.objects.create(phone='09000000016', name='')
self.token = issue_access_token(self.user.id)
def _grade_with_nodes(self, n_nodes):
@@ -661,7 +580,7 @@ class UnlimitedHeartsSubscriptionTests(TestCase):
def setUp(self):
self.client = APIClient()
self.user = User.objects.create(device_id='dev-unlimited-hearts', is_guest=True, name='')
self.user = User.objects.create(phone='09000000017', name='')
self.token = issue_access_token(self.user.id)
grade = Grade.objects.create(title='پایه اول', icon='icon', color='#fff')
@@ -717,7 +636,7 @@ class ReadThrottleTests(TestCase):
def setUp(self):
_django_cache.clear()
self.client = APIClient()
self.user = User.objects.create(device_id='dev-read-throttle', is_guest=True, name='')
self.user = User.objects.create(phone='09000000018', name='')
self.token = issue_access_token(self.user.id)
self.grade = Grade.objects.create(title='g', icon='i', color='#fff')
@@ -742,7 +661,7 @@ class HomeGradeFallbackTests(TestCase):
def setUp(self):
_django_cache.clear()
self.client = APIClient()
self.user = User.objects.create(device_id='dev-home-fallback', is_guest=True, name='')
self.user = User.objects.create(phone='09000000019', name='')
self.token = issue_access_token(self.user.id)
self.grade = Grade.objects.create(title='پایه نهم', icon='grade-9', color='#FF9800')
@@ -805,7 +724,7 @@ class NodeProgressUnitParamTests(TestCase):
from accounts.authentication import issue_access_token
from accounts.models import User
user = User.objects.create(device_id='unit-param', is_guest=True)
user = User.objects.create(phone='09000000020')
response = APIClient().get('/api/progress/nodes?unit=abc',
HTTP_AUTHORIZATION=f'Bearer {issue_access_token(user.id)}')
self.assertEqual(response.status_code, 400)

View File

@@ -13,7 +13,7 @@ from django.test import TestCase
from rest_framework.test import APIClient
from accounts.models import User
from accounts.testing import guest_session
from accounts.authentication import issue_access_token
from content.models import Choice, Grade, Lesson, Node, Question, Unit
@@ -46,8 +46,8 @@ class BasicLearningFlowTests(TestCase):
self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {token}')
def test_a_new_user_can_go_from_signup_to_finishing_a_lesson(self):
# ۱. حسابِ مهمانِ قدیمی (ورودِ مهمان حذف شده؛ این ردیف‌ها هنوز هستند).
session = guest_session('flow-device-1')
# ۱. کاربرِ ثبت‌نام‌شده با شماره (توکنِ ورود).
session = {'token': issue_access_token(User.objects.create(phone='09000000101', name='').id)}
self._auth(session['token'])
# ۲. صفحه‌ی «بیا باهم آشنا بشیم»: اسم + پایه.
@@ -110,7 +110,7 @@ class BasicLearningFlowTests(TestCase):
def test_completing_twice_with_the_same_key_does_not_double_count(self):
from content.models import UserLessonCompletion
session = guest_session('flow-device-2')
session = {'token': issue_access_token(User.objects.create(phone='09000000102', name='').id)}
self._auth(session['token'])
self.client.post('/api/user/grade', {'gradeId': self.grade.id}, format='json')
self.client.post(
@@ -144,7 +144,7 @@ class BasicLearningFlowTests(TestCase):
به‌روز می‌شود. بدونِ دانستنِ این، توسعه‌دهنده‌ی فرانت دنبالِ باگی
می‌گردد که وجود ندارد.
"""
session = guest_session('flow-device-4')
session = {'token': issue_access_token(User.objects.create(phone='09000000104', name='').id)}
self._auth(session['token'])
self.client.post('/api/user/grade', {'gradeId': self.grade.id}, format='json')
@@ -162,7 +162,7 @@ class BasicLearningFlowTests(TestCase):
self.assertGreater(self.client.get('/api/user/summary').data['xp'], 0)
def test_the_map_is_empty_but_healthy_before_a_grade_is_chosen(self):
session = guest_session('flow-device-3')
session = {'token': issue_access_token(User.objects.create(phone='09000000103', name='').id)}
self._auth(session['token'])
home = self.client.get('/api/home')
@@ -191,7 +191,7 @@ class LessonWithoutQuestionsTests(TestCase):
)
Choice.objects.create(question=question, value='الف', is_correct=True)
user = User.objects.create(device_id='empty-lesson', is_guest=True, grade_id=grade.id)
user = User.objects.create(phone='09000000008', grade_id=grade.id)
from accounts.authentication import issue_access_token
self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {issue_access_token(user.id)}')
@@ -315,7 +315,7 @@ class LessonCompletionNotificationTests(TestCase):
self.correct = Choice.objects.create(question=question, value='الف', is_correct=True)
self.question = question
self.user = User.objects.create(device_id='notif-on-complete', is_guest=True, grade_id=grade.id)
self.user = User.objects.create(phone='09000000009', grade_id=grade.id)
from accounts.authentication import issue_access_token
self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {issue_access_token(self.user.id)}')

View File

@@ -67,7 +67,7 @@ class SkillMapTests(TestCase):
self.assertEqual(rows['ویدیوی توان']['kind'], 'video')
self.assertEqual(rows['ویدیوی توان']['meta'], '12 دقیقه')
def test_done_is_false_for_a_guest_and_true_after_completion(self):
def test_done_is_false_when_signed_out_and_true_after_completion(self):
self.assertFalse(any(r['done'] for r in self._lessons()['lessons']))
UserLessonCompletion.objects.create(user_id=self.user.id, lesson=self.quiz,

View File

@@ -32,7 +32,7 @@ def _client(user):
class _World:
def build(self, phone):
cache.clear()
self.user = User.objects.create(phone=phone, is_guest=False)
self.user = User.objects.create(phone=phone)
self.client = _client(self.user)
grade = Grade.objects.create(title='پایه نهم', icon='', color='')
self.node = Node.objects.create(unit=Unit.objects.create(grade=grade, title='فصل', order=1),

View File

@@ -33,7 +33,7 @@ class SupervisorCannotEditContentTests(TestCase):
self.q = Question.objects.create(lesson=self.lesson, type='multiple_choice', prompt='p?')
ok = Choice.objects.create(question=self.q, value='1', is_correct=True)
Choice.objects.create(question=self.q, value='2', is_correct=False)
self.student = User.objects.create(phone='09120009999', is_guest=False)
self.student = User.objects.create(phone='09120009999')
UserAnswer.objects.create(user_id=self.student.id, question=self.q, choice=ok, correct=True)
def test_question_import_with_replace_is_refused(self):
@@ -64,7 +64,7 @@ class NonAsciiDigitsAre400Tests(TestCase):
def setUp(self):
cache.clear()
user = User.objects.create(device_id='digits', is_guest=True)
user = User.objects.create(phone='09000000010')
self.app = APIClient()
self.app.credentials(HTTP_AUTHORIZATION=f'Bearer {issue_access_token(user.id)}')
@@ -97,7 +97,7 @@ class JunkGradeIdsDoNotFlushCacheTests(TestCase):
patcher = mock.patch.dict(ScopedRateThrottle.THROTTLE_RATES, {'read': '100000/min'})
patcher.start()
self.addCleanup(patcher.stop)
user = User.objects.create(phone='09120009911', is_guest=False)
user = User.objects.create(phone='09120009911')
self.app = APIClient()
self.app.credentials(HTTP_AUTHORIZATION=f'Bearer {issue_access_token(user.id)}')
@@ -122,7 +122,7 @@ class ProgressNodesInputAndQueriesTests(TestCase):
def setUp(self):
cache.clear()
self.user = User.objects.create(phone='09120009912', is_guest=False)
self.user = User.objects.create(phone='09120009912')
self.app = APIClient()
self.app.credentials(HTTP_AUTHORIZATION=f'Bearer {issue_access_token(self.user.id)}')

View File

@@ -49,27 +49,6 @@ from .serializers import (
logger = logging.getLogger(__name__)
class GuestAwareContentThrottle(ScopedRateThrottle):
"""
مشترک با GuestAwareAIThrottle/GuestAwareDiscountThrottle
و مشابه‌هاش: چون GuestSessionView با هر deviceId جدید یک pk تازه می‌سازه،
throttleِ استانداردِ بر اساسِ user.pk با چرخشِ حسابِ مهمان دور زده می‌شه —
درحالی‌که answer/complete اثرِ جانبیِ ماندگار (هارت، xp/coins/progress) دارن.
برای همین فقط کاربرهای مهمان با IP کلید می‌خورن؛ کاربرهای غیرمهمان مثلِ قبل بر اساسِ pk.
"""
def get_cache_key(self, request, view):
user = request.user
if user and user.is_authenticated and getattr(user, 'is_guest', False):
ident = f'guest-ip:{self.get_ident(request)}'
elif user and user.is_authenticated:
ident = user.pk
else:
ident = self.get_ident(request)
return self.cache_format % {'scope': self.scope, 'ident': ident}
# --- کشِ داده‌ی شبهِ‌ثابتِ برنامه‌ی درسی -------------------------------------
# گریدها و درختِ unit/node/lesson تقریباً هیچ‌وقت عوض نمی‌شوند ولی روی هر بار
# باز شدنِ اپ خوانده می‌شوند. یک نسخه‌کلیدِ مشترک نگه می‌داریم که با هر تغییرِ
@@ -172,7 +151,7 @@ class GradesListView(APIView):
"""GET /api/grades — لیست پایه‌ها"""
throttle_scope = 'read'
throttle_classes = [GuestAwareContentThrottle]
throttle_classes = [ScopedRateThrottle]
@extend_schema(responses=GradesListResponseSerializer, tags=['Curriculum (content)'])
def get(self, request):
@@ -260,7 +239,7 @@ class HomeView(APIView):
# userId همیشه از request.user.id (توکن معتبر) گرفته می‌شود، نه ورودی کلاینت (IDOR).
permission_classes = [IsAuthenticated]
throttle_scope = 'read'
throttle_classes = [GuestAwareContentThrottle]
throttle_classes = [ScopedRateThrottle]
@extend_schema(
parameters=[
@@ -438,9 +417,9 @@ class QuestionAnswerView(APIView):
permission_classes = [IsAuthenticated]
# Idempotency-Key فقط جلوی retry با همون کلید رو می‌گیره، نه حجمِ درخواست‌های
# *متفاوت* که هرکدوم UserAnswer جدید می‌سازن و هارت/NodeProgress رو تغییر می‌دن؛
# برای همون GuestAwareContentThrottle لازمه (نگاه کنید به تعریفش بالا).
# برای همین throttle لازمه.
throttle_scope = 'question_answer'
throttle_classes = [GuestAwareContentThrottle]
throttle_classes = [ScopedRateThrottle]
@extend_schema(
request=AnswerRequestSerializer,
@@ -556,11 +535,11 @@ class LessonCompleteView(APIView):
# می‌تونست به‌جای هر کاربرِ دیگری xp/coins ثبت کنه.
permission_classes = [IsAuthenticated]
# Idempotency-Key فقط جلوی retry با همون کلید رو می‌گیره؛ برای همینه که
# GuestAwareContentThrottle لازمه (نگاه کنید به تعریفش بالا). نرخ در
# throttle لازمه. نرخ در
# settings (`lesson_complete`، الان ۱۲۰/hour) کمتر از question_answer است
# چون این endpoint رکوردِ ماندگار می‌سازه.
throttle_scope = 'lesson_complete'
throttle_classes = [GuestAwareContentThrottle]
throttle_classes = [ScopedRateThrottle]
@extend_schema(
request=LessonCompleteRequestSerializer,
@@ -823,7 +802,7 @@ class NodeQuizView(APIView):
"""
throttle_scope = 'read'
throttle_classes = [GuestAwareContentThrottle]
throttle_classes = [ScopedRateThrottle]
DEFAULT_SIZE = 4
MAX_SIZE = 20
@@ -909,7 +888,7 @@ class NodeQuizCompleteView(APIView):
permission_classes = [IsAuthenticated]
throttle_scope = 'node_complete'
throttle_classes = [GuestAwareContentThrottle]
throttle_classes = [ScopedRateThrottle]
@extend_schema(
request=NodeQuizCompleteRequestSerializer,
@@ -1047,7 +1026,7 @@ class NodeLessonsView(APIView):
"""
throttle_scope = 'read'
throttle_classes = [GuestAwareContentThrottle]
throttle_classes = [ScopedRateThrottle]
@extend_schema(responses=NodeLessonsResponseSerializer, tags=['Curriculum (content)'])
def get(self, request, node_id: int):