diff --git a/config/settings.py b/config/settings.py index 2782091..7c9f965 100644 --- a/config/settings.py +++ b/config/settings.py @@ -806,6 +806,28 @@ def _parse_test_login_phones(raw: str) -> dict[str, str]: TEST_LOGIN_PHONES = _parse_test_login_phones(os.environ.get('TEST_LOGIN_PHONES', '')) +# شماره‌های آزمونگر: برای این کاربرها هیچ جزیره‌ای روی نقشه قفل نیست (قفلِ «جزیره‌ی قبلی را تمام کن»). +# فقط قفلِ ترتیب را برمی‌دارد؛ اشتراکِ فصل‌ها، قلب و سقفِ دستیار همان قواعدِ عادی‌اند و برای آزمونگر +# با یک ردیفِ Subscription در ادمین داده می‌شود. چرا env و نه فیلدِ کاربر: تیمِ تست عوض می‌شود و +# نباید برای هر نفر migration یا دسترسیِ ادمین لازم باشد؛ چرا نه DEBUG: روی سرورِ واقعی لازم است. +# شماره‌ی نامعتبر راهاندازی را می‌شکند (بی‌صدا نادیده گرفته نمی‌شود). +def _parse_unlock_all_phones(raw: str) -> frozenset: + from accounts.phone import normalize_phone + + phones = set() + for item in raw.split(','): + item = item.strip() + if not item: + continue + normalized = normalize_phone(item) + if normalized is None: + raise ImproperlyConfigured(f'UNLOCK_ALL_PHONES شماره‌ی موبایلِ ایران نیست: «{item}».') + phones.add(normalized) + return frozenset(phones) + + +UNLOCK_ALL_PHONES = _parse_unlock_all_phones(os.environ.get('UNLOCK_ALL_PHONES', '')) + # ----------------------------------------------------------------------- # OTP SMS delivery (accounts/services.py: send_otp_sms) # ----------------------------------------------------------------------- diff --git a/content/tests_finish_requires_answers.py b/content/tests_finish_requires_answers.py index 8f74903..961b66a 100644 --- a/content/tests_finish_requires_answers.py +++ b/content/tests_finish_requires_answers.py @@ -190,6 +190,19 @@ class NodeIsUnlockedMatchesHomeTests(_World, TestCase): self._assert_matches_home('بعد از درس‌های جزیره‌ی دو') self.assertEqual(self.statuses()['سه'], 'unlocked') + def test_tester_phone_has_every_node_unlocked_on_map_and_server(self): + """UNLOCK_ALL_PHONES: نقشه و قفلِ سرور هر دو باز؛ کاربرِ دیگر همان قفلِ عادی را دارد. + فقط قفلِ ترتیب را سنجیده؛ اشتراکِ فصل‌ها دست نمی‌خورد (تستش جای دیگر است).""" + from django.test import override_settings + self.assertEqual(self.statuses()['سه'], 'locked') + self.assertFalse(node_is_unlocked(self.user.id, self.n3)) + with override_settings(UNLOCK_ALL_PHONES=frozenset({self.user.phone})): + self.assertNotIn('locked', self.statuses().values()) + self.assertTrue(node_is_unlocked(self.user.id, self.n3)) + with override_settings(UNLOCK_ALL_PHONES=frozenset({'09129999999'})): + self.assertEqual(self.statuses()['سه'], 'locked') + self.assertFalse(node_is_unlocked(self.user.id, self.n3)) + def test_a_node_missing_from_the_tree_is_locked_and_logged(self): ghost = Node(id=999999, unit=self.unit, title='شبح', order=9) with self.assertLogs('content.views', level='WARNING'): diff --git a/content/views.py b/content/views.py index 6ba2710..4336339 100644 --- a/content/views.py +++ b/content/views.py @@ -263,6 +263,18 @@ def answers_missing_response(answered: int, required: int) -> Response: ) +def user_has_all_nodes_unlocked(user_id) -> bool: + """ + کاربرِ آزمونگر (`settings.UNLOCK_ALL_PHONES`): قفلِ ترتیبِ جزیره‌ها برایش برداشته می‌شود. + وقتی فهرست خالی است (حالتِ عادی) هیچ کوئری‌ای زده نمی‌شود. + """ + from django.conf import settings + if user_id is None or not settings.UNLOCK_ALL_PHONES: + return False + from accounts.models import User + return User.objects.filter(pk=user_id, phone__in=settings.UNLOCK_ALL_PHONES).exists() + + def node_is_unlocked(user_id, node) -> bool: """ آیا این جزیره برای این کاربر روی نقشه قفل نیست؟ همان قاعده‌ی `HomeView` است @@ -273,6 +285,8 @@ def node_is_unlocked(user_id, node) -> bool: اگر جزیره در درختِ کش‌شده نبود (مثلاً تازه ساخته شده و کش کهنه است) «قفل» حساب می‌شود و در لاگ WARNING می‌ماند؛ همان‌طور که نقشه هم آن را نشان نمی‌دهد. """ + if user_has_all_nodes_unlocked(user_id): + return True tree = _grade_tree(node.unit.grade_id) or [] unit = next((u for u in tree if u['id'] == str(node.unit_id)), None) if unit is not None: @@ -455,6 +469,7 @@ class HomeView(APIView): from subscription.services import has_active_subscription subscribed = user_id is not None and has_active_subscription(user_id) + unlock_all = user_has_all_nodes_unlocked(user_id) units_payload = [] for index, unit in enumerate(tree): node_payload = [] @@ -471,6 +486,8 @@ class HomeView(APIView): ) if node_status != 'completed': unlock_next = False + if unlock_all and node_status == 'locked': + node_status = 'unlocked' node_payload.append({ 'id': node['id'], 'title': node['title'],