diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c7209b4284..c53f65a767 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -15,6 +15,10 @@ Change Log Unreleased ---------- +[4.13.11] +--------- +* feat: pass force_enrollment when bulk enrolling learners + [4.13.10] --------- * fix: remove filter to debug failing transmissions diff --git a/enterprise/__init__.py b/enterprise/__init__.py index ae11b10a58..2d9498e47f 100644 --- a/enterprise/__init__.py +++ b/enterprise/__init__.py @@ -2,4 +2,4 @@ Your project description goes here. """ -__version__ = "4.13.10" +__version__ = "4.13.11" diff --git a/enterprise/api/v1/views/enterprise_customer.py b/enterprise/api/v1/views/enterprise_customer.py index 1685304d17..3046b30660 100644 --- a/enterprise/api/v1/views/enterprise_customer.py +++ b/enterprise/api/v1/views/enterprise_customer.py @@ -171,9 +171,15 @@ def enroll_learners_in_courses(self, request, pk): Parameters: enrollments_info (list of dicts): an array of dictionaries, each containing the necessary information to create an enrollment based on a subsidy for a user in a specified course. Each dictionary must contain - a user email (or user_id), a course run key, and either a UUID of the license that the learner is using - to enroll with or a transaction ID related to Executive Education the enrollment. `licenses_info` is - also accepted as a body param name. + the following keys: + + * 'user_id' OR 'email': Either unique identifier describing the user to enroll. + * 'course_run_key': The course to enroll into. + * 'license_uuid' OR 'transaction_id': ID of either accepted form of subsidy. `license_uuid` refers to + subscription licenses, and `transaction_id` refers to Learner Credit transactions. + * 'force_enrollment' (bool, optional): Enroll even if enrollment deadline is expired (default False). + + `licenses_info` is also accepted as a body param name. Example:: diff --git a/enterprise/utils.py b/enterprise/utils.py index 631eeefd48..f0f47d38b5 100644 --- a/enterprise/utils.py +++ b/enterprise/utils.py @@ -1807,6 +1807,7 @@ def customer_admin_enroll_user_with_status( enrollment_source=None, license_uuid=None, transaction_id=None, + force_enrollment=False, ): """ For use with bulk enrollment, or any use case of admin enrolling a user @@ -1848,6 +1849,7 @@ def customer_admin_enroll_user_with_status( course_mode, is_active=True, enterprise_uuid=enterprise_customer.uuid, + force_enrollment=force_enrollment, ) succeeded = True LOGGER.info("Successfully enrolled user %s in course %s", user.id, course_id) @@ -1987,6 +1989,7 @@ def enroll_subsidy_users_in_courses(enterprise_customer, subsidy_users_info, dis * 'course_run_key': The course to enroll into. * 'course_mode': The course mode. * 'license_uuid' OR 'transaction_id': ID of either accepted form of subsidy. + * 'force_enrollment' (bool, optional): Enroll user even enrollment deadline is expired (default False). Example:: @@ -2037,6 +2040,7 @@ def enroll_subsidy_users_in_courses(enterprise_customer, subsidy_users_info, dis license_uuid = subsidy_user_info.get('license_uuid') transaction_id = subsidy_user_info.get('transaction_id') activation_link = subsidy_user_info.get('activation_link') + force_enrollment = subsidy_user_info.get('force_enrollment', False) if user_id and user_email: user = User.objects.filter(id=subsidy_user_info['user_id']).first() @@ -2066,7 +2070,8 @@ def enroll_subsidy_users_in_courses(enterprise_customer, subsidy_users_info, dis course_run_key, enrollment_source, license_uuid, - transaction_id + transaction_id, + force_enrollment=force_enrollment, ) if succeeded: success_dict = { diff --git a/tests/test_enterprise/test_utils.py b/tests/test_enterprise/test_utils.py index 9dc3cac117..fcc2fa793f 100644 --- a/tests/test_enterprise/test_utils.py +++ b/tests/test_enterprise/test_utils.py @@ -4,6 +4,7 @@ import unittest from datetime import timedelta from unittest import mock +from unittest.mock import call from urllib.parse import quote, urlencode import ddt @@ -325,6 +326,119 @@ def test_enroll_subsidy_users_in_courses_with_user_id_succeeds( ) self.assertEqual(len(EnterpriseCourseEnrollment.objects.all()), 2) + @mock.patch('enterprise.utils.lms_update_or_create_enrollment') + def test_enroll_subsidy_users_in_courses_with_force_enrollment( + self, + mock_update_or_create_enrollment, + ): + """ + """ + self.create_user() + another_user_1 = factories.UserFactory(is_active=True) + another_user_2 = factories.UserFactory(is_active=True) + ent_customer = factories.EnterpriseCustomerFactory( + uuid=FAKE_UUIDS[0], + name="test_enterprise" + ) + licensed_users_info = [ + { + # Should succeed with force_enrollment passed as False under the hood. + 'user_id': self.user.id, + 'course_run_key': 'course-key-1', + 'course_mode': 'verified', + 'license_uuid': '5b77bdbade7b4fcb838f8111b68e18ae', + }, + { + # Should also succeed with force_enrollment passed as False. + 'user_id': another_user_1.id, + 'course_run_key': 'course-key-2', + 'course_mode': 'verified', + 'license_uuid': '5b77bdbade7b4fcb838f8111b68e18ae', + 'force_enrollment': False, + }, + { + # Should succeed with force_enrollment passed as True. + 'user_id': another_user_2.id, + 'course_run_key': 'course-key-3', + 'course_mode': 'verified', + 'license_uuid': '5b77bdbade7b4fcb838f8111b68e18ae', + 'force_enrollment': True, + }, + ] + + mock_update_or_create_enrollment.return_value = True + + result = enroll_subsidy_users_in_courses(ent_customer, licensed_users_info) + self.assertEqual( + { + 'pending': [], + 'successes': [ + { + 'user_id': self.user.id, + 'email': self.user.email, + 'course_run_key': 'course-key-1', + 'user': self.user, + 'created': True, + 'activation_link': None, + 'enterprise_fulfillment_source_uuid': EnterpriseCourseEnrollment.objects.filter( + enterprise_customer_user__user_id=self.user.id + ).first().licensedenterprisecourseenrollment_enrollment_fulfillment.uuid, + }, + { + 'user_id': another_user_1.id, + 'email': another_user_1.email, + 'course_run_key': 'course-key-2', + 'user': another_user_1, + 'created': True, + 'activation_link': None, + 'enterprise_fulfillment_source_uuid': EnterpriseCourseEnrollment.objects.filter( + enterprise_customer_user__user_id=another_user_1.id + ).first().licensedenterprisecourseenrollment_enrollment_fulfillment.uuid, + }, + { + 'user_id': another_user_2.id, + 'email': another_user_2.email, + 'course_run_key': 'course-key-3', + 'user': another_user_2, + 'created': True, + 'activation_link': None, + 'enterprise_fulfillment_source_uuid': EnterpriseCourseEnrollment.objects.filter( + enterprise_customer_user__user_id=another_user_2.id + ).first().licensedenterprisecourseenrollment_enrollment_fulfillment.uuid, + }, + ], + 'failures': [], + }, + result + ) + self.assertEqual(len(EnterpriseCourseEnrollment.objects.all()), 3) + assert mock_update_or_create_enrollment.mock_calls == [ + call( + self.user.username, + 'course-key-1', + 'verified', + is_active=True, + enterprise_uuid=ent_customer.uuid, + force_enrollment=False, + ), + call( + another_user_1.username, + 'course-key-2', + 'verified', + is_active=True, + enterprise_uuid=ent_customer.uuid, + force_enrollment=False, + ), + call( + another_user_2.username, + 'course-key-3', + 'verified', + is_active=True, + enterprise_uuid=ent_customer.uuid, + force_enrollment=True, + ), + ] + @mock.patch('enterprise.utils.lms_update_or_create_enrollment') def test_enroll_subsidy_users_in_courses_user_identifier_failures( self,