+
+{% endblock %}
diff --git a/enterprise/urls.py b/enterprise/urls.py
index 5f99e9839a..95e10c4d40 100644
--- a/enterprise/urls.py
+++ b/enterprise/urls.py
@@ -8,7 +8,7 @@
from django.conf.urls import include, url
from enterprise.constants import COURSE_KEY_URL_PATTERN
-from enterprise.views import GrantDataSharingPermissions, RouterView
+from enterprise.views import EnterpriseSelectionView, GrantDataSharingPermissions, RouterView
ENTERPRISE_ROUTER = RouterView.as_view()
@@ -18,6 +18,11 @@
GrantDataSharingPermissions.as_view(),
name='grant_data_sharing_permissions'
),
+ url(
+ r'^enterprise/select/active',
+ EnterpriseSelectionView.as_view(),
+ name='enterprise_select_active'
+ ),
url(
r'^enterprise/handle_consent_enrollment/(?P[^/]+)/course/{}/$'.format(
settings.COURSE_ID_PATTERN
diff --git a/enterprise/views.py b/enterprise/views.py
index addb5105dd..d2d2edbd5c 100644
--- a/enterprise/views.py
+++ b/enterprise/views.py
@@ -20,7 +20,7 @@
from django.core.exceptions import ImproperlyConfigured, PermissionDenied
from django.core.urlresolvers import reverse
from django.db import IntegrityError, transaction
-from django.http import Http404
+from django.http import Http404, JsonResponse
from django.shortcuts import redirect, render
from django.utils.decorators import method_decorator
from django.utils.text import slugify
@@ -28,14 +28,17 @@
from django.utils.translation import ugettext as _
from django.utils.translation import ungettext
from django.views.generic import View
+from django.views.generic.edit import FormView
from consent.helpers import get_data_sharing_consent
from consent.models import DataSharingConsent
from enterprise import constants, messages
+from enterprise.api.v1.serializers import EnterpriseCustomerUserWriteSerializer
from enterprise.api_client.discovery import get_course_catalog_api_service_client
from enterprise.api_client.ecommerce import EcommerceApiClient
from enterprise.api_client.lms import CourseApiClient, EmbargoApiClient, EnrollmentApiClient
from enterprise.decorators import enterprise_login_required, force_fresh_session
+from enterprise.forms import ENTERPRISE_SELECT_SUBTITLE, EnterpriseSelectionForm
from enterprise.models import EnterpriseCourseEnrollment, EnterpriseCustomerCatalog, EnterpriseCustomerUser
from enterprise.utils import (
CourseEnrollmentDowngradeError,
@@ -91,13 +94,13 @@ def verify_edx_resources():
)
-def get_global_context(request, enterprise_customer):
+def get_global_context(request, enterprise_customer=None):
"""
Get the set of variables that are needed by default across views.
"""
platform_name = get_configuration_value("PLATFORM_NAME", settings.PLATFORM_NAME)
# pylint: disable=no-member
- return {
+ context = {
'enterprise_customer': enterprise_customer,
'LMS_SEGMENT_KEY': settings.LMS_SEGMENT_KEY,
'LANGUAGE_CODE': get_language_from_request(request),
@@ -110,19 +113,25 @@ def get_global_context(request, enterprise_customer):
'platform_name': platform_name,
'header_logo_alt_text': _('{platform_name} home page').format(platform_name=platform_name),
'welcome_text': constants.WELCOME_TEXT.format(platform_name=platform_name),
- 'enterprise_welcome_text': constants.ENTERPRISE_WELCOME_TEXT.format(
- enterprise_customer_name=enterprise_customer.name,
- platform_name=platform_name,
- strong_start='',
- strong_end='',
- line_break=' ',
- privacy_policy_link_start="".format(
- pp_url=get_configuration_value('PRIVACY', 'https://www.edx.org/edx-privacy-policy', type='url'),
- ),
- privacy_policy_link_end="",
- ),
}
+ if enterprise_customer is not None:
+ context.update({
+ 'enterprise_welcome_text': constants.ENTERPRISE_WELCOME_TEXT.format(
+ enterprise_customer_name=enterprise_customer.name,
+ platform_name=platform_name,
+ strong_start='',
+ strong_end='',
+ line_break=' ',
+ privacy_policy_link_start="".format(
+ pp_url=get_configuration_value('PRIVACY', 'https://www.edx.org/edx-privacy-policy', type='url'),
+ ),
+ privacy_policy_link_end="",
+ ),
+ })
+
+ return context
+
def get_price_text(price, request):
"""
@@ -808,6 +817,69 @@ def post(self, request):
return redirect(success_url if consent_provided else failure_url)
+@method_decorator(login_required, name='dispatch')
+class EnterpriseSelectionView(FormView):
+ """
+ Allow an enterprise learner to activate one of learner's linked enterprises.
+ """
+
+ form_class = EnterpriseSelectionForm
+ template_name = 'enterprise/enterprise_customer_select_form.html'
+
+ def get_initial(self):
+ """Return the initial data to use for forms on this view."""
+ initial = super(EnterpriseSelectionView, self).get_initial()
+ enterprises = EnterpriseCustomerUser.objects.filter(
+ user_id=self.request.user.id
+ ).values_list(
+ 'enterprise_customer__uuid', 'enterprise_customer__name'
+ )
+ initial.update({
+ 'enterprises': [(str(uuid), name) for uuid, name in enterprises],
+ 'success_url': self.request.GET.get('success_url'),
+ 'user_id': self.request.user.id
+ })
+ return initial
+
+ def get_context_data(self, **kwargs):
+ """Return the context data needed to render the view."""
+ context_data = super(EnterpriseSelectionView, self).get_context_data(**kwargs)
+ context_data.update({
+ 'page_title': _(u'Select Organization'),
+ 'select_enterprise_message_title': _(u'Select an organization'),
+ 'select_enterprise_message_subtitle': ENTERPRISE_SELECT_SUBTITLE,
+ })
+ context_data.update(get_global_context(self.request, None))
+ return context_data
+
+ def form_invalid(self, form):
+ """
+ If the form is invalid then return the errors.
+ """
+ # flatten the list of lists
+ errors = [item for sublist in form.errors.values() for item in sublist]
+ return JsonResponse({'errors': errors}, status=400)
+
+ def form_valid(self, form):
+ """
+ If the form is valid, activate the selected enterprise and return `success_url`.
+ """
+ enterprise_customer = form.cleaned_data['enterprise']
+ serializer = EnterpriseCustomerUserWriteSerializer(data={
+ 'enterprise_customer': enterprise_customer,
+ 'username': self.request.user.username,
+ 'active': True
+ })
+ if serializer.is_valid():
+ serializer.save()
+ LOGGER.info(
+ '[Enterprise Selection Page] Learner activated an enterprise. User: %s, EnterpriseCustomer: %s',
+ enterprise_customer,
+ self.request.user.username
+ )
+ return JsonResponse({'success_url': form.cleaned_data['success_url']})
+
+
class HandleConsentEnrollment(View):
"""
Handle enterprise course enrollment at providing data sharing consent.
diff --git a/spec/javascripts/enterprise_select_spec.js b/spec/javascripts/enterprise_select_spec.js
new file mode 100644
index 0000000000..c9bd886503
--- /dev/null
+++ b/spec/javascripts/enterprise_select_spec.js
@@ -0,0 +1,90 @@
+describe('Enterprise Selection Page', function () {
+ beforeEach(function () {
+ jasmine.getFixtures().fixturesPath = '__spec__/fixtures';
+ loadFixtures('enterprise_select.html');
+ setupFormSubmit();
+ });
+
+ describe('Rendering', function () {
+ it('renders page correctly', function () {
+ expect($('.select-enterprise-title').text()).toBe('Select an organization');
+ expect($('.select-enterprise-message p').text()).toBe(
+ 'You have access to multiple organizations. Select the organization that you will use ' +
+ 'to sign up for courses. If you want to change organizations, sign out and sign back in.'
+ );
+ expect($('#select-enterprise-form label').text()).toBe('Organization:');
+
+ var optionValues = $.map($('#id_enterprise option') ,function(option) {
+ return option.value;
+ });
+ var optionTexts = $.map($('#id_enterprise option') ,function(option) {
+ return option.text;
+ });
+ expect(optionValues).toEqual(
+ ['6ae013d4-c5c4-474d-8da9-0e559b2448e2', '885f4d97-5a21-4e8a-8723-a434bc527e74']
+ );
+ expect(optionTexts).toEqual(['batman', 'riddler']);
+
+ expect($('#select-enterprise-submit').text().trim()).toBe('Continue');
+ });
+ });
+
+ describe('Form', function () {
+ beforeEach(function () {
+ jasmine.Ajax.install();
+ });
+
+ afterEach(function () {
+ jasmine.Ajax.uninstall();
+ });
+
+ it('works expected on correct post data', function () {
+ var response = {
+ 'success_url': '/dashboard'
+ };
+ var redirectSpy = spyOn(window, 'redirectToURL');
+
+ jasmine.Ajax
+ .stubRequest('/enterprise/select/active')
+ .andReturn({
+ responseText: JSON.stringify(response)
+ });
+
+ $( '#select-enterprise-submit' ).trigger( 'click' );
+
+ var request = jasmine.Ajax.requests.mostRecent();
+ expect(request.url).toBe('/enterprise/select/active');
+ expect(request.method).toBe('POST');
+ expect(request.data().enterprise).toEqual(['6ae013d4-c5c4-474d-8da9-0e559b2448e2']);
+ expect(request.data().success_url).toEqual(['/dashboard']);
+ expect(redirectSpy.calls.count()).toEqual(1);
+ expect(redirectSpy.calls.first().args).toEqual(['/dashboard']);
+ });
+
+ it('works expected on incorrect post data', function () {
+ var response = {
+ 'errors': ['Incorrect success url']
+ };
+
+ jasmine.Ajax
+ .stubRequest('/enterprise/select/active')
+ .andReturn({
+ status: 400,
+ responseText: JSON.stringify(response)
+ });
+
+ // remove success url value from hidden input
+ $('#id_success_url').removeAttr('value');
+
+ $( '#select-enterprise-submit' ).trigger( 'click' );
+
+ var request = jasmine.Ajax.requests.mostRecent();
+ expect(request.url).toBe('/enterprise/select/active');
+ expect(request.method).toBe('POST');
+ expect(request.data().enterprise).toEqual(['6ae013d4-c5c4-474d-8da9-0e559b2448e2']);
+ expect(request.data().success_url).toEqual(['']);
+
+ expect($('#select-enterprise-form-error').text().trim()).toEqual(response.errors[0]);
+ });
+ });
+});
diff --git a/spec/javascripts/fixtures/enterprise_select.html b/spec/javascripts/fixtures/enterprise_select.html
new file mode 100644
index 0000000000..908850aa70
--- /dev/null
+++ b/spec/javascripts/fixtures/enterprise_select.html
@@ -0,0 +1,41 @@
+
+
+
+
+
Select an organization
+
+
You have access to multiple organizations. Select the organization that you will use to sign up for courses. If you want to change organizations, sign out and sign back in.
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/test_enterprise/views/test_enterprise_selection.py b/tests/test_enterprise/views/test_enterprise_selection.py
new file mode 100644
index 0000000000..36038faf64
--- /dev/null
+++ b/tests/test_enterprise/views/test_enterprise_selection.py
@@ -0,0 +1,166 @@
+# -*- coding: utf-8 -*-
+"""
+Tests for the ``EnterpriseSelectionView`` view of the Enterprise app.
+"""
+
+from __future__ import absolute_import, unicode_literals
+
+import os
+import tempfile
+
+import ddt
+import mock
+from pytest import mark
+
+from django.conf import settings
+from django.core.urlresolvers import reverse
+from django.test import Client, TestCase
+
+from enterprise.forms import ENTERPRISE_SELECT_SUBTITLE
+from enterprise.models import EnterpriseCustomerUser
+from test_utils.factories import EnterpriseCustomerFactory, EnterpriseCustomerUserFactory, UserFactory
+
+
+@mark.django_db
+@ddt.ddt
+class TestEnterpriseSelectionView(TestCase):
+ """
+ Test EnterpriseSelectionView.
+ """
+ url = reverse('enterprise_select_active')
+
+ def setUp(self):
+ self.user = UserFactory.create(is_active=True)
+ self.user.set_password("QWERTY")
+ self.user.save()
+ self.client = Client()
+ super(TestEnterpriseSelectionView, self).setUp()
+
+ self.success_url = '/enterprise/grant_data_sharing_permissions'
+ enterprises = ['Gryffindor', 'Hufflepuff', 'Ravenclaw', 'Slytherin']
+ for enterprise in enterprises:
+ enterprise_customer = EnterpriseCustomerFactory(name=enterprise)
+ EnterpriseCustomerUserFactory(
+ user_id=self.user.id,
+ enterprise_customer=enterprise_customer
+ )
+
+ enterprises = EnterpriseCustomerUser.objects.filter(
+ user_id=self.user.id
+ ).values_list(
+ 'enterprise_customer__uuid', 'enterprise_customer__name'
+ )
+ self.enterprise_choices = [(str(uuid), name) for uuid, name in enterprises]
+
+ # create a temporary template file
+ # rendering `enterprise/enterprise_customer_select_form.html` fails becuase of dependency on edx-platform
+ tpl = tempfile.NamedTemporaryFile(
+ prefix='test_template.',
+ suffix=".html",
+ dir=settings.REPO_ROOT + '/templates/enterprise/',
+ delete=False,
+ )
+ tpl.close()
+ self.addCleanup(os.remove, tpl.name)
+
+ patcher = mock.patch(
+ 'enterprise.views.EnterpriseSelectionView.template_name',
+ mock.PropertyMock(return_value=tpl.name)
+ )
+ patcher.start()
+ self.addCleanup(patcher.stop)
+
+ def _login(self):
+ """
+ Log user in.
+ """
+ assert self.client.login(username=self.user.username, password="QWERTY")
+
+ def test_view_unauthenticated_user(self):
+ """
+ Test that view will be available to logged in user only.
+ """
+ response = self.client.get(self.url)
+ assert response.status_code == 302
+ assert response.url == '/accounts/login/?next=/enterprise/select/active'
+
+ def test_view_get(self):
+ """
+ Test that view HTTP GET works as expected.
+ """
+ self._login()
+ response = self.client.get(self.url + '?success_url={}'.format(self.success_url))
+ assert response.status_code == 200
+
+ assert response.context['select_enterprise_message_title'] == u'Select an organization'
+ assert response.context['select_enterprise_message_subtitle'] == ENTERPRISE_SELECT_SUBTITLE
+
+ assert sorted(response.context['form'].fields.keys()) == sorted(['enterprise', 'success_url'])
+ assert response.context['form'].fields['enterprise'].choices == self.enterprise_choices
+ assert response.context['form'].fields['success_url'].initial == self.success_url
+
+ def test_view_post(self):
+ """
+ Test that view HTTP POST works as expected.
+ """
+ self._login()
+ user_id = self.user.pk
+
+ # before selection all enterprises are active for learner
+ for obj in EnterpriseCustomerUser.objects.filter(user_id=user_id):
+ assert obj.active
+
+ new_enterprise = self.enterprise_choices[2][0]
+ post_data = {
+ 'enterprise': new_enterprise,
+ 'success_url': self.success_url
+ }
+
+ with mock.patch('enterprise.views.LOGGER.info') as mock_logger:
+ response = self.client.post(self.url, post_data)
+ assert mock_logger.called
+ assert mock_logger.call_args.args == (
+ u'[Enterprise Selection Page] Learner activated an enterprise. User: %s, EnterpriseCustomer: %s',
+ new_enterprise,
+ self.user.username
+ )
+
+ assert response.status_code == 200
+ assert response.json().get('success_url') == self.success_url
+
+ # after selection only the selected enterprise should be active for learner
+ assert EnterpriseCustomerUser.objects.get(user_id=user_id, enterprise_customer=new_enterprise).active
+
+ # all other enterprises for learner should be non-active
+ for obj in EnterpriseCustomerUser.objects.filter(user_id=user_id).exclude(enterprise_customer=new_enterprise):
+ assert not obj.active
+
+ @ddt.data(
+ {
+ 'enterprise': '111',
+ 'success_url': '',
+ 'errors': [
+ u'Enterprise not found',
+ u'Select a valid choice. 111 is not one of the available choices.'
+ ]
+ },
+ {
+ 'enterprise': None,
+ 'success_url': '',
+ 'errors': [u'Incorrect success url']
+ },
+ )
+ @ddt.unpack
+ def test_post_errors(self, enterprise, success_url, errors):
+ """
+ Test errors are raised if incorrect data is POSTed.
+ """
+ self._login()
+ selected_enterprise = self.enterprise_choices[2][0]
+ post_data = {
+ 'enterprise': enterprise or selected_enterprise,
+ 'success_url': success_url,
+ }
+ response = self.client.post(self.url, post_data)
+ assert response.status_code == 400
+ assert sorted(response.json().get('errors')) == sorted(errors)