Commit bfde6d63 by asadiqbal

ENT-251 Updated track selection UI for Enterprise context

parent b10083cb
...@@ -6,6 +6,7 @@ from datetime import datetime ...@@ -6,6 +6,7 @@ from datetime import datetime
import unittest import unittest
import decimal import decimal
import ddt import ddt
import httpretty
import freezegun import freezegun
from mock import patch from mock import patch
from nose.plugins.attrib import attr from nose.plugins.attrib import attr
...@@ -25,12 +26,14 @@ from student.models import CourseEnrollment ...@@ -25,12 +26,14 @@ from student.models import CourseEnrollment
from student.tests.factories import CourseEnrollmentFactory, UserFactory from student.tests.factories import CourseEnrollmentFactory, UserFactory
from util.testing import UrlResetMixin from util.testing import UrlResetMixin
from openedx.core.djangoapps.theming.tests.test_util import with_comprehensive_theme 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) @attr(shard=3)
@ddt.ddt @ddt.ddt
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') @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 Course Mode View tests
""" """
...@@ -44,6 +47,7 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase): ...@@ -44,6 +47,7 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase):
self.client.login(username=self.user.username, password="edx") self.client.login(username=self.user.username, password="edx")
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
@httpretty.activate
@ddt.data( @ddt.data(
# is_active?, enrollment_mode, redirect? # is_active?, enrollment_mode, redirect?
(True, 'verified', True), (True, 'verified', True),
...@@ -69,6 +73,14 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase): ...@@ -69,6 +73,14 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase):
user=self.user 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 # Configure whether we're upgrading or not
url = reverse('course_modes_choose', args=[unicode(self.course.id)]) url = reverse('course_modes_choose', args=[unicode(self.course.id)])
response = self.client.get(url) response = self.client.get(url)
...@@ -118,17 +130,101 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase): ...@@ -118,17 +130,101 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase):
self.assertRedirects(response, 'http://testserver/test_basket/?sku=TEST', fetch_redirect_response=False) self.assertRedirects(response, 'http://testserver/test_basket/?sku=TEST', fetch_redirect_response=False)
ecomm_test_utils.update_commerce_config(enabled=False) ecomm_test_utils.update_commerce_config(enabled=False)
@httpretty.activate
def test_no_enrollment(self): def test_no_enrollment(self):
# Create the course modes # Create the course modes
for mode in ('audit', 'honor', 'verified'): for mode in ('audit', 'honor', 'verified'):
CourseModeFactory.create(mode_slug=mode, course_id=self.course.id) 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 # User visits the track selection page directly without ever enrolling
url = reverse('course_modes_choose', args=[unicode(self.course.id)]) url = reverse('course_modes_choose', args=[unicode(self.course.id)])
response = self.client.get(url) 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.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( @ddt.data(
'', '',
'1,,2', '1,,2',
...@@ -155,6 +251,14 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase): ...@@ -155,6 +251,14 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase):
user=self.user 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 # Verify that the prices render correctly
response = self.client.get( response = self.client.get(
reverse('course_modes_choose', args=[unicode(self.course.id)]), reverse('course_modes_choose', args=[unicode(self.course.id)]),
...@@ -165,6 +269,7 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase): ...@@ -165,6 +269,7 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase):
# TODO: Fix it so that response.templates works w/ mako templates, and then assert # TODO: Fix it so that response.templates works w/ mako templates, and then assert
# that the right template rendered # that the right template rendered
@httpretty.activate
@ddt.data( @ddt.data(
(['honor', 'verified', 'credit'], True), (['honor', 'verified', 'credit'], True),
(['honor', 'verified'], False), (['honor', 'verified'], False),
...@@ -175,6 +280,14 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase): ...@@ -175,6 +280,14 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase):
for mode in available_modes: for mode in available_modes:
CourseModeFactory.create(mode_slug=mode, course_id=self.course.id) 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 # Check whether credit upsell is shown on the page
# This should *only* be shown when a credit mode is available # This should *only* be shown when a credit mode is available
url = reverse('course_modes_choose', args=[unicode(self.course.id)]) url = reverse('course_modes_choose', args=[unicode(self.course.id)])
...@@ -375,11 +488,20 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase): ...@@ -375,11 +488,20 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase):
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
@with_comprehensive_theme("edx.org") @with_comprehensive_theme("edx.org")
@httpretty.activate
def test_hide_nav(self): def test_hide_nav(self):
# Create the course modes # Create the course modes
for mode in ["honor", "verified"]: for mode in ["honor", "verified"]:
CourseModeFactory.create(mode_slug=mode, course_id=self.course.id) 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 # Load the track selection page
url = reverse('course_modes_choose', args=[unicode(self.course.id)]) url = reverse('course_modes_choose', args=[unicode(self.course.id)])
response = self.client.get(url) response = self.client.get(url)
...@@ -406,7 +528,7 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase): ...@@ -406,7 +528,7 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase):
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') @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. """ """Test embargo restrictions on the track selection page. """
URLCONF_MODULES = ['openedx.core.djangoapps.embargo'] URLCONF_MODULES = ['openedx.core.djangoapps.embargo']
...@@ -433,6 +555,15 @@ class TrackSelectionEmbargoTest(UrlResetMixin, ModuleStoreTestCase): ...@@ -433,6 +555,15 @@ class TrackSelectionEmbargoTest(UrlResetMixin, ModuleStoreTestCase):
response = self.client.get(self.url) response = self.client.get(self.url)
self.assertRedirects(response, redirect_url) self.assertRedirects(response, redirect_url)
@httpretty.activate
def test_embargo_allow(self): 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) response = self.client.get(self.url)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
...@@ -26,6 +26,8 @@ from edxmako.shortcuts import render_to_response ...@@ -26,6 +26,8 @@ from edxmako.shortcuts import render_to_response
from openedx.core.djangoapps.embargo import api as embargo_api from openedx.core.djangoapps.embargo import api as embargo_api
from student.models import CourseEnrollment from student.models import CourseEnrollment
from util.db import outer_atomic 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): class ChooseModeView(View):
...@@ -148,6 +150,20 @@ class ChooseModeView(View): ...@@ -148,6 +150,20 @@ class ChooseModeView(View):
"responsive": True, "responsive": True,
"nav_hidden": 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: if "verified" in modes:
verified_mode = modes["verified"] verified_mode = modes["verified"]
context["suggested_prices"] = [ context["suggested_prices"] = [
......
...@@ -9,6 +9,7 @@ from django.contrib.auth.models import User ...@@ -9,6 +9,7 @@ from django.contrib.auth.models import User
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.shortcuts import redirect from django.shortcuts import redirect
from django.utils.http import urlencode from django.utils.http import urlencode
from django.core.cache import cache
from edx_rest_api_client.client import EdxRestApiClient from edx_rest_api_client.client import EdxRestApiClient
try: try:
from enterprise import utils as enterprise_utils from enterprise import utils as enterprise_utils
...@@ -18,6 +19,8 @@ except ImportError: ...@@ -18,6 +19,8 @@ except ImportError:
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from openedx.core.lib.token_utils import JwtBuilder from openedx.core.lib.token_utils import JwtBuilder
from slumber.exceptions import HttpClientError, HttpServerError from slumber.exceptions import HttpClientError, HttpServerError
import hashlib
import six
ENTERPRISE_CUSTOMER_BRANDING_OVERRIDE_DETAILS = 'enterprise_customer_branding_override_details' ENTERPRISE_CUSTOMER_BRANDING_OVERRIDE_DETAILS = 'enterprise_customer_branding_override_details'
...@@ -71,6 +74,108 @@ class EnterpriseApiClient(object): ...@@ -71,6 +74,108 @@ class EnterpriseApiClient(object):
LOGGER.exception(message) LOGGER.exception(message)
raise EnterpriseApiException(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): def data_sharing_consent_required(view_func):
""" """
...@@ -225,3 +330,39 @@ def get_enterprise_branding_filter_param(request): ...@@ -225,3 +330,39 @@ def get_enterprise_branding_filter_param(request):
""" """
return request.session.get(ENTERPRISE_CUSTOMER_BRANDING_OVERRIDE_DETAILS, None) 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']
...@@ -57,6 +57,80 @@ class EnterpriseServiceMockMixin(object): ...@@ -57,6 +57,80 @@ class EnterpriseServiceMockMixin(object):
status=500 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): class EnterpriseTestConsentRequired(object):
""" """
......
[
{
"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"
}
}
]
...@@ -3063,3 +3063,4 @@ DOC_LINK_BASE_URL = None ...@@ -3063,3 +3063,4 @@ DOC_LINK_BASE_URL = None
ENTERPRISE_ENROLLMENT_API_URL = LMS_ROOT_URL + "/api/enrollment/v1/" ENTERPRISE_ENROLLMENT_API_URL = LMS_ROOT_URL + "/api/enrollment/v1/"
ENTERPRISE_PUBLIC_ENROLLMENT_API_URL = ENTERPRISE_ENROLLMENT_API_URL ENTERPRISE_PUBLIC_ENROLLMENT_API_URL = ENTERPRISE_ENROLLMENT_API_URL
ENTERPRISE_API_CACHE_TIMEOUT = 3600 # Value is in seconds
...@@ -73,7 +73,13 @@ from openedx.core.djangolib.markup import HTML, Text ...@@ -73,7 +73,13 @@ from openedx.core.djangolib.markup import HTML, Text
<article class="register-choose content-main"> <article class="register-choose content-main">
<header class="page-header content-main"> <header class="page-header content-main">
<h3 class="title"> <h3 class="title">
% if show_enterprise_context:
${_("Welcome, {username}! You are about to enroll in {course_name}, from "
"{partner_names}, sponsored by {enterprise_name}. Please select your enrollment"
" information below.").format(username=username, course_name=course_name, partner_names=partner_names, enterprise_name=enterprise_name)}
% else:
${_("Congratulations! You are now enrolled in {course_name}").format(course_name=course_name)} ${_("Congratulations! You are now enrolled in {course_name}").format(course_name=course_name)}
% endif
</h3> </h3>
</header> </header>
......
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