Commit 960b02cf by Renzo Lucioni

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.
parent 41363ead
......@@ -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(
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment