diff --git a/lms/djangoapps/student_account/test/test_views.py b/lms/djangoapps/student_account/test/test_views.py index 8bca85b5ca..f796ec4750 100644 --- a/lms/djangoapps/student_account/test/test_views.py +++ b/lms/djangoapps/student_account/test/test_views.py @@ -866,10 +866,10 @@ class AccountSettingsViewTest(ThirdPartyAuthTestMixin, TestCase, ProgramsApiConf MessageMiddleware().process_request(self.request) messages.error(self.request, 'Facebook is already in use.', extra_tags='Auth facebook') - @mock.patch('student_account.views.get_enterprise_learner_data') - def test_context(self, mock_get_enterprise_learner_data): + @mock.patch('openedx.features.enterprise_support.api.get_enterprise_customer_for_learner') + def test_context(self, mock_get_enterprise_customer_for_learner): self.request.site = SiteFactory.create() - mock_get_enterprise_learner_data.return_value = [] + mock_get_enterprise_customer_for_learner.return_value = {} context = account_settings_context(self.request) user_accounts_api_url = reverse("accounts_api", kwargs={'username': self.user.username}) @@ -899,19 +899,17 @@ class AccountSettingsViewTest(ThirdPartyAuthTestMixin, TestCase, ProgramsApiConf context['enterprise_readonly_account_fields'], {'fields': settings.ENTERPRISE_READONLY_ACCOUNT_FIELDS} ) - @mock.patch('student_account.views.get_enterprise_learner_data') - @mock.patch('student_account.views.third_party_auth.provider.Registry.get') + @mock.patch('student_account.views.get_enterprise_customer_for_learner') + @mock.patch('openedx.features.enterprise_support.utils.third_party_auth.provider.Registry.get') def test_context_for_enterprise_learner( - self, mock_get_auth_provider, mock_get_enterprise_learner_data + self, mock_get_auth_provider, mock_get_enterprise_customer_for_learner ): dummy_enterprise_customer = { 'uuid': 'real-ent-uuid', 'name': 'Dummy Enterprise', 'identity_provider': 'saml-ubc' } - mock_get_enterprise_learner_data.return_value = [ - {'enterprise_customer': dummy_enterprise_customer} - ] + mock_get_enterprise_customer_for_learner.return_value = dummy_enterprise_customer self.request.site = SiteFactory.create() mock_get_auth_provider.return_value.sync_learner_profile_data = True context = account_settings_context(self.request) diff --git a/lms/djangoapps/student_account/views.py b/lms/djangoapps/student_account/views.py index 3a6fcc62b7..c3200e6522 100644 --- a/lms/djangoapps/student_account/views.py +++ b/lms/djangoapps/student_account/views.py @@ -39,9 +39,13 @@ from openedx.core.djangoapps.user_api.errors import ( ) from openedx.core.lib.edx_api_utils import get_edx_api_data from openedx.core.lib.time_zone_utils import TIME_ZONE_CHOICES -from openedx.features.enterprise_support.api import enterprise_customer_for_request, get_enterprise_learner_data +from openedx.features.enterprise_support.api import enterprise_customer_for_request, get_enterprise_customer_for_learner from student.cookies import set_experiments_is_enterprise_cookie -from openedx.features.enterprise_support.utils import update_third_party_auth_context_for_enterprise +from openedx.features.enterprise_support.utils import ( + handle_enterprise_cookies_for_logistration, + update_logistration_context_for_enterprise, + update_account_settings_context_for_enterprise, +) from student.helpers import destroy_oauth_tokens, get_next_url_for_login_page from student.models import UserProfile from student.views import register_user as old_register_view @@ -160,20 +164,11 @@ def login_and_registration_form(request, initial_mode="login"): ), } - context = update_context_for_enterprise(request, context) + enterprise_customer = enterprise_customer_for_request(request) + update_logistration_context_for_enterprise(request, context, enterprise_customer) response = render_to_response('student_account/login_and_register.html', context) - - # This cookie can be used for tests or minor features, - # but should not be used for payment related or other critical work - # since users can edit their cookies - set_experiments_is_enterprise_cookie(request, response, context['enable_enterprise_sidebar']) - - # Remove enterprise cookie so that subsequent requests show default login page. - response.delete_cookie( - configuration_helpers.get_value("ENTERPRISE_CUSTOMER_COOKIE_NAME", settings.ENTERPRISE_CUSTOMER_COOKIE_NAME), - domain=configuration_helpers.get_value("BASE_COOKIE_DOMAIN", settings.BASE_COOKIE_DOMAIN), - ) + handle_enterprise_cookies_for_logistration(request, response, context) return response @@ -232,85 +227,6 @@ 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 - context['data']['hide_auth_warnings'] = 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) - - branding_configuration = enterprise_customer.get('branding_configuration', {}) - logo_url = branding_configuration.get('logo', '') if isinstance(branding_configuration, dict) 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': 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. @@ -337,26 +253,24 @@ def _third_party_auth_context(request, redirect_to, tpa_hint=None): } if third_party_auth.is_enabled(): - enterprise_customer = enterprise_customer_for_request(request) - if not enterprise_customer: - 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) + 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: @@ -368,19 +282,8 @@ def _third_party_auth_context(request, redirect_to, tpa_hint=None): context["syncLearnerProfileData"] = current_provider.sync_learner_profile_data if current_provider.skip_registration_form: - # For enterprise (and later for everyone), we need to get explicit consent to the - # Terms of service instead of auto submitting the registration form outright. - if not enterprise_customer: - # As a reliable way of "skipping" the registration form, we just submit it automatically - context["autoSubmitRegForm"] = True - else: - context["autoRegisterWelcomeMessage"] = ( - 'Thank you for joining {}. ' - 'Just a couple steps before you start learning!' - ).format( - configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME) - ) - context["registerFormSubmitButtonText"] = _("Continue") + # As a reliable way of "skipping" the registration form, we just submit it automatically + context["autoSubmitRegForm"] = True # Check for any error messages we may want to display: for msg in messages.get_messages(request): @@ -389,8 +292,6 @@ def _third_party_auth_context(request, redirect_to, tpa_hint=None): context['errorMessage'] = _(unicode(msg)) # pylint: disable=translation-of-non-string break - context = update_third_party_auth_context_for_enterprise(context, enterprise_customer) - return context @@ -638,21 +539,8 @@ def account_settings_context(request): 'extended_profile_fields': _get_extended_profile_fields(), } - enterprise_customer_name = None - sync_learner_profile_data = False - enterprise_learner_data = get_enterprise_learner_data(site=request.site, user=request.user) - if enterprise_learner_data: - enterprise_customer_name = enterprise_learner_data[0]['enterprise_customer']['name'] - enterprise_idp = enterprise_learner_data[0]['enterprise_customer']['identity_provider'] - identity_provider = third_party_auth.provider.Registry.get(provider_id=enterprise_idp) - sync_learner_profile_data = identity_provider.sync_learner_profile_data if identity_provider else False - - context['sync_learner_profile_data'] = sync_learner_profile_data - context['edx_support_url'] = configuration_helpers.get_value('SUPPORT_SITE_LINK', settings.SUPPORT_SITE_LINK) - context['enterprise_name'] = enterprise_customer_name - context['enterprise_readonly_account_fields'] = { - 'fields': settings.ENTERPRISE_READONLY_ACCOUNT_FIELDS - } + enterprise_customer = get_enterprise_customer_for_learner(site=request.site, user=request.user) + update_account_settings_context_for_enterprise(context, enterprise_customer) if third_party_auth.is_enabled(): # If the account on the third party provider is already connected with another edX account, diff --git a/openedx/features/enterprise_support/api.py b/openedx/features/enterprise_support/api.py index a35350a846..ed614f7ca9 100644 --- a/openedx/features/enterprise_support/api.py +++ b/openedx/features/enterprise_support/api.py @@ -485,6 +485,20 @@ def get_enterprise_learner_data(site, user): return enterprise_learner_data['results'] +def get_enterprise_customer_for_learner(site, user): + """ + Return enterprise customer to whom given learner belongs. + """ + if not enterprise_enabled(): + return {} + + enterprise_learner_data = get_enterprise_learner_data(site, user) + if enterprise_learner_data: + return enterprise_learner_data[0]['enterprise_customer'] + + return {} + + 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. diff --git a/openedx/features/enterprise_support/utils.py b/openedx/features/enterprise_support/utils.py index 8878f2da04..3620749637 100644 --- a/openedx/features/enterprise_support/utils.py +++ b/openedx/features/enterprise_support/utils.py @@ -5,6 +5,11 @@ import hashlib import six from django.conf import settings from django.utils.translation import ugettext as _ + +import third_party_auth +from third_party_auth import pipeline +from student.cookies import set_experiments_is_enterprise_cookie + from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers from openedx.core.djangolib.markup import HTML, Text @@ -33,11 +38,96 @@ def get_cache_key(**kwargs): return hashlib.md5(key).hexdigest() -def update_third_party_auth_context_for_enterprise(context, enterprise_customer=None): +def update_logistration_context_for_enterprise(request, context, enterprise_customer): + """ + 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. + + Arguments: + request (HttpRequest): The request for the logistration page. + context (dict): Context for logistration page. + enterprise_customer (dict): data for enterprise customer + + """ + sidebar_context = {} + if enterprise_customer: + sidebar_context = get_enterprise_sidebar_context(enterprise_customer) + + 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 + context['data']['hide_auth_warnings'] = True + else: + context['enable_enterprise_sidebar'] = False + + update_third_party_auth_context_for_enterprise(request, context, enterprise_customer) + + +def get_enterprise_sidebar_context(enterprise_customer): + """ + Get context information for enterprise sidebar for the given enterprise customer. + + Enterprise Sidebar Context has the following key-value pairs. + { + 'enterprise_name': 'Enterprise Name', + 'enterprise_logo_url': 'URL of the enterprise logo image', + 'enterprise_branded_welcome_string': 'Human readable welcome message customized for the enterprise', + 'platform_welcome_string': 'Human readable welcome message for an enterprise learner', + } + """ + platform_name = configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME) + + branding_configuration = enterprise_customer.get('branding_configuration', {}) + logo_url = branding_configuration.get('logo', '') if isinstance(branding_configuration, dict) 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) + + return { + 'enterprise_name': enterprise_customer['name'], + 'enterprise_logo_url': logo_url, + 'enterprise_branded_welcome_string': branded_welcome_string, + 'platform_welcome_string': platform_welcome_string, + } + + +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 update_third_party_auth_context_for_enterprise(request, context, enterprise_customer=None): """ Return updated context of third party auth with modified for enterprise. Arguments: + request (HttpRequest): The request for the logistration page. context (dict): Context for third party auth providers and auth pipeline. enterprise_customer (dict): data for enterprise customer @@ -45,8 +135,8 @@ def update_third_party_auth_context_for_enterprise(context, enterprise_customer= context (dict): Updated context of third party auth with modified `errorMessage`. """ - if enterprise_customer and context['errorMessage']: - context['errorMessage'] = Text(_( + if enterprise_customer and context['data']['third_party_auth']['errorMessage']: + context['data']['third_party_auth']['errorMessage'] = Text(_( u'We are sorry, you are not authorized to access {platform_name} via this channel. ' u'Please contact your {enterprise} administrator in order to access {platform_name} ' u'or contact {edx_support_link}.{line_break}' @@ -55,7 +145,7 @@ def update_third_party_auth_context_for_enterprise(context, enterprise_customer= ).format( platform_name=configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME), enterprise=enterprise_customer['name'], - error_message=context['errorMessage'], + error_message=context['data']['third_party_auth']['errorMessage'], edx_support_link=HTML( '{support_url_name}' ).format( @@ -67,4 +157,74 @@ def update_third_party_auth_context_for_enterprise(context, enterprise_customer= line_break=HTML('
') ) + if enterprise_customer: + context['data']['third_party_auth']['providers'] = [] + context['data']['third_party_auth']['secondaryProviders'] = [] + + running_pipeline = pipeline.get(request) + if running_pipeline is not None: + current_provider = third_party_auth.provider.Registry.get_from_pipeline(running_pipeline) + if current_provider is not None and current_provider.skip_registration_form and enterprise_customer: + # For enterprise (and later for everyone), we need to get explicit consent to the + # Terms of service instead of auto submitting the registration form outright. + context['data']['third_party_auth']['autoSubmitRegForm'] = False + context['data']['third_party_auth']['autoRegisterWelcomeMessage'] = Text(_( + 'Thank you for joining {platform_name}. ' + 'Just a couple steps before you start learning!') + ).format( + platform_name=configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME) + ) + context['data']['third_party_auth']['registerFormSubmitButtonText'] = _('Continue') + return context + + +def handle_enterprise_cookies_for_logistration(request, response, context): + """ + Helper method for setting or deleting enterprise cookies on logistration response. + + Arguments: + request (HttpRequest): The request for the logistration page. + response (HttpResponse): The response for the logistration page. + context (dict): Context for logistration page. + + """ + # This cookie can be used for tests or minor features, + # but should not be used for payment related or other critical work + # since users can edit their cookies + set_experiments_is_enterprise_cookie(request, response, context['enable_enterprise_sidebar']) + + # Remove enterprise cookie so that subsequent requests show default login page. + response.delete_cookie( + configuration_helpers.get_value('ENTERPRISE_CUSTOMER_COOKIE_NAME', settings.ENTERPRISE_CUSTOMER_COOKIE_NAME), + domain=configuration_helpers.get_value('BASE_COOKIE_DOMAIN', settings.BASE_COOKIE_DOMAIN), + ) + + +def update_account_settings_context_for_enterprise(context, enterprise_customer): + """ + Take processed context for account settings page and update it taking enterprise customer into account. + + Arguments: + context (dict): Context for account settings page. + enterprise_customer (dict): data for enterprise customer + + """ + enterprise_context = { + 'enterprise_name': None, + 'sync_learner_profile_data': False, + 'edx_support_url': configuration_helpers.get_value('SUPPORT_SITE_LINK', settings.SUPPORT_SITE_LINK), + 'enterprise_readonly_account_fields': { + 'fields': settings.ENTERPRISE_READONLY_ACCOUNT_FIELDS + } + } + + if enterprise_customer: + enterprise_context['enterprise_name'] = enterprise_customer['name'] + identity_provider = third_party_auth.provider.Registry.get( + provider_id=enterprise_customer['identity_provider'], + ) + if identity_provider: + enterprise_context['sync_learner_profile_data'] = identity_provider.sync_learner_profile_data + + context.update(enterprise_context)