diff --git a/common/djangoapps/util/enterprise_helpers.py b/common/djangoapps/util/enterprise_helpers.py index 9117b09f4e..c995a9416b 100644 --- a/common/djangoapps/util/enterprise_helpers.py +++ b/common/djangoapps/util/enterprise_helpers.py @@ -2,9 +2,12 @@ Helpers to access the enterprise app """ from django.conf import settings +from django.core.urlresolvers import reverse from django.utils.translation import ugettext as _ import logging +from django.utils.http import urlencode + try: from enterprise.models import EnterpriseCustomer from enterprise import utils as enterprise_utils @@ -13,6 +16,7 @@ try: active_provider_enforces_data_sharing, get_enterprise_customer_for_request, ) + from enterprise.utils import consent_necessary_for_course except ImportError: pass @@ -26,7 +30,32 @@ def enterprise_enabled(): """ Determines whether the Enterprise app is installed """ - return 'enterprise' in settings.INSTALLED_APPS + return 'enterprise' in settings.INSTALLED_APPS and getattr(settings, 'ENABLE_ENTERPRISE_INTEGRATION', True) + + +def consent_needed_for_course(user, course_id): + """ + Wrap the enterprise app check to determine if the user needs to grant + data sharing permissions before accessing a course. + """ + if not enterprise_enabled(): + return False + return consent_necessary_for_course(user, course_id) + + +def get_course_specific_consent_url(request, course_id, return_to): + """ + Build a URL to redirect the user to the Enterprise app to provide data sharing + consent for a specific course ID. + """ + url_params = { + 'course_id': course_id, + 'next': request.build_absolute_uri(reverse(return_to, args=(course_id,))) + } + querystring = urlencode(url_params) + full_url = reverse('grant_data_sharing_permissions') + '?' + querystring + LOGGER.info('Redirecting to %s to complete data sharing consent', full_url) + return full_url def data_sharing_consent_requested(request): diff --git a/lms/djangoapps/ccx/tests/test_field_override_performance.py b/lms/djangoapps/ccx/tests/test_field_override_performance.py index 1582c63211..aedcab4313 100644 --- a/lms/djangoapps/ccx/tests/test_field_override_performance.py +++ b/lms/djangoapps/ccx/tests/test_field_override_performance.py @@ -188,6 +188,7 @@ class FieldOverridePerformanceTestCase(FieldOverrideTestMixin, ProceduralCourseT @override_settings( XBLOCK_FIELD_DATA_WRAPPERS=[], MODULESTORE_FIELD_OVERRIDE_PROVIDERS=[], + ENABLE_ENTERPRISE_INTEGRATION=False, ) def test_field_overrides(self, overrides, course_width, enable_ccx, view_as_ccx): """ diff --git a/lms/djangoapps/courseware/tests/test_course_info.py b/lms/djangoapps/courseware/tests/test_course_info.py index a584ca6948..510b269c8f 100644 --- a/lms/djangoapps/courseware/tests/test_course_info.py +++ b/lms/djangoapps/courseware/tests/test_course_info.py @@ -60,6 +60,31 @@ class CourseInfoTestCase(LoginEnrollmentTestCase, SharedModuleStoreTestCase): resp = self.client.get(url) self.assertNotIn("You are not currently enrolled in this course", resp.content) + @mock.patch('courseware.views.views.get_course_specific_consent_url') + @mock.patch('courseware.views.views.consent_needed_for_course') + def test_redirection_missing_enterprise_consent(self, mock_consent_needed, mock_get_url): + """ + Verify that users viewing the course info who are enrolled, but have not provided + data sharing consent, are first redirected to a consent page, and then, once they've + provided consent, are able to view the course info. + """ + self.setup_user() + self.enroll(self.course) + mock_consent_needed.return_value = True + mock_get_url.return_value = reverse('dashboard') + url = reverse('info', args=[self.course.id.to_deprecated_string()]) + + response = self.client.get(url) + + self.assertRedirects( + response, + reverse('dashboard') + ) + mock_consent_needed.assert_called_once_with(self.user, unicode(self.course.id)) + mock_consent_needed.return_value = False + response = self.client.get(url) + self.assertNotIn("You are not currently enrolled in this course", response.content) + def test_anonymous_user(self): url = reverse('info', args=[self.course.id.to_deprecated_string()]) resp = self.client.get(url) @@ -313,7 +338,7 @@ class CourseInfoTestCaseXML(LoginEnrollmentTestCase, ModuleStoreTestCase): @attr(shard=1) -@override_settings(FEATURES=dict(settings.FEATURES, EMBARGO=False)) +@override_settings(FEATURES=dict(settings.FEATURES, EMBARGO=False), ENABLE_ENTERPRISE_INTEGRATION=False) class SelfPacedCourseInfoTestCase(LoginEnrollmentTestCase, SharedModuleStoreTestCase): """ Tests for the info page of self-paced courses. diff --git a/lms/djangoapps/courseware/tests/test_view_authentication.py b/lms/djangoapps/courseware/tests/test_view_authentication.py index 304ce1544a..24fe5fd527 100644 --- a/lms/djangoapps/courseware/tests/test_view_authentication.py +++ b/lms/djangoapps/courseware/tests/test_view_authentication.py @@ -197,6 +197,29 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): ) ) + @patch('courseware.views.index.get_course_specific_consent_url') + @patch('courseware.views.index.consent_needed_for_course') + def test_redirection_missing_enterprise_consent(self, mock_consent_needed, mock_get_url): + """ + Verify that enrolled students are redirected to the Enterprise consent + URL if a linked Enterprise Customer requires data sharing consent + and it has not yet been provided. + """ + mock_consent_needed.return_value = True + mock_get_url.return_value = reverse('dashboard') + self.login(self.enrolled_user) + response = self.client.get( + reverse( + 'courseware', + kwargs={'course_id': self.course.id.to_deprecated_string()} + ) + ) + self.assertRedirects( + response, + reverse('dashboard') + ) + mock_consent_needed.assert_called_once_with(self.enrolled_user, unicode(self.course.id)) + def test_instructor_page_access_nonstaff(self): """ Verify non-staff cannot load the instructor diff --git a/lms/djangoapps/courseware/tests/test_views.py b/lms/djangoapps/courseware/tests/test_views.py index 6f2c7b1682..f491be579d 100644 --- a/lms/djangoapps/courseware/tests/test_views.py +++ b/lms/djangoapps/courseware/tests/test_views.py @@ -1130,6 +1130,7 @@ class StartDateTests(ModuleStoreTestCase): # pylint: disable=protected-access, no-member @attr(shard=1) +@override_settings(ENABLE_ENTERPRISE_INTEGRATION=False) @ddt.ddt class ProgressPageTests(ModuleStoreTestCase): """ diff --git a/lms/djangoapps/courseware/views/index.py b/lms/djangoapps/courseware/views/index.py index cffdee677d..e07f055760 100644 --- a/lms/djangoapps/courseware/views/index.py +++ b/lms/djangoapps/courseware/views/index.py @@ -31,6 +31,7 @@ from shoppingcart.models import CourseRegistrationCode from student.models import CourseEnrollment from student.views import is_course_blocked from student.roles import GlobalStaff +from util.enterprise_helpers import consent_needed_for_course, get_course_specific_consent_url from util.views import ensure_valid_course_key from xmodule.modulestore.django import modulestore from xmodule.x_module import STUDENT_VIEW @@ -194,6 +195,21 @@ class CoursewareIndex(View): self._redirect_if_needed_to_register() self._redirect_if_needed_for_prereqs() self._redirect_if_needed_for_course_survey() + self._redirect_if_data_sharing_consent_needed() + + def _redirect_if_data_sharing_consent_needed(self): + """ + Determine if the user needs to provide data sharing consent before accessing + the course, and redirect the user to provide consent if needed. + """ + course_id = unicode(self.course_key) + if consent_needed_for_course(self.real_user, course_id): + log.warning( + u'User %s cannot access the course %s because they have not granted consent', + self.real_user, + course_id, + ) + raise Redirect(get_course_specific_consent_url(self.request, course_id, 'courseware')) def _redirect_if_needed_to_pay_for_course(self): """ diff --git a/lms/djangoapps/courseware/views/views.py b/lms/djangoapps/courseware/views/views.py index 7eef57c1b9..5e235cac68 100644 --- a/lms/djangoapps/courseware/views/views.py +++ b/lms/djangoapps/courseware/views/views.py @@ -86,6 +86,7 @@ from student.roles import GlobalStaff from util.cache import cache, cache_if_anonymous from util.date_utils import strftime_localized from util.db import outer_atomic +from util.enterprise_helpers import consent_needed_for_course, get_course_specific_consent_url from util.milestones_helpers import get_prerequisite_courses_display from util.views import _record_feedback_in_zendesk from util.views import ensure_valid_course_key, ensure_valid_usage_key @@ -315,6 +316,11 @@ def course_info(request, course_id): # to access CCX redirect him to dashboard. return redirect(reverse('dashboard')) + # If the user is sponsored by an enterprise customer, and we still need to get data + # sharing consent, redirect to do that first. + if consent_needed_for_course(user, course_id): + return redirect(get_course_specific_consent_url(request, course_id, 'info')) + # If the user needs to take an entrance exam to access this course, then we'll need # to send them to that specific course module before allowing them into other areas if user_must_complete_entrance_exam(request, user, course): @@ -702,6 +708,11 @@ def _progress(request, course_key, student_id): course = get_course_with_access(request.user, 'load', course_key, depth=None, check_if_enrolled=True) prep_course_for_grading(course, request) + # If the user is sponsored by an enterprise customer, and we still need to get data + # sharing consent, redirect to do that first. + if consent_needed_for_course(request.user, unicode(course.id)): + return redirect(get_course_specific_consent_url(request, unicode(course.id), 'progress')) + # check to see if there is a required survey that must be taken before # the user can access the course. if survey.utils.must_answer_survey(course, request.user): diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index 3bd710c0cd..0fd206ccf9 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -51,7 +51,7 @@ edx-lint==0.4.3 astroid==1.3.8 edx-django-oauth2-provider==1.1.4 edx-django-sites-extensions==2.1.1 -edx-enterprise==0.19.1 +edx-enterprise==0.21.0 edx-oauth2-provider==1.2.0 edx-opaque-keys==0.4.0 edx-organizations==0.4.2