<%- _.sprintf(gettext("%(field)s must have at least %(count)d characters."), context) %>
',
- max: '
<%- _.sprintf(gettext("%(field)s can only contain up to %(count)d characters."), context) %>
',
- required: '
<%- _.sprintf(gettext("The %(field)s field cannot be empty."), context) %>
',
+ min: '
<%- _.sprintf( gettext("%(field)s must have at least %(count)d characters."), context ) %>
',
+ max: '
<%- _.sprintf( gettext("%(field)s can only contain up to %(count)d characters."), context ) %>
',
+ required: '
<%- _.sprintf( gettext("Please enter your %(field)s."), context ) %>
',
custom: '
<%= content %>
'
},
diff --git a/common/test/acceptance/pages/lms/login_and_register.py b/common/test/acceptance/pages/lms/login_and_register.py
index 1bda4fa2d8..3714179858 100644
--- a/common/test/acceptance/pages/lms/login_and_register.py
+++ b/common/test/acceptance/pages/lms/login_and_register.py
@@ -120,8 +120,8 @@ class CombinedLoginAndRegisterPage(PageObject):
def is_browser_on_page(self):
"""Check whether the combined login/registration page has loaded. """
return (
- self.q(css="#register-option").is_present() and
- self.q(css="#login-option").is_present() and
+ self.q(css="#login-anchor").is_present() and
+ self.q(css="#register-anchor").is_present() and
self.current_form is not None
)
@@ -130,7 +130,10 @@ class CombinedLoginAndRegisterPage(PageObject):
old_form = self.current_form
# Toggle the form
- self.q(css=".form-toggle:not(:checked)").click()
+ if old_form == "login":
+ self.q(css=".form-toggle[data-type='register']").click()
+ else:
+ self.q(css=".form-toggle[data-type='login']").click()
# Wait for the form to change before returning
EmptyPromise(
@@ -157,9 +160,9 @@ class CombinedLoginAndRegisterPage(PageObject):
"""
# Fill in the form
self.q(css="#register-email").fill(email)
- self.q(css="#register-password").fill(password)
- self.q(css="#register-username").fill(username)
self.q(css="#register-name").fill(full_name)
+ self.q(css="#register-username").fill(username)
+ self.q(css="#register-password").fill(password)
if country:
self.q(css="#register-country option[value='{country}']".format(country=country)).click()
if (terms_of_service):
@@ -168,7 +171,7 @@ class CombinedLoginAndRegisterPage(PageObject):
# Submit it
self.q(css=".register-button").click()
- def login(self, email="", password="", remember_me=True):
+ def login(self, email="", password=""):
"""Fills in and submits the login form.
Requires that the "login" form is visible.
@@ -179,14 +182,11 @@ class CombinedLoginAndRegisterPage(PageObject):
Keyword Arguments:
email (unicode): The user's email address.
password (unicode): The user's password.
- remember_me (boolean): If True, check the "remember me" box.
"""
# Fill in the form
self.q(css="#login-email").fill(email)
self.q(css="#login-password").fill(password)
- if remember_me:
- self.q(css="#login-remember").click()
# Submit it
self.q(css=".login-button").click()
@@ -217,6 +217,8 @@ class CombinedLoginAndRegisterPage(PageObject):
# Submit it
self.q(css="button.js-reset").click()
+ return CombinedLoginAndRegisterPage(self.browser).wait_for_page()
+
@property
@unguarded
def current_form(self):
@@ -233,7 +235,7 @@ class CombinedLoginAndRegisterPage(PageObject):
return "register"
elif self.q(css=".login-button").visible:
return "login"
- elif self.q(css=".js-reset").visible or self.q(css=".js-reset-success").visible:
+ elif self.q(css=".js-reset").visible:
return "password-reset"
@property
diff --git a/common/test/acceptance/tests/lms/test_lms.py b/common/test/acceptance/tests/lms/test_lms.py
index 09853250bf..94dd57d64a 100644
--- a/common/test/acceptance/tests/lms/test_lms.py
+++ b/common/test/acceptance/tests/lms/test_lms.py
@@ -218,9 +218,9 @@ class RegisterFromCombinedPageTest(UniqueCourseTest):
# Verify that the expected errors are displayed.
errors = self.register_page.wait_for_errors()
- self.assertIn(u'The Username field cannot be empty.', errors)
+ self.assertIn(u'Please enter your Public username.', errors)
self.assertIn(u'You must agree to the edX Terms of Service and Honor Code.', errors)
- self.assertIn(u'The Country field cannot be empty.', errors)
+ self.assertIn(u'Please select your Country.', errors)
def test_toggle_to_login_form(self):
self.register_page.visit().toggle_form()
diff --git a/lms/djangoapps/courseware/features/homepage.feature b/lms/djangoapps/courseware/features/homepage.feature
index c66d06694a..ac9b91e8d1 100644
--- a/lms/djangoapps/courseware/features/homepage.feature
+++ b/lms/djangoapps/courseware/features/homepage.feature
@@ -1,12 +1,12 @@
@shard_1
Feature: LMS.Homepage for web users
In order to get an idea what edX is about
- As a an anonymous web user
+ As an anonymous web user
I want to check the information on the home page
- Scenario: User can see the "Login" button
+ Scenario: User can see the "Sign in" button
Given I visit the homepage
- Then I should see a link called "Log in"
+ Then I should see a link called "Sign in"
Scenario: User can see the "Register Now" button
Given I visit the homepage
diff --git a/lms/djangoapps/courseware/features/login.feature b/lms/djangoapps/courseware/features/login.feature
index 3576a1adaf..acb5b38e7e 100644
--- a/lms/djangoapps/courseware/features/login.feature
+++ b/lms/djangoapps/courseware/features/login.feature
@@ -8,7 +8,7 @@ Feature: LMS.Login in as a registered user
Given I am an edX user
And I am an unactivated user
And I visit the homepage
- When I click the link with the text "Log in"
+ When I click the link with the text "Sign in"
And I submit my credentials on the login form
Then I should see the login error message "This account has not been activated"
@@ -18,15 +18,15 @@ Feature: LMS.Login in as a registered user
Given I am an edX user
And I am an activated user
And I visit the homepage
- When I click the link with the text "Log in"
+ When I click the link with the text "Sign in"
And I submit my credentials on the login form
Then I should be on the dashboard page
Scenario: Logout of a signed in account
Given I am logged in
When I click the dropdown arrow
- And I click the link with the text "Log Out"
- Then I should see a link with the text "Log in"
+ And I click the link with the text "Sign out"
+ Then I should see a link with the text "Sign in"
And I should see that the path is "/"
Scenario: Login with valid redirect
diff --git a/lms/static/js/spec/main.js b/lms/static/js/spec/main.js
index 4db456cc9c..82594d4af7 100644
--- a/lms/static/js/spec/main.js
+++ b/lms/static/js/spec/main.js
@@ -56,6 +56,7 @@
'string_utils': 'xmodule_js/common_static/js/src/string_utils',
// Manually specify LMS files that are not converted to RequireJS
+ 'history': 'js/vendor/history',
'js/verify_student/photocapture': 'js/verify_student/photocapture',
'js/staff_debug_actions': 'js/staff_debug_actions',
@@ -372,6 +373,7 @@
'underscore',
'backbone',
'gettext',
+ 'history',
'utility',
'js/student_account/views/LoginView',
'js/student_account/views/PasswordResetView',
diff --git a/lms/static/js/spec/student_account/access_spec.js b/lms/static/js/spec/student_account/access_spec.js
index 2df49b5d58..110c5bf6cc 100644
--- a/lms/static/js/spec/student_account/access_spec.js
+++ b/lms/static/js/spec/student_account/access_spec.js
@@ -96,13 +96,13 @@ define([
var assertForms = function(visibleType, hiddenType) {
expect($(visibleType)).not.toHaveClass('hidden');
expect($(hiddenType)).toHaveClass('hidden');
- expect($('#password-reset-wrapper')).toBeEmpty();
+ expect($('#password-reset-form')).toHaveClass('hidden');
};
var selectForm = function(type) {
// Create a fake change event to control form toggling
var changeEvent = $.Event('change');
- changeEvent.currentTarget = $('#' + type + '-option');
+ changeEvent.currentTarget = $('.form-toggle[data-type="' + type + '"]');
// Load form corresponding to the change event
view.toggleForm(changeEvent);
@@ -133,9 +133,7 @@ define([
TemplateHelpers.installTemplate('templates/student_account/form_field');
// Stub analytics tracking
- // TODO: use RequireJS to ensure that this is loaded correctly
- window.analytics = window.analytics || {};
- window.analytics.track = window.analytics.track || function() {};
+ window.analytics = jasmine.createSpyObj('analytics', ['track', 'page', 'pageview', 'trackLink']);
});
it('can initially display the login form', function() {
diff --git a/lms/static/js/spec/student_account/login_spec.js b/lms/static/js/spec/student_account/login_spec.js
index 428e09d340..93a0f1b69d 100644
--- a/lms/static/js/spec/student_account/login_spec.js
+++ b/lms/static/js/spec/student_account/login_spec.js
@@ -4,12 +4,14 @@ define([
'js/common_helpers/template_helpers',
'js/common_helpers/ajax_helpers',
'js/student_account/models/LoginModel',
- 'js/student_account/views/LoginView'
-], function($, _, TemplateHelpers, AjaxHelpers, LoginModel, LoginView) {
+ 'js/student_account/views/LoginView',
+ 'js/student_account/models/PasswordResetModel'
+], function($, _, TemplateHelpers, AjaxHelpers, LoginModel, LoginView, PasswordResetModel) {
'use strict';
describe('edx.student.account.LoginView', function() {
var model = null,
+ resetModel = null,
view = null,
requests = null,
authComplete = false,
@@ -79,10 +81,17 @@ define([
method: FORM_DESCRIPTION.method
});
+ // Initialize the passwordReset model
+ resetModel = new PasswordResetModel({}, {
+ method: 'GET',
+ url: '#'
+ });
+
// Initialize the login view
view = new LoginView({
fields: FORM_DESCRIPTION.fields,
model: model,
+ resetModel: resetModel,
thirdPartyAuth: THIRD_PARTY_AUTH,
platformName: PLATFORM_NAME
});
diff --git a/lms/static/js/spec/student_account/password_reset_spec.js b/lms/static/js/spec/student_account/password_reset_spec.js
index 37a6ab4b67..0eeff353ca 100644
--- a/lms/static/js/spec/student_account/password_reset_spec.js
+++ b/lms/static/js/spec/student_account/password_reset_spec.js
@@ -67,7 +67,7 @@ define([
};
beforeEach(function() {
- setFixtures('');
+ setFixtures('');
TemplateHelpers.installTemplate('templates/student_account/password_reset');
TemplateHelpers.installTemplate('templates/student_account/form_field');
});
@@ -90,6 +90,12 @@ define([
// Verify that the success message is visible
expect($('.js-reset-success')).not.toHaveClass('hidden');
+
+ // Verify that login form has loaded
+ expect($('#login-form')).not.toHaveClass('hidden');
+
+ // Verify that password reset view has been removed
+ expect($( view.el ).html().length).toEqual(0);
});
it('validates the email field', function() {
diff --git a/lms/static/js/student_account/models/PasswordResetModel.js b/lms/static/js/student_account/models/PasswordResetModel.js
index 60858dc630..691b4a86f5 100644
--- a/lms/static/js/student_account/models/PasswordResetModel.js
+++ b/lms/static/js/student_account/models/PasswordResetModel.js
@@ -21,7 +21,7 @@ var edx = edx || {};
this.urlRoot = options.url;
},
- sync: function(method, model) {
+ sync: function( method, model ) {
var headers = {
'X-CSRFToken': $.cookie('csrftoken')
};
diff --git a/lms/static/js/student_account/views/AccessView.js b/lms/static/js/student_account/views/AccessView.js
index 52902d173f..4796834b3a 100644
--- a/lms/static/js/student_account/views/AccessView.js
+++ b/lms/static/js/student_account/views/AccessView.js
@@ -6,13 +6,21 @@ var edx = edx || {};
edx.student = edx.student || {};
edx.student.account = edx.student.account || {};
+ // Bind to StateChange Event
+ History.Adapter.bind( window, 'statechange', function() {
+ /* Note: We are using History.getState() for legacy browser (IE) support
+ * using History.js plugin instead of the native event.state
+ */
+ var State = History.getState();
+ });
+
edx.student.account.AccessView = Backbone.View.extend({
el: '#login-and-registration-container',
tpl: '#access-tpl',
events: {
- 'change .form-toggle': 'toggleForm'
+ 'click .form-toggle': 'toggleForm'
},
subview: {
@@ -37,8 +45,15 @@ var edx = edx || {};
currentProvider: null,
providers: []
};
+
this.platformName = obj.platformName;
+ // The login view listens for 'sync' events from the reset model
+ this.resetModel = new edx.student.account.PasswordResetModel({}, {
+ method: 'GET',
+ url: '#'
+ });
+
this.render();
},
@@ -55,7 +70,6 @@ var edx = edx || {};
postRender: function() {
// Load the default form
this.loadForm( this.activeForm );
- this.$header = $(this.el).find('.js-login-register-header');
},
loadForm: function( type ) {
@@ -72,6 +86,7 @@ var edx = edx || {};
context.subview.login = new edx.student.account.LoginView({
fields: data.fields,
model: model,
+ resetModel: context.resetModel,
thirdPartyAuth: context.thirdPartyAuth,
platformName: context.platformName
});
@@ -85,15 +100,16 @@ var edx = edx || {};
},
reset: function( data, context ) {
- var model = new edx.student.account.PasswordResetModel({}, {
- method: data.method,
- url: data.submit_url
- });
+ context.resetModel.ajaxType = data.method;
+ context.resetModel.urlRoot = data.submit_url;
context.subview.passwordHelp = new edx.student.account.PasswordResetView({
fields: data.fields,
- model: model
+ model: context.resetModel
});
+
+ // Listen for 'password-email-sent' event to toggle sub-views
+ context.listenTo( context.subview.passwordHelp, 'password-email-sent', context.passwordEmailSent );
},
register: function( data, context ) {
@@ -133,15 +149,20 @@ var edx = edx || {};
});
},
+ passwordEmailSent: function() {
+ this.element.hide( $(this.el).find('#password-reset-anchor') );
+ this.element.show( $('#login-anchor') );
+ this.element.scrollTop( $('#login-anchor') );
+ },
+
resetPassword: function() {
window.analytics.track('edx.bi.password_reset_form.viewed', {
category: 'user-engagement'
});
- this.element.hide( this.$header );
- this.element.hide( $(this.el).find('.form-type') );
+ this.element.hide( $(this.el).find('#login-anchor') );
this.loadForm('reset');
- this.element.scrollTop( $('#password-reset-wrapper') );
+ this.element.scrollTop( $('#password-reset-anchor') );
},
showFormError: function() {
@@ -149,10 +170,12 @@ var edx = edx || {};
},
toggleForm: function( e ) {
- var type = $(e.currentTarget).val(),
+ var type = $(e.currentTarget).data('type'),
$form = $('#' + type + '-form'),
$anchor = $('#' + type + '-anchor');
+ e.preventDefault();
+
window.analytics.track('edx.bi.' + type + '_form.toggled', {
category: 'user-engagement'
});
@@ -161,9 +184,14 @@ var edx = edx || {};
this.loadForm( type );
}
+ this.element.hide( $(this.el).find('.submission-success') );
this.element.hide( $(this.el).find('.form-wrapper') );
this.element.show( $form );
this.element.scrollTop( $anchor );
+
+ // Update url without reloading page
+ History.pushState( null, document.title, '/account/' + type + '/' );
+ analytics.page( 'login_and_registration', type );
},
/**
@@ -291,8 +319,7 @@ var edx = edx || {};
*/
element: {
hide: function( $el ) {
- $el.addClass('hidden')
- .attr('aria-hidden', true);
+ $el.addClass('hidden');
},
scrollTop: function( $el ) {
@@ -303,8 +330,7 @@ var edx = edx || {};
},
show: function( $el ) {
- $el.removeClass('hidden')
- .attr('aria-hidden', false);
+ $el.removeClass('hidden');
}
}
});
diff --git a/lms/static/js/student_account/views/FormView.js b/lms/static/js/student_account/views/FormView.js
index 32b3e87472..12f0d51100 100644
--- a/lms/static/js/student_account/views/FormView.js
+++ b/lms/static/js/student_account/views/FormView.js
@@ -98,8 +98,7 @@ var edx = edx || {};
element: {
hide: function( $el ) {
if ( $el ) {
- $el.addClass('hidden')
- .attr('aria-hidden', true);
+ $el.addClass('hidden');
}
},
@@ -112,8 +111,7 @@ var edx = edx || {};
show: function( $el ) {
if ( $el ) {
- $el.removeClass('hidden')
- .attr('aria-hidden', false);
+ $el.removeClass('hidden');
}
}
},
@@ -148,7 +146,6 @@ var edx = edx || {};
},
getFormData: function() {
-
var obj = {},
$form = this.$form,
elements = $form[0].elements,
@@ -229,6 +226,15 @@ var edx = edx || {};
} else {
this.toggleErrorMsg( true );
}
+
+ this.postFormSubmission();
+ },
+
+ /* Allows extended views to add custom
+ * code after form submission
+ */
+ postFormSubmission: function() {
+ return true;
},
toggleErrorMsg: function( show ) {
diff --git a/lms/static/js/student_account/views/LoginView.js b/lms/static/js/student_account/views/LoginView.js
index 85edd59d3f..7b567b9fbd 100644
--- a/lms/static/js/student_account/views/LoginView.js
+++ b/lms/static/js/student_account/views/LoginView.js
@@ -27,8 +27,10 @@ var edx = edx || {};
this.providers = data.thirdPartyAuth.providers || [];
this.currentProvider = data.thirdPartyAuth.currentProvider || '';
this.platformName = data.platformName;
+ this.resetModel = data.resetModel;
this.listenTo( this.model, 'sync', this.saveSuccess );
+ this.listenTo( this.resetModel, 'sync', this.resetEmail );
},
render: function( html ) {
@@ -55,6 +57,7 @@ var edx = edx || {};
this.$form = this.$container.find('form');
this.$errors = this.$container.find('.submission-error');
+ this.$resetSuccess = this.$container.find('.js-reset-success');
this.$authError = this.$container.find('.already-authenticated-msg');
this.$submitButton = this.$container.find(this.submitButton);
@@ -71,6 +74,16 @@ var edx = edx || {};
event.preventDefault();
this.trigger('password-help');
+ this.element.hide( this.$resetSuccess );
+ },
+
+ postFormSubmission: function() {
+ this.element.hide( this.$resetSuccess );
+ },
+
+ resetEmail: function() {
+ this.element.hide( this.$errors );
+ this.element.show( this.$resetSuccess );
},
thirdPartyAuth: function( event ) {
@@ -81,13 +94,15 @@ var edx = edx || {};
}
},
- saveSuccess: function () {
+ saveSuccess: function() {
this.trigger('auth-complete');
+ this.element.hide( this.$resetSuccess );
},
saveError: function( error ) {
this.errors = ['