diff --git a/.github/workflows/check_python_dependencies.yml b/.github/workflows/check_python_dependencies.yml
index 85a4e796ce78..b691e68d4be9 100644
--- a/.github/workflows/check_python_dependencies.yml
+++ b/.github/workflows/check_python_dependencies.yml
@@ -14,18 +14,18 @@ jobs:
steps:
- name: Checkout Repository
uses: actions/checkout@v4
-
+
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
-
+
- name: Install repo-tools
run: pip install edx-repo-tools[find_dependencies]
- name: Install setuptool
- run: pip install setuptools
-
+ run: pip install setuptools
+
- name: Run Python script
run: |
find_python_dependencies \
@@ -35,6 +35,5 @@ jobs:
--ignore https://github.com/edx/braze-client \
--ignore https://github.com/edx/edx-name-affirmation \
--ignore https://github.com/mitodl/edx-sga \
- --ignore https://github.com/edx/token-utils \
--ignore https://github.com/open-craft/xblock-poll
-
+
diff --git a/.github/workflows/publish-ci-docker-image.yml b/.github/workflows/publish-ci-docker-image.yml
deleted file mode 100644
index e69de29bb2d1..000000000000
diff --git a/cms/djangoapps/api/v1/views/course_runs.py b/cms/djangoapps/api/v1/views/course_runs.py
index b405207bd9c6..7b27193d175f 100644
--- a/cms/djangoapps/api/v1/views/course_runs.py
+++ b/cms/djangoapps/api/v1/views/course_runs.py
@@ -23,6 +23,7 @@ class CourseRunViewSet(viewsets.GenericViewSet): # lint-amnesty, pylint: disabl
lookup_value_regex = settings.COURSE_KEY_REGEX
permission_classes = (permissions.IsAdminUser,)
serializer_class = CourseRunSerializer
+ queryset = []
def get_object(self):
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
diff --git a/cms/djangoapps/contentstore/api/views/course_import.py b/cms/djangoapps/contentstore/api/views/course_import.py
index dd7828c2d94f..3027b1926d0f 100644
--- a/cms/djangoapps/contentstore/api/views/course_import.py
+++ b/cms/djangoapps/contentstore/api/views/course_import.py
@@ -106,7 +106,7 @@ class CourseImportView(CourseImportExportViewMixin, GenericAPIView):
# TODO: ARCH-91
# This view is excluded from Swagger doc generation because it
# does not specify a serializer class.
- exclude_from_schema = True
+ swagger_schema = None
@course_author_access_required
def post(self, request, course_key):
diff --git a/cms/djangoapps/contentstore/api/views/course_quality.py b/cms/djangoapps/contentstore/api/views/course_quality.py
index 8f647accaa91..b301f5ac1420 100644
--- a/cms/djangoapps/contentstore/api/views/course_quality.py
+++ b/cms/djangoapps/contentstore/api/views/course_quality.py
@@ -77,6 +77,11 @@ class CourseQualityView(DeveloperErrorViewMixin, GenericAPIView):
* mode
"""
+ # TODO: ARCH-91
+ # This view is excluded from Swagger doc generation because it
+ # does not specify a serializer class.
+ swagger_schema = None
+
@course_author_access_required
def get(self, request, course_key):
"""
diff --git a/cms/djangoapps/contentstore/api/views/course_validation.py b/cms/djangoapps/contentstore/api/views/course_validation.py
index 0fa8d9041c1b..d1c2c2b8c46f 100644
--- a/cms/djangoapps/contentstore/api/views/course_validation.py
+++ b/cms/djangoapps/contentstore/api/views/course_validation.py
@@ -65,6 +65,11 @@ class CourseValidationView(DeveloperErrorViewMixin, GenericAPIView):
* has_proctoring_escalation_email - whether the course has a proctoring escalation email
"""
+ # TODO: ARCH-91
+ # This view is excluded from Swagger doc generation because it
+ # does not specify a serializer class.
+ swagger_schema = None
+
@course_author_access_required
def get(self, request, course_key):
"""
diff --git a/cms/djangoapps/contentstore/management/commands/backfill_course_tabs.py b/cms/djangoapps/contentstore/management/commands/backfill_course_tabs.py
index 878a8dabaa27..768c3a53f7ac 100644
--- a/cms/djangoapps/contentstore/management/commands/backfill_course_tabs.py
+++ b/cms/djangoapps/contentstore/management/commands/backfill_course_tabs.py
@@ -71,6 +71,5 @@ def handle(self, *args, **options):
if error_keys:
msg = 'The following courses encountered errors and were not updated:\n'
- for error_key in error_keys:
- msg += f' - {error_key}\n'
+ msg += '\n'.join(f' - {error_key}' for error_key in error_keys)
logger.info(msg)
diff --git a/cms/djangoapps/contentstore/management/commands/export_content_library.py b/cms/djangoapps/contentstore/management/commands/export_content_library.py
index 0b4cbfb1fbad..b56c172e374e 100644
--- a/cms/djangoapps/contentstore/management/commands/export_content_library.py
+++ b/cms/djangoapps/contentstore/management/commands/export_content_library.py
@@ -51,16 +51,15 @@ def handle(self, *args, **options):
tarball = tasks.create_export_tarball(library, library_key, {}, None)
except Exception as e:
raise CommandError(f'Failed to export "{library_key}" with "{e}"') # lint-amnesty, pylint: disable=raise-missing-from
- else:
- with tarball:
- # Save generated archive with keyed filename
- prefix, suffix, n = str(library_key).replace(':', '+'), '.tar.gz', 0
- while os.path.exists(prefix + suffix):
- n += 1
- prefix = '{}_{}'.format(prefix.rsplit('_', 1)[0], n) if n > 1 else f'{prefix}_1'
- filename = prefix + suffix
- target = os.path.join(dest_path, filename)
- tarball.file.seek(0)
- with open(target, 'wb') as f:
- shutil.copyfileobj(tarball.file, f)
- print(f'Library "{library.location.library_key}" exported to "{target}"')
+ with tarball:
+ # Save generated archive with keyed filename
+ prefix, suffix, n = str(library_key).replace(':', '+'), '.tar.gz', 0
+ while os.path.exists(prefix + suffix):
+ n += 1
+ prefix = '{}_{}'.format(prefix.rsplit('_', 1)[0], n) if n > 1 else f'{prefix}_1'
+ filename = prefix + suffix
+ target = os.path.join(dest_path, filename)
+ tarball.file.seek(0)
+ with open(target, 'wb') as f:
+ shutil.copyfileobj(tarball.file, f)
+ print(f'Library "{library.location.library_key}" exported to "{target}"')
diff --git a/cms/djangoapps/contentstore/rest_api/v0/serializers/authoring_grading.py b/cms/djangoapps/contentstore/rest_api/v0/serializers/authoring_grading.py
index e3dd070573aa..e42c3e2ee397 100644
--- a/cms/djangoapps/contentstore/rest_api/v0/serializers/authoring_grading.py
+++ b/cms/djangoapps/contentstore/rest_api/v0/serializers/authoring_grading.py
@@ -14,7 +14,13 @@ class GradersSerializer(serializers.Serializer):
weight = serializers.IntegerField()
id = serializers.IntegerField()
+ class Meta:
+ ref_name = "authoring_grading.Graders.v0"
+
class CourseGradingModelSerializer(serializers.Serializer):
""" Serializer for course grading model data """
graders = GradersSerializer(many=True, allow_null=True, allow_empty=True)
+
+ class Meta:
+ ref_name = "authoring_grading.CourseGrading.v0"
diff --git a/cms/djangoapps/contentstore/rest_api/v0/urls.py b/cms/djangoapps/contentstore/rest_api/v0/urls.py
index cc1e13b0929c..6e0c11d22b8f 100644
--- a/cms/djangoapps/contentstore/rest_api/v0/urls.py
+++ b/cms/djangoapps/contentstore/rest_api/v0/urls.py
@@ -63,7 +63,7 @@
authoring_videos.VideoEncodingsDownloadView.as_view(), name='cms_api_videos_encodings'
),
re_path(
- fr'grading/{settings.COURSE_ID_PATTERN}',
+ fr'grading/{settings.COURSE_ID_PATTERN}$',
AuthoringGradingView.as_view(), name='cms_api_update_grading'
),
path(
diff --git a/cms/djangoapps/contentstore/rest_api/v0/views/authoring_videos.py b/cms/djangoapps/contentstore/rest_api/v0/views/authoring_videos.py
index 972b6229f55a..8fb66070f3bf 100644
--- a/cms/djangoapps/contentstore/rest_api/v0/views/authoring_videos.py
+++ b/cms/djangoapps/contentstore/rest_api/v0/views/authoring_videos.py
@@ -128,6 +128,11 @@ class VideoEncodingsDownloadView(DeveloperErrorViewMixin, RetrieveAPIView):
course_key: required argument, needed to authorize course authors and identify relevant videos.
"""
+ # TODO: ARCH-91
+ # This view is excluded from Swagger doc generation because it
+ # does not specify a serializer class.
+ swagger_schema = None
+
def dispatch(self, request, *args, **kwargs):
# TODO: probably want to refactor this to a decorator.
"""
@@ -151,6 +156,11 @@ class VideoFeaturesView(DeveloperErrorViewMixin, RetrieveAPIView):
public rest API endpoint providing a list of enabled video features.
"""
+ # TODO: ARCH-91
+ # This view is excluded from Swagger doc generation because it
+ # does not specify a serializer class.
+ swagger_schema = None
+
def dispatch(self, request, *args, **kwargs):
# TODO: probably want to refactor this to a decorator.
"""
diff --git a/cms/djangoapps/contentstore/views/tests/test_clipboard_paste.py b/cms/djangoapps/contentstore/views/tests/test_clipboard_paste.py
index 9244ffa989b6..3b39f2918a36 100644
--- a/cms/djangoapps/contentstore/views/tests/test_clipboard_paste.py
+++ b/cms/djangoapps/contentstore/views/tests/test_clipboard_paste.py
@@ -555,7 +555,6 @@ def _copy_paste_and_assert_link(key_to_copy):
assert new_block.upstream == str(self.lib_block_key)
assert new_block.upstream_version == 3
assert new_block.upstream_display_name == "MCQ-draft"
- assert new_block.upstream_max_attempts == 5
return new_block_key
# first verify link for copied block from library
diff --git a/cms/envs/common.py b/cms/envs/common.py
index 591247388a9d..7a36bfab3d96 100644
--- a/cms/envs/common.py
+++ b/cms/envs/common.py
@@ -843,6 +843,7 @@
# Public domain name of Studio (should be resolvable from the end-user's browser)
CMS_BASE = 'localhost:18010'
+CMS_ROOT_URL = "https://localhost:18010"
LOG_DIR = '/edx/var/log/edx'
@@ -2520,6 +2521,8 @@
CREDENTIALS_INTERNAL_SERVICE_URL = 'http://localhost:8005'
CREDENTIALS_PUBLIC_SERVICE_URL = 'http://localhost:8005'
CREDENTIALS_SERVICE_USERNAME = 'credentials_service_user'
+# time between scheduled runs, in seconds
+NOTIFY_CREDENTIALS_FREQUENCY = 14400
ANALYTICS_DASHBOARD_URL = 'http://localhost:18110/courses'
ANALYTICS_DASHBOARD_NAME = 'Your Platform Name Here Insights'
diff --git a/cms/envs/production.py b/cms/envs/production.py
index ad7667772f9a..da5642b53c6c 100644
--- a/cms/envs/production.py
+++ b/cms/envs/production.py
@@ -163,6 +163,7 @@ def get_env_setting(setting):
CMS_BASE = ENV_TOKENS.get('CMS_BASE')
LMS_BASE = ENV_TOKENS.get('LMS_BASE')
LMS_ROOT_URL = ENV_TOKENS.get('LMS_ROOT_URL')
+CMS_ROOT_URL = ENV_TOKENS.get('CMS_ROOT_URL')
LMS_INTERNAL_ROOT_URL = ENV_TOKENS.get('LMS_INTERNAL_ROOT_URL', LMS_ROOT_URL)
ENTERPRISE_API_URL = ENV_TOKENS.get('ENTERPRISE_API_URL', LMS_INTERNAL_ROOT_URL + '/enterprise/api/v1/')
ENTERPRISE_CONSENT_API_URL = ENV_TOKENS.get('ENTERPRISE_CONSENT_API_URL', LMS_INTERNAL_ROOT_URL + '/consent/api/v1/')
diff --git a/cms/envs/test.py b/cms/envs/test.py
index 49db50608858..6a7c17b001fe 100644
--- a/cms/envs/test.py
+++ b/cms/envs/test.py
@@ -136,6 +136,9 @@
LMS_ROOT_URL = f"http://{LMS_BASE}"
FEATURES['PREVIEW_LMS_BASE'] = "preview.localhost"
+CMS_BASE = "localhost:8001"
+CMS_ROOT_URL = f"http://{CMS_BASE}"
+
COURSE_AUTHORING_MICROFRONTEND_URL = "http://course-authoring-mfe"
DISCUSSIONS_MICROFRONTEND_URL = "http://discussions-mfe"
diff --git a/cms/lib/xblock/test/test_upstream_sync.py b/cms/lib/xblock/test/test_upstream_sync.py
index cc3d661ca6e9..2f8f77ab6560 100644
--- a/cms/lib/xblock/test/test_upstream_sync.py
+++ b/cms/lib/xblock/test/test_upstream_sync.py
@@ -1,7 +1,9 @@
"""
Test CMS's upstream->downstream syncing system
"""
+import datetime
import ddt
+from pytz import utc
from organizations.api import ensure_organization
from organizations.models import Organization
@@ -42,13 +44,33 @@ def setUp(self):
title="Test Upstream Library",
)
self.upstream_key = libs.create_library_block(self.library.key, "html", "test-upstream").usage_key
- libs.create_library_block(self.library.key, "video", "video-upstream")
upstream = xblock.load_block(self.upstream_key, self.user)
upstream.display_name = "Upstream Title V2"
upstream.data = "
Upstream content V2"
upstream.save()
+ self.upstream_problem_key = libs.create_library_block(self.library.key, "problem", "problem-upstream").usage_key
+ libs.set_library_block_olx(self.upstream_problem_key, (
+ '\n'
+ ))
+
libs.publish_changes(self.library.key, self.user.id)
self.taxonomy_all_org = tagging_api.create_taxonomy(
@@ -179,6 +201,100 @@ def test_sync_updates_happy_path(self):
for object_tag in object_tags:
assert object_tag.value in new_upstream_tags
+ # pylint: disable=too-many-statements
+ def test_sync_updates_to_downstream_only_fields(self):
+ """
+ If we sync to modified content, will it preserve downstream-only fields, and overwrite the rest?
+ """
+ downstream = BlockFactory.create(category='problem', parent=self.unit, upstream=str(self.upstream_problem_key))
+
+ # Initial sync
+ sync_from_upstream(downstream, self.user)
+
+ # These fields are copied from upstream
+ assert downstream.upstream_display_name == "Upstream Problem Title V2"
+ assert downstream.display_name == "Upstream Problem Title V2"
+ assert downstream.rerandomize == '"always"'
+ assert downstream.matlab_api_key == 'abc'
+ assert not downstream.use_latex_compiler
+
+ # These fields are "downstream only", so field defaults are preserved, and values are NOT copied from upstream
+ assert downstream.attempts_before_showanswer_button == 0
+ assert downstream.due is None
+ assert not downstream.force_save_button
+ assert downstream.graceperiod is None
+ assert downstream.grading_method == 'last_score'
+ assert downstream.max_attempts is None
+ assert downstream.show_correctness == 'always'
+ assert not downstream.show_reset_button
+ assert downstream.showanswer == 'finished'
+ assert downstream.submission_wait_seconds == 0
+ assert downstream.weight is None
+
+ # Upstream updates
+ libs.set_library_block_olx(self.upstream_problem_key, (
+ '\n'
+ ))
+ libs.publish_changes(self.library.key, self.user.id)
+
+ # Modifing downstream-only fields are "safe" customizations
+ downstream.display_name = "Downstream Title Override"
+ downstream.attempts_before_showanswer_button = 2
+ downstream.due = datetime.datetime(2025, 2, 2, tzinfo=utc)
+ downstream.force_save_button = True
+ downstream.graceperiod = '2d'
+ downstream.grading_method = 'last_score'
+ downstream.max_attempts = 100
+ downstream.show_correctness = 'always'
+ downstream.show_reset_button = True
+ downstream.showanswer = 'on_expired'
+ downstream.submission_wait_seconds = 100
+ downstream.weight = 3
+
+ # Modifying synchronized fields are "unsafe" customizations
+ downstream.rerandomize = '"onreset"'
+ downstream.matlab_api_key = 'hij'
+ downstream.save()
+
+ # Follow-up sync.
+ sync_from_upstream(downstream, self.user)
+
+ # "unsafe" customizations are overridden by upstream
+ assert downstream.upstream_display_name == "Upstream Problem Title V3"
+ assert downstream.rerandomize == '"per_student"'
+ assert downstream.matlab_api_key == 'def'
+ assert downstream.use_latex_compiler
+
+ # but "safe" customizations survive
+ assert downstream.display_name == "Downstream Title Override"
+ assert downstream.attempts_before_showanswer_button == 2
+ assert downstream.due == datetime.datetime(2025, 2, 2, tzinfo=utc)
+ assert downstream.force_save_button
+ assert downstream.graceperiod == '2d'
+ assert downstream.grading_method == 'last_score'
+ assert downstream.max_attempts == 100
+ assert downstream.show_correctness == 'always'
+ assert downstream.show_reset_button
+ assert downstream.showanswer == 'on_expired'
+ assert downstream.submission_wait_seconds == 100
+ assert downstream.weight == 3
+
def test_sync_updates_to_modified_content(self):
"""
If we sync to modified content, will it preserve customizable fields, but overwrite the rest?
diff --git a/cms/lib/xblock/upstream_sync.py b/cms/lib/xblock/upstream_sync.py
index 0d95931ce29d..4723e566ffc3 100644
--- a/cms/lib/xblock/upstream_sync.py
+++ b/cms/lib/xblock/upstream_sync.py
@@ -252,10 +252,6 @@ def _update_customizable_fields(*, upstream: XBlock, downstream: XBlock, only_fe
* Set `course_problem.upstream_display_name = lib_problem.display_name` ("fetch").
* If `not only_fetch`, and `course_problem.display_name` wasn't customized, then:
* Set `course_problem.display_name = lib_problem.display_name` ("sync").
-
- * Set `course_problem.upstream_max_attempts = lib_problem.max_attempts` ("fetch").
- * If `not only_fetch`, and `course_problem.max_attempts` wasn't customized, then:
- * Set `course_problem.max_attempts = lib_problem.max_attempts` ("sync").
"""
syncable_field_names = _get_synchronizable_fields(upstream, downstream)
@@ -264,6 +260,10 @@ def _update_customizable_fields(*, upstream: XBlock, downstream: XBlock, only_fe
if field_name not in syncable_field_names:
continue
+ # Downstream-only fields don't have an upstream fetch field
+ if fetch_field_name is None:
+ continue
+
# FETCH the upstream's value and save it on the downstream (ie, `downstream.upstream_$FIELD`).
old_upstream_value = getattr(downstream, fetch_field_name)
new_upstream_value = getattr(upstream, field_name)
@@ -361,6 +361,9 @@ def sever_upstream_link(downstream: XBlock) -> None:
downstream.upstream = None
downstream.upstream_version = None
for _, fetched_upstream_field in downstream.get_customizable_fields().items():
+ # Downstream-only fields don't have an upstream fetch field
+ if fetched_upstream_field is None:
+ continue
setattr(downstream, fetched_upstream_field, None) # Null out upstream_display_name, et al.
@@ -414,21 +417,30 @@ class UpstreamSyncMixin(XBlockMixin):
help=("The value of display_name on the linked upstream block."),
default=None, scope=Scope.settings, hidden=True, enforce_type=True,
)
- upstream_max_attempts = Integer(
- help=("The value of max_attempts on the linked upstream block."),
- default=None, scope=Scope.settings, hidden=True, enforce_type=True,
- )
@classmethod
- def get_customizable_fields(cls) -> dict[str, str]:
+ def get_customizable_fields(cls) -> dict[str, str | None]:
"""
Mapping from each customizable field to the field which can be used to restore its upstream value.
+ If the customizable field is mapped to None, then it is considered "downstream only", and cannot be restored
+ from the upstream value.
+
XBlocks outside of edx-platform can override this in order to set up their own customizable fields.
"""
return {
"display_name": "upstream_display_name",
- "max_attempts": "upstream_max_attempts",
+ "attempts_before_showanswer_button": None,
+ "due": None,
+ "force_save_button": None,
+ "graceperiod": None,
+ "grading_method": None,
+ "max_attempts": None,
+ "show_correctness": None,
+ "show_reset_button": None,
+ "showanswer": None,
+ "submission_wait_seconds": None,
+ "weight": None,
}
# PRESERVING DOWNSTREAM CUSTOMIZATIONS and RESTORING UPSTREAM VALUES
@@ -485,6 +497,10 @@ def get_customizable_fields(cls) -> dict[str, str]:
# if field_name in self.downstream_customized:
# continue
#
+ # # If there is no restore_field name, it's a downstream-only field
+ # if restore_field_name is None:
+ # continue
+ #
# # If this field's value doesn't match the synced upstream value, then mark the field
# # as customized so that we don't clobber it later when syncing.
# # NOTE: Need to consider the performance impact of all these field lookups.
diff --git a/cms/static/sass/course-unit-mfe-iframe-bundle.scss b/cms/static/sass/course-unit-mfe-iframe-bundle.scss
index bc0c3901b147..1f813fac253d 100644
--- a/cms/static/sass/course-unit-mfe-iframe-bundle.scss
+++ b/cms/static/sass/course-unit-mfe-iframe-bundle.scss
@@ -200,7 +200,7 @@ body {
}
}
- .modal-window.modal-editor {
+ .modal-window.modal-editor, &.xblock-iframe-content {
background-color: $white;
border-radius: 6px;
@@ -381,6 +381,37 @@ body {
.modal-lg.modal-window.confirm.openassessment_modal_window {
height: 635px;
}
+
+ // Additions for the xblock editor on the Library Authoring
+ &.xblock-iframe-content {
+ // Reset the max-height to allow the settings list to grow
+ .wrapper-comp-settings .list-input.settings-list {
+ max-height: unset;
+ }
+
+ // For Google Docs and Google Calendar xblock editor
+ .google-edit-wrapper .xblock-inputs {
+ position: unset;
+ overflow-y: unset;
+ }
+
+ .xblock-actions {
+ padding: ($baseline*0.75) 2% ($baseline/2) 2%;
+ position: sticky;
+ bottom: 0;
+
+ .action-item {
+ @extend %t-action3;
+
+ display: inline-block;
+ margin-right: ($baseline*0.75);
+
+ &:last-child {
+ margin-right: 0;
+ }
+ }
+ }
+ }
}
.view-container .content-primary {
diff --git a/cms/urls.py b/cms/urls.py
index 2e64d4bbeb79..58503f9ed92f 100644
--- a/cms/urls.py
+++ b/cms/urls.py
@@ -168,7 +168,7 @@
contentstore_views.textbooks_detail_handler, name='textbooks_detail_handler'),
re_path(fr'^videos/{settings.COURSE_KEY_PATTERN}(?:/(?P[-\w]+))?$',
contentstore_views.videos_handler, name='videos_handler'),
- re_path(fr'^generate_video_upload_link/{settings.COURSE_KEY_PATTERN}',
+ re_path(fr'^generate_video_upload_link/{settings.COURSE_KEY_PATTERN}$',
contentstore_views.generate_video_upload_link_handler, name='generate_video_upload_link'),
re_path(fr'^video_images/{settings.COURSE_KEY_PATTERN}(?:/(?P[-\w]+))?$',
contentstore_views.video_images_handler, name='video_images_handler'),
diff --git a/common/djangoapps/course_modes/tests/test_views.py b/common/djangoapps/course_modes/tests/test_views.py
index d7b069865317..b4a777d2d677 100644
--- a/common/djangoapps/course_modes/tests/test_views.py
+++ b/common/djangoapps/course_modes/tests/test_views.py
@@ -21,7 +21,6 @@
from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, UserFactory
from common.djangoapps.util.testing import UrlResetMixin
from common.djangoapps.util.tests.mixins.discovery import CourseCatalogServiceMockMixin
-from edx_toggles.toggles.testutils import override_waffle_flag # lint-amnesty, pylint: disable=wrong-import-order
from lms.djangoapps.commerce.tests import test_utils as ecomm_test_utils
from lms.djangoapps.commerce.tests.mocks import mock_payment_processors
from lms.djangoapps.verify_student.services import IDVerificationService
@@ -33,8 +32,6 @@
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.modulestore.tests.factories import CourseFactory # lint-amnesty, pylint: disable=wrong-import-order
-from ..views import VALUE_PROP_TRACK_SELECTION_FLAG
-
# Name of the method to mock for Content Type Gating.
GATING_METHOD_NAME = 'openedx.features.content_type_gating.models.ContentTypeGatingConfig.enabled_for_enrollment'
@@ -186,27 +183,6 @@ def test_suggested_prices(self, price_list):
# TODO: Fix it so that response.templates works w/ mako templates, and then assert
# that the right template rendered
- @httpretty.activate
- @ddt.data(
- (['honor', 'verified', 'credit'], True),
- (['honor', 'verified'], False),
- )
- @ddt.unpack
- def test_credit_upsell_message(self, available_modes, show_upsell):
- # Create the course modes
- for mode in available_modes:
- CourseModeFactory.create(mode_slug=mode, course_id=self.course.id)
-
- # Check whether credit upsell is shown on the page
- # This should *only* be shown when a credit mode is available
- url = reverse('course_modes_choose', args=[str(self.course.id)])
- response = self.client.get(url)
-
- if show_upsell:
- self.assertContains(response, "Credit")
- else:
- self.assertNotContains(response, "Credit")
-
@httpretty.activate
@patch('common.djangoapps.course_modes.views.enterprise_customer_for_request')
@patch('common.djangoapps.course_modes.views.get_course_final_price')
@@ -240,29 +216,6 @@ def test_display_after_discounted_price(
self.assertContains(response, discounted_price)
self.assertContains(response, verified_mode.min_price)
- @httpretty.activate
- @ddt.data(True, False)
- def test_congrats_on_enrollment_message(self, create_enrollment):
- # Create the course mode
- CourseModeFactory.create(mode_slug='verified', course_id=self.course.id)
-
- if create_enrollment:
- CourseEnrollmentFactory(
- is_active=True,
- course_id=self.course.id,
- user=self.user
- )
-
- # Check whether congratulations message is shown on the page
- # This should *only* be shown when an enrollment exists
- url = reverse('course_modes_choose', args=[str(self.course.id)])
- response = self.client.get(url)
-
- if create_enrollment:
- self.assertContains(response, "Congratulations! You are now enrolled in")
- else:
- self.assertNotContains(response, "Congratulations! You are now enrolled in")
-
@ddt.data('professional', 'no-id-professional')
def test_professional_enrollment(self, mode):
# The only course mode is professional ed
@@ -529,26 +482,24 @@ def test_errors(self, has_perm, post_params, error_msg, status_code, mock_has_pe
for mode in ('audit', 'honor', 'verified'):
CourseModeFactory.create(mode_slug=mode, course_id=self.course.id)
- # Value Prop TODO (REV-2378): remove waffle flag from tests once flag is removed.
- with override_waffle_flag(VALUE_PROP_TRACK_SELECTION_FLAG, active=True):
- mock_has_perm.return_value = has_perm
- url = reverse('course_modes_choose', args=[str(self.course.id)])
+ mock_has_perm.return_value = has_perm
+ url = reverse('course_modes_choose', args=[str(self.course.id)])
- # Choose mode (POST request)
- response = self.client.post(url, post_params)
- self.assertEqual(response.status_code, status_code)
+ # Choose mode (POST request)
+ response = self.client.post(url, post_params)
+ self.assertEqual(response.status_code, status_code)
- if has_perm:
- self.assertContains(response, error_msg)
- self.assertContains(response, 'Sorry, we were unable to enroll you')
+ if has_perm:
+ self.assertContains(response, error_msg)
+ self.assertContains(response, 'Sorry, we were unable to enroll you')
- # Check for CTA button on error page
- marketing_root = settings.MKTG_URLS.get('ROOT')
- search_courses_url = urljoin(marketing_root, '/search?tab=course')
- self.assertContains(response, search_courses_url)
- self.assertContains(response, 'Explore all courses')
- else:
- self.assertTrue(CourseEnrollment.is_enrollment_closed(self.user, self.course))
+ # Check for CTA button on error page
+ marketing_root = settings.MKTG_URLS.get('ROOT')
+ search_courses_url = urljoin(marketing_root, '/search?tab=course')
+ self.assertContains(response, search_courses_url)
+ self.assertContains(response, 'Explore all courses')
+ else:
+ self.assertTrue(CourseEnrollment.is_enrollment_closed(self.user, self.course))
def _assert_fbe_page(self, response, min_price=None, **_):
"""
@@ -607,33 +558,19 @@ def _assert_unfbe_page(self, response, min_price=None, **_):
# Check for the HTML element for courses with more than one mode
self.assertContains(response, '
')
- def _assert_legacy_page(self, response, **_):
- """
- Assert choose.html was rendered.
- """
- # Check for string unique to the legacy choose.html.
- self.assertContains(response, "Choose Your Track")
- # This string only occurs in lms/templates/course_modes/choose.html
- # and related theme and translation files.
-
@override_settings(MKTG_URLS={'ROOT': 'https://www.example.edx.org'})
@ddt.data(
- # gated_content_on, course_duration_limits_on, waffle_flag_on, expected_page_assertion_function
- (True, True, True, _assert_fbe_page),
- (True, False, True, _assert_unfbe_page),
- (False, True, True, _assert_unfbe_page),
- (False, False, True, _assert_unfbe_page),
- (True, True, False, _assert_legacy_page),
- (True, False, False, _assert_legacy_page),
- (False, True, False, _assert_legacy_page),
- (False, False, False, _assert_legacy_page),
+ # gated_content_on, course_duration_limits_on, expected_page_assertion_function
+ (True, True, _assert_fbe_page),
+ (True, False, _assert_unfbe_page),
+ (False, True, _assert_unfbe_page),
+ (False, False, _assert_unfbe_page),
)
@ddt.unpack
def test_track_selection_types(
self,
gated_content_on,
course_duration_limits_on,
- waffle_flag_on,
expected_page_assertion_function
):
"""
@@ -644,7 +581,6 @@ def test_track_selection_types(
verified course modes), the learner may view 3 different pages:
1. fbe.html - full FBE
2. unfbe.html - partial or no FBE
- 3. choose.html - legacy track selection page
This test checks that the right template is rendered.
@@ -667,15 +603,11 @@ def test_track_selection_types(
user=self.user
)
- # Value Prop TODO (REV-2378): remove waffle flag from tests once the new Track Selection template is rolled out.
- # Check whether new track selection template is rendered.
- # This should *only* be shown when the waffle flag is on.
- with override_waffle_flag(VALUE_PROP_TRACK_SELECTION_FLAG, active=waffle_flag_on):
- with patch(GATING_METHOD_NAME, return_value=gated_content_on):
- with patch(CDL_METHOD_NAME, return_value=course_duration_limits_on):
- url = reverse('course_modes_choose', args=[str(self.course_that_started.id)])
- response = self.client.get(url)
- expected_page_assertion_function(self, response, min_price=verified_mode.min_price)
+ with patch(GATING_METHOD_NAME, return_value=gated_content_on):
+ with patch(CDL_METHOD_NAME, return_value=course_duration_limits_on):
+ url = reverse('course_modes_choose', args=[str(self.course_that_started.id)])
+ response = self.client.get(url)
+ expected_page_assertion_function(self, response, min_price=verified_mode.min_price)
def test_verified_mode_only(self):
# Create only the verified mode and enroll the user
@@ -690,18 +622,16 @@ def test_verified_mode_only(self):
user=self.user
)
- # Value Prop TODO (REV-2378): remove waffle flag from tests once the new Track Selection template is rolled out.
- with override_waffle_flag(VALUE_PROP_TRACK_SELECTION_FLAG, active=True):
- with patch(GATING_METHOD_NAME, return_value=True):
- with patch(CDL_METHOD_NAME, return_value=True):
- url = reverse('course_modes_choose', args=[str(self.course_that_started.id)])
- response = self.client.get(url)
- # Check that only the verified option is rendered
- self.assertNotContains(response, "Choose a path for your course in")
- self.assertContains(response, "Earn a certificate")
- self.assertNotContains(response, "Access this course")
- self.assertContains(response, '
')
- self.assertNotContains(response, '
')
+ with patch(GATING_METHOD_NAME, return_value=True):
+ with patch(CDL_METHOD_NAME, return_value=True):
+ url = reverse('course_modes_choose', args=[str(self.course_that_started.id)])
+ response = self.client.get(url)
+ # Check that only the verified option is rendered
+ self.assertNotContains(response, "Choose a path for your course in")
+ self.assertContains(response, "Earn a certificate")
+ self.assertNotContains(response, "Access this course")
+ self.assertContains(response, '
')
+ self.assertNotContains(response, '
')
@skip_unless_lms
diff --git a/common/djangoapps/course_modes/views.py b/common/djangoapps/course_modes/views.py
index 759073a13583..09164dc40e7c 100644
--- a/common/djangoapps/course_modes/views.py
+++ b/common/djangoapps/course_modes/views.py
@@ -29,7 +29,6 @@
from common.djangoapps.course_modes.helpers import get_course_final_price, get_verified_track_links
from common.djangoapps.edxmako.shortcuts import render_to_response
from common.djangoapps.util.date_utils import strftime_localized_html
-from edx_toggles.toggles import WaffleFlag # lint-amnesty, pylint: disable=wrong-import-order
from lms.djangoapps.commerce.utils import EcommerceService
from lms.djangoapps.experiments.utils import get_experiment_user_metadata_context
from lms.djangoapps.verify_student.services import IDVerificationService
@@ -47,17 +46,6 @@
LOG = logging.getLogger(__name__)
-# .. toggle_name: course_modes.use_new_track_selection
-# .. toggle_implementation: WaffleFlag
-# .. toggle_default: False
-# .. toggle_description: This flag enables the use of the new track selection template for testing purposes before full rollout
-# .. toggle_use_cases: temporary
-# .. toggle_creation_date: 2021-8-23
-# .. toggle_target_removal_date: None
-# .. toggle_tickets: REV-2133
-# .. toggle_warning: This temporary feature toggle does not have a target removal date.
-VALUE_PROP_TRACK_SELECTION_FLAG = WaffleFlag('course_modes.use_new_track_selection', __name__)
-
class ChooseModeView(View):
"""View used when the user is asked to pick a mode.
@@ -158,18 +146,6 @@ def get(self, request, course_id, error=None): # lint-amnesty, pylint: disable=
)
return redirect('{}?{}'.format(reverse('dashboard'), params))
- # When a credit mode is available, students will be given the option
- # to upgrade from a verified mode to a credit mode at the end of the course.
- # This allows students who have completed photo verification to be eligible
- # for university credit.
- # Since credit isn't one of the selectable options on the track selection page,
- # we need to check *all* available course modes in order to determine whether
- # a credit mode is available. If so, then we show slightly different messaging
- # for the verified track.
- has_credit_upsell = any(
- CourseMode.is_credit_mode(mode) for mode
- in CourseMode.modes_for_course(course_key, only_selectable=False)
- )
course_id = str(course_key)
gated_content = ContentTypeGatingConfig.enabled_for_enrollment(
user=request.user,
@@ -184,7 +160,6 @@ def get(self, request, course_id, error=None): # lint-amnesty, pylint: disable=
),
"modes": modes,
"is_single_mode": is_single_mode,
- "has_credit_upsell": has_credit_upsell,
"course_name": course.display_name_with_default,
"course_org": course.display_org_with_default,
"course_num": course.display_number_with_default,
@@ -204,14 +179,6 @@ def get(self, request, course_id, error=None): # lint-amnesty, pylint: disable=
)
)
- title_content = ''
- if enrollment_mode:
- title_content = _("Congratulations! You are now enrolled in {course_name}").format(
- course_name=course.display_name_with_default
- )
-
- context["title_content"] = title_content
-
if "verified" in modes:
verified_mode = modes["verified"]
context["suggested_prices"] = [
@@ -266,19 +233,12 @@ def get(self, request, course_id, error=None): # lint-amnesty, pylint: disable=
context['audit_access_deadline'] = formatted_audit_access_date
fbe_is_on = deadline and gated_content
- # Route to correct Track Selection page.
- # REV-2378 TODO Value Prop: remove waffle flag after all edge cases for track selection are completed.
- if VALUE_PROP_TRACK_SELECTION_FLAG.is_enabled():
- if not enterprise_customer_for_request(request): # TODO: Remove by executing REV-2342
- if error:
- return render_to_response("course_modes/error.html", context)
- if fbe_is_on:
- return render_to_response("course_modes/fbe.html", context)
- else:
- return render_to_response("course_modes/unfbe.html", context)
-
- # If enterprise_customer, failover to old choose.html page
- return render_to_response("course_modes/choose.html", context)
+ if error:
+ return render_to_response("course_modes/error.html", context)
+ if fbe_is_on:
+ return render_to_response("course_modes/fbe.html", context)
+ else:
+ return render_to_response("course_modes/unfbe.html", context)
@method_decorator(transaction.non_atomic_requests)
@method_decorator(login_required)
diff --git a/common/djangoapps/student/management/commands/bulk_change_enrollment_csv.py b/common/djangoapps/student/management/commands/bulk_change_enrollment_csv.py
index a9c1d5349858..69f2e35de44a 100644
--- a/common/djangoapps/student/management/commands/bulk_change_enrollment_csv.py
+++ b/common/djangoapps/student/management/commands/bulk_change_enrollment_csv.py
@@ -81,7 +81,7 @@ def handle(self, *args, **options):
self.change_enrollments(csv_file)
else:
- CommandError('No file is provided. File is required')
+ raise CommandError('No file is provided. File is required')
def change_enrollments(self, csv_file):
""" change the enrollments of the learners. """
diff --git a/common/djangoapps/track/views/segmentio.py b/common/djangoapps/track/views/segmentio.py
index 2ab5306232c5..8a35a0e421cf 100644
--- a/common/djangoapps/track/views/segmentio.py
+++ b/common/djangoapps/track/views/segmentio.py
@@ -205,9 +205,8 @@ def track_segmentio_event(request): # pylint: disable=too-many-statements
raise EventValidationError(ERROR_USER_NOT_EXIST) # lint-amnesty, pylint: disable=raise-missing-from
except ValueError:
raise EventValidationError(ERROR_INVALID_USER_ID) # lint-amnesty, pylint: disable=raise-missing-from
- else:
- context['user_id'] = user.id
- context['username'] = user.username
+ context['user_id'] = user.id
+ context['username'] = user.username
# course_id is expected to be provided in the context when applicable
course_id = context.get('course_id')
diff --git a/common/templates/xblock_v2/xblock_iframe.html b/common/templates/xblock_v2/xblock_iframe.html
index 07d81b962a65..57ff4684ddbd 100644
--- a/common/templates/xblock_v2/xblock_iframe.html
+++ b/common/templates/xblock_v2/xblock_iframe.html
@@ -1,6 +1,7 @@
-
+
+
@@ -81,8 +82,11 @@
href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
-
-
+
+
+
+
+
-%block>
-
-<%block name="content">
- % if error:
-
-
-
-
-
${_("Sorry, there was an error when trying to enroll you")}
-
-
${error}
-
-
-
-
- %endif
-
-
-
-
-
-
-
- ${title_content}
-
-
-
-
-
-
-
-
-%block>
diff --git a/openedx/core/djangoapps/content_libraries/views.py b/openedx/core/djangoapps/content_libraries/views.py
index 048226c5b16c..a1b7a2602450 100644
--- a/openedx/core/djangoapps/content_libraries/views.py
+++ b/openedx/core/djangoapps/content_libraries/views.py
@@ -81,6 +81,7 @@
from django.views.decorators.clickjacking import xframe_options_exempt
from django.views.decorators.csrf import csrf_exempt
from django.views.generic.base import TemplateResponseMixin, View
+from drf_yasg.utils import swagger_auto_schema
from pylti1p3.contrib.django import DjangoCacheDataStorage, DjangoDbToolConf, DjangoMessageLaunch, DjangoOIDCLogin
from pylti1p3.exception import LtiException, OIDCException
@@ -201,8 +202,10 @@ class LibraryRootView(GenericAPIView):
"""
Views to list, search for, and create content libraries.
"""
+ serializer_class = ContentLibraryMetadataSerializer
@apidocs.schema(
+ responses={200: ContentLibraryMetadataSerializer(many=True)},
parameters=[
*LibraryApiPaginationDocs.apidoc_params,
apidocs.query_parameter(
@@ -530,7 +533,13 @@ class LibraryPasteClipboardView(GenericAPIView):
"""
Paste content of clipboard into Library.
"""
+ serializer_class = LibraryXBlockMetadataSerializer
+
@convert_exceptions
+ @swagger_auto_schema(
+ request_body=LibraryPasteClipboardSerializer,
+ responses={200: LibraryXBlockMetadataSerializer}
+ )
def post(self, request, lib_key_str):
"""
Import the contents of the user's clipboard and paste them into the Library
@@ -558,6 +567,7 @@ class LibraryBlocksView(GenericAPIView):
"""
Views to work with XBlocks in a specific content library.
"""
+ serializer_class = LibraryXBlockMetadataSerializer
@apidocs.schema(
parameters=[
@@ -595,6 +605,10 @@ def get(self, request, lib_key_str):
return self.get_paginated_response(serializer.data)
@convert_exceptions
+ @swagger_auto_schema(
+ request_body=LibraryXBlockCreationSerializer,
+ responses={200: LibraryXBlockMetadataSerializer}
+ )
def post(self, request, lib_key_str):
"""
Add a new XBlock to this content library
@@ -870,6 +884,9 @@ class LibraryImportTaskViewSet(GenericViewSet):
Import blocks from Courseware through modulestore.
"""
+ queryset = [] # type: ignore[assignment]
+ serializer_class = ContentLibraryBlockImportTaskSerializer
+
@convert_exceptions
def list(self, request, lib_key_str):
"""
@@ -889,6 +906,10 @@ def list(self, request, lib_key_str):
)
@convert_exceptions
+ @swagger_auto_schema(
+ request_body=ContentLibraryBlockImportTaskCreateSerializer,
+ responses={200: ContentLibraryBlockImportTaskSerializer}
+ )
def create(self, request, lib_key_str):
"""
Create and queue an import tasks for this library.
diff --git a/openedx/core/djangoapps/content_staging/tests/test_clipboard.py b/openedx/core/djangoapps/content_staging/tests/test_clipboard.py
index 551f94e90e1a..ab65d444ed6f 100644
--- a/openedx/core/djangoapps/content_staging/tests/test_clipboard.py
+++ b/openedx/core/djangoapps/content_staging/tests/test_clipboard.py
@@ -1,3 +1,4 @@
+# pylint: skip-file
"""
Tests for the clipboard functionality
"""
diff --git a/openedx/core/djangoapps/credentials/management/commands/notify_credentials.py b/openedx/core/djangoapps/credentials/management/commands/notify_credentials.py
index cbfdd8e7337f..f79ead24d9b0 100644
--- a/openedx/core/djangoapps/credentials/management/commands/notify_credentials.py
+++ b/openedx/core/djangoapps/credentials/management/commands/notify_credentials.py
@@ -7,11 +7,13 @@
This management command will manually trigger the receivers we care about. (We don't want to trigger all receivers
for these signals, since these are busy signals.)
"""
+
import logging
import shlex
from datetime import datetime, timedelta
import dateutil.parser
+from django.conf import settings
from django.core.management.base import BaseCommand, CommandError
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey
@@ -58,6 +60,7 @@ class Command(BaseCommand):
course-v1:edX+RecordsSelfPaced+1 for user 17
course-v1:edX+RecordsSelfPaced+1 for user 18
"""
+
help = (
"Simulate certificate/grade changes without actually modifying database "
"content. Specifically, trigger the handlers that send data to Credentials."
@@ -65,98 +68,99 @@ class Command(BaseCommand):
def add_arguments(self, parser):
parser.add_argument(
- '--dry-run',
- action='store_true',
- help='Just show a preview of what would happen.',
+ "--dry-run",
+ action="store_true",
+ help="Just show a preview of what would happen.",
)
parser.add_argument(
- '--site',
+ "--site",
default=None,
help="Site domain to notify for (if not specified, all sites are notified). Uses course_org_filter.",
)
parser.add_argument(
- '--courses',
- nargs='+',
- help='Send information only for specific course runs.',
+ "--courses",
+ nargs="+",
+ help="Send information only for specific course runs.",
)
parser.add_argument(
- '--program_uuids',
- nargs='+',
- help='Send user data for course runs for courses within a program based on program uuids provided.',
+ "--program_uuids",
+ nargs="+",
+ help="Send user data for course runs for courses within a program based on program uuids provided.",
)
parser.add_argument(
- '--start-date',
+ "--start-date",
type=parsetime,
- help='Send information only for certificates or grades that have changed since this date.',
+ help="Send information only for certificates or grades that have changed since this date.",
)
parser.add_argument(
- '--end-date',
+ "--end-date",
type=parsetime,
- help='Send information only for certificates or grades that have changed before this date.',
+ help="Send information only for certificates or grades that have changed before this date.",
)
parser.add_argument(
- '--delay',
+ "--delay",
type=float,
default=0,
help="Number of seconds to sleep between processing queries, so that we don't flood our queues.",
)
parser.add_argument(
- '--page-size',
+ "--page-size",
type=int,
default=100,
help="Number of items to query at once.",
)
parser.add_argument(
- '--auto',
- action='store_true',
- help='Use to run the management command periodically',
+ "--auto",
+ action="store_true",
+ help="Use to run the management command periodically",
)
parser.add_argument(
- '--args-from-database',
- action='store_true',
- help='Use arguments from the NotifyCredentialsConfig model instead of the command line.',
+ "--args-from-database",
+ action="store_true",
+ help="Use arguments from the NotifyCredentialsConfig model instead of the command line.",
)
parser.add_argument(
- '--verbose',
- action='store_true',
- help='Run grade/cert change signal in verbose mode',
+ "--verbose",
+ action="store_true",
+ help="Run grade/cert change signal in verbose mode",
)
parser.add_argument(
- '--notify_programs',
- action='store_true',
- help='Send program award notifications with course notification tasks',
+ "--notify_programs",
+ action="store_true",
+ help="Send program award notifications with course notification tasks",
)
parser.add_argument(
- '--user_ids',
+ "--user_ids",
default=None,
- nargs='+',
- help='Run the command for the given user or list of users',
+ nargs="+",
+ help="Run the command for the given user or list of users",
)
parser.add_argument(
- '--revoke_program_certs',
- action='store_true',
- help="If true, system will check if any program certificates need to be revoked from learners"
+ "--revoke_program_certs",
+ action="store_true",
+ help="If true, system will check if any program certificates need to be revoked from learners",
)
def get_args_from_database(self):
- """ Returns an options dictionary from the current NotifyCredentialsConfig model. """
+ """Returns an options dictionary from the current NotifyCredentialsConfig model."""
config = NotifyCredentialsConfig.current()
if not config.enabled:
- raise CommandError('NotifyCredentialsConfig is disabled, but --args-from-database was requested.')
+ raise CommandError("NotifyCredentialsConfig is disabled, but --args-from-database was requested.")
# This split will allow for quotes to wrap datetimes, like "2020-10-20 04:00:00" and other
# arguments as if it were the command line
argv = shlex.split(config.arguments)
- parser = self.create_parser('manage.py', 'notify_credentials')
- return parser.parse_args(argv).__dict__ # we want a dictionary, not a non-iterable Namespace object
+ parser = self.create_parser("manage.py", "notify_credentials")
+ return parser.parse_args(argv).__dict__ # we want a dictionary, not a non-iterable Namespace object
def handle(self, *args, **options):
- if options['args_from_database']:
+ if options["args_from_database"]:
options = self.get_args_from_database()
- if options['auto']:
- options['end_date'] = datetime.now().replace(minute=0, second=0, microsecond=0)
- options['start_date'] = options['end_date'] - timedelta(hours=4)
+ if options["auto"]:
+ run_frequency = settings.NOTIFY_CREDENTIALS_FREQUENCY
+ options["end_date"] = datetime.now().replace(minute=0, second=0, microsecond=0)
+ options["start_date"] = options["end_date"] - timedelta(seconds=run_frequency)
log.info(
f"notify_credentials starting, dry-run={options['dry_run']}, site={options['site']}, "
@@ -176,14 +180,9 @@ def handle(self, *args, **options):
course_runs.extend(program_course_run_keys)
course_run_keys = self._get_validated_course_run_keys(course_runs)
- if not (
- course_run_keys or
- options['start_date'] or
- options['end_date'] or
- options['user_ids']
- ):
+ if not (course_run_keys or options["start_date"] or options["end_date"] or options["user_ids"]):
raise CommandError(
- 'You must specify a filter (e.g. --courses, --program_uuids, --start-date, or --user_ids)'
+ "You must specify a filter (e.g. --courses, --program_uuids, --start-date, or --user_ids)"
)
handle_notify_credentials.delay(options, course_run_keys)
diff --git a/openedx/core/djangoapps/credentials/management/commands/tests/test_notify_credentials.py b/openedx/core/djangoapps/credentials/management/commands/tests/test_notify_credentials.py
index e50173001fd3..f7bed3446aca 100644
--- a/openedx/core/djangoapps/credentials/management/commands/tests/test_notify_credentials.py
+++ b/openedx/core/djangoapps/credentials/management/commands/tests/test_notify_credentials.py
@@ -7,7 +7,7 @@
from django.core.management import call_command
from django.core.management.base import CommandError
-from django.test import TestCase, override_settings # lint-amnesty, pylint: disable=unused-import
+from django.test import TestCase, override_settings
from freezegun import freeze_time
from openedx.core.djangoapps.catalog.tests.factories import ProgramFactory, CourseFactory, CourseRunFactory
@@ -125,6 +125,7 @@ def test_multiple_programs_uuid_args(self, mock_get_programs, mock_task):
@freeze_time(datetime(2017, 5, 1, 4))
def test_auto_execution(self, mock_task):
+ """Verify that an automatic execution designed for scheduled windows works correctly"""
self.expected_options['auto'] = True
self.expected_options['start_date'] = datetime(2017, 5, 1, 0, 0)
self.expected_options['end_date'] = datetime(2017, 5, 1, 4, 0)
@@ -133,6 +134,19 @@ def test_auto_execution(self, mock_task):
assert mock_task.called
assert mock_task.call_args[0][0] == self.expected_options
+ @override_settings(NOTIFY_CREDENTIALS_FREQUENCY=3600)
+ @freeze_time(datetime(2017, 5, 1, 4))
+ def test_auto_execution_different_schedule(self, mock_task):
+ """Verify that an automatic execution designed for scheduled windows
+ works correctly if the window frequency has been changed"""
+ self.expected_options["auto"] = True
+ self.expected_options["start_date"] = datetime(2017, 5, 1, 3, 0)
+ self.expected_options["end_date"] = datetime(2017, 5, 1, 4, 0)
+
+ call_command(Command(), "--auto")
+ assert mock_task.called
+ assert mock_task.call_args[0][0] == self.expected_options
+
def test_date_args(self, mock_task):
self.expected_options['start_date'] = datetime(2017, 1, 31, 0, 0, tzinfo=timezone.utc)
call_command(Command(), '--start-date', '2017-01-31')
diff --git a/openedx/core/djangoapps/credit/tasks.py b/openedx/core/djangoapps/credit/tasks.py
index 312e278a985f..79ef613e3d19 100644
--- a/openedx/core/djangoapps/credit/tasks.py
+++ b/openedx/core/djangoapps/credit/tasks.py
@@ -41,8 +41,7 @@ def update_credit_course_requirements(course_id):
except (InvalidKeyError, ItemNotFoundError, InvalidCreditRequirements) as exc:
LOGGER.error('Error on adding the requirements for course %s - %s', course_id, str(exc))
raise update_credit_course_requirements.retry(args=[course_id], exc=exc)
- else:
- LOGGER.info('Requirements added for course %s', course_id)
+ LOGGER.info('Requirements added for course %s', course_id)
def _get_course_credit_requirements(course_key):
diff --git a/openedx/core/djangoapps/enrollments/data.py b/openedx/core/djangoapps/enrollments/data.py
index 9986830a3491..b76042f72c9d 100644
--- a/openedx/core/djangoapps/enrollments/data.py
+++ b/openedx/core/djangoapps/enrollments/data.py
@@ -341,8 +341,7 @@ def get_course_enrollment_info(course_id, include_expired=False):
msg = f"Requested enrollment information for unknown course {course_id}"
log.warning(msg)
raise CourseNotFoundError(msg) # lint-amnesty, pylint: disable=raise-missing-from
- else:
- return CourseSerializer(course, include_expired=include_expired).data
+ return CourseSerializer(course, include_expired=include_expired).data
def get_user_roles(username):
diff --git a/openedx/core/djangoapps/safe_sessions/middleware.py b/openedx/core/djangoapps/safe_sessions/middleware.py
index f3948217efd9..950a3e08c54f 100644
--- a/openedx/core/djangoapps/safe_sessions/middleware.py
+++ b/openedx/core/djangoapps/safe_sessions/middleware.py
@@ -244,14 +244,13 @@ def parse(cls, safe_cookie_string):
raise SafeCookieError( # lint-amnesty, pylint: disable=raise-missing-from
f"SafeCookieData BWC parse error: {safe_cookie_string!r}."
)
- else:
- if safe_cookie_data.version != cls.CURRENT_VERSION:
- raise SafeCookieError(
- "SafeCookieData version {!r} is not supported. Current version is {}.".format(
- safe_cookie_data.version,
- cls.CURRENT_VERSION,
- ))
- return safe_cookie_data
+ if safe_cookie_data.version != cls.CURRENT_VERSION:
+ raise SafeCookieError(
+ "SafeCookieData version {!r} is not supported. Current version is {}.".format(
+ safe_cookie_data.version,
+ cls.CURRENT_VERSION,
+ ))
+ return safe_cookie_data
def __str__(self):
"""
diff --git a/openedx/core/djangoapps/user_api/management/commands/bulk_user_org_email_optout.py b/openedx/core/djangoapps/user_api/management/commands/bulk_user_org_email_optout.py
index e465ff5610ed..d194e58ee880 100644
--- a/openedx/core/djangoapps/user_api/management/commands/bulk_user_org_email_optout.py
+++ b/openedx/core/djangoapps/user_api/management/commands/bulk_user_org_email_optout.py
@@ -135,11 +135,10 @@ def handle(self, *args, **options):
optout_rows[end_idx][0], optout_rows[end_idx][1],
str(err))
raise
- else:
- cursor.execute('COMMIT;')
- log.info("Committed opt-out for rows (%s, %s) through (%s, %s).",
- optout_rows[start_idx][0], optout_rows[start_idx][1],
- optout_rows[end_idx][0], optout_rows[end_idx][1])
+ cursor.execute('COMMIT;')
+ log.info("Committed opt-out for rows (%s, %s) through (%s, %s).",
+ optout_rows[start_idx][0], optout_rows[start_idx][1],
+ optout_rows[end_idx][0], optout_rows[end_idx][1])
log.info("Sleeping %s seconds...", sleep_between)
time.sleep(sleep_between)
curr_row_idx += chunk_size
diff --git a/openedx/core/djangoapps/user_authn/views/login_form.py b/openedx/core/djangoapps/user_authn/views/login_form.py
index bb78a9df1a3c..3729c40868f6 100644
--- a/openedx/core/djangoapps/user_authn/views/login_form.py
+++ b/openedx/core/djangoapps/user_authn/views/login_form.py
@@ -187,12 +187,7 @@ def login_and_registration_form(request, initial_mode="login"):
log.exception("Unknown tpa_hint provider: %s", ex)
# Redirect to authn MFE if it is enabled
- # AND
- # user is not an enterprise user
- # AND
- # tpa_hint_provider is not available
- # AND
- # user is not coming from a SAML IDP.
+ # except if user is an enterprise user with tpa_hint_provider coming from a SAML IDP.
saml_provider = False
running_pipeline = pipeline.get(request)
if running_pipeline:
@@ -202,10 +197,8 @@ def login_and_registration_form(request, initial_mode="login"):
enterprise_customer = enterprise_customer_for_request(request)
- if should_redirect_to_authn_microfrontend() and \
- not enterprise_customer and \
- not tpa_hint_provider and \
- not saml_provider:
+ if should_redirect_to_authn_microfrontend() and not \
+ (enterprise_customer and tpa_hint_provider and saml_provider):
# This is to handle a case where a logged-in cookie is not present but the user is authenticated.
# Note: If we don't handle this learner is redirected to authn MFE and then back to dashboard
diff --git a/openedx/core/djangoapps/user_authn/views/tests/test_logistration.py b/openedx/core/djangoapps/user_authn/views/tests/test_logistration.py
index 3220cd513974..5b387adaf8ca 100644
--- a/openedx/core/djangoapps/user_authn/views/tests/test_logistration.py
+++ b/openedx/core/djangoapps/user_authn/views/tests/test_logistration.py
@@ -648,6 +648,80 @@ def test_browser_language_dialent(self):
assert response['Content-Language'] == 'es-es'
+ @ddt.data(
+ (None, None, None, True),
+ ({
+ 'name': 'Test Enterprise',
+ 'uuid': 'test-uuid'
+ }, None, None, True),
+ ({
+ 'name': 'Test Enterprise',
+ 'uuid': 'test-uuid'
+ }, 'test-provider', None, True),
+ ({
+ 'name': 'Test Enterprise',
+ 'uuid': 'test-uuid'
+ }, 'test-provider', True, False),
+ )
+ @ddt.unpack
+ @override_settings(FEATURES=FEATURES_WITH_AUTHN_MFE_ENABLED)
+ def test_enterprise_saml_redirection(self, enterprise_customer_data, provider_id, is_saml, should_redirect):
+ """
+ Test that authentication MFE redirection respects the enterprise + SAML provider conditions.
+ In particular, verify that if we have an enterprise customer with a SAML-based tpa_hint_provider,
+ we do NOT redirect to the MFE, but handle the request in LMS. All other combinations should
+ redirect to the MFE when it's enabled.
+ """
+ if provider_id and is_saml:
+ self.enable_saml()
+ self._configure_testshib_provider('TestShib', provider_id)
+
+ with mock.patch(
+ 'openedx.core.djangoapps.user_authn.views.login_form.enterprise_customer_for_request') as mock_ec, \
+ mock.patch(
+ 'openedx.core.djangoapps.user_authn.views.login_form.should_redirect_to_authn_microfrontend') as mock_should_redirect, \
+ mock.patch(
+ 'openedx.core.djangoapps.user_authn.views.login_form.third_party_auth.utils.is_saml_provider') as mock_is_saml:
+
+ mock_ec.return_value = enterprise_customer_data
+ mock_should_redirect.return_value = should_redirect
+ mock_is_saml.return_value = (True, None) if is_saml else (False, None)
+
+ params = {}
+ if provider_id:
+ params['tpa_hint'] = provider_id
+
+ if provider_id and is_saml:
+ pipeline_target = 'openedx.core.djangoapps.user_authn.views.login_form.third_party_auth.pipeline'
+ with mock.patch(pipeline_target + '.get') as mock_pipeline:
+ pipeline_data = {
+ 'backend': 'tpa-saml',
+ 'kwargs': {
+ 'response': {
+ 'idp_name': provider_id
+ },
+ 'details': {
+ 'email': 'test@example.com',
+ 'fullname': 'Test User',
+ 'username': 'testuser'
+ }
+ }
+ }
+ mock_pipeline.return_value = pipeline_data
+ response = self.client.get(reverse('signin_user'), params)
+ else:
+ response = self.client.get(reverse('signin_user'), params)
+
+ if should_redirect:
+ self.assertRedirects(
+ response,
+ settings.AUTHN_MICROFRONTEND_URL + '/login' +
+ ('?' + urlencode(params) if params else ''),
+ fetch_redirect_response=False
+ )
+ else:
+ self.assertEqual(response.status_code, 200)
+
@skip_unless_lms
class AccountCreationTestCaseWithSiteOverrides(SiteMixin, TestCase):
diff --git a/openedx/core/djangoapps/util/management/commands/dump_settings.py b/openedx/core/djangoapps/util/management/commands/dump_settings.py
new file mode 100644
index 000000000000..965ea16c6d27
--- /dev/null
+++ b/openedx/core/djangoapps/util/management/commands/dump_settings.py
@@ -0,0 +1,100 @@
+"""
+Defines the dump_settings management command.
+"""
+import inspect
+import json
+import re
+
+from django.conf import settings
+from django.core.management.base import BaseCommand
+
+
+SETTING_NAME_REGEX = re.compile(r'^[A-Z][A-Z0-9_]*$')
+
+
+class Command(BaseCommand):
+ """
+ Dump current Django settings to JSON for debugging/diagnostics.
+
+ BEWARE: OUTPUT IS NOT SUITABLE FOR CONSUMPTION BY PRODUCTION SYSTEMS.
+ The purpose of this output is to be *helpful* for a *human* operator to understand how their settings are being
+ rendered and how they differ between different settings files. The serialization format is NOT perfect: there are
+ certain situations where two different settings will output identical JSON. For example, this command does NOT:
+
+ disambiguate between lists and tuples:
+ * (1, 2, 3) # <-- this tuple will be printed out as [1, 2, 3]
+ * [1, 2, 3]
+
+ disambiguate between sets and sorted lists:
+ * {2, 1, 3} # <-- this set will be printed out as [1, 2, 3]
+ * [1, 2, 3]
+
+ disambiguate between internationalized and non-internationalized strings:
+ * _("hello") # <-- this will become just "hello"
+ * "hello"
+
+ Furthermore, functions and classes are printed as JSON objects like:
+ {
+ "module": "path.to.module",
+ "qualname": "MyClass.MyInnerClass.my_method", // Or, ""
+ "source_hint": "MY_SETTING = lambda: x + y", // For s only
+ }
+
+ And everything else will be stringified as its `repr(...)`.
+ """
+
+ def handle(self, *args, **kwargs):
+ """
+ Handle the command.
+ """
+ settings_json = {
+ name: _to_json_friendly_repr(getattr(settings, name))
+ for name in dir(settings)
+ if SETTING_NAME_REGEX.match(name)
+ }
+ print(json.dumps(settings_json, indent=4))
+
+
+def _to_json_friendly_repr(value: object) -> object:
+ """
+ Turn the value into something that we can print to a JSON file (that is: str, bool, None, int, float, list, dict).
+
+ See the docstring of `Command` for warnings about this function's behavior.
+ """
+ if isinstance(value, (type(None), bool, int, float, str)):
+ # All these types can be printed directly
+ return value
+ if isinstance(value, (list, tuple, set)):
+ if isinstance(value, set):
+ # Print sets by sorting them (so that order doesn't matter) into a JSON array.
+ elements = sorted(value)
+ else:
+ # Print both lists and tuples as JSON arrays.
+ elements = value
+ return [_to_json_friendly_repr(element) for ix, element in enumerate(elements)]
+ if isinstance(value, dict):
+ # Print dicts as JSON objects
+ for subkey in value.keys():
+ if not isinstance(subkey, (str, int)):
+ raise ValueError(f"Unexpected dict key {subkey} of type {type(subkey)}")
+ return {subkey: _to_json_friendly_repr(subval) for subkey, subval in value.items()}
+ if proxy_args := getattr(value, "_proxy____args", None):
+ if len(proxy_args) == 1 and isinstance(proxy_args[0], str):
+ # Print gettext_lazy as simply the wrapped string
+ return proxy_args[0]
+ try:
+ module = value.__module__
+ qualname = value.__qualname__
+ except AttributeError:
+ pass
+ else:
+ # Handle functions and classes by printing their location (plus approximate source, for lambdas)
+ return {
+ "module": module,
+ "qualname": qualname,
+ **({
+ "source_hint": inspect.getsource(value).strip(),
+ } if qualname == "" else {}),
+ }
+ # For all other objects, print the repr
+ return repr(value)
diff --git a/openedx/core/djangoapps/util/tests/test_dump_settings.py b/openedx/core/djangoapps/util/tests/test_dump_settings.py
new file mode 100644
index 000000000000..90171eb48c95
--- /dev/null
+++ b/openedx/core/djangoapps/util/tests/test_dump_settings.py
@@ -0,0 +1,64 @@
+"""
+Basic tests for dump_settings management command.
+
+These are moreso testing that dump_settings works, less-so testing anything about the Django
+settings files themselves. Remember that tests only run with (lms,cms)/envs/test.py,
+which are based on (lms,cms)/envs/common.py, so these tests will not execute any of the
+YAML-loading or post-processing defined in (lms,cms)/envs/production.py.
+"""
+import json
+
+from django.core.management import call_command
+
+from openedx.core.djangolib.testing.utils import skip_unless_lms, skip_unless_cms
+
+
+@skip_unless_lms
+def test_for_lms_settings(capsys):
+ """
+ Ensure LMS's test settings can be dumped, and sanity-check them for certain values.
+ """
+ dump = _get_settings_dump(capsys)
+
+ # Check: something LMS-specific
+ assert dump['MODULESTORE_BRANCH'] == "published-only"
+
+ # Check: tuples are converted to lists
+ assert isinstance(dump['XBLOCK_MIXINS'], list)
+
+ # Check: classes are converted to dicts of info on the class location
+ assert {"module": "xmodule.x_module", "qualname": "XModuleMixin"} in dump['XBLOCK_MIXINS']
+
+ # Check: nested dictionaries come through OK, and int'l strings are just strings
+ assert dump['COURSE_ENROLLMENT_MODES']['audit']['display_name'] == "Audit"
+
+
+@skip_unless_cms
+def test_for_cms_settings(capsys):
+ """
+ Ensure CMS's test settings can be dumped, and sanity-check them for certain values.
+ """
+ dump = _get_settings_dump(capsys)
+
+ # Check: something CMS-specific
+ assert dump['MODULESTORE_BRANCH'] == "draft-preferred"
+
+ # Check: tuples are converted to lists
+ assert isinstance(dump['XBLOCK_MIXINS'], list)
+
+ # Check: classes are converted to dicts of info on the class location
+ assert {"module": "xmodule.x_module", "qualname": "XModuleMixin"} in dump['XBLOCK_MIXINS']
+
+ # Check: nested dictionaries come through OK, and int'l strings are just strings
+ assert dump['COURSE_ENROLLMENT_MODES']['audit']['display_name'] == "Audit"
+
+
+def _get_settings_dump(captured_sys):
+ """
+ Call dump_settings, ensure no error output, and return parsed JSON.
+ """
+ call_command('dump_settings')
+ out, err = captured_sys.readouterr()
+ assert out
+ assert not err
+ return json.loads(out)
diff --git a/openedx/core/djangoapps/xblock/rest_api/views.py b/openedx/core/djangoapps/xblock/rest_api/views.py
index a1fbd1e062a6..0fd202b4eff2 100644
--- a/openedx/core/djangoapps/xblock/rest_api/views.py
+++ b/openedx/core/djangoapps/xblock/rest_api/views.py
@@ -118,10 +118,12 @@ def embed_block_view(request, usage_key: UsageKeyV2, view_name: str):
# for key in itertools.chain([block.scope_ids.usage_id], getattr(block, 'children', []))
# }
lms_root_url = configuration_helpers.get_value('LMS_ROOT_URL', settings.LMS_ROOT_URL)
+ cms_root_url = configuration_helpers.get_value('CMS_ROOT_URL', settings.CMS_ROOT_URL)
context = {
'fragment': fragment,
'handler_urls_json': json.dumps(handler_urls),
'lms_root_url': lms_root_url,
+ 'cms_root_url': cms_root_url,
'view_name': view_name,
'is_development': settings.DEBUG,
}
diff --git a/openedx/core/lib/api/view_utils.py b/openedx/core/lib/api/view_utils.py
index 054755ae3cc1..d876e49ae579 100644
--- a/openedx/core/lib/api/view_utils.py
+++ b/openedx/core/lib/api/view_utils.py
@@ -265,8 +265,7 @@ def __len__(self):
def __iter__(self):
# Yield all the known data first
- for item in self._data:
- yield item
+ yield from self._data
# Capture and yield data from the underlying iterator
# until it is exhausted
diff --git a/openedx/core/lib/celery/task_utils.py b/openedx/core/lib/celery/task_utils.py
index 738f074be68c..9a54f1b3a550 100644
--- a/openedx/core/lib/celery/task_utils.py
+++ b/openedx/core/lib/celery/task_utils.py
@@ -50,9 +50,8 @@ def emulate_http_request(site=None, user=None, middleware_classes=None):
for middleware in reversed(middleware_instances):
_run_method_if_implemented(middleware, 'process_exception', request, exc)
raise
- else:
- for middleware in reversed(middleware_instances):
- _run_method_if_implemented(middleware, 'process_response', request, response)
+ for middleware in reversed(middleware_instances):
+ _run_method_if_implemented(middleware, 'process_response', request, response)
def _run_method_if_implemented(instance, method_name, *args, **kwargs):
diff --git a/openedx/core/lib/jwt.py b/openedx/core/lib/jwt.py
new file mode 100644
index 000000000000..47642b869560
--- /dev/null
+++ b/openedx/core/lib/jwt.py
@@ -0,0 +1,91 @@
+"""
+JWT Token handling and signing functions.
+"""
+
+import json
+from time import time
+
+from django.conf import settings
+from jwkest import Expired, Invalid, MissingKey, jwk
+from jwkest.jws import JWS
+
+
+def create_jwt(lms_user_id, expires_in_seconds, additional_token_claims, now=None):
+ """
+ Produce an encoded JWT (string) indicating some temporary permission for the indicated user.
+
+ What permission that is must be encoded in additional_claims.
+ Arguments:
+ lms_user_id (int): LMS user ID this token is being generated for
+ expires_in_seconds (int): Time to token expiry, specified in seconds.
+ additional_token_claims (dict): Additional claims to include in the token.
+ now(int): optional now value for testing
+ """
+ now = now or int(time())
+
+ payload = {
+ 'lms_user_id': lms_user_id,
+ 'exp': now + expires_in_seconds,
+ 'iat': now,
+ 'iss': settings.TOKEN_SIGNING['JWT_ISSUER'],
+ 'version': settings.TOKEN_SIGNING['JWT_SUPPORTED_VERSION'],
+ }
+ payload.update(additional_token_claims)
+ return _encode_and_sign(payload)
+
+
+def _encode_and_sign(payload):
+ """
+ Encode and sign the provided payload.
+
+ The signing key and algorithm are pulled from settings.
+ """
+ keys = jwk.KEYS()
+
+ serialized_keypair = json.loads(settings.TOKEN_SIGNING['JWT_PRIVATE_SIGNING_JWK'])
+ keys.add(serialized_keypair)
+ algorithm = settings.TOKEN_SIGNING['JWT_SIGNING_ALGORITHM']
+
+ data = json.dumps(payload)
+ jws = JWS(data, alg=algorithm)
+ return jws.sign_compact(keys=keys)
+
+
+def unpack_jwt(token, lms_user_id, now=None):
+ """
+ Unpack and verify an encoded JWT.
+
+ Validate the user and expiration.
+
+ Arguments:
+ token (string): The token to be unpacked and verified.
+ lms_user_id (int): LMS user ID this token should match with.
+ now (int): Optional now value for testing.
+
+ Returns a valid, decoded json payload (string).
+ """
+ now = now or int(time())
+ payload = _unpack_and_verify(token)
+
+ if "lms_user_id" not in payload:
+ raise MissingKey("LMS user id is missing")
+ if "exp" not in payload:
+ raise MissingKey("Expiration is missing")
+ if payload["lms_user_id"] != lms_user_id:
+ raise Invalid("User does not match")
+ if payload["exp"] < now:
+ raise Expired("Token is expired")
+
+ return payload
+
+
+def _unpack_and_verify(token):
+ """
+ Unpack and verify the provided token.
+
+ The signing key and algorithm are pulled from settings.
+ """
+ keys = jwk.KEYS()
+ keys.load_jwks(settings.TOKEN_SIGNING['JWT_PUBLIC_SIGNING_JWK_SET'])
+ decoded = JWS().verify_compact(token.encode('utf-8'), keys)
+ return decoded
diff --git a/openedx/core/lib/tests/test_jwt.py b/openedx/core/lib/tests/test_jwt.py
new file mode 100644
index 000000000000..7a678dd3c09b
--- /dev/null
+++ b/openedx/core/lib/tests/test_jwt.py
@@ -0,0 +1,129 @@
+"""
+Tests for token handling
+"""
+import unittest
+
+from django.conf import settings
+from jwkest import BadSignature, Expired, Invalid, MissingKey, jwk
+from jwkest.jws import JWS
+
+from openedx.core.djangolib.testing.utils import skip_unless_lms
+from openedx.core.lib.jwt import _encode_and_sign, create_jwt, unpack_jwt
+
+
+test_user_id = 121
+invalid_test_user_id = 120
+test_timeout = 60
+test_now = 1661432902
+test_claims = {"foo": "bar", "baz": "quux", "meaning": 42}
+expected_full_token = {
+ "lms_user_id": test_user_id,
+ "iat": 1661432902,
+ "exp": 1661432902 + 60,
+ "iss": "token-test-issuer", # these lines from test_settings.py
+ "version": "1.2.0", # these lines from test_settings.py
+}
+
+
+@skip_unless_lms
+class TestSign(unittest.TestCase):
+ """
+ Tests for JWT creation and signing.
+ """
+
+ def test_create_jwt(self):
+ token = create_jwt(test_user_id, test_timeout, {}, test_now)
+
+ decoded = _verify_jwt(token)
+ self.assertEqual(expected_full_token, decoded)
+
+ def test_create_jwt_with_claims(self):
+ token = create_jwt(test_user_id, test_timeout, test_claims, test_now)
+
+ expected_token_with_claims = expected_full_token.copy()
+ expected_token_with_claims.update(test_claims)
+
+ decoded = _verify_jwt(token)
+ self.assertEqual(expected_token_with_claims, decoded)
+
+ def test_malformed_token(self):
+ token = create_jwt(test_user_id, test_timeout, test_claims, test_now)
+ token = token + "a"
+
+ expected_token_with_claims = expected_full_token.copy()
+ expected_token_with_claims.update(test_claims)
+
+ with self.assertRaises(BadSignature):
+ _verify_jwt(token)
+
+
+def _verify_jwt(jwt_token):
+ """
+ Helper function which verifies the signature and decodes the token
+ from string back to claims form
+ """
+ keys = jwk.KEYS()
+ keys.load_jwks(settings.TOKEN_SIGNING['JWT_PUBLIC_SIGNING_JWK_SET'])
+ decoded = JWS().verify_compact(jwt_token.encode('utf-8'), keys)
+ return decoded
+
+
+@skip_unless_lms
+class TestUnpack(unittest.TestCase):
+ """
+ Tests for JWT unpacking.
+ """
+
+ def test_unpack_jwt(self):
+ token = create_jwt(test_user_id, test_timeout, {}, test_now)
+ decoded = unpack_jwt(token, test_user_id, test_now)
+
+ self.assertEqual(expected_full_token, decoded)
+
+ def test_unpack_jwt_with_claims(self):
+ token = create_jwt(test_user_id, test_timeout, test_claims, test_now)
+
+ expected_token_with_claims = expected_full_token.copy()
+ expected_token_with_claims.update(test_claims)
+
+ decoded = unpack_jwt(token, test_user_id, test_now)
+
+ self.assertEqual(expected_token_with_claims, decoded)
+
+ def test_malformed_token(self):
+ token = create_jwt(test_user_id, test_timeout, test_claims, test_now)
+ token = token + "a"
+
+ expected_token_with_claims = expected_full_token.copy()
+ expected_token_with_claims.update(test_claims)
+
+ with self.assertRaises(BadSignature):
+ unpack_jwt(token, test_user_id, test_now)
+
+ def test_unpack_token_with_invalid_user(self):
+ token = create_jwt(invalid_test_user_id, test_timeout, {}, test_now)
+
+ with self.assertRaises(Invalid):
+ unpack_jwt(token, test_user_id, test_now)
+
+ def test_unpack_expired_token(self):
+ token = create_jwt(test_user_id, test_timeout, {}, test_now)
+
+ with self.assertRaises(Expired):
+ unpack_jwt(token, test_user_id, test_now + test_timeout + 1)
+
+ def test_missing_expired_lms_user_id(self):
+ payload = expected_full_token.copy()
+ del payload['lms_user_id']
+ token = _encode_and_sign(payload)
+
+ with self.assertRaises(MissingKey):
+ unpack_jwt(token, test_user_id, test_now)
+
+ def test_missing_expired_key(self):
+ payload = expected_full_token.copy()
+ del payload['exp']
+ token = _encode_and_sign(payload)
+
+ with self.assertRaises(MissingKey):
+ unpack_jwt(token, test_user_id, test_now)
diff --git a/pylint_django_settings.py b/pylint_django_settings.py
index 46abfd81f883..6051d9ab4b56 100644
--- a/pylint_django_settings.py
+++ b/pylint_django_settings.py
@@ -1,5 +1,5 @@
-from pylint_django.checkers import ForeignKeyStringsChecker
-from pylint_plugin_utils import get_checker
+import os
+import sys
class ArgumentCompatibilityError(Exception):
@@ -47,6 +47,4 @@ def load_configuration(linter):
"""
Configures the Django settings module based on the command-line arguments passed to pylint.
"""
- name_checker = get_checker(linter, ForeignKeyStringsChecker)
- arguments = linter.cmdline_parser.parse_args()[1]
- name_checker.config.django_settings_module = _get_django_settings_module(arguments)
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", _get_django_settings_module(sys.argv[1:]))
diff --git a/pylintrc b/pylintrc
index 55a9bbab3b9c..e06941853774 100644
--- a/pylintrc
+++ b/pylintrc
@@ -64,7 +64,7 @@
# SERIOUSLY.
#
# ------------------------------
-# Generated by edx-lint version: 5.3.7
+# Generated by edx-lint version: 5.4.1
# ------------------------------
[MASTER]
ignore = ,.git,.tox,migrations,node_modules,.pycharm_helpers
@@ -72,10 +72,10 @@ persistent = yes
load-plugins = edx_lint.pylint,pylint_django_settings,pylint_django,pylint_celery,pylint_pytest
[MESSAGES CONTROL]
-enable =
+enable =
blacklisted-name,
line-too-long,
-
+
abstract-class-instantiated,
abstract-method,
access-member-before-definition,
@@ -184,26 +184,26 @@ enable =
used-before-assignment,
using-constant-test,
yield-outside-function,
-
+
astroid-error,
fatal,
method-check-failed,
parse-error,
raw-checker-failed,
-
+
empty-docstring,
invalid-characters-in-docstring,
missing-docstring,
wrong-spelling-in-comment,
wrong-spelling-in-docstring,
-
+
unused-argument,
unused-import,
unused-variable,
-
+
eval-used,
exec-used,
-
+
bad-classmethod-argument,
bad-mcs-classmethod-argument,
bad-mcs-method-argument,
@@ -234,30 +234,30 @@ enable =
unneeded-not,
useless-else-on-loop,
wrong-assert-type,
-
+
deprecated-method,
deprecated-module,
-
+
too-many-boolean-expressions,
too-many-nested-blocks,
too-many-statements,
-
+
wildcard-import,
wrong-import-order,
wrong-import-position,
-
+
missing-final-newline,
mixed-line-endings,
trailing-newlines,
trailing-whitespace,
unexpected-line-ending-format,
-
+
bad-inline-option,
bad-option-value,
deprecated-pragma,
unrecognized-inline-option,
useless-suppression,
-disable =
+disable =
bad-indentation,
broad-exception-raised,
consider-using-f-string,
@@ -282,10 +282,10 @@ disable =
unspecified-encoding,
unused-wildcard-import,
use-maxsplit-arg,
-
+
feature-toggle-needs-doc,
illegal-waffle-usage,
-
+
logging-fstring-interpolation,
import-outside-toplevel,
inconsistent-return-statements,
@@ -314,6 +314,13 @@ disable =
c-extension-no-member,
no-name-in-module,
unnecessary-lambda-assignment,
+ too-many-positional-arguments,
+ possibly-used-before-assignment,
+ use-dict-literal,
+ useless-return,
+ superfluous-parens,
+ logging-not-lazy,
+ broad-exception-caught,
[REPORTS]
output-format = text
@@ -356,7 +363,7 @@ ignore-imports = no
ignore-mixin-members = yes
ignored-classes = SQLObject
unsafe-load-any-extension = yes
-generated-members =
+generated-members =
REQUEST,
acl_users,
aq_parent,
@@ -382,7 +389,7 @@ generated-members =
[VARIABLES]
init-import = no
dummy-variables-rgx = _|dummy|unused|.*_unused
-additional-builtins =
+additional-builtins =
[CLASSES]
defining-attr-methods = __init__,__new__,setUp
@@ -403,11 +410,11 @@ max-public-methods = 20
[IMPORTS]
deprecated-modules = regsub,TERMIOS,Bastion,rexec
-import-graph =
-ext-import-graph =
-int-import-graph =
+import-graph =
+ext-import-graph =
+int-import-graph =
[EXCEPTIONS]
overgeneral-exceptions = builtins.Exception
-# e624ea03d8124aa9cf2e577f830632344a0a07d9
+# 85c3d025c367597a0a7b23a05fdde9d8c63e1374
diff --git a/pylintrc_tweaks b/pylintrc_tweaks
index 1633da5c10a4..fb0bebfd2a12 100644
--- a/pylintrc_tweaks
+++ b/pylintrc_tweaks
@@ -33,6 +33,13 @@ disable+ =
c-extension-no-member,
no-name-in-module,
unnecessary-lambda-assignment,
+ too-many-positional-arguments,
+ possibly-used-before-assignment,
+ use-dict-literal,
+ useless-return,
+ superfluous-parens,
+ logging-not-lazy,
+ broad-exception-caught,
[BASIC]
attr-rgx = [a-z_][a-z0-9_]{2,40}$
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index 283ab625f42b..94d960359a91 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -80,7 +80,7 @@ django-storages<1.14.4
# The team that owns this package will manually bump this package rather than having it pulled in automatically.
# This is to allow them to better control its deployment and to do it in a process that works better
# for them.
-edx-enterprise==5.6.4
+edx-enterprise==5.6.6
# Date: 2024-05-09
# This has to be constrained as well because newer versions of edx-i18n-tools need the
@@ -131,7 +131,7 @@ optimizely-sdk<5.0
# Date: 2023-09-18
# pinning this version to avoid updates while the library is being developed
# Issue for unpinning: https://github.com/openedx/edx-platform/issues/35269
-openedx-learning==0.18.1
+openedx-learning==0.18.2
# Date: 2023-11-29
# Open AI version 1.0.0 dropped support for openai.ChatCompletion which is currently in use in enterprise.
@@ -149,11 +149,6 @@ path<16.12.0
# Constraint can be removed once the issue https://github.com/PyCQA/pycodestyle/issues/1090 is fixed.
pycodestyle<2.9.0
-# Date: 2021-07-12
-# Issue for unpinning: https://github.com/openedx/edx-platform/issues/33560
-pylint<2.16.0 # greater version failing quality test. Fix them in seperate ticket.
-astroid<2.14.0
-
# Date: 2021-08-25
# At the time of writing this comment, we do not know whether py2neo>=2022
# will support our currently-deployed Neo4j version (3.5).
diff --git a/requirements/edx-sandbox/base.txt b/requirements/edx-sandbox/base.txt
index da781eb84587..821664fe9fc3 100644
--- a/requirements/edx-sandbox/base.txt
+++ b/requirements/edx-sandbox/base.txt
@@ -20,7 +20,7 @@ cryptography==44.0.0
# via -r requirements/edx-sandbox/base.in
cycler==0.12.1
# via matplotlib
-fonttools==4.55.4
+fonttools==4.55.6
# via matplotlib
joblib==1.4.2
# via nltk
diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt
index 4861b7be1f63..66428919cc61 100644
--- a/requirements/edx/base.txt
+++ b/requirements/edx/base.txt
@@ -37,7 +37,7 @@ asgiref==3.8.1
# django-countries
asn1crypto==1.5.1
# via snowflake-connector-python
-attrs==24.3.0
+attrs==25.1.0
# via
# -r requirements/edx/kernel.in
# aiohttp
@@ -72,13 +72,13 @@ bleach[css]==6.2.0
# xblock-poll
boto==2.49.0
# via -r requirements/edx/kernel.in
-boto3==1.36.3
+boto3==1.36.6
# via
# -r requirements/edx/kernel.in
# django-ses
# fs-s3fs
# ora2
-botocore==1.36.3
+botocore==1.36.6
# via
# -r requirements/edx/kernel.in
# boto3
@@ -87,7 +87,7 @@ bridgekeeper==0.9
# via -r requirements/edx/kernel.in
cachecontrol==0.14.2
# via firebase-admin
-cachetools==5.5.0
+cachetools==5.5.1
# via google-auth
camel-converter[pydantic]==4.0.1
# via meilisearch
@@ -221,7 +221,6 @@ django==4.2.18
# edx-search
# edx-submissions
# edx-toggles
- # edx-token-utils
# edx-when
# edxval
# enmerkar
@@ -468,7 +467,7 @@ edx-drf-extensions==10.5.0
# edx-when
# edxval
# openedx-learning
-edx-enterprise==5.6.4
+edx-enterprise==5.6.6
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/kernel.in
@@ -514,7 +513,7 @@ edx-rest-api-client==6.0.0
# -r requirements/edx/kernel.in
# edx-enterprise
# edx-proctoring
-edx-search==4.1.1
+edx-search==4.1.2
# via
# -r requirements/edx/kernel.in
# openedx-forum
@@ -538,8 +537,6 @@ edx-toggles==5.2.0
# edxval
# event-tracking
# ora2
-edx-token-utils==0.2.1
- # via -r requirements/edx/kernel.in
edx-when==2.5.1
# via
# -r requirements/edx/kernel.in
@@ -598,7 +595,7 @@ google-api-core[grpc]==2.24.0
# google-cloud-storage
google-api-python-client==2.159.0
# via firebase-admin
-google-auth==2.37.0
+google-auth==2.38.0
# via
# google-api-core
# google-api-python-client
@@ -626,11 +623,11 @@ googleapis-common-protos==1.66.0
# via
# google-api-core
# grpcio-status
-grpcio==1.69.0
+grpcio==1.70.0
# via
# google-api-core
# grpcio-status
-grpcio-status==1.69.0
+grpcio-status==1.70.0
# via google-api-core
gunicorn==23.0.0
# via -r requirements/edx/kernel.in
@@ -705,7 +702,7 @@ lazy==1.6
# xblock
loremipsum==1.0.5
# via ora2
-lti-consumer-xblock==9.13.1
+lti-consumer-xblock==9.13.2
# via -r requirements/edx/kernel.in
lxml[html-clean,html_clean]==5.3.0
# via
@@ -830,7 +827,7 @@ openedx-filters==1.12.0
# ora2
openedx-forum==0.1.6
# via -r requirements/edx/kernel.in
-openedx-learning==0.18.1
+openedx-learning==0.18.2
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/kernel.in
@@ -922,7 +919,7 @@ pycryptodomex==3.21.0
# edx-proctoring
# lti-consumer-xblock
# pyjwkest
-pydantic==2.10.5
+pydantic==2.10.6
# via camel-converter
pydantic-core==2.27.2
# via pydantic
@@ -931,7 +928,6 @@ pygments==2.19.1
pyjwkest==1.4.2
# via
# -r requirements/edx/kernel.in
- # edx-token-utils
# lti-consumer-xblock
pyjwt[crypto]==2.10.1
# via
@@ -1044,7 +1040,7 @@ redis==5.2.1
# via
# -r requirements/edx/kernel.in
# walrus
-referencing==0.36.1
+referencing==0.36.2
# via
# jsonschema
# jsonschema-specifications
@@ -1093,7 +1089,7 @@ rules==3.5
# edx-enterprise
# edx-proctoring
# openedx-learning
-s3transfer==0.11.1
+s3transfer==0.11.2
# via boto3
sailthru-client==2.2.3
# via edx-ace
@@ -1139,7 +1135,7 @@ slumber==0.7.1
# -r requirements/edx/kernel.in
# edx-bulk-grades
# edx-enterprise
-snowflake-connector-python==3.12.4
+snowflake-connector-python==3.13.0
# via edx-enterprise
social-auth-app-django==5.4.1
# via
diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt
index ae377bd5cc7c..faab27d2a6ba 100644
--- a/requirements/edx/development.txt
+++ b/requirements/edx/development.txt
@@ -82,15 +82,14 @@ asn1crypto==1.5.1
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# snowflake-connector-python
-astroid==2.13.5
+astroid==3.3.8
# via
- # -c requirements/edx/../constraints.txt
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# pylint
# pylint-celery
# sphinx-autoapi
-attrs==24.3.0
+attrs==25.1.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -145,14 +144,14 @@ boto==2.49.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
-boto3==1.36.3
+boto3==1.36.6
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# django-ses
# fs-s3fs
# ora2
-botocore==1.36.3
+botocore==1.36.6
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -171,7 +170,7 @@ cachecontrol==0.14.2
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# firebase-admin
-cachetools==5.5.0
+cachetools==5.5.1
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -395,7 +394,6 @@ django==4.2.18
# edx-search
# edx-submissions
# edx-toggles
- # edx-token-utils
# edx-when
# edxval
# enmerkar
@@ -747,7 +745,7 @@ edx-drf-extensions==10.5.0
# edx-when
# edxval
# openedx-learning
-edx-enterprise==5.6.4
+edx-enterprise==5.6.6
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/doc.txt
@@ -767,7 +765,7 @@ edx-i18n-tools==1.5.0
# -r requirements/edx/testing.txt
# ora2
# xblocks-contrib
-edx-lint==5.4.1
+edx-lint==5.6.0
# via -r requirements/edx/testing.txt
edx-milestones==0.6.0
# via
@@ -813,7 +811,7 @@ edx-rest-api-client==6.0.0
# -r requirements/edx/testing.txt
# edx-enterprise
# edx-proctoring
-edx-search==4.1.1
+edx-search==4.1.2
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -845,10 +843,6 @@ edx-toggles==5.2.0
# edxval
# event-tracking
# ora2
-edx-token-utils==0.2.1
- # via
- # -r requirements/edx/doc.txt
- # -r requirements/edx/testing.txt
edx-when==2.5.1
# via
# -r requirements/edx/doc.txt
@@ -889,11 +883,11 @@ execnet==2.1.1
# pytest-xdist
factory-boy==3.3.1
# via -r requirements/edx/testing.txt
-faker==33.3.1
+faker==35.0.0
# via
# -r requirements/edx/testing.txt
# factory-boy
-fastapi==0.115.6
+fastapi==0.115.7
# via
# -r requirements/edx/testing.txt
# pact-python
@@ -967,7 +961,7 @@ google-api-python-client==2.159.0
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# firebase-admin
-google-auth==2.37.0
+google-auth==2.38.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1019,13 +1013,13 @@ grimp==3.5
# via
# -r requirements/edx/testing.txt
# import-linter
-grpcio==1.69.0
+grpcio==1.70.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# google-api-core
# grpcio-status
-grpcio-status==1.69.0
+grpcio-status==1.70.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1175,11 +1169,6 @@ lazy==1.6
# lti-consumer-xblock
# ora2
# xblock
-lazy-object-proxy==1.10.0
- # via
- # -r requirements/edx/doc.txt
- # -r requirements/edx/testing.txt
- # astroid
libsass==0.10.0
# via
# -c requirements/edx/../constraints.txt
@@ -1189,7 +1178,7 @@ loremipsum==1.0.5
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# ora2
-lti-consumer-xblock==9.13.1
+lti-consumer-xblock==9.13.2
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1392,7 +1381,7 @@ openedx-forum==0.1.6
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
-openedx-learning==0.18.1
+openedx-learning==0.18.2
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/doc.txt
@@ -1424,7 +1413,7 @@ packaging==24.2
# snowflake-connector-python
# sphinx
# tox
-pact-python==2.3.0
+pact-python==2.3.1
# via -r requirements/edx/testing.txt
pansi==2024.11.0
# via
@@ -1569,7 +1558,7 @@ pycryptodomex==3.21.0
# edx-proctoring
# lti-consumer-xblock
# pyjwkest
-pydantic==2.10.5
+pydantic==2.10.6
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1598,7 +1587,6 @@ pyjwkest==1.4.2
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
- # edx-token-utils
# lti-consumer-xblock
pyjwt[crypto]==2.10.1
# via
@@ -1619,9 +1607,8 @@ pylatexenc==2.10
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# olxcleaner
-pylint==2.15.10
+pylint==3.3.3
# via
- # -c requirements/edx/../constraints.txt
# -r requirements/edx/testing.txt
# edx-lint
# pylint-celery
@@ -1632,7 +1619,7 @@ pylint-celery==0.3
# via
# -r requirements/edx/testing.txt
# edx-lint
-pylint-django==2.5.5
+pylint-django==2.6.1
# via
# -r requirements/edx/testing.txt
# edx-lint
@@ -1641,7 +1628,7 @@ pylint-plugin-utils==0.8.2
# -r requirements/edx/testing.txt
# pylint-celery
# pylint-django
-pylint-pytest==0.3.0
+pylint-pytest==1.1.8
# via -r requirements/edx/testing.txt
pylti1p3==2.0.0
# via
@@ -1705,7 +1692,7 @@ pysrt==1.1.2
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# edxval
-pytest==8.3.4
+pytest==8.2.0
# via
# -r requirements/edx/testing.txt
# pylint-pytest
@@ -1724,7 +1711,7 @@ pytest-django==4.9.0
# via -r requirements/edx/testing.txt
pytest-json-report==1.5.0
# via -r requirements/edx/testing.txt
-pytest-metadata==1.8.0
+pytest-metadata==3.1.1
# via
# -r requirements/edx/testing.txt
# pytest-json-report
@@ -1822,7 +1809,7 @@ redis==5.2.1
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# walrus
-referencing==0.36.1
+referencing==0.36.2
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1888,7 +1875,7 @@ rules==3.5
# edx-enterprise
# edx-proctoring
# openedx-learning
-s3transfer==0.11.1
+s3transfer==0.11.2
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1969,7 +1956,7 @@ snowballstemmer==2.2.0
# via
# -r requirements/edx/doc.txt
# sphinx
-snowflake-connector-python==3.12.4
+snowflake-connector-python==3.13.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -2067,7 +2054,7 @@ staff-graded-xblock==3.0.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
-starlette==0.41.3
+starlette==0.45.3
# via
# -r requirements/edx/testing.txt
# fastapi
@@ -2252,7 +2239,6 @@ wrapt==1.17.2
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
- # astroid
xblock[django]==5.1.1
# via
# -r requirements/edx/doc.txt
diff --git a/requirements/edx/doc.txt b/requirements/edx/doc.txt
index 354e85c98f55..0f927c5218f8 100644
--- a/requirements/edx/doc.txt
+++ b/requirements/edx/doc.txt
@@ -57,11 +57,9 @@ asn1crypto==1.5.1
# via
# -r requirements/edx/base.txt
# snowflake-connector-python
-astroid==2.13.5
- # via
- # -c requirements/edx/../constraints.txt
- # sphinx-autoapi
-attrs==24.3.0
+astroid==3.3.8
+ # via sphinx-autoapi
+attrs==25.1.0
# via
# -r requirements/edx/base.txt
# aiohttp
@@ -107,13 +105,13 @@ bleach[css]==6.2.0
# xblock-poll
boto==2.49.0
# via -r requirements/edx/base.txt
-boto3==1.36.3
+boto3==1.36.6
# via
# -r requirements/edx/base.txt
# django-ses
# fs-s3fs
# ora2
-botocore==1.36.3
+botocore==1.36.6
# via
# -r requirements/edx/base.txt
# boto3
@@ -124,7 +122,7 @@ cachecontrol==0.14.2
# via
# -r requirements/edx/base.txt
# firebase-admin
-cachetools==5.5.0
+cachetools==5.5.1
# via
# -r requirements/edx/base.txt
# google-auth
@@ -280,7 +278,6 @@ django==4.2.18
# edx-search
# edx-submissions
# edx-toggles
- # edx-token-utils
# edx-when
# edxval
# enmerkar
@@ -555,7 +552,7 @@ edx-drf-extensions==10.5.0
# edx-when
# edxval
# openedx-learning
-edx-enterprise==5.6.4
+edx-enterprise==5.6.6
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/base.txt
@@ -603,7 +600,7 @@ edx-rest-api-client==6.0.0
# -r requirements/edx/base.txt
# edx-enterprise
# edx-proctoring
-edx-search==4.1.1
+edx-search==4.1.2
# via
# -r requirements/edx/base.txt
# openedx-forum
@@ -629,8 +626,6 @@ edx-toggles==5.2.0
# edxval
# event-tracking
# ora2
-edx-token-utils==0.2.1
- # via -r requirements/edx/base.txt
edx-when==2.5.1
# via
# -r requirements/edx/base.txt
@@ -708,7 +703,7 @@ google-api-python-client==2.159.0
# via
# -r requirements/edx/base.txt
# firebase-admin
-google-auth==2.37.0
+google-auth==2.38.0
# via
# -r requirements/edx/base.txt
# google-api-core
@@ -748,12 +743,12 @@ googleapis-common-protos==1.66.0
# -r requirements/edx/base.txt
# google-api-core
# grpcio-status
-grpcio==1.69.0
+grpcio==1.70.0
# via
# -r requirements/edx/base.txt
# google-api-core
# grpcio-status
-grpcio-status==1.69.0
+grpcio-status==1.70.0
# via
# -r requirements/edx/base.txt
# google-api-core
@@ -854,13 +849,11 @@ lazy==1.6
# lti-consumer-xblock
# ora2
# xblock
-lazy-object-proxy==1.10.0
- # via astroid
loremipsum==1.0.5
# via
# -r requirements/edx/base.txt
# ora2
-lti-consumer-xblock==9.13.1
+lti-consumer-xblock==9.13.2
# via -r requirements/edx/base.txt
lxml[html-clean]==5.3.0
# via
@@ -1007,7 +1000,7 @@ openedx-filters==1.12.0
# ora2
openedx-forum==0.1.6
# via -r requirements/edx/base.txt
-openedx-learning==0.18.1
+openedx-learning==0.18.2
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/base.txt
@@ -1126,7 +1119,7 @@ pycryptodomex==3.21.0
# edx-proctoring
# lti-consumer-xblock
# pyjwkest
-pydantic==2.10.5
+pydantic==2.10.6
# via
# -r requirements/edx/base.txt
# camel-converter
@@ -1147,7 +1140,6 @@ pygments==2.19.1
pyjwkest==1.4.2
# via
# -r requirements/edx/base.txt
- # edx-token-utils
# lti-consumer-xblock
pyjwt[crypto]==2.10.1
# via
@@ -1275,7 +1267,7 @@ redis==5.2.1
# via
# -r requirements/edx/base.txt
# walrus
-referencing==0.36.1
+referencing==0.36.2
# via
# -r requirements/edx/base.txt
# jsonschema
@@ -1332,7 +1324,7 @@ rules==3.5
# edx-enterprise
# edx-proctoring
# openedx-learning
-s3transfer==0.11.1
+s3transfer==0.11.2
# via
# -r requirements/edx/base.txt
# boto3
@@ -1390,7 +1382,7 @@ smmap==5.0.2
# via gitdb
snowballstemmer==2.2.0
# via sphinx
-snowflake-connector-python==3.12.4
+snowflake-connector-python==3.13.0
# via
# -r requirements/edx/base.txt
# edx-enterprise
@@ -1580,9 +1572,7 @@ wheel==0.45.1
# -r requirements/edx/base.txt
# django-pipeline
wrapt==1.17.2
- # via
- # -r requirements/edx/base.txt
- # astroid
+ # via -r requirements/edx/base.txt
xblock[django]==5.1.1
# via
# -r requirements/edx/base.txt
diff --git a/requirements/edx/kernel.in b/requirements/edx/kernel.in
index d1a132778133..a17b9db4c868 100644
--- a/requirements/edx/kernel.in
+++ b/requirements/edx/kernel.in
@@ -84,7 +84,6 @@ edx-rest-api-client
edx-search
edx-submissions
edx-toggles # Feature toggles management
-edx-token-utils # Validate exam access tokens
edx-when
edxval
event-tracking
diff --git a/requirements/edx/semgrep.txt b/requirements/edx/semgrep.txt
index 65a0b794b9e6..9d25000148f3 100644
--- a/requirements/edx/semgrep.txt
+++ b/requirements/edx/semgrep.txt
@@ -4,7 +4,7 @@
#
# make upgrade
#
-attrs==24.3.0
+attrs==25.1.0
# via
# glom
# jsonschema
@@ -34,7 +34,7 @@ colorama==0.4.6
# via semgrep
defusedxml==0.7.1
# via semgrep
-deprecated==1.2.15
+deprecated==1.2.18
# via
# opentelemetry-api
# opentelemetry-exporter-otlp-proto-http
@@ -92,13 +92,13 @@ packaging==24.2
# via semgrep
peewee==3.17.8
# via semgrep
-protobuf==4.25.5
+protobuf==4.25.6
# via
# googleapis-common-protos
# opentelemetry-proto
pygments==2.19.1
# via rich
-referencing==0.36.1
+referencing==0.36.2
# via
# jsonschema
# jsonschema-specifications
@@ -116,7 +116,7 @@ ruamel-yaml==0.18.10
# via semgrep
ruamel-yaml-clib==0.2.12
# via ruamel-yaml
-semgrep==1.103.0
+semgrep==1.104.0
# via -r requirements/edx/semgrep.in
tomli==2.0.2
# via semgrep
diff --git a/requirements/edx/testing.in b/requirements/edx/testing.in
index cf57aeb0fc46..14a0c781da82 100644
--- a/requirements/edx/testing.in
+++ b/requirements/edx/testing.in
@@ -37,13 +37,13 @@ pytest-attrib # Select tests based on attributes
pytest-cov # pytest plugin for measuring code coverage
pytest-django # Django support for pytest
pytest-json-report # Output json formatted warnings after running pytest
-pytest-metadata==1.8.0 # To prevent 'make upgrade' failure, dependency of pytest-json-report
+pytest-metadata # To prevent 'make upgrade' failure, dependency of pytest-json-report
pytest-randomly # pytest plugin to randomly order tests
pytest-xdist[psutil] # Parallel execution of tests on multiple CPU cores or hosts
singledispatch # Backport of functools.singledispatch from Python 3.4+, used in tests of XBlock rendering
testfixtures # Provides a LogCapture utility used by several tests
tox # virtualenv management for tests
unidiff # Required by coverage_pytest_plugin
-pylint-pytest==0.3.0 # A Pylint plugin to suppress pytest-related false positives.
+pylint-pytest # A Pylint plugin to suppress pytest-related false positives.
pact-python # Library for contract testing
py # Needed for pytest configurations, was previously been fetched through tox
diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt
index 799f8b16ed26..5c991ae3e7d6 100644
--- a/requirements/edx/testing.txt
+++ b/requirements/edx/testing.txt
@@ -55,12 +55,11 @@ asn1crypto==1.5.1
# via
# -r requirements/edx/base.txt
# snowflake-connector-python
-astroid==2.13.5
+astroid==3.3.8
# via
- # -c requirements/edx/../constraints.txt
# pylint
# pylint-celery
-attrs==24.3.0
+attrs==25.1.0
# via
# -r requirements/edx/base.txt
# aiohttp
@@ -104,13 +103,13 @@ bleach[css]==6.2.0
# xblock-poll
boto==2.49.0
# via -r requirements/edx/base.txt
-boto3==1.36.3
+boto3==1.36.6
# via
# -r requirements/edx/base.txt
# django-ses
# fs-s3fs
# ora2
-botocore==1.36.3
+botocore==1.36.6
# via
# -r requirements/edx/base.txt
# boto3
@@ -121,7 +120,7 @@ cachecontrol==0.14.2
# via
# -r requirements/edx/base.txt
# firebase-admin
-cachetools==5.5.0
+cachetools==5.5.1
# via
# -r requirements/edx/base.txt
# google-auth
@@ -306,7 +305,6 @@ django==4.2.18
# edx-search
# edx-submissions
# edx-toggles
- # edx-token-utils
# edx-when
# edxval
# enmerkar
@@ -576,7 +574,7 @@ edx-drf-extensions==10.5.0
# edx-when
# edxval
# openedx-learning
-edx-enterprise==5.6.4
+edx-enterprise==5.6.6
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/base.txt
@@ -590,7 +588,7 @@ edx-i18n-tools==1.5.0
# -r requirements/edx/base.txt
# ora2
# xblocks-contrib
-edx-lint==5.4.1
+edx-lint==5.6.0
# via -r requirements/edx/testing.in
edx-milestones==0.6.0
# via -r requirements/edx/base.txt
@@ -626,7 +624,7 @@ edx-rest-api-client==6.0.0
# -r requirements/edx/base.txt
# edx-enterprise
# edx-proctoring
-edx-search==4.1.1
+edx-search==4.1.2
# via
# -r requirements/edx/base.txt
# openedx-forum
@@ -652,8 +650,6 @@ edx-toggles==5.2.0
# edxval
# event-tracking
# ora2
-edx-token-utils==0.2.1
- # via -r requirements/edx/base.txt
edx-when==2.5.1
# via
# -r requirements/edx/base.txt
@@ -684,9 +680,9 @@ execnet==2.1.1
# via pytest-xdist
factory-boy==3.3.1
# via -r requirements/edx/testing.in
-faker==33.3.1
+faker==35.0.0
# via factory-boy
-fastapi==0.115.6
+fastapi==0.115.7
# via pact-python
fastavro==1.10.0
# via
@@ -739,7 +735,7 @@ google-api-python-client==2.159.0
# via
# -r requirements/edx/base.txt
# firebase-admin
-google-auth==2.37.0
+google-auth==2.38.0
# via
# -r requirements/edx/base.txt
# google-api-core
@@ -781,12 +777,12 @@ googleapis-common-protos==1.66.0
# grpcio-status
grimp==3.5
# via import-linter
-grpcio==1.69.0
+grpcio==1.70.0
# via
# -r requirements/edx/base.txt
# google-api-core
# grpcio-status
-grpcio-status==1.69.0
+grpcio-status==1.70.0
# via
# -r requirements/edx/base.txt
# google-api-core
@@ -897,13 +893,11 @@ lazy==1.6
# lti-consumer-xblock
# ora2
# xblock
-lazy-object-proxy==1.10.0
- # via astroid
loremipsum==1.0.5
# via
# -r requirements/edx/base.txt
# ora2
-lti-consumer-xblock==9.13.1
+lti-consumer-xblock==9.13.2
# via -r requirements/edx/base.txt
lxml[html-clean]==5.3.0
# via
@@ -1054,7 +1048,7 @@ openedx-filters==1.12.0
# ora2
openedx-forum==0.1.6
# via -r requirements/edx/base.txt
-openedx-learning==0.18.1
+openedx-learning==0.18.2
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/base.txt
@@ -1076,7 +1070,7 @@ packaging==24.2
# pytest
# snowflake-connector-python
# tox
-pact-python==2.3.0
+pact-python==2.3.1
# via -r requirements/edx/testing.in
pansi==2024.11.0
# via
@@ -1193,7 +1187,7 @@ pycryptodomex==3.21.0
# edx-proctoring
# lti-consumer-xblock
# pyjwkest
-pydantic==2.10.5
+pydantic==2.10.6
# via
# -r requirements/edx/base.txt
# camel-converter
@@ -1211,7 +1205,6 @@ pygments==2.19.1
pyjwkest==1.4.2
# via
# -r requirements/edx/base.txt
- # edx-token-utils
# lti-consumer-xblock
pyjwt[crypto]==2.10.1
# via
@@ -1230,9 +1223,8 @@ pylatexenc==2.10
# via
# -r requirements/edx/base.txt
# olxcleaner
-pylint==2.15.10
+pylint==3.3.3
# via
- # -c requirements/edx/../constraints.txt
# edx-lint
# pylint-celery
# pylint-django
@@ -1240,13 +1232,13 @@ pylint==2.15.10
# pylint-pytest
pylint-celery==0.3
# via edx-lint
-pylint-django==2.5.5
+pylint-django==2.6.1
# via edx-lint
pylint-plugin-utils==0.8.2
# via
# pylint-celery
# pylint-django
-pylint-pytest==0.3.0
+pylint-pytest==1.1.8
# via -r requirements/edx/testing.in
pylti1p3==2.0.0
# via -r requirements/edx/base.txt
@@ -1291,7 +1283,7 @@ pysrt==1.1.2
# via
# -r requirements/edx/base.txt
# edxval
-pytest==8.3.4
+pytest==8.2.0
# via
# -r requirements/edx/testing.in
# pylint-pytest
@@ -1310,7 +1302,7 @@ pytest-django==4.9.0
# via -r requirements/edx/testing.in
pytest-json-report==1.5.0
# via -r requirements/edx/testing.in
-pytest-metadata==1.8.0
+pytest-metadata==3.1.1
# via
# -r requirements/edx/testing.in
# pytest-json-report
@@ -1388,7 +1380,7 @@ redis==5.2.1
# via
# -r requirements/edx/base.txt
# walrus
-referencing==0.36.1
+referencing==0.36.2
# via
# -r requirements/edx/base.txt
# jsonschema
@@ -1445,7 +1437,7 @@ rules==3.5
# edx-enterprise
# edx-proctoring
# openedx-learning
-s3transfer==0.11.1
+s3transfer==0.11.2
# via
# -r requirements/edx/base.txt
# boto3
@@ -1504,7 +1496,7 @@ slumber==0.7.1
# edx-enterprise
sniffio==1.3.1
# via anyio
-snowflake-connector-python==3.12.4
+snowflake-connector-python==3.13.0
# via
# -r requirements/edx/base.txt
# edx-enterprise
@@ -1536,7 +1528,7 @@ sqlparse==0.5.3
# django
staff-graded-xblock==3.0.0
# via -r requirements/edx/base.txt
-starlette==0.41.3
+starlette==0.45.3
# via fastapi
stevedore==5.4.0
# via
@@ -1670,9 +1662,7 @@ wheel==0.45.1
# -r requirements/edx/base.txt
# django-pipeline
wrapt==1.17.2
- # via
- # -r requirements/edx/base.txt
- # astroid
+ # via -r requirements/edx/base.txt
xblock[django]==5.1.1
# via
# -r requirements/edx/base.txt
diff --git a/scripts/user_retirement/requirements/base.txt b/scripts/user_retirement/requirements/base.txt
index 5629aee43eea..35cf225d6b1b 100644
--- a/scripts/user_retirement/requirements/base.txt
+++ b/scripts/user_retirement/requirements/base.txt
@@ -6,17 +6,17 @@
#
asgiref==3.8.1
# via django
-attrs==24.3.0
+attrs==25.1.0
# via zeep
backoff==2.2.1
# via -r scripts/user_retirement/requirements/base.in
-boto3==1.36.3
+boto3==1.36.6
# via -r scripts/user_retirement/requirements/base.in
-botocore==1.36.3
+botocore==1.36.6
# via
# boto3
# s3transfer
-cachetools==5.5.0
+cachetools==5.5.1
# via google-auth
certifi==2024.12.14
# via requests
@@ -54,7 +54,7 @@ google-api-core==2.24.0
# via google-api-python-client
google-api-python-client==2.159.0
# via -r scripts/user_retirement/requirements/base.in
-google-auth==2.37.0
+google-auth==2.38.0
# via
# google-api-core
# google-api-python-client
@@ -136,7 +136,7 @@ requests-toolbelt==1.0.0
# via zeep
rsa==4.9
# via google-auth
-s3transfer==0.11.1
+s3transfer==0.11.2
# via boto3
simple-salesforce==1.12.6
# via -r scripts/user_retirement/requirements/base.in
diff --git a/scripts/user_retirement/requirements/testing.txt b/scripts/user_retirement/requirements/testing.txt
index ad69c332b702..4f13157aee07 100644
--- a/scripts/user_retirement/requirements/testing.txt
+++ b/scripts/user_retirement/requirements/testing.txt
@@ -8,23 +8,23 @@ asgiref==3.8.1
# via
# -r scripts/user_retirement/requirements/base.txt
# django
-attrs==24.3.0
+attrs==25.1.0
# via
# -r scripts/user_retirement/requirements/base.txt
# zeep
backoff==2.2.1
# via -r scripts/user_retirement/requirements/base.txt
-boto3==1.36.3
+boto3==1.36.6
# via
# -r scripts/user_retirement/requirements/base.txt
# moto
-botocore==1.36.3
+botocore==1.36.6
# via
# -r scripts/user_retirement/requirements/base.txt
# boto3
# moto
# s3transfer
-cachetools==5.5.0
+cachetools==5.5.1
# via
# -r scripts/user_retirement/requirements/base.txt
# google-auth
@@ -78,7 +78,7 @@ google-api-core==2.24.0
# google-api-python-client
google-api-python-client==2.159.0
# via -r scripts/user_retirement/requirements/base.txt
-google-auth==2.37.0
+google-auth==2.38.0
# via
# -r scripts/user_retirement/requirements/base.txt
# google-api-core
@@ -235,7 +235,7 @@ rsa==4.9
# via
# -r scripts/user_retirement/requirements/base.txt
# google-auth
-s3transfer==0.11.1
+s3transfer==0.11.2
# via
# -r scripts/user_retirement/requirements/base.txt
# boto3
diff --git a/xmodule/capa/tests/test_input_templates.py b/xmodule/capa/tests/test_input_templates.py
index 4b14bd5ef86c..3048f62095de 100644
--- a/xmodule/capa/tests/test_input_templates.py
+++ b/xmodule/capa/tests/test_input_templates.py
@@ -76,8 +76,7 @@ def render_to_xml(self, context_dict):
except Exception as exc:
raise TemplateError("Could not parse XML from '{0}': {1}".format( # lint-amnesty, pylint: disable=raise-missing-from
xml_str, str(exc)))
- else:
- return xml
+ return xml
def assert_has_xpath(self, xml_root, xpath, context_dict, exact_num=1):
"""
diff --git a/xmodule/split_test_block.py b/xmodule/split_test_block.py
index 05ca3a5db454..52f40547879a 100644
--- a/xmodule/split_test_block.py
+++ b/xmodule/split_test_block.py
@@ -419,9 +419,8 @@ def log_child_render(self, request, suffix=''): # lint-amnesty, pylint: disable
)
)
raise
- else:
- self.runtime.publish(self, 'xblock.split_test.child_render', {'child_id': child_id})
- return Response()
+ self.runtime.publish(self, 'xblock.split_test.child_render', {'child_id': child_id})
+ return Response()
def get_icon_class(self):
return self.child.get_icon_class() if self.child else 'other'