From dc4e4f7d6a525e16c5a50d98a53564988a78a074 Mon Sep 17 00:00:00 2001 From: Waheed Ahmed Date: Thu, 5 May 2016 15:37:34 +0500 Subject: [PATCH] Account settings navigation. ECOM-2981 --- .../acceptance/pages/lms/account_settings.py | 6 + common/test/acceptance/pages/lms/fields.py | 4 +- .../tests/lms/test_account_settings.py | 27 +- common/test/acceptance/tests/lms/test_lms.py | 21 +- .../account_settings_factory_spec.js | 10 +- .../account_settings_fields_helpers.js | 4 +- .../account_settings_fields_spec.js | 2 +- .../account_settings_view_spec.js | 6 +- lms/static/js/spec/student_account/helpers.js | 6 +- .../views/account_section_view.js | 27 ++ .../views/account_settings_factory.js | 79 ++-- .../views/account_settings_fields.js | 388 ++++++++++-------- .../views/account_settings_view.js | 60 ++- lms/static/js/views/fields.js | 9 +- lms/static/sass/base/_variables.scss | 1 - lms/static/sass/shared-v2/_footer.scss | 1 - lms/static/sass/shared/_footer.scss | 1 - lms/static/sass/views/_account-settings.scss | 242 ++++++++++- .../fields/field_dropdown_account.underscore | 42 ++ .../fields/field_link_account.underscore | 8 + .../fields/field_readonly_account.underscore | 8 + .../field_social_link_account.underscore | 13 + .../fields/field_text_account.underscore | 8 + .../account_settings.underscore | 23 +- .../account_settings_section.underscore | 18 + 25 files changed, 722 insertions(+), 292 deletions(-) create mode 100644 lms/static/js/student_account/views/account_section_view.js create mode 100644 lms/templates/fields/field_dropdown_account.underscore create mode 100644 lms/templates/fields/field_link_account.underscore create mode 100644 lms/templates/fields/field_readonly_account.underscore create mode 100644 lms/templates/fields/field_social_link_account.underscore create mode 100644 lms/templates/fields/field_text_account.underscore create mode 100644 lms/templates/student_account/account_settings_section.underscore diff --git a/common/test/acceptance/pages/lms/account_settings.py b/common/test/acceptance/pages/lms/account_settings.py index 648d7c5831..33d852de43 100644 --- a/common/test/acceptance/pages/lms/account_settings.py +++ b/common/test/acceptance/pages/lms/account_settings.py @@ -57,3 +57,9 @@ class AccountSettingsPage(FieldsMixin, PageObject): Wait for loading indicator to become visible. """ EmptyPromise(self._is_loading_in_progress, "Loading is in progress.").fulfill() + + def switch_account_settings_tabs(self, tab_id): + """ + Switch between the different account settings tabs. + """ + self.q(css='#{}'.format(tab_id)).click() diff --git a/common/test/acceptance/pages/lms/fields.py b/common/test/acceptance/pages/lms/fields.py index 628337cf63..9b02729f1e 100644 --- a/common/test/acceptance/pages/lms/fields.py +++ b/common/test/acceptance/pages/lms/fields.py @@ -228,13 +228,13 @@ class FieldsMixin(object): "Link field with link title \"{0}\" is visible.".format(expected_title) ).fulfill() - def click_on_link_in_link_field(self, field_id): + def click_on_link_in_link_field(self, field_id, field_type='a'): """ Click the link in a link field. """ self.wait_for_field(field_id) - query = self.q(css='.u-field-{} a'.format(field_id)) + query = self.q(css='.u-field-{} {}'.format(field_id, field_type)) if query.present: query.first.click() diff --git a/common/test/acceptance/tests/lms/test_account_settings.py b/common/test/acceptance/tests/lms/test_account_settings.py index c2756781f9..8f8cb12def 100644 --- a/common/test/acceptance/tests/lms/test_account_settings.py +++ b/common/test/acceptance/tests/lms/test_account_settings.py @@ -158,7 +158,7 @@ class AccountSettingsPageTest(AccountSettingsTestMixin, WebAppTest): """ expected_sections_structure = [ { - 'title': 'Basic Account Information (required)', + 'title': 'Basic Account Information', 'fields': [ 'Username', 'Full Name', @@ -169,21 +169,13 @@ class AccountSettingsPageTest(AccountSettingsTestMixin, WebAppTest): ] }, { - 'title': 'Additional Information (optional)', + 'title': 'Additional Information', 'fields': [ 'Education Completed', 'Gender', 'Year of Birth', 'Preferred Language', ] - }, - { - 'title': 'Connected Accounts', - 'fields': [ - 'Dummy', - 'Facebook', - 'Google', - ] } ] @@ -240,13 +232,13 @@ class AccountSettingsPageTest(AccountSettingsTestMixin, WebAppTest): self.account_settings_page.wait_for_page() self.assertEqual(self.account_settings_page.value_for_dropdown_field(field_id), new_value) - def _test_link_field(self, field_id, title, link_title, success_message): + def _test_link_field(self, field_id, title, link_title, field_type, success_message): """ Test behaviour a link field. """ self.assertEqual(self.account_settings_page.title_for_field(field_id), title) self.assertEqual(self.account_settings_page.link_title_for_link_field(field_id), link_title) - self.account_settings_page.click_on_link_in_link_field(field_id) + self.account_settings_page.click_on_link_in_link_field(field_id, field_type=field_type) self.account_settings_page.wait_for_message(field_id, success_message) def test_username_field(self): @@ -316,7 +308,8 @@ class AccountSettingsPageTest(AccountSettingsTestMixin, WebAppTest): self._test_link_field( u'password', u'Password', - u'Reset Password', + u'Reset Your Password', + u'button', success_message='Click the link in the message to reset your password.', ) @@ -434,7 +427,7 @@ class AccountSettingsPageTest(AccountSettingsTestMixin, WebAppTest): actual_events ) - def test_connected_accounts(self): + def test_linked_accounts(self): """ Test that fields for third party auth providers exist. @@ -442,9 +435,11 @@ class AccountSettingsPageTest(AccountSettingsTestMixin, WebAppTest): because that would require accounts with the providers. """ providers = ( - ['auth-oa2-facebook', 'Facebook', 'Link'], - ['auth-oa2-google-oauth2', 'Google', 'Link'], + ['auth-oa2-facebook', 'Facebook', 'Link Your Account'], + ['auth-oa2-google-oauth2', 'Google', 'Link Your Account'], ) + # switch to "Linked Accounts" tab + self.account_settings_page.switch_account_settings_tabs('accounts-tab') for field_id, title, link_title in providers: self.assertEqual(self.account_settings_page.title_for_field(field_id), title) self.assertEqual(self.account_settings_page.link_title_for_link_field(field_id), link_title) diff --git a/common/test/acceptance/tests/lms/test_lms.py b/common/test/acceptance/tests/lms/test_lms.py index 4f7c2aaa2f..ddfed9c6ff 100644 --- a/common/test/acceptance/tests/lms/test_lms.py +++ b/common/test/acceptance/tests/lms/test_lms.py @@ -222,19 +222,29 @@ class LoginFromCombinedPageTest(UniqueCourseTest): 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() + # switch to "Linked Accounts" tab + account_settings.switch_account_settings_tabs('accounts-tab') + field_id = "auth-oa2-dummy" account_settings.wait_for_field(field_id) - self.assertEqual("Link", account_settings.link_title_for_link_field(field_id)) + self.assertEqual("Link Your Account", 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") + + # make sure we are on "Linked Accounts" tab after the account settings + # page is reloaded + account_settings.switch_account_settings_tabs('accounts-tab') + account_settings.wait_for_link_title_for_link_field(field_id, "Unlink This Account") 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() + # switch to "Linked Accounts" tab + account_settings.switch_account_settings_tabs('accounts-tab') + field_id = "auth-oa2-dummy" account_settings.wait_for_field(field_id) - self.assertEqual("Unlink", account_settings.link_title_for_link_field(field_id)) + self.assertEqual("Unlink This Account", account_settings.link_title_for_link_field(field_id)) account_settings.click_on_link_in_link_field(field_id) account_settings.wait_for_message(field_id, "Successfully unlinked") @@ -372,9 +382,12 @@ class RegisterFromCombinedPageTest(UniqueCourseTest): # Now unlink the account (To test the account settings view and also to prevent cross-test side effects) account_settings = AccountSettingsPage(self.browser).visit() + # switch to "Linked Accounts" tab + account_settings.switch_account_settings_tabs('accounts-tab') + field_id = "auth-oa2-dummy" account_settings.wait_for_field(field_id) - self.assertEqual("Unlink", account_settings.link_title_for_link_field(field_id)) + self.assertEqual("Unlink This Account", account_settings.link_title_for_link_field(field_id)) account_settings.click_on_link_in_link_field(field_id) account_settings.wait_for_message(field_id, "Successfully unlinked") diff --git a/lms/static/js/spec/student_account/account_settings_factory_spec.js b/lms/static/js/spec/student_account/account_settings_factory_spec.js index 656f696ae8..0f479188f4 100644 --- a/lms/static/js/spec/student_account/account_settings_factory_spec.js +++ b/lms/static/js/spec/student_account/account_settings_factory_spec.js @@ -141,7 +141,7 @@ define(['backbone', 'jquery', 'underscore', 'common/js/spec_helpers/ajax_helpers AjaxHelpers.respondWithJson(requests, Helpers.createUserPreferencesData()); AjaxHelpers.respondWithJson(requests, {}); // Page viewed analytics event - var sectionsData = accountSettingsView.options.sectionsData; + var sectionsData = accountSettingsView.options.tabSections.aboutTabSections; expect(sectionsData[0].fields.length).toBe(6); @@ -180,14 +180,6 @@ define(['backbone', 'jquery', 'underscore', 'common/js/spec_helpers/ajax_helpers defaultValue: null }, requests); }); - - var section2Fields = sectionsData[2].fields; - expect(section2Fields.length).toBe(2); - for (var i = 0; i < section2Fields.length; i++) { - - var view = section2Fields[i].view; - AccountSettingsFieldViewSpecHelpers.verifyAuthField(view, view.options, requests); - } }); }); }); diff --git a/lms/static/js/spec/student_account/account_settings_fields_helpers.js b/lms/static/js/spec/student_account/account_settings_fields_helpers.js index 21a2e659cb..2e908b0f8e 100644 --- a/lms/static/js/spec/student_account/account_settings_fields_helpers.js +++ b/lms/static/js/spec/student_account/account_settings_fields_helpers.js @@ -10,13 +10,13 @@ define(['backbone', 'jquery', 'underscore', 'common/js/spec_helpers/ajax_helpers spyOn(view, 'redirect_to'); FieldViewsSpecHelpers.expectTitleAndMessageToContain(view, data.title, data.helpMessage); - expect(view.$(selector).text().trim()).toBe('Unlink'); + expect(view.$(selector).text().trim()).toBe('Unlink This Account'); view.$(selector).click(); FieldViewsSpecHelpers.expectMessageContains(view, 'Unlinking'); AjaxHelpers.expectRequest(requests, 'POST', data.disconnectUrl); AjaxHelpers.respondWithNoContent(requests); - expect(view.$(selector).text().trim()).toBe('Link'); + expect(view.$(selector).text().trim()).toBe('Link Your Account'); FieldViewsSpecHelpers.expectMessageContains(view, 'Successfully unlinked.'); view.$(selector).click(); diff --git a/lms/static/js/spec/student_account/account_settings_fields_spec.js b/lms/static/js/spec/student_account/account_settings_fields_spec.js index 626c5b2f83..99be212f75 100644 --- a/lms/static/js/spec/student_account/account_settings_fields_spec.js +++ b/lms/static/js/spec/student_account/account_settings_fields_spec.js @@ -32,7 +32,7 @@ define(['backbone', 'jquery', 'underscore', 'common/js/spec_helpers/ajax_helpers }); var view = new AccountSettingsFieldViews.PasswordFieldView(fieldData).render(); - view.$('.u-field-value > a').click(); + view.$('.u-field-value > button').click(); AjaxHelpers.expectRequest(requests, 'POST', '/password_reset', "email=legolas%40woodland.middlearth"); AjaxHelpers.respondWithJson(requests, {"success": "true"}); FieldViewsSpecHelpers.expectMessageContains( diff --git a/lms/static/js/spec/student_account/account_settings_view_spec.js b/lms/static/js/spec/student_account/account_settings_view_spec.js index d1ab101a15..d7d3664480 100644 --- a/lms/static/js/spec/student_account/account_settings_view_spec.js +++ b/lms/static/js/spec/student_account/account_settings_view_spec.js @@ -15,7 +15,7 @@ define(['backbone', 'jquery', 'underscore', 'common/js/spec_helpers/ajax_helpers var model = new UserAccountModel(); model.set(Helpers.createAccountSettingsData()); - var sectionsData = [ + var aboutSectionsData = [ { title: "Basic Account Information", fields: [ @@ -53,7 +53,9 @@ define(['backbone', 'jquery', 'underscore', 'common/js/spec_helpers/ajax_helpers var accountSettingsView = new AccountSettingsView({ el: $('.wrapper-account-settings'), model: model, - sectionsData : sectionsData + tabSections: { + aboutTabSections: aboutSectionsData + } }); return accountSettingsView; diff --git a/lms/static/js/spec/student_account/helpers.js b/lms/static/js/spec/student_account/helpers.js index 159d903628..765c2ac0d7 100644 --- a/lms/static/js/spec/student_account/helpers.js +++ b/lms/static/js/spec/student_account/helpers.js @@ -75,8 +75,8 @@ define(['underscore'], function(_) { if ('fieldValue' in view) { expect(view.fieldValue()).toBe(view.modelValue()); - } else if (view.fieldType === 'link') { - expect($(element).find('a').length).toBe(1); + } else if (view.fieldType === 'button') { + expect($(element).find('button').length).toBe(1); } else { throw new Error('Unexpected field type: ' + view.fieldType); } @@ -87,7 +87,7 @@ define(['underscore'], function(_) { }; var expectSettingsSectionsAndFieldsToBeRendered = function (accountSettingsView, fieldsAreRendered) { - var sectionsData = accountSettingsView.options.sectionsData; + var sectionsData = accountSettingsView.options.tabSections.aboutTabSections; var sectionElements = accountSettingsView.$('.section'); expect(sectionElements.length).toBe(sectionsData.length); diff --git a/lms/static/js/student_account/views/account_section_view.js b/lms/static/js/student_account/views/account_section_view.js new file mode 100644 index 0000000000..4164c81c77 --- /dev/null +++ b/lms/static/js/student_account/views/account_section_view.js @@ -0,0 +1,27 @@ +;(function (define, undefined) { + 'use strict'; + define([ + 'gettext', + 'jquery', + 'underscore', + 'backbone', + 'text!templates/student_account/account_settings_section.underscore' + ], function (gettext, $, _, Backbone, sectionTemplate) { + + var AccountSectionView = Backbone.View.extend({ + + initialize: function (options) { + this.options = options; + }, + + render: function () { + this.$el.html(_.template(sectionTemplate)({ + sections: this.options.sections, + activeTabName: this.options.activeTabName + })); + } + }); + + return AccountSectionView; + }); +}).call(this, define || RequireJS.define); diff --git a/lms/static/js/student_account/views/account_settings_factory.js b/lms/static/js/student_account/views/account_settings_factory.js index 8739606e23..05517d00a9 100644 --- a/lms/static/js/student_account/views/account_settings_factory.js +++ b/lms/static/js/student_account/views/account_settings_factory.js @@ -2,30 +2,35 @@ 'use strict'; define([ 'gettext', 'jquery', 'underscore', 'backbone', 'logger', - 'js/views/fields', 'js/student_account/models/user_account_model', 'js/student_account/models/user_preferences_model', 'js/student_account/views/account_settings_fields', 'js/student_account/views/account_settings_view' - ], function (gettext, $, _, Backbone, Logger, FieldViews, UserAccountModel, UserPreferencesModel, + ], function (gettext, $, _, Backbone, Logger, UserAccountModel, UserPreferencesModel, AccountSettingsFieldViews, AccountSettingsView) { return function (fieldsData, authData, userAccountsApiUrl, userPreferencesApiUrl, accountUserId, platformName) { + var accountSettingsElement, userAccountModel, userPreferencesModel, aboutSectionsData, + accountsSectionData, accountSettingsView, showAccountSettingsPage, showLoadingError; - var accountSettingsElement = $('.wrapper-account-settings'); + accountSettingsElement = $('.wrapper-account-settings'); - var userAccountModel = new UserAccountModel(); + userAccountModel = new UserAccountModel(); userAccountModel.url = userAccountsApiUrl; - var userPreferencesModel = new UserPreferencesModel(); + userPreferencesModel = new UserPreferencesModel(); userPreferencesModel.url = userPreferencesApiUrl; - var sectionsData = [ + aboutSectionsData = [ { - title: gettext('Basic Account Information (required)'), + title: gettext('Basic Account Information'), + subtitle: gettext( + 'These settings include basic information about your account. You can also ' + + 'specify additional information and see your linked social accounts on this page.' + ), fields: [ { - view: new FieldViews.ReadonlyFieldView({ + view: new AccountSettingsFieldViews.ReadonlyFieldView({ model: userAccountModel, title: gettext('Username'), valueAttribute: 'username', @@ -35,7 +40,7 @@ }) }, { - view: new FieldViews.TextFieldView({ + view: new AccountSettingsFieldViews.TextFieldView({ model: userAccountModel, title: gettext('Full Name'), valueAttribute: 'name', @@ -60,12 +65,14 @@ view: new AccountSettingsFieldViews.PasswordFieldView({ model: userAccountModel, title: gettext('Password'), - screenReaderTitle: gettext('Reset your Password'), + screenReaderTitle: gettext('Reset Your Password'), valueAttribute: 'password', emailAttribute: 'email', - linkTitle: gettext('Reset Password'), + linkTitle: gettext('Reset Your Password'), linkHref: fieldsData.password.url, - helpMessage: gettext('When you click "Reset Password", a message will be sent to your email address. Click the link in the message to reset your password.') + helpMessage: gettext('When you click "Reset Your Password", edX will send a message ' + + 'to the email address for your edX account. Click the link in the message to ' + + 'reset your password.') }) }, { @@ -83,7 +90,7 @@ }) }, { - view: new FieldViews.DropdownFieldView({ + view: new AccountSettingsFieldViews.DropdownFieldView({ model: userAccountModel, required: true, title: gettext('Country or Region'), @@ -95,10 +102,10 @@ ] }, { - title: gettext('Additional Information (optional)'), + title: gettext('Additional Information'), fields: [ { - view: new FieldViews.DropdownFieldView({ + view: new AccountSettingsFieldViews.DropdownFieldView({ model: userAccountModel, title: gettext('Education Completed'), valueAttribute: 'level_of_education', @@ -107,7 +114,7 @@ }) }, { - view: new FieldViews.DropdownFieldView({ + view: new AccountSettingsFieldViews.DropdownFieldView({ model: userAccountModel, title: gettext('Gender'), valueAttribute: 'gender', @@ -116,7 +123,7 @@ }) }, { - view: new FieldViews.DropdownFieldView({ + view: new AccountSettingsFieldViews.DropdownFieldView({ model: userAccountModel, title: gettext('Year of Birth'), valueAttribute: 'year_of_birth', @@ -137,16 +144,17 @@ } ]; - if (_.isArray(authData.providers)) { - var accountsSectionData = { - title: gettext('Connected Accounts'), + accountsSectionData = [ + { + title: gettext('Linked Accounts'), + subtitle: gettext( + 'You can link your social media accounts to your edX account to make signing in to edx.org ' + + 'and the edX mobile apps easier.' + ), fields: _.map(authData.providers, function(provider) { return { 'view': new AccountSettingsFieldViews.AuthFieldView({ title: provider.name, - screenReaderTitle: interpolate_text( - gettext("Connect your {accountName} account"), {accountName: provider['name']} - ), valueAttribute: 'auth-' + provider.id, helpMessage: '', connected: provider.connected, @@ -156,24 +164,23 @@ }) }; }) - }; - sectionsData.push(accountsSectionData); - } + } + ]; - var accountSettingsView = new AccountSettingsView({ + accountSettingsView = new AccountSettingsView({ model: userAccountModel, accountUserId: accountUserId, el: accountSettingsElement, - sectionsData: sectionsData + tabSections: { + aboutTabSections: aboutSectionsData, + accountsTabSections: accountsSectionData + }, + userPreferencesModel: userPreferencesModel }); accountSettingsView.render(); - var showLoadingError = function () { - accountSettingsView.showLoadingError(); - }; - - var showAccountFields = function () { + showAccountSettingsPage = function () { // Record that the account settings page was viewed. Logger.log('edx.user.settings.viewed', { page: "account", @@ -185,11 +192,15 @@ accountSettingsView.renderFields(); }; + showLoadingError = function () { + accountSettingsView.showLoadingError(); + }; + userAccountModel.fetch({ success: function () { // Fetch the user preferences model userPreferencesModel.fetch({ - success: showAccountFields, + success: showAccountSettingsPage, error: showLoadingError }); }, diff --git a/lms/static/js/student_account/views/account_settings_fields.js b/lms/static/js/student_account/views/account_settings_fields.js index 179ab3991e..8446037fa7 100644 --- a/lms/static/js/student_account/views/account_settings_fields.js +++ b/lms/static/js/student_account/views/account_settings_fields.js @@ -1,191 +1,233 @@ ;(function (define, undefined) { 'use strict'; define([ - 'gettext', 'jquery', 'underscore', 'backbone', 'js/views/fields' - ], function (gettext, $, _, Backbone, FieldViews) { + 'gettext', + 'jquery', + 'underscore', + 'backbone', + 'js/views/fields', + 'text!templates/fields/field_text_account.underscore', + 'text!templates/fields/field_readonly_account.underscore', + 'text!templates/fields/field_link_account.underscore', + 'text!templates/fields/field_dropdown_account.underscore', + 'text!templates/fields/field_social_link_account.underscore' + ], function ( + gettext, $, _, Backbone, + FieldViews, + field_text_account_template, + field_readonly_account_template, + field_link_account_template, + field_dropdown_account_template, + field_social_link_template) + { - var AccountSettingsFieldViews = {}; + var AccountSettingsFieldViews = { + ReadonlyFieldView: FieldViews.ReadonlyFieldView.extend({ + fieldTemplate: field_readonly_account_template + }), + TextFieldView: FieldViews.TextFieldView.extend({ + fieldTemplate: field_text_account_template + }), + DropdownFieldView: FieldViews.DropdownFieldView.extend({ + fieldTemplate: field_dropdown_account_template + }), + EmailFieldView: FieldViews.TextFieldView.extend({ + fieldTemplate: field_text_account_template, + successMessage: function () { + return this.indicators.success + window.interpolate_text( + gettext( + 'We\'ve sent a confirmation message to {new_email_address}. Click the link in the ' + + 'message to update your email address.' + ), + {'new_email_address': this.fieldValue()} + ); + } + }), + LanguagePreferenceFieldView: FieldViews.DropdownFieldView.extend({ + fieldTemplate: field_dropdown_account_template, + saveSucceeded: function () { + var data = { + 'language': this.modelValue() + }; - AccountSettingsFieldViews.EmailFieldView = FieldViews.TextFieldView.extend({ - - successMessage: function() { - return this.indicators.success + interpolate_text( - gettext( - /* jshint maxlen: false */ - 'We\'ve sent a confirmation message to {new_email_address}. Click the link in the message to update your email address.' - ), - {'new_email_address': this.fieldValue()} - ); - } - }); - - AccountSettingsFieldViews.LanguagePreferenceFieldView = FieldViews.DropdownFieldView.extend({ - - saveSucceeded: function () { - var data = { - 'language': this.modelValue() - }; - - var view = this; - $.ajax({ - type: 'POST', - url: '/i18n/setlang/', - data: data, - dataType: 'html', - success: function () { - view.showSuccessMessage(); - }, - error: function () { - view.showNotificationMessage( - view.indicators.error + + var view = this; + $.ajax({ + type: 'POST', + url: '/i18n/setlang/', + data: data, + dataType: 'html', + success: function () { + view.showSuccessMessage(); + }, + error: function () { + view.showNotificationMessage( + view.indicators.error + gettext('You must sign out and sign back in before your language changes take effect.') + ); + } + }); + } + + }), + PasswordFieldView: FieldViews.LinkFieldView.extend({ + fieldType: 'button', + fieldTemplate: field_link_account_template, + events: { + 'click button': 'linkClicked' + }, + initialize: function (options) { + this.options = _.extend({}, options); + this._super(options); + _.bindAll(this, 'resetPassword'); + }, + linkClicked: function (event) { + event.preventDefault(); + this.resetPassword(event); + }, + resetPassword: function () { + var data = {}; + data[this.options.emailAttribute] = this.model.get(this.options.emailAttribute); + + var view = this; + $.ajax({ + type: 'POST', + url: view.options.linkHref, + data: data, + success: function () { + view.showSuccessMessage(); + }, + error: function (xhr) { + view.showErrorMessage(xhr); + } + }); + }, + successMessage: function () { + return this.indicators.success + window.interpolate_text( + gettext( + 'We\'ve sent a message to {email_address}. Click the link in the message to reset ' + + 'your password.' + ), + {'email_address': this.model.get(this.options.emailAttribute)} + ); + } + }), + LanguageProficienciesFieldView: FieldViews.DropdownFieldView.extend({ + fieldTemplate: field_dropdown_account_template, + modelValue: function () { + var modelValue = this.model.get(this.options.valueAttribute); + if (_.isArray(modelValue) && modelValue.length > 0) { + return modelValue[0].code; + } else { + return null; + } + }, + saveValue: function () { + if (this.persistChanges === true) { + var attributes = {}, + value = this.fieldValue() ? [{'code': this.fieldValue()}] : []; + attributes[this.options.valueAttribute] = value; + this.saveAttributes(attributes); + } + } + }), + AuthFieldView: FieldViews.LinkFieldView.extend({ + fieldTemplate: field_social_link_template, + className: function () { + return 'u-field u-field-social u-field-' + this.options.valueAttribute; + }, + initialize: function (options) { + this.options = _.extend({}, options); + this._super(options); + _.bindAll(this, 'redirect_to', 'disconnect', 'successMessage', 'inProgressMessage'); + }, + render: function () { + var linkTitle = '', + linkClass = '', + subTitle = '', + screenReaderTitle = window.interpolate_text( + gettext("Link your {accountName} account"), {accountName: this.options.title} + ); + if (this.options.connected) { + linkTitle = gettext('Unlink This Account'); + linkClass = 'social-field-linked'; + subTitle = window.interpolate_text( + gettext('You can use your {accountName} account to sign in to your edX account.'), + {accountName: this.options.title} + ); + screenReaderTitle = window.interpolate_text( + gettext("Unlink your {accountName} account"), {accountName: this.options.title} + ); + } else if (this.options.acceptsLogins) { + linkTitle = gettext('Link Your Account'); + linkClass = 'social-field-unlinked'; + subTitle = window.interpolate_text( + gettext( + 'Link your {accountName} account to your edX account and ' + + 'use {accountName} to sign in to edX.' + ), {accountName: this.options.title} ); } - }); - } - }); + this.$el.html(this.template({ + id: this.options.valueAttribute, + title: this.options.title, + screenReaderTitle: screenReaderTitle, + linkTitle: linkTitle, + subTitle: subTitle, + linkClass: linkClass, + linkHref: '#', + message: this.helpMessage + })); + this.delegateEvents(); + return this; + }, + linkClicked: function (event) { + event.preventDefault(); - AccountSettingsFieldViews.PasswordFieldView = FieldViews.LinkFieldView.extend({ + this.showInProgressMessage(); - initialize: function (options) { - this.options = _.extend({}, options); - this._super(options); - _.bindAll(this, 'resetPassword'); - }, - - linkClicked: function (event) { - event.preventDefault(); - this.resetPassword(event); - }, - - resetPassword: function () { - var data = {}; - data[this.options.emailAttribute] = this.model.get(this.options.emailAttribute); - - var view = this; - $.ajax({ - type: 'POST', - url: view.options.linkHref, - data: data, - success: function () { - view.showSuccessMessage(); - }, - error: function (xhr) { - view.showErrorMessage(xhr); + if (this.options.connected) { + this.disconnect(); + } else { + // Direct the user to the providers site to start the authentication process. + // See python-social-auth docs for more information. + this.redirect_to(this.options.connectUrl); } - }); - }, + }, + redirect_to: function (url) { + window.location.href = url; + }, + disconnect: function () { + var data = {}; - successMessage: function () { - return this.indicators.success + interpolate_text( - gettext( - /* jshint maxlen: false */ - 'We\'ve sent a message to {email_address}. Click the link in the message to reset your password.' - ), - {'email_address': this.model.get(this.options.emailAttribute)} - ); - } - }); - - AccountSettingsFieldViews.LanguageProficienciesFieldView = FieldViews.DropdownFieldView.extend({ - - modelValue: function () { - var modelValue = this.model.get(this.options.valueAttribute); - if (_.isArray(modelValue) && modelValue.length > 0) { - return modelValue[0].code; - } else { - return null; - } - }, - - saveValue: function () { - if (this.persistChanges === true) { - var attributes = {}, - value = this.fieldValue() ? [{'code': this.fieldValue()}] : []; - attributes[this.options.valueAttribute] = value; - this.saveAttributes(attributes); - } - } - }); - - AccountSettingsFieldViews.AuthFieldView = FieldViews.LinkFieldView.extend({ - - initialize: function (options) { - this.options = _.extend({}, options); - this._super(options); - _.bindAll(this, 'redirect_to', 'disconnect', 'successMessage', 'inProgressMessage'); - }, - - render: function () { - var linkTitle; - if (this.options.connected) { - linkTitle = gettext('Unlink'); - } else if (this.options.acceptsLogins) { - linkTitle = gettext('Link') - } else { - linkTitle = '' - } - - this.$el.html(this.template({ - id: this.options.valueAttribute, - title: this.options.title, - screenReaderTitle: this.options.screenReaderTitle, - linkTitle: linkTitle, - linkHref: '', - message: this.helpMessage - })); - return this; - }, - - linkClicked: function (event) { - event.preventDefault(); - - this.showInProgressMessage(); - - if (this.options.connected) { - this.disconnect(); - } else { - // Direct the user to the providers site to start the authentication process. + // Disconnects the provider from the user's edX account. // See python-social-auth docs for more information. - this.redirect_to(this.options.connectUrl); + var view = this; + $.ajax({ + type: 'POST', + url: this.options.disconnectUrl, + data: data, + dataType: 'html', + success: function () { + view.options.connected = false; + view.render(); + view.showSuccessMessage(); + }, + error: function (xhr) { + view.showErrorMessage(xhr); + } + }); + }, + inProgressMessage: function () { + return this.indicators.inProgress + ( + this.options.connected ? gettext('Unlinking') : gettext('Linking') + ); + }, + successMessage: function () { + return this.indicators.success + gettext('Successfully unlinked.'); } - }, - - redirect_to: function (url) { - window.location.href = url; - }, - - disconnect: function () { - var data = {}; - - // Disconnects the provider from the user's edX account. - // See python-social-auth docs for more information. - var view = this; - $.ajax({ - type: 'POST', - url: this.options.disconnectUrl, - data: data, - dataType: 'html', - success: function () { - view.options.connected = false; - view.render(); - view.showSuccessMessage(); - }, - error: function (xhr) { - view.showErrorMessage(xhr); - } - }); - }, - - inProgressMessage: function() { - return this.indicators.inProgress + (this.options.connected ? gettext('Unlinking') : gettext('Linking')); - }, - - successMessage: function() { - return this.indicators.success + gettext('Successfully unlinked.'); - } - }); + }), + }; return AccountSettingsFieldViews; }); diff --git a/lms/static/js/student_account/views/account_settings_view.js b/lms/static/js/student_account/views/account_settings_view.js index 2cae445b68..b3b80a5edd 100644 --- a/lms/static/js/student_account/views/account_settings_view.js +++ b/lms/static/js/student_account/views/account_settings_view.js @@ -1,29 +1,71 @@ ;(function (define, undefined) { 'use strict'; define([ - 'gettext', 'jquery', 'underscore', 'backbone', 'text!templates/student_account/account_settings.underscore' - ], function (gettext, $, _, Backbone, accountSettingsTemplate) { + 'gettext', + 'jquery', + 'underscore', + 'backbone', + 'js/student_account/views/account_section_view', + 'text!templates/student_account/account_settings.underscore' + ], function (gettext, $, _, Backbone, AccountSectionView, accountSettingsTemplate) { var AccountSettingsView = Backbone.View.extend({ + navLink: '.account-nav-link', + activeTab: 'aboutTabSections', + accountSettingsTabs: [ + {name: 'aboutTabSections', id: 'about-tab', label: gettext('Account Information'), class: 'active'}, + {name: 'accountsTabSections', id: 'accounts-tab', label: gettext('Linked Accounts')} + ], + events: { + 'click .account-nav-link': 'changeTab' + }, + initialize: function (options) { - this.options = _.extend({}, options); - _.bindAll(this, 'render', 'renderFields', 'showLoadingError'); + this.options = options; + _.bindAll(this, 'render', 'changeTab', 'renderFields', 'showLoadingError'); }, render: function () { this.$el.html(_.template(accountSettingsTemplate)({ - sections: this.options.sectionsData + accountSettingsTabs: this.accountSettingsTabs })); + this.renderSection(this.options.tabSections[this.activeTab]); return this; }, - renderFields: function () { - this.$('.ui-loading-indicator').addClass('is-hidden'); + changeTab: function(e) { + var $currentTab; + e.preventDefault(); + $currentTab = $(e.target); + this.activeTab = $currentTab.data('name'); + this.renderSection(this.options.tabSections[this.activeTab]); + this.renderFields(); + + $(this.navLink).removeClass('active'); + $currentTab.addClass('active'); + + $(this.navLink).removeAttr('aria-describedby'); + $currentTab.attr('aria-describedby', 'header-subtitle-'+this.activeTab); + }, + + renderSection: function (tabSections) { + var accountSectionView = new AccountSectionView({ + activeTabName: this.activeTab, + sections: tabSections, + el: '.account-settings-sections' + }); + + accountSectionView.render(); + }, + + renderFields: function () { var view = this; - _.each(this.$('.account-settings-section-body'), function (sectionEl, index) { - _.each(view.options.sectionsData[index].fields, function (field) { + view.$('.ui-loading-indicator').addClass('is-hidden'); + + _.each(view.$('.account-settings-section-body'), function (sectionEl, index) { + _.each(view.options.tabSections[view.activeTab][index].fields, function (field) { $(sectionEl).append(field.view.render().el); }); }); diff --git a/lms/static/js/views/fields.js b/lms/static/js/views/fields.js index baff798dbf..e22f7e840c 100644 --- a/lms/static/js/views/fields.js +++ b/lms/static/js/views/fields.js @@ -230,8 +230,15 @@ }, finishEditing: function() { + var modelValue; if (this.persistChanges === false || this.mode !== 'edit') {return;} - if (this.fieldValue() !== this.modelValue()) { + + modelValue = this.modelValue(); + if (!(_.isUndefined(modelValue) || _.isNull(modelValue))) { + modelValue = modelValue.toString(); + } + + if (this.fieldValue() !== modelValue) { this.saveValue(); } else { if (this.editable === 'always') { diff --git a/lms/static/sass/base/_variables.scss b/lms/static/sass/base/_variables.scss index 0608be8817..ef80e9822a 100644 --- a/lms/static/sass/base/_variables.scss +++ b/lms/static/sass/base/_variables.scss @@ -518,7 +518,6 @@ $dark-gray: rgb(51, 51, 51) !default; $border-color: rgb(200, 200, 200) !default; $sidebar-color: rgb(246, 246, 246) !default; $outer-border-color: $gray-l3; -$light-gray: rgb(221,221,221) !default; // used by descriptor css $lightGrey: rgb(237,241,245) !default; diff --git a/lms/static/sass/shared-v2/_footer.scss b/lms/static/sass/shared-v2/_footer.scss index 8951da58af..f88e9190a2 100644 --- a/lms/static/sass/shared-v2/_footer.scss +++ b/lms/static/sass/shared-v2/_footer.scss @@ -3,7 +3,6 @@ .wrapper-footer { @extend %ui-print-excluded; - margin-top: ($baseline*2) + px; box-shadow: 0 -1px 5px 0 $shadow-l1; border-top: 1px solid tint(palette(grayscale, light), 50%); padding: 25px ($baseline/2 + px) ($baseline*1.5 + px) ($baseline/2 + px); diff --git a/lms/static/sass/shared/_footer.scss b/lms/static/sass/shared/_footer.scss index a19c655438..b92cdb475c 100644 --- a/lms/static/sass/shared/_footer.scss +++ b/lms/static/sass/shared/_footer.scss @@ -5,7 +5,6 @@ .wrapper-footer { @extend %ui-print-excluded; - margin-top: ($baseline*2); box-shadow: 0 -1px 5px 0 $shadow-l1; border-top: 1px solid tint($m-gray, 50%); padding: 25px ($baseline/2) ($baseline*1.5) ($baseline/2); diff --git a/lms/static/sass/views/_account-settings.scss b/lms/static/sass/views/_account-settings.scss index 18457f6757..88fff0224e 100644 --- a/lms/static/sass/views/_account-settings.scss +++ b/lms/static/sass/views/_account-settings.scss @@ -9,11 +9,13 @@ // +Container - Account Settings .wrapper-account-settings { - @extend .container; - padding-top: ($baseline*2); + background: $white; + width: 100%; .account-settings-container { - padding: 0; + max-width: grid-width(12); + padding: 10px; + margin: 0 auto; } .ui-loading-indicator, @@ -36,15 +38,59 @@ .wrapper-account-settings { .wrapper-header { + max-width: grid-width(12); + height: 139px; + border-bottom: 4px solid $m-gray-l4; .header-title { @extend %t-title4; margin-bottom: ($baseline/2); + padding-top: ($baseline*2); } .header-subtitle { color: $gray-l2; } + + .account-nav { + @include float(left); + margin: ($baseline/2) 0; + padding: 0; + list-style: none; + + .account-nav-item { + @include float(left); + display: flex; + margin: 0; + text-transform: none; + justify-content: center; + + .account-nav-link { + font-size: em(14); + color: $gray; + padding: 5px 25px 23px; + display: inline-block; + border-radius: 0; + } + + button { + @extend %ui-clear-button; + @extend %btn-no-style; + @include appearance(none); + display:block; + padding: ($baseline/4); + + &:hover, + &:focus { + text-decoration: none; + border-bottom: 4px solid $courseware-border-bottom-color !important; + } + &.active{ + border-bottom: 4px solid $black-t3 !important; + } + } + } + } } } @@ -54,27 +100,189 @@ .section-header { @extend %t-title6; @extend %t-strong; - padding-bottom: ($baseline/2); - border-bottom: 1px solid $gray-l4; + padding-top: ($baseline/2)*3; + color: $dark-gray1; } .section { background-color: $white; - padding: $baseline; margin-top: $baseline; - border: 1px solid $gray-l4; - box-shadow: 0 0 1px 1px $shadow-l2; - border-radius: 5px; - } + border-bottom: 4px solid $m-gray-l4; - a span { - color: $link-color; - } + .account-settings-header-subtitle { + font-size: em(18); + line-height: normal; + color: $dark-gray; + padding-top: 20px; + padding-bottom: 10px; + } - a span { - &:hover, &:focus { - color: $pink; - text-decoration: none !important; + .account-settings-section-body { + + .u-field { + border-bottom: 2px solid $m-gray-l4; + + .field { + width: 30%; + vertical-align: top; + display: inline-block; + position: relative; + + select { + @include appearance(none); + padding: 14px 30px 14px 15px; + border: 1px solid $light-gray; + background-color: transparent; + border-radius: 2px; + position: relative; + z-index: 10; + &::-ms-expand{ + display: none; + } + ~ .icon-caret-down{ + &:after{ + content: ""; + border-left: 6px solid transparent; + border-right: 6px solid transparent; + border-top: 7px solid $blue; + position: absolute; + right: 10px; + bottom: 20px; + z-index: 0; + } + } + } + .field-label { + display: block; + width: auto; + margin-bottom: 0.625rem; + font-size: 1rem; + line-height: 1; + color: $dark-gray; + } + + .field-input { + @include transition(all 0.125s ease-in-out 0s); + display: inline-block; + padding: 0.625rem; + border: 1px solid $light-gray; + border-radius: 2px; + background: $white; + font-size: $body-font-size; + color: $dark-gray; + width: 100%; + height: 48px; + box-shadow: none; + } + + .u-field-link { + @extend %ui-clear-button; + + // set styles + @extend %btn-pl-default-base; + @include font-size(18); + width: 100%; + border: 1px solid $blue; + color: $blue; + padding: 11px 14px; + line-height: normal; + } + + #u-field-value-username { + padding-top: ($baseline/2); + } + } + + .social-field-linked { + background: $m-gray-l4; + box-shadow: 0 1px 2px 1px $shadow-l2; + padding: 1.25rem; + box-sizing: border-box; + margin: 10px; + width: 100%; + + .field-label { + @include font-size(24); + } + + .u-field-social-help { + display: inline-block; + padding: 20px 0 6px; + } + + .u-field-link { + @include font-size(14); + @include text-align(left); + border: none; + margin-top: $baseline; + font-weight: $font-semibold; + padding: 0; + + &:focus, &:hover, &:active { + background-color: transparent; + color: $m-blue-d3; + border: none; + } + } + } + + .social-field-unlinked { + background: $m-gray-l4; + box-shadow: 0 1px 2px 1px $shadow-l2; + padding: 1.25rem; + box-sizing: border-box; + text-align: center; + margin: 10px; + width: 100%; + + .field-label { + @include font-size(24); + text-align: center; + } + + .u-field-link { + @include font-size(14); + margin-top: $baseline; + font-weight: $font-semibold; + } + } + + .u-field-message { + position: relative; + padding: 24px 0 0 ($baseline*5); + + .u-field-message-notification { + position: absolute; + left: 0; + top: 0; + bottom: 0; + margin: auto; + padding: 38px 0 0 ($baseline*5); + } + } + + &:last-child { + border-bottom: none; + margin-bottom: ($baseline*2); + } + } + + .u-field-social { + border-bottom: none; + margin-right: 20px; + width: 30%; + display: inline-block; + vertical-align: top; + + .u-field-social-help { + @include font-size(12); + color: $m-gray-d1; + } + } + } + + &:last-child { + border-bottom: none; } } } diff --git a/lms/templates/fields/field_dropdown_account.underscore b/lms/templates/fields/field_dropdown_account.underscore new file mode 100644 index 0000000000..535cee9a15 --- /dev/null +++ b/lms/templates/fields/field_dropdown_account.underscore @@ -0,0 +1,42 @@ +
+ <% if (editable !== 'never') { %> + <% if (title && titleVisible) { %> + + <% } else { %> + + <% } %> + <% } %> + + <% if (iconName) { %> + + <% } %> + + <% if (editable === 'never') { %> + <%- screenReaderTitle %> + + <% } else { %> + + + + <% } %> +
+ + + + <%- message %> + diff --git a/lms/templates/fields/field_link_account.underscore b/lms/templates/fields/field_link_account.underscore new file mode 100644 index 0000000000..33babf7a82 --- /dev/null +++ b/lms/templates/fields/field_link_account.underscore @@ -0,0 +1,8 @@ +
+ <%- title %> + +
+ + + <%- message %> + diff --git a/lms/templates/fields/field_readonly_account.underscore b/lms/templates/fields/field_readonly_account.underscore new file mode 100644 index 0000000000..c8db13d9d1 --- /dev/null +++ b/lms/templates/fields/field_readonly_account.underscore @@ -0,0 +1,8 @@ +
+ <%- title %> + <%- value %> +
+ + + <%- message %> + diff --git a/lms/templates/fields/field_social_link_account.underscore b/lms/templates/fields/field_social_link_account.underscore new file mode 100644 index 0000000000..f3120be4f0 --- /dev/null +++ b/lms/templates/fields/field_social_link_account.underscore @@ -0,0 +1,13 @@ +
+

<%- title %>

+ <%- subTitle %> + + <%- screenReaderTitle %> + + +
+ + + + <%- message %> + diff --git a/lms/templates/fields/field_text_account.underscore b/lms/templates/fields/field_text_account.underscore new file mode 100644 index 0000000000..18133ea2eb --- /dev/null +++ b/lms/templates/fields/field_text_account.underscore @@ -0,0 +1,8 @@ +
+ + +
+ + + <%- message %> + diff --git a/lms/templates/student_account/account_settings.underscore b/lms/templates/student_account/account_settings.underscore index 6292dbdb91..d2a168630c 100644 --- a/lms/templates/student_account/account_settings.underscore +++ b/lms/templates/student_account/account_settings.underscore @@ -2,25 +2,16 @@

<%- gettext("Account Settings") %>

- +
diff --git a/lms/templates/student_account/account_settings_section.underscore b/lms/templates/student_account/account_settings_section.underscore new file mode 100644 index 0000000000..eb7f420a82 --- /dev/null +++ b/lms/templates/student_account/account_settings_section.underscore @@ -0,0 +1,18 @@ +<% _.each(sections, function(section) { %> +
+ <% if (section.subtitle) { %> + + <% } %> +

<%- gettext(section.title) %>

+ +
+<% }); %>