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