diff --git a/common/djangoapps/enrollment/api.py b/common/djangoapps/enrollment/api.py index 07c3b2ec87..f9edd9e414 100644 --- a/common/djangoapps/enrollment/api.py +++ b/common/djangoapps/enrollment/api.py @@ -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'. - Args: + Arguments: user_id (str): The user to enroll. 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', 'professional'. If not specified, this defaults to 'honor'. 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): } } """ - _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) @@ -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. - Args: + Arguments: user_id (str): The user 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. is_active (bool): Sets whether the enrollment is active or not. + enrollment_attributes (list): Attributes to be set the enrollment. Returns: A serializable dictionary representing the updated enrollment. @@ -226,7 +231,7 @@ def update_enrollment(user_id, course_id, mode=None, is_active=None, enrollment_ """ 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) if enrollment is None: 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): 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. 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): 'honor', return true, allowing the enrollment to be 'honor' even if the mode is not explicitly set for the course. - Args: + Arguments: course_id (str): The course to check against for available course modes. 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: None Raises: 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"] available_modes = [m['slug'] for m in course_modes] if mode not in available_modes: diff --git a/common/djangoapps/enrollment/serializers.py b/common/djangoapps/enrollment/serializers.py index d9e6ce7918..cf24a97b96 100644 --- a/common/djangoapps/enrollment/serializers.py +++ b/common/djangoapps/enrollment/serializers.py @@ -40,7 +40,11 @@ class CourseField(serializers.RelatedField): def to_native(self, course, **kwargs): 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 return { diff --git a/common/djangoapps/enrollment/tests/test_views.py b/common/djangoapps/enrollment/tests/test_views.py index e8cd16b936..4960c273b4 100644 --- a/common/djangoapps/enrollment/tests/test_views.py +++ b/common/djangoapps/enrollment/tests/test_views.py @@ -18,6 +18,7 @@ from django.conf import settings from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory, check_mongo_calls_range from django.test.utils import override_settings +import pytz from course_modes.models import CourseMode from embargo.models import CountryAccessRule, Country, RestrictedCourse @@ -716,6 +717,26 @@ class EnrollmentTest(EnrollmentTestMixin, ModuleStoreTestCase, APITestCase): 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): """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. diff --git a/common/djangoapps/enrollment/views.py b/common/djangoapps/enrollment/views.py index b558828e7e..faeb4d9369 100644 --- a/common/djangoapps/enrollment/views.py +++ b/common/djangoapps/enrollment/views.py @@ -598,7 +598,7 @@ class EnrollmentListView(APIView, ApiKeyPermissionMixIn): data={ "message": ( 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 }) except CourseNotFoundError: