tags to ensure that it is valid XML
- if isinstance(grade_response, dict) and 'msg' in grade_response:
- grade_response['msg'] = HTML("
{0}
").format(grade_response['msg'])
-
- data = {
- 'xqueue_header': json.dumps(xqueue_header),
- 'xqueue_body': json.dumps(grade_response)
- }
-
- post(postback_url, data=data)
- self.log_message(f"XQueue: sent grading response {data} to {postback_url}")
-
- def _register_submission(self, xqueue_body_json):
- """
- If configured, send the submission's grader payload to another service.
- """
- url = self.server.config.get('register_submission_url')
-
- # If not configured, do not need to send anything
- if url is not None:
-
- try:
- xqueue_body = json.loads(xqueue_body_json)
- except ValueError:
- self.log_error(
- f"Could not decode XQueue body as JSON: '{xqueue_body_json}'")
-
- else:
-
- # Retrieve the grader payload, which should be a JSON-encoded dict.
- # We pass the payload directly to the service we are notifying, without
- # inspecting the contents.
- grader_payload = xqueue_body.get('grader_payload')
-
- if grader_payload is not None:
- response = post(url, data={'grader_payload': grader_payload})
- if not response.ok:
- self.log_error(
- "Could register submission at URL '{}'. Status was {}".format(
- url, response.status_code))
-
- else:
- self.log_message(
- f"XQueue body is missing 'grader_payload' key: '{xqueue_body}'"
- )
-
- def _is_grade_request(self):
- """
- Return a boolean indicating whether the requested URL indicates a submission.
- """
- return 'xqueue/submit' in self.path
-
-
-class StubXQueueService(StubHttpService):
- """
- A stub XQueue grading server that responds to POST requests to localhost.
- """
-
- HANDLER_CLASS = StubXQueueHandler
- NON_QUEUE_CONFIG_KEYS = ['default', 'register_submission_url']
-
- @property
- def queue_responses(self):
- """
- Returns a list of (pattern, response) tuples, where `pattern` is a pattern
- to match in the XQueue body, and `response` is a dictionary to return
- as the response from the grader.
-
- Every configuration key is a queue name,
- except for 'default' and 'register_submission_url' which have special meaning
- """
- return list({
- key: value
- for key, value in self.config.items()
- if key not in self.NON_QUEUE_CONFIG_KEYS
- }.items())
diff --git a/common/djangoapps/terrain/stubs/youtube.py b/common/djangoapps/terrain/stubs/youtube.py
deleted file mode 100644
index 67cac950f9fb..000000000000
--- a/common/djangoapps/terrain/stubs/youtube.py
+++ /dev/null
@@ -1,172 +0,0 @@
-"""
-Stub implementation of YouTube for acceptance tests.
-
-
-To start this stub server on its own from Vagrant:
-
-1.) Locally, modify your Vagrantfile so that it contains:
-
- config.vm.network :forwarded_port, guest: 8031, host: 8031
-
-2.) From within Vagrant dev environment do:
-
- cd common/djangoapps/terrain
- python -m stubs.start youtube 8031
-
-3.) Locally, try accessing http://localhost:8031/ and see that
- you get "Unused url" message inside the browser.
-"""
-
-
-import json
-import time
-from collections import OrderedDict
-
-import requests
-from six.moves.urllib.parse import urlparse
-
-from .http import StubHttpRequestHandler, StubHttpService
-
-
-class StubYouTubeHandler(StubHttpRequestHandler):
- """
- A handler for Youtube GET requests.
- """
-
- # Default number of seconds to delay the response to simulate network latency.
- DEFAULT_DELAY_SEC = 0.5
-
- def do_DELETE(self): # pylint: disable=invalid-name
- """
- Allow callers to delete all the server configurations using the /del_config URL.
- """
- if self.path in ("/del_config", "/del_config/"):
- self.server.config = {}
- self.log_message("Reset Server Configuration.")
- self.send_response(200)
- else:
- self.send_response(404)
-
- def do_GET(self):
- """
- Handle a GET request from the client and sends response back.
- """
- self.log_message(
- f"Youtube provider received GET request to path {self.path}"
- )
-
- if 'get_config' in self.path:
- self.send_json_response(self.server.config)
-
- elif 'test_transcripts_youtube' in self.path:
-
- if 't__eq_exist' in self.path:
- status_message = "".join([
- '',
- '
',
- 'Equal transcripts'
- ]).encode('utf-8')
-
- self.send_response(
- 200, content=status_message, headers={'Content-type': 'application/xml'}
- )
-
- elif 't_neq_exist' in self.path:
- status_message = "".join([
- '',
- '
',
- 'Transcripts sample, different that on server',
- ''
- ]).encode('utf-8')
-
- self.send_response(
- 200, content=status_message, headers={'Content-type': 'application/xml'}
- )
-
- else:
- self.send_response(404)
-
- elif 'test_youtube' in self.path:
- params = urlparse(self.path)
- youtube_id = params.path.split('/').pop()
-
- if self.server.config.get('youtube_api_private_video'):
- self._send_private_video_response(youtube_id, "I'm youtube private video.") # lint-amnesty, pylint: disable=too-many-function-args
- else:
- self._send_video_response(youtube_id, "I'm youtube.")
-
- elif 'get_youtube_api' in self.path:
- # Delay the response to simulate network latency
- time.sleep(self.server.config.get('time_to_response', self.DEFAULT_DELAY_SEC))
- if self.server.config.get('youtube_api_blocked'):
- self.send_response(404, content=b'', headers={'Content-type': 'text/plain'})
- else:
- # Get the response to send from YouTube.
- # We need to do this every time because Google sometimes sends different responses
- # as part of their own experiments, which has caused our tests to become "flaky"
- self.log_message("Getting iframe api from youtube.com")
- iframe_api_response = requests.get('https://www.youtube.com/iframe_api').content.strip(b"\n")
- self.send_response(200, content=iframe_api_response, headers={'Content-type': 'text/html'})
-
- else:
- self.send_response(
- 404, content=b"Unused url", headers={'Content-type': 'text/plain'}
- )
-
- def _send_video_response(self, youtube_id, message):
- """
- Send message back to the client for video player requests.
- Requires sending back callback id.
- """
- # Delay the response to simulate network latency
- time.sleep(self.server.config.get('time_to_response', self.DEFAULT_DELAY_SEC))
-
- # Construct the response content
- callback = self.get_params['callback']
-
- data = OrderedDict({
- 'items': list(
- OrderedDict({
- 'contentDetails': OrderedDict({
- 'id': youtube_id,
- 'duration': 'PT2M20S',
- })
- })
- )
- })
- response = f"{callback}({json.dumps(data)})".encode('utf-8')
-
- self.send_response(200, content=response, headers={'Content-type': 'text/html'})
- self.log_message(f"Youtube: sent response {message}")
-
- def _send_private_video_response(self, message):
- """
- Send private video error message back to the client for video player requests.
- """
- # Construct the response content
- callback = self.get_params['callback']
- data = OrderedDict({
- "error": OrderedDict({
- "code": 403,
- "errors": [
- {
- "code": "ServiceForbiddenException",
- "domain": "GData",
- "internalReason": "Private video"
- }
- ],
- "message": message,
- })
- })
- response = f"{callback}({json.dumps(data)})".encode('utf-8')
-
- self.send_response(200, content=response, headers={'Content-type': 'text/html'})
- self.log_message(f"Youtube: sent response {message}")
-
-
-class StubYouTubeService(StubHttpService):
- """
- A stub Youtube provider server that responds to GET requests to localhost.
- """
-
- HANDLER_CLASS = StubYouTubeHandler
diff --git a/conf/locale/config.yaml b/conf/locale/config.yaml
index 6faf3188be1f..8a486ec626d3 100644
--- a/conf/locale/config.yaml
+++ b/conf/locale/config.yaml
@@ -26,7 +26,6 @@ ignore_dirs:
# Directories that only contain tests.
- common/test
- test_root
- - '*/terrain'
- '*/spec'
- '*/tests'
- '*/djangoapps/*/features'
diff --git a/lms/djangoapps/courseware/date_summary.py b/lms/djangoapps/courseware/date_summary.py
index 4123da5f38b9..e6bd5ef70597 100644
--- a/lms/djangoapps/courseware/date_summary.py
+++ b/lms/djangoapps/courseware/date_summary.py
@@ -268,7 +268,7 @@ def date_type(self):
@property
def title(self):
enrollment = CourseEnrollment.get_enrollment(self.user, self.course_id)
- if enrollment and self.course.end and enrollment.created > self.course.end:
+ if self.course.self_paced and enrollment and self.course.start and enrollment.created > self.course.start:
return gettext_lazy('Enrollment Date')
return gettext_lazy('Course starts')
diff --git a/lms/djangoapps/courseware/tests/test_date_summary.py b/lms/djangoapps/courseware/tests/test_date_summary.py
index 27e7f1a3c226..e487af8a1f70 100644
--- a/lms/djangoapps/courseware/tests/test_date_summary.py
+++ b/lms/djangoapps/courseware/tests/test_date_summary.py
@@ -400,6 +400,37 @@ def test_course_start_date(self):
block = CourseStartDate(course, user)
assert block.date == course.start
+ @ddt.data(
+ # Instructor-paced course: Use course start date
+ (False, datetime(2025, 1, 10, tzinfo=utc), datetime(2025, 1, 12, tzinfo=utc),
+ datetime(2025, 1, 10, tzinfo=utc), 'Course starts'),
+
+ # Self-paced course: Enrollment created later than course start
+ (True, datetime(2025, 1, 10, tzinfo=utc), datetime(2025, 1, 12), datetime(2025, 1, 12, tzinfo=utc),
+ 'Enrollment Date'),
+
+ # Self-paced course: Enrollment created earlier than course start
+ (True, datetime(2025, 1, 10, tzinfo=utc), datetime(2025, 1, 8), datetime(2025, 1, 10, tzinfo=utc),
+ 'Course starts'),
+
+ # Self-paced course: No enrollment
+ (True, datetime(2025, 1, 10, tzinfo=utc), None, datetime(2025, 1, 10, tzinfo=utc), 'Course starts'),
+ )
+ @ddt.unpack
+ def test_course_start_date_label(self, self_paced, course_start, enrollment_created, expected_date, expected_title):
+ """
+ Test the CourseStartDate class has correct label for course start date
+ """
+ course = CourseFactory(self_paced=self_paced, start=course_start)
+ user = create_user()
+ if enrollment_created:
+ enrollment = CourseEnrollmentFactory(course_id=course.id, user=user, mode=CourseMode.VERIFIED)
+ enrollment.created = enrollment_created
+ enrollment.save()
+ date_summary = CourseStartDate(user=user, course=course)
+ self.assertEqual(date_summary.date, expected_date)
+ self.assertEqual(str(date_summary.title), expected_title)
+
## Tests Course End Date Block
def test_course_end_date_for_certificate_eligible_mode(self):
course = create_course_run(days_till_start=-1)
diff --git a/lms/djangoapps/grades/models.py b/lms/djangoapps/grades/models.py
index a5608eb39afd..4dd5fb51f4ef 100644
--- a/lms/djangoapps/grades/models.py
+++ b/lms/djangoapps/grades/models.py
@@ -467,12 +467,6 @@ def update_or_create_grade(cls, **params):
defaults=params,
)
- # TODO: Remove as part of EDUCATOR-4602.
- if str(usage_key.course_key) == 'course-v1:UQx+BUSLEAD5x+2T2019':
- log.info('Created/updated grade ***{}*** for user ***{}*** in course ***{}***'
- 'for subsection ***{}*** with default params ***{}***'
- .format(grade, user_id, usage_key.course_key, usage_key, params))
-
grade.override = PersistentSubsectionGradeOverride.get_override(user_id, usage_key)
if first_attempted is not None and grade.first_attempted is None:
grade.first_attempted = first_attempted
@@ -822,11 +816,6 @@ def update_or_create_override(
grade_defaults['override_reason'] = override_data['comment'] if 'comment' in override_data else None
grade_defaults['system'] = override_data['system'] if 'system' in override_data else None
- # TODO: Remove as part of EDUCATOR-4602.
- if str(subsection_grade_model.course_id) == 'course-v1:UQx+BUSLEAD5x+2T2019':
- log.info('Creating override for user ***{}*** for PersistentSubsectionGrade'
- '***{}*** with override data ***{}*** and derived grade_defaults ***{}***.'
- .format(requesting_user, subsection_grade_model, override_data, grade_defaults))
try:
override = PersistentSubsectionGradeOverride.objects.get(grade=subsection_grade_model)
for key, value in grade_defaults.items():
diff --git a/lms/djangoapps/grades/rest_api/v1/gradebook_views.py b/lms/djangoapps/grades/rest_api/v1/gradebook_views.py
index 26df7ce2583d..c295563da565 100644
--- a/lms/djangoapps/grades/rest_api/v1/gradebook_views.py
+++ b/lms/djangoapps/grades/rest_api/v1/gradebook_views.py
@@ -859,11 +859,6 @@ def post(self, request, course_key):
subsection = course.get_child(usage_key)
if subsection:
subsection_grade_model = self._create_subsection_grade(user, course, subsection)
- # TODO: Remove as part of EDUCATOR-4602.
- if str(course_key) == 'course-v1:UQx+BUSLEAD5x+2T2019':
- log.info('PersistentSubsectionGrade ***{}*** created for'
- ' subsection ***{}*** in course ***{}*** for user ***{}***.'
- .format(subsection_grade_model, subsection.location, course, user.id))
else:
self._log_update_result(request.user, requested_user_id, requested_usage_id, success=False)
result.append(GradebookUpdateResponseItem(
diff --git a/lms/djangoapps/grades/scores.py b/lms/djangoapps/grades/scores.py
index 38dd0dc18926..7a89a88c794b 100644
--- a/lms/djangoapps/grades/scores.py
+++ b/lms/djangoapps/grades/scores.py
@@ -102,10 +102,6 @@ def get_score(submissions_scores, csm_scores, persisted_block, block):
weight, graded - retrieved from the latest block content
"""
weight = _get_weight_from_block(persisted_block, block)
- # TODO: Remove as part of EDUCATOR-4602.
- if str(block.location.course_key) == 'course-v1:UQx+BUSLEAD5x+2T2019':
- log.info('Weight for block: ***{}*** is {}'
- .format(str(block.location), weight))
# Priority order for retrieving the scores:
# submissions API -> CSM -> grades persisted block -> latest block content
@@ -115,13 +111,6 @@ def get_score(submissions_scores, csm_scores, persisted_block, block):
_get_score_from_persisted_or_latest_block(persisted_block, block, weight)
)
- # TODO: Remove as part of EDUCATOR-4602.
- if str(block.location.course_key) == 'course-v1:UQx+BUSLEAD5x+2T2019':
- log.info('Calculated raw-earned: {}, raw_possible: {}, weighted_earned: '
- '{}, weighted_possible: {}, first_attempted: {} for block: ***{}***.'
- .format(raw_earned, raw_possible, weighted_earned,
- weighted_possible, first_attempted, str(block.location)))
-
if weighted_possible is None or weighted_earned is None:
return None
@@ -219,11 +208,6 @@ def _get_score_from_persisted_or_latest_block(persisted_block, block, weight):
Uses the raw_possible value from the persisted_block if found, else from
the latest block content.
"""
- # TODO: Remove as part of EDUCATOR-4602.
- if str(block.location.course_key) == 'course-v1:UQx+BUSLEAD5x+2T2019':
- log.info('Using _get_score_from_persisted_or_latest_block to calculate score for block: ***{}***.'.format(
- str(block.location)
- ))
raw_earned = 0.0
first_attempted = None
@@ -231,10 +215,6 @@ def _get_score_from_persisted_or_latest_block(persisted_block, block, weight):
raw_possible = persisted_block.raw_possible
else:
raw_possible = block.transformer_data[GradesTransformer].max_score
- # TODO: Remove as part of EDUCATOR-4602.
- if str(block.location.course_key) == 'course-v1:UQx+BUSLEAD5x+2T2019':
- log.info('Using latest block content to calculate score for block: ***{}***.')
- log.info(f'weight for block: ***{str(block.location)}*** is {raw_possible}.')
# TODO TNL-5982 remove defensive code for scorables without max_score
if raw_possible is None:
diff --git a/lms/djangoapps/grades/subsection_grade.py b/lms/djangoapps/grades/subsection_grade.py
index ba098a92a417..4ce0a1f3a463 100644
--- a/lms/djangoapps/grades/subsection_grade.py
+++ b/lms/djangoapps/grades/subsection_grade.py
@@ -170,39 +170,21 @@ def _compute_block_score( # lint-amnesty, pylint: disable=missing-function-docs
csm_scores,
persisted_block=None,
):
- # TODO: Remove as part of EDUCATOR-4602.
- if str(block_key.course_key) == 'course-v1:UQx+BUSLEAD5x+2T2019':
- log.info('Computing block score for block: ***{}*** in course: ***{}***.'.format(
- str(block_key),
- str(block_key.course_key),
- ))
try:
block = course_structure[block_key]
except KeyError:
- # TODO: Remove as part of EDUCATOR-4602.
- if str(block_key.course_key) == 'course-v1:UQx+BUSLEAD5x+2T2019':
- log.info('User\'s access to block: ***{}*** in course: ***{}*** has changed. '
- 'No block score calculated.'.format(str(block_key), str(block_key.course_key)))
# It's possible that the user's access to that
# block has changed since the subsection grade
# was last persisted.
+ pass
else:
if getattr(block, 'has_score', False):
- # TODO: Remove as part of EDUCATOR-4602.
- if str(block_key.course_key) == 'course-v1:UQx+BUSLEAD5x+2T2019':
- log.info('Block: ***{}*** in course: ***{}*** HAS has_score attribute. Continuing.'
- .format(str(block_key), str(block_key.course_key)))
return get_score(
submissions_scores,
csm_scores,
persisted_block,
block,
)
- # TODO: Remove as part of EDUCATOR-4602.
- if str(block_key.course_key) == 'course-v1:UQx+BUSLEAD5x+2T2019':
- log.info('Block: ***{}*** in course: ***{}*** DOES NOT HAVE has_score attribute. '
- 'No block score calculated.'
- .format(str(block_key), str(block_key.course_key)))
@staticmethod
def _aggregated_score_from_model(grade_model, is_graded):
@@ -283,23 +265,11 @@ def __init__(self, subsection, course_structure, submissions_scores, csm_scores)
start_node=subsection.location,
):
problem_score = self._compute_block_score(block_key, course_structure, submissions_scores, csm_scores)
-
- # TODO: Remove as part of EDUCATOR-4602.
- if str(block_key.course_key) == 'course-v1:UQx+BUSLEAD5x+2T2019':
- log.info('Calculated problem score ***{}*** for block ***{!s}***'
- ' in subsection ***{}***.'
- .format(problem_score, block_key, subsection.location))
if problem_score:
self.problem_scores[block_key] = problem_score
all_total, graded_total = graders.aggregate_scores(list(self.problem_scores.values()))
- # TODO: Remove as part of EDUCATOR-4602.
- if str(subsection.location.course_key) == 'course-v1:UQx+BUSLEAD5x+2T2019':
- log.info('Calculated aggregate all_total ***{}***'
- ' and grade_total ***{}*** for subsection ***{}***'
- .format(all_total, graded_total, subsection.location))
-
super().__init__(subsection, all_total, graded_total)
def update_or_create_model(self, student, score_deleted=False, force_update_subsections=False):
@@ -307,11 +277,6 @@ def update_or_create_model(self, student, score_deleted=False, force_update_subs
Saves or updates the subsection grade in a persisted model.
"""
if self._should_persist_per_attempted(score_deleted, force_update_subsections):
- # TODO: Remove as part of EDUCATOR-4602.
- if str(self.location.course_key) == 'course-v1:UQx+BUSLEAD5x+2T2019':
- log.info('Updating PersistentSubsectionGrade for student ***{}*** in'
- ' subsection ***{}*** with params ***{}***.'
- .format(student.id, self.location, self._persisted_model_params(student)))
model = PersistentSubsectionGrade.update_or_create_grade(**self._persisted_model_params(student))
if hasattr(model, 'override'):
diff --git a/lms/djangoapps/program_enrollments/management/commands/send_program_course_nudge_email.py b/lms/djangoapps/program_enrollments/management/commands/send_program_course_nudge_email.py
index c095b2d161a1..af2bfdca752d 100644
--- a/lms/djangoapps/program_enrollments/management/commands/send_program_course_nudge_email.py
+++ b/lms/djangoapps/program_enrollments/management/commands/send_program_course_nudge_email.py
@@ -15,6 +15,7 @@
from django.contrib.sites.models import Site
from django.core.management import BaseCommand
from django.utils import timezone
+from opaque_keys.edx.keys import CourseKey
from opaque_keys.edx.locator import CourseLocator
from common.djangoapps.track import segment
@@ -140,7 +141,9 @@ def get_course_run_to_suggest(self, candidate_programs, completed_course_id, use
)
break
for course_run in candidate_course['course_runs']:
- if self.valid_course_run(course_run) and course_run['key'] != completed_course_id:
+ course_org = CourseKey.from_string(course_run['key']).org
+ if self.valid_course_run(course_run) and course_run['key'] != completed_course_id \
+ and course_org not in settings.DISABLED_ORGS_FOR_PROGRAM_NUDGE:
return program, course_run, candidate_course
return None, None, None
diff --git a/lms/envs/common.py b/lms/envs/common.py
index e354f75a8530..cb7643c3668e 100644
--- a/lms/envs/common.py
+++ b/lms/envs/common.py
@@ -5376,6 +5376,14 @@ def _make_locale_paths(settings): # pylint: disable=missing-function-docstring
NOTIFICATION_TYPE_ICONS = {}
DEFAULT_NOTIFICATION_ICON_URL = ""
+############## NUDGE EMAILS ###############
+# .. setting_name: DISABLED_ORGS_FOR_PROGRAM_NUDGE
+# .. setting_default: []
+# .. setting_description: List of organization codes that should be disabled
+# .. for program nudge emails.
+# .. eg ['BTDx', 'MYTx']
+DISABLED_ORGS_FOR_PROGRAM_NUDGE = []
+
############################ AI_TRANSLATIONS ##################################
AI_TRANSLATIONS_API_URL = 'http://localhost:18760/api/v1'
diff --git a/lms/envs/production.py b/lms/envs/production.py
index 6dc6be634178..addcea0e6028 100644
--- a/lms/envs/production.py
+++ b/lms/envs/production.py
@@ -185,7 +185,6 @@ def get_env_setting(setting):
CMS_BASE = ENV_TOKENS.get('CMS_BASE', 'studio.edx.org')
ALLOWED_HOSTS = [
- # TODO: bbeggs remove this before prod, temp fix to get load testing running
"*",
ENV_TOKENS.get('LMS_BASE'),
FEATURES['PREVIEW_LMS_BASE'],
diff --git a/lms/templates/header/navbar-authenticated.html b/lms/templates/header/navbar-authenticated.html
index 58c3924ea9a7..c9ea97a423b5 100644
--- a/lms/templates/header/navbar-authenticated.html
+++ b/lms/templates/header/navbar-authenticated.html
@@ -48,7 +48,7 @@
% endif
diff --git a/lms/templates/header/navbar-not-authenticated.html b/lms/templates/header/navbar-not-authenticated.html
index 61448b73bd84..b50e535acda6 100644
--- a/lms/templates/header/navbar-not-authenticated.html
+++ b/lms/templates/header/navbar-not-authenticated.html
@@ -39,7 +39,7 @@
% if allows_login:
% if can_discover_courses:
%endif
% endif
diff --git a/openedx/core/djangoapps/content/search/documents.py b/openedx/core/djangoapps/content/search/documents.py
index 40fe4529272b..98cd7d576e0a 100644
--- a/openedx/core/djangoapps/content/search/documents.py
+++ b/openedx/core/djangoapps/content/search/documents.py
@@ -14,6 +14,7 @@
from rest_framework.exceptions import NotFound
from openedx.core.djangoapps.content.search.models import SearchAccess
+from openedx.core.djangoapps.content.search.plain_text_math import process_mathjax
from openedx.core.djangoapps.content_libraries import api as lib_api
from openedx.core.djangoapps.content_tagging import api as tagging_api
from openedx.core.djangoapps.xblock import api as xblock_api
@@ -220,7 +221,7 @@ class implementation returns only:
# Generate description from the content
description = _get_description_from_block_content(block_type, content_data)
if description:
- block_data[Fields.description] = description
+ block_data[Fields.description] = process_mathjax(description)
except Exception as err: # pylint: disable=broad-except
log.exception(f"Failed to process index_dictionary for {block.usage_key}: {err}")
diff --git a/openedx/core/djangoapps/content/search/plain_text_math.py b/openedx/core/djangoapps/content/search/plain_text_math.py
new file mode 100644
index 000000000000..70f6c3fd2cf0
--- /dev/null
+++ b/openedx/core/djangoapps/content/search/plain_text_math.py
@@ -0,0 +1,161 @@
+"""
+Helper class to convert mathjax equations to plain text.
+"""
+
+import re
+
+import unicodeit
+
+
+class InvalidMathEquation(Exception):
+ """Raised when mathjax equation is invalid. This is used to skip all transformations."""
+
+
+class EqnPatternNotFound(Exception):
+ """Raised when a pattern is not found in equation. This is used to skip a specific transformation."""
+
+
+class PlainTextMath:
+ """
+ Converts mathjax equations to plain text using unicodeit and some preprocessing.
+ """
+ equation_pattern = re.compile(
+ r'\[mathjaxinline\](.*?)\[\/mathjaxinline\]|\[mathjax\](.*?)\[\/mathjax\]|\\\((.*?)\\\)|\\\[(.*?)\\\]'
+ )
+ eqn_replacements = (
+ # just remove prefix `\`
+ ("\\sin", "sin"),
+ ("\\cos", "cos"),
+ ("\\tan", "tan"),
+ ("\\arcsin", "arcsin"),
+ ("\\arccos", "arccos"),
+ ("\\arctan", "arctan"),
+ ("\\cot", "cot"),
+ ("\\sec", "sec"),
+ ("\\csc", "csc"),
+ # Is used for matching brackets in mathjax, should not be required in plain text.
+ ("\\left", ""),
+ ("\\right", ""),
+ )
+ regex_replacements = (
+ # Makes text bold, so not required in plain text.
+ (re.compile(r'{\\bf (.*?)}'), r"\1"),
+ )
+ extract_inner_texts = (
+ # Replaces any eqn: `\name{inner_text}` with `inner_text`
+ "\\mathbf{",
+ "\\bm{",
+ )
+ frac_open_close_pattern = re.compile(r"}\s*{")
+
+ @staticmethod
+ def _nested_bracket_matcher(equation: str, opening_pattern: str) -> str:
+ r"""
+ Matches opening and closing brackets in given string.
+
+ Args:
+ equation: string
+ opening_pattern: for example, `\mathbf{`
+
+ Returns:
+ String inside the eqn brackets
+ """
+ start = equation.find(opening_pattern)
+ if start == -1:
+ raise EqnPatternNotFound()
+ open_count = 0
+ inner_start = start + len(opening_pattern)
+ for i, char in enumerate(equation[inner_start:]):
+ if char == "{":
+ open_count += 1
+ if char == "}":
+ if open_count == 0:
+ break
+ open_count -= 1
+ else:
+ raise InvalidMathEquation()
+ # In below example `|` symbol is used to denote index position
+ # |\mathbf{, \mathbf{|, \mathbf{some_text|}, \mathbf{some_text}|
+ return (start, inner_start, inner_start + i, inner_start + i + 1)
+
+ def _fraction_handler(self, equation: str) -> str:
+ r"""
+ Converts `\frac{x}{y}` to `(x/y)` while handling nested `{}`.
+
+ For example: `\frac{2}{\sqrt{1+y}}` is converted to `(2/\sqrt{1+y})`.
+
+ Args:
+ equation: string
+
+ Returns:
+ String with `\frac` replaced by normal `/` symbol.
+ """
+ try:
+ n_start, n_inner_start, n_inner_end, n_end = self._nested_bracket_matcher(equation, "\\frac{")
+ except EqnPatternNotFound:
+ return equation
+
+ numerator = equation[n_inner_start:n_inner_end]
+ # Handle nested fractions
+ numerator = self._fraction_handler(numerator)
+
+ try:
+ _, d_inner_start, d_inner_end, d_end = self._nested_bracket_matcher(equation[n_end:], "{")
+ except EqnPatternNotFound:
+ return equation
+
+ denominator = equation[n_end + d_inner_start:n_end + d_inner_end]
+ # Handle nested fractions
+ denominator = self._fraction_handler(denominator)
+ # Now re-create the equation with `(numerator / denominator)`
+ equation = equation[:n_start] + f"({numerator}/{denominator})" + equation[n_end + d_end:]
+ return equation
+
+ def _nested_text_extractor(self, equation: str, pattern: str) -> str:
+ """
+ Recursively extracts text from equation for given pattern
+ """
+ try:
+ start, inner_start, inner_end, end = self._nested_bracket_matcher(equation, pattern)
+ inner_text = equation[inner_start:inner_end]
+ inner_text = self._nested_text_extractor(inner_text, pattern)
+ equation = equation[:start] + inner_text + equation[end:]
+ except EqnPatternNotFound:
+ pass
+ return equation
+
+ def _handle_replacements(self, equation: str) -> str:
+ """
+ Makes a bunch of replacements in equation string.
+ """
+ for q, replacement in self.eqn_replacements:
+ equation = equation.replace(q, replacement)
+ for pattern in self.extract_inner_texts:
+ equation = self._nested_text_extractor(equation, pattern)
+ for pattern, replacement in self.regex_replacements:
+ equation = re.sub(pattern, replacement, equation)
+ return equation
+
+ def run(self, eqn_matches: re.Match) -> str:
+ """
+ Takes re.Match object and runs conversion process on each match group.
+ """
+ groups = eqn_matches.groups()
+ for group in groups:
+ if not group:
+ continue
+ original = group
+ try:
+ group = self._handle_replacements(group)
+ group = self._fraction_handler(group)
+ return unicodeit.replace(group)
+ except Exception: # pylint: disable=broad-except
+ return original
+ return None
+
+
+processor = PlainTextMath()
+
+
+def process_mathjax(content: str) -> str:
+ return re.sub(processor.equation_pattern, processor.run, content)
diff --git a/openedx/core/djangoapps/content/search/tests/test_documents.py b/openedx/core/djangoapps/content/search/tests/test_documents.py
index 603cc8d92f5e..a97caae168d6 100644
--- a/openedx/core/djangoapps/content/search/tests/test_documents.py
+++ b/openedx/core/djangoapps/content/search/tests/test_documents.py
@@ -477,3 +477,121 @@ def test_collection_with_published_library(self):
"num_children": 1
}
}
+
+ def test_mathjax_plain_text_conversion_for_search(self):
+ """
+ Test how an HTML block with mathjax equations gets converted to plain text in search description.
+ """
+ # pylint: disable=line-too-long
+ eqns = [
+ # (input, expected output)
+ ('Simple addition: \\( 2 + 3 \\)', 'Simple addition: 2 + 3'),
+ ('Simple subtraction: \\( 5 - 2 \\)', 'Simple subtraction: 5 − 2'),
+ ('Simple multiplication: \\( 4 * 6 \\)', 'Simple multiplication: 4 * 6'),
+ ('Simple division: \\( 8 / 2 \\)', 'Simple division: 8 / 2'),
+ ('Mixed arithmetic: \\( 2 + 3 4 \\)', 'Mixed arithmetic: 2 + 3 4'),
+ ('Simple exponentiation: \\[ 2^3 \\]', 'Simple exponentiation: 2³'),
+ ('Root extraction: \\[ 16^{1/2} \\]', 'Root extraction: 16¹^/²'),
+ ('Exponent with multiple terms: \\[ (2 + 3)^2 \\]', 'Exponent with multiple terms: (2 + 3)²'),
+ ('Nested exponents: \\[ 2^(3^2) \\]', 'Nested exponents: 2⁽3²)'),
+ ('Mixed roots: \\[ 8^{1/2} 3^2 \\]', 'Mixed roots: 8¹^/² 3²'),
+ ('Simple fraction: [mathjaxinline] 3/4 [/mathjaxinline]', 'Simple fraction: 3/4'),
+ (
+ 'Decimal to fraction conversion: [mathjaxinline] 0.75 = 3/4 [/mathjaxinline]',
+ 'Decimal to fraction conversion: 0.75 = 3/4',
+ ),
+ ('Mixed fractions: [mathjaxinline] 1 1/2 = 3/2 [/mathjaxinline]', 'Mixed fractions: 1 1/2 = 3/2'),
+ (
+ 'Converting decimals to mixed fractions: [mathjaxinline] 2.5 = 5/2 [/mathjaxinline]',
+ 'Converting decimals to mixed fractions: 2.5 = 5/2',
+ ),
+ (
+ 'Trig identities: [mathjaxinline] \\sin(x + y) = \\sin(x) \\cos(y) + \\cos(x) \\sin(y) [/mathjaxinline]',
+ 'Trig identities: sin(x + y) = sin(x) cos(y) + cos(x) sin(y)',
+ ),
+ (
+ 'Sine, cosine, and tangent: [mathjaxinline] \\sin(x) [/mathjaxinline] [mathjaxinline] \\cos(x) [/mathjaxinline] [mathjaxinline] \\tan(x) [/mathjaxinline]',
+ 'Sine, cosine, and tangent: sin(x) cos(x) tan(x)',
+ ),
+ (
+ 'Hyperbolic trig functions: [mathjaxinline] \\sinh(x) [/mathjaxinline] [mathjaxinline] \\cosh(x) [/mathjaxinline]',
+ 'Hyperbolic trig functions: sinh(x) cosh(x)',
+ ),
+ (
+ "Simple derivative: [mathjax] f(x) = x^2, f'(x) = 2x [/mathjax]",
+ "Simple derivative: f(x) = x², f'(x) = 2x",
+ ),
+ ('Double integral: [mathjax] int\\int (x + y) dxdy [/mathjax]', 'Double integral: int∫ (x + y) dxdy'),
+ (
+ 'Partial derivatives: [mathjax] f(x,y) = xy, \\frac{\\partial f}{\\partial x} = y [/mathjax] [mathjax] \\frac{\\partial f}{\\partial y} = x [/mathjax]',
+ 'Partial derivatives: f(x,y) = xy, (∂ f/∂ x) = y (∂ f/∂ y) = x',
+ ),
+ (
+ 'Mean and standard deviation: [mathjax] mu = 2, \\sigma = 1 [/mathjax]',
+ 'Mean and standard deviation: mu = 2, σ = 1',
+ ),
+ (
+ 'Binomial probability: [mathjax] P(X = k) = (\\binom{n}{k} p^k (1-p)^{n-k}) [/mathjax]',
+ 'Binomial probability: P(X = k) = (\\binom{n}{k} pᵏ (1−p)ⁿ⁻ᵏ)',
+ ),
+ ('Gaussian distribution: [mathjax] N(\\mu, \\sigma^2) [/mathjax]', 'Gaussian distribution: N(μ, σ²)'),
+ (
+ 'Greek letters: [mathjaxinline] \\alpha [/mathjaxinline] [mathjaxinline] \\beta [/mathjaxinline] [mathjaxinline] \\gamma [/mathjaxinline]',
+ 'Greek letters: α β γ',
+ ),
+ (
+ 'Subscripted variables: [mathjaxinline] x_i [/mathjaxinline] [mathjaxinline] y_j [/mathjaxinline]',
+ 'Subscripted variables: xᵢ yⱼ',
+ ),
+ ('Superscripted variables: [mathjaxinline] x^{i} [/mathjaxinline]', 'Superscripted variables: xⁱ'),
+ (
+ 'Not supported: \\( \\begin{bmatrix} 1 & 0 \\ 0 & 1 \\end{bmatrix} = I \\)',
+ 'Not supported: \\begin{bmatrix} 1 & 0 \\ 0 & 1 \\end{bmatrix} = I',
+ ),
+ (
+ 'Bold text: \\( {\\bf a} \\cdot {\\bf b} = |{\\bf a}| |{\\bf b}| \\cos(\\theta) \\)',
+ 'Bold text: a ⋅ b = |a| |b| cos(θ)',
+ ),
+ ('Bold text: \\( \\frac{\\sqrt{\\mathbf{2}+3}}{\\sqrt{4}} \\)', 'Bold text: (√{2+3}/√{4})'),
+ ('Nested Bold text 1: \\( \\mathbf{ \\frac{1}{2} } \\)', 'Nested Bold text 1: (1/2)'),
+ (
+ 'Nested Bold text 2: \\( \\mathbf{a \\cdot (a \\mathbf{\\times} b)} \\)',
+ 'Nested Bold text 2: a ⋅ (a × b)'
+ ),
+ (
+ 'Nested Bold text 3: \\( \\mathbf{a \\cdot (a \\bm{\\times} b)} \\)',
+ 'Nested Bold text 3: a ⋅ (a × b)'
+ ),
+ ('Sqrt test 1: \\(\\sqrt\\)', 'Sqrt test 1: √'),
+ ('Sqrt test 2: \\(x^2 + \\sqrt(y)\\)', 'Sqrt test 2: x² + √(y)'),
+ ('Sqrt test 3: [mathjaxinline]x^2 + \\sqrt(y)[/mathjaxinline]', 'Sqrt test 3: x² + √(y)'),
+ ('Fraction test 1: \\( \\frac{2} {3} \\)', 'Fraction test 1: (2/3)'),
+ ('Fraction test 2: \\( \\frac{2}{3} \\)', 'Fraction test 2: (2/3)'),
+ ('Fraction test 3: \\( \\frac{\\frac{2}{3}}{4} \\)', 'Fraction test 3: ((2/3)/4)'),
+ ('Fraction test 4: \\( \\frac{\\frac{2} {3}}{4} \\)', 'Fraction test 4: ((2/3)/4)'),
+ ('Fraction test 5: \\( \\frac{\\frac{2} {3}}{\\frac{4}{3}} \\)', 'Fraction test 5: ((2/3)/(4/3))'),
+ # Invalid equations.
+ ('Fraction error: \\( \\frac{2} \\)', 'Fraction error: \\frac{2}'),
+ ('Fraction error 2: \\( \\frac{\\frac{2}{3}{4} \\)', 'Fraction error 2: \\frac{\\frac{2}{3}{4}'),
+ ('Unclosed: [mathjaxinline]x^2', 'Unclosed: [mathjaxinline]x^2'),
+ (
+ 'Missing closing bracket: \\( \\frac{\\frac{2} {3}{\\frac{4}{3}} \\)',
+ 'Missing closing bracket: \\frac{\\frac{2} {3}{\\frac{4}{3}}'
+ ),
+ ('No equation: normal text', 'No equation: normal text'),
+ ]
+ # pylint: enable=line-too-long
+ block = BlockFactory.create(
+ parent_location=self.toy_course.location,
+ category="html",
+ display_name="Non-default HTML Block",
+ editor="raw",
+ use_latex_compiler=True,
+ data="|||".join(e[0] for e in eqns),
+ )
+ doc = {}
+ doc.update(searchable_doc_for_course_block(block))
+ doc.update(searchable_doc_tags(block.usage_key))
+ result = doc['description'].split('|||')
+ for i, eqn in enumerate(result):
+ assert eqn.strip() == eqns[i][1]
diff --git a/openedx/core/djangoapps/content_libraries/tests/test_content_libraries.py b/openedx/core/djangoapps/content_libraries/tests/test_content_libraries.py
index 83b277604071..f3a111cb0561 100644
--- a/openedx/core/djangoapps/content_libraries/tests/test_content_libraries.py
+++ b/openedx/core/djangoapps/content_libraries/tests/test_content_libraries.py
@@ -8,6 +8,7 @@
import ddt
from django.contrib.auth.models import Group
+from django.test import override_settings
from django.test.client import Client
from freezegun import freeze_time
from opaque_keys.edx.locator import LibraryLocatorV2, LibraryUsageLocatorV2
@@ -139,6 +140,63 @@ def test_library_validation(self):
'slug': ['Enter a valid “slug” consisting of Unicode letters, numbers, underscores, or hyphens.'],
}
+ def test_library_org_validation(self):
+ """
+ Staff users can create libraries in any existing or auto-created organization.
+ """
+ assert Organization.objects.filter(short_name='auto-created-org').count() == 0
+ self._create_library(slug="auto-created-org-1", title="Library in an auto-created org", org='auto-created-org')
+ assert Organization.objects.filter(short_name='auto-created-org').count() == 1
+ self._create_library(slug="existing-org-1", title="Library in an existing org", org="CL-TEST")
+
+ @patch(
+ "openedx.core.djangoapps.content_libraries.views.user_can_create_organizations",
+ )
+ @patch(
+ "openedx.core.djangoapps.content_libraries.views.get_allowed_organizations_for_libraries",
+ )
+ @override_settings(ORGANIZATIONS_AUTOCREATE=False)
+ def test_library_org_no_autocreate(self, mock_get_allowed_organizations, mock_can_create_organizations):
+ """
+ When org auto-creation is disabled, user must use one of their allowed orgs.
+ """
+ mock_can_create_organizations.return_value = False
+ mock_get_allowed_organizations.return_value = ["CL-TEST"]
+ assert Organization.objects.filter(short_name='auto-created-org').count() == 0
+ response = self._create_library(
+ slug="auto-created-org-2",
+ org="auto-created-org",
+ title="Library in an auto-created org",
+ expect_response=400,
+ )
+ assert response == {
+ 'org': "No such organization 'auto-created-org' found.",
+ }
+
+ Organization.objects.get_or_create(
+ short_name="not-allowed-org",
+ defaults={"name": "Content Libraries Test Org Membership"},
+ )
+ response = self._create_library(
+ slug="not-allowed-org",
+ org="not-allowed-org",
+ title="Library in an not-allowed org",
+ expect_response=400,
+ )
+ assert response == {
+ 'org': "User not allowed to create libraries in 'not-allowed-org'.",
+ }
+ assert mock_can_create_organizations.call_count == 1
+ assert mock_get_allowed_organizations.call_count == 1
+
+ self._create_library(
+ slug="allowed-org-2",
+ org="CL-TEST",
+ title="Library in an allowed org",
+ )
+ assert mock_can_create_organizations.call_count == 2
+ assert mock_get_allowed_organizations.call_count == 2
+
@skip("This endpoint shouldn't support num_blocks and has_unpublished_*.")
@patch("openedx.core.djangoapps.content_libraries.views.LibraryRootView.pagination_class.page_size", new=2)
def test_list_library(self):
diff --git a/openedx/core/djangoapps/content_libraries/views.py b/openedx/core/djangoapps/content_libraries/views.py
index c8c91d50331f..048226c5b16c 100644
--- a/openedx/core/djangoapps/content_libraries/views.py
+++ b/openedx/core/djangoapps/content_libraries/views.py
@@ -99,6 +99,10 @@
from rest_framework.views import APIView
from rest_framework.viewsets import GenericViewSet
+from cms.djangoapps.contentstore.views.course import (
+ get_allowed_organizations_for_libraries,
+ user_can_create_organizations,
+)
from openedx.core.djangoapps.content_libraries import api, permissions
from openedx.core.djangoapps.content_libraries.serializers import (
ContentLibraryBlockImportTaskCreateSerializer,
@@ -122,6 +126,7 @@
from openedx.core.lib.api.view_utils import view_auth_classes
from openedx.core.djangoapps.safe_sessions.middleware import mark_user_change_as_expected
from openedx.core.djangoapps.xblock import api as xblock_api
+from openedx.core.types.http import RestRequest
from .models import ContentLibrary, LtiGradedResource, LtiProfile
@@ -268,6 +273,14 @@ def post(self, request):
raise ValidationError( # lint-amnesty, pylint: disable=raise-missing-from
detail={"org": f"No such organization '{org_name}' found."}
)
+ # Ensure the user is allowed to create libraries under this org
+ if not (
+ user_can_create_organizations(request.user) or
+ org_name in get_allowed_organizations_for_libraries(request.user)
+ ):
+ raise ValidationError( # lint-amnesty, pylint: disable=raise-missing-from
+ detail={"org": f"User not allowed to create libraries in '{org_name}'."}
+ )
org = Organization.objects.get(short_name=org_name)
try:
@@ -667,7 +680,7 @@ class LibraryBlockCollectionsView(APIView):
View to set collections for a component.
"""
@convert_exceptions
- def patch(self, request, usage_key_str) -> Response:
+ def patch(self, request: RestRequest, usage_key_str) -> Response:
"""
Sets Collections for a Component.
@@ -688,7 +701,7 @@ def patch(self, request, usage_key_str) -> Response:
library_key=key.lib_key,
component=component,
collection_keys=collection_keys,
- created_by=self.request.user.id,
+ created_by=request.user.id,
content_library=content_library,
)
diff --git a/openedx/core/djangoapps/content_libraries/views_collections.py b/openedx/core/djangoapps/content_libraries/views_collections.py
index b6c1c999ba94..21c4b12dd3da 100644
--- a/openedx/core/djangoapps/content_libraries/views_collections.py
+++ b/openedx/core/djangoapps/content_libraries/views_collections.py
@@ -25,6 +25,7 @@
ContentLibraryCollectionComponentsUpdateSerializer,
ContentLibraryCollectionUpdateSerializer,
)
+from openedx.core.types.http import RestRequest
class LibraryCollectionsView(ModelViewSet):
@@ -89,7 +90,7 @@ def get_object(self) -> Collection:
return collection
@convert_exceptions
- def retrieve(self, request, *args, **kwargs) -> Response:
+ def retrieve(self, request: RestRequest, *args, **kwargs) -> Response:
"""
Retrieve the Content Library Collection
"""
@@ -97,7 +98,7 @@ def retrieve(self, request, *args, **kwargs) -> Response:
return super().retrieve(request, *args, **kwargs)
@convert_exceptions
- def list(self, request, *args, **kwargs) -> Response:
+ def list(self, request: RestRequest, *args, **kwargs) -> Response:
"""
List Collections that belong to Content Library
"""
@@ -105,7 +106,7 @@ def list(self, request, *args, **kwargs) -> Response:
return super().list(request, *args, **kwargs)
@convert_exceptions
- def create(self, request, *args, **kwargs) -> Response:
+ def create(self, request: RestRequest, *args, **kwargs) -> Response:
"""
Create a Collection that belongs to a Content Library
"""
@@ -139,7 +140,7 @@ def create(self, request, *args, **kwargs) -> Response:
return Response(serializer.data)
@convert_exceptions
- def partial_update(self, request, *args, **kwargs) -> Response:
+ def partial_update(self, request: RestRequest, *args, **kwargs) -> Response:
"""
Update a Collection that belongs to a Content Library
"""
@@ -161,7 +162,7 @@ def partial_update(self, request, *args, **kwargs) -> Response:
return Response(serializer.data)
@convert_exceptions
- def destroy(self, request, *args, **kwargs) -> Response:
+ def destroy(self, request: RestRequest, *args, **kwargs) -> Response:
"""
Soft-deletes a Collection that belongs to a Content Library
"""
@@ -176,7 +177,7 @@ def destroy(self, request, *args, **kwargs) -> Response:
@convert_exceptions
@action(detail=True, methods=['post'], url_path='restore', url_name='collection-restore')
- def restore(self, request, *args, **kwargs) -> Response:
+ def restore(self, request: RestRequest, *args, **kwargs) -> Response:
"""
Restores a soft-deleted Collection that belongs to a Content Library
"""
@@ -191,7 +192,7 @@ def restore(self, request, *args, **kwargs) -> Response:
@convert_exceptions
@action(detail=True, methods=['delete', 'patch'], url_path='components', url_name='components-update')
- def update_components(self, request, *args, **kwargs) -> Response:
+ def update_components(self, request: RestRequest, *args, **kwargs) -> Response:
"""
Adds (PATCH) or removes (DELETE) Components to/from a Collection.
@@ -209,7 +210,7 @@ def update_components(self, request, *args, **kwargs) -> Response:
content_library=content_library,
collection_key=collection_key,
usage_keys=usage_keys,
- created_by=self.request.user.id,
+ created_by=request.user.id,
remove=(request.method == "DELETE"),
)
diff --git a/openedx/core/djangoapps/content_staging/api.py b/openedx/core/djangoapps/content_staging/api.py
index f0432922dcb0..5f85d701faa5 100644
--- a/openedx/core/djangoapps/content_staging/api.py
+++ b/openedx/core/djangoapps/content_staging/api.py
@@ -66,7 +66,7 @@ def save_xblock_to_user_clipboard(block: XBlock, user_id: int, version_num: int
olx=block_data.olx_str,
display_name=block_metadata_utils.display_name_with_default(block),
suggested_url_name=usage_key.block_id,
- tags=block_data.tags,
+ tags=block_data.tags or {},
version_num=(version_num or 0),
)
(clipboard, _created) = _UserClipboard.objects.update_or_create(user_id=user_id, defaults={
@@ -209,7 +209,7 @@ def _user_clipboard_model_to_data(clipboard: _UserClipboard) -> UserClipboardDat
status=content.status,
block_type=content.block_type,
display_name=content.display_name,
- tags=content.tags,
+ tags=content.tags or {},
version_num=content.version_num,
),
source_usage_key=clipboard.source_usage_key,
diff --git a/openedx/core/djangoapps/content_staging/models.py b/openedx/core/djangoapps/content_staging/models.py
index 2eab7954e826..5e007bc4485a 100644
--- a/openedx/core/djangoapps/content_staging/models.py
+++ b/openedx/core/djangoapps/content_staging/models.py
@@ -67,7 +67,9 @@ class Meta:
version_num = models.PositiveIntegerField(default=0)
# Tags applied to the original source block(s) will be copied to the new block(s) on paste.
- tags = models.JSONField(null=True, help_text=_("Content tags applied to these blocks"))
+ tags: models.JSONField[dict | None, dict | None] = models.JSONField(
+ null=True, help_text=_("Content tags applied to these blocks")
+ )
@property
def olx_filename(self) -> str:
diff --git a/openedx/core/djangoapps/content_tagging/rest_api/v1/views.py b/openedx/core/djangoapps/content_tagging/rest_api/v1/views.py
index c2f79ef677db..615406ffccc5 100644
--- a/openedx/core/djangoapps/content_tagging/rest_api/v1/views.py
+++ b/openedx/core/djangoapps/content_tagging/rest_api/v1/views.py
@@ -10,7 +10,6 @@
from rest_framework import status
from rest_framework.decorators import action
from rest_framework.exceptions import PermissionDenied, ValidationError
-from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.views import APIView
from openedx_events.content_authoring.data import ContentObjectData, ContentObjectChangedData
@@ -19,6 +18,8 @@
CONTENT_OBJECT_TAGS_CHANGED,
)
+from openedx.core.types.http import RestRequest
+
from ...auth import has_view_object_tags_access
from ...api import (
create_taxonomy,
@@ -99,7 +100,7 @@ def perform_create(self, serializer):
serializer.instance = create_taxonomy(**serializer.validated_data, orgs=user_admin_orgs)
@action(detail=False, url_path="import", methods=["post"])
- def create_import(self, request: Request, **kwargs) -> Response: # type: ignore
+ def create_import(self, request: RestRequest, **kwargs) -> Response: # type: ignore
"""
Creates a new taxonomy with the given orgs and imports the tags from the uploaded file.
"""
@@ -183,7 +184,7 @@ class ObjectTagExportView(APIView):
""""
View to export a CSV with all children and tags for a given course/context.
"""
- def get(self, request: Request, **kwargs) -> StreamingHttpResponse:
+ def get(self, request: RestRequest, **kwargs) -> StreamingHttpResponse:
"""
Export a CSV with all children and tags for a given course/context.
"""
diff --git a/openedx/core/djangoapps/notifications/email/utils.py b/openedx/core/djangoapps/notifications/email/utils.py
index 34c245308785..ad8b8b85dfd8 100644
--- a/openedx/core/djangoapps/notifications/email/utils.py
+++ b/openedx/core/djangoapps/notifications/email/utils.py
@@ -100,7 +100,7 @@ def create_email_template_context(username):
"mailing_address": settings.CONTACT_MAILING_ADDRESS,
"logo_url": get_logo_url_for_email(),
"social_media": social_media_info,
- "notification_settings_url": f"{settings.ACCOUNT_MICROFRONTEND_URL}/notifications",
+ "notification_settings_url": f"{settings.ACCOUNT_MICROFRONTEND_URL}/#notifications",
"unsubscribe_url": get_unsubscribe_link(username, patch)
}
diff --git a/openedx/core/djangoapps/notifications/serializers.py b/openedx/core/djangoapps/notifications/serializers.py
index 80b1577b6355..b5e22a36a682 100644
--- a/openedx/core/djangoapps/notifications/serializers.py
+++ b/openedx/core/djangoapps/notifications/serializers.py
@@ -202,6 +202,7 @@ class Meta:
'content_context',
'content',
'content_url',
+ 'course_id',
'last_read',
'last_seen',
'created',
diff --git a/openedx/core/types/http.py b/openedx/core/types/http.py
new file mode 100644
index 000000000000..2896256a107a
--- /dev/null
+++ b/openedx/core/types/http.py
@@ -0,0 +1,45 @@
+"""
+Typing utilities for the HTTP requests, responses, etc.
+
+Includes utilties to work with both vanilla django as well as djangorestframework.
+"""
+from __future__ import annotations
+
+import django.contrib.auth.models # pylint: disable=imported-auth-user
+import django.http
+import rest_framework.request
+
+import openedx.core.types.user
+from openedx.core.types.meta import type_annotation_only
+
+
+@type_annotation_only
+class HttpRequest(django.http.HttpRequest):
+ """
+ A request which either has a concrete User (from django.contrib.auth) or is anonymous.
+ """
+ user: openedx.core.types.User
+
+
+@type_annotation_only
+class AuthenticatedHttpRequest(HttpRequest):
+ """
+ A request which is guaranteed to have a concrete User (from django.contrib.auth).
+ """
+ user: django.contrib.auth.models.User
+
+
+@type_annotation_only
+class RestRequest(rest_framework.request.Request):
+ """
+ Same as HttpRequest, but extended for rest_framework views.
+ """
+ user: openedx.core.types.User
+
+
+@type_annotation_only
+class AuthenticatedRestRequest(RestRequest):
+ """
+ Same as AuthenticatedHttpRequest, but extended for rest_framework views.
+ """
+ user: django.contrib.auth.models.User
diff --git a/openedx/core/types/meta.py b/openedx/core/types/meta.py
new file mode 100644
index 000000000000..39162b05b879
--- /dev/null
+++ b/openedx/core/types/meta.py
@@ -0,0 +1,37 @@
+"""
+Typing utilities for use on other typing utilities.
+"""
+from __future__ import annotations
+
+import typing as t
+
+
+def type_annotation_only(cls: type) -> type:
+ """
+ Decorates class which should only be used in type annotations.
+
+ This is useful when you want to enhance an existing 3rd-party concrete class with
+ type annotations for its members, but don't want the enhanced class to ever actually
+ be instantiated. For examples, see openedx.core.types.http.
+ """
+ if t.TYPE_CHECKING:
+ return cls
+ return _forbid_init(cls)
+
+
+def _forbid_init(forbidden: type) -> type:
+ """
+ Return a class which refuses to be instantiated.
+ """
+ class _ForbidInit:
+ """
+ The resulting class.
+ """
+ def __init__(self, *args, **kwargs):
+ raise NotImplementedError(
+ f"Class {forbidden.__module__}:{forbidden.__name__} "
+ "cannot be instantiated. You may use it as a type annotation, but objects "
+ "can only be created from its concrete superclasses."
+ )
+
+ return _ForbidInit
diff --git a/openedx/core/types/user.py b/openedx/core/types/user.py
index 95b1fec607fc..9eb63edba358 100644
--- a/openedx/core/types/user.py
+++ b/openedx/core/types/user.py
@@ -1,8 +1,10 @@
"""
Typing utilities for the User models.
"""
-from typing import Union
+from __future__ import annotations
+
+import typing as t
import django.contrib.auth.models
-User = Union[django.contrib.auth.models.User, django.contrib.auth.models.AnonymousUser]
+User: t.TypeAlias = django.contrib.auth.models.User | django.contrib.auth.models.AnonymousUser
diff --git a/requirements/common_constraints.txt b/requirements/common_constraints.txt
index b8166ba67540..f3cc8fc9c9e8 100644
--- a/requirements/common_constraints.txt
+++ b/requirements/common_constraints.txt
@@ -28,3 +28,7 @@ elasticsearch<7.14.0
# Cause: https://github.com/openedx/edx-lint/issues/458
# This can be unpinned once https://github.com/openedx/edx-lint/issues/459 has been resolved.
pip<24.3
+
+# Cause: https://github.com/openedx/edx-lint/issues/475
+# This can be unpinned once https://github.com/openedx/edx-lint/issues/476 has been resolved.
+urllib3<2.3.0
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index a2c90429c5b0..865d224beab7 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -61,11 +61,13 @@ django-webpack-loader==0.7.0
djangorestframework<3.15.0
# Date: 2024-07-19
-# Generally speaking, the major version of django-stubs should match the major version of django.
-# Specifically, we need to perpetually constrain django-stubs to a compatible version based on:
+# Generally speaking, the major version of django-stubs must either match the major version
+# of django, or exceed it by 1. So, we will need to perpetually constrain django-stubs and
+# update it as we perform django upgrades. For more details, see:
# https://github.com/typeddjango/django-stubs?tab=readme-ov-file#version-compatibility
+# including the note on "Partial Support".
# Issue: https://github.com/openedx/edx-platform/issues/35275
-django-stubs<5
+django-stubs<6
# Date: 2024-07-23
# django-storages==1.14.4 breaks course imports
@@ -78,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.5.2
+edx-enterprise==5.6.1
# Date: 2024-05-09
# This has to be constrained as well because newer versions of edx-i18n-tools need the
@@ -112,12 +114,6 @@ markdown<3.4.0
# Issue for unpinning: https://github.com/openedx/edx-platform/issues/35270
moto<5.0
-# Date: 2024-10-16
-# MyPY 1.12.0 fails on all PRs with the following error:
-# openedx/core/djangoapps/content_libraries/api.py:732: error: INTERNAL ERROR
-# Issue for unpinning: https://github.com/openedx/edx-platform/issues/35667
-mypy<1.12.0
-
# Date: 2024-07-16
# We need to upgrade the version of elasticsearch to atleast 7.15 before we can upgrade to Numpy 2.0.0
# Otherwise we see a failure while running the following command:
@@ -181,3 +177,15 @@ social-auth-app-django<=5.4.1
# # Date: 2024-10-14
# # The edx-enterprise is currently using edx-rest-api-client==5.7.1, which needs to be updated first.
# edx-rest-api-client==5.7.1
+
+# Date 2025-01-08
+# elasticsearch==7.13.x is downgrading urllib3 from 2.2.3 to 1.26.20
+# https://github.com/elastic/elasticsearch-py/blob/v7.13.4/setup.py#L42
+# We are pinning this until we can upgrade to a version of elasticsearch that uses a more recent version of urllib3.
+# Issue for unpinning: https://github.com/openedx/edx-platform/issues/35126
+elasticsearch==7.9.1
+
+# Date 2025-01-10
+# Cause: https://github.com/openedx/edx-platform/issues/36095
+# Issue for unpinning https://github.com/openedx/edx-platform/issues/36096
+lti-consumer-xblock==9.12.1
diff --git a/requirements/edx-sandbox/base.txt b/requirements/edx-sandbox/base.txt
index 60a5eff8a62d..1bd3d149f487 100644
--- a/requirements/edx-sandbox/base.txt
+++ b/requirements/edx-sandbox/base.txt
@@ -20,11 +20,11 @@ cryptography==44.0.0
# via -r requirements/edx-sandbox/base.in
cycler==0.12.1
# via matplotlib
-fonttools==4.55.2
+fonttools==4.55.3
# via matplotlib
joblib==1.4.2
# via nltk
-kiwisolver==1.4.7
+kiwisolver==1.4.8
# via matplotlib
lxml[html-clean,html_clean]==5.3.0
# via
@@ -37,7 +37,7 @@ markupsafe==3.0.2
# via
# chem
# openedx-calc
-matplotlib==3.9.3
+matplotlib==3.10.0
# via -r requirements/edx-sandbox/base.in
mpmath==1.3.0
# via sympy
@@ -59,11 +59,11 @@ openedx-calc==4.0.1
# via -r requirements/edx-sandbox/base.in
packaging==24.2
# via matplotlib
-pillow==11.0.0
+pillow==11.1.0
# via matplotlib
pycparser==2.22
# via cffi
-pyparsing==3.2.0
+pyparsing==3.2.1
# via
# -r requirements/edx-sandbox/base.in
# chem
@@ -75,7 +75,7 @@ random2==1.0.2
# via -r requirements/edx-sandbox/base.in
regex==2024.11.6
# via nltk
-scipy==1.14.1
+scipy==1.15.0
# via
# -r requirements/edx-sandbox/base.in
# chem
diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt
index 7778afbb0f47..86431ca50e3e 100644
--- a/requirements/edx/base.txt
+++ b/requirements/edx/base.txt
@@ -10,11 +10,11 @@ acid-xblock==0.4.1
# via -r requirements/edx/kernel.in
aiohappyeyeballs==2.4.4
# via aiohttp
-aiohttp==3.11.9
+aiohttp==3.11.11
# via
# geoip2
# openai
-aiosignal==1.3.1
+aiosignal==1.3.2
# via aiohttp
algoliasearch==3.0.0
# via
@@ -37,7 +37,7 @@ asgiref==3.8.1
# django-countries
asn1crypto==1.5.1
# via snowflake-connector-python
-attrs==24.2.0
+attrs==24.3.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.35.76
+boto3==1.35.93
# via
# -r requirements/edx/kernel.in
# django-ses
# fs-s3fs
# ora2
-botocore==1.35.76
+botocore==1.35.93
# via
# -r requirements/edx/kernel.in
# boto3
@@ -101,7 +101,7 @@ celery==5.4.0
# edx-enterprise
# event-tracking
# openedx-learning
-certifi==2024.8.30
+certifi==2024.12.14
# via
# elasticsearch
# py2neo
@@ -138,7 +138,7 @@ click-plugins==1.1.1
# via celery
click-repl==0.3.0
# via celery
-code-annotations==2.0.0
+code-annotations==2.1.0
# via
# edx-enterprise
# edx-toggles
@@ -243,7 +243,7 @@ django==4.2.17
# xss-utils
django-appconf==1.0.6
# via django-statici18n
-django-cache-memoize==0.2.0
+django-cache-memoize==0.2.1
# via edx-enterprise
django-celery-results==2.5.1
# via -r requirements/edx/kernel.in
@@ -282,7 +282,7 @@ django-ipware==7.0.1
# -r requirements/edx/kernel.in
# edx-enterprise
# edx-proctoring
-django-js-asset==2.2.0
+django-js-asset==3.0.1
# via django-mptt
django-method-override==1.0.4
# via -r requirements/edx/kernel.in
@@ -319,7 +319,7 @@ django-oauth-toolkit==1.7.1
# edx-enterprise
django-object-actions==4.3.0
# via edx-enterprise
-django-pipeline==3.1.0
+django-pipeline==4.0.0
# via -r requirements/edx/kernel.in
django-push-notifications==3.1.0
# via edx-ace
@@ -329,7 +329,7 @@ django-sekizai==4.1.0
# via
# -r requirements/edx/kernel.in
# openedx-django-wiki
-django-ses==4.3.0
+django-ses==4.3.1
# via -r requirements/edx/bundled.in
django-simple-history==3.4.0
# via
@@ -430,7 +430,7 @@ edx-celeryutils==1.3.0
# super-csv
edx-codejail==3.5.2
# via -r requirements/edx/kernel.in
-edx-completion==4.7.6
+edx-completion==4.7.8
# via -r requirements/edx/kernel.in
edx-django-release-util==1.4.0
# via
@@ -468,7 +468,7 @@ edx-drf-extensions==10.5.0
# edx-when
# edxval
# openedx-learning
-edx-enterprise==5.5.2
+edx-enterprise==5.6.1
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/kernel.in
@@ -503,7 +503,7 @@ edx-opaque-keys[django]==2.11.0
# ora2
edx-organizations==6.13.0
# via -r requirements/edx/kernel.in
-edx-proctoring==4.18.4
+edx-proctoring==5.0.1
# via
# -r requirements/edx/kernel.in
# edx-proctoring-proctortrack
@@ -520,7 +520,7 @@ edx-search==4.1.1
# openedx-forum
edx-sga==0.25.0
# via -r requirements/edx/bundled.in
-edx-submissions==3.8.3
+edx-submissions==3.8.4
# via
# -r requirements/edx/kernel.in
# ora2
@@ -544,11 +544,12 @@ edx-when==2.5.0
# via
# -r requirements/edx/kernel.in
# edx-proctoring
-edxval==2.7.0
+edxval==2.8.0
# via -r requirements/edx/kernel.in
elasticsearch==7.9.1
# via
# -c requirements/edx/../common_constraints.txt
+ # -c requirements/edx/../constraints.txt
# edx-search
# openedx-forum
enmerkar==0.7.1
@@ -562,7 +563,7 @@ event-tracking==3.0.0
# edx-completion
# edx-proctoring
# edx-search
-fastavro==1.9.7
+fastavro==1.10.0
# via openedx-events
filelock==3.16.1
# via snowflake-connector-python
@@ -588,16 +589,16 @@ geoip2==4.8.1
# via -r requirements/edx/kernel.in
glob2==0.7
# via -r requirements/edx/kernel.in
-google-api-core[grpc]==2.23.0
+google-api-core[grpc]==2.24.0
# via
# firebase-admin
# google-api-python-client
# google-cloud-core
# google-cloud-firestore
# google-cloud-storage
-google-api-python-client==2.154.0
+google-api-python-client==2.157.0
# via firebase-admin
-google-auth==2.36.0
+google-auth==2.37.0
# via
# google-api-core
# google-api-python-client
@@ -625,11 +626,11 @@ googleapis-common-protos==1.66.0
# via
# google-api-core
# grpcio-status
-grpcio==1.68.1
+grpcio==1.69.0
# via
# google-api-core
# grpcio-status
-grpcio-status==1.68.1
+grpcio-status==1.69.0
# via google-api-core
gunicorn==23.0.0
# via -r requirements/edx/kernel.in
@@ -663,7 +664,7 @@ ipaddress==1.0.23
# via -r requirements/edx/kernel.in
isodate==0.7.2
# via python3-saml
-jinja2==3.1.4
+jinja2==3.1.5
# via code-annotations
jmespath==1.0.1
# via
@@ -704,8 +705,10 @@ lazy==1.6
# xblock
loremipsum==1.0.5
# via ora2
-lti-consumer-xblock==9.12.0
- # via -r requirements/edx/kernel.in
+lti-consumer-xblock==9.12.1
+ # via
+ # -c requirements/edx/../constraints.txt
+ # -r requirements/edx/kernel.in
lxml[html-clean,html_clean]==5.3.0
# via
# -r requirements/edx/kernel.in
@@ -723,7 +726,7 @@ lxml-html-clean==0.4.1
# via lxml
mailsnake==1.6.4
# via -r requirements/edx/bundled.in
-mako==1.3.7
+mako==1.3.8
# via
# -r requirements/edx/kernel.in
# acid-xblock
@@ -770,9 +773,9 @@ mysqlclient==2.2.6
# via
# -r requirements/edx/kernel.in
# openedx-forum
-newrelic==10.3.1
+newrelic==10.4.0
# via edx-django-utils
-nh3==0.2.19
+nh3==0.2.20
# via -r requirements/edx/kernel.in
nltk==3.9.1
# via chem
@@ -839,7 +842,7 @@ optimizely-sdk==4.1.1
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/bundled.in
-ora2==6.14.1
+ora2==6.14.3
# via -r requirements/edx/bundled.in
packaging==24.2
# via
@@ -868,7 +871,7 @@ pgpy==0.6.0
# via edx-enterprise
piexif==1.1.3
# via -r requirements/edx/kernel.in
-pillow==11.0.0
+pillow==11.1.0
# via
# -r requirements/edx/kernel.in
# edx-enterprise
@@ -889,14 +892,14 @@ proto-plus==1.25.0
# via
# google-api-core
# google-cloud-firestore
-protobuf==5.29.1
+protobuf==5.29.2
# via
# google-api-core
# google-cloud-firestore
# googleapis-common-protos
# grpcio-status
# proto-plus
-psutil==6.1.0
+psutil==6.1.1
# via
# -r requirements/edx/kernel.in
# edx-django-utils
@@ -921,11 +924,11 @@ pycryptodomex==3.21.0
# edx-proctoring
# lti-consumer-xblock
# pyjwkest
-pydantic==2.10.3
+pydantic==2.10.4
# via camel-converter
-pydantic-core==2.27.1
+pydantic-core==2.27.2
# via pydantic
-pygments==2.18.0
+pygments==2.19.1
# via py2neo
pyjwkest==1.4.2
# via
@@ -969,7 +972,7 @@ pyopenssl==24.3.0
# via
# optimizely-sdk
# snowflake-connector-python
-pyparsing==3.2.0
+pyparsing==3.2.1
# via
# chem
# httplib2
@@ -1038,7 +1041,7 @@ random2==1.0.2
# via -r requirements/edx/kernel.in
recommender-xblock==3.0.0
# via -r requirements/edx/bundled.in
-redis==5.2.0
+redis==5.2.1
# via
# -r requirements/edx/kernel.in
# walrus
@@ -1095,7 +1098,7 @@ s3transfer==0.10.4
# via boto3
sailthru-client==2.2.3
# via edx-ace
-scipy==1.14.1
+scipy==1.15.0
# via
# chem
# openedx-calc
@@ -1159,7 +1162,7 @@ sortedcontainers==2.4.0
# snowflake-connector-python
soupsieve==2.6
# via beautifulsoup4
-sqlparse==0.5.2
+sqlparse==0.5.3
# via django
staff-graded-xblock==2.3.0
# via -r requirements/edx/bundled.in
@@ -1205,6 +1208,8 @@ unicodecsv==0.14.1
# via
# -r requirements/edx/kernel.in
# edx-enterprise
+unicodeit==0.7.5
+ # via -r requirements/edx/kernel.in
uritemplate==4.1.1
# via
# drf-spectacular
@@ -1212,6 +1217,7 @@ uritemplate==4.1.1
# google-api-python-client
urllib3==2.2.3
# via
+ # -c requirements/edx/../common_constraints.txt
# botocore
# elasticsearch
# py2neo
@@ -1246,6 +1252,8 @@ webob==1.8.9
# via
# -r requirements/edx/kernel.in
# xblock
+wheel==0.45.1
+ # via django-pipeline
wrapt==1.17.0
# via -r requirements/edx/kernel.in
xblock[django]==5.1.0
diff --git a/requirements/edx/coverage.txt b/requirements/edx/coverage.txt
index 38acef7c7978..26669aec6cb0 100644
--- a/requirements/edx/coverage.txt
+++ b/requirements/edx/coverage.txt
@@ -6,15 +6,15 @@
#
chardet==5.2.0
# via diff-cover
-coverage==7.6.8
+coverage==7.6.10
# via -r requirements/edx/coverage.in
-diff-cover==9.2.0
+diff-cover==9.2.1
# via -r requirements/edx/coverage.in
-jinja2==3.1.4
+jinja2==3.1.5
# via diff-cover
markupsafe==3.0.2
# via jinja2
pluggy==1.5.0
# via diff-cover
-pygments==2.18.0
+pygments==2.19.1
# via diff-cover
diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt
index 2b51f6a979cf..e328b6dcb1f2 100644
--- a/requirements/edx/development.txt
+++ b/requirements/edx/development.txt
@@ -21,13 +21,13 @@ aiohappyeyeballs==2.4.4
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# aiohttp
-aiohttp==3.11.9
+aiohttp==3.11.11
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# geoip2
# openai
-aiosignal==1.3.1
+aiosignal==1.3.2
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -60,7 +60,7 @@ annotated-types==0.7.0
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# pydantic
-anyio==4.7.0
+anyio==4.8.0
# via
# -r requirements/edx/testing.txt
# starlette
@@ -76,6 +76,7 @@ asgiref==3.8.1
# django
# django-cors-headers
# django-countries
+ # django-stubs
asn1crypto==1.5.1
# via
# -r requirements/edx/doc.txt
@@ -89,7 +90,7 @@ astroid==2.13.5
# pylint
# pylint-celery
# sphinx-autoapi
-attrs==24.2.0
+attrs==24.3.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -144,14 +145,14 @@ boto==2.49.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
-boto3==1.35.76
+boto3==1.35.93
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# django-ses
# fs-s3fs
# ora2
-botocore==1.35.76
+botocore==1.35.93
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -192,7 +193,7 @@ celery==5.4.0
# edx-enterprise
# event-tracking
# openedx-learning
-certifi==2024.8.30
+certifi==2024.12.14
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -267,7 +268,7 @@ click-repl==0.3.0
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# celery
-code-annotations==2.0.0
+code-annotations==2.1.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -282,7 +283,7 @@ colorama==0.4.6
# via
# -r requirements/edx/testing.txt
# tox
-coverage[toml]==7.6.8
+coverage[toml]==7.6.10
# via
# -r requirements/edx/testing.txt
# pytest-cov
@@ -327,7 +328,7 @@ defusedxml==0.7.1
# ora2
# python3-openid
# social-auth-core
-diff-cover==9.2.0
+diff-cover==9.2.1
# via -r requirements/edx/testing.txt
dill==0.3.9
# via
@@ -419,7 +420,7 @@ django-appconf==1.0.6
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# django-statici18n
-django-cache-memoize==0.2.0
+django-cache-memoize==0.2.1
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -478,7 +479,7 @@ django-ipware==7.0.1
# -r requirements/edx/testing.txt
# edx-enterprise
# edx-proctoring
-django-js-asset==2.2.0
+django-js-asset==3.0.1
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -531,7 +532,7 @@ django-object-actions==4.3.0
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# edx-enterprise
-django-pipeline==3.1.0
+django-pipeline==4.0.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -549,7 +550,7 @@ django-sekizai==4.1.0
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# openedx-django-wiki
-django-ses==4.3.0
+django-ses==4.3.1
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -577,7 +578,7 @@ django-storages==1.14.3
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# edxval
-django-stubs==4.2.7
+django-stubs==5.1.1
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/development.in
@@ -625,7 +626,7 @@ djangorestframework==3.14.0
# openedx-learning
# ora2
# super-csv
-djangorestframework-stubs==3.14.5
+djangorestframework-stubs==3.15.2
# via -r requirements/edx/development.in
djangorestframework-xml==2.0.0
# via
@@ -701,7 +702,7 @@ edx-codejail==3.5.2
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
-edx-completion==4.7.6
+edx-completion==4.7.8
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -746,7 +747,7 @@ edx-drf-extensions==10.5.0
# edx-when
# edxval
# openedx-learning
-edx-enterprise==5.5.2
+edx-enterprise==5.6.1
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/doc.txt
@@ -796,7 +797,7 @@ edx-organizations==6.13.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
-edx-proctoring==4.18.4
+edx-proctoring==5.0.1
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -821,7 +822,7 @@ edx-sga==0.25.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
-edx-submissions==3.8.3
+edx-submissions==3.8.4
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -853,13 +854,14 @@ edx-when==2.5.0
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# edx-proctoring
-edxval==2.7.0
+edxval==2.8.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
elasticsearch==7.9.1
# via
# -c requirements/edx/../common_constraints.txt
+ # -c requirements/edx/../constraints.txt
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# edx-search
@@ -887,7 +889,7 @@ execnet==2.1.1
# pytest-xdist
factory-boy==3.3.1
# via -r requirements/edx/testing.txt
-faker==33.1.0
+faker==33.3.0
# via
# -r requirements/edx/testing.txt
# factory-boy
@@ -895,7 +897,7 @@ fastapi==0.115.6
# via
# -r requirements/edx/testing.txt
# pact-python
-fastavro==1.9.7
+fastavro==1.10.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -941,17 +943,17 @@ geoip2==4.8.1
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
-gitdb==4.0.11
+gitdb==4.0.12
# via
# -r requirements/edx/doc.txt
# gitpython
-gitpython==3.1.43
+gitpython==3.1.44
# via -r requirements/edx/doc.txt
glob2==0.7
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
-google-api-core[grpc]==2.23.0
+google-api-core[grpc]==2.24.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -960,12 +962,12 @@ google-api-core[grpc]==2.23.0
# google-cloud-core
# google-cloud-firestore
# google-cloud-storage
-google-api-python-client==2.154.0
+google-api-python-client==2.157.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# firebase-admin
-google-auth==2.36.0
+google-auth==2.37.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1017,13 +1019,13 @@ grimp==3.5
# via
# -r requirements/edx/testing.txt
# import-linter
-grpcio==1.68.1
+grpcio==1.69.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# google-api-core
# grpcio-status
-grpcio-status==1.68.1
+grpcio-status==1.69.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1104,7 +1106,7 @@ isort==5.13.2
# via
# -r requirements/edx/testing.txt
# pylint
-jinja2==3.1.4
+jinja2==3.1.5
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1187,8 +1189,9 @@ loremipsum==1.0.5
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# ora2
-lti-consumer-xblock==9.12.0
+lti-consumer-xblock==9.12.1
# via
+ # -c requirements/edx/../constraints.txt
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
lxml[html-clean]==5.3.0
@@ -1215,7 +1218,7 @@ mailsnake==1.6.4
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
-mako==1.3.7
+mako==1.3.8
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1254,7 +1257,7 @@ meilisearch==0.33.0
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# edx-search
-mistune==3.0.2
+mistune==3.1.0
# via
# -r requirements/edx/doc.txt
# sphinx-mdinclude
@@ -1291,10 +1294,8 @@ multidict==6.1.0
# -r requirements/edx/testing.txt
# aiohttp
# yarl
-mypy==1.11.2
- # via
- # -c requirements/edx/../constraints.txt
- # -r requirements/edx/development.in
+mypy==1.14.1
+ # via -r requirements/edx/development.in
mypy-extensions==1.0.0
# via mypy
mysqlclient==2.2.6
@@ -1302,12 +1303,12 @@ mysqlclient==2.2.6
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# openedx-forum
-newrelic==10.3.1
+newrelic==10.4.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# edx-django-utils
-nh3==0.2.19
+nh3==0.2.20
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1406,7 +1407,7 @@ optimizely-sdk==4.1.1
# -c requirements/edx/../constraints.txt
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
-ora2==6.14.1
+ora2==6.14.3
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1424,7 +1425,7 @@ packaging==24.2
# snowflake-connector-python
# sphinx
# tox
-pact-python==2.2.2
+pact-python==2.3.0
# via -r requirements/edx/testing.txt
pansi==2024.11.0
# via
@@ -1468,7 +1469,7 @@ piexif==1.1.3
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
-pillow==11.0.0
+pillow==11.1.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1514,7 +1515,7 @@ proto-plus==1.25.0
# -r requirements/edx/testing.txt
# google-api-core
# google-cloud-firestore
-protobuf==5.29.1
+protobuf==5.29.2
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1523,7 +1524,7 @@ protobuf==5.29.1
# googleapis-common-protos
# grpcio-status
# proto-plus
-psutil==6.1.0
+psutil==6.1.1
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1569,22 +1570,22 @@ pycryptodomex==3.21.0
# edx-proctoring
# lti-consumer-xblock
# pyjwkest
-pydantic==2.10.3
+pydantic==2.10.4
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# camel-converter
# fastapi
-pydantic-core==2.27.1
+pydantic-core==2.27.2
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# pydantic
-pydata-sphinx-theme==0.16.0
+pydata-sphinx-theme==0.16.1
# via
# -r requirements/edx/doc.txt
# sphinx-book-theme
-pygments==2.18.0
+pygments==2.19.1
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1676,7 +1677,7 @@ pyopenssl==24.3.0
# -r requirements/edx/testing.txt
# optimizely-sdk
# snowflake-connector-python
-pyparsing==3.2.0
+pyparsing==3.2.1
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1816,7 +1817,7 @@ recommender-xblock==3.0.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
-redis==5.2.0
+redis==5.2.1
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1897,7 +1898,7 @@ sailthru-client==2.2.3
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# edx-ace
-scipy==1.14.1
+scipy==1.15.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1956,7 +1957,7 @@ slumber==0.7.1
# -r requirements/edx/testing.txt
# edx-bulk-grades
# edx-enterprise
-smmap==5.0.1
+smmap==5.0.2
# via
# -r requirements/edx/doc.txt
# gitdb
@@ -2056,7 +2057,7 @@ sphinxcontrib-serializinghtml==2.0.0
# sphinx
sphinxext-rediraffe==0.2.7
# via -r requirements/edx/doc.txt
-sqlparse==0.5.2
+sqlparse==0.5.3
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -2118,14 +2119,14 @@ tqdm==4.67.1
# -r requirements/edx/testing.txt
# nltk
# openai
-types-pytz==2024.2.0.20241003
- # via django-stubs
-types-pyyaml==6.0.12.20240917
+types-pyyaml==6.0.12.20241230
# via
# django-stubs
# djangorestframework-stubs
-types-requests==2.32.0.20241016
+types-requests==2.31.0.6
# via djangorestframework-stubs
+types-urllib3==1.26.25.14
+ # via types-requests
typing-extensions==4.12.2
# via
# -r requirements/edx/doc.txt
@@ -2159,6 +2160,10 @@ unicodecsv==0.14.1
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# edx-enterprise
+unicodeit==0.7.5
+ # via
+ # -r requirements/edx/doc.txt
+ # -r requirements/edx/testing.txt
unidiff==0.7.5
# via -r requirements/edx/testing.txt
uritemplate==4.1.1
@@ -2170,18 +2175,18 @@ uritemplate==4.1.1
# google-api-python-client
urllib3==2.2.3
# via
+ # -c requirements/edx/../common_constraints.txt
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# botocore
# elasticsearch
# py2neo
# requests
- # types-requests
user-util==1.1.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
-uvicorn==0.32.1
+uvicorn==0.34.0
# via
# -r requirements/edx/testing.txt
# pact-python
@@ -2192,7 +2197,7 @@ vine==5.1.0
# amqp
# celery
# kombu
-virtualenv==20.28.0
+virtualenv==20.28.1
# via
# -r requirements/edx/testing.txt
# tox
@@ -2201,7 +2206,7 @@ voluptuous==0.15.2
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# ora2
-vulture==2.13
+vulture==2.14
# via -r requirements/edx/development.in
walrus==0.9.4
# via
@@ -2239,6 +2244,9 @@ webob==1.8.9
wheel==0.45.1
# via
# -r requirements/edx/../pip-tools.txt
+ # -r requirements/edx/doc.txt
+ # -r requirements/edx/testing.txt
+ # django-pipeline
# pip-tools
wrapt==1.17.0
# via
diff --git a/requirements/edx/doc.txt b/requirements/edx/doc.txt
index 4315eb8f300c..97c9de68dfd9 100644
--- a/requirements/edx/doc.txt
+++ b/requirements/edx/doc.txt
@@ -14,12 +14,12 @@ aiohappyeyeballs==2.4.4
# via
# -r requirements/edx/base.txt
# aiohttp
-aiohttp==3.11.9
+aiohttp==3.11.11
# via
# -r requirements/edx/base.txt
# geoip2
# openai
-aiosignal==1.3.1
+aiosignal==1.3.2
# via
# -r requirements/edx/base.txt
# aiohttp
@@ -61,7 +61,7 @@ astroid==2.13.5
# via
# -c requirements/edx/../constraints.txt
# sphinx-autoapi
-attrs==24.2.0
+attrs==24.3.0
# via
# -r requirements/edx/base.txt
# aiohttp
@@ -107,13 +107,13 @@ bleach[css]==6.2.0
# xblock-poll
boto==2.49.0
# via -r requirements/edx/base.txt
-boto3==1.35.76
+boto3==1.35.93
# via
# -r requirements/edx/base.txt
# django-ses
# fs-s3fs
# ora2
-botocore==1.35.76
+botocore==1.35.93
# via
# -r requirements/edx/base.txt
# boto3
@@ -142,7 +142,7 @@ celery==5.4.0
# edx-enterprise
# event-tracking
# openedx-learning
-certifi==2024.8.30
+certifi==2024.12.14
# via
# -r requirements/edx/base.txt
# elasticsearch
@@ -191,7 +191,7 @@ click-repl==0.3.0
# via
# -r requirements/edx/base.txt
# celery
-code-annotations==2.0.0
+code-annotations==2.1.0
# via
# -r requirements/edx/base.txt
# -r requirements/edx/doc.in
@@ -304,7 +304,7 @@ django-appconf==1.0.6
# via
# -r requirements/edx/base.txt
# django-statici18n
-django-cache-memoize==0.2.0
+django-cache-memoize==0.2.1
# via
# -r requirements/edx/base.txt
# edx-enterprise
@@ -349,7 +349,7 @@ django-ipware==7.0.1
# -r requirements/edx/base.txt
# edx-enterprise
# edx-proctoring
-django-js-asset==2.2.0
+django-js-asset==3.0.1
# via
# -r requirements/edx/base.txt
# django-mptt
@@ -392,7 +392,7 @@ django-object-actions==4.3.0
# via
# -r requirements/edx/base.txt
# edx-enterprise
-django-pipeline==3.1.0
+django-pipeline==4.0.0
# via -r requirements/edx/base.txt
django-push-notifications==3.1.0
# via
@@ -404,7 +404,7 @@ django-sekizai==4.1.0
# via
# -r requirements/edx/base.txt
# openedx-django-wiki
-django-ses==4.3.0
+django-ses==4.3.1
# via -r requirements/edx/base.txt
django-simple-history==3.4.0
# via
@@ -517,7 +517,7 @@ edx-celeryutils==1.3.0
# super-csv
edx-codejail==3.5.2
# via -r requirements/edx/base.txt
-edx-completion==4.7.6
+edx-completion==4.7.8
# via -r requirements/edx/base.txt
edx-django-release-util==1.4.0
# via
@@ -555,7 +555,7 @@ edx-drf-extensions==10.5.0
# edx-when
# edxval
# openedx-learning
-edx-enterprise==5.5.2
+edx-enterprise==5.6.1
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/base.txt
@@ -590,7 +590,7 @@ edx-opaque-keys[django]==2.11.0
# ora2
edx-organizations==6.13.0
# via -r requirements/edx/base.txt
-edx-proctoring==4.18.4
+edx-proctoring==5.0.1
# via
# -r requirements/edx/base.txt
# edx-proctoring-proctortrack
@@ -609,7 +609,7 @@ edx-search==4.1.1
# openedx-forum
edx-sga==0.25.0
# via -r requirements/edx/base.txt
-edx-submissions==3.8.3
+edx-submissions==3.8.4
# via
# -r requirements/edx/base.txt
# ora2
@@ -635,11 +635,12 @@ edx-when==2.5.0
# via
# -r requirements/edx/base.txt
# edx-proctoring
-edxval==2.7.0
+edxval==2.8.0
# via -r requirements/edx/base.txt
elasticsearch==7.9.1
# via
# -c requirements/edx/../common_constraints.txt
+ # -c requirements/edx/../constraints.txt
# -r requirements/edx/base.txt
# edx-search
# openedx-forum
@@ -656,7 +657,7 @@ event-tracking==3.0.0
# edx-completion
# edx-proctoring
# edx-search
-fastavro==1.9.7
+fastavro==1.10.0
# via
# -r requirements/edx/base.txt
# openedx-events
@@ -689,13 +690,13 @@ future==1.0.0
# pyjwkest
geoip2==4.8.1
# via -r requirements/edx/base.txt
-gitdb==4.0.11
+gitdb==4.0.12
# via gitpython
-gitpython==3.1.43
+gitpython==3.1.44
# via -r requirements/edx/doc.in
glob2==0.7
# via -r requirements/edx/base.txt
-google-api-core[grpc]==2.23.0
+google-api-core[grpc]==2.24.0
# via
# -r requirements/edx/base.txt
# firebase-admin
@@ -703,11 +704,11 @@ google-api-core[grpc]==2.23.0
# google-cloud-core
# google-cloud-firestore
# google-cloud-storage
-google-api-python-client==2.154.0
+google-api-python-client==2.157.0
# via
# -r requirements/edx/base.txt
# firebase-admin
-google-auth==2.36.0
+google-auth==2.37.0
# via
# -r requirements/edx/base.txt
# google-api-core
@@ -747,12 +748,12 @@ googleapis-common-protos==1.66.0
# -r requirements/edx/base.txt
# google-api-core
# grpcio-status
-grpcio==1.68.1
+grpcio==1.69.0
# via
# -r requirements/edx/base.txt
# google-api-core
# grpcio-status
-grpcio-status==1.68.1
+grpcio-status==1.69.0
# via
# -r requirements/edx/base.txt
# google-api-core
@@ -797,7 +798,7 @@ isodate==0.7.2
# via
# -r requirements/edx/base.txt
# python3-saml
-jinja2==3.1.4
+jinja2==3.1.5
# via
# -r requirements/edx/base.txt
# code-annotations
@@ -859,8 +860,10 @@ loremipsum==1.0.5
# via
# -r requirements/edx/base.txt
# ora2
-lti-consumer-xblock==9.12.0
- # via -r requirements/edx/base.txt
+lti-consumer-xblock==9.12.1
+ # via
+ # -c requirements/edx/../constraints.txt
+ # -r requirements/edx/base.txt
lxml[html-clean]==5.3.0
# via
# -r requirements/edx/base.txt
@@ -880,7 +883,7 @@ lxml-html-clean==0.4.1
# lxml
mailsnake==1.6.4
# via -r requirements/edx/base.txt
-mako==1.3.7
+mako==1.3.8
# via
# -r requirements/edx/base.txt
# acid-xblock
@@ -910,7 +913,7 @@ meilisearch==0.33.0
# via
# -r requirements/edx/base.txt
# edx-search
-mistune==3.0.2
+mistune==3.1.0
# via sphinx-mdinclude
mongoengine==0.29.1
# via -r requirements/edx/base.txt
@@ -940,11 +943,11 @@ mysqlclient==2.2.6
# via
# -r requirements/edx/base.txt
# openedx-forum
-newrelic==10.3.1
+newrelic==10.4.0
# via
# -r requirements/edx/base.txt
# edx-django-utils
-nh3==0.2.19
+nh3==0.2.20
# via -r requirements/edx/base.txt
nltk==3.9.1
# via
@@ -1016,7 +1019,7 @@ optimizely-sdk==4.1.1
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/base.txt
-ora2==6.14.1
+ora2==6.14.3
# via -r requirements/edx/base.txt
packaging==24.2
# via
@@ -1058,7 +1061,7 @@ picobox==4.0.0
# via sphinxcontrib-openapi
piexif==1.1.3
# via -r requirements/edx/base.txt
-pillow==11.0.0
+pillow==11.1.0
# via
# -r requirements/edx/base.txt
# edx-enterprise
@@ -1087,7 +1090,7 @@ proto-plus==1.25.0
# -r requirements/edx/base.txt
# google-api-core
# google-cloud-firestore
-protobuf==5.29.1
+protobuf==5.29.2
# via
# -r requirements/edx/base.txt
# google-api-core
@@ -1095,7 +1098,7 @@ protobuf==5.29.1
# googleapis-common-protos
# grpcio-status
# proto-plus
-psutil==6.1.0
+psutil==6.1.1
# via
# -r requirements/edx/base.txt
# edx-django-utils
@@ -1125,17 +1128,17 @@ pycryptodomex==3.21.0
# edx-proctoring
# lti-consumer-xblock
# pyjwkest
-pydantic==2.10.3
+pydantic==2.10.4
# via
# -r requirements/edx/base.txt
# camel-converter
-pydantic-core==2.27.1
+pydantic-core==2.27.2
# via
# -r requirements/edx/base.txt
# pydantic
-pydata-sphinx-theme==0.16.0
+pydata-sphinx-theme==0.16.1
# via sphinx-book-theme
-pygments==2.18.0
+pygments==2.19.1
# via
# -r requirements/edx/base.txt
# accessible-pygments
@@ -1189,7 +1192,7 @@ pyopenssl==24.3.0
# -r requirements/edx/base.txt
# optimizely-sdk
# snowflake-connector-python
-pyparsing==3.2.0
+pyparsing==3.2.1
# via
# -r requirements/edx/base.txt
# chem
@@ -1269,7 +1272,7 @@ random2==1.0.2
# via -r requirements/edx/base.txt
recommender-xblock==3.0.0
# via -r requirements/edx/base.txt
-redis==5.2.0
+redis==5.2.1
# via
# -r requirements/edx/base.txt
# walrus
@@ -1338,7 +1341,7 @@ sailthru-client==2.2.3
# via
# -r requirements/edx/base.txt
# edx-ace
-scipy==1.14.1
+scipy==1.15.0
# via
# -r requirements/edx/base.txt
# chem
@@ -1384,7 +1387,7 @@ slumber==0.7.1
# -r requirements/edx/base.txt
# edx-bulk-grades
# edx-enterprise
-smmap==5.0.1
+smmap==5.0.2
# via gitdb
snowballstemmer==2.2.0
# via sphinx
@@ -1454,7 +1457,7 @@ sphinxcontrib-serializinghtml==2.0.0
# via sphinx
sphinxext-rediraffe==0.2.7
# via -r requirements/edx/doc.in
-sqlparse==0.5.2
+sqlparse==0.5.3
# via
# -r requirements/edx/base.txt
# django
@@ -1518,6 +1521,8 @@ unicodecsv==0.14.1
# via
# -r requirements/edx/base.txt
# edx-enterprise
+unicodeit==0.7.5
+ # via -r requirements/edx/base.txt
uritemplate==4.1.1
# via
# -r requirements/edx/base.txt
@@ -1526,6 +1531,7 @@ uritemplate==4.1.1
# google-api-python-client
urllib3==2.2.3
# via
+ # -c requirements/edx/../common_constraints.txt
# -r requirements/edx/base.txt
# botocore
# elasticsearch
@@ -1569,6 +1575,10 @@ webob==1.8.9
# via
# -r requirements/edx/base.txt
# xblock
+wheel==0.45.1
+ # via
+ # -r requirements/edx/base.txt
+ # django-pipeline
wrapt==1.17.0
# via
# -r requirements/edx/base.txt
diff --git a/requirements/edx/kernel.in b/requirements/edx/kernel.in
index d2ec04314801..d1a132778133 100644
--- a/requirements/edx/kernel.in
+++ b/requirements/edx/kernel.in
@@ -163,3 +163,4 @@ web-fragments # Provides the ability to render fragments o
wrapt # Better functools.wrapped. TODO: functools has since improved, maybe we can switch?
XBlock[django] # Courseware component architecture
xss-utils # https://github.com/openedx/edx-platform/pull/20633 Fix XSS via Translations
+unicodeit # Converts mathjax equation to plain text by using unicode symbols
diff --git a/requirements/edx/semgrep.txt b/requirements/edx/semgrep.txt
index c244159342bc..d7386250db56 100644
--- a/requirements/edx/semgrep.txt
+++ b/requirements/edx/semgrep.txt
@@ -4,7 +4,7 @@
#
# make upgrade
#
-attrs==24.2.0
+attrs==24.3.0
# via
# glom
# jsonschema
@@ -17,7 +17,7 @@ boltons==21.0.0
# semgrep
bracex==2.5.post1
# via wcmatch
-certifi==2024.8.30
+certifi==2024.12.14
# via requests
charset-normalizer==2.0.12
# via
@@ -96,7 +96,7 @@ protobuf==4.25.5
# via
# googleapis-common-protos
# opentelemetry-proto
-pygments==2.18.0
+pygments==2.19.1
# via rich
referencing==0.35.1
# via
@@ -112,11 +112,11 @@ rpds-py==0.22.3
# via
# jsonschema
# referencing
-ruamel-yaml==0.17.40
+ruamel-yaml==0.18.10
# via semgrep
ruamel-yaml-clib==0.2.12
# via ruamel-yaml
-semgrep==1.97.0
+semgrep==1.101.0
# via -r requirements/edx/semgrep.in
tomli==2.0.2
# via semgrep
@@ -126,6 +126,7 @@ typing-extensions==4.12.2
# semgrep
urllib3==2.2.3
# via
+ # -c requirements/edx/../common_constraints.txt
# requests
# semgrep
wcmatch==8.5.2
diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt
index a5faae04ccd8..5e99738b3cc8 100644
--- a/requirements/edx/testing.txt
+++ b/requirements/edx/testing.txt
@@ -12,12 +12,12 @@ aiohappyeyeballs==2.4.4
# via
# -r requirements/edx/base.txt
# aiohttp
-aiohttp==3.11.9
+aiohttp==3.11.11
# via
# -r requirements/edx/base.txt
# geoip2
# openai
-aiosignal==1.3.1
+aiosignal==1.3.2
# via
# -r requirements/edx/base.txt
# aiohttp
@@ -39,7 +39,7 @@ annotated-types==0.7.0
# via
# -r requirements/edx/base.txt
# pydantic
-anyio==4.7.0
+anyio==4.8.0
# via starlette
appdirs==1.4.4
# via
@@ -60,7 +60,7 @@ astroid==2.13.5
# -c requirements/edx/../constraints.txt
# pylint
# pylint-celery
-attrs==24.2.0
+attrs==24.3.0
# via
# -r requirements/edx/base.txt
# aiohttp
@@ -104,13 +104,13 @@ bleach[css]==6.2.0
# xblock-poll
boto==2.49.0
# via -r requirements/edx/base.txt
-boto3==1.35.76
+boto3==1.35.93
# via
# -r requirements/edx/base.txt
# django-ses
# fs-s3fs
# ora2
-botocore==1.35.76
+botocore==1.35.93
# via
# -r requirements/edx/base.txt
# boto3
@@ -140,7 +140,7 @@ celery==5.4.0
# edx-enterprise
# event-tracking
# openedx-learning
-certifi==2024.8.30
+certifi==2024.12.14
# via
# -r requirements/edx/base.txt
# elasticsearch
@@ -200,7 +200,7 @@ click-repl==0.3.0
# via
# -r requirements/edx/base.txt
# celery
-code-annotations==2.0.0
+code-annotations==2.1.0
# via
# -r requirements/edx/base.txt
# -r requirements/edx/testing.in
@@ -211,7 +211,7 @@ codejail-includes==1.0.0
# via -r requirements/edx/base.txt
colorama==0.4.6
# via tox
-coverage[toml]==7.6.8
+coverage[toml]==7.6.10
# via
# -r requirements/edx/coverage.txt
# pytest-cov
@@ -247,7 +247,7 @@ defusedxml==0.7.1
# ora2
# python3-openid
# social-auth-core
-diff-cover==9.2.0
+diff-cover==9.2.1
# via -r requirements/edx/coverage.txt
dill==0.3.9
# via pylint
@@ -330,7 +330,7 @@ django-appconf==1.0.6
# via
# -r requirements/edx/base.txt
# django-statici18n
-django-cache-memoize==0.2.0
+django-cache-memoize==0.2.1
# via
# -r requirements/edx/base.txt
# edx-enterprise
@@ -375,7 +375,7 @@ django-ipware==7.0.1
# -r requirements/edx/base.txt
# edx-enterprise
# edx-proctoring
-django-js-asset==2.2.0
+django-js-asset==3.0.1
# via
# -r requirements/edx/base.txt
# django-mptt
@@ -418,7 +418,7 @@ django-object-actions==4.3.0
# via
# -r requirements/edx/base.txt
# edx-enterprise
-django-pipeline==3.1.0
+django-pipeline==4.0.0
# via -r requirements/edx/base.txt
django-push-notifications==3.1.0
# via
@@ -430,7 +430,7 @@ django-sekizai==4.1.0
# via
# -r requirements/edx/base.txt
# openedx-django-wiki
-django-ses==4.3.0
+django-ses==4.3.1
# via -r requirements/edx/base.txt
django-simple-history==3.4.0
# via
@@ -538,7 +538,7 @@ edx-celeryutils==1.3.0
# super-csv
edx-codejail==3.5.2
# via -r requirements/edx/base.txt
-edx-completion==4.7.6
+edx-completion==4.7.8
# via -r requirements/edx/base.txt
edx-django-release-util==1.4.0
# via
@@ -576,7 +576,7 @@ edx-drf-extensions==10.5.0
# edx-when
# edxval
# openedx-learning
-edx-enterprise==5.5.2
+edx-enterprise==5.6.1
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/base.txt
@@ -613,7 +613,7 @@ edx-opaque-keys[django]==2.11.0
# ora2
edx-organizations==6.13.0
# via -r requirements/edx/base.txt
-edx-proctoring==4.18.4
+edx-proctoring==5.0.1
# via
# -r requirements/edx/base.txt
# edx-proctoring-proctortrack
@@ -632,7 +632,7 @@ edx-search==4.1.1
# openedx-forum
edx-sga==0.25.0
# via -r requirements/edx/base.txt
-edx-submissions==3.8.3
+edx-submissions==3.8.4
# via
# -r requirements/edx/base.txt
# ora2
@@ -658,11 +658,12 @@ edx-when==2.5.0
# via
# -r requirements/edx/base.txt
# edx-proctoring
-edxval==2.7.0
+edxval==2.8.0
# via -r requirements/edx/base.txt
elasticsearch==7.9.1
# via
# -c requirements/edx/../common_constraints.txt
+ # -c requirements/edx/../constraints.txt
# -r requirements/edx/base.txt
# edx-search
# openedx-forum
@@ -683,11 +684,11 @@ execnet==2.1.1
# via pytest-xdist
factory-boy==3.3.1
# via -r requirements/edx/testing.in
-faker==33.1.0
+faker==33.3.0
# via factory-boy
fastapi==0.115.6
# via pact-python
-fastavro==1.9.7
+fastavro==1.10.0
# via
# -r requirements/edx/base.txt
# openedx-events
@@ -726,7 +727,7 @@ geoip2==4.8.1
# via -r requirements/edx/base.txt
glob2==0.7
# via -r requirements/edx/base.txt
-google-api-core[grpc]==2.23.0
+google-api-core[grpc]==2.24.0
# via
# -r requirements/edx/base.txt
# firebase-admin
@@ -734,11 +735,11 @@ google-api-core[grpc]==2.23.0
# google-cloud-core
# google-cloud-firestore
# google-cloud-storage
-google-api-python-client==2.154.0
+google-api-python-client==2.157.0
# via
# -r requirements/edx/base.txt
# firebase-admin
-google-auth==2.36.0
+google-auth==2.37.0
# via
# -r requirements/edx/base.txt
# google-api-core
@@ -780,12 +781,12 @@ googleapis-common-protos==1.66.0
# grpcio-status
grimp==3.5
# via import-linter
-grpcio==1.68.1
+grpcio==1.69.0
# via
# -r requirements/edx/base.txt
# google-api-core
# grpcio-status
-grpcio-status==1.68.1
+grpcio-status==1.69.0
# via
# -r requirements/edx/base.txt
# google-api-core
@@ -841,7 +842,7 @@ isort==5.13.2
# via
# -r requirements/edx/testing.in
# pylint
-jinja2==3.1.4
+jinja2==3.1.5
# via
# -r requirements/edx/base.txt
# -r requirements/edx/coverage.txt
@@ -902,8 +903,10 @@ loremipsum==1.0.5
# via
# -r requirements/edx/base.txt
# ora2
-lti-consumer-xblock==9.12.0
- # via -r requirements/edx/base.txt
+lti-consumer-xblock==9.12.1
+ # via
+ # -c requirements/edx/../constraints.txt
+ # -r requirements/edx/base.txt
lxml[html-clean]==5.3.0
# via
# -r requirements/edx/base.txt
@@ -924,7 +927,7 @@ lxml-html-clean==0.4.1
# lxml
mailsnake==1.6.4
# via -r requirements/edx/base.txt
-mako==1.3.7
+mako==1.3.8
# via
# -r requirements/edx/base.txt
# acid-xblock
@@ -987,11 +990,11 @@ mysqlclient==2.2.6
# via
# -r requirements/edx/base.txt
# openedx-forum
-newrelic==10.3.1
+newrelic==10.4.0
# via
# -r requirements/edx/base.txt
# edx-django-utils
-nh3==0.2.19
+nh3==0.2.20
# via -r requirements/edx/base.txt
nltk==3.9.1
# via
@@ -1063,7 +1066,7 @@ optimizely-sdk==4.1.1
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/base.txt
-ora2==6.14.1
+ora2==6.14.3
# via -r requirements/edx/base.txt
packaging==24.2
# via
@@ -1075,7 +1078,7 @@ packaging==24.2
# pytest
# snowflake-connector-python
# tox
-pact-python==2.2.2
+pact-python==2.3.0
# via -r requirements/edx/testing.in
pansi==2024.11.0
# via
@@ -1107,7 +1110,7 @@ pgpy==0.6.0
# edx-enterprise
piexif==1.1.3
# via -r requirements/edx/base.txt
-pillow==11.0.0
+pillow==11.1.0
# via
# -r requirements/edx/base.txt
# edx-enterprise
@@ -1146,7 +1149,7 @@ proto-plus==1.25.0
# -r requirements/edx/base.txt
# google-api-core
# google-cloud-firestore
-protobuf==5.29.1
+protobuf==5.29.2
# via
# -r requirements/edx/base.txt
# google-api-core
@@ -1154,7 +1157,7 @@ protobuf==5.29.1
# googleapis-common-protos
# grpcio-status
# proto-plus
-psutil==6.1.0
+psutil==6.1.1
# via
# -r requirements/edx/base.txt
# edx-django-utils
@@ -1192,16 +1195,16 @@ pycryptodomex==3.21.0
# edx-proctoring
# lti-consumer-xblock
# pyjwkest
-pydantic==2.10.3
+pydantic==2.10.4
# via
# -r requirements/edx/base.txt
# camel-converter
# fastapi
-pydantic-core==2.27.1
+pydantic-core==2.27.2
# via
# -r requirements/edx/base.txt
# pydantic
-pygments==2.18.0
+pygments==2.19.1
# via
# -r requirements/edx/base.txt
# -r requirements/edx/coverage.txt
@@ -1271,7 +1274,7 @@ pyopenssl==24.3.0
# -r requirements/edx/base.txt
# optimizely-sdk
# snowflake-connector-python
-pyparsing==3.2.0
+pyparsing==3.2.1
# via
# -r requirements/edx/base.txt
# chem
@@ -1382,7 +1385,7 @@ random2==1.0.2
# via -r requirements/edx/base.txt
recommender-xblock==3.0.0
# via -r requirements/edx/base.txt
-redis==5.2.0
+redis==5.2.1
# via
# -r requirements/edx/base.txt
# walrus
@@ -1451,7 +1454,7 @@ sailthru-client==2.2.3
# via
# -r requirements/edx/base.txt
# edx-ace
-scipy==1.14.1
+scipy==1.15.0
# via
# -r requirements/edx/base.txt
# chem
@@ -1528,7 +1531,7 @@ soupsieve==2.6
# via
# -r requirements/edx/base.txt
# beautifulsoup4
-sqlparse==0.5.2
+sqlparse==0.5.3
# via
# -r requirements/edx/base.txt
# django
@@ -1602,6 +1605,8 @@ unicodecsv==0.14.1
# via
# -r requirements/edx/base.txt
# edx-enterprise
+unicodeit==0.7.5
+ # via -r requirements/edx/base.txt
unidiff==0.7.5
# via -r requirements/edx/testing.in
uritemplate==4.1.1
@@ -1612,6 +1617,7 @@ uritemplate==4.1.1
# google-api-python-client
urllib3==2.2.3
# via
+ # -c requirements/edx/../common_constraints.txt
# -r requirements/edx/base.txt
# botocore
# elasticsearch
@@ -1619,7 +1625,7 @@ urllib3==2.2.3
# requests
user-util==1.1.0
# via -r requirements/edx/base.txt
-uvicorn==0.32.1
+uvicorn==0.34.0
# via pact-python
vine==5.1.0
# via
@@ -1627,7 +1633,7 @@ vine==5.1.0
# amqp
# celery
# kombu
-virtualenv==20.28.0
+virtualenv==20.28.1
# via tox
voluptuous==0.15.2
# via
@@ -1659,6 +1665,10 @@ webob==1.8.9
# via
# -r requirements/edx/base.txt
# xblock
+wheel==0.45.1
+ # via
+ # -r requirements/edx/base.txt
+ # django-pipeline
wrapt==1.17.0
# via
# -r requirements/edx/base.txt
diff --git a/requirements/pip.txt b/requirements/pip.txt
index 0bdb9d7ac387..ea16b6b4de8b 100644
--- a/requirements/pip.txt
+++ b/requirements/pip.txt
@@ -12,5 +12,5 @@ pip==24.2
# via
# -c requirements/common_constraints.txt
# -r requirements/pip.in
-setuptools==75.6.0
+setuptools==75.7.0
# via -r requirements/pip.in
diff --git a/scripts/user_retirement/requirements/base.txt b/scripts/user_retirement/requirements/base.txt
index 622ffd2bc135..56fe29b90dc3 100644
--- a/scripts/user_retirement/requirements/base.txt
+++ b/scripts/user_retirement/requirements/base.txt
@@ -6,19 +6,19 @@
#
asgiref==3.8.1
# via django
-attrs==24.2.0
+attrs==24.3.0
# via zeep
backoff==2.2.1
# via -r scripts/user_retirement/requirements/base.in
-boto3==1.35.76
+boto3==1.35.93
# via -r scripts/user_retirement/requirements/base.in
-botocore==1.35.76
+botocore==1.35.93
# via
# boto3
# s3transfer
cachetools==5.5.0
# via google-auth
-certifi==2024.8.30
+certifi==2024.12.14
# via requests
cffi==1.17.1
# via
@@ -50,11 +50,11 @@ edx-django-utils==7.1.0
# via edx-rest-api-client
edx-rest-api-client==6.0.0
# via -r scripts/user_retirement/requirements/base.in
-google-api-core==2.23.0
+google-api-core==2.24.0
# via google-api-python-client
-google-api-python-client==2.154.0
+google-api-python-client==2.157.0
# via -r scripts/user_retirement/requirements/base.in
-google-auth==2.36.0
+google-auth==2.37.0
# via
# google-api-core
# google-api-python-client
@@ -81,7 +81,7 @@ lxml==5.3.0
# via zeep
more-itertools==10.5.0
# via simple-salesforce
-newrelic==10.3.1
+newrelic==10.4.0
# via edx-django-utils
pbr==6.1.0
# via stevedore
@@ -89,12 +89,12 @@ platformdirs==4.3.6
# via zeep
proto-plus==1.25.0
# via google-api-core
-protobuf==5.29.1
+protobuf==5.29.2
# via
# google-api-core
# googleapis-common-protos
# proto-plus
-psutil==6.1.0
+psutil==6.1.1
# via edx-django-utils
pyasn1==0.6.1
# via
@@ -110,7 +110,7 @@ pyjwt[crypto]==2.10.1
# simple-salesforce
pynacl==1.5.0
# via edx-django-utils
-pyparsing==3.2.0
+pyparsing==3.2.1
# via httplib2
python-dateutil==2.9.0.post0
# via botocore
@@ -146,7 +146,7 @@ six==1.17.0
# via
# jenkinsapi
# python-dateutil
-sqlparse==0.5.2
+sqlparse==0.5.3
# via django
stevedore==5.4.0
# via edx-django-utils
@@ -158,6 +158,7 @@ uritemplate==4.1.1
# via google-api-python-client
urllib3==1.26.20
# via
+ # -c scripts/user_retirement/requirements/../../../requirements/common_constraints.txt
# -r scripts/user_retirement/requirements/base.in
# botocore
# requests
diff --git a/scripts/user_retirement/requirements/testing.txt b/scripts/user_retirement/requirements/testing.txt
index efaa4369170f..e63881b77cac 100644
--- a/scripts/user_retirement/requirements/testing.txt
+++ b/scripts/user_retirement/requirements/testing.txt
@@ -8,17 +8,17 @@ asgiref==3.8.1
# via
# -r scripts/user_retirement/requirements/base.txt
# django
-attrs==24.2.0
+attrs==24.3.0
# via
# -r scripts/user_retirement/requirements/base.txt
# zeep
backoff==2.2.1
# via -r scripts/user_retirement/requirements/base.txt
-boto3==1.35.76
+boto3==1.35.93
# via
# -r scripts/user_retirement/requirements/base.txt
# moto
-botocore==1.35.76
+botocore==1.35.93
# via
# -r scripts/user_retirement/requirements/base.txt
# boto3
@@ -28,7 +28,7 @@ cachetools==5.5.0
# via
# -r scripts/user_retirement/requirements/base.txt
# google-auth
-certifi==2024.8.30
+certifi==2024.12.14
# via
# -r scripts/user_retirement/requirements/base.txt
# requests
@@ -72,13 +72,13 @@ edx-django-utils==7.1.0
# edx-rest-api-client
edx-rest-api-client==6.0.0
# via -r scripts/user_retirement/requirements/base.txt
-google-api-core==2.23.0
+google-api-core==2.24.0
# via
# -r scripts/user_retirement/requirements/base.txt
# google-api-python-client
-google-api-python-client==2.154.0
+google-api-python-client==2.157.0
# via -r scripts/user_retirement/requirements/base.txt
-google-auth==2.36.0
+google-auth==2.37.0
# via
# -r scripts/user_retirement/requirements/base.txt
# google-api-core
@@ -109,7 +109,7 @@ isodate==0.7.2
# zeep
jenkinsapi==0.3.13
# via -r scripts/user_retirement/requirements/base.txt
-jinja2==3.1.4
+jinja2==3.1.5
# via moto
jmespath==1.0.1
# via
@@ -132,7 +132,7 @@ more-itertools==10.5.0
# simple-salesforce
moto==4.2.14
# via -r scripts/user_retirement/requirements/testing.in
-newrelic==10.3.1
+newrelic==10.4.0
# via
# -r scripts/user_retirement/requirements/base.txt
# edx-django-utils
@@ -152,13 +152,13 @@ proto-plus==1.25.0
# via
# -r scripts/user_retirement/requirements/base.txt
# google-api-core
-protobuf==5.29.1
+protobuf==5.29.2
# via
# -r scripts/user_retirement/requirements/base.txt
# google-api-core
# googleapis-common-protos
# proto-plus
-psutil==6.1.0
+psutil==6.1.1
# via
# -r scripts/user_retirement/requirements/base.txt
# edx-django-utils
@@ -184,7 +184,7 @@ pynacl==1.5.0
# via
# -r scripts/user_retirement/requirements/base.txt
# edx-django-utils
-pyparsing==3.2.0
+pyparsing==3.2.1
# via
# -r scripts/user_retirement/requirements/base.txt
# httplib2
@@ -248,7 +248,7 @@ six==1.17.0
# -r scripts/user_retirement/requirements/base.txt
# jenkinsapi
# python-dateutil
-sqlparse==0.5.2
+sqlparse==0.5.3
# via
# -r scripts/user_retirement/requirements/base.txt
# django
diff --git a/scripts/vulture/find-dead-code.sh b/scripts/vulture/find-dead-code.sh
index e24882595ebb..c78793ec3a6d 100755
--- a/scripts/vulture/find-dead-code.sh
+++ b/scripts/vulture/find-dead-code.sh
@@ -35,9 +35,9 @@ mkdir -p "$OUTPUT_DIR"
OUTPUT_FILE="${OUTPUT_DIR}/vulture-report.txt"
echo '' > "$OUTPUT_FILE"
# exclude test code from analysis, as it isn't explicitly called by other
-# code. Additionally, application code that is only called by tests
+# code. Additionally, application code that is only called by tests
# should be considered dead
-EXCLUSIONS='/test,/acceptance,cms/envs,lms/envs,/terrain,migrations/,signals.py'
+EXCLUSIONS='/test,/acceptance,cms/envs,lms/envs,migrations/,signals.py'
MIN_CONFIDENCE=90
# paths to the code on which to run the analysis
CODE_PATHS=('cms' 'common' 'lms' 'openedx')
diff --git a/scripts/xblock/requirements.txt b/scripts/xblock/requirements.txt
index 920cf0cf6ac1..9af137853dc5 100644
--- a/scripts/xblock/requirements.txt
+++ b/scripts/xblock/requirements.txt
@@ -4,7 +4,7 @@
#
# make upgrade
#
-certifi==2024.8.30
+certifi==2024.12.14
# via requests
charset-normalizer==2.0.12
# via
@@ -15,4 +15,6 @@ idna==3.10
requests==2.32.3
# via -r scripts/xblock/requirements.in
urllib3==2.2.3
- # via requests
+ # via
+ # -c scripts/xblock/../../requirements/common_constraints.txt
+ # requests