Skip to content

Commit

Permalink
feat: adds force enrollment checkbox in manage learners page
Browse files Browse the repository at this point in the history
  • Loading branch information
tecoholic committed May 9, 2023
1 parent d813fc4 commit fdac6f5
Show file tree
Hide file tree
Showing 7 changed files with 108 additions and 21 deletions.
6 changes: 6 additions & 0 deletions enterprise/admin/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ class ManageLearnersForm(forms.Form):
label=_("Enroll these learners in this course"), required=False,
help_text=_("To enroll learners in a course, enter a course ID."),
)
force_enrollment = forms.BooleanField(
label=_("Force Enrollment"),
help_text=_("The selected course is 'Invite Only'. Only staff can enroll learners to this course."),
required=False,
)
course_mode = forms.ChoiceField(
label=_("Course enrollment track"), required=False,
choices=BLANK_CHOICE_DASH + [
Expand Down Expand Up @@ -130,6 +135,7 @@ class Fields:
REASON = "reason"
SALES_FORCE_ID = "sales_force_id"
DISCOUNT = "discount"
FORCE_ENROLLMENT = "force_enrollment"

class CsvColumns:
"""
Expand Down
9 changes: 7 additions & 2 deletions enterprise/admin/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -676,7 +676,8 @@ def _enroll_users(
notify=True,
enrollment_reason=None,
sales_force_id=None,
discount=0.0
discount=0.0,
force_enrollment=False
):
"""
Enroll the users with the given email addresses to the course.
Expand All @@ -689,6 +690,7 @@ def _enroll_users(
mode: The enrollment mode the users will be enrolled in the course with
course_id: The ID of the course in which we want to enroll
notify: Whether to notify (by email) the users that have been enrolled
force_enrollment: Force enrollment into "Invite Only" courses
"""
pending_messages = []
paid_modes = ['verified', 'professional']
Expand All @@ -702,6 +704,7 @@ def _enroll_users(
enrollment_reason=enrollment_reason,
discount=discount,
sales_force_id=sales_force_id,
force_enrollment=force_enrollment,
)
all_successes = succeeded + pending
if notify:
Expand Down Expand Up @@ -818,6 +821,7 @@ def post(self, request, customer_uuid):
sales_force_id = manage_learners_form.cleaned_data.get(ManageLearnersForm.Fields.SALES_FORCE_ID)
course_mode = manage_learners_form.cleaned_data.get(ManageLearnersForm.Fields.COURSE_MODE)
course_id = None
force_enrollment = manage_learners_form.cleaned_data.get(ManageLearnersForm.Fields.FORCE_ENROLLMENT)

if not course_id_with_emails:
course_details = manage_learners_form.cleaned_data.get(ManageLearnersForm.Fields.COURSE) or {}
Expand All @@ -832,7 +836,8 @@ def post(self, request, customer_uuid):
notify=notify,
enrollment_reason=manual_enrollment_reason,
sales_force_id=sales_force_id,
discount=discount
discount=discount,
force_enrollment=force_enrollment,
)
else:
for course_id, emails in course_id_with_emails.items():
Expand Down
6 changes: 4 additions & 2 deletions enterprise/api_client/lms.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ def has_course_mode(self, course_run_id, mode):
course_modes = self.get_course_modes(course_run_id)
return any(course_mode for course_mode in course_modes if course_mode['slug'] == mode)

def enroll_user_in_course(self, username, course_id, mode, cohort=None, enterprise_uuid=None):
def enroll_user_in_course(self, username, course_id, mode, cohort=None, enterprise_uuid=None, force_enrollment=False):
"""
Call the enrollment API to enroll the user in the course specified by course_id.
Expand All @@ -138,6 +138,7 @@ def enroll_user_in_course(self, username, course_id, mode, cohort=None, enterpri
mode (str): The enrollment mode which should be used for the enrollment
cohort (str): Add the user to this named cohort
enterprise_uuid (str): Add course enterprise uuid
force_enrollment (bool): Force the enrollment even if course is Invite Only
Returns:
dict: A dictionary containing details of the enrollment, including course details, mode, username, etc.
Expand All @@ -152,7 +153,8 @@ def enroll_user_in_course(self, username, course_id, mode, cohort=None, enterpri
'is_active': True,
'mode': mode,
'cohort': cohort,
'enterprise_uuid': str(enterprise_uuid)
'enterprise_uuid': str(enterprise_uuid),
'force_enrollment': force_enrollment,
}
)
response.raise_for_status()
Expand Down
14 changes: 12 additions & 2 deletions enterprise/static/enterprise/js/manage_learners.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ function makeOption(name, value) {
return $("<option></option>").text(name).val(value);
}

function fillModeDropdown(data) {
function updateCourseData(data) {
/*
Given a set of data fetched from the enrollment API, populate the Course Mode
dropdown with those options that are valid for the course entered in the
Expand All @@ -19,6 +19,12 @@ function fillModeDropdown(data) {
var previous_value = $course_mode.val();
applyModes(data.course_modes);
$course_mode.val(previous_value);
/*
* If the course is invite-only, show the force enrollment box.
*/
if (data.invite_only) {
$("#id_force_enrollment").parent().show();
}
}

function applyModes(modes) {
Expand All @@ -43,7 +49,7 @@ function loadCourseModes(success, failure) {
return;
}
$.ajax({method: 'get', url: enrollmentApiRoot + "course/" + courseId})
.done(success || fillModeDropdown)
.done(success || updateCourseData)
.fail(failure || function (err, jxHR, errstat) { disableMode(disableReason); });
});
}
Expand Down Expand Up @@ -139,6 +145,10 @@ function loadPage() {
} else if (programEnrollment.$control.val()) {
programEnrollment.$control.trigger("input");
}

// hide the force_invite_only checkbox by default
$("#id_force_enrollment").parent().hide();

$("#learner-management-form").submit(addCheckedLearnersToEnrollBox);
}

Expand Down
12 changes: 9 additions & 3 deletions enterprise/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -1700,12 +1700,15 @@ def enroll_user(enterprise_customer, user, course_mode, *course_ids, **kwargs):
user: The user model object who needs to be enrolled in the course
course_mode: The string representation of the mode with which the enrollment should be created
*course_ids: An iterable containing any number of course IDs to eventually enroll the user in.
kwargs: Should contain enrollment_client if it's already been instantiated and should be passed in.
kwargs: Contains optional params such as:
- enrollment_client, if it's already been instantiated and should be passed in
- force_enrollment, if the course is "Invite Only" and the "force_enrollment" is needed
Returns:
Boolean: Whether or not enrollment succeeded for all courses specified
"""
enrollment_client = kwargs.pop('enrollment_client', None)
force_enrollment = kwargs.pop('force_enrollment', False)
if not enrollment_client:
from enterprise.api_client.lms import EnrollmentApiClient # pylint: disable=import-outside-toplevel
enrollment_client = EnrollmentApiClient()
Expand All @@ -1720,7 +1723,8 @@ def enroll_user(enterprise_customer, user, course_mode, *course_ids, **kwargs):
user.username,
course_id,
course_mode,
enterprise_uuid=str(enterprise_customer_user.enterprise_customer.uuid)
enterprise_uuid=str(enterprise_customer_user.enterprise_customer.uuid),
force_enrollment=force_enrollment,
)
except HttpClientError as exc:
# Check if user is already enrolled then we should ignore exception
Expand Down Expand Up @@ -2059,6 +2063,7 @@ def enroll_users_in_course(
enrollment_reason=None,
discount=0.0,
sales_force_id=None,
force_enrollment=False,
):
"""
Enroll existing users in a course, and create a pending enrollment for nonexisting users.
Expand All @@ -2072,6 +2077,7 @@ def enroll_users_in_course(
enrollment_reason (str): A reason for enrollment.
discount (Decimal): Percentage discount for enrollment.
sales_force_id (str): Salesforce opportunity id.
force_enrollment (bool): Force enrollment into 'Invite Only' courses.
Returns:
successes: A list of users who were successfully enrolled in the course.
Expand All @@ -2088,7 +2094,7 @@ def enroll_users_in_course(
failures = []

for user in existing_users:
succeeded = enroll_user(enterprise_customer, user, course_mode, course_id)
succeeded = enroll_user(enterprise_customer, user, course_mode, course_id, force_enrollment=force_enrollment)
if succeeded:
successes.append(user)
if enrollment_requester and enrollment_reason:
Expand Down
2 changes: 1 addition & 1 deletion test_utils/fake_enrollment_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ def get_course_details(course_id):
return None


def enroll_user_in_course(user, course_id, mode, cohort=None, enterprise_uuid=None):
def enroll_user_in_course(user, course_id, mode, cohort=None, enterprise_uuid=None, force_enrollment=False):
"""
Fake implementation.
"""
Expand Down
80 changes: 69 additions & 11 deletions tests/test_admin/test_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -894,7 +894,7 @@ def test_post_existing_pending_record_with_another_enterprise_customer(self):
self._test_post_existing_record_response(response)
assert PendingEnterpriseCustomerUser.objects.filter(user_email=email).count() == 2

def _enroll_user_request(self, user, mode, course_id="", notify=True, reason="tests", discount=0.0):
def _enroll_user_request(self, user, mode, course_id="", notify=True, reason="tests", discount=0.0, force_enrollment=False):
"""
Perform post request to log in and submit the form to enroll a user.
"""
Expand All @@ -919,6 +919,7 @@ def _enroll_user_request(self, user, mode, course_id="", notify=True, reason="te
ManageLearnersForm.Fields.NOTIFY: notify,
ManageLearnersForm.Fields.REASON: reason,
ManageLearnersForm.Fields.DISCOUNT: discount,
ManageLearnersForm.Fields.FORCE_ENROLLMENT: force_enrollment,
})
return response

Expand Down Expand Up @@ -977,7 +978,8 @@ def test_post_enroll_user(
user.username,
course_id,
mode,
enterprise_uuid=str(self.enterprise_customer.uuid)
enterprise_uuid=str(self.enterprise_customer.uuid),
force_enrollment=False
)
if enrollment_exists:
track_enrollment.assert_not_called()
Expand Down Expand Up @@ -1050,7 +1052,8 @@ def _post_multi_enroll(
user.username,
course_id,
mode,
enterprise_uuid=str(self.enterprise_customer.uuid)
enterprise_uuid=str(self.enterprise_customer.uuid),
force_enrollment=False,
)
track_enrollment.assert_called_with('admin-enrollment', user.id, course_id)
self._assert_django_messages(response, {
Expand Down Expand Up @@ -1150,7 +1153,8 @@ def test_post_enroll_no_course_detail(
user.username,
course_id,
mode,
enterprise_uuid=str(self.enterprise_customer.uuid)
enterprise_uuid=str(self.enterprise_customer.uuid),
force_enrollment=False
)
track_enrollment.assert_called_once_with('admin-enrollment', user.id, course_id)
self._assert_django_messages(response, {
Expand All @@ -1167,6 +1171,51 @@ def test_post_enroll_no_course_detail(
num_messages = len(mail.outbox)
assert num_messages == 0

@mock.patch("enterprise.utils.track_enrollment")
@mock.patch("enterprise.models.CourseCatalogApiClient")
@mock.patch("enterprise.api_client.lms.EnrollmentApiClient")
@mock.patch("enterprise.models.EnterpriseCatalogApiClient")
@ddt.data(True, False)
def test_post_enroll_force_enrollment(
self,
force_enrollment,
enterprise_catalog_client,
enrollment_client,
course_catalog_client,
track_enrollment,
):
catalog_instance = course_catalog_client.return_value
catalog_instance.get_course_run.return_value = {}
enrollment_instance = enrollment_client.return_value
enrollment_instance.enroll_user_in_course.side_effect = fake_enrollment_api.enroll_user_in_course
enrollment_instance.get_course_details.side_effect = fake_enrollment_api.get_course_details
enterprise_catalog_instance = enterprise_catalog_client.return_value
enterprise_catalog_instance.enterprise_contains_content_items.return_value = True

user = UserFactory()
course_id = "course-v1:HarvardX+CoolScience+2016"
mode = "verified"
response = self._enroll_user_request(user, mode, course_id=course_id, force_enrollment=force_enrollment)
enrollment_instance.enroll_user_in_course.assert_called_once_with(
user.username,
course_id,
mode,
enterprise_uuid=str(self.enterprise_customer.uuid),
force_enrollment=force_enrollment
)
track_enrollment.assert_called_once_with('admin-enrollment', user.id, course_id)
self._assert_django_messages(response, {
(messages.SUCCESS, "1 learner was enrolled in {}.".format(course_id)),
})
all_enterprise_enrollments = EnterpriseCourseEnrollment.objects.all()
num_enterprise_enrollments = len(all_enterprise_enrollments)
assert num_enterprise_enrollments == 1
enrollment = all_enterprise_enrollments[0]
assert enrollment.enterprise_customer_user.user == user
assert enrollment.course_id == course_id
assert enrollment.source is not None
assert enrollment.source.slug == EnterpriseEnrollmentSource.MANUAL

@mock.patch("enterprise.utils.track_enrollment")
@mock.patch("enterprise.models.CourseCatalogApiClient")
@mock.patch("enterprise.api_client.lms.EnrollmentApiClient")
Expand Down Expand Up @@ -1215,7 +1264,8 @@ def test_post_enroll_course_when_enrollment_closed(
user.username,
course_id,
mode,
enterprise_uuid=str(self.enterprise_customer.uuid)
enterprise_uuid=str(self.enterprise_customer.uuid),
force_enrollment=False
)

@mock.patch("enterprise.utils.track_enrollment")
Expand Down Expand Up @@ -1249,7 +1299,8 @@ def test_post_enroll_course_when_enrollment_closed_mode_changed(
user.username,
course_id,
mode,
enterprise_uuid=str(self.enterprise_customer.uuid)
enterprise_uuid=str(self.enterprise_customer.uuid),
force_enrollment=False
)
track_enrollment.assert_not_called()
self._assert_django_messages(response, {
Expand Down Expand Up @@ -1290,7 +1341,8 @@ def test_post_enroll_course_when_enrollment_closed_no_sce_exists(
user.username,
course_id,
mode,
enterprise_uuid=str(self.enterprise_customer.uuid)
enterprise_uuid=str(self.enterprise_customer.uuid),
force_enrollment=False
)
track_enrollment.assert_not_called()
self._assert_django_messages(response, {
Expand Down Expand Up @@ -1335,7 +1387,8 @@ def test_post_enroll_with_missing_course_start_date(
user.username,
course_id,
mode,
enterprise_uuid=str(self.enterprise_customer.uuid)
enterprise_uuid=str(self.enterprise_customer.uuid),
force_enrollment=False
)
track_enrollment.assert_called_once_with('admin-enrollment', user.id, course_id)
self._assert_django_messages(response, {
Expand Down Expand Up @@ -1681,6 +1734,7 @@ def test_post_create_course_enrollments(
enrollment_requester=ANY,
enterprise_customer=ANY,
sales_force_id=ANY,
force_enrollment=ANY,
)
enroll_users_in_course_mock.assert_any_call(
course_id=second_course_id,
Expand All @@ -1691,6 +1745,7 @@ def test_post_create_course_enrollments(
enrollment_requester=ANY,
enterprise_customer=ANY,
sales_force_id=ANY,
force_enrollment=ANY,
)
else:
enroll_users_in_course_mock.assert_not_called()
Expand Down Expand Up @@ -1821,7 +1876,8 @@ def test_post_link_and_enroll(
user.username,
course_id,
course_mode,
enterprise_uuid=str(self.enterprise_customer.uuid)
enterprise_uuid=str(self.enterprise_customer.uuid),
force_enrollment=False
)
track_enrollment.assert_called_once_with('admin-enrollment', user.id, course_id)
pending_user_message = (
Expand Down Expand Up @@ -1884,7 +1940,8 @@ def test_post_link_and_enroll_no_course_details(
user.username,
course_id,
course_mode,
enterprise_uuid=str(self.enterprise_customer.uuid)
enterprise_uuid=str(self.enterprise_customer.uuid),
force_enrollment=False
)
track_enrollment.assert_called_once_with('admin-enrollment', user.id, course_id)
pending_user_message = (
Expand Down Expand Up @@ -1940,7 +1997,8 @@ def test_post_link_and_enroll_no_notification(
user.username,
course_id,
course_mode,
enterprise_uuid=str(self.enterprise_customer.uuid)
enterprise_uuid=str(self.enterprise_customer.uuid),
force_enrollment=False
)
track_enrollment.assert_called_once_with('admin-enrollment', user.id, course_id)
pending_user_message = (
Expand Down

0 comments on commit fdac6f5

Please sign in to comment.