From bfde6d63c56bf42ecf6c884aa7071422d0ece4e2 Mon Sep 17 00:00:00 2001 From: asadiqbal Date: Fri, 17 Mar 2017 11:45:11 +0500 Subject: [PATCH] ENT-251 Updated track selection UI for Enterprise context --- .../course_modes/tests/test_views.py | 135 ++++++++++++++++- common/djangoapps/course_modes/views.py | 16 ++ common/djangoapps/util/enterprise_helpers.py | 141 ++++++++++++++++++ .../util/tests/mixins/enterprise.py | 74 +++++++++ common/test/db_fixtures/enterprise.json | 33 ++++ lms/envs/common.py | 1 + lms/templates/course_modes/choose.html | 8 +- 7 files changed, 405 insertions(+), 3 deletions(-) create mode 100644 common/test/db_fixtures/enterprise.json diff --git a/common/djangoapps/course_modes/tests/test_views.py b/common/djangoapps/course_modes/tests/test_views.py index 46e1ba2740..55da469595 100644 --- a/common/djangoapps/course_modes/tests/test_views.py +++ b/common/djangoapps/course_modes/tests/test_views.py @@ -6,6 +6,7 @@ from datetime import datetime import unittest import decimal import ddt +import httpretty import freezegun from mock import patch from nose.plugins.attrib import attr @@ -25,12 +26,14 @@ from student.models import CourseEnrollment from student.tests.factories import CourseEnrollmentFactory, UserFactory from util.testing import UrlResetMixin from openedx.core.djangoapps.theming.tests.test_util import with_comprehensive_theme +from util.tests.mixins.enterprise import EnterpriseServiceMockMixin +from util import organizations_helpers as organizations_api @attr(shard=3) @ddt.ddt @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') -class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase): +class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase, EnterpriseServiceMockMixin): """ Course Mode View tests """ @@ -44,6 +47,7 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase): self.client.login(username=self.user.username, password="edx") @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') + @httpretty.activate @ddt.data( # is_active?, enrollment_mode, redirect? (True, 'verified', True), @@ -69,6 +73,14 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase): user=self.user ) + self.mock_enterprise_learner_api() + # Create a service user and log in. + UserFactory.create( + username='enterprise_worker', + email="bob@example.com", + password="edx", + ) + # Configure whether we're upgrading or not url = reverse('course_modes_choose', args=[unicode(self.course.id)]) response = self.client.get(url) @@ -118,17 +130,101 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase): self.assertRedirects(response, 'http://testserver/test_basket/?sku=TEST', fetch_redirect_response=False) ecomm_test_utils.update_commerce_config(enabled=False) + @httpretty.activate def test_no_enrollment(self): # Create the course modes for mode in ('audit', 'honor', 'verified'): CourseModeFactory.create(mode_slug=mode, course_id=self.course.id) + self.mock_enterprise_learner_api() + # Create a service user and log in. + UserFactory.create( + username='enterprise_worker', + email="bob@example.com", + password="edx", + ) + # User visits the track selection page directly without ever enrolling url = reverse('course_modes_choose', args=[unicode(self.course.id)]) response = self.client.get(url) self.assertEquals(response.status_code, 200) + @httpretty.activate + def test_enterprise_learner_context(self): + """ + Test: Track selection page should show the enterprise context message if user belongs to the Enterprise. + """ + # Create the course modes + for mode in ('audit', 'honor', 'verified'): + CourseModeFactory.create(mode_slug=mode, course_id=self.course.id) + + self.mock_enterprise_learner_api() + # Create a service user and log in. + UserFactory.create( + username='enterprise_worker', + email="bob@example.com", + password="edx", + ) + + # User visits the track selection page directly without ever enrolling + url = reverse('course_modes_choose', args=[unicode(self.course.id)]) + response = self.client.get(url) + self.assertEquals(response.status_code, 200) + self.assertContains( + response, + 'Welcome, {username}! You are about to enroll in {course_name}, from {partner_names}, ' + 'sponsored by TestShib. Please select your enrollment information below.'.format( + username=self.user.username, + course_name=self.course.display_name_with_default_escaped, + partner_names=self.course.org + ) + ) + + @httpretty.activate + def test_enterprise_learner_context_with_multiple_organizations(self): + """ + Test: Track selection page should show the enterprise context message with multiple organization names + if user belongs to the Enterprise. + """ + # Create the course modes + for mode in ('audit', 'honor', 'verified'): + CourseModeFactory.create(mode_slug=mode, course_id=self.course.id) + + self.mock_enterprise_learner_api() + # Create a service user and log in. + UserFactory.create( + username='enterprise_worker', + email="bob@example.com", + password="edx", + ) + + # Creating organization + for i in xrange(2): + test_organization_data = { + 'name': 'test organization ' + str(i), + 'short_name': 'test_organization_' + str(i), + 'description': 'Test Organization Description', + 'active': True, + 'logo': '/logo_test1.png/' + } + test_org = organizations_api.add_organization(organization_data=test_organization_data) + organizations_api.add_organization_course(organization_data=test_org, course_id=unicode(self.course.id)) + + # User visits the track selection page directly without ever enrolling + url = reverse('course_modes_choose', args=[unicode(self.course.id)]) + response = self.client.get(url) + self.assertEquals(response.status_code, 200) + self.assertContains( + response, + 'Welcome, {username}! You are about to enroll in {course_name}, from test organization 0 and ' + 'test organization 1, sponsored by TestShib. Please select your enrollment information below.'.format( + username=self.user.username, + course_name=self.course.display_name_with_default_escaped + ) + ) + + @httpretty.activate @ddt.data( '', '1,,2', @@ -155,6 +251,14 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase): user=self.user ) + self.mock_enterprise_learner_api() + # Create a service user and log in. + UserFactory.create( + username='enterprise_worker', + email="bob@example.com", + password="edx", + ) + # Verify that the prices render correctly response = self.client.get( reverse('course_modes_choose', args=[unicode(self.course.id)]), @@ -165,6 +269,7 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase): # TODO: Fix it so that response.templates works w/ mako templates, and then assert # that the right template rendered + @httpretty.activate @ddt.data( (['honor', 'verified', 'credit'], True), (['honor', 'verified'], False), @@ -175,6 +280,14 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase): for mode in available_modes: CourseModeFactory.create(mode_slug=mode, course_id=self.course.id) + self.mock_enterprise_learner_api() + # Create a service user and log in. + UserFactory.create( + username='enterprise_worker', + email="bob@example.com", + password="edx", + ) + # Check whether credit upsell is shown on the page # This should *only* be shown when a credit mode is available url = reverse('course_modes_choose', args=[unicode(self.course.id)]) @@ -375,11 +488,20 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase): @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') @with_comprehensive_theme("edx.org") + @httpretty.activate def test_hide_nav(self): # Create the course modes for mode in ["honor", "verified"]: CourseModeFactory.create(mode_slug=mode, course_id=self.course.id) + self.mock_enterprise_learner_api() + # Create a service user and log in. + UserFactory.create( + username='enterprise_worker', + email="bob@example.com", + password="edx", + ) + # Load the track selection page url = reverse('course_modes_choose', args=[unicode(self.course.id)]) response = self.client.get(url) @@ -406,7 +528,7 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase): @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') -class TrackSelectionEmbargoTest(UrlResetMixin, ModuleStoreTestCase): +class TrackSelectionEmbargoTest(UrlResetMixin, ModuleStoreTestCase, EnterpriseServiceMockMixin): """Test embargo restrictions on the track selection page. """ URLCONF_MODULES = ['openedx.core.djangoapps.embargo'] @@ -433,6 +555,15 @@ class TrackSelectionEmbargoTest(UrlResetMixin, ModuleStoreTestCase): response = self.client.get(self.url) self.assertRedirects(response, redirect_url) + @httpretty.activate def test_embargo_allow(self): + + self.mock_enterprise_learner_api() + # Create a service user and log in. + UserFactory.create( + username='enterprise_worker', + email="bob@example.com", + password="edx", + ) response = self.client.get(self.url) self.assertEqual(response.status_code, 200) diff --git a/common/djangoapps/course_modes/views.py b/common/djangoapps/course_modes/views.py index 65a1934391..6f2eba5204 100644 --- a/common/djangoapps/course_modes/views.py +++ b/common/djangoapps/course_modes/views.py @@ -26,6 +26,8 @@ from edxmako.shortcuts import render_to_response from openedx.core.djangoapps.embargo import api as embargo_api from student.models import CourseEnrollment from util.db import outer_atomic +from util import enterprise_helpers as enterprise_api +from util import organizations_helpers as organization_api class ChooseModeView(View): @@ -148,6 +150,20 @@ class ChooseModeView(View): "responsive": True, "nav_hidden": True, } + + enterprise_learner_data = enterprise_api.get_enterprise_learner_data(site=request.site, user=request.user) + if enterprise_learner_data: + context["show_enterprise_context"] = True + context["partner_names"] = partner_name = course.display_organization \ + if course.display_organization else course.org + context["enterprise_name"] = enterprise_learner_data[0]['enterprise_customer']['name'] + context["username"] = request.user.username + organizations = organization_api.get_course_organizations(course_id=course.id) + if organizations: + context["partner_names"] = ' and '.join([ + org.get('name', partner_name) for org in organizations + ]) + if "verified" in modes: verified_mode = modes["verified"] context["suggested_prices"] = [ diff --git a/common/djangoapps/util/enterprise_helpers.py b/common/djangoapps/util/enterprise_helpers.py index be3ff19751..6d5ff1788e 100644 --- a/common/djangoapps/util/enterprise_helpers.py +++ b/common/djangoapps/util/enterprise_helpers.py @@ -9,6 +9,7 @@ from django.contrib.auth.models import User from django.core.urlresolvers import reverse from django.shortcuts import redirect from django.utils.http import urlencode +from django.core.cache import cache from edx_rest_api_client.client import EdxRestApiClient try: from enterprise import utils as enterprise_utils @@ -18,6 +19,8 @@ except ImportError: from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers from openedx.core.lib.token_utils import JwtBuilder from slumber.exceptions import HttpClientError, HttpServerError +import hashlib +import six ENTERPRISE_CUSTOMER_BRANDING_OVERRIDE_DETAILS = 'enterprise_customer_branding_override_details' @@ -71,6 +74,108 @@ class EnterpriseApiClient(object): LOGGER.exception(message) raise EnterpriseApiException(message) + def fetch_enterprise_learner_data(self, site, user): + """ + Fetch information related to enterprise from the Enterprise Service. + + Example: + fetch_enterprise_learner_data(site, user) + + Argument: + site: (Site) site instance + user: (User) django auth user + + Returns: + dict: { + "enterprise_api_response_for_learner": { + "count": 1, + "num_pages": 1, + "current_page": 1, + "results": [ + { + "enterprise_customer": { + "uuid": "cf246b88-d5f6-4908-a522-fc307e0b0c59", + "name": "TestShib", + "catalog": 2, + "active": true, + "site": { + "domain": "example.com", + "name": "example.com" + }, + "enable_data_sharing_consent": true, + "enforce_data_sharing_consent": "at_login", + "enterprise_customer_users": [ + 1 + ], + "branding_configuration": { + "enterprise_customer": "cf246b88-d5f6-4908-a522-fc307e0b0c59", + "logo": "https://open.edx.org/sites/all/themes/edx_open/logo.png" + }, + "enterprise_customer_entitlements": [ + { + "enterprise_customer": "cf246b88-d5f6-4908-a522-fc307e0b0c59", + "entitlement_id": 69 + } + ] + }, + "user_id": 5, + "user": { + "username": "staff", + "first_name": "", + "last_name": "", + "email": "staff@example.com", + "is_staff": true, + "is_active": true, + "date_joined": "2016-09-01T19:18:26.026495Z" + }, + "data_sharing_consent": [ + { + "user": 1, + "state": "enabled", + "enabled": true + } + ] + } + ], + "next": null, + "start": 0, + "previous": null + } + } + + Raises: + ConnectionError: requests exception "ConnectionError", raised if if ecommerce is unable to connect + to enterprise api server. + SlumberBaseException: base slumber exception "SlumberBaseException", raised if API response contains + http error status like 4xx, 5xx etc. + Timeout: requests exception "Timeout", raised if enterprise API is taking too long for returning + a response. This exception is raised for both connection timeout and read timeout. + + """ + api_resource_name = 'enterprise-learner' + + cache_key = get_cache_key( + site_domain=site.domain, + resource=api_resource_name, + username=user.username + ) + + response = cache.get(cache_key) + if not response: + try: + endpoint = getattr(self.client, api_resource_name) + querystring = {'username': user.username} + response = endpoint().get(**querystring) + cache.set(cache_key, response, settings.ENTERPRISE_API_CACHE_TIMEOUT) + except (HttpClientError, HttpServerError): + message = ("An error occurred while getting EnterpriseLearner data for user {username}".format( + username=user.username + )) + LOGGER.exception(message) + return None + + return response + def data_sharing_consent_required(view_func): """ @@ -225,3 +330,39 @@ def get_enterprise_branding_filter_param(request): """ return request.session.get(ENTERPRISE_CUSTOMER_BRANDING_OVERRIDE_DETAILS, None) + + +def get_cache_key(**kwargs): + """ + Get MD5 encoded cache key for given arguments. + + Here is the format of key before MD5 encryption. + key1:value1__key2:value2 ... + + Example: + >>> get_cache_key(site_domain="example.com", resource="enterprise-learner") + # Here is key format for above call + # "site_domain:example.com__resource:enterprise-learner" + a54349175618ff1659dee0978e3149ca + + Arguments: + **kwargs: Key word arguments that need to be present in cache key. + + Returns: + An MD5 encoded key uniquely identified by the key word arguments. + """ + key = '__'.join(['{}:{}'.format(item, value) for item, value in six.iteritems(kwargs)]) + + return hashlib.md5(key).hexdigest() + + +def get_enterprise_learner_data(site, user): + """ + Client API operation adapter/wrapper + """ + if not enterprise_enabled(): + return None + + enterprise_learner_data = EnterpriseApiClient().fetch_enterprise_learner_data(site=site, user=user) + if enterprise_learner_data: + return enterprise_learner_data['results'] diff --git a/common/djangoapps/util/tests/mixins/enterprise.py b/common/djangoapps/util/tests/mixins/enterprise.py index 48285fb39d..7dcfc88363 100644 --- a/common/djangoapps/util/tests/mixins/enterprise.py +++ b/common/djangoapps/util/tests/mixins/enterprise.py @@ -57,6 +57,80 @@ class EnterpriseServiceMockMixin(object): status=500 ) + def mock_enterprise_learner_api( + self, + catalog_id=1, + entitlement_id=1, + learner_id=1, + enterprise_customer_uuid='cf246b88-d5f6-4908-a522-fc307e0b0c59' + ): + """ + Helper function to register enterprise learner API endpoint. + """ + enterprise_learner_api_response = { + 'count': 1, + 'num_pages': 1, + 'current_page': 1, + 'results': [ + { + 'id': learner_id, + 'enterprise_customer': { + 'uuid': enterprise_customer_uuid, + 'name': 'TestShib', + 'catalog': catalog_id, + 'active': True, + 'site': { + 'domain': 'example.com', + 'name': 'example.com' + }, + 'enable_data_sharing_consent': True, + 'enforce_data_sharing_consent': 'at_login', + 'enterprise_customer_users': [ + 1 + ], + 'branding_configuration': { + 'enterprise_customer': enterprise_customer_uuid, + 'logo': 'https://open.edx.org/sites/all/themes/edx_open/logo.png' + }, + 'enterprise_customer_entitlements': [ + { + 'enterprise_customer': enterprise_customer_uuid, + 'entitlement_id': entitlement_id + } + ] + }, + 'user_id': 5, + 'user': { + 'username': 'verified', + 'first_name': '', + 'last_name': '', + 'email': 'verified@example.com', + 'is_staff': True, + 'is_active': True, + 'date_joined': '2016-09-01T19:18:26.026495Z' + }, + 'data_sharing_consent': [ + { + 'user': 1, + 'state': 'enabled', + 'enabled': True + } + ] + } + ], + 'next': None, + 'start': 0, + 'previous': None + } + enterprise_learner_api_response_json = json.dumps(enterprise_learner_api_response) + + httpretty.register_uri( + method=httpretty.GET, + uri=self.get_enterprise_url('enterprise-learner'), + body=enterprise_learner_api_response_json, + content_type='application/json' + ) + class EnterpriseTestConsentRequired(object): """ diff --git a/common/test/db_fixtures/enterprise.json b/common/test/db_fixtures/enterprise.json new file mode 100644 index 0000000000..d19220cc90 --- /dev/null +++ b/common/test/db_fixtures/enterprise.json @@ -0,0 +1,33 @@ +[ + { + "pk": 2, + "model": "auth.user", + "fields": { + "date_joined": "2015-06-12 11:02:13.007790+00:00", + "username": "enterprise_worker", + "first_name": "enterprise", + "last_name": "worker", + "email":"enterprise_worker@example.com", + "password": "enterpriseworker", + "is_staff": false, + "is_active": true + } + }, + { + "pk": 2, + "model": "student.userprofile", + "fields": { + "user": 2, + "name": "enterprise worker", + "courseware": "course.xml" + } + }, + { + "pk": 2, + "model": "student.registration", + "fields": { + "user": 2, + "activation_key": "52bfac10384d49219385dcd4cc17177h" + } + } +] diff --git a/lms/envs/common.py b/lms/envs/common.py index 6712bef1f8..562c4c1de8 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -3063,3 +3063,4 @@ DOC_LINK_BASE_URL = None ENTERPRISE_ENROLLMENT_API_URL = LMS_ROOT_URL + "/api/enrollment/v1/" ENTERPRISE_PUBLIC_ENROLLMENT_API_URL = ENTERPRISE_ENROLLMENT_API_URL +ENTERPRISE_API_CACHE_TIMEOUT = 3600 # Value is in seconds diff --git a/lms/templates/course_modes/choose.html b/lms/templates/course_modes/choose.html index 936d651cb8..0bbc6a3913 100644 --- a/lms/templates/course_modes/choose.html +++ b/lms/templates/course_modes/choose.html @@ -73,7 +73,13 @@ from openedx.core.djangolib.markup import HTML, Text