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:
Ali Raza Abbasi
2022-12-09 01:58:54 +05:00
committed by GitHub
parent 4c827b5ed5
commit 3e35e3af8d
5 changed files with 121 additions and 9 deletions

View File

@@ -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)

View File

@@ -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

View File

@@ -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(),

View File

@@ -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,

View File

@@ -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')