Skip to content

Commit

Permalink
feat: pass force_enrollment when bulk enrolling learners
Browse files Browse the repository at this point in the history
A `force_enrollment` boolean flag has been added to the "enrollment
info" dict fed into the bulk enrollment endpoint.  This enables
consumers of the enterprise bulk enrollment endpoint to force specific
enrollments even after the enrollment deadline has passed for the
course.

ENT-8525
  • Loading branch information
pwnage101 committed Mar 18, 2024
1 parent 02c8d10 commit 3f5667a
Show file tree
Hide file tree
Showing 5 changed files with 134 additions and 5 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion enterprise/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
Your project description goes here.
"""

__version__ = "4.13.10"
__version__ = "4.13.11"
12 changes: 9 additions & 3 deletions enterprise/api/v1/views/enterprise_customer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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::
Expand Down
7 changes: 6 additions & 1 deletion enterprise/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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::
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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 = {
Expand Down
114 changes: 114 additions & 0 deletions tests/test_enterprise/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down

0 comments on commit 3f5667a

Please sign in to comment.