Skip to content

Commit

Permalink
feat: enable enrolling in invite_only courses via manage learners UI
Browse files Browse the repository at this point in the history
  • Loading branch information
tecoholic committed May 24, 2023
1 parent 1be2ff9 commit 7e726fa
Show file tree
Hide file tree
Showing 11 changed files with 408 additions and 147 deletions.
261 changes: 170 additions & 91 deletions enterprise/admin/forms.py

Large diffs are not rendered by default.

12 changes: 9 additions & 3 deletions enterprise/admin/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -641,7 +641,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 @@ -654,6 +655,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 @@ -667,6 +669,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 @@ -783,6 +786,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 @@ -797,7 +801,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 All @@ -812,7 +817,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,
)

# Redirect to GET if everything went smooth.
Expand Down
13 changes: 11 additions & 2 deletions enterprise/api_client/lms.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,15 @@ def has_course_mode(self, course_run_id, mode):
return any(course_mode for course_mode in course_modes if course_mode['slug'] == mode)

@JwtLmsApiClient.refresh_token
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 @@ -252,7 +260,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,
}
)

Expand Down
16 changes: 15 additions & 1 deletion enterprise/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
)

try:
from common.djangoapps.student.models import CourseEnrollment
from common.djangoapps.student.models import CourseEnrollment, CourseEnrollmentAllowed
except ImportError:
CourseEnrollment = None

Expand Down Expand Up @@ -647,7 +647,21 @@ def enroll_user_pending_registration_with_status(self, email, course_mode, *cour
license_uuid = None

new_enrollments = {}
enrollment_api_client = EnrollmentApiClient()

for course_id in course_ids:
# Check if the course is "Invite Only" and add CEA if it is.
course_details = enrollment_api_client.get_course_details(course_id)

if course_details["invite_only"]:
if not CourseEnrollmentAllowed:
raise NotConnectedToOpenEdX()

CourseEnrollmentAllowed.objects.update_or_create(
email=email,
course_id=course_id
)

__, created = PendingEnrollment.objects.update_or_create(
user=pending_ecu,
course_id=course_id,
Expand Down
36 changes: 34 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,11 @@ 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 +48,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 @@ -134,11 +139,38 @@ function loadPage() {
programEnrollment.$control.oldValue = null;
});

// NOTE: As the course details won't be fetched for course id in the CSV
// file, this has a potential side-effect of enrolling learners into the courses
// which might be marked as closed for reasons other then being "Invite Only".
//
// This is considered as a reasonable tradeoff at the time of this addition.
// Currently, the EnrollmentListView does not support invitation only courses.
// This problem does not happen in the Instructor Dashboard because it doesn't
// invoke access checks when calling the enroll method. Modifying the enroll method
// is a high-risk change, and it seems that the API will need some changes in
// the near future anyway - when the Instructor Dashboard is converted into an
// MFE (it could be an excellent opportunity to eliminate many legacy behaviors
// there, too).
$("#id_bulk_upload_csv").change(function(e) {
if (e.target.value) {
var force_enrollment = $("#id_force_enrollment");
force_enrollment.parent().show();
force_enrollment.siblings(".helptext")[0].innerHTML = gettext(
"If any of the courses in the CSV file are marked 'Invite Only', " +
"this should be enabled for the enrollments to go through in those courses."
);
}
});

if (courseEnrollment.$control.val()) {
courseEnrollment.$control.trigger("input");
} 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 @@ -1658,12 +1658,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 @@ -1678,7 +1681,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 @@ -1956,6 +1960,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 @@ -1969,6 +1974,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 @@ -1985,7 +1991,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): # pylint: disable=unused-argument
"""
Fake implementation.
"""
Expand Down
Loading

0 comments on commit 7e726fa

Please sign in to comment.