diff --git a/common/test/acceptance/pages/lms/fields.py b/common/test/acceptance/pages/lms/fields.py index 803f5a7886..014aa11bdb 100644 --- a/common/test/acceptance/pages/lms/fields.py +++ b/common/test/acceptance/pages/lms/fields.py @@ -211,6 +211,15 @@ class FieldsMixin(object): query = self.q(css='.u-field-link-title-{}'.format(field_id)) return query.text[0] if query.present else None + def wait_for_link_title_for_link_field(self, field_id, expected_title): + """ + Wait until the title of the specified link field equals expected_title. + """ + return EmptyPromise( + lambda: self.link_title_for_link_field(field_id) == expected_title, + "Link field with link title \"{0}\" is visible.".format(expected_title) + ).fulfill() + def click_on_link_in_link_field(self, field_id): """ Click the link in a link field. diff --git a/common/test/acceptance/pages/lms/login_and_register.py b/common/test/acceptance/pages/lms/login_and_register.py index 283cb5028e..9cbd6af99f 100644 --- a/common/test/acceptance/pages/lms/login_and_register.py +++ b/common/test/acceptance/pages/lms/login_and_register.py @@ -281,6 +281,8 @@ class CombinedLoginAndRegisterPage(PageObject): return "login" elif self.q(css=".js-reset").visible: return "password-reset" + elif self.q(css=".proceed-button").visible: + return "hinted-login" @property def email_value(self): @@ -335,3 +337,9 @@ class CombinedLoginAndRegisterPage(PageObject): return (True, msg_element.text[0]) return (False, None) return Promise(_check_func, "Result of third party auth is visible").fulfill() + + @property + def hinted_login_prompt(self): + """Get the message displayed to the user on the hinted-login form""" + if self.q(css=".wrapper-other-login .instructions").visible: + return self.q(css=".wrapper-other-login .instructions").text[0] diff --git a/common/test/acceptance/tests/lms/test_lms.py b/common/test/acceptance/tests/lms/test_lms.py index 2d10d78935..00eb2b33cc 100644 --- a/common/test/acceptance/tests/lms/test_lms.py +++ b/common/test/acceptance/tests/lms/test_lms.py @@ -164,7 +164,41 @@ class LoginFromCombinedPageTest(UniqueCourseTest): self.dashboard_page.wait_for_page() - # Now unlink the account (To test the account settings view and also to prevent cross-test side effects) + self._unlink_dummy_account() + + def test_hinted_login(self): + """ Test the login page when coming from course URL that specified which third party provider to use """ + # Create a user account and link it to third party auth with the dummy provider: + AutoAuthPage(self.browser, course_id=self.course_id).visit() + self._link_dummy_account() + LogoutPage(self.browser).visit() + + # When not logged in, try to load a course URL that includes the provider hint ?tpa_hint=... + course_page = CoursewarePage(self.browser, self.course_id) + self.browser.get(course_page.url + '?tpa_hint=oa2-dummy') + + # We should now be redirected to the login page + self.login_page.wait_for_page() + self.assertIn("Would you like to sign in using your Dummy credentials?", self.login_page.hinted_login_prompt) + self.login_page.click_third_party_dummy_provider() + + # We should now be redirected to the course page + course_page.wait_for_page() + + self._unlink_dummy_account() + + def _link_dummy_account(self): + """ Go to Account Settings page and link the user's account to the Dummy provider """ + account_settings = AccountSettingsPage(self.browser).visit() + field_id = "auth-oa2-dummy" + account_settings.wait_for_field(field_id) + self.assertEqual("Link", account_settings.link_title_for_link_field(field_id)) + account_settings.click_on_link_in_link_field(field_id) + account_settings.wait_for_link_title_for_link_field(field_id, "Unlink") + + def _unlink_dummy_account(self): + """ Verify that the 'Dummy' third party auth provider is linked, then unlink it """ + # This must be done after linking the account, or we'll get cross-test side effects account_settings = AccountSettingsPage(self.browser).visit() field_id = "auth-oa2-dummy" account_settings.wait_for_field(field_id) diff --git a/lms/djangoapps/student_account/test/test_views.py b/lms/djangoapps/student_account/test/test_views.py index c11c858c7d..786c3427b7 100644 --- a/lms/djangoapps/student_account/test/test_views.py +++ b/lms/djangoapps/student_account/test/test_views.py @@ -329,6 +329,11 @@ class StudentAccountLoginAndRegistrationTest(ThirdPartyAuthTestMixin, UrlResetMi ] self._assert_third_party_auth_data(response, current_backend, current_provider, expected_providers) + def test_hinted_login(self): + params = [("next", "/courses/something/?tpa_hint=oa2-google-oauth2")] + response = self.client.get(reverse('account_login'), params) + self.assertContains(response, "data-third-party-auth-hint='oa2-google-oauth2'") + @override_settings(SITE_NAME=settings.MICROSITE_TEST_HOSTNAME) def test_microsite_uses_old_login_page(self): # Retrieve the login page from a microsite domain diff --git a/lms/djangoapps/student_account/views.py b/lms/djangoapps/student_account/views.py index ffc10c04b4..76602b1337 100644 --- a/lms/djangoapps/student_account/views.py +++ b/lms/djangoapps/student_account/views.py @@ -2,6 +2,7 @@ import logging import json +import urlparse from django.conf import settings from django.contrib import messages @@ -77,12 +78,26 @@ def login_and_registration_form(request, initial_mode="login"): if ext_auth_response is not None: return ext_auth_response + # Our ?next= URL may itself contain a parameter 'tpa_hint=x' that we need to check. + # If present, we display a login page focused on third-party auth with that provider. + third_party_auth_hint = None + if '?' in redirect_to: + try: + next_args = urlparse.parse_qs(urlparse.urlparse(redirect_to).query) + provider_id = next_args['tpa_hint'][0] + if third_party_auth.provider.Registry.get(provider_id=provider_id): + third_party_auth_hint = provider_id + initial_mode = "hinted_login" + except (KeyError, ValueError, IndexError): + pass + # Otherwise, render the combined login/registration page context = { 'login_redirect_url': redirect_to, # This gets added to the query string of the "Sign In" button in the header 'disable_courseware_js': True, 'initial_mode': initial_mode, 'third_party_auth': json.dumps(_third_party_auth_context(request, redirect_to)), + 'third_party_auth_hint': third_party_auth_hint or '', 'platform_name': settings.PLATFORM_NAME, 'responsive': True, diff --git a/lms/envs/common.py b/lms/envs/common.py index da207b500f..e58fe8db64 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -1271,6 +1271,7 @@ student_account_js = [ 'js/student_account/models/PasswordResetModel.js', 'js/student_account/views/FormView.js', 'js/student_account/views/LoginView.js', + 'js/student_account/views/HintedLoginView.js', 'js/student_account/views/RegisterView.js', 'js/student_account/views/PasswordResetView.js', 'js/student_account/views/AccessView.js', diff --git a/lms/static/js/spec/main.js b/lms/static/js/spec/main.js index 6420609847..2640cd23d5 100644 --- a/lms/static/js/spec/main.js +++ b/lms/static/js/spec/main.js @@ -90,6 +90,7 @@ 'js/student_account/models/RegisterModel': 'js/student_account/models/RegisterModel', 'js/student_account/views/RegisterView': 'js/student_account/views/RegisterView', 'js/student_account/views/AccessView': 'js/student_account/views/AccessView', + 'js/student_account/views/HintedLoginView': 'js/student_account/views/HintedLoginView', 'js/student_profile/profile': 'js/student_profile/profile', 'js/student_profile/views/learner_profile_fields': 'js/student_profile/views/learner_profile_fields', 'js/student_profile/views/learner_profile_factory': 'js/student_profile/views/learner_profile_factory', @@ -448,6 +449,15 @@ 'js/student_account/views/FormView' ] }, + 'js/student_account/views/HintedLoginView': { + exports: 'edx.student.account.HintedLoginView', + deps: [ + 'jquery', + 'underscore', + 'backbone', + 'gettext' + ] + }, 'js/student_account/views/AccessView': { exports: 'edx.student.account.AccessView', deps: [ @@ -622,6 +632,7 @@ 'lms/include/js/spec/student_account/account_spec.js', 'lms/include/js/spec/student_account/access_spec.js', 'lms/include/js/spec/student_account/finish_auth_spec.js', + 'lms/include/js/spec/student_account/hinted_login_spec.js', 'lms/include/js/spec/student_account/login_spec.js', 'lms/include/js/spec/student_account/institution_login_spec.js', 'lms/include/js/spec/student_account/register_spec.js', diff --git a/lms/static/js/spec/student_account/hinted_login_spec.js b/lms/static/js/spec/student_account/hinted_login_spec.js new file mode 100644 index 0000000000..b6f346a56e --- /dev/null +++ b/lms/static/js/spec/student_account/hinted_login_spec.js @@ -0,0 +1,71 @@ +define([ + 'jquery', + 'underscore', + 'common/js/spec_helpers/template_helpers', + 'common/js/spec_helpers/ajax_helpers', + 'js/student_account/views/HintedLoginView', +], function($, _, TemplateHelpers, AjaxHelpers, HintedLoginView) { + 'use strict'; + describe('edx.student.account.HintedLoginView', function() { + + var view = null, + requests = null, + PLATFORM_NAME = 'edX', + THIRD_PARTY_AUTH = { + currentProvider: null, + providers: [ + { + id: 'oa2-google-oauth2', + name: 'Google', + iconClass: 'fa-google-plus', + loginUrl: '/auth/login/google-oauth2/?auth_entry=account_login', + registerUrl: '/auth/login/google-oauth2/?auth_entry=account_register' + }, + { + id: 'oa2-facebook', + name: 'Facebook', + iconClass: 'fa-facebook', + loginUrl: '/auth/login/facebook/?auth_entry=account_login', + registerUrl: '/auth/login/facebook/?auth_entry=account_register' + } + ] + }, + HINTED_PROVIDER = "oa2-google-oauth2"; + + var createHintedLoginView = function(test) { + // Initialize the login view + view = new HintedLoginView({ + thirdPartyAuth: THIRD_PARTY_AUTH, + hintedProvider: HINTED_PROVIDER, + platformName: PLATFORM_NAME + }); + + // Mock the redirect call + spyOn( view, 'redirect' ).andCallFake( function() {} ); + + view.render(); + }; + + beforeEach(function() { + setFixtures('
'); + TemplateHelpers.installTemplate('templates/student_account/hinted_login'); + }); + + it('displays a choice as two buttons', function() { + createHintedLoginView(this); + + expect($('.proceed-button.button-oa2-google-oauth2')).toBeVisible(); + expect($('.form-toggle')).toBeVisible(); + expect($('.proceed-button.button-oa2-facebook')).not.toBeVisible(); + }); + + it('redirects the user to the hinted provider if the user clicks the proceed button', function() { + createHintedLoginView(this); + + // Click the "Yes, proceed" button + $('.proceed-button').click(); + + expect(view.redirect).toHaveBeenCalledWith( '/auth/login/google-oauth2/?auth_entry=account_login' ); + }); + }); +}); diff --git a/lms/static/js/student_account/accessApp.js b/lms/static/js/student_account/accessApp.js index 7c6579b536..2510bc1cff 100644 --- a/lms/static/js/student_account/accessApp.js +++ b/lms/static/js/student_account/accessApp.js @@ -11,6 +11,7 @@ var edx = edx || {}; return new edx.student.account.AccessView({ mode: container.data('initial-mode'), thirdPartyAuth: container.data('third-party-auth'), + thirdPartyAuthHint: container.data('third-party-auth-hint'), nextUrl: container.data('next-url'), platformName: container.data('platform-name'), loginFormDesc: container.data('login-form-desc'), diff --git a/lms/static/js/student_account/views/AccessView.js b/lms/static/js/student_account/views/AccessView.js index 4374eed3d2..a909c61fae 100644 --- a/lms/static/js/student_account/views/AccessView.js +++ b/lms/static/js/student_account/views/AccessView.js @@ -19,7 +19,8 @@ var edx = edx || {}; login: {}, register: {}, passwordHelp: {}, - institutionLogin: {} + institutionLogin: {}, + hintedLogin: {} }, nextUrl: '/dashboard', @@ -43,6 +44,8 @@ var edx = edx || {}; providers: [] }; + this.thirdPartyAuthHint = obj.thirdPartyAuthHint || null; + if (obj.nextUrl) { // Ensure that the next URL is internal for security reasons if ( ! window.isExternal( obj.nextUrl ) ) { @@ -54,7 +57,8 @@ var edx = edx || {}; login: obj.loginFormDesc, register: obj.registrationFormDesc, reset: obj.passwordResetFormDesc, - institution_login: null + institution_login: null, + hinted_login: null }; this.platformName = obj.platformName; @@ -160,6 +164,16 @@ var edx = edx || {}; }); this.subview.institutionLogin.render(); + }, + + hinted_login: function ( unused ) { + this.subview.hintedLogin = new edx.student.account.HintedLoginView({ + thirdPartyAuth: this.thirdPartyAuth, + hintedProvider: this.thirdPartyAuthHint, + platformName: this.platformName + }); + + this.subview.hintedLogin.render(); } }, diff --git a/lms/static/js/student_account/views/HintedLoginView.js b/lms/static/js/student_account/views/HintedLoginView.js new file mode 100644 index 0000000000..ae178e00ef --- /dev/null +++ b/lms/static/js/student_account/views/HintedLoginView.js @@ -0,0 +1,52 @@ +var edx = edx || {}; + +(function($, _, gettext) { + 'use strict'; + + edx.student = edx.student || {}; + edx.student.account = edx.student.account || {}; + + edx.student.account.HintedLoginView = Backbone.View.extend({ + el: '#hinted-login-form', + + tpl: '#hinted_login-tpl', + + events: { + 'click .proceed-button': 'proceedWithHintedAuth' + }, + + formType: 'hinted-login', + + initialize: function( data ) { + this.tpl = $(this.tpl).html(); + this.providers = data.thirdPartyAuth.providers || []; + this.hintedProvider = _.findWhere(this.providers, {id: data.hintedProvider}) + this.platformName = data.platformName; + + }, + + render: function() { + $(this.el).html( _.template( this.tpl, { + // We pass the context object to the template so that + // we can perform variable interpolation using sprintf + providers: this.providers, + platformName: this.platformName, + hintedProvider: this.hintedProvider + })); + + return this; + }, + + proceedWithHintedAuth: function( event ) { + this.redirect(this.hintedProvider.loginUrl); + }, + + /** + * Redirect to a URL. Mainly useful for mocking out in tests. + * @param {string} url The URL to redirect to. + */ + redirect: function( url ) { + window.location.href = url; + } + }); +})(jQuery, _, gettext); diff --git a/lms/templates/student_account/access.underscore b/lms/templates/student_account/access.underscore index fff58d5cbf..a2bc97030f 100644 --- a/lms/templates/student_account/access.underscore +++ b/lms/templates/student_account/access.underscore @@ -13,3 +13,7 @@<%- _.sprintf( gettext("Would you like to sign in using your %(providerName)s credentials?"), { providerName: hintedProvider.name } ) %>
+ + + +