feat: force enrollment after enrollment end date via API (#31309)
It includes support for course enrollment in case of enrollment_end date has passed or the upgrade_deadline has passed. The force_enrollment argument is used to support this functionality, and can_upgrade and include_expired will be True if force_enrollment is True. Only a user who has GlobalSupport access can perform this operation.
This commit is contained in:
@@ -199,7 +199,16 @@ def get_enrollment(username, course_id):
|
||||
return _data_api().get_course_enrollment(username, course_id)
|
||||
|
||||
|
||||
def add_enrollment(username, course_id, mode=None, is_active=True, enrollment_attributes=None, enterprise_uuid=None):
|
||||
def add_enrollment(
|
||||
username,
|
||||
course_id,
|
||||
mode=None,
|
||||
is_active=True,
|
||||
enrollment_attributes=None,
|
||||
enterprise_uuid=None,
|
||||
force_enrollment=False,
|
||||
include_expired=False
|
||||
):
|
||||
"""Enrolls a user in a course.
|
||||
|
||||
Enrolls a user in a course. If the mode is not specified, this will default to `CourseMode.DEFAULT_MODE_SLUG`.
|
||||
@@ -213,6 +222,8 @@ def add_enrollment(username, course_id, mode=None, is_active=True, enrollment_at
|
||||
defaults to True.
|
||||
enrollment_attributes (list): Attributes to be set the enrollment.
|
||||
enterprise_uuid (str): Add course enterprise uuid
|
||||
force_enrollment (bool): Enroll user even if course enrollment_end date is expired
|
||||
include_expired (bool): Boolean denoting whether expired course modes should be included.
|
||||
|
||||
Returns:
|
||||
A serializable dictionary of the new course enrollment.
|
||||
@@ -250,8 +261,10 @@ def add_enrollment(username, course_id, mode=None, is_active=True, enrollment_at
|
||||
"""
|
||||
if mode is None:
|
||||
mode = _default_course_mode(course_id)
|
||||
validate_course_mode(course_id, mode, is_active=is_active)
|
||||
enrollment = _data_api().create_course_enrollment(username, course_id, mode, is_active, enterprise_uuid)
|
||||
validate_course_mode(course_id, mode, is_active=is_active, include_expired=include_expired)
|
||||
enrollment = _data_api().create_course_enrollment(
|
||||
username, course_id, mode, is_active, enterprise_uuid, force_enrollment=force_enrollment
|
||||
)
|
||||
|
||||
if enrollment_attributes is not None:
|
||||
set_enrollment_attributes(username, course_id, enrollment_attributes)
|
||||
|
||||
@@ -115,7 +115,7 @@ def get_user_enrollments(course_key):
|
||||
).order_by('created')
|
||||
|
||||
|
||||
def create_course_enrollment(username, course_id, mode, is_active, enterprise_uuid=None):
|
||||
def create_course_enrollment(username, course_id, mode, is_active, enterprise_uuid=None, force_enrollment=False):
|
||||
"""Create a new course enrollment for the given user.
|
||||
|
||||
Creates a new course enrollment for the specified user username.
|
||||
@@ -126,6 +126,7 @@ def create_course_enrollment(username, course_id, mode, is_active, enterprise_uu
|
||||
mode (str): (Optional) The mode for the new enrollment.
|
||||
is_active (boolean): (Optional) Determines if the enrollment is active.
|
||||
enterprise_uuid (str): Add course enterprise uuid
|
||||
force_enrollment (bool): Enroll user even if course enrollment_end date is expired
|
||||
|
||||
Returns:
|
||||
A serializable dictionary representing the new course enrollment.
|
||||
@@ -147,7 +148,9 @@ def create_course_enrollment(username, course_id, mode, is_active, enterprise_uu
|
||||
raise UserNotFoundError(msg) # lint-amnesty, pylint: disable=raise-missing-from
|
||||
|
||||
try:
|
||||
enrollment = CourseEnrollment.enroll(user, course_key, check_access=True, enterprise_uuid=enterprise_uuid)
|
||||
enrollment = CourseEnrollment.enroll(
|
||||
user, course_key, check_access=True, can_upgrade=force_enrollment, enterprise_uuid=enterprise_uuid
|
||||
)
|
||||
return _update_enrollment(enrollment, is_active=is_active, mode=mode)
|
||||
except NonExistentCourseError as err:
|
||||
raise CourseNotFoundError(str(err)) # lint-amnesty, pylint: disable=raise-missing-from
|
||||
|
||||
@@ -36,9 +36,18 @@ def get_course_enrollment(student_id, course_id):
|
||||
return _get_fake_enrollment(student_id, course_id)
|
||||
|
||||
|
||||
def create_course_enrollment(student_id, course_id, mode='honor', is_active=True, enterprise_uuid=None):
|
||||
def create_course_enrollment(
|
||||
student_id, course_id, mode='honor', is_active=True, enterprise_uuid=None, force_enrollment=False
|
||||
):
|
||||
"""Stubbed out Enrollment creation request. """
|
||||
return add_enrollment(student_id, course_id, mode=mode, is_active=is_active, enterprise_uuid=enterprise_uuid)
|
||||
return add_enrollment(
|
||||
student_id,
|
||||
course_id,
|
||||
mode=mode,
|
||||
is_active=is_active,
|
||||
enterprise_uuid=enterprise_uuid,
|
||||
force_enrollment=force_enrollment
|
||||
)
|
||||
|
||||
|
||||
def update_course_enrollment(student_id, course_id, mode=None, is_active=None):
|
||||
@@ -74,7 +83,9 @@ def _get_fake_course_info(course_id, include_expired=False):
|
||||
return course
|
||||
|
||||
|
||||
def add_enrollment(student_id, course_id, is_active=True, mode='honor', enterprise_uuid=None):
|
||||
def add_enrollment(
|
||||
student_id, course_id, is_active=True, mode='honor', enterprise_uuid=None, force_enrollment=False
|
||||
):
|
||||
"""Append an enrollment to the enrollments array."""
|
||||
enrollment = {
|
||||
"created": datetime.datetime.now(),
|
||||
|
||||
@@ -67,6 +67,7 @@ class EnrollmentTestMixin:
|
||||
max_mongo_calls=0,
|
||||
linked_enterprise_customer=None,
|
||||
cohort=None,
|
||||
force_enrollment=False,
|
||||
):
|
||||
"""
|
||||
Enroll in the course and verify the response's status code. If the expected status is 200, also validates
|
||||
@@ -84,6 +85,7 @@ class EnrollmentTestMixin:
|
||||
'course_id': course_id
|
||||
},
|
||||
'user': username,
|
||||
'force_enrollment': force_enrollment,
|
||||
'enrollment_attributes': enrollment_attributes
|
||||
}
|
||||
|
||||
@@ -228,6 +230,75 @@ class EnrollmentTest(EnrollmentTestMixin, ModuleStoreTestCase, APITestCase, Ente
|
||||
assert is_active
|
||||
assert course_mode == enrollment_mode
|
||||
|
||||
@ddt.data(
|
||||
# Default (no course modes in the database)
|
||||
# Expect that users are automatically enrolled as the default
|
||||
([CourseMode.VERIFIED], CourseMode.VERIFIED, False),
|
||||
|
||||
# Audit / Verified
|
||||
# We should always go to the "choose your course" page.
|
||||
# We should also be enrolled as the default.
|
||||
([CourseMode.VERIFIED], CourseMode.VERIFIED, True),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_force_enrollment(self, course_modes, enrollment_mode, force_enrollment):
|
||||
# Create the course modes (if any) required for this test case
|
||||
start_date = datetime.datetime(2021, 12, 1, 5, 0, 0, tzinfo=pytz.UTC)
|
||||
end_date = datetime.datetime(2022, 12, 1, 5, 0, 0, tzinfo=pytz.UTC)
|
||||
self.course = CourseFactory.create(
|
||||
emit_signals=True,
|
||||
start=start_date,
|
||||
end=end_date,
|
||||
enrollment_start=start_date,
|
||||
enrollment_end=end_date,
|
||||
)
|
||||
for mode_slug in course_modes:
|
||||
CourseModeFactory.create(
|
||||
course_id=self.course.id,
|
||||
mode_slug=mode_slug,
|
||||
mode_display_name=mode_slug,
|
||||
)
|
||||
|
||||
# If a user enroll himself in expired course
|
||||
# whether force_enrollmet is True or False
|
||||
self.assert_enrollment_status(
|
||||
mode=CourseMode.VERIFIED,
|
||||
expected_status=status.HTTP_403_FORBIDDEN,
|
||||
force_enrollment=force_enrollment,
|
||||
)
|
||||
|
||||
self.client.logout()
|
||||
AdminFactory.create(username='global_staff', email='global_staff@example.com', password=self.PASSWORD)
|
||||
self.client.login(username="global_staff", password=self.PASSWORD)
|
||||
|
||||
if force_enrollment:
|
||||
# Create an enrollment
|
||||
resp = self.assert_enrollment_status(
|
||||
username=self.USERNAME,
|
||||
mode=CourseMode.VERIFIED,
|
||||
force_enrollment=force_enrollment,
|
||||
)
|
||||
|
||||
# Verify that the response contains the correct course_name
|
||||
data = json.loads(resp.content.decode('utf-8'))
|
||||
assert self.course.display_name_with_default == data['course_details']['course_name']
|
||||
|
||||
# Verify that the enrollment was created correctly
|
||||
assert CourseEnrollment.is_enrolled(self.user, self.course.id)
|
||||
course_mode, is_active = CourseEnrollment.enrollment_mode_for_user(self.user, self.course.id)
|
||||
assert is_active
|
||||
assert course_mode == enrollment_mode
|
||||
else:
|
||||
# If a staff user enroll other user in expired course
|
||||
# This will raise the CourseEnrollmentClosedError excecption
|
||||
# and return status will be 400
|
||||
self.assert_enrollment_status(
|
||||
username=self.USERNAME,
|
||||
mode=CourseMode.VERIFIED,
|
||||
expected_status=status.HTTP_400_BAD_REQUEST,
|
||||
force_enrollment=force_enrollment,
|
||||
)
|
||||
|
||||
def test_check_enrollment(self):
|
||||
CourseModeFactory.create(
|
||||
course_id=self.course.id,
|
||||
|
||||
@@ -764,6 +764,17 @@ class EnrollmentListView(APIView, ApiKeyPermissionMixIn):
|
||||
consent_client.provide_consent(**kwargs)
|
||||
|
||||
enrollment_attributes = request.data.get('enrollment_attributes')
|
||||
force_enrollment = request.data.get('force_enrollment')
|
||||
# Check if the force enrollment status is None or a Boolean
|
||||
if force_enrollment is not None and not isinstance(force_enrollment, bool):
|
||||
return Response(
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
data={
|
||||
'message': ("'{value}' is an invalid force enrollment status.").format(value=force_enrollment)
|
||||
}
|
||||
)
|
||||
# Only a staff user role can enroll a user forcefully
|
||||
force_enrollment = force_enrollment and GlobalStaff().has_user(request.user)
|
||||
enrollment = api.get_enrollment(username, str(course_id))
|
||||
mode_changed = enrollment and mode is not None and enrollment['mode'] != mode
|
||||
active_changed = enrollment and is_active is not None and enrollment['is_active'] != is_active
|
||||
@@ -809,7 +820,10 @@ class EnrollmentListView(APIView, ApiKeyPermissionMixIn):
|
||||
mode=mode,
|
||||
is_active=is_active,
|
||||
enrollment_attributes=enrollment_attributes,
|
||||
enterprise_uuid=request.data.get('enterprise_uuid')
|
||||
enterprise_uuid=request.data.get('enterprise_uuid'),
|
||||
force_enrollment=force_enrollment,
|
||||
# If we are creating enrollment by staff user with force_enrollment, we should allow expired modes
|
||||
include_expired=force_enrollment
|
||||
)
|
||||
|
||||
cohort_name = request.data.get('cohort')
|
||||
|
||||
Reference in New Issue
Block a user