diff --git a/openedx/core/djangoapps/enrollments/errors.py b/openedx/core/djangoapps/enrollments/errors.py index e68228d367..167b176129 100644 --- a/openedx/core/djangoapps/enrollments/errors.py +++ b/openedx/core/djangoapps/enrollments/errors.py @@ -52,3 +52,7 @@ class EnrollmentApiLoadError(CourseEnrollmentError): class InvalidEnrollmentAttribute(CourseEnrollmentError): """Enrollment Attributes could not be validated""" pass # lint-amnesty, pylint: disable=unnecessary-pass + + +class CourseEnrollmentNotUpdatableError(CourseEnrollmentError): + """The requested enrollment could not be updated.""" diff --git a/openedx/features/enterprise_support/enrollments/tests/test_utils.py b/openedx/features/enterprise_support/enrollments/tests/test_utils.py index 2bfab38704..d0588b2f01 100644 --- a/openedx/features/enterprise_support/enrollments/tests/test_utils.py +++ b/openedx/features/enterprise_support/enrollments/tests/test_utils.py @@ -1,11 +1,12 @@ """ Test the enterprise support utils. """ - +import ddt from unittest import mock from unittest.case import TestCase from django.core.exceptions import ObjectDoesNotExist +from django.test import override_settings from opaque_keys.edx.keys import CourseKey from openedx.core.djangoapps.course_groups.cohorts import CourseUserGroup @@ -15,8 +16,10 @@ from openedx.features.enterprise_support.enrollments.exceptions import ( CourseIdMissingException, UserDoesNotExistException ) -from openedx.features.enterprise_support.enrollments.utils import lms_enroll_user_in_course - +from openedx.features.enterprise_support.enrollments.utils import ( + lms_enroll_user_in_course, + lms_update_or_create_enrollment, +) COURSE_STRING = 'course-v1:OpenEdX+OutlineCourse+Run3' ENTERPRISE_UUID = 'enterprise_uuid' COURSE_ID = CourseKey.from_string(COURSE_STRING) @@ -26,6 +29,7 @@ COURSE_MODE = 'verified' @skip_unless_lms +@ddt.ddt class EnrollmentUtilsTest(TestCase): """ Test enterprise support utils. @@ -37,104 +41,222 @@ class EnrollmentUtilsTest(TestCase): self.a_user.id = USER_ID self.a_user.username = USERNAME - def test_validation_of_inputs_course_id(self): - with self.assertRaises(CourseIdMissingException): - lms_enroll_user_in_course(USERNAME, None, COURSE_MODE, ENTERPRISE_UUID) + def run_test_with_setting( + self, + setting, + mock_update_create_enroll, + mock_enroll_user, + test_function_true, + test_function_false, + ): + """ + Run a test with a setting. + """ + with override_settings( + ENABLE_ENTERPRISE_BACKEND_EMET_AUTO_UPGRADE_ENROLLMENT_MODE=setting + ): + if setting: + return test_function_true(mock_update_create_enroll) + return test_function_false(mock_enroll_user) - def test_validation_of_inputs_user_not_provided(self): - with self.assertRaises(UserDoesNotExistException): - lms_enroll_user_in_course( - None, - COURSE_ID, - COURSE_MODE, - ENTERPRISE_UUID, + @mock.patch('openedx.features.enterprise_support.enrollments.utils.lms_enroll_user_in_course') + @mock.patch('openedx.features.enterprise_support.enrollments.utils.lms_update_or_create_enrollment') + @ddt.data(True, False) + def test_validation_of_inputs_course_id(self, setting_value, mock_update_create_enroll, mock_enroll_user): + test_function_true = lambda mock_fn: lms_update_or_create_enrollment( + USERNAME, None, COURSE_MODE, is_active=True, enterprise_uuid=ENTERPRISE_UUID + ) + test_function_false = lambda mock_fn: lms_enroll_user_in_course(USERNAME, None, COURSE_MODE, ENTERPRISE_UUID) + with self.assertRaises(CourseIdMissingException): + self.run_test_with_setting( + setting_value, + mock_update_create_enroll, + mock_enroll_user, + test_function_true, + test_function_false ) + @mock.patch('openedx.features.enterprise_support.enrollments.utils.lms_enroll_user_in_course') + @mock.patch('openedx.features.enterprise_support.enrollments.utils.lms_update_or_create_enrollment') + @ddt.data(True, False) + def test_validation_of_inputs_user_not_provided(self, setting_value, mock_update_create_enroll, mock_enroll_user): + test_function_true = lambda mock_fn: lms_update_or_create_enrollment( + None, COURSE_ID, COURSE_MODE, is_active=True, enterprise_uuid=ENTERPRISE_UUID + ) + test_function_false = lambda mock_fn: lms_enroll_user_in_course(None, COURSE_ID, COURSE_MODE, ENTERPRISE_UUID) + with self.assertRaises(UserDoesNotExistException): + self.run_test_with_setting( + setting_value, + mock_update_create_enroll, + mock_enroll_user, + test_function_true, + test_function_false + ) + + @mock.patch('openedx.features.enterprise_support.enrollments.utils.lms_enroll_user_in_course') + @mock.patch('openedx.features.enterprise_support.enrollments.utils.lms_update_or_create_enrollment') @mock.patch('openedx.features.enterprise_support.enrollments.utils.User.objects.get') @mock.patch('openedx.features.enterprise_support.enrollments.utils.transaction') - def test_validation_of_inputs_user_not_found(self, mock_tx, mock_user_model): + @ddt.data(True, False) + def test_validation_of_inputs_user_not_found( + self, + setting_value, + mock_tx, + mock_user_model, + mock_update_create_enroll, + mock_enroll_user + ): mock_tx.return_value.atomic.side_effect = None mock_user_model.side_effect = ObjectDoesNotExist() + test_function_true = lambda mock_fn: lms_update_or_create_enrollment( + USERNAME, COURSE_ID, COURSE_MODE, is_active=True, enterprise_uuid=ENTERPRISE_UUID + ) + test_function_false = lambda mock_fn: lms_enroll_user_in_course( + USERNAME, + COURSE_ID, + COURSE_MODE, + ENTERPRISE_UUID + ) with self.assertRaises(UserDoesNotExistException): - lms_enroll_user_in_course( - USERNAME, - COURSE_ID, - COURSE_MODE, - ENTERPRISE_UUID, + self.run_test_with_setting( + setting_value, + mock_update_create_enroll, + mock_enroll_user, + test_function_true, + test_function_false ) + @mock.patch('openedx.features.enterprise_support.enrollments.utils.lms_enroll_user_in_course') + @mock.patch('openedx.features.enterprise_support.enrollments.utils.lms_update_or_create_enrollment') @mock.patch('openedx.features.enterprise_support.enrollments.utils.enrollment_api.add_enrollment') @mock.patch('openedx.features.enterprise_support.enrollments.utils.enrollment_api.get_enrollment') @mock.patch('openedx.features.enterprise_support.enrollments.utils.User.objects.get') @mock.patch('openedx.features.enterprise_support.enrollments.utils.transaction') + @ddt.data(True, False) def test_course_enrollment_error_raises( self, + setting_value, mock_tx, mock_user_model, mock_get_enrollment_api, mock_add_enrollment_api, + mock_update_create_enroll, + mock_enroll_user ): - enrollment_response = {'mode': COURSE_MODE, 'is_active': True} + test_function_true = lambda mock_fn: lms_update_or_create_enrollment( + USERNAME, COURSE_ID, COURSE_MODE, is_active=True, enterprise_uuid=ENTERPRISE_UUID + ) + test_function_false = lambda mock_fn: lms_enroll_user_in_course( + USERNAME, + COURSE_ID, + COURSE_MODE, + ENTERPRISE_UUID + ) mock_add_enrollment_api.side_effect = CourseEnrollmentError("test") mock_tx.return_value.atomic.side_effect = None - mock_get_enrollment_api.return_value = enrollment_response - mock_user_model.return_value = self.a_user + enrollment_response = {'mode': COURSE_MODE, 'is_active': True} if not setting_value else None + mock_get_enrollment_api.return_value = enrollment_response with self.assertRaises(CourseEnrollmentError): - lms_enroll_user_in_course(USERNAME, COURSE_ID, COURSE_MODE, ENTERPRISE_UUID) - mock_get_enrollment_api.assert_called_once() + self.run_test_with_setting( + setting_value, + mock_update_create_enroll, + mock_enroll_user, + test_function_true, + test_function_false + ) + mock_get_enrollment_api.assert_called_once_with(USERNAME, str(COURSE_ID)) + @mock.patch('openedx.features.enterprise_support.enrollments.utils.lms_enroll_user_in_course') + @mock.patch('openedx.features.enterprise_support.enrollments.utils.lms_update_or_create_enrollment') @mock.patch('openedx.features.enterprise_support.enrollments.utils.enrollment_api.add_enrollment') @mock.patch('openedx.features.enterprise_support.enrollments.utils.enrollment_api.get_enrollment') @mock.patch('openedx.features.enterprise_support.enrollments.utils.User.objects.get') @mock.patch('openedx.features.enterprise_support.enrollments.utils.transaction') + @ddt.data(True, False) def test_course_group_error_raises( self, + setting_value, mock_tx, mock_user_model, mock_get_enrollment_api, mock_add_enrollment_api, + mock_update_create_enroll, + mock_enroll_user ): - enrollment_response = {'mode': COURSE_MODE, 'is_active': True} - + test_function_true = lambda mock_fn: lms_update_or_create_enrollment( + USERNAME, COURSE_ID, COURSE_MODE, is_active=True, enterprise_uuid=ENTERPRISE_UUID + ) + test_function_false = lambda mock_fn: lms_enroll_user_in_course( + USERNAME, + COURSE_ID, + COURSE_MODE, + ENTERPRISE_UUID + ) mock_add_enrollment_api.side_effect = CourseUserGroup.DoesNotExist() mock_tx.return_value.atomic.side_effect = None - mock_get_enrollment_api.return_value = enrollment_response - mock_user_model.return_value = self.a_user - + enrollment_response = {'mode': COURSE_MODE, 'is_active': True} if not setting_value else None + mock_get_enrollment_api.return_value = enrollment_response with self.assertRaises(CourseUserGroup.DoesNotExist): - lms_enroll_user_in_course(USERNAME, COURSE_ID, COURSE_MODE, ENTERPRISE_UUID) - mock_get_enrollment_api.assert_called_once() + self.run_test_with_setting( + setting_value, + mock_update_create_enroll, + mock_enroll_user, + test_function_true, + test_function_false + ) + mock_get_enrollment_api.assert_called_once_with(USERNAME, str(COURSE_ID)) + @mock.patch('openedx.features.enterprise_support.enrollments.utils.lms_enroll_user_in_course') + @mock.patch('openedx.features.enterprise_support.enrollments.utils.lms_update_or_create_enrollment') @mock.patch('openedx.features.enterprise_support.enrollments.utils.enrollment_api.add_enrollment') @mock.patch('openedx.features.enterprise_support.enrollments.utils.enrollment_api.get_enrollment') @mock.patch('openedx.features.enterprise_support.enrollments.utils.User.objects.get') @mock.patch('openedx.features.enterprise_support.enrollments.utils.transaction') + @ddt.data(True, False) def test_calls_enrollment_and_cohort_apis( self, + setting, mock_tx, mock_user_model, mock_get_enrollment_api, mock_add_enrollment_api, + mock_update_create_enroll, + mock_enroll_user, ): - - expected_response = {'a': 'value'} + test_function_true = lambda mock_fn: lms_update_or_create_enrollment( + USERNAME, COURSE_ID, COURSE_MODE, is_active=True, enterprise_uuid=ENTERPRISE_UUID + ) + test_function_false = lambda mock_fn: lms_enroll_user_in_course( + USERNAME, + COURSE_ID, + COURSE_MODE, + ENTERPRISE_UUID + ) + expected_response = {'mode': COURSE_MODE, 'is_active': True} enrollment_response = {'mode': COURSE_MODE, 'is_active': True} mock_add_enrollment_api.return_value = expected_response mock_tx.return_value.atomic.side_effect = None - mock_get_enrollment_api.return_value = enrollment_response - mock_user_model.return_value = self.a_user - response = lms_enroll_user_in_course(USERNAME, COURSE_ID, COURSE_MODE, ENTERPRISE_UUID) - + if setting: + mock_get_enrollment_api.return_value = None + else: + mock_get_enrollment_api.return_value = enrollment_response + response = self.run_test_with_setting( + setting, + mock_update_create_enroll, + mock_enroll_user, + test_function_true, + test_function_false + ) assert response == expected_response mock_add_enrollment_api.assert_called_once_with( USERNAME, @@ -144,22 +266,35 @@ class EnrollmentUtilsTest(TestCase): enrollment_attributes=None, enterprise_uuid=ENTERPRISE_UUID, ) - mock_get_enrollment_api.assert_called_once_with(USERNAME, str(COURSE_ID)) + @mock.patch('openedx.features.enterprise_support.enrollments.utils.lms_enroll_user_in_course') + @mock.patch('openedx.features.enterprise_support.enrollments.utils.lms_update_or_create_enrollment') @mock.patch('openedx.features.enterprise_support.enrollments.utils.enrollment_api.add_enrollment') @mock.patch('openedx.features.enterprise_support.enrollments.utils.enrollment_api.get_enrollment') @mock.patch('openedx.features.enterprise_support.enrollments.utils.User.objects.get') @mock.patch('openedx.features.enterprise_support.enrollments.utils.transaction') + @ddt.data(True, False) def test_existing_enrollment_does_not_fail( self, + setting_value, mock_tx, mock_user_model, mock_get_enrollment_api, mock_add_enrollment_api, + mock_update_create_enroll, + mock_enroll_user, ): - - expected_response = None + test_function_true = lambda mock_fn: lms_update_or_create_enrollment( + USERNAME, COURSE_ID, COURSE_MODE, is_active=True, enterprise_uuid=ENTERPRISE_UUID + ) + test_function_false = lambda mock_fn: lms_enroll_user_in_course( + USERNAME, + COURSE_ID, + COURSE_MODE, + ENTERPRISE_UUID + ) + expected_response = {'mode': COURSE_MODE, 'is_active': True} enrollment_response = {'mode': COURSE_MODE, 'is_active': True} mock_add_enrollment_api.side_effect = CourseEnrollmentExistsError("test", {}) @@ -169,16 +304,97 @@ class EnrollmentUtilsTest(TestCase): mock_user_model.return_value = self.a_user - response = lms_enroll_user_in_course(USERNAME, COURSE_ID, COURSE_MODE, ENTERPRISE_UUID) + response = self.run_test_with_setting( + setting_value, + mock_update_create_enroll, + mock_enroll_user, + test_function_true, + test_function_false + ) + if setting_value: + mock_add_enrollment_api.assert_not_called() + assert response == expected_response + else: + mock_add_enrollment_api.assert_called_once_with( + USERNAME, + str(COURSE_ID), + mode=COURSE_MODE, + is_active=True, + enrollment_attributes=None, + enterprise_uuid=ENTERPRISE_UUID, + ) + assert response is None + mock_get_enrollment_api.assert_called_once() - assert response == expected_response - mock_add_enrollment_api.assert_called_once_with( - USERNAME, - str(COURSE_ID), - mode=COURSE_MODE, - is_active=True, - enrollment_attributes=None, - enterprise_uuid=ENTERPRISE_UUID, + @mock.patch('openedx.features.enterprise_support.enrollments.utils.enrollment_api.update_enrollment') + @mock.patch('openedx.features.enterprise_support.enrollments.utils.enrollment_api.get_enrollment') + @mock.patch('openedx.features.enterprise_support.enrollments.utils.enrollment_api.add_enrollment') + @mock.patch('openedx.features.enterprise_support.enrollments.utils.User.objects.get') + @mock.patch('openedx.features.enterprise_support.enrollments.utils.transaction') + def test_upgrade_user_enrollment_mode( + self, + mock_tx, + mock_user_model, + mock_add_enrollment_api, + mock_get_enrollment_api, + mock_update_enrollment_api, + ): + enrollment_response = {'mode': COURSE_MODE, 'is_active': True} + mock_get_enrollment_api.return_value = { + 'mode': 'audit', + 'is_active': True, + } + + mock_update_enrollment_api.return_value = { + 'mode': 'verified', + 'is_active': True, + } + mock_tx.return_value.atomic.side_effect = None + mock_user_model.return_value = self.a_user + + upgraded_enrollment = lms_update_or_create_enrollment( + USERNAME, COURSE_ID, desired_mode=COURSE_MODE, is_active=True ) + assert upgraded_enrollment == enrollment_response + mock_update_enrollment_api.assert_called_once_with( + USERNAME, + str(COURSE_ID), + mode='verified', + is_active=True, + enrollment_attributes=None, + ) + + mock_get_enrollment_api.assert_called_once_with(USERNAME, str(COURSE_ID)) + mock_add_enrollment_api.assert_not_called() + + @mock.patch('openedx.features.enterprise_support.enrollments.utils.enrollment_api.update_enrollment') + @mock.patch('openedx.features.enterprise_support.enrollments.utils.enrollment_api.get_enrollment') + @mock.patch('openedx.features.enterprise_support.enrollments.utils.enrollment_api.add_enrollment') + @mock.patch('openedx.features.enterprise_support.enrollments.utils.User.objects.get') + @mock.patch('openedx.features.enterprise_support.enrollments.utils.transaction') + def test_upgrade_user_enrollment_mode_already_verified( + self, + mock_tx, + mock_user_model, + mock_add_enrollment_api, + mock_get_enrollment_api, + mock_update_enrollment_api, + ): + existing_enrollment = { + 'mode': 'verified', + 'is_active': True, + } + mock_get_enrollment_api.return_value = existing_enrollment + + mock_tx.return_value.atomic.side_effect = None + mock_user_model.return_value = self.a_user + + upgraded_enrollment = lms_update_or_create_enrollment( + USERNAME, COURSE_ID, desired_mode='verified', is_active=True + ) + + assert upgraded_enrollment == existing_enrollment + mock_update_enrollment_api.assert_not_called() mock_get_enrollment_api.assert_called_once() + mock_add_enrollment_api.assert_not_called() diff --git a/openedx/features/enterprise_support/enrollments/utils.py b/openedx/features/enterprise_support/enrollments/utils.py index dbc4ef6ebf..f44de3e2d4 100644 --- a/openedx/features/enterprise_support/enrollments/utils.py +++ b/openedx/features/enterprise_support/enrollments/utils.py @@ -8,7 +8,11 @@ from django.db import transaction from common.djangoapps.student.models import User from openedx.core.djangoapps.enrollments import api as enrollment_api -from openedx.core.djangoapps.enrollments.errors import CourseEnrollmentError, CourseEnrollmentExistsError +from openedx.core.djangoapps.enrollments.errors import ( + CourseEnrollmentError, + CourseEnrollmentExistsError, + CourseEnrollmentNotUpdatableError, +) from openedx.core.lib.log_utils import audit_log from openedx.features.enterprise_support.enrollments.exceptions import ( CourseIdMissingException, @@ -26,25 +30,7 @@ def lms_enroll_user_in_course( is_active=True, ): """ - Enrollment function meant to be called by edx-enterprise to replace the - current uses of the EnrollmentApiClient - The REST enrollment endpoint may also eventually also want to reuse this function - since it's a subset of what the endpoint handles - - Unlike the REST endpoint, this function does not check for enterprise enabled, or user api key - permissions etc. Those concerns are still going to be used by REST endpoint but this function - is meant for use from within edx-enterprise hence already presume such privileges. - - Arguments: - - username (str): User name - - course_id (obj) : Course key obtained using CourseKey.from_string(course_id_input) - - mode (CourseMode): course mode - - enterprise_uuid (str): id to identify the enterprise to enroll under - - is_active (bool): Optional. A Boolean value that indicates whether the - enrollment is to be set to inactive (if False). Usually we want a True if enrolling anew. - - Returns: A serializable dictionary of the new course enrollment. If it hits - `CourseEnrollmentExistsError` then it logs the error and returns None. + Temporarily keeping the original enrollment function to help with deployment """ user = _validate_enrollment_inputs(username, course_id) @@ -81,6 +67,134 @@ def lms_enroll_user_in_course( ) +def lms_update_or_create_enrollment( + username, + course_id, + desired_mode, + is_active, + enterprise_uuid=None, +): + """ + Update or create the user's course enrollment based on the existing enrollment mode. + If an enrollment exists and its mode is not equal to the desired mode, + then it updates the enrollment. + Otherwise, it creates a new enrollment. + Enrollment function meant to be called by edx-enterprise to replace the + current uses of the EnrollmentApiClient + The REST enrollment endpoint may also eventually also want to reuse this function + since it's a subset of what the endpoint handles + + Unlike the REST endpoint, this function does not check for enterprise enabled, or user api key + permissions etc. Those concerns are still going to be used by REST endpoint but this function + is meant for use from within edx-enterprise hence already presume such privileges. + + Arguments: + - username (str): User name + - course_id (obj) : Course key obtained using CourseKey.from_string(course_id_input) + - desired_mode (CourseMode): desired course mode + - is_active (bool): A Boolean value that indicates whether the + enrollment is to be set to inactive (if False). Usually we want a True if enrolling anew. + - enterprise_uuid (str): Optional. id to identify the enterprise to enroll under + + Returns: A serializable dictionary of the new or updated course enrollment. If it hits + CourseEnrollmentError or CourseEnrollmentNotUpdatableError, it raises those exceptions. + In case of the add_enrollment call, it returns None if the enrollment already exists and + the desired_mode or is_active match the existing enrollment. + """ + user = _validate_enrollment_inputs(username, course_id) + current_enrollment = enrollment_api.get_enrollment(username, str(course_id)) + response = None + if ( + current_enrollment + and current_enrollment['mode'] == desired_mode + and current_enrollment['is_active'] == is_active + ): + log.info( + "Existing enrollment [%s] for user [%s] matches desired enrollment. No action taken.", + current_enrollment, + username, + ) + return current_enrollment + with transaction.atomic(): + try: + if current_enrollment: + response = enrollment_api.update_enrollment( + username, + str(course_id), + mode=desired_mode, + is_active=is_active, + enrollment_attributes=None, + ) + if not response or ( + response['mode'] != desired_mode or + response['is_active'] != is_active + ): + log.exception( + "An error occurred while updating the course enrollment for user " + "[%s]: course run = [%s], enterprise_uuid = [%s], is_active = [%s], ", + username, + course_id, + str(enterprise_uuid), + is_active, + ) + raise CourseEnrollmentNotUpdatableError( + f"Unable to upgrade enrollment for user {username} " + "in course {course_id} to {desired_mode} mode." + "Response from update_enrollment: {response}" + ) + else: + response = enrollment_api.add_enrollment( + username, + str(course_id), + mode=desired_mode, + is_active=is_active, + enrollment_attributes=None, + enterprise_uuid=enterprise_uuid, + ) + if not response: + log.exception( + "An error occurred while creating the new course enrollment for user " + "[%s] in course run [%s]", + username, + course_id, + ) + raise CourseEnrollmentError( + f"Unable to create enrollment for user {username} in course {course_id}." + ) + except CourseEnrollmentExistsError as error: + # This will rarely be raised when we hit a race condition in adding a net-new enrollment + log.warning( + "An enrollment [%s] already exists for user [%s] in course run [%s].", + error.enrollment, + username, + course_id, + ) + return None + except (CourseEnrollmentError, CourseEnrollmentNotUpdatableError) as error: + log.exception( + "Raising error [%s] for user " + "[%s]: course run = [%s], enterprise_uuid = [%s], is_active = [%s], ", + error, + username, + course_id, + str(enterprise_uuid), + is_active, + ) + raise error + finally: + final_enrollment = response or current_enrollment + audit_log( + 'enrollment_change_requested', + course_id=str(course_id), + requested_mode=desired_mode, + actual_mode=final_enrollment['mode'] if final_enrollment else None, + requested_activation=is_active, + actual_activation=final_enrollment['is_active'] if final_enrollment else None, + user_id=user.id + ) + return response + + def _validate_enrollment_inputs(username, course_id): """ Validates username and course_id. diff --git a/requirements/constraints.txt b/requirements/constraints.txt index 3fdefab02a..c96ddb4ba0 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -27,7 +27,7 @@ django-storages==1.9.1 # 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==4.0.0 +edx-enterprise==4.0.1 # oauthlib>3.0.1 causes test failures ( also remove the django-oauth-toolkit constraint when this is fixed ) oauthlib==3.0.1 diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index ad06c70931..e6cbf28a99 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -484,7 +484,7 @@ edx-drf-extensions==8.8.0 # edx-when # edxval # learner-pathway-progress -edx-enterprise==4.0.0 +edx-enterprise==4.0.1 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/kernel.in diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt index 49b95e50a5..d5820595bc 100644 --- a/requirements/edx/development.txt +++ b/requirements/edx/development.txt @@ -633,7 +633,7 @@ edx-drf-extensions==8.8.0 # edx-when # edxval # learner-pathway-progress -edx-enterprise==4.0.0 +edx-enterprise==4.0.1 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/testing.txt diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt index d11a06211b..1dcf214f0c 100644 --- a/requirements/edx/testing.txt +++ b/requirements/edx/testing.txt @@ -590,7 +590,7 @@ edx-drf-extensions==8.8.0 # edx-when # edxval # learner-pathway-progress -edx-enterprise==4.0.0 +edx-enterprise==4.0.1 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/base.txt