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:
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Python API function to link program enrollments and external_student_keys to an
|
||||
Python API function to link program enrollments and external_user_keys to an
|
||||
LMS user.
|
||||
|
||||
Outside of this subpackage, import these functions
|
||||
@@ -12,6 +12,7 @@ import logging
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.db import IntegrityError, transaction
|
||||
|
||||
from course_modes.models import CourseMode
|
||||
from student.api import get_access_role_by_role_name
|
||||
from student.models import CourseEnrollmentException
|
||||
|
||||
@@ -24,16 +25,15 @@ User = get_user_model()
|
||||
|
||||
NO_PROGRAM_ENROLLMENT_TEMPLATE = (
|
||||
'No program enrollment found for program uuid={program_uuid} and external student '
|
||||
'key={external_student_key}'
|
||||
'key={external_user_key}'
|
||||
)
|
||||
NO_LMS_USER_TEMPLATE = 'No user found with username {}'
|
||||
EXISTING_USER_TEMPLATE = (
|
||||
'Program enrollment with external_student_key={external_student_key} is already linked to '
|
||||
'Program enrollment with external_student_key={external_user_key} is already linked to '
|
||||
'{account_relation} account username={username}'
|
||||
)
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def link_program_enrollments(program_uuid, external_keys_to_usernames):
|
||||
"""
|
||||
Utility function to link ProgramEnrollments to LMS Users
|
||||
@@ -55,11 +55,20 @@ def link_program_enrollments(program_uuid, external_keys_to_usernames):
|
||||
For each external_user_key:lms_username, if:
|
||||
- The user is not found
|
||||
- No enrollment is found for the given program and external_user_key
|
||||
- The enrollment already has a user
|
||||
- The enrollment already has a user and that user is the same as the given user
|
||||
An error message will be logged, and added to a dictionary of error messages keyed by
|
||||
external_key. The input will be skipped. All other inputs will be processed and
|
||||
enrollments updated, and then the function will return the dictionary of error messages.
|
||||
|
||||
For each external_user_key:lms_username, if the enrollment already has a user, but that user
|
||||
is different than the requested user, we do the following. We unlink the existing user from
|
||||
the program enrollment and link the requested user to the program enrollment. This is accomplished by
|
||||
removing the existing user's link to the program enrollment. If the program enrollment
|
||||
has course enrollments, then we unenroll the user. If there is an audit track in the course,
|
||||
we also move the enrollment into the audit track. We also remove the association between those
|
||||
course enrollments and the program course enrollments. The
|
||||
requested user is then linked to the program following the above logic.
|
||||
|
||||
If there is an error while enrolling a user in a waiting program course enrollment, the
|
||||
error will be logged, and added to the returned error dictionary, and we will roll back all
|
||||
transactions for that user so that their db state will be the same as it was before this
|
||||
@@ -72,37 +81,56 @@ def link_program_enrollments(program_uuid, external_keys_to_usernames):
|
||||
program_uuid, external_keys_to_usernames.keys()
|
||||
)
|
||||
users_by_username = _get_lms_users(external_keys_to_usernames.values())
|
||||
for external_student_key, username in external_keys_to_usernames.items():
|
||||
program_enrollment = program_enrollments.get(external_student_key)
|
||||
for external_user_key, username in external_keys_to_usernames.items():
|
||||
program_enrollment = program_enrollments.get(external_user_key)
|
||||
user = users_by_username.get(username)
|
||||
if not user:
|
||||
error_message = NO_LMS_USER_TEMPLATE.format(username)
|
||||
elif not program_enrollment:
|
||||
error_message = NO_PROGRAM_ENROLLMENT_TEMPLATE.format(
|
||||
program_uuid=program_uuid,
|
||||
external_student_key=external_student_key
|
||||
external_user_key=external_user_key
|
||||
)
|
||||
elif program_enrollment.user:
|
||||
# if we're trying to establish a link that already exists
|
||||
elif program_enrollment.user and program_enrollment.user == user:
|
||||
error_message = _user_already_linked_message(program_enrollment, user)
|
||||
else:
|
||||
error_message = None
|
||||
if error_message:
|
||||
logger.warning(error_message)
|
||||
errors[external_student_key] = error_message
|
||||
errors[external_user_key] = error_message
|
||||
continue
|
||||
try:
|
||||
with transaction.atomic():
|
||||
# If the ProgramEnrollment already has a linked edX user that is different than
|
||||
# the requested user, then we should sever the link to the existing edX user before
|
||||
# linking the ProgramEnrollment to the new user.
|
||||
if program_enrollment.user and program_enrollment.user != user:
|
||||
message = ('Unlinking user with username={old_username} from program enrollment with '
|
||||
'program uuid={program_uuid} with external_student_key={external_user_key} '
|
||||
'and linking user with username={new_username} '
|
||||
'to program enrollment.').format(
|
||||
old_username=program_enrollment.user.username,
|
||||
program_uuid=program_uuid,
|
||||
external_user_key=external_user_key,
|
||||
new_username=user,
|
||||
)
|
||||
logger.info(_user_already_linked_message(program_enrollment, user))
|
||||
logger.info(message)
|
||||
|
||||
unlink_program_enrollment(program_enrollment)
|
||||
|
||||
link_program_enrollment_to_lms_user(program_enrollment, user)
|
||||
except (CourseEnrollmentException, IntegrityError) as e:
|
||||
logger.exception("Rolling back all operations for {}:{}".format(
|
||||
external_student_key,
|
||||
external_user_key,
|
||||
username,
|
||||
))
|
||||
error_message = type(e).__name__
|
||||
if str(e):
|
||||
error_message += ': '
|
||||
error_message += str(e)
|
||||
errors[external_student_key] = error_message
|
||||
errors[external_user_key] = error_message
|
||||
return errors
|
||||
|
||||
|
||||
@@ -111,22 +139,22 @@ def _user_already_linked_message(program_enrollment, user):
|
||||
Creates an error message that the specified program enrollment is already linked to an lms user
|
||||
"""
|
||||
existing_username = program_enrollment.user.username
|
||||
external_student_key = program_enrollment.external_user_key
|
||||
external_user_key = program_enrollment.external_user_key
|
||||
return EXISTING_USER_TEMPLATE.format(
|
||||
external_student_key=external_student_key,
|
||||
external_user_key=external_user_key,
|
||||
account_relation='target' if program_enrollment.user.id == user.id else 'a different',
|
||||
username=existing_username,
|
||||
)
|
||||
|
||||
|
||||
def _get_program_enrollments_by_ext_key(program_uuid, external_student_keys):
|
||||
def _get_program_enrollments_by_ext_key(program_uuid, external_user_keys):
|
||||
"""
|
||||
Does a bulk read of ProgramEnrollments for a given program and list of external student keys
|
||||
and returns a dict keyed by external student key
|
||||
"""
|
||||
program_enrollments = fetch_program_enrollments(
|
||||
program_uuid=program_uuid,
|
||||
external_user_keys=external_student_keys,
|
||||
external_user_keys=external_user_keys,
|
||||
).prefetch_related(
|
||||
'program_course_enrollments'
|
||||
).select_related('user')
|
||||
@@ -146,6 +174,45 @@ def _get_lms_users(lms_usernames):
|
||||
}
|
||||
|
||||
|
||||
def unlink_program_enrollment(program_enrollment):
|
||||
"""
|
||||
Unlinks CourseEnrollments from the ProgramEnrollment by doing the following for
|
||||
each ProgramCourseEnrollment associated with the Program Enrollment.
|
||||
1. unenrolling the corresponding user from the course
|
||||
2. moving the user into the audit track, if the track exists
|
||||
3. removing the link between the ProgramCourseEnrollment and the CourseEnrollment
|
||||
|
||||
Arguments:
|
||||
program_enrollment: the ProgramEnrollment object
|
||||
"""
|
||||
program_course_enrollments = program_enrollment.program_course_enrollments.all()
|
||||
|
||||
for pce in program_course_enrollments:
|
||||
course_key = pce.course_enrollment.course.id
|
||||
modes = CourseMode.modes_for_course_dict(course_key)
|
||||
|
||||
update_enrollment_kwargs = {
|
||||
'is_active': False,
|
||||
'skip_refund': True,
|
||||
}
|
||||
|
||||
if CourseMode.contains_audit_mode(modes):
|
||||
# if the course contains an audit mode, move the
|
||||
# learner's enrollment into the audit mode
|
||||
update_enrollment_kwargs['mode'] = 'audit'
|
||||
|
||||
# deactive the learner's course enrollment and move them into the
|
||||
# audit track, if it exists
|
||||
pce.course_enrollment.update_enrollment(**update_enrollment_kwargs)
|
||||
|
||||
# sever ties to the user from the ProgramCourseEnrollment
|
||||
pce.course_enrollment = None
|
||||
pce.save()
|
||||
|
||||
program_enrollment.user = None
|
||||
program_enrollment.save()
|
||||
|
||||
|
||||
def link_program_enrollment_to_lms_user(program_enrollment, user):
|
||||
"""
|
||||
Attempts to link the given program enrollment to the given user
|
||||
|
||||
Reference in New Issue
Block a user