Enhance the Link Program Enrollments Support Tool to handle the case of linking a learner to a ProgramEnrollment that is already linked to a different edX account.

In this case,
* unenroll the currently linked learner from the courses associated with related ProgramCourseEnrollments
* for each course the is enrolled in as part of a ProgramCourseEnrollment, if the coures has an audit track, then move the enrollment into the audit track. Otherwise, keep the learner's enrollment in the existing track.
* link the new user to the ProgramEnrollment
* enroll the new user in the Master's track in the courses associated with related ProgramCourseEnrollments
This commit is contained in:
Michael Roytman
2020-05-01 16:58:26 -04:00
parent 0e7a0b6d59
commit 38c255dbd2
6 changed files with 249 additions and 53 deletions

View File

@@ -5,6 +5,7 @@ Tests for account linking Python API.
from uuid import uuid4
from unittest.mock import patch
from django.test import TestCase
from edx_django_utils.cache import RequestCache
from opaque_keys.edx.keys import CourseKey
@@ -103,7 +104,7 @@ class TestLinkProgramEnrollmentsMixin(object):
def _assert_user_enrolled_in_program_courses(self, user, program_uuid, *course_keys):
"""
Assert that the given user is has active enrollments in the given courses
Assert that the given user has active enrollments in the given courses
through the given program.
"""
user.refresh_from_db()
@@ -256,6 +257,86 @@ class TestLinkProgramEnrollments(TestLinkProgramEnrollmentsMixin, TestCase):
# assert that all CourseAccessRoleAssignment objects are deleted
assert not active_enrollment_1.courseaccessroleassignment_set.all().exists()
@staticmethod
def _assert_course_enrollments_in_mode(course_enrollments, course_keys_to_mode):
"""
Assert that all program course enrollments are in the correct modes as
described by course_keys_to_mode.
Arguments:
user: the user whose course enrollments we are checking
program_uuid: the UUID of the program in which the user is enrolled
course_keys_to_mode: a mapping from course keys to the the mode
slug the user's enrollment should be in
"""
assert len(course_enrollments) == len(course_keys_to_mode)
for course_enrollment in course_enrollments:
assert course_enrollment.mode == course_keys_to_mode[course_enrollment.course.id]
@patch('lms.djangoapps.program_enrollments.api.linking.CourseMode.modes_for_course_dict')
def test_update_linking_enrollment_to_another_user(self, mock_modes_for_course_dict):
"""
Test that when link_program_enrollments is called with a program and an external_user_key,
user pair and that program is already linked to a different user with the same external_user_key
that the original user's link is removed and replaced by a link with the new user.
"""
program_enrollment = self._create_waiting_enrollment(self.program, '0001')
self._create_waiting_course_enrollment(program_enrollment, self.fruit_course)
self._create_waiting_course_enrollment(program_enrollment, self.animal_course)
# in order to test what happens to a learner's enrollment in a course without an audit mode
# (e.g. Master's only course), we need to mock out the course modes that exist for our courses;
# doing it this way helps to avoid needing to use the modulestore when using the CourseModeFactory
def mocked_modes_for_course_dict(course_key):
if course_key == self.animal_course:
return {'masters': 'masters'}
else:
return {'audit': 'audit'}
mock_modes_for_course_dict.side_effect = mocked_modes_for_course_dict
# do the initial link of user_1 to the program enrollment
link_program_enrollments(self.program, {'0001': self.user_1.username})
self._assert_program_enrollment(self.user_1, self.program, '0001', refresh=False)
self._assert_no_program_enrollment(self.user_2, self.program, refresh=False)
# grab the user's original course enrollment before the link between the program
# and the course enrollments is severed
course_enrollments_for_user_1 = [pce.course_enrollment
for pce
in program_enrollment.program_course_enrollments.all()]
errors = link_program_enrollments(
self.program,
{
'0001': self.user_2.username,
}
)
assert errors == {}
self._assert_program_enrollment(self.user_2, self.program, '0001')
self._assert_no_program_enrollment(self.user_1, self.program)
# assert that all of user_1's course enrollments as part of the program
# are inactive
for course_enrollment in course_enrollments_for_user_1:
course_enrollment.refresh_from_db()
assert not course_enrollment.is_active
# assert that user_1's course enrollments are in the expected mode
# after unlinking
course_keys_to_mode = {
self.fruit_course: 'audit',
self.animal_course: 'masters',
}
self._assert_course_enrollments_in_mode(course_enrollments_for_user_1, course_keys_to_mode)
# assert that user_2 has been successfully linked to the program
self._assert_program_enrollment(self.user_2, self.program, '0001')
self._assert_user_enrolled_in_program_courses(self.user_2, self.program, self.fruit_course, self.animal_course)
class TestLinkProgramEnrollmentsErrors(TestLinkProgramEnrollmentsMixin, TestCase):
""" Tests for linking error behavior """
@@ -285,7 +366,7 @@ class TestLinkProgramEnrollmentsErrors(TestLinkProgramEnrollmentsMixin, TestCase
)
expected_error_msg = NO_PROGRAM_ENROLLMENT_TEMPLATE.format(
program_uuid=self.program,
external_student_key='0002'
external_user_key='0002'
)
logger.check_present((LOG_PATH, 'WARNING', expected_error_msg))
@@ -337,34 +418,6 @@ class TestLinkProgramEnrollmentsErrors(TestLinkProgramEnrollmentsMixin, TestCase
self._assert_program_enrollment(self.user_1, self.program, '0001')
self._assert_program_enrollment(self.user_2, self.program, '0002')
def test_enrollment_already_linked_to_different_user(self):
self._create_waiting_enrollment(self.program, '0001')
enrollment = ProgramEnrollmentFactory.create(
program_uuid=self.program,
external_user_key='0003',
)
user_3 = enrollment.user
self._assert_no_program_enrollment(self.user_1, self.program, refresh=False)
self._assert_no_program_enrollment(self.user_2, self.program, refresh=False)
self._assert_program_enrollment(user_3, self.program, '0003', refresh=False)
with LogCapture() as logger:
errors = link_program_enrollments(
self.program,
{
'0001': self.user_1.username,
'0003': self.user_2.username,
}
)
expected_error_msg = _user_already_linked_message(enrollment, self.user_2)
logger.check_present((LOG_PATH, 'WARNING', expected_error_msg))
self.assertDictEqual(errors, {'0003': expected_error_msg})
self._assert_program_enrollment(self.user_1, self.program, '0001')
self._assert_no_program_enrollment(self.user_2, self.program)
self._assert_program_enrollment(user_3, self.program, '0003')
def test_error_enrolling_in_course(self):
nonexistant_course = CourseKey.from_string('course-v1:edX+Zilch+Bupkis')