Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Backport of enhancements for invite only courses #4

1 change: 0 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ on:
push:
branches: [master]
pull_request:
branches: [master]
tecoholic marked this conversation as resolved.
Show resolved Hide resolved

jobs:
run_tests:
Expand Down
6 changes: 6 additions & 0 deletions enterprise/admin/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,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 @@ -128,6 +133,7 @@ class Fields:
REASON = "reason"
SALES_FORCE_ID = "sales_force_id"
DISCOUNT = "discount"
FORCE_ENROLLMENT = "force_enrollment"

class CsvColumns:
"""
Expand Down
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
17 changes: 16 additions & 1 deletion enterprise/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,10 @@
)

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

try:
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
Expand Down Expand Up @@ -647,7 +648,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 requirements/ci.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ certifi==2021.10.8
# via requests
charset-normalizer==2.0.11
# via requests
codecov==2.1.12
codecov==2.1.13
tecoholic marked this conversation as resolved.
Show resolved Hide resolved
# via -r requirements/ci.in
coverage==6.3.1
# via codecov
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