From 1106746fd41a10e4d091f54adafc44497cbd494a Mon Sep 17 00:00:00 2001 From: Jesse Shapiro <jesse@opencraft.com> Date: Thu, 23 Mar 2017 16:44:24 -0400 Subject: [PATCH] Add consent declined banner to dashboard --- common/djangoapps/student/views.py | 4 + common/djangoapps/util/enterprise_helpers.py | 80 +++++++++- .../util/tests/test_enterprise_helpers.py | 143 ++++++++++++++++++ ...erprise_consent_declined_notification.html | 12 ++ .../sass/elements/_system-feedback.scss | 14 ++ lms/templates/dashboard.html | 6 + requirements/edx/base.txt | 2 +- 7 files changed, 258 insertions(+), 3 deletions(-) create mode 100644 common/templates/util/enterprise_consent_declined_notification.html diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index bc1c82d9f17..696b7ff29fc 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -102,6 +102,7 @@ from util.milestones_helpers import ( ) from util.password_policy_validators import validate_password_strength +from util.enterprise_helpers import get_dashboard_consent_notification import third_party_auth from third_party_auth import pipeline, provider from student.helpers import ( @@ -684,6 +685,8 @@ def dashboard(request): {'email': user.email, 'platform_name': platform_name} ) + enterprise_message = get_dashboard_consent_notification(request, user, course_enrollments) + # Global staff can see what courses errored on their dashboard staff_access = False errored_courses = {} @@ -804,6 +807,7 @@ def dashboard(request): display_sidebar_on_dashboard = len(order_history_list) or verification_status in valid_verification_statuses context = { + 'enterprise_message': enterprise_message, 'enrollment_message': enrollment_message, 'redirect_message': redirect_message, 'course_enrollments': course_enrollments, diff --git a/common/djangoapps/util/enterprise_helpers.py b/common/djangoapps/util/enterprise_helpers.py index 6d5ff1788e2..2fde41482d6 100644 --- a/common/djangoapps/util/enterprise_helpers.py +++ b/common/djangoapps/util/enterprise_helpers.py @@ -6,13 +6,16 @@ import logging from functools import wraps from django.conf import settings from django.contrib.auth.models import User +from django.core.cache import cache from django.core.urlresolvers import reverse from django.shortcuts import redirect from django.utils.http import urlencode -from django.core.cache import cache +from django.utils.translation import ugettext as _ +from edxmako.shortcuts import render_to_string from edx_rest_api_client.client import EdxRestApiClient try: from enterprise import utils as enterprise_utils + from enterprise.models import EnterpriseCourseEnrollment from enterprise.utils import consent_necessary_for_course except ImportError: pass @@ -23,6 +26,7 @@ import hashlib import six +CONSENT_FAILED_PARAMETER = 'consent_failed' ENTERPRISE_CUSTOMER_BRANDING_OVERRIDE_DETAILS = 'enterprise_customer_branding_override_details' LOGGER = logging.getLogger("edx.enterprise_helpers") @@ -249,7 +253,14 @@ def get_enterprise_consent_url(request, course_id, user=None, return_to=None): url_params = { 'course_id': course_id, - 'next': request.build_absolute_uri(return_path) + 'next': request.build_absolute_uri(return_path), + 'failure_url': request.build_absolute_uri( + reverse('dashboard') + '?' + urlencode( + { + CONSENT_FAILED_PARAMETER: course_id + } + ) + ), } querystring = urlencode(url_params) full_url = reverse('grant_data_sharing_permissions') + '?' + querystring @@ -366,3 +377,68 @@ def get_enterprise_learner_data(site, user): enterprise_learner_data = EnterpriseApiClient().fetch_enterprise_learner_data(site=site, user=user) if enterprise_learner_data: return enterprise_learner_data['results'] + + +def get_dashboard_consent_notification(request, user, course_enrollments): + """ + If relevant to the request at hand, create a banner on the dashboard indicating consent failed. + + Args: + request: The WSGIRequest object produced by the user browsing to the Dashboard page. + user: The logged-in user + course_enrollments: A list of the courses to be rendered on the Dashboard page. + + Returns: + str: Either an empty string, or a string containing the HTML code for the notification banner. + """ + enrollment = None + enterprise_enrollment = None + course_id = request.GET.get(CONSENT_FAILED_PARAMETER) + + if course_id: + for course_enrollment in course_enrollments: + if str(course_enrollment.course_id) == course_id: + enrollment = course_enrollment + break + + try: + enterprise_enrollment = EnterpriseCourseEnrollment.objects.get( + course_id=course_id, + enterprise_customer_user__user_id=user.id, + ) + except EnterpriseCourseEnrollment.DoesNotExist: + pass + + if enterprise_enrollment and enrollment: + enterprise_customer = enterprise_enrollment.enterprise_customer_user.enterprise_customer + contact_info = getattr(enterprise_customer, 'contact_email', None) + + if contact_info is None: + message_template = _( + 'If you have concerns about sharing your data, please contact your administrator ' + 'at {enterprise_customer_name}.' + ) + else: + message_template = _( + 'If you have concerns about sharing your data, please contact your administrator ' + 'at {enterprise_customer_name} at {contact_info}.' + ) + + message = message_template.format( + enterprise_customer_name=enterprise_customer.name, + contact_info=contact_info, + ) + title = _( + 'Enrollment in {course_name} was not complete.' + ).format( + course_name=enrollment.course_overview.display_name, + ) + + return render_to_string( + 'util/enterprise_consent_declined_notification.html', + { + 'title': title, + 'message': message, + } + ) + return '' diff --git a/common/djangoapps/util/tests/test_enterprise_helpers.py b/common/djangoapps/util/tests/test_enterprise_helpers.py index ed76b80817c..333a34768b3 100644 --- a/common/djangoapps/util/tests/test_enterprise_helpers.py +++ b/common/djangoapps/util/tests/test_enterprise_helpers.py @@ -13,7 +13,9 @@ from util.enterprise_helpers import ( insert_enterprise_pipeline_elements, data_sharing_consent_required, set_enterprise_branding_filter_param, + get_dashboard_consent_notification, get_enterprise_branding_filter_param, + get_enterprise_consent_url, get_enterprise_customer_logo_url ) @@ -192,3 +194,144 @@ class TestEnterpriseHelpers(unittest.TestCase): mock_get_consent_url.assert_called_once() mock_enterprise_enabled.assert_called_once() mock_consent_necessary.assert_called_once() + + @mock.patch('util.enterprise_helpers.consent_needed_for_course') + def test_get_enterprise_consent_url(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' + return_to = 'info' + + 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%2Fcours' + 'es%2Fcourse-v1%3AedX%2BDemoX%2BDemo_Course%2Finfo' + ) + actual_url = get_enterprise_consent_url(request_mock, course_id, return_to=return_to) + 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. + """ + request = mock.MagicMock( + GET={} + ) + notification_string = get_dashboard_consent_notification( + request, None, None + ) + self.assertEqual(notification_string, '') + + def test_get_dashboard_consent_notification_no_enrollments(self): + request = mock.MagicMock( + GET={'consent_failed': 'course-v1:edX+DemoX+Demo_Course'} + ) + enrollments = [] + user = mock.MagicMock(id=1) + notification_string = get_dashboard_consent_notification( + request, user, enrollments, + ) + self.assertEqual(notification_string, '') + + def test_get_dashboard_consent_notification_no_matching_enrollments(self): + request = mock.MagicMock( + GET={'consent_failed': 'course-v1:edX+DemoX+Demo_Course'} + ) + enrollments = [mock.MagicMock(course_id='other_course_id')] + user = mock.MagicMock(id=1) + notification_string = get_dashboard_consent_notification( + request, user, enrollments, + ) + self.assertEqual(notification_string, '') + + def test_get_dashboard_consent_notification_no_matching_ece(self): + request = mock.MagicMock( + GET={'consent_failed': 'course-v1:edX+DemoX+Demo_Course'} + ) + enrollments = [mock.MagicMock(course_id='course-v1:edX+DemoX+Demo_Course')] + user = mock.MagicMock(id=1) + notification_string = get_dashboard_consent_notification( + request, user, enrollments, + ) + self.assertEqual(notification_string, '') + + @mock.patch('util.enterprise_helpers.EnterpriseCourseEnrollment') + def test_get_dashboard_consent_notification_no_contact_info(self, ece_mock): + mock_get_ece = ece_mock.objects.get + ece_mock.DoesNotExist = Exception + mock_ece = mock_get_ece.return_value + mock_ece.enterprise_customer_user = mock.MagicMock( + enterprise_customer=mock.MagicMock( + contact_email=None + ) + ) + mock_ec = mock_ece.enterprise_customer_user.enterprise_customer + mock_ec.name = 'Veridian Dynamics' + + request = mock.MagicMock( + GET={'consent_failed': 'course-v1:edX+DemoX+Demo_Course'} + ) + enrollments = [ + mock.MagicMock( + course_id='course-v1:edX+DemoX+Demo_Course', + course_overview=mock.MagicMock( + display_name='edX Demo Course', + ) + ), + ] + user = mock.MagicMock(id=1) + notification_string = get_dashboard_consent_notification( + request, user, enrollments, + ) + expected_message = ( + 'If you have concerns about sharing your data, please contact your ' + 'administrator at Veridian Dynamics.' + ) + self.assertIn(expected_message, notification_string) + expected_header = 'Enrollment in edX Demo Course was not complete.' + self.assertIn(expected_header, notification_string) + + @mock.patch('util.enterprise_helpers.EnterpriseCourseEnrollment') + def test_get_dashboard_consent_notification_contact_info(self, ece_mock): + mock_get_ece = ece_mock.objects.get + ece_mock.DoesNotExist = Exception + mock_ece = mock_get_ece.return_value + mock_ece.enterprise_customer_user = mock.MagicMock( + enterprise_customer=mock.MagicMock( + contact_email='v.palmer@veridiandynamics.com' + ) + ) + mock_ec = mock_ece.enterprise_customer_user.enterprise_customer + mock_ec.name = 'Veridian Dynamics' + + request = mock.MagicMock( + GET={'consent_failed': 'course-v1:edX+DemoX+Demo_Course'} + ) + enrollments = [ + mock.MagicMock( + course_id='course-v1:edX+DemoX+Demo_Course', + course_overview=mock.MagicMock( + display_name='edX Demo Course', + ) + ), + ] + user = mock.MagicMock(id=1) + notification_string = get_dashboard_consent_notification( + request, user, enrollments, + ) + expected_message = ( + 'If you have concerns about sharing your data, please contact your ' + 'administrator at Veridian Dynamics at v.palmer@veridiandynamics.com.' + ) + self.assertIn(expected_message, notification_string) + expected_header = 'Enrollment in edX Demo Course was not complete.' + self.assertIn(expected_header, notification_string) diff --git a/common/templates/util/enterprise_consent_declined_notification.html b/common/templates/util/enterprise_consent_declined_notification.html new file mode 100644 index 00000000000..e7a0f4bdb69 --- /dev/null +++ b/common/templates/util/enterprise_consent_declined_notification.html @@ -0,0 +1,12 @@ +<%page expression_filter="h"/> +<div class="wrapper-msg urgency-info"> + <div class="msg"> + <span class="msg-icon fa fa-info-circle" aria-hidden="true"></span> + <div class="msg-content"> + <h2 class="title">${ title }</h2> + <div class="copy"> + <p class='consent-declined-message'>${ message }</p> + </div> + </div> + </div> +</div> diff --git a/lms/static/sass/elements/_system-feedback.scss b/lms/static/sass/elements/_system-feedback.scss index c915be77e94..8b3eea07a55 100644 --- a/lms/static/sass/elements/_system-feedback.scss +++ b/lms/static/sass/elements/_system-feedback.scss @@ -118,6 +118,20 @@ } } + &.urgency-info { + background: $msg-bg; + .msg { + color: $white; + } + .msg-icon { + font-size: 2.5em; + padding: 20px; + } + .msg-content { + max-width: 80%; + } + } + &.alert { border-top: 3px solid $alert-color; } diff --git a/lms/templates/dashboard.html b/lms/templates/dashboard.html index 36309e144f9..faf9ed0fae5 100644 --- a/lms/templates/dashboard.html +++ b/lms/templates/dashboard.html @@ -73,6 +73,12 @@ from openedx.core.djangolib.markup import HTML, Text ${enrollment_message | n, decode.utf8} </div> %endif + + %if enterprise_message: + <div class="dashboard-banner"> + ${ enterprise_message | n, decode.utf8 } + </div> + %endif </div> <main id="main" aria-label="Content" tabindex="-1"> diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index b5309977658..2887cf79d06 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -52,7 +52,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.27.6 +edx-enterprise==0.28.0 edx-oauth2-provider==1.2.0 edx-opaque-keys==0.4.0 edx-organizations==0.4.3 -- GitLab