Skip to content

Commit

Permalink
fix: respect allow_enrollment_in_invite_only_courses
Browse files Browse the repository at this point in the history
Fixes `enroll_learners_in_courses` endpoint (and
`enroll_subsidy_users_in_courses` utility function) to respect
enterprise customer's `allow_enrollment_in_invite_only_courses` flag.

(cherry picked from commit 75ea83c)
  • Loading branch information
0x29a committed Mar 26, 2024
1 parent f538e88 commit a876fa8
Show file tree
Hide file tree
Showing 3 changed files with 100 additions and 1 deletion.
4 changes: 4 additions & 0 deletions enterprise/api/v1/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
NotConnectedToOpenEdX,
enroll_subsidy_users_in_courses,
get_best_mode_from_course_key,
get_course_details_from_course_keys,
get_enterprise_customer,
get_request_value,
track_enrollment,
Expand Down Expand Up @@ -328,6 +329,8 @@ def enroll_learners_in_courses(self, request, pk):
for course_run in course_runs_modes:
course_runs_modes[course_run] = get_best_mode_from_course_key(course_run)

course_details = get_course_details_from_course_keys(course_runs_modes.keys())

emails = set()

for info in enrollments_info:
Expand All @@ -341,6 +344,7 @@ def enroll_learners_in_courses(self, request, pk):
else:
emails.add(info['email'])
info['course_mode'] = course_runs_modes[info['course_run_key']]
info['invitation_only'] = course_details[info['course_run_key']].invitation_only

for email in emails:
try:
Expand Down
20 changes: 20 additions & 0 deletions enterprise/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@
CourseUserGroup = None
CourseEnrollmentError = None

try:
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
except ImportError:
CourseOverview = None

try:
from common.djangoapps.course_modes.models import CourseMode
from common.djangoapps.student.models import CourseEnrollmentAllowed
Expand Down Expand Up @@ -1979,6 +1984,7 @@ def enroll_subsidy_users_in_courses(enterprise_customer, subsidy_users_info, dis
user_id = subsidy_user_info.get('user_id')
user_email = subsidy_user_info['email'].strip().lower() if 'email' in subsidy_user_info else None
course_mode = subsidy_user_info.get('course_mode')
invitation_only = subsidy_user_info.get('invitation_only')
course_run_key = subsidy_user_info.get('course_run_key')
license_uuid = subsidy_user_info.get('license_uuid')
transaction_id = subsidy_user_info.get('transaction_id')
Expand All @@ -2005,6 +2011,12 @@ def enroll_subsidy_users_in_courses(enterprise_customer, subsidy_users_info, dis
enrollment_source = enterprise_enrollment_source_model().get_source(
enterprise_enrollment_source_model().CUSTOMER_ADMIN
)
if invitation_only and enterprise_customer.allow_enrollment_in_invite_only_courses:
CourseEnrollmentAllowed.objects.update_or_create(
course_id=course_run_key,
email=user.email,
)

succeeded, created, source_uuid = customer_admin_enroll_user_with_status(
enterprise_customer,
user,
Expand Down Expand Up @@ -2243,6 +2255,14 @@ def get_best_mode_from_course_key(course_key):
return CourseModes.AUDIT


def get_course_details_from_course_keys(course_keys):
"""
Helper to get a mapping of course keys to course details.
"""
course_overviews = CourseOverview.objects.filter(id__in=course_keys)
return {str(course_overview.id): course_overview for course_overview in course_overviews}


def parse_lms_api_datetime(datetime_string, datetime_format=LMS_API_DATETIME_FORMAT):
"""
Parse a received datetime into a timezone-aware, Python datetime object.
Expand Down
77 changes: 76 additions & 1 deletion tests/test_enterprise/api/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -4004,6 +4004,7 @@ def _create_user_and_enterprise_customer(self, username, password):
},
)
@ddt.unpack
@mock.patch('enterprise.api.v1.views.get_course_details_from_course_keys')
@mock.patch('enterprise.api.v1.views.get_best_mode_from_course_key')
@mock.patch('enterprise.api.v1.views.track_enrollment')
@mock.patch("enterprise.models.EnterpriseCustomer.notify_enrolled_learners")
Expand All @@ -4014,6 +4015,7 @@ def test_bulk_enrollment_in_bulk_courses_pending_licenses(
mock_notify_task,
mock_track_enroll,
mock_get_course_mode,
mock_get_course_details,
body,
expected_code,
expected_response,
Expand All @@ -4034,6 +4036,7 @@ def test_bulk_enrollment_in_bulk_courses_pending_licenses(
permission = Permission.objects.get(name='Can add Enterprise Customer')
self.user.user_permissions.add(permission)
mock_get_course_mode.return_value = VERIFIED_SUBSCRIPTION_COURSE_MODE
mock_get_course_details.return_value.__getitem__.return_value.invitation_only = False

self.assertEqual(len(PendingEnrollment.objects.all()), 0)

Expand Down Expand Up @@ -4066,6 +4069,7 @@ def test_bulk_enrollment_in_bulk_courses_pending_licenses(
# no notifications to be sent unless 'notify' specifically asked for in payload
mock_notify_task.assert_not_called()

@mock.patch('enterprise.api.v1.views.get_course_details_from_course_keys')
@mock.patch('enterprise.api.v1.views.get_best_mode_from_course_key')
@mock.patch('enterprise.api.v1.views.track_enrollment')
@mock.patch('enterprise.models.EnterpriseCustomer.notify_enrolled_learners')
Expand All @@ -4076,13 +4080,15 @@ def test_bulk_enrollment_in_bulk_courses_existing_users(
mock_notify_task,
mock_track_enroll,
mock_get_course_mode,
mock_get_course_details,
):
"""
Tests the bulk enrollment endpoint at enroll_learners_in_courses.
This tests the case where existing users are supplied, so the enrollments are fulfilled rather than pending.
"""
mock_customer_admin_enroll_user.return_value = True
mock_get_course_details.return_value.__getitem__.return_value.invitation_only = False

user_one = factories.UserFactory(is_active=True)
user_two = factories.UserFactory(is_active=True)
Expand Down Expand Up @@ -4153,6 +4159,7 @@ def test_bulk_enrollment_in_bulk_courses_existing_users(
# no notifications to be sent unless 'notify' specifically asked for in payload
mock_notify_task.assert_not_called()

@mock.patch('enterprise.api.v1.views.get_course_details_from_course_keys')
@mock.patch('enterprise.api.v1.views.get_best_mode_from_course_key')
@mock.patch('enterprise.api.v1.views.track_enrollment')
@mock.patch('enterprise.models.EnterpriseCustomer.notify_enrolled_learners')
Expand All @@ -4161,6 +4168,7 @@ def test_bulk_enrollment_in_bulk_courses_nonexisting_user_id(
mock_notify_task,
mock_track_enroll,
mock_get_course_mode,
mock_get_course_details,
):
"""
Tests the bulk enrollment endpoint at enroll_learners_in_courses.
Expand All @@ -4177,6 +4185,7 @@ def test_bulk_enrollment_in_bulk_courses_nonexisting_user_id(
permission = Permission.objects.get(name='Can add Enterprise Customer')
self.user.user_permissions.add(permission)
mock_get_course_mode.return_value = VERIFIED_SUBSCRIPTION_COURSE_MODE
mock_get_course_details.return_value.__getitem__.return_value.invitation_only = False

self.assertEqual(len(PendingEnrollment.objects.all()), 0)
body = {
Expand Down Expand Up @@ -4243,12 +4252,14 @@ def test_bulk_enrollment_in_bulk_courses_nonexisting_user_id(
},
)
@ddt.unpack
@mock.patch('enterprise.api.v1.views.get_course_details_from_course_keys')
@mock.patch('enterprise.api.v1.views.get_best_mode_from_course_key')
@mock.patch("enterprise.utils.lms_enroll_user_in_course")
def test_bulk_enrollment_includes_fulfillment_source_uuid(
self,
mock_platform_enrollment,
mock_get_course_mode,
mock_get_course_details,
body,
fulfillment_source,
):
Expand All @@ -4265,6 +4276,7 @@ def test_bulk_enrollment_includes_fulfillment_source_uuid(
permission = Permission.objects.get(name='Can add Enterprise Customer')
user.user_permissions.add(permission)
mock_get_course_mode.return_value = VERIFIED_SUBSCRIPTION_COURSE_MODE
mock_get_course_details.return_value.__getitem__.return_value.invitation_only = False

enrollment_url = reverse(
'enterprise-customer-enroll-learners-in-courses',
Expand Down Expand Up @@ -4351,6 +4363,7 @@ def test_bulk_enrollment_includes_fulfillment_source_uuid(
},
)
@ddt.unpack
@mock.patch('enterprise.api.v1.views.get_course_details_from_course_keys')
@mock.patch('enterprise.api.v1.views.get_best_mode_from_course_key')
@mock.patch('enterprise.api.v1.views.track_enrollment')
@mock.patch("enterprise.models.EnterpriseCustomer.notify_enrolled_learners")
Expand All @@ -4359,6 +4372,7 @@ def test_bulk_enrollment_with_notification(
mock_notify_task,
mock_track_enroll,
mock_get_course_mode,
mock_get_course_details,
body,
expected_code,
expected_response,
Expand All @@ -4378,6 +4392,7 @@ def test_bulk_enrollment_with_notification(
permission = Permission.objects.get(name='Can add Enterprise Customer')
self.user.user_permissions.add(permission)
mock_get_course_mode.return_value = VERIFIED_SUBSCRIPTION_COURSE_MODE
mock_get_course_details.return_value.__getitem__.return_value.invitation_only = False

self.assertEqual(len(PendingEnrollment.objects.all()), 0)

Expand Down Expand Up @@ -4425,12 +4440,72 @@ def _make_call(course_run, enrolled_learners):

mock_notify_task.assert_has_calls(mock_calls, any_order=True)

@mock.patch('enterprise.utils.CourseEnrollmentAllowed')
@mock.patch('enterprise.api.v1.views.get_course_details_from_course_keys')
@mock.patch('enterprise.api.v1.views.get_best_mode_from_course_key')
@mock.patch('enterprise.utils.lms_update_or_create_enrollment')
def test_bulk_enrollment_invitation_only(
self,
mock_update_or_create_enrollment,
mock_get_course_mode,
mock_get_course_details,
mock_cea,
):
"""
Tests that bulk enrollment endpoint creates CourseEnrollmentAllowed object when enterprise customer allows
enrollment in invitation only courses and the course is invitation only.
"""
mock_update_or_create_enrollment.return_value = True
mock_get_course_details.return_value.__getitem__.return_value.invitation_only = True
mock_get_course_mode.return_value = VERIFIED_SUBSCRIPTION_COURSE_MODE

user, enterprise_customer = self._create_user_and_enterprise_customer("abc@test.com", "test_password")
course_id = 'course-v1:edX+DemoX+Demo_Course'
body = {
'enrollments_info': [
{
'user_id': user.id,
'course_run_key': course_id,
'license_uuid': '5a88bdcade7c4ecb838f8111b68e18ac'
},
]
}

def enroll():
self.client.post(
settings.TEST_SERVER + reverse(
'enterprise-customer-enroll-learners-in-courses', (enterprise_customer.uuid,)
),
data=json.dumps(body),
content_type='application/json',
)

enroll()
mock_cea.objects.update_or_create.assert_not_called()

enterprise_customer.allow_enrollment_in_invite_only_courses = True
enterprise_customer.save()

enroll()
mock_cea.objects.update_or_create.assert_called_with(
course_id=course_id,
email=user.email
)

@mock.patch('enterprise.api.v1.views.enterprise_customer.get_course_details_from_course_keys')
@mock.patch('enterprise.api.v1.views.enroll_subsidy_users_in_courses')
@mock.patch('enterprise.api.v1.views.get_best_mode_from_course_key')
def test_enroll_learners_in_courses_partial_failure(self, mock_get_course_mode, mock_enroll_user):
def test_enroll_learners_in_courses_partial_failure(
self,
mock_get_course_mode,
mock_enroll_user,
mock_get_course_details,
):
"""
Tests that bulk users bulk enrollment endpoint properly handles partial failures.
"""
mock_get_course_details.return_value.__getitem__.return_value.invitation_only = True

ent_customer = factories.EnterpriseCustomerFactory(
uuid=FAKE_UUIDS[0],
name="test_enterprise"
Expand Down

0 comments on commit a876fa8

Please sign in to comment.