Skip to content

Commit a7bd47a

Browse files
Merge branch 'master' into issue-35276-unpin-django-webpack-loader
2 parents 85bc2ba + 8c8a567 commit a7bd47a

39 files changed

+401
-296
lines changed

README.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ Language Packages:
103103
* Backend application:
104104

105105
- ``pip install -r requirements/edx/base.txt`` (production)
106-
- ``pip install -r requirements/edx/dev.txt`` (development)
106+
- ``pip install -r requirements/edx/development.txt`` (development)
107107

108108
Some Python packages have system dependencies. For example, installing these packages on Debian or Ubuntu will require first running ``sudo apt install python3-dev default-libmysqlclient-dev build-essential pkg-config`` to satisfy the requirements of the ``mysqlclient`` Python package.
109109

cms/envs/common.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,6 @@
378378
# Prevent auto auth from creating superusers or modifying existing users
379379
'RESTRICT_AUTOMATIC_AUTH': True,
380380

381-
'PREVIEW_LMS_BASE': "preview.localhost:18000",
382381
'ENABLE_GRADE_DOWNLOADS': True,
383382
'ENABLE_MKTG_SITE': False,
384383
'ENABLE_DISCUSSION_HOME_PANEL': True,

cms/envs/devstack-experimental.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,6 @@ FEATURES:
324324
ENABLE_SYSADMIN_DASHBOARD: false
325325
ENABLE_THIRD_PARTY_AUTH: true
326326
ENABLE_VIDEO_UPLOAD_PIPELINE: false
327-
PREVIEW_LMS_BASE: preview.localhost:18000
328327
SHOW_FOOTER_LANGUAGE_SELECTOR: false
329328
SHOW_HEADER_LANGUAGE_SELECTOR: false
330329
FEEDBACK_SUBMISSION_EMAIL: ''

cms/envs/devstack.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@
4545

4646
LMS_BASE = 'localhost:18000'
4747
LMS_ROOT_URL = f'http://{LMS_BASE}'
48-
FEATURES['PREVIEW_LMS_BASE'] = "preview." + LMS_BASE
4948

5049
FRONTEND_REGISTER_URL = LMS_ROOT_URL + '/register'
5150

cms/envs/mock.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -454,7 +454,6 @@ FEATURES:
454454
LTI_1P3_ENABLED: true
455455
MILESTONES_APP: true
456456
PREVENT_CONCURRENT_LOGINS: true
457-
PREVIEW_LMS_BASE: preview.courses.localhost
458457
SEGMENT_IO_LMS: true
459458
SEPARATE_VERIFICATION_FROM_PAYMENT: true
460459
SHOW_FOOTER_LANGUAGE_SELECTOR: true

cms/envs/test.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,6 @@
137137

138138
LMS_BASE = "localhost:8000"
139139
LMS_ROOT_URL = f"http://{LMS_BASE}"
140-
FEATURES["PREVIEW_LMS_BASE"] = "preview.localhost"
141140

142141
CMS_BASE = "localhost:8001"
143142
CMS_ROOT_URL = f"http://{CMS_BASE}"

common/djangoapps/student/management/commands/backfill_is_disabled.py

Lines changed: 96 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@
88
"""
99

1010
import logging
11+
import time
1112
from django.core.management.base import BaseCommand
1213
from django.contrib.auth import get_user_model
1314
from django.contrib.auth.hashers import UNUSABLE_PASSWORD_PREFIX
1415
from django.db import DatabaseError
1516
from common.djangoapps.track import segment
17+
import requests
1618

1719
LOGGER = logging.getLogger(__name__)
1820
User = get_user_model()
@@ -28,47 +30,123 @@ def add_arguments(self, parser):
2830
parser.add_argument(
2931
'--batch-size',
3032
type=int,
31-
default=100,
33+
default=9000,
3234
help='Number of users to process per batch'
3335
)
3436
parser.add_argument(
3537
'--dry-run',
3638
action='store_true',
3739
help='Simulate the back fill without calling Segment'
3840
)
41+
parser.add_argument(
42+
'--retry-limit',
43+
type=int, default=3,
44+
help='Retry attempts for failed API calls'
45+
)
46+
47+
def _process_user(self, user_id, batch_number, dry_run):
48+
"""Process a single user, logging success or failure."""
49+
if dry_run:
50+
LOGGER.info(
51+
f"[Dry Run] Would update user {user_id} with is_disabled=true "
52+
f"in batch {batch_number}"
53+
)
54+
return True
55+
try:
56+
segment.analytics.identify(user_id=user_id, traits={'is_disabled': True})
57+
LOGGER.info(
58+
f"Successfully updated user {user_id} with is_disabled=true "
59+
f"in batch {batch_number}"
60+
)
61+
return True
62+
except (ConnectionError, ValueError) as e:
63+
LOGGER.error(
64+
f"Failed to update user {user_id} in batch {batch_number}: "
65+
f"{str(e)}"
66+
)
67+
return False
68+
69+
def _process_batch(self, users_batch, batch_number, dry_run, retry_limit):
70+
"""Process a batch of users with retries."""
71+
current_batch_size = len(users_batch)
72+
if dry_run:
73+
for user_id in users_batch:
74+
self._process_user(user_id, batch_number, dry_run)
75+
return current_batch_size
76+
77+
retry_count = 0
78+
success = False
79+
while not success and retry_count <= retry_limit:
80+
try:
81+
for user_id in users_batch:
82+
self._process_user(user_id, batch_number, dry_run)
83+
segment.analytics.flush()
84+
LOGGER.info(f"Successfully processed batch {batch_number}")
85+
success = True
86+
return current_batch_size
87+
except (requests.exceptions.RequestException, segment.analytics.errors.APIError) as e:
88+
retry_count += 1
89+
if retry_count <= retry_limit:
90+
LOGGER.warning(
91+
f"Batch {batch_number} failed (attempt {retry_count}/"
92+
f"{retry_limit}): {str(e)}"
93+
)
94+
time.sleep(2 * retry_count)
95+
else:
96+
LOGGER.error(
97+
f"Batch {batch_number} failed after {retry_limit} attempts, "
98+
f"processed {{processed}} users: {str(e)}"
99+
)
100+
return None
39101

40102
def handle(self, *args, **options):
41103
batch_size = options['batch_size']
42104
dry_run = options['dry_run']
105+
retry_limit = options['retry_limit']
43106

44107
try:
45-
LOGGER.info(f"Starting back fill with batch_size={batch_size}, dry_run={dry_run}")
108+
LOGGER.info(
109+
f"Starting backfill (batch_size={batch_size}, dry_run={dry_run}, "
110+
f"retry_limit={retry_limit})"
111+
)
46112

47-
queryset = User.objects.filter(
113+
total_users = User.objects.filter(
48114
password__startswith=UNUSABLE_PASSWORD_PREFIX
49-
).values('id', 'password')
50-
51-
total_users = queryset.count()
115+
).count()
52116

53117
if total_users == 0:
54118
LOGGER.info("No users to process, exiting")
55119
return
56120

57121
LOGGER.info(f"Found {total_users} users that are disabled")
58122

123+
offset = 0
59124
processed = 0
60-
for user in queryset.iterator(chunk_size=batch_size):
61-
try:
62-
if dry_run:
63-
LOGGER.info(f"[Dry Run] Would update user {user['id']} with is_disabled=true")
64-
else:
65-
segment.identify(user['id'], {'is_disabled': 'true'})
66-
LOGGER.info(f"Successfully updated user {user['id']} with is_disabled=true")
67-
processed += 1
68-
except (ConnectionError, ValueError) as e:
69-
LOGGER.error(f"Failed to update user {user['id']}: {str(e)}")
70-
71-
LOGGER.info(f"Back fill completed: processed {processed}/{total_users} users")
125+
batch_number = 0
126+
127+
while offset < total_users:
128+
batch_number += 1
129+
users_batch = User.objects.filter(
130+
password__startswith=UNUSABLE_PASSWORD_PREFIX
131+
).values_list('id', flat=True)[offset:offset + batch_size]
132+
LOGGER.info(f"Processing batch {batch_number} ({len(users_batch)} users)")
133+
134+
batch_result = self._process_batch(
135+
users_batch, batch_number, dry_run, retry_limit
136+
)
137+
if batch_result is None:
138+
LOGGER.error(
139+
f"Backfill stopped, processed {processed} users"
140+
)
141+
return
142+
processed += batch_result
143+
offset += batch_size
144+
LOGGER.info(f"Processed {processed} / {total_users} users")
145+
146+
LOGGER.info(
147+
f"Completed: processed {processed} / {total_users} users in "
148+
f"{batch_number} batches"
149+
)
72150

73151
except DatabaseError as e:
74152
LOGGER.error(f"Back fill failed: {str(e)}")

docs/decisions/0002-inter-app-apis.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ Decisions
3434
part of the app's domain API), then test-relevant Python APIs should be
3535
defined/exported in an intentional Python module called "api_for_tests.py".
3636

37-
Exmaples
37+
Examples
3838
~~~~~~~~
3939

4040
As a reference example, see the Python APIs exposed by the grades app in the

lms/djangoapps/courseware/access.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@
3434
check_course_open_for_learner,
3535
check_start_date,
3636
debug,
37-
in_preview_mode
3837
)
3938
from lms.djangoapps.courseware.masquerade import get_masquerade_role, is_masquerading_as_student
4039
from lms.djangoapps.ccx.custom_exception import CCXLocatorValidationException
@@ -158,11 +157,6 @@ def has_access(user, action, obj, course_key=None):
158157
if not user:
159158
user = AnonymousUser()
160159

161-
# Preview mode is only accessible by staff.
162-
if in_preview_mode() and course_key:
163-
if not has_staff_access_to_preview_mode(user, course_key):
164-
return ACCESS_DENIED
165-
166160
# delegate the work to type-specific functions.
167161
# (start with more specific types, then get more general)
168162
if isinstance(obj, CourseBlock):

lms/djangoapps/courseware/access_utils.py

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
from lms.djangoapps.courseware.masquerade import get_course_masquerade, is_masquerading_as_student
2626
from openedx.features.course_experience import COURSE_ENABLE_UNENROLLED_ACCESS_FLAG, COURSE_PRE_START_ACCESS_FLAG
2727
from xmodule.course_block import COURSE_VISIBILITY_PUBLIC # lint-amnesty, pylint: disable=wrong-import-order
28-
from xmodule.util.xmodule_django import get_current_request_hostname # lint-amnesty, pylint: disable=wrong-import-order
2928

3029
DEBUG_ACCESS = False
3130
log = getLogger(__name__)
@@ -138,7 +137,7 @@ def check_start_date(user, days_early_for_beta, start, course_key, display_error
138137
if start_dates_disabled and not masquerading_as_student:
139138
return ACCESS_GRANTED
140139
else:
141-
if start is None or in_preview_mode() or get_course_masquerade(user, course_key):
140+
if start is None or get_course_masquerade(user, course_key):
142141
return ACCESS_GRANTED
143142

144143
if now is None:
@@ -158,15 +157,6 @@ def check_start_date(user, days_early_for_beta, start, course_key, display_error
158157
return StartDateError(start, display_error_to_user=display_error_to_user)
159158

160159

161-
def in_preview_mode():
162-
"""
163-
Returns whether the user is in preview mode or not.
164-
"""
165-
hostname = get_current_request_hostname()
166-
preview_lms_base = settings.FEATURES.get("PREVIEW_LMS_BASE", None)
167-
return bool(preview_lms_base and hostname and hostname.split(":")[0] == preview_lms_base.split(":")[0])
168-
169-
170160
def check_course_open_for_learner(user, course):
171161
"""
172162
Check if the course is open for learners based on the start date.

lms/djangoapps/courseware/tests/helpers.py

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,8 @@
88
import json
99
from collections import OrderedDict
1010
from datetime import timedelta
11-
from unittest.mock import Mock, patch
11+
from unittest.mock import Mock
1212

13-
from django.conf import settings
1413
from django.contrib import messages
1514
from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user
1615
from django.test import TestCase
@@ -20,7 +19,6 @@
2019
from xblock.field_data import DictFieldData
2120

2221
from common.djangoapps.edxmako.shortcuts import render_to_string
23-
from lms.djangoapps.courseware import access_utils
2422
from lms.djangoapps.courseware.access import has_access
2523
from lms.djangoapps.courseware.utils import verified_upgrade_deadline_link
2624
from lms.djangoapps.courseware.masquerade import MasqueradeView
@@ -460,11 +458,3 @@ def get_context_dict_from_string(data):
460458
sorted(json.loads(validated_data['metadata']).items(), key=lambda t: t[0])
461459
)
462460
return validated_data
463-
464-
465-
def set_preview_mode(preview_mode: bool):
466-
"""
467-
A decorator to force the preview mode on or off.
468-
"""
469-
hostname = settings.FEATURES.get('PREVIEW_LMS_BASE') if preview_mode else None
470-
return patch.object(access_utils, 'get_current_request_hostname', new=lambda: hostname)

0 commit comments

Comments
 (0)