Commit 50f920be by Renzo Lucioni

Merge pull request #9135 from edx/renzo/always-allow-unenrollment

Allow enrollments in expired modes to be deactivated
parents 614baab3 4131aa4d
......@@ -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:
......
......@@ -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 {
......
......@@ -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.
......
......@@ -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:
......
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