Skip to content

Commit d77191a

Browse files
authored
fix: create CEA object when enrolling using a license flow (#18)
* fix: create CEA object when enrolling using a license flow * test: verify that allow_enrollment is called_correctly * fix: xmlsec issue xmlsec/python-xmlsec#314 * build: enable CI for pull requests * style: fix some pycodestyle issues
1 parent ad03ac6 commit d77191a

File tree

9 files changed

+97
-25
lines changed

9 files changed

+97
-25
lines changed

.github/workflows/ci.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ on:
44
push:
55
branches: [master]
66
pull_request:
7-
branches: [master]
87

98
concurrency:
109
group: ci-${{ github.event.pull_request.number || github.ref }}

.github/workflows/mysql8-migrations.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ jobs:
5656
pip uninstall -y mysqlclient
5757
pip install --no-binary mysqlclient mysqlclient
5858
pip uninstall -y xmlsec
59-
pip install --no-binary xmlsec xmlsec
59+
pip install --no-binary xmlsec xmlsec==1.3.13
6060
pip install backports.zoneinfo
6161
- name: Initiate Services
6262
run: |

enterprise/api_client/lms.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import time
77
from urllib.parse import urljoin
88

9+
import requests
910
from opaque_keys.edx.keys import CourseKey
1011
from requests.exceptions import ( # pylint: disable=redefined-builtin
1112
ConnectionError,
@@ -274,6 +275,34 @@ def get_enrolled_courses(self, username):
274275
response.raise_for_status()
275276
return response.json()
276277

278+
def allow_enrollment(self, email, course_id, auto_enroll=False):
279+
"""
280+
Call the enrollment API to allow enrollment for the given email and course_id.
281+
282+
Args:
283+
email (str): The email address of the user to be allowed to enroll in the course.
284+
course_id (str): The string value of the course's unique identifier.
285+
auto_enroll (bool): Whether to auto-enroll the user in the course upon registration / activation.
286+
287+
Returns:
288+
dict: A dictionary containing details of the created CourseEnrollmentAllowed object.
289+
290+
"""
291+
api_url = self.get_api_url("enrollment_allowed")
292+
response = self.client.post(
293+
f"{api_url}/",
294+
json={
295+
'email': email,
296+
'course_id': course_id,
297+
'auto_enroll': auto_enroll,
298+
}
299+
)
300+
if response.status_code == requests.codes.conflict:
301+
LOGGER.info(response.json()["message"])
302+
else:
303+
response.raise_for_status()
304+
return response.json()
305+
277306

278307
class CourseApiClient(NoAuthAPIClient):
279308
"""

enterprise/utils.py

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2407,19 +2407,14 @@ def truncate_string(string, max_length=MAX_ALLOWED_TEXT_LENGTH):
24072407

24082408
def ensure_course_enrollment_is_allowed(course_id, email, enrollment_api_client):
24092409
"""
2410-
Create a CourseEnrollmentAllowed object for invitation-only courses.
2410+
Calls the enrollment API to create a CourseEnrollmentAllowed object for
2411+
invitation-only courses.
24112412
24122413
Arguments:
24132414
course_id (str): ID of the course to allow enrollment
24142415
email (str): email of the user whose enrollment should be allowed
24152416
enrollment_api_client (:class:`enterprise.api_client.lms.EnrollmentApiClient`): Enrollment API Client
24162417
"""
2417-
if not CourseEnrollmentAllowed:
2418-
raise NotConnectedToOpenEdX()
2419-
24202418
course_details = enrollment_api_client.get_course_details(course_id)
24212419
if course_details["invite_only"]:
2422-
CourseEnrollmentAllowed.objects.update_or_create(
2423-
course_id=course_id,
2424-
email=email,
2425-
)
2420+
enrollment_api_client.allow_enrollment(email, course_id)

enterprise/views.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -683,6 +683,15 @@ def _enroll_learner_in_course(
683683
existing_enrollment.get('mode') == constants.CourseModes.AUDIT or
684684
existing_enrollment.get('is_active') is False
685685
):
686+
if enterprise_customer.allow_enrollment_in_invite_only_courses:
687+
ensure_course_enrollment_is_allowed(course_id, request.user.email, enrollment_api_client)
688+
LOGGER.info(
689+
'User {user} is allowed to enroll in Course {course_id}.'.format(
690+
user=request.user.username,
691+
course_id=course_id
692+
)
693+
)
694+
686695
course_mode = get_best_mode_from_course_key(course_id)
687696
LOGGER.info(
688697
'Retrieved Course Mode: {course_modes} for Course {course_id}'.format(

tests/test_enterprise/api/test_views.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4512,7 +4512,6 @@ def test_bulk_enrollment_in_bulk_courses_existing_users(
45124512
mock_update_or_create_enrollment.return_value = True
45134513
mock_get_course_details.return_value.__getitem__.return_value.invitation_only = False
45144514

4515-
45164515
user_one = factories.UserFactory(is_active=True)
45174516
user_two = factories.UserFactory(is_active=True)
45184517

@@ -5001,6 +5000,7 @@ def test_bulk_enrollment_invitation_only(
50015000
},
50025001
]
50035002
}
5003+
50045004
def enroll():
50055005
self.client.post(
50065006
settings.TEST_SERVER + reverse(

tests/test_enterprise/test_utils.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -539,10 +539,9 @@ def test_truncate_string(self):
539539
self.assertEqual(len(truncated_string), MAX_ALLOWED_TEXT_LENGTH)
540540

541541
@ddt.data(True, False)
542-
@mock.patch("enterprise.utils.CourseEnrollmentAllowed")
543-
def test_ensure_course_enrollment_is_allowed(self, invite_only, mock_cea):
542+
def test_ensure_course_enrollment_is_allowed(self, invite_only):
544543
"""
545-
Test that the CourseEnrollmentAllowed is created only for the "invite_only" courses.
544+
Test that the enrollment allow endpoint is called for the "invite_only" courses.
546545
"""
547546
self.create_user()
548547
mock_enrollment_api = mock.Mock()
@@ -551,9 +550,9 @@ def test_ensure_course_enrollment_is_allowed(self, invite_only, mock_cea):
551550
ensure_course_enrollment_is_allowed("test-course-id", self.user.email, mock_enrollment_api)
552551

553552
if invite_only:
554-
mock_cea.objects.update_or_create.assert_called_with(
555-
course_id="test-course-id",
556-
email=self.user.email
553+
mock_enrollment_api.allow_enrollment.assert_called_with(
554+
self.user.email,
555+
"test-course-id",
557556
)
558557
else:
559-
mock_cea.objects.update_or_create.assert_not_called()
558+
mock_enrollment_api.allow_enrollment.assert_not_called()

tests/test_enterprise/views/test_course_enrollment_view.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1623,10 +1623,8 @@ def test_post_course_specific_enrollment_view_premium_mode(
16231623
@mock.patch('enterprise.views.EnrollmentApiClient')
16241624
@mock.patch('enterprise.views.get_data_sharing_consent')
16251625
@mock.patch('enterprise.utils.Registry')
1626-
@mock.patch('enterprise.utils.CourseEnrollmentAllowed')
16271626
def test_post_course_specific_enrollment_view_invite_only_courses(
16281627
self,
1629-
mock_cea,
16301628
registry_mock,
16311629
get_data_sharing_consent_mock,
16321630
enrollment_api_client_mock,
@@ -1664,9 +1662,9 @@ def test_post_course_specific_enrollment_view_invite_only_courses(
16641662
}
16651663
)
16661664

1667-
mock_cea.objects.update_or_create.assert_called_with(
1668-
course_id=course_id,
1669-
email=self.user.email
1665+
enrollment_api_client_mock.return_value.allow_enrollment.assert_called_with(
1666+
self.user.email,
1667+
course_id,
16701668
)
16711669
assert response.status_code == 302
16721670

tests/test_enterprise/views/test_grant_data_sharing_permissions.py

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,30 @@ def _assert_enterprise_linking_messages(self, response, user_is_active=True):
9090
'You will not be able to log back into your account until you have activated it.'
9191
)
9292

93+
def _assert_allow_enrollment_is_called_correctly(
94+
self,
95+
mock_enrollment_api_client,
96+
license_is_present,
97+
course_invite_only,
98+
enrollment_in_invite_only_courses_allowed,
99+
):
100+
"""
101+
Verify that the allow_enrollment endpoint is called only when:
102+
- License is present
103+
- Course is invite only
104+
- Enrollment in invite only courses is allowed
105+
"""
106+
if license_is_present:
107+
if course_invite_only:
108+
if enrollment_in_invite_only_courses_allowed:
109+
mock_enrollment_api_client.return_value.allow_enrollment.assert_called_once()
110+
else:
111+
mock_enrollment_api_client.return_value.allow_enrollment.assert_not_called()
112+
else:
113+
mock_enrollment_api_client.return_value.allow_enrollment.assert_not_called()
114+
else:
115+
mock_enrollment_api_client.return_value.allow_enrollment.assert_not_called()
116+
93117
@mock.patch('enterprise.views.render', side_effect=fake_render)
94118
@mock.patch('enterprise.models.EnterpriseCatalogApiClient')
95119
@mock.patch('enterprise.api_client.discovery.CourseCatalogApiServiceClient')
@@ -398,12 +422,21 @@ def test_get_course_specific_consent_not_needed(
398422
@mock.patch('enterprise.views.get_best_mode_from_course_key')
399423
@mock.patch('enterprise.api_client.discovery.CourseCatalogApiServiceClient')
400424
@ddt.data(
401-
str(uuid.uuid4()),
402-
'',
425+
(str(uuid.uuid4()), True, True),
426+
(str(uuid.uuid4()), True, False),
427+
(str(uuid.uuid4()), False, True),
428+
(str(uuid.uuid4()), False, False),
429+
('', True, True),
430+
('', True, False),
431+
('', False, True),
432+
('', False, False),
403433
)
434+
@ddt.unpack
404435
def test_get_course_specific_data_sharing_consent_not_enabled(
405436
self,
406437
license_uuid,
438+
course_invite_only,
439+
allow_enrollment_in_invite_only_courses,
407440
course_catalog_api_client_mock,
408441
mock_get_course_mode,
409442
mock_enrollment_api_client,
@@ -414,6 +447,7 @@ def test_get_course_specific_data_sharing_consent_not_enabled(
414447
enterprise_customer = EnterpriseCustomerFactory(
415448
name='Starfleet Academy',
416449
enable_data_sharing_consent=False,
450+
allow_enrollment_in_invite_only_courses=allow_enrollment_in_invite_only_courses,
417451
)
418452
content_filter = {
419453
'key': [
@@ -432,6 +466,8 @@ def test_get_course_specific_data_sharing_consent_not_enabled(
432466
course_catalog_api_client_mock.return_value.program_exists.return_value = True
433467
course_catalog_api_client_mock.return_value.get_course_id.return_value = course_id
434468

469+
mock_enrollment_api_client.return_value.get_course_details.return_value = {"invite_only": course_invite_only}
470+
435471
course_mode = 'verified'
436472
mock_get_course_mode.return_value = course_mode
437473
mock_enrollment_api_client.return_value.get_course_enrollment.return_value = {
@@ -467,6 +503,13 @@ def test_get_course_specific_data_sharing_consent_not_enabled(
467503
else:
468504
assert not mock_enrollment_api_client.return_value.enroll_user_in_course.called
469505

506+
self._assert_allow_enrollment_is_called_correctly(
507+
mock_enrollment_api_client,
508+
bool(license_uuid),
509+
course_invite_only,
510+
allow_enrollment_in_invite_only_courses
511+
)
512+
470513
@mock.patch('enterprise.views.render', side_effect=fake_render)
471514
@mock.patch('enterprise.views.get_best_mode_from_course_key')
472515
@mock.patch('enterprise.models.EnterpriseCatalogApiClient')

0 commit comments

Comments
 (0)