Allow enrollment API to deactivate enrollments
Will allow Otto to revoke fulfillment of course seat products. Only server-to-server calls are currently allowed to deactivate or otherwise modify existing enrollments.
This commit is contained in:
@@ -225,7 +225,8 @@ def update_enrollment(user_id, course_id, mode=None, is_active=None):
|
||||
}
|
||||
|
||||
"""
|
||||
_validate_course_mode(course_id, mode)
|
||||
if mode is not None:
|
||||
_validate_course_mode(course_id, mode)
|
||||
enrollment = _data_api().update_course_enrollment(user_id, course_id, mode=mode, is_active=is_active)
|
||||
if enrollment is None:
|
||||
msg = u"Course Enrollment not found for user {user} in course {course}".format(user=user_id, course=course_id)
|
||||
|
||||
@@ -43,6 +43,7 @@ class EnrollmentTestMixin(object):
|
||||
email_opt_in=None,
|
||||
as_server=False,
|
||||
mode=CourseMode.HONOR,
|
||||
is_active=None,
|
||||
):
|
||||
"""
|
||||
Enroll in the course and verify the response's status code. If the expected status is 200, also validates
|
||||
@@ -61,6 +62,10 @@ class EnrollmentTestMixin(object):
|
||||
},
|
||||
'user': username
|
||||
}
|
||||
|
||||
if is_active is not None:
|
||||
data['is_active'] = is_active
|
||||
|
||||
if email_opt_in is not None:
|
||||
data['email_opt_in'] = email_opt_in
|
||||
|
||||
@@ -72,14 +77,32 @@ class EnrollmentTestMixin(object):
|
||||
response = self.client.post(url, json.dumps(data), content_type='application/json', **extra)
|
||||
self.assertEqual(response.status_code, expected_status)
|
||||
|
||||
if expected_status in [status.HTTP_200_OK, status.HTTP_200_OK]:
|
||||
if expected_status == status.HTTP_200_OK:
|
||||
data = json.loads(response.content)
|
||||
self.assertEqual(course_id, data['course_details']['course_id'])
|
||||
self.assertEqual(mode, data['mode'])
|
||||
self.assertTrue(data['is_active'])
|
||||
|
||||
if mode is not None:
|
||||
self.assertEqual(mode, data['mode'])
|
||||
|
||||
if is_active is not None:
|
||||
self.assertEqual(is_active, data['is_active'])
|
||||
else:
|
||||
self.assertTrue(data['is_active'])
|
||||
|
||||
return response
|
||||
|
||||
def assert_enrollment_activation(self, expected_activation, expected_mode=CourseMode.VERIFIED):
|
||||
"""Change an enrollment's activation and verify its activation and mode are as expected."""
|
||||
self.assert_enrollment_status(
|
||||
as_server=True,
|
||||
mode=None,
|
||||
is_active=expected_activation,
|
||||
expected_status=status.HTTP_200_OK
|
||||
)
|
||||
actual_mode, actual_activation = CourseEnrollment.enrollment_mode_for_user(self.user, self.course.id)
|
||||
self.assertEqual(actual_activation, expected_activation)
|
||||
self.assertEqual(actual_mode, expected_mode)
|
||||
|
||||
|
||||
@override_settings(EDX_API_KEY="i am a key")
|
||||
@ddt.ddt
|
||||
@@ -503,6 +526,39 @@ class EnrollmentTest(EnrollmentTestMixin, ModuleStoreTestCase, APITestCase):
|
||||
self.assertTrue(is_active)
|
||||
self.assertEqual(course_mode, CourseMode.HONOR)
|
||||
|
||||
def test_deactivate_enrollment(self):
|
||||
"""With the right API key, deactivate (i.e., unenroll from) an existing enrollment."""
|
||||
# Create an honor and verified mode for a course. This allows an update.
|
||||
for mode in [CourseMode.HONOR, CourseMode.VERIFIED]:
|
||||
CourseModeFactory.create(
|
||||
course_id=self.course.id,
|
||||
mode_slug=mode,
|
||||
mode_display_name=mode,
|
||||
)
|
||||
|
||||
# Create a 'verified' enrollment
|
||||
self.assert_enrollment_status(as_server=True, mode=CourseMode.VERIFIED)
|
||||
|
||||
# Check that the enrollment is 'verified' and active.
|
||||
self.assertTrue(CourseEnrollment.is_enrolled(self.user, self.course.id))
|
||||
course_mode, is_active = CourseEnrollment.enrollment_mode_for_user(self.user, self.course.id)
|
||||
self.assertTrue(is_active)
|
||||
self.assertEqual(course_mode, CourseMode.VERIFIED)
|
||||
|
||||
# Verify that a non-Boolean enrollment status is treated as invalid.
|
||||
self.assert_enrollment_status(
|
||||
as_server=True,
|
||||
mode=None,
|
||||
is_active='foo',
|
||||
expected_status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
# Verify that the enrollment has been deactivated, and that the mode is unchanged.
|
||||
self.assert_enrollment_activation(False)
|
||||
|
||||
# Verify that enrollment deactivation is idempotent.
|
||||
self.assert_enrollment_activation(False)
|
||||
|
||||
def test_change_mode_from_user(self):
|
||||
"""Users should not be able to alter the enrollment mode on an enrollment. """
|
||||
# Create an honor and verified mode for a course. This allows an update.
|
||||
|
||||
@@ -257,13 +257,16 @@ class EnrollmentListView(APIView, ApiKeyPermissionMixIn):
|
||||
* user: The user ID of the currently logged in user. Optional. You cannot use the command to enroll a different user.
|
||||
|
||||
* mode: The Course Mode for the enrollment. Individual users cannot upgrade their enrollment mode from
|
||||
'honor'. Only server to server requests can enroll with other modes. Optional.
|
||||
'honor'. Only server-to-server requests can enroll with other modes. Optional.
|
||||
|
||||
* is_active: A Boolean indicating whether the enrollment is active. Only server-to-server requests are
|
||||
allowed to deactivate an enrollment. Optional.
|
||||
|
||||
* course details: A collection that contains:
|
||||
|
||||
* course_id: The unique identifier for the course.
|
||||
|
||||
* email_opt_in: A boolean indicating whether the user
|
||||
* email_opt_in: A Boolean indicating whether the user
|
||||
wishes to opt into email from the organization running this course. Optional.
|
||||
|
||||
**Response Values**
|
||||
@@ -313,9 +316,7 @@ class EnrollmentListView(APIView, ApiKeyPermissionMixIn):
|
||||
# cross-domain CSRF.
|
||||
@method_decorator(ensure_csrf_cookie_cross_domain)
|
||||
def get(self, request):
|
||||
"""
|
||||
Gets a list of all course enrollments for the currently logged in user.
|
||||
"""
|
||||
"""Gets a list of all course enrollments for the currently logged in user."""
|
||||
username = request.GET.get('user', request.user.username)
|
||||
if request.user.username != username and not self.has_api_key_permissions(request):
|
||||
# Return a 404 instead of a 403 (Unauthorized). If one user is looking up
|
||||
@@ -334,8 +335,10 @@ class EnrollmentListView(APIView, ApiKeyPermissionMixIn):
|
||||
)
|
||||
|
||||
def post(self, request):
|
||||
"""
|
||||
Enrolls the currently logged in user in a course.
|
||||
"""Enrolls the currently logged-in user in a course.
|
||||
|
||||
Server-to-server calls may deactivate or modify the mode of existing enrollments. All other requests
|
||||
go through `add_enrollment()`, which allows creation of new and reactivation of old enrollments.
|
||||
"""
|
||||
# Get the User, Course ID, and Mode from the request.
|
||||
username = request.DATA.get('user', request.user.username)
|
||||
@@ -407,22 +410,28 @@ class EnrollmentListView(APIView, ApiKeyPermissionMixIn):
|
||||
)
|
||||
|
||||
try:
|
||||
# Check if the user is currently enrolled, and if it is the same as the current enrolled mode. We do not
|
||||
# have to check if it is inactive or not, because if it is, we are still upgrading if the mode is different,
|
||||
# and either path will re-activate the enrollment.
|
||||
#
|
||||
# Only server-to-server calls will currently be allowed to modify the mode for existing enrollments. All
|
||||
# other requests will go through add_enrollment(), which will allow creating of new enrollments, and
|
||||
# re-activating enrollments
|
||||
is_active = request.DATA.get('is_active')
|
||||
# Check if the requested activation status is None or a Boolean
|
||||
if is_active is not None and not isinstance(is_active, bool):
|
||||
return Response(
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
data={
|
||||
'message': (u"'{value}' is an invalid enrollment activation status.").format(value=is_active)
|
||||
}
|
||||
)
|
||||
|
||||
enrollment = api.get_enrollment(username, unicode(course_id))
|
||||
if has_api_key_permissions and enrollment and enrollment['mode'] != mode:
|
||||
response = api.update_enrollment(username, unicode(course_id), mode=mode)
|
||||
response = api.update_enrollment(username, unicode(course_id), mode=mode, is_active=is_active)
|
||||
else:
|
||||
# Will reactivate inactive enrollments.
|
||||
response = api.add_enrollment(username, unicode(course_id), mode=mode)
|
||||
|
||||
email_opt_in = request.DATA.get('email_opt_in', None)
|
||||
if email_opt_in is not None:
|
||||
org = course_id.org
|
||||
update_email_opt_in(request.user, org, email_opt_in)
|
||||
|
||||
return Response(response)
|
||||
except CourseModeNotFoundError as error:
|
||||
return Response(
|
||||
|
||||
Reference in New Issue
Block a user