Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions common/test/acceptance/pages/lms/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,15 @@ def link_title_for_link_field(self, field_id):
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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a note (totally don't have to fix this) but in python since you can define a string with single or double quotes, if you define it with single quotes you don't have to escape the double quotes (and vice versa).

).fulfill()

def click_on_link_in_link_field(self, field_id):
"""
Click the link in a link field.
Expand Down
8 changes: 8 additions & 0 deletions common/test/acceptance/pages/lms/login_and_register.py
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,8 @@ def current_form(self):
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):
Expand Down Expand Up @@ -335,3 +337,9 @@ def _check_func():
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]
36 changes: 35 additions & 1 deletion common/test/acceptance/tests/lms/test_lms.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,41 @@ def test_third_party_login(self):

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)
Expand Down
5 changes: 5 additions & 0 deletions lms/djangoapps/student_account/test/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,11 @@ def test_third_party_auth(self, url_name, current_backend, current_provider):
]
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
Expand Down
15 changes: 15 additions & 0 deletions lms/djangoapps/student_account/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import logging
import json
import urlparse

from django.conf import settings
from django.contrib import messages
Expand Down Expand Up @@ -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"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: does it make sense to have this as a query param, rather than as a separate url?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My thought was that a query param is easier for partner universities to construct - just copy the URL to some part of the courseware, then append the query string part. If it's a separate URL we'd probably need to create a UI/tool that creates the URL for them, no? And there may be many different types of links that we'd need to accommodate - so several new URL patterns may be required.

Edit: I guess something like /hinted_login?next=... would work as well, as a new URL. It would just be harder for partner universities to construct such a URL. It is likely a cleaner and more generic approach though.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I that makes sense. I was thinking there was just one url, but if the idea is to just have any of our normal urls, but decorate with an optional query param, then that sounds fine.

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,

Expand Down
1 change: 1 addition & 0 deletions lms/envs/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -1272,6 +1272,7 @@
'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',
Expand Down
11 changes: 11 additions & 0 deletions lms/static/js/spec/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,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',
Expand Down Expand Up @@ -452,6 +453,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: [
Expand Down Expand Up @@ -605,6 +615,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',
Expand Down
71 changes: 71 additions & 0 deletions lms/static/js/spec/student_account/hinted_login_spec.js
Original file line number Diff line number Diff line change
@@ -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() {} );

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Weird spacing here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True; I had just copy-pasted that line from account_spec. If I need to amend this commit I'll fix that up :)


view.render();
};

beforeEach(function() {
setFixtures('<div id="hinted-login-form"></div>');
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' );
});
});
});
1 change: 1 addition & 0 deletions lms/static/js/student_account/accessApp.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down
18 changes: 16 additions & 2 deletions lms/static/js/student_account/views/AccessView.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ var edx = edx || {};
login: {},
register: {},
passwordHelp: {},
institutionLogin: {}
institutionLogin: {},
hintedLogin: {}
},

nextUrl: '/dashboard',
Expand All @@ -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 ) ) {
Expand All @@ -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;
Expand Down Expand Up @@ -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();
}
},

Expand Down
52 changes: 52 additions & 0 deletions lms/static/js/student_account/views/HintedLoginView.js
Original file line number Diff line number Diff line change
@@ -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);
4 changes: 4 additions & 0 deletions lms/templates/student_account/access.underscore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,7 @@
<section id="institution_login-anchor" class="form-type">
<div id="institution_login-form" class="form-wrapper hidden" aria-hidden="true"></div>
</section>

<section id="hinted-login-anchor" class="form-type">
<div id="hinted-login-form" class="form-wrapper <% if ( mode !== 'hinted_login' ) { %>hidden<% } %>"></div>
</section>
24 changes: 24 additions & 0 deletions lms/templates/student_account/hinted_login.underscore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<div class="wrapper-other-login">
<div class="section-title lines">
<h2>
<span class="text"><%- gettext("Sign in") %></span>
</h2>
</div>

<p class="instructions"><%- _.sprintf( gettext("Would you like to sign in using your %(providerName)s credentials?"), { providerName: hintedProvider.name } ) %></p>

<button class="action action-primary action-update proceed-button button-<%- hintedProvider.id %> hinted-login-<%- hintedProvider.id %>">
<div class="icon fa <%- hintedProvider.iconClass %>" aria-hidden="true"></div>
<%- _.sprintf( gettext("Sign in using %(providerName)s"), { providerName: hintedProvider.name } ) %>
</button>

<div class="section-title lines">
<h2>
<span class="text"><%- gettext("or") %></span>
</h2>
</div>

<div class="toggle-form">
<button class="nav-btn form-toggle" data-type="login"><%- gettext("Show me other ways to sign in or register") %></button>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there binding that I'm missing that makes this button work?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cpennington Yep, it's an existing handler for the page as a whole in AccessView: https://github.com/edx/edx-platform/pull/8591/files#diff-aa10400ca417043aab41f33394c90790R15

</div>
</div>
3 changes: 2 additions & 1 deletion lms/templates/student_account/login_and_register.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
</%block>

<%block name="header_extras">
% for template_name in ["account", "access", "form_field", "login", "register", "institution_login", "institution_register", "password_reset"]:
% for template_name in ["account", "access", "form_field", "login", "register", "institution_login", "institution_register", "password_reset", "hinted_login"]:
<script type="text/template" id="${template_name}-tpl">
<%static:include path="student_account/${template_name}.underscore" />
</script>
Expand All @@ -27,6 +27,7 @@
class="login-register"
data-initial-mode="${initial_mode}"
data-third-party-auth='${third_party_auth|h}'
data-third-party-auth-hint='${third_party_auth_hint}'
data-next-url='${login_redirect_url|h}'
data-platform-name='${platform_name}'
data-login-form-desc='${login_form_desc|h}'
Expand Down