Commit 4131aa4d by Renzo Lucioni

Allow enrollments in expired modes to be deactivated

Facilitates revocation of enrollments in expired modes. XCOM-490.
parent 40f89473
...@@ -137,9 +137,11 @@ def add_enrollment(user_id, course_id, mode='honor', is_active=True): ...@@ -137,9 +137,11 @@ def add_enrollment(user_id, course_id, mode='honor', is_active=True):
Enrolls a user in a course. If the mode is not specified, this will default to 'honor'. Enrolls a user in a course. If the mode is not specified, this will default to 'honor'.
Args: Arguments:
user_id (str): The user to enroll. user_id (str): The user to enroll.
course_id (str): The course to enroll the user in. course_id (str): The course to enroll the user in.
Keyword Arguments:
mode (str): Optional argument for the type of enrollment to create. Ex. 'audit', 'honor', 'verified', mode (str): Optional argument for the type of enrollment to create. Ex. 'audit', 'honor', 'verified',
'professional'. If not specified, this defaults to 'honor'. 'professional'. If not specified, this defaults to 'honor'.
is_active (boolean): Optional argument for making the new enrollment inactive. If not specified, is_active is_active (boolean): Optional argument for making the new enrollment inactive. If not specified, is_active
...@@ -177,7 +179,7 @@ def add_enrollment(user_id, course_id, mode='honor', is_active=True): ...@@ -177,7 +179,7 @@ def add_enrollment(user_id, course_id, mode='honor', is_active=True):
} }
} }
""" """
_validate_course_mode(course_id, mode) _validate_course_mode(course_id, mode, is_active=is_active)
return _data_api().create_course_enrollment(user_id, course_id, mode, is_active) return _data_api().create_course_enrollment(user_id, course_id, mode, is_active)
...@@ -186,11 +188,14 @@ def update_enrollment(user_id, course_id, mode=None, is_active=None, enrollment_ ...@@ -186,11 +188,14 @@ def update_enrollment(user_id, course_id, mode=None, is_active=None, enrollment_
Update a course enrollment for the given user and course. Update a course enrollment for the given user and course.
Args: Arguments:
user_id (str): The user associated with the updated enrollment. user_id (str): The user associated with the updated enrollment.
course_id (str): The course associated with the updated enrollment. course_id (str): The course associated with the updated enrollment.
Keyword Arguments:
mode (str): The new course mode for this enrollment. mode (str): The new course mode for this enrollment.
is_active (bool): Sets whether the enrollment is active or not. is_active (bool): Sets whether the enrollment is active or not.
enrollment_attributes (list): Attributes to be set the enrollment.
Returns: Returns:
A serializable dictionary representing the updated enrollment. A serializable dictionary representing the updated enrollment.
...@@ -226,7 +231,7 @@ def update_enrollment(user_id, course_id, mode=None, is_active=None, enrollment_ ...@@ -226,7 +231,7 @@ def update_enrollment(user_id, course_id, mode=None, is_active=None, enrollment_
""" """
if mode is not None: if mode is not None:
_validate_course_mode(course_id, mode) _validate_course_mode(course_id, mode, is_active=is_active)
enrollment = _data_api().update_course_enrollment(user_id, course_id, mode=mode, is_active=is_active) enrollment = _data_api().update_course_enrollment(user_id, course_id, mode=mode, is_active=is_active)
if enrollment is None: if enrollment is None:
msg = u"Course Enrollment not found for user {user} in course {course}".format(user=user_id, course=course_id) msg = u"Course Enrollment not found for user {user} in course {course}".format(user=user_id, course=course_id)
...@@ -353,7 +358,7 @@ def get_enrollment_attributes(user_id, course_id): ...@@ -353,7 +358,7 @@ def get_enrollment_attributes(user_id, course_id):
return _data_api().get_enrollment_attributes(user_id, course_id) return _data_api().get_enrollment_attributes(user_id, course_id)
def _validate_course_mode(course_id, mode): def _validate_course_mode(course_id, mode, is_active=None):
"""Checks to see if the specified course mode is valid for the course. """Checks to see if the specified course mode is valid for the course.
If the requested course mode is not available for the course, raise an error with corresponding If the requested course mode is not available for the course, raise an error with corresponding
...@@ -363,17 +368,24 @@ def _validate_course_mode(course_id, mode): ...@@ -363,17 +368,24 @@ def _validate_course_mode(course_id, mode):
'honor', return true, allowing the enrollment to be 'honor' even if the mode is not explicitly 'honor', return true, allowing the enrollment to be 'honor' even if the mode is not explicitly
set for the course. set for the course.
Args: Arguments:
course_id (str): The course to check against for available course modes. course_id (str): The course to check against for available course modes.
mode (str): The slug for the course mode specified in the enrollment. mode (str): The slug for the course mode specified in the enrollment.
Keyword Arguments:
is_active (bool): Whether the enrollment is to be activated or deactivated.
Returns: Returns:
None None
Raises: Raises:
CourseModeNotFound: raised if the course mode is not found. CourseModeNotFound: raised if the course mode is not found.
""" """
course_enrollment_info = _data_api().get_course_enrollment_info(course_id) # If the client has requested an enrollment deactivation, we want to include expired modes
# in the set of available modes. This allows us to unenroll users from expired modes.
include_expired = not is_active if is_active is not None else False
course_enrollment_info = _data_api().get_course_enrollment_info(course_id, include_expired=include_expired)
course_modes = course_enrollment_info["course_modes"] course_modes = course_enrollment_info["course_modes"]
available_modes = [m['slug'] for m in course_modes] available_modes = [m['slug'] for m in course_modes]
if mode not in available_modes: if mode not in available_modes:
......
...@@ -40,7 +40,11 @@ class CourseField(serializers.RelatedField): ...@@ -40,7 +40,11 @@ class CourseField(serializers.RelatedField):
def to_native(self, course, **kwargs): def to_native(self, course, **kwargs):
course_modes = ModeSerializer( course_modes = ModeSerializer(
CourseMode.modes_for_course(course.id, kwargs.get('include_expired', False), only_selectable=False) CourseMode.modes_for_course(
course.id,
include_expired=kwargs.get('include_expired', False),
only_selectable=False
)
).data # pylint: disable=no-member ).data # pylint: disable=no-member
return { return {
......
...@@ -18,6 +18,7 @@ from django.conf import settings ...@@ -18,6 +18,7 @@ from django.conf import settings
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, check_mongo_calls_range from xmodule.modulestore.tests.factories import CourseFactory, check_mongo_calls_range
from django.test.utils import override_settings from django.test.utils import override_settings
import pytz
from course_modes.models import CourseMode from course_modes.models import CourseMode
from embargo.models import CountryAccessRule, Country, RestrictedCourse from embargo.models import CountryAccessRule, Country, RestrictedCourse
...@@ -716,6 +717,26 @@ class EnrollmentTest(EnrollmentTestMixin, ModuleStoreTestCase, APITestCase): ...@@ -716,6 +717,26 @@ class EnrollmentTest(EnrollmentTestMixin, ModuleStoreTestCase, APITestCase):
expected_status=expected_status, expected_status=expected_status,
) )
def test_deactivate_enrollment_expired_mode(self):
"""Verify that an enrollment in an expired mode can be deactivated."""
for mode in (CourseMode.HONOR, CourseMode.VERIFIED):
CourseModeFactory.create(
course_id=self.course.id,
mode_slug=mode,
mode_display_name=mode,
)
# Create verified enrollment.
self.assert_enrollment_status(as_server=True, mode=CourseMode.VERIFIED)
# Change verified mode expiration.
mode = CourseMode.objects.get(course_id=self.course.id, mode_slug=CourseMode.VERIFIED)
mode.expiration_datetime = datetime.datetime(year=1970, month=1, day=1, tzinfo=pytz.utc)
mode.save()
# Deactivate enrollment.
self.assert_enrollment_activation(False, CourseMode.VERIFIED)
def test_change_mode_from_user(self): def test_change_mode_from_user(self):
"""Users should not be able to alter the enrollment mode on an enrollment. """ """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. # Create an honor and verified mode for a course. This allows an update.
......
...@@ -598,7 +598,7 @@ class EnrollmentListView(APIView, ApiKeyPermissionMixIn): ...@@ -598,7 +598,7 @@ class EnrollmentListView(APIView, ApiKeyPermissionMixIn):
data={ data={
"message": ( "message": (
u"The course mode '{mode}' is not available for course '{course_id}'." u"The course mode '{mode}' is not available for course '{course_id}'."
).format(mode="honor", course_id=course_id), ).format(mode=mode, course_id=course_id),
"course_details": error.data "course_details": error.data
}) })
except CourseNotFoundError: except CourseNotFoundError:
......
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