Commit d940bbfd by Jesse Shapiro

Create EnterpriseCourseEnrollment when enrolling via Track Selection page

parent 2b81a940
......@@ -233,6 +233,52 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest
self.assertContains(response, 'Audit This Course')
@httpretty.activate
@patch('course_modes.views.get_enterprise_consent_url')
@ddt.data(
(True, True),
(True, False),
(False, True),
(False, False),
)
@ddt.unpack
def test_enterprise_course_enrollment_creation(
self,
enterprise_enrollment_exists,
course_in_catalog,
get_consent_url_mock,
):
for mode in ('audit', 'honor', 'verified'):
CourseModeFactory.create(mode_slug=mode, course_id=self.course.id)
catalog_integration = self.create_catalog_integration()
UserFactory(username=catalog_integration.service_username)
courses_in_catalog = [str(self.course.id)] if course_in_catalog else []
enterprise_enrollment = {'course_id': str(self.course.id)} if enterprise_enrollment_exists else {}
self.mock_course_discovery_api_for_catalog_contains(
catalog_id=1, course_run_ids=courses_in_catalog
)
self.mock_enterprise_course_enrollment_get_api(**enterprise_enrollment)
self.mock_enterprise_course_enrollment_post_api()
self.mock_enterprise_learner_api(enable_audit_enrollment=True)
get_consent_url_mock.return_value = 'http://appropriate-consent-url.com/'
url = reverse('course_modes_choose', args=[unicode(self.course.id)])
response = self.client.post(url, self.POST_PARAMS_FOR_COURSE_MODE['audit'])
final_url = reverse('dashboard') if not course_in_catalog else 'http://appropriate-consent-url.com/'
self.assertRedirects(response, final_url, fetch_redirect_response=False)
if course_in_catalog:
if enterprise_enrollment_exists:
self.assertEquals(httpretty.last_request().method, 'GET')
else:
self.assertEquals(httpretty.last_request().method, 'POST')
@httpretty.activate
@ddt.data(
'',
'1,,2',
......@@ -330,6 +376,7 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest
'unsupported': {'unsupported_mode': True},
}
@httpretty.activate
@ddt.data(
('audit', 'dashboard'),
('honor', 'dashboard'),
......@@ -337,6 +384,8 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest
)
@ddt.unpack
def test_choose_mode_redirect(self, course_mode, expected_redirect):
self.mock_enterprise_learner_api()
self.mock_enterprise_course_enrollment_get_api()
# Create the course modes
for mode in ('audit', 'honor', 'verified'):
min_price = 0 if mode in ["honor", "audit"] else 1
......@@ -359,7 +408,38 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest
self.assertRedirects(response, redirect_url)
@httpretty.activate
def test_choose_mode_audit_enroll_on_get(self):
"""
Confirms that the learner will be enrolled in Audit track if it is the only possible option
"""
self.mock_enterprise_learner_api()
self.mock_enterprise_course_enrollment_get_api()
# Create the course mode
audit_mode = 'audit'
CourseModeFactory.create(mode_slug=audit_mode, course_id=self.course.id, min_price=0)
# Assert learner is not enrolled in Audit track pre-POST
mode, is_active = CourseEnrollment.enrollment_mode_for_user(self.user, self.course.id)
self.assertIsNone(mode)
self.assertIsNone(is_active)
# Choose the audit mode (POST request)
choose_track_url = reverse('course_modes_choose', args=[unicode(self.course.id)])
response = self.client.get(choose_track_url)
# Assert learner is enrolled in Audit track and sent to the dashboard
mode, is_active = CourseEnrollment.enrollment_mode_for_user(self.user, self.course.id)
self.assertEquals(mode, audit_mode)
self.assertTrue(is_active)
redirect_url = reverse('dashboard')
self.assertRedirects(response, redirect_url)
@httpretty.activate
def test_choose_mode_audit_enroll_on_post(self):
self.mock_enterprise_learner_api()
self.mock_enterprise_course_enrollment_get_api()
audit_mode = 'audit'
# Create the course modes
for mode in (audit_mode, 'verified'):
......@@ -394,7 +474,10 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest
self.assertEqual(mode, audit_mode)
self.assertTrue(is_active)
@httpretty.activate
def test_remember_donation_for_course(self):
self.mock_enterprise_learner_api()
self.mock_enterprise_course_enrollment_get_api()
# Create the course modes
CourseModeFactory.create(mode_slug='honor', course_id=self.course.id)
CourseModeFactory.create(mode_slug='verified', course_id=self.course.id, min_price=1)
......@@ -411,7 +494,10 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest
expected_amount = decimal.Decimal(self.POST_PARAMS_FOR_COURSE_MODE['verified']['contribution'])
self.assertEqual(actual_amount, expected_amount)
@httpretty.activate
def test_successful_default_enrollment(self):
self.mock_enterprise_learner_api()
self.mock_enterprise_course_enrollment_get_api()
# Create the course modes
for mode in (CourseMode.DEFAULT_MODE_SLUG, 'verified'):
CourseModeFactory.create(mode_slug=mode, course_id=self.course.id)
......@@ -433,7 +519,10 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest
self.assertEqual(mode, CourseMode.DEFAULT_MODE_SLUG)
self.assertEqual(is_active, True)
@httpretty.activate
def test_unsupported_enrollment_mode_failure(self):
self.mock_enterprise_learner_api()
self.mock_enterprise_course_enrollment_get_api()
# Create the supported course modes
for mode in ('honor', 'verified'):
CourseModeFactory.create(mode_slug=mode, course_id=self.course.id)
......
......@@ -25,6 +25,7 @@ from edxmako.shortcuts import render_to_response
from lms.djangoapps.commerce.utils import EcommerceService
from openedx.core.djangoapps.embargo import api as embargo_api
from openedx.features.enterprise_support import api as enterprise_api
from openedx.features.enterprise_support.api import get_enterprise_consent_url
from student.models import CourseEnrollment
from third_party_auth.decorators import tpa_hint_ends_existing_session
from util import organizations_helpers as organization_api
......@@ -107,6 +108,16 @@ class ChooseModeView(View):
# If there isn't a verified mode available, then there's nothing
# to do on this page. Send the user to the dashboard.
if not CourseMode.has_verified_mode(modes):
# If the learner has arrived at this screen via the traditional enrollment workflow,
# then they should already be enrolled in an audit mode for the course, assuming one has
# been configured. However, alternative enrollment workflows have been introduced into the
# system, such as third-party discovery. These workflows result in learners arriving
# directly at this screen, and they will not necessarily be pre-enrolled in the audit mode.
# In this particular case, Audit is the ONLY option available, and thus we need to ensure
# that the learner is truly enrolled before we redirect them away to the dashboard.
if len(modes) == 1 and modes.get(CourseMode.AUDIT):
CourseEnrollment.enroll(request.user, course_key, CourseMode.AUDIT)
return redirect(self._get_redirect_url_for_audit_enrollment(request, course_id))
return redirect(reverse('dashboard'))
# If a user has already paid, redirect them to the dashboard.
......@@ -241,19 +252,14 @@ class ChooseModeView(View):
allowed_modes = CourseMode.modes_for_course_dict(course_key)
if requested_mode not in allowed_modes:
return HttpResponseBadRequest(_("Enrollment mode not supported"))
if requested_mode == 'audit':
if requested_mode in CourseMode.AUDIT_MODES:
# If the learner has arrived at this screen via the traditional enrollment workflow,
# then they should already be enrolled in an audit mode for the course, assuming one has
# been configured. However, alternative enrollment workflows have been introduced into the
# system, such as third-party discovery. These workflows result in learners arriving
# directly at this screen, and they will not necessarily be pre-enrolled in the audit mode.
CourseEnrollment.enroll(request.user, course_key, CourseMode.AUDIT)
return redirect(reverse('dashboard'))
if requested_mode == 'honor':
CourseEnrollment.enroll(user, course_key, mode=requested_mode)
return redirect(reverse('dashboard'))
return redirect(self._get_redirect_url_for_audit_enrollment(request, course_id))
mode_info = allowed_modes[requested_mode]
......@@ -284,6 +290,44 @@ class ChooseModeView(View):
)
)
def _get_redirect_url_for_audit_enrollment(self, request, course_id):
"""
After a user has been enrolled in a course in an audit mode, determine the appropriate location
to which they ought to be redirected, bearing in mind enterprise data sharing consent considerations.
"""
enterprise_learner_data = enterprise_api.get_enterprise_learner_data(site=request.site, user=request.user)
if enterprise_learner_data:
enterprise_learner = enterprise_learner_data[0]
# If we have an enterprise learner, check to see if the current course is in the enterprise's catalog.
is_course_in_enterprise_catalog = enterprise_api.is_course_in_enterprise_catalog(
site=request.site,
course_id=course_id,
enterprise_catalog_id=enterprise_learner['enterprise_customer']['catalog']
)
# If the course is in the catalog, check for an existing Enterprise enrollment
if is_course_in_enterprise_catalog:
client = enterprise_api.EnterpriseApiClient()
if not client.get_enterprise_course_enrollment(enterprise_learner['id'], course_id):
# If there's no existing Enterprise enrollment, create one.
client.post_enterprise_course_enrollment(request.user.username, course_id, None)
# Check if consent is required, and generate a redirect URL to the
# consent service if so; this function returns None if consent
# is not required or has already been granted.
consent_url = get_enterprise_consent_url(
request,
course_id,
user=request.user,
return_to='dashboard',
course_specific_return=False,
)
# If we got a redirect URL for consent, go there.
if consent_url:
return consent_url
# If the enrollment isn't Enterprise-linked, or if consent isn't necessary, go to the Dashboard.
return reverse('dashboard')
def _get_requested_mode(self, request_dict):
"""Get the user's requested mode
......
......@@ -59,6 +59,32 @@ class EnterpriseApiClient(object):
jwt=jwt
)
def get_enterprise_course_enrollment(self, ec_user_id, course_id):
"""
Check for an EnterpriseCourseEnrollment linking a particular EnterpriseCustomerUser to a particular course.
"""
params = {
'enterprise_customer_user': ec_user_id,
'course_id': course_id,
}
try:
response = getattr(self.client, 'enterprise-course-enrollment').get(**params)
except (HttpClientError, HttpServerError):
message = (
"An error occured while getting EnterpriseCourseEnrollment for EnterpriseCustomerUser with "
"ID {ec_user_id} and course run {course_id}."
).format(
username=username,
course_id=course_id,
)
LOGGER.exception(message)
raise EnterpriseApiException(message)
else:
if response.get('results'):
return response['results'][0]
else:
return None
def post_enterprise_course_enrollment(self, username, course_id, consent_granted):
"""
Create an EnterpriseCourseEnrollment by using the corresponding serializer (for validation).
......@@ -268,7 +294,7 @@ def consent_needed_for_course(user, course_id):
return consent_necessary_for_course(user, course_id)
def get_enterprise_consent_url(request, course_id, user=None, return_to=None):
def get_enterprise_consent_url(request, course_id, user=None, return_to=None, course_specific_return=True):
"""
Build a URL to redirect the user to the Enterprise app to provide data sharing
consent for a specific course ID.
......@@ -286,10 +312,15 @@ def get_enterprise_consent_url(request, course_id, user=None, return_to=None):
if not consent_needed_for_course(user, course_id):
return None
if course_specific_return:
reverse_args = (course_id,)
else:
reverse_args = tuple()
if return_to is None:
return_path = request.path
else:
return_path = reverse(return_to, args=(course_id,))
return_path = reverse(return_to, args=reverse_args)
url_params = {
'course_id': course_id,
......
......@@ -57,6 +57,18 @@ class EnterpriseServiceMockMixin(object):
status=500
)
def mock_enterprise_course_enrollment_get_api(self, **kwargs):
result = {
'results': [kwargs] if kwargs else []
}
httpretty.register_uri(
method=httpretty.GET,
uri=self.get_enterprise_url('enterprise-course-enrollment'),
body=json.dumps(result),
content_type='application/json',
status=200
)
def mock_enterprise_learner_api(
self,
catalog_id=1,
......
......@@ -194,6 +194,29 @@ class TestEnterpriseApi(unittest.TestCase):
actual_url = get_enterprise_consent_url(request_mock, course_id, return_to=return_to)
self.assertEqual(actual_url, expected_url)
@mock.patch('openedx.features.enterprise_support.api.consent_needed_for_course')
def test_get_enterprise_consent_url_next_provided_not_course_specific(self, needed_for_course_mock):
"""
Verify that get_enterprise_consent_url correctly builds URLs.
"""
needed_for_course_mock.return_value = True
request_mock = mock.MagicMock(
user=None,
build_absolute_uri=lambda x: 'http://localhost:8000' + x # Don't do it like this in prod. Ever.
)
course_id = 'course-v1:edX+DemoX+Demo_Course'
expected_url = (
'/enterprise/grant_data_sharing_permissions?course_id=course-v1%3AedX%2BDemoX%2BDemo_'
'Course&failure_url=http%3A%2F%2Flocalhost%3A8000%2Fdashboard%3Fconsent_failed%3Dcou'
'rse-v1%253AedX%252BDemoX%252BDemo_Course&next=http%3A%2F%2Flocalhost%3A8000%2Fdashboard'
)
actual_url = get_enterprise_consent_url(request_mock, course_id, return_to='dashboard', course_specific_return=False)
self.assertEqual(actual_url, expected_url)
def test_get_dashboard_consent_notification_no_param(self):
"""
Test that the output of the consent notification renderer meets expectations.
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment