Commit 9a573de5 by Jesse Shapiro

Add consent check to course access prerequisites; add utility functions to…

Add consent check to course access prerequisites; add utility functions to provide interface to course-specific consent in Enterprise app
parent 8a53f3b6
...@@ -2,9 +2,12 @@ ...@@ -2,9 +2,12 @@
Helpers to access the enterprise app Helpers to access the enterprise app
""" """
from django.conf import settings from django.conf import settings
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
import logging import logging
from django.utils.http import urlencode
try: try:
from enterprise.models import EnterpriseCustomer from enterprise.models import EnterpriseCustomer
from enterprise import utils as enterprise_utils from enterprise import utils as enterprise_utils
...@@ -13,6 +16,7 @@ try: ...@@ -13,6 +16,7 @@ try:
active_provider_enforces_data_sharing, active_provider_enforces_data_sharing,
get_enterprise_customer_for_request, get_enterprise_customer_for_request,
) )
from enterprise.utils import consent_necessary_for_course
except ImportError: except ImportError:
pass pass
...@@ -26,7 +30,32 @@ def enterprise_enabled(): ...@@ -26,7 +30,32 @@ def enterprise_enabled():
""" """
Determines whether the Enterprise app is installed 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): def data_sharing_consent_requested(request):
......
...@@ -188,6 +188,7 @@ class FieldOverridePerformanceTestCase(FieldOverrideTestMixin, ProceduralCourseT ...@@ -188,6 +188,7 @@ class FieldOverridePerformanceTestCase(FieldOverrideTestMixin, ProceduralCourseT
@override_settings( @override_settings(
XBLOCK_FIELD_DATA_WRAPPERS=[], XBLOCK_FIELD_DATA_WRAPPERS=[],
MODULESTORE_FIELD_OVERRIDE_PROVIDERS=[], MODULESTORE_FIELD_OVERRIDE_PROVIDERS=[],
ENABLE_ENTERPRISE_INTEGRATION=False,
) )
def test_field_overrides(self, overrides, course_width, enable_ccx, view_as_ccx): def test_field_overrides(self, overrides, course_width, enable_ccx, view_as_ccx):
""" """
......
...@@ -60,6 +60,31 @@ class CourseInfoTestCase(LoginEnrollmentTestCase, SharedModuleStoreTestCase): ...@@ -60,6 +60,31 @@ class CourseInfoTestCase(LoginEnrollmentTestCase, SharedModuleStoreTestCase):
resp = self.client.get(url) resp = self.client.get(url)
self.assertNotIn("You are not currently enrolled in this course", resp.content) 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): def test_anonymous_user(self):
url = reverse('info', args=[self.course.id.to_deprecated_string()]) url = reverse('info', args=[self.course.id.to_deprecated_string()])
resp = self.client.get(url) resp = self.client.get(url)
...@@ -313,7 +338,7 @@ class CourseInfoTestCaseXML(LoginEnrollmentTestCase, ModuleStoreTestCase): ...@@ -313,7 +338,7 @@ class CourseInfoTestCaseXML(LoginEnrollmentTestCase, ModuleStoreTestCase):
@attr(shard=1) @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): class SelfPacedCourseInfoTestCase(LoginEnrollmentTestCase, SharedModuleStoreTestCase):
""" """
Tests for the info page of self-paced courses. Tests for the info page of self-paced courses.
......
...@@ -197,6 +197,29 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): ...@@ -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): def test_instructor_page_access_nonstaff(self):
""" """
Verify non-staff cannot load the instructor Verify non-staff cannot load the instructor
......
...@@ -1130,6 +1130,7 @@ class StartDateTests(ModuleStoreTestCase): ...@@ -1130,6 +1130,7 @@ class StartDateTests(ModuleStoreTestCase):
# pylint: disable=protected-access, no-member # pylint: disable=protected-access, no-member
@attr(shard=1) @attr(shard=1)
@override_settings(ENABLE_ENTERPRISE_INTEGRATION=False)
@ddt.ddt @ddt.ddt
class ProgressPageTests(ModuleStoreTestCase): class ProgressPageTests(ModuleStoreTestCase):
""" """
......
...@@ -31,6 +31,7 @@ from shoppingcart.models import CourseRegistrationCode ...@@ -31,6 +31,7 @@ from shoppingcart.models import CourseRegistrationCode
from student.models import CourseEnrollment from student.models import CourseEnrollment
from student.views import is_course_blocked from student.views import is_course_blocked
from student.roles import GlobalStaff 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 util.views import ensure_valid_course_key
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.x_module import STUDENT_VIEW from xmodule.x_module import STUDENT_VIEW
...@@ -194,6 +195,21 @@ class CoursewareIndex(View): ...@@ -194,6 +195,21 @@ class CoursewareIndex(View):
self._redirect_if_needed_to_register() self._redirect_if_needed_to_register()
self._redirect_if_needed_for_prereqs() self._redirect_if_needed_for_prereqs()
self._redirect_if_needed_for_course_survey() 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): def _redirect_if_needed_to_pay_for_course(self):
""" """
......
...@@ -86,6 +86,7 @@ from student.roles import GlobalStaff ...@@ -86,6 +86,7 @@ from student.roles import GlobalStaff
from util.cache import cache, cache_if_anonymous from util.cache import cache, cache_if_anonymous
from util.date_utils import strftime_localized from util.date_utils import strftime_localized
from util.db import outer_atomic 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.milestones_helpers import get_prerequisite_courses_display
from util.views import _record_feedback_in_zendesk from util.views import _record_feedback_in_zendesk
from util.views import ensure_valid_course_key, ensure_valid_usage_key from util.views import ensure_valid_course_key, ensure_valid_usage_key
...@@ -315,6 +316,11 @@ def course_info(request, course_id): ...@@ -315,6 +316,11 @@ def course_info(request, course_id):
# to access CCX redirect him to dashboard. # to access CCX redirect him to dashboard.
return redirect(reverse('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 # 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 # to send them to that specific course module before allowing them into other areas
if user_must_complete_entrance_exam(request, user, course): if user_must_complete_entrance_exam(request, user, course):
...@@ -702,6 +708,11 @@ def _progress(request, course_key, student_id): ...@@ -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) course = get_course_with_access(request.user, 'load', course_key, depth=None, check_if_enrolled=True)
prep_course_for_grading(course, request) 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 # check to see if there is a required survey that must be taken before
# the user can access the course. # the user can access the course.
if survey.utils.must_answer_survey(course, request.user): if survey.utils.must_answer_survey(course, request.user):
......
...@@ -48,7 +48,7 @@ edx-i18n-tools==0.3.7 ...@@ -48,7 +48,7 @@ edx-i18n-tools==0.3.7
edx-lint==0.4.3 edx-lint==0.4.3
edx-django-oauth2-provider==1.1.4 edx-django-oauth2-provider==1.1.4
edx-django-sites-extensions==2.1.1 edx-django-sites-extensions==2.1.1
edx-enterprise==0.19.1 edx-enterprise==0.21.0
edx-oauth2-provider==1.2.0 edx-oauth2-provider==1.2.0
edx-opaque-keys==0.4.0 edx-opaque-keys==0.4.0
edx-organizations==0.4.2 edx-organizations==0.4.2
......
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