diff --git a/common/test/acceptance/pages/lms/login_and_register.py b/common/test/acceptance/pages/lms/login_and_register.py index b6e2191f98..7700be8a61 100644 --- a/common/test/acceptance/pages/lms/login_and_register.py +++ b/common/test/acceptance/pages/lms/login_and_register.py @@ -356,10 +356,10 @@ class CombinedLoginAndRegisterPage(PageObject): """Wait for a status message to be visible following third_party registration, then return it.""" def _check_func(): """Return third party auth status notice message.""" - for selector in ['.already-authenticated-msg p', '.status p']: - msg_element = self.q(css=selector) - if msg_element.visible: - return (True, msg_element.text[0]) + selector = '.js-auth-warning p' + msg_element = self.q(css=selector) + if msg_element.visible: + return (True, msg_element.text[0]) return (False, None) return Promise(_check_func, "Result of third party auth is visible").fulfill() diff --git a/lms/static/js/financial-assistance/views/financial_assistance_form_view.js b/lms/static/js/financial-assistance/views/financial_assistance_form_view.js index 7b03e112f8..5ebe1eaffc 100644 --- a/lms/static/js/financial-assistance/views/financial_assistance_form_view.js +++ b/lms/static/js/financial-assistance/views/financial_assistance_form_view.js @@ -31,6 +31,8 @@ tpl: formViewTpl, fieldTpl: formFieldTpl, formType: 'financial-assistance', + successTpl: successTpl, + defaultFormErrorsTitle: gettext('Unable to submit application'), requiredStr: '', submitButton: '.js-submit-form', @@ -81,7 +83,7 @@ }, renderSuccess: function() { - this.$el.html(_.template(successTpl)({ + this.$el.html(_.template(this.successTpl)({ course: this.model.get('course'), dashboard_url: this.context.dashboard_url })); @@ -102,8 +104,7 @@ } this.errors = ['
  • ' + msg + '
  • ']; - this.setErrors(); - this.element.hide(this.$resetSuccess); + this.renderErrors(this.defaultFormErrorsTitle, this.errors); this.toggleDisableButton(false); }, @@ -112,9 +113,7 @@ }, validateCountry: function() { - var $submissionContainer = $('.submission-error'), - $errorMessageContainer = $submissionContainer.find('.message-copy'), - $countryLabel = $('#user-country-title'), + var $countryLabel = $('#user-country-title'), txt = [ 'Please go to your {link_start}profile page{link_end} ', 'and provide your country of residence.' @@ -130,9 +129,8 @@ if (!this.model.get('country')) { $countryLabel.addClass('error'); - $errorMessageContainer.append('
  • ' + msg + '
  • '); + this.renderErrors(this.defaultFormErrorsTitle, ['
  • ' + msg + '
  • ']); this.toggleDisableButton(true); - $submissionContainer.removeClass('hidden'); } }, diff --git a/lms/static/js/spec/financial-assistance/financial_assistance_form_view_spec.js b/lms/static/js/spec/financial-assistance/financial_assistance_form_view_spec.js index a94a62dfea..219236cd56 100644 --- a/lms/static/js/spec/financial-assistance/financial_assistance_form_view_spec.js +++ b/lms/static/js/spec/financial-assistance/financial_assistance_form_view_spec.js @@ -133,23 +133,23 @@ define([ failedSubmission = function() { expect(view.$('.js-success-message').length).toEqual(0); - expect(view.$('.submission-error')).toHaveClass('hidden'); + expect(view.$formFeedback.find('.' + view.formErrorsJsHook).length).toEqual(0); validSubmission(); view.model.trigger('error', {status: 500}); expect(view.$('.js-success-message').length).toEqual(0); - expect(view.$('.submission-error')).not.toHaveClass('hidden'); + expect(view.$formFeedback.find('.' + view.formErrorsJsHook).length).toEqual(1); }; invalidCountry = function() { expect(view.$('.js-success-message').length).toEqual(0); - expect(view.$('.submission-error')).not.toHaveClass('hidden'); + expect(view.$formFeedback.find('.' + view.formErrorsJsHook).length).toEqual(1); expect(view.$('#user-country-title')).toHaveClass('error'); expect(view.$('.js-submit-form').prop('disabled')).toBeTruthy(); }; validCountry = function() { expect(view.$('.js-success-message').length).toEqual(0); - expect(view.$('.submission-error')).toHaveClass('hidden'); + expect(view.$formFeedback.find('.' + view.formErrorsJsHook).length).toEqual(0); expect(view.$('#user-country-title')).not.toHaveClass('error'); expect(view.$('.js-submit-form').prop('disabled')).toBeFalsy(); }; @@ -184,10 +184,10 @@ define([ }); it('should not submit the form if the front end validation fails', function() { - expect(view.$('.submission-error')).toHaveClass('hidden'); + expect(view.$formFeedback.find('.' + view.formErrorsJsHook).length).toEqual(0); view.$('.js-submit-form').click(); expect(view.model.save).not.toHaveBeenCalled(); - expect(view.$('.submission-error')).not.toHaveClass('hidden'); + expect(view.$formFeedback.find('.' + view.formErrorsJsHook).length).toEqual(1); }); it('should submit the form data and additional data if validation passes', function() { diff --git a/lms/static/js/spec/student_account/login_spec.js b/lms/static/js/spec/student_account/login_spec.js index 7ecc50ee7b..5f5da06c48 100644 --- a/lms/static/js/spec/student_account/login_spec.js +++ b/lms/static/js/spec/student_account/login_spec.js @@ -191,10 +191,10 @@ }); AjaxHelpers.expectRequest( - requests, 'POST', - FORM_DESCRIPTION.submit_url, - $.param(expectedData) - ); + requests, 'POST', + FORM_DESCRIPTION.submit_url, + $.param(expectedData) + ); }); it('displays third-party auth login buttons', function() { @@ -212,6 +212,21 @@ expect($('.forgot-password')).toBeVisible(); }); + it('displays password reset success message after password reset request', function() { + createLoginView(this); + + // Verify that the success message is not visible + expect(view.$formFeedback.find('.' + view.passwordResetSuccessJsHook).length).toEqual(0); + + /* After a successful password reset request, the resetModel will trigger a 'sync' + * event, which lets the LoginView know to render the password reset success message. + */ + view.resetModel.trigger('sync'); + + // Verify that the success message is visible + expect(view.$formFeedback.find('.' + view.passwordResetSuccessJsHook).length).toEqual(1); + }); + it('validates login form fields', function() { createLoginView(this); @@ -229,10 +244,11 @@ submitForm(false); // Verify that submission errors are visible - expect(view.$errors).not.toHaveClass('hidden'); + expect(view.$formFeedback.find('.' + view.formErrorsJsHook).length).toEqual(1); // Expect auth complete NOT to have been triggered expect(authComplete).toBe(false); + // Form button should be re-enabled when errors occur expect(view.$submitButton).not.toHaveAttr('disabled'); }); @@ -247,8 +263,10 @@ AjaxHelpers.respondWithError(requests); // Expect that an error is displayed and that auth complete is not triggered - expect(view.$errors).not.toHaveClass('hidden'); + expect(view.$formFeedback.find('.' + view.formErrorsJsHook).length).toEqual(1); + expect(authComplete).toBe(false); + // Form button should be re-enabled on server failure. expect(view.$submitButton).not.toHaveAttr('disabled'); @@ -262,17 +280,18 @@ AjaxHelpers.respondWithJson(requests, {}); // Expect that the error is hidden and auth complete is triggered - expect(view.$errors).toHaveClass('hidden'); + expect(view.$formFeedback.find('.' + view.formErrorsJsHook).length).toEqual(0); expect(authComplete).toBe(true); }); it('displays an error if there is no internet connection', function() { var clock, oldTimeout, - timeout; + timeout, + $error; // We're defining "no internet connection" in this case as the - // request timing out. We use a combination of the sinon fake + // request timing out. We use a combination of the sinon fake // timer and jQuery.ajaxSetup() to force a request timeout. clock = sinon.useFakeTimers(); oldTimeout = $.ajaxSetup().timeout; @@ -288,11 +307,12 @@ clock.tick(timeout + 1); // Expect that an error is displayed and that auth complete is not triggered - expect(view.$errors).not.toHaveClass('hidden'); + $error = view.$formFeedback.find('.' + view.formErrorsJsHook); + expect($error.length).toEqual(1); + expect($error.text()).toContain( + 'An error has occurred. Check your Internet connection and try again.' + ); expect(authComplete).toBe(false); - expect(view.$errors.text()).toContain( - 'An error has occurred. Check your Internet connection and try again.' - ); // Finally, restore the old timeout and turn off the fake timer. $.ajaxSetup({timeout: oldTimeout}); @@ -300,6 +320,7 @@ }); it('displays an error if there is a server error', function() { + var $error; createLoginView(this); // Submit the form, with successful validation @@ -309,11 +330,12 @@ AjaxHelpers.respondWithError(requests, 500); // Expect that an error is displayed and that auth complete is not triggered - expect(view.$errors).not.toHaveClass('hidden'); + $error = view.$formFeedback.find('.' + view.formErrorsJsHook); + expect($error.length).toEqual(1); + expect($error.text()).toContain( + 'An error has occurred. Try refreshing the page, or check your Internet connection.' + ); expect(authComplete).toBe(false); - expect(view.$errors.text()).toContain( - 'An error has occurred. Try refreshing the page, or check your Internet connection.' - ); }); }); }); 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 2124d6c57b..8846655312 100644 --- a/lms/static/js/spec/student_account/password_reset_spec.js +++ b/lms/static/js/spec/student_account/password_reset_spec.js @@ -74,26 +74,32 @@ }); it('allows the user to request a new password', function() { + var syncSpy, passwordEmailSentSpy; + createPasswordResetView(this); + // We expect these events to be triggered upon a successful password reset + syncSpy = jasmine.createSpy('syncEvent'); + passwordEmailSentSpy = jasmine.createSpy('passwordEmailSentEvent'); + view.listenTo(view.model, 'sync', syncSpy); + view.listenTo(view, 'password-email-sent', passwordEmailSentSpy); + // Submit the form, with successful validation submitEmail(true); // Verify that the client contacts the server with the expected data AjaxHelpers.expectRequest( - requests, 'POST', - FORM_DESCRIPTION.submit_url, - $.param({email: EMAIL}) - ); + requests, 'POST', + FORM_DESCRIPTION.submit_url, + $.param({email: EMAIL}) + ); // Respond with status code 200 AjaxHelpers.respondWithJson(requests, {}); - // 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 the events were triggered + expect(syncSpy).toHaveBeenCalled(); + expect(passwordEmailSentSpy).toHaveBeenCalled(); // Verify that password reset view has been removed expect($(view.el).html().length).toEqual(0); @@ -109,7 +115,7 @@ expect(view.validate).toHaveBeenCalledWith($('#password-reset-email')[0]); // Verify that no submission errors are visible - expect(view.$errors).toHaveClass('hidden'); + expect(view.$formFeedback.find('.' + view.formErrorsJsHook).length).toEqual(0); }); it('displays password reset validation errors', function() { @@ -119,7 +125,7 @@ submitEmail(false); // Verify that submission errors are visible - expect(view.$errors).not.toHaveClass('hidden'); + expect(view.$formFeedback.find('.' + view.formErrorsJsHook).length).toEqual(1); }); it('displays an error if the server returns an error while sending a password reset email', function() { @@ -130,7 +136,7 @@ AjaxHelpers.respondWithError(requests); // Expect that an error is displayed - expect(view.$errors).not.toHaveClass('hidden'); + expect(view.$formFeedback.find('.' + view.formErrorsJsHook).length).toEqual(1); // If we try again and succeed, the error should go away submitEmail(); @@ -139,7 +145,7 @@ AjaxHelpers.respondWithJson(requests, {}); // Expect that the error is hidden - expect(view.$errors).toHaveClass('hidden'); + expect(view.$formFeedback.find('.' + view.formErrorsJsHook).length).toEqual(0); }); }); }); diff --git a/lms/static/js/spec/student_account/register_spec.js b/lms/static/js/spec/student_account/register_spec.js index 1b5b614224..3947aceee8 100644 --- a/lms/static/js/spec/student_account/register_spec.js +++ b/lms/static/js/spec/student_account/register_spec.js @@ -243,16 +243,17 @@ // Verify that the client contacts the server with the expected data AjaxHelpers.expectRequest( - requests, 'POST', - FORM_DESCRIPTION.submit_url, - $.param(USER_DATA) - ); + requests, 'POST', + FORM_DESCRIPTION.submit_url, + $.param(USER_DATA) + ); // Respond with status code 200 AjaxHelpers.respondWithJson(requests, {}); // Verify that auth complete is triggered expect(authComplete).toBe(true); + // Form button should be disabled on success. expect(view.$submitButton).toHaveAttr('disabled'); }); @@ -278,10 +279,10 @@ $.extend(expectedData, USER_DATA); AjaxHelpers.expectRequest( - requests, 'POST', - FORM_DESCRIPTION.submit_url, - $.param(expectedData) - ); + requests, 'POST', + FORM_DESCRIPTION.submit_url, + $.param(expectedData) + ); }); it('displays third-party auth registration buttons', function() { @@ -305,7 +306,8 @@ expect(view.validate).toHaveBeenCalledWith($('#register-password')[0]); // Verify that no submission errors are visible - expect(view.$errors).toHaveClass('hidden'); + expect(view.$formFeedback.find('.' + view.formErrorsJsHook).length).toEqual(0); + // Form button should be disabled on success. expect(view.$submitButton).toHaveAttr('disabled'); }); @@ -317,10 +319,11 @@ submitForm(false); // Verify that submission errors are visible - expect(view.$errors).not.toHaveClass('hidden'); + expect(view.$formFeedback.find('.' + view.formErrorsJsHook).length).toEqual(1); // Expect that auth complete is NOT triggered expect(authComplete).toBe(false); + // Form button should be re-enabled on error. expect(view.$submitButton).not.toHaveAttr('disabled'); }); @@ -335,7 +338,7 @@ AjaxHelpers.respondWithError(requests); // Expect that an error is displayed and that auth complete is NOT triggered - expect(view.$errors).not.toHaveClass('hidden'); + expect(view.$formFeedback.find('.' + view.formErrorsJsHook).length).toEqual(1); expect(authComplete).toBe(false); // If we try again and succeed, the error should go away @@ -345,8 +348,9 @@ AjaxHelpers.respondWithJson(requests, {}); // Expect that the error is hidden and that auth complete is triggered - expect(view.$errors).toHaveClass('hidden'); + expect(view.$formFeedback.find('.' + view.formErrorsJsHook).length).toEqual(0); expect(authComplete).toBe(true); + // Form button should be disabled on success. expect(view.$submitButton).toHaveAttr('disabled'); }); diff --git a/lms/static/js/student_account/views/FormView.js b/lms/static/js/student_account/views/FormView.js index 7bd52379c3..719288d4bd 100644 --- a/lms/static/js/student_account/views/FormView.js +++ b/lms/static/js/student_account/views/FormView.js @@ -4,9 +4,11 @@ 'jquery', 'underscore', 'backbone', - 'common/js/utils/edx.utils.validate' + 'common/js/utils/edx.utils.validate', + 'edx-ui-toolkit/js/utils/html-utils', + 'text!templates/student_account/form_errors.underscore' ], - function($, _, Backbone, EdxUtilsValidate) { + function($, _, Backbone, EdxUtilsValidate, HtmlUtils, formErrorsTpl) { return Backbone.View.extend({ tagName: 'form', @@ -16,6 +18,12 @@ fieldTpl: '#form_field-tpl', + formErrorsTpl: formErrorsTpl, + + formErrorsJsHook: 'js-form-errors', + + defaultFormErrorsTitle: gettext('An error occurred.'), + events: {}, errors: [], @@ -66,7 +74,7 @@ postRender: function() { var $container = $(this.el); this.$form = $container.find('form'); - this.$errors = $container.find('.submission-error'); + this.$formFeedback = $container.find('.js-form-feedback'); this.$submitButton = $container.find(this.submitButton); }, @@ -126,21 +134,6 @@ return obj; }, - focusFirstError: function() { - var $error = this.$form.find('.error').first(), - $field = {}, - $parent = {}; - - if ($error.is('label')) { - $parent = $error.parent('.form-field'); - $error = $parent.find('input') || $parent.find('select'); - } else { - $field = $error; - } - - $error.focus(); - }, - forgotPassword: function(event) { event.preventDefault(); @@ -185,32 +178,34 @@ saveError: function(error) { this.errors = ['
  • ' + error.responseText + '
  • ']; - this.setErrors(); + this.renderErrors(this.defaultFormErrorsTitle, this.errors); this.toggleDisableButton(false); }, - setErrors: function() { - var $msg = this.$errors.find('.message-copy'), - html = [], - errors = this.errors, - i, - len = errors.length; + /* Wrapper for renderFormFeedback provided for convenience since the majority of + * our calls to renderFormFeedback are for rendering error messages. + */ + renderErrors: function(title, errorMessages) { + this.clearFormErrors(); - for (i = 0; i < len; i++) { - html.push(errors[i]); - } + this.renderFormFeedback(this.formErrorsTpl, { + jsHook: this.formErrorsJsHook, + title: title, + messagesHtml: HtmlUtils.HTML(errorMessages.join('')) + }); + }, - $msg.html(html.join('')); + renderFormFeedback: function(template, context) { + var tpl = HtmlUtils.template(template); + HtmlUtils.prepend(this.$formFeedback, tpl(context)); - this.element.show(this.$errors); - - // Scroll to error messages + // Scroll to feedback container $('html,body').animate({ - scrollTop: this.$errors.offset().top + scrollTop: this.$formFeedback.offset().top }, 'slow'); - // Focus on first error field - this.focusFirstError(); + // Focus on the feedback container to ensure screen readers see the messages. + this.$formFeedback.focus(); }, /* Allows extended views to add non-form attributes @@ -233,9 +228,10 @@ data = this.setExtraData(data); this.model.set(data); this.model.save(); - this.toggleErrorMsg(false); + this.clearFormErrors(); } else { - this.toggleErrorMsg(true); + this.renderErrors(this.defaultFormErrorsTitle, this.errors); + this.toggleDisableButton(false); } this.postFormSubmission(); @@ -248,12 +244,15 @@ return true; }, - toggleErrorMsg: function(show) { - if (show) { - this.setErrors(); - this.toggleDisableButton(false); - } else { - this.element.hide(this.$errors); + clearFormErrors: function() { + var query = '.' + this.formErrorsJsHook; + this.clearFormFeedbackItems(query); + }, + + clearFormFeedbackItems: function(query) { + var $items = this.$formFeedback.find(query); + if ($items.length > 0) { + $items.remove(); } }, diff --git a/lms/static/js/student_account/views/LoginView.js b/lms/static/js/student_account/views/LoginView.js index b12d079b51..274b579868 100644 --- a/lms/static/js/student_account/views/LoginView.js +++ b/lms/static/js/student_account/views/LoginView.js @@ -5,9 +5,11 @@ 'underscore', 'gettext', 'edx-ui-toolkit/js/utils/html-utils', - 'js/student_account/views/FormView' + 'js/student_account/views/FormView', + 'text!templates/student_account/form_success.underscore', + 'text!templates/student_account/form_status.underscore' ], - function($, _, gettext, HtmlUtils, FormView) { + function($, _, gettext, HtmlUtils, FormView, formSuccessTpl, formStatusTpl) { return FormView.extend({ el: '#login-form', tpl: '#login-tpl', @@ -19,12 +21,17 @@ formType: 'login', requiredStr: '', submitButton: '.js-login', + formSuccessTpl: formSuccessTpl, + formStatusTpl: formStatusTpl, + authWarningJsHook: 'js-auth-warning', + passwordResetSuccessJsHook: 'js-password-reset-success', + defaultFormErrorsTitle: gettext('We couldn\'t sign you in.'), preRender: function(data) { this.providers = data.thirdPartyAuth.providers || []; this.hasSecondaryProviders = ( - data.thirdPartyAuth.secondaryProviders && data.thirdPartyAuth.secondaryProviders.length - ); + data.thirdPartyAuth.secondaryProviders && data.thirdPartyAuth.secondaryProviders.length + ); this.currentProvider = data.thirdPartyAuth.currentProvider || ''; this.errorMessage = data.thirdPartyAuth.errorMessage || ''; this.platformName = data.platformName; @@ -44,7 +51,6 @@ context: { fields: fields, currentProvider: this.currentProvider, - errorMessage: this.errorMessage, providers: this.providers, hasSecondaryProviders: this.hasSecondaryProviders, platformName: this.platformName @@ -57,19 +63,23 @@ }, postRender: function() { + var formErrorsTitle; this.$container = $(this.el); - 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.$formFeedback = this.$container.find('.js-form-feedback'); this.$submitButton = this.$container.find(this.submitButton); - /* If we're already authenticated with a third-party - * provider, try logging in. The easiest way to do this - * is to simply submit the form. - */ - if (this.currentProvider) { + if (this.errorMessage) { + formErrorsTitle = _.sprintf( + gettext('An error occurred when signing you in to %s.'), + this.platformName + ); + this.renderErrors(formErrorsTitle, [this.errorMessage]); + } else if (this.currentProvider) { + /* If we're already authenticated with a third-party + * provider, try logging in. The easiest way to do this + * is to simply submit the form. + */ this.model.save(); } }, @@ -78,37 +88,39 @@ event.preventDefault(); this.trigger('password-help'); - this.element.hide(this.$resetSuccess); + this.clearPasswordResetSuccess(); }, postFormSubmission: function() { - this.element.hide(this.$resetSuccess); + this.clearPasswordResetSuccess(); }, resetEmail: function() { var email = $('#password-reset-email').val(), - successMessage; - this.element.hide(this.$errors); - this.resetMessage = this.$resetSuccess.find('.message-copy'); + successTitle = gettext('Check Your Email'), + successMessageHtml = HtmlUtils.interpolateHtml( + gettext('{paragraphStart}You entered {boldStart}{email}{boldEnd}. If this email address is associated with your {platform_name} account, we will send a message with password reset instructions to this email address.{paragraphEnd}' + // eslint-disable-line max-len + '{paragraphStart}If you do not receive a password reset message, verify that you entered the correct email address, or check your spam folder.{paragraphEnd}' + // eslint-disable-line max-len + '{paragraphStart}If you need further assistance, {anchorStart}contact technical support{anchorEnd}.{paragraphEnd}'), { // eslint-disable-line max-len + boldStart: HtmlUtils.HTML(''), + boldEnd: HtmlUtils.HTML(''), + paragraphStart: HtmlUtils.HTML('

    '), + paragraphEnd: HtmlUtils.HTML('

    '), + email: email, + platform_name: this.platformName, + anchorStart: HtmlUtils.HTML(''), + anchorEnd: HtmlUtils.HTML('') + } + ); - successMessage = HtmlUtils.interpolateHtml( - gettext('{paragraphStart}You entered {boldStart}{email}{boldEnd}. If this email address is associated with your {platform_name} account, we will send a message with password reset instructions to this email address.{paragraphEnd}' + // eslint-disable-line max-len - '{paragraphStart}If you do not receive a password reset message, verify that you entered the correct email address, or check your spam folder.{paragraphEnd}' + // eslint-disable-line max-len - '{paragraphStart}If you need further assistance, {anchorStart}contact technical support{anchorEnd}.{paragraphEnd}'), { // eslint-disable-line max-len - boldStart: HtmlUtils.HTML(''), - boldEnd: HtmlUtils.HTML(''), - paragraphStart: HtmlUtils.HTML('

    '), - paragraphEnd: HtmlUtils.HTML('

    '), - email: email, - platform_name: this.platformName, - anchorStart: HtmlUtils.HTML(''), - anchorEnd: HtmlUtils.HTML('') - }); + this.clearFormErrors(); + this.clearPasswordResetSuccess(); - if (this.resetMessage.find('p').length === 0) { - this.resetMessage.append(HtmlUtils.joinHtml(successMessage).toString()); - } - this.element.show(this.$resetSuccess); + this.renderFormFeedback(this.formSuccessTpl, { + jsHook: this.passwordResetSuccessJsHook, + title: successTitle, + messageHtml: successMessageHtml + }); }, thirdPartyAuth: function(event) { @@ -121,7 +133,7 @@ saveSuccess: function() { this.trigger('auth-complete'); - this.element.hide(this.$resetSuccess); + this.clearPasswordResetSuccess(); }, saveError: function(error) { @@ -132,8 +144,7 @@ msg = gettext('An error has occurred. Try refreshing the page, or check your Internet connection.'); } this.errors = ['
  • ' + msg + '
  • ']; - this.setErrors(); - this.element.hide(this.$resetSuccess); + this.clearPasswordResetSuccess(); /* If we've gotten a 403 error, it means that we've successfully * authenticated with a third-party provider, but we haven't @@ -144,13 +155,37 @@ if (error.status === 403 && error.responseText === 'third-party-auth' && this.currentProvider) { - this.element.show(this.$authError); - this.element.hide(this.$errors); + this.clearFormErrors(); + this.renderAuthWarning(); } else { - this.element.hide(this.$authError); - this.element.show(this.$errors); + this.renderErrors(this.defaultFormErrorsTitle, this.errors); } this.toggleDisableButton(false); + }, + + renderAuthWarning: function() { + var message = _.sprintf( + gettext('You have successfully signed into %(currentProvider)s, but your %(currentProvider)s' + + ' account does not have a linked %(platformName)s account. To link your accounts,' + + ' sign in now using your %(platformName)s password.'), + {currentProvider: this.currentProvider, platformName: this.platformName} + ); + + this.clearAuthWarning(); + this.renderFormFeedback(this.formStatusTpl, { + jsHook: this.authWarningJsHook, + message: message + }); + }, + + clearPasswordResetSuccess: function() { + var query = '.' + this.passwordResetSuccessJsHook; + this.clearFormFeedbackItems(query); + }, + + clearAuthWarning: function() { + var query = '.' + this.authWarningJsHook; + this.clearFormFeedbackItems(query); } }); }); diff --git a/lms/static/js/student_account/views/PasswordResetView.js b/lms/static/js/student_account/views/PasswordResetView.js index 32a0ee9956..a9b0baf70d 100644 --- a/lms/static/js/student_account/views/PasswordResetView.js +++ b/lms/static/js/student_account/views/PasswordResetView.js @@ -26,15 +26,6 @@ this.listenTo(this.model, 'sync', this.saveSuccess); }, - toggleErrorMsg: function(show) { - if (show) { - this.setErrors(); - this.toggleDisableButton(false); - } else { - this.element.hide(this.$errors); - } - }, - saveSuccess: function() { this.trigger('password-email-sent'); diff --git a/lms/static/js/student_account/views/RegisterView.js b/lms/static/js/student_account/views/RegisterView.js index 8b03015732..db54ce14f7 100644 --- a/lms/static/js/student_account/views/RegisterView.js +++ b/lms/static/js/student_account/views/RegisterView.js @@ -3,9 +3,11 @@ define([ 'jquery', 'underscore', - 'js/student_account/views/FormView' + 'gettext', + 'js/student_account/views/FormView', + 'text!templates/student_account/form_status.underscore' ], - function($, _, FormView) { + function($, _, gettext, FormView, formStatusTpl) { return FormView.extend({ el: '#register-form', @@ -18,13 +20,19 @@ formType: 'register', + formStatusTpl: formStatusTpl, + + authWarningJsHook: 'js-auth-warning', + + defaultFormErrorsTitle: gettext('We couldn\'t create your account.'), + submitButton: '.js-register', preRender: function(data) { this.providers = data.thirdPartyAuth.providers || []; this.hasSecondaryProviders = ( - data.thirdPartyAuth.secondaryProviders && data.thirdPartyAuth.secondaryProviders.length - ); + data.thirdPartyAuth.secondaryProviders && data.thirdPartyAuth.secondaryProviders.length + ); this.currentProvider = data.thirdPartyAuth.currentProvider || ''; this.errorMessage = data.thirdPartyAuth.errorMessage || ''; this.platformName = data.platformName; @@ -34,7 +42,8 @@ }, render: function(html) { - var fields = html || ''; + var fields = html || '', + formErrorsTitle = gettext('An error occurred.'); $(this.el).html(_.template(this.tpl)({ /* We pass the context object to the template so that @@ -43,7 +52,6 @@ context: { fields: fields, currentProvider: this.currentProvider, - errorMessage: this.errorMessage, providers: this.providers, hasSecondaryProviders: this.hasSecondaryProviders, platformName: this.platformName @@ -52,6 +60,13 @@ this.postRender(); + // Must be called after postRender, since postRender sets up $formFeedback. + if (this.errorMessage) { + this.renderErrors(formErrorsTitle, [this.errorMessage]); + } else if (this.currentProvider) { + this.renderAuthWarning(); + } + if (this.autoSubmit) { $(this.el).hide(); $('#register-honor_code').prop('checked', true); @@ -76,18 +91,18 @@ saveError: function(error) { $(this.el).show(); // Show in case the form was hidden for auto-submission this.errors = _.flatten( - _.map( - // Something is passing this 'undefined'. Protect against this. - JSON.parse(error.responseText || '[]'), - function(error_list) { - return _.map( - error_list, - function(error) { return '
  • ' + error.user_message + '
  • '; } - ); - } - ) - ); - this.setErrors(); + _.map( + // Something is passing this 'undefined'. Protect against this. + JSON.parse(error.responseText || '[]'), + function(errorList) { + return _.map( + errorList, + function(errorItem) { return '
  • ' + errorItem.user_message + '
  • '; } + ); + } + ) + ); + this.renderErrors(this.defaultFormErrorsTitle, this.errors); this.toggleDisableButton(false); }, @@ -96,6 +111,22 @@ // The form did not get submitted due to validation errors. $(this.el).show(); // Show in case the form was hidden for auto-submission } + }, + + renderAuthWarning: function() { + var msgPart1 = gettext('You\'ve successfully signed into %(currentProvider)s.'), + msgPart2 = gettext( + 'We just need a little more information before you start learning with %(platformName)s.' + ), + fullMsg = _.sprintf( + msgPart1 + ' ' + msgPart2, + {currentProvider: this.currentProvider, platformName: this.platformName} + ); + + this.renderFormFeedback(this.formStatusTpl, { + jsHook: this.authWarningJsHook, + message: fullMsg + }); } }); }); diff --git a/lms/templates/financial-assistance/financial_assessment_form.underscore b/lms/templates/financial-assistance/financial_assessment_form.underscore index ee970977bf..859a2fc586 100644 --- a/lms/templates/financial-assistance/financial_assessment_form.underscore +++ b/lms/templates/financial-assistance/financial_assessment_form.underscore @@ -7,10 +7,8 @@
    - +
    +

    <%- gettext('About You') %>

    diff --git a/lms/templates/student_account/form_errors.underscore b/lms/templates/student_account/form_errors.underscore new file mode 100644 index 0000000000..6ce814b09d --- /dev/null +++ b/lms/templates/student_account/form_errors.underscore @@ -0,0 +1,6 @@ +
    +

    <%- title %>

    +
      + <%= HtmlUtils.ensureHtml(messagesHtml) %> +
    +
    diff --git a/lms/templates/student_account/form_status.underscore b/lms/templates/student_account/form_status.underscore new file mode 100644 index 0000000000..68dc4e4e05 --- /dev/null +++ b/lms/templates/student_account/form_status.underscore @@ -0,0 +1,6 @@ +
    +

    + <%- message %> +

    +
    + diff --git a/lms/templates/student_account/form_success.underscore b/lms/templates/student_account/form_success.underscore new file mode 100644 index 0000000000..c8a30d13eb --- /dev/null +++ b/lms/templates/student_account/form_success.underscore @@ -0,0 +1,6 @@ +
    +

    <%- title %>

    +
    + <%= HtmlUtils.ensureHtml(messageHtml) %> +
    +
    diff --git a/lms/templates/student_account/login.underscore b/lms/templates/student_account/login.underscore index 3bfa724d9e..ad51d08ccc 100644 --- a/lms/templates/student_account/login.underscore +++ b/lms/templates/student_account/login.underscore @@ -1,31 +1,6 @@ -