diff --git a/lms/djangoapps/student_account/test/test_views.py b/lms/djangoapps/student_account/test/test_views.py index 19817f8701..49c367f3e5 100644 --- a/lms/djangoapps/student_account/test/test_views.py +++ b/lms/djangoapps/student_account/test/test_views.py @@ -367,20 +367,38 @@ class StudentAccountLoginAndRegistrationTest(ThirdPartyAuthTestMixin, UrlResetMi @ddt.data("signin_user", "register_user") def test_third_party_auth_disabled(self, url_name): response = self.client.get(reverse(url_name)) - self._assert_third_party_auth_data(response, None, None, []) + self._assert_third_party_auth_data(response, None, None, [], None) + @mock.patch('student_account.views.enterprise_customer_for_request') @ddt.data( - ("signin_user", None, None), - ("register_user", None, None), - ("signin_user", "google-oauth2", "Google"), - ("register_user", "google-oauth2", "Google"), - ("signin_user", "facebook", "Facebook"), - ("register_user", "facebook", "Facebook"), - ("signin_user", "dummy", "Dummy"), - ("register_user", "dummy", "Dummy"), + ("signin_user", None, None, None), + ("register_user", None, None, None), + ("signin_user", "google-oauth2", "Google", None), + ("register_user", "google-oauth2", "Google", None), + ("signin_user", "facebook", "Facebook", None), + ("register_user", "facebook", "Facebook", None), + ("signin_user", "dummy", "Dummy", None), + ("register_user", "dummy", "Dummy", None), + ( + "signin_user", + "google-oauth2", + "Google", + { + 'name': 'FakeName', + 'logo': 'https://host.com/logo.jpg', + 'welcome_msg': 'No message' + } + ) ) @ddt.unpack - def test_third_party_auth(self, url_name, current_backend, current_provider): + def test_third_party_auth( + self, + url_name, + current_backend, + current_provider, + expected_enterprise_customer_mock_attrs, + enterprise_customer_mock + ): params = [ ('course_id', 'course-v1:Org+Course+Run'), ('enrollment_action', 'enroll'), @@ -389,6 +407,21 @@ class StudentAccountLoginAndRegistrationTest(ThirdPartyAuthTestMixin, UrlResetMi ('next', '/custom/final/destination'), ] + if expected_enterprise_customer_mock_attrs: + expected_ec = mock.MagicMock( + branding_configuration=mock.MagicMock( + logo=mock.MagicMock( + url=expected_enterprise_customer_mock_attrs['logo'] + ), + welcome_message=expected_enterprise_customer_mock_attrs['welcome_msg'] + ) + ) + expected_ec.name = expected_enterprise_customer_mock_attrs['name'] + else: + expected_ec = None + + enterprise_customer_mock.return_value = expected_ec + # Simulate a running pipeline if current_backend is not None: pipeline_target = "student_account.views.third_party_auth.pipeline" @@ -426,7 +459,13 @@ class StudentAccountLoginAndRegistrationTest(ThirdPartyAuthTestMixin, UrlResetMi "registerUrl": self._third_party_login_url("google-oauth2", "register", params) }, ] - self._assert_third_party_auth_data(response, current_backend, current_provider, expected_providers) + self._assert_third_party_auth_data( + response, + current_backend, + current_provider, + expected_providers, + expected_ec + ) def test_hinted_login(self): params = [("next", "/courses/something/?tpa_hint=oa2-google-oauth2")] @@ -455,6 +494,59 @@ class StudentAccountLoginAndRegistrationTest(ThirdPartyAuthTestMixin, UrlResetMi target_status_code=302 ) + @mock.patch('student_account.views.enterprise_customer_for_request') + @ddt.data( + ('signin_user', False, None, None, None), + ('register_user', False, None, None, None), + ('signin_user', True, 'Fake EC', 'http://logo.com/logo.jpg', u'{enterprise_name} - {platform_name}'), + ('register_user', True, 'Fake EC', 'http://logo.com/logo.jpg', u'{enterprise_name} - {platform_name}'), + ('signin_user', True, 'Fake EC', None, u'{enterprise_name} - {platform_name}'), + ('register_user', True, 'Fake EC', None, u'{enterprise_name} - {platform_name}'), + ('signin_user', True, 'Fake EC', 'http://logo.com/logo.jpg', None), + ('register_user', True, 'Fake EC', 'http://logo.com/logo.jpg', None), + ('signin_user', True, 'Fake EC', None, None), + ('register_user', True, 'Fake EC', None, None), + ) + @ddt.unpack + def test_enterprise_register(self, url_name, ec_present, ec_name, logo_url, welcome_message, mock_get_ec): + """ + Verify that when an EnterpriseCustomer is received on the login and register views, + the appropriate sidebar is rendered. + """ + if ec_present: + mock_ec = mock_get_ec.return_value + mock_ec.name = ec_name + if logo_url: + mock_ec.branding_configuration.logo.url = logo_url + else: + mock_ec.branding_configuration.logo = None + if welcome_message: + mock_ec.branding_configuration.welcome_message = welcome_message + else: + del mock_ec.branding_configuration.welcome_message + else: + mock_get_ec.return_value = None + + response = self.client.get(reverse(url_name), HTTP_ACCEPT="text/html") + + enterprise_sidebar_div_id = u'enterprise-content-container' + + if not ec_present: + self.assertNotContains(response, text=enterprise_sidebar_div_id) + else: + self.assertContains(response, text=enterprise_sidebar_div_id) + if not welcome_message: + welcome_message = settings.ENTERPRISE_SPECIFIC_BRANDED_WELCOME_TEMPLATE + expected_message = welcome_message.format( + start_bold=u'', + end_bold=u'', + enterprise_name=ec_name, + platform_name=settings.PLATFORM_NAME + ) + self.assertContains(response, expected_message) + if logo_url: + self.assertContains(response, logo_url) + @override_settings(SITE_NAME=settings.MICROSITE_TEST_HOSTNAME) def test_microsite_uses_old_login_page(self): # Retrieve the login page from a microsite domain @@ -494,7 +586,7 @@ class StudentAccountLoginAndRegistrationTest(ThirdPartyAuthTestMixin, UrlResetMi self.assertEqual(resp['X-Frame-Options'], 'ALLOW') - def _assert_third_party_auth_data(self, response, current_backend, current_provider, providers): + def _assert_third_party_auth_data(self, response, current_backend, current_provider, providers, expected_ec): """Verify that third party auth info is rendered correctly in a DOM data attribute. """ finish_auth_url = None if current_backend: @@ -507,6 +599,9 @@ class StudentAccountLoginAndRegistrationTest(ThirdPartyAuthTestMixin, UrlResetMi "finishAuthUrl": finish_auth_url, "errorMessage": None, } + if expected_ec is not None: + # If we set an EnterpriseCustomer, third-party auth providers ought to be hidden. + auth_info['providers'] = [] auth_info = dump_js_escaped_json(auth_info) expected_data = '"third_party_auth": {auth_info}'.format( diff --git a/lms/djangoapps/student_account/views.py b/lms/djangoapps/student_account/views.py index d22c78409e..d9e51780a7 100644 --- a/lms/djangoapps/student_account/views.py +++ b/lms/djangoapps/student_account/views.py @@ -18,7 +18,7 @@ from django.utils.translation import ugettext as _ from django.views.decorators.csrf import ensure_csrf_cookie from django.views.decorators.http import require_http_methods from django_countries import countries -from edxmako.shortcuts import render_to_response +from edxmako.shortcuts import render_to_response, render_to_string import pytz from commerce.models import CommerceConfiguration @@ -34,7 +34,10 @@ from openedx.core.djangoapps.site_configuration import helpers as configuration_ from openedx.core.djangoapps.theming.helpers import is_request_in_themed_site from openedx.core.djangoapps.user_api.accounts.api import request_password_change from openedx.core.djangoapps.user_api.errors import UserNotFound -from openedx.features.enterprise_support.api import set_enterprise_branding_filter_param +from openedx.features.enterprise_support.api import ( + enterprise_customer_for_request, + set_enterprise_branding_filter_param +) from openedx.core.lib.time_zone_utils import TIME_ZONE_CHOICES from openedx.core.lib.edx_api_utils import get_edx_api_data from student.models import UserProfile @@ -144,6 +147,8 @@ def login_and_registration_form(request, initial_mode="login"): ), } + context = update_context_for_enterprise(request, context) + return render_to_response('student_account/login_and_register.html', context) @@ -197,6 +202,89 @@ def password_change_request_handler(request): return HttpResponseBadRequest(_("No email address provided.")) +def update_context_for_enterprise(request, context): + """ + Take the processed context produced by the view, determine if it's relevant + to a particular Enterprise Customer, and update it to include that customer's + enterprise metadata. + """ + + context = context.copy() + + sidebar_context = enterprise_sidebar_context(request) + + if sidebar_context: + context['data']['registration_form_desc']['fields'] = enterprise_fields_only( + context['data']['registration_form_desc'] + ) + context.update(sidebar_context) + context['enable_enterprise_sidebar'] = True + else: + context['enable_enterprise_sidebar'] = False + + return context + + +def enterprise_fields_only(fields): + """ + Take the received field definition, and exclude those fields that we don't want + to require if the user is going to be a member of an Enterprise Customer. + """ + enterprise_exclusions = configuration_helpers.get_value( + 'ENTERPRISE_EXCLUDED_REGISTRATION_FIELDS', + settings.ENTERPRISE_EXCLUDED_REGISTRATION_FIELDS + ) + return [field for field in fields['fields'] if field['name'] not in enterprise_exclusions] + + +def enterprise_sidebar_context(request): + """ + Given the current request, render the HTML of a sidebar for the current + logistration view that depicts Enterprise-related information. + """ + enterprise_customer = enterprise_customer_for_request(request) + + if not enterprise_customer: + return {} + + platform_name = configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME) + + if enterprise_customer.branding_configuration.logo: + enterprise_logo_url = enterprise_customer.branding_configuration.logo.url + else: + enterprise_logo_url = '' + + if getattr(enterprise_customer.branding_configuration, 'welcome_message', None): + branded_welcome_template = enterprise_customer.branding_configuration.welcome_message + else: + branded_welcome_template = configuration_helpers.get_value( + 'ENTERPRISE_SPECIFIC_BRANDED_WELCOME_TEMPLATE', + settings.ENTERPRISE_SPECIFIC_BRANDED_WELCOME_TEMPLATE + ) + + branded_welcome_string = branded_welcome_template.format( + start_bold=u'', + end_bold=u'', + enterprise_name=enterprise_customer.name, + platform_name=platform_name + ) + + platform_welcome_template = configuration_helpers.get_value( + 'ENTERPRISE_PLATFORM_WELCOME_TEMPLATE', + settings.ENTERPRISE_PLATFORM_WELCOME_TEMPLATE + ) + platform_welcome_string = platform_welcome_template.format(platform_name=platform_name) + + context = { + 'enterprise_name': enterprise_customer.name, + 'enterprise_logo_url': enterprise_logo_url, + 'enterprise_branded_welcome_string': branded_welcome_string, + 'platform_welcome_string': platform_welcome_string, + } + + return context + + def _third_party_auth_context(request, redirect_to, tpa_hint=None): """Context for third party auth providers and the currently running pipeline. @@ -221,24 +309,25 @@ def _third_party_auth_context(request, redirect_to, tpa_hint=None): } if third_party_auth.is_enabled(): - for enabled in third_party_auth.provider.Registry.displayed_for_login(tpa_hint=tpa_hint): - info = { - "id": enabled.provider_id, - "name": enabled.name, - "iconClass": enabled.icon_class or None, - "iconImage": enabled.icon_image.url if enabled.icon_image else None, - "loginUrl": pipeline.get_login_url( - enabled.provider_id, - pipeline.AUTH_ENTRY_LOGIN, - redirect_url=redirect_to, - ), - "registerUrl": pipeline.get_login_url( - enabled.provider_id, - pipeline.AUTH_ENTRY_REGISTER, - redirect_url=redirect_to, - ), - } - context["providers" if not enabled.secondary else "secondaryProviders"].append(info) + if not enterprise_customer_for_request(request): + for enabled in third_party_auth.provider.Registry.displayed_for_login(tpa_hint=tpa_hint): + info = { + "id": enabled.provider_id, + "name": enabled.name, + "iconClass": enabled.icon_class or None, + "iconImage": enabled.icon_image.url if enabled.icon_image else None, + "loginUrl": pipeline.get_login_url( + enabled.provider_id, + pipeline.AUTH_ENTRY_LOGIN, + redirect_url=redirect_to, + ), + "registerUrl": pipeline.get_login_url( + enabled.provider_id, + pipeline.AUTH_ENTRY_REGISTER, + redirect_url=redirect_to, + ), + } + context["providers" if not enabled.secondary else "secondaryProviders"].append(info) running_pipeline = pipeline.get(request) if running_pipeline is not None: diff --git a/lms/envs/aws.py b/lms/envs/aws.py index 1ef08b4343..29b7584d54 100644 --- a/lms/envs/aws.py +++ b/lms/envs/aws.py @@ -951,6 +951,25 @@ ENTERPRISE_API_CACHE_TIMEOUT = ENV_TOKENS.get( ENTERPRISE_API_CACHE_TIMEOUT ) +############## ENTERPRISE SERVICE LMS CONFIGURATION ################################## +# The LMS has some features embedded that are related to the Enterprise service, but +# which are not provided by the Enterprise service. These settings override the +# base values for the parameters as defined in common.py + +ENTERPRISE_PLATFORM_WELCOME_TEMPLATE = ENV_TOKENS.get( + 'ENTERPRISE_PLATFORM_WELCOME_TEMPLATE', + ENTERPRISE_PLATFORM_WELCOME_TEMPLATE +) +ENTERPRISE_SPECIFIC_BRANDED_WELCOME_TEMPLATE = ENV_TOKENS.get( + 'ENTERPRISE_SPECIFIC_BRANDED_WELCOME_TEMPLATE', + ENTERPRISE_SPECIFIC_BRANDED_WELCOME_TEMPLATE +) +ENTERPRISE_EXCLUDED_REGISTRATION_FIELDS = set( + ENV_TOKENS.get( + 'ENTERPRISE_EXCLUDED_REGISTRATION_FIELDS', + ENTERPRISE_EXCLUDED_REGISTRATION_FIELDS + ) +) ############## CATALOG/DISCOVERY SERVICE API CLIENT CONFIGURATION ###################### # The LMS communicates with the Catalog service via the EdxRestApiClient class diff --git a/lms/envs/common.py b/lms/envs/common.py index 2cc2aad53b..50342814d4 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -3142,6 +3142,26 @@ ENTERPRISE_SERVICE_WORKER_USERNAME = 'enterprise_worker' ENTERPRISE_API_CACHE_TIMEOUT = 3600 # Value is in seconds ENTERPRISE_CUSTOMER_LOGO_IMAGE_SIZE = 512 # Enterprise logo image size limit in KB's +############## ENTERPRISE SERVICE LMS CONFIGURATION ################################## +# The LMS has some features embedded that are related to the Enterprise service, but +# which are not provided by the Enterprise service. These settings provide base values +# for those features. + +ENTERPRISE_PLATFORM_WELCOME_TEMPLATE = _(u'Welcome to {platform_name}.') +ENTERPRISE_SPECIFIC_BRANDED_WELCOME_TEMPLATE = _( + u'{start_bold}{enterprise_name}{end_bold} has partnered with {start_bold}' + '{platform_name}{end_bold} to offer you high-quality learning opportunities ' + 'from the world\'s best universities.' +) +ENTERPRISE_EXCLUDED_REGISTRATION_FIELDS = { + 'age', + 'level_of_education', + 'gender', + 'goals', + 'year_of_birth', + 'mailing_address', +} + ############## Settings for Course Enrollment Modes ###################### COURSE_ENROLLMENT_MODES = { "audit": 1, diff --git a/lms/static/sass/views/_login-register.scss b/lms/static/sass/views/_login-register.scss index f58c1878a0..d99b80130e 100644 --- a/lms/static/sass/views/_login-register.scss +++ b/lms/static/sass/views/_login-register.scss @@ -3,16 +3,50 @@ @import '../base/grid-settings'; @import "neat/neat"; // lib - Neat -.login-register { +@media (min-width: 768px) { + .enterprise-content { + width: 20%; + float: left; + height: 100%; + padding-left: $baseline; + padding-right: $baseline; + } + + .login-register.border-left { + border-left: 1px solid #d9d9d9; + padding-left: ($baseline*1.5); + padding-right: $baseline; + } +} + +@media (max-width: 767px) { + .enterprise-content { + margin: auto auto; + display: block; + padding-left: ($baseline/2); + padding-right: ($baseline/2); + + img.enterprise-logo { + display: none; + } + } +} + +.window-wrap { + background: $white; +} + +.login-register-content { @include box-sizing(border-box); @include outer-container; - $grid-columns: 12; - background: $white; - min-height: 100%; width: 100%; - padding-left: ($baseline/2); - padding-right: ($baseline/2); - $third-party-button-height: ($baseline*1.75); + justify-content: center; + margin-top: $baseline; + background: $white; + display: flex; + flex-wrap: wrap; + -webkit-flex-wrap: wrap; + -moz-flex-wrap: wrap; h2 { @extend %t-title4; @@ -35,6 +69,21 @@ @extend %expand-clickable-area; } + a { + text-decoration: underline; + } +} + +.login-register { + $grid-columns: 12; + background: $white; + min-height: 100%; + padding-left: ($baseline/2); + padding-right: ($baseline/2); + $third-party-button-height: ($baseline*1.75); + display: inline-block; + max-width: 500px; + .instructions { @extend %t-copy-base; } @@ -587,3 +636,26 @@ .supplemental-link { margin: 1rem 0; } + +.enterprise-content { + display: inline-block; + text-align: left; + vertical-align: top; + max-width: 500px; + + .centered-div { + margin: 0 auto; + margin-right: 0px; + float: right; + } + + img { + height: 100px; + } + + h2 { + font-size: 16px; + line-height: 1.5; + color: $gray-d2; + } +} diff --git a/lms/templates/student_account/enterprise_sidebar.html b/lms/templates/student_account/enterprise_sidebar.html new file mode 100644 index 0000000000..7657840e70 --- /dev/null +++ b/lms/templates/student_account/enterprise_sidebar.html @@ -0,0 +1,13 @@ +