From 3af769b4ad14321c383c4b283ce05d16715c758a Mon Sep 17 00:00:00 2001 From: Kevin Kim Date: Mon, 1 Aug 2016 14:02:20 +0000 Subject: [PATCH] Update time zone dropdown in account settings based on user country --- .../account_settings_factory_spec.js | 7 ++ .../account_settings_fields_spec.js | 69 ++++++++++++++++++- lms/static/js/spec/student_account/helpers.js | 6 ++ .../views/account_settings_factory.js | 22 +++++- .../views/account_settings_fields.js | 61 ++++++++++++++++ lms/static/js/views/fields.js | 25 ++++++- .../fields/field_dropdown.underscore | 9 ++- .../fields/field_dropdown_account.underscore | 9 ++- .../djangoapps/user_api/preferences/api.py | 36 ++++++++-- .../user_api/preferences/tests/test_api.py | 18 +++-- .../core/djangoapps/user_api/serializers.py | 17 +---- 11 files changed, 244 insertions(+), 35 deletions(-) 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 58c1554250..fd417507c2 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 @@ -108,6 +108,11 @@ define(['backbone', request = requests[1]; expect(request.method).toBe('GET'); + expect(request.url).toBe('/user_api/v1/preferences/time_zones/?country_code=1'); + AjaxHelpers.respondWithJson(requests, Helpers.TIME_ZONE_RESPONSE); + + request = requests[2]; + expect(request.method).toBe('GET'); expect(request.url).toBe(Helpers.USER_PREFERENCES_API_URL); AjaxHelpers.respondWithError(requests, 500); @@ -126,6 +131,7 @@ define(['backbone', Helpers.expectSettingsSectionsButNotFieldsToBeRendered(accountSettingsView); AjaxHelpers.respondWithJson(requests, Helpers.createAccountSettingsData()); + AjaxHelpers.respondWithJson(requests, Helpers.TIME_ZONE_RESPONSE); AjaxHelpers.respondWithJson(requests, Helpers.createUserPreferencesData()); Helpers.expectLoadingIndicatorIsVisible(accountSettingsView, false); @@ -141,6 +147,7 @@ define(['backbone', var accountSettingsView = createAccountSettingsPage(); AjaxHelpers.respondWithJson(requests, Helpers.createAccountSettingsData()); + AjaxHelpers.respondWithJson(requests, Helpers.TIME_ZONE_RESPONSE); AjaxHelpers.respondWithJson(requests, Helpers.createUserPreferencesData()); AjaxHelpers.respondWithJson(requests, {}); // Page viewed analytics event 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 ca19620440..c69958b75a 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 @@ -45,7 +45,74 @@ define(['backbone', ); }); - it('sends request to /i18n/setlang/ after changing language preference in LanguagePreferenceFieldView', function() { + it('update time zone dropdown after country dropdown changes', function() { + var baseSelector = '.u-field-value > select'; + var groupsSelector = baseSelector + '> optgroup'; + var groupOptionsSelector = groupsSelector + '> option'; + + var timeZoneData = FieldViewsSpecHelpers.createFieldData(AccountSettingsFieldViews.TimeZoneFieldView, { + valueAttribute: 'time_zone', + groupOptions: [{ + groupTitle: gettext('All Time Zones'), + selectOptions: FieldViewsSpecHelpers.SELECT_OPTIONS + }], + persistChanges: true, + required: true + }); + var countryData = FieldViewsSpecHelpers.createFieldData(AccountSettingsFieldViews.DropdownFieldView, { + valueAttribute: 'country', + options: [['KY', 'Cayman Islands'], ['CA', 'Canada'], ['GY', 'Guyana']], + persistChanges: true + }); + + var countryChange = {country: 'GY'}; + var timeZoneChange = {time_zone: 'Pacific/Kosrae'}; + + var timeZoneView = new AccountSettingsFieldViews.TimeZoneFieldView(timeZoneData).render(); + var countryView = new AccountSettingsFieldViews.DropdownFieldView(countryData).render(); + + requests = AjaxHelpers.requests(this); + + timeZoneView.listenToCountryView(countryView); + + // expect time zone dropdown to have single subheader ('All Time Zones') + expect(timeZoneView.$(groupsSelector).length).toBe(1); + expect(timeZoneView.$(groupOptionsSelector).length).toBe(3); + expect(timeZoneView.$(groupOptionsSelector)[0].value).toBe(FieldViewsSpecHelpers.SELECT_OPTIONS[0][0]); + + // change country + countryView.$(baseSelector).val(countryChange[countryData.valueAttribute]).change(); + FieldViewsSpecHelpers.expectAjaxRequestWithData(requests, countryChange); + AjaxHelpers.respondWithJson(requests, {success: 'true'}); + + AjaxHelpers.expectRequest( + requests, + 'GET', + '/user_api/v1/preferences/time_zones/?country_code=GY' + ); + AjaxHelpers.respondWithJson(requests, [ + {time_zone: 'America/Guyana', description: 'America/Guyana (ECT, UTC-0500)'}, + {time_zone: 'Pacific/Kosrae', description: 'Pacific/Kosrae (KOST, UTC+1100)'} + ]); + + // expect time zone dropdown to have two subheaders (country/all time zone sub-headers) with new values + expect(timeZoneView.$(groupsSelector).length).toBe(2); + expect(timeZoneView.$(groupOptionsSelector).length).toBe(5); + expect(timeZoneView.$(groupOptionsSelector)[0].value).toBe('America/Guyana'); + + // select time zone option from option + timeZoneView.$(baseSelector).val(timeZoneChange[timeZoneData.valueAttribute]).change(); + FieldViewsSpecHelpers.expectAjaxRequestWithData(requests, timeZoneChange); + AjaxHelpers.respondWithJson(requests, {success: 'true'}); + timeZoneView.render(); + + // expect time zone dropdown to have three subheaders (currently selected/country/all time zones) + expect(timeZoneView.$(groupsSelector).length).toBe(3); + expect(timeZoneView.$(groupOptionsSelector).length).toBe(6); + expect(timeZoneView.$(groupOptionsSelector)[0].value).toBe('Pacific/Kosrae'); + }); + + it('sends request to /i18n/setlang/ after changing language in LanguagePreferenceFieldView', function() { requests = AjaxHelpers.requests(this); var selector = '.u-field-value > select'; diff --git a/lms/static/js/spec/student_account/helpers.js b/lms/static/js/spec/student_account/helpers.js index 49e9bd44fa..0bfc695ef1 100644 --- a/lms/static/js/spec/student_account/helpers.js +++ b/lms/static/js/spec/student_account/helpers.js @@ -49,6 +49,11 @@ define(['underscore'], function(_) { ['3', 'Option 3'] ]; + var TIME_ZONE_RESPONSE = [{ + time_zone: 'America/Guyana', + description: 'America/Guyana (ECT, UTC-0500)' + }]; + var IMAGE_MAX_BYTES = 1024 * 1024; var IMAGE_MIN_BYTES = 100; @@ -123,6 +128,7 @@ define(['underscore'], function(_) { createAccountSettingsData: createAccountSettingsData, createUserPreferencesData: createUserPreferencesData, FIELD_OPTIONS: FIELD_OPTIONS, + TIME_ZONE_RESPONSE: TIME_ZONE_RESPONSE, expectLoadingIndicatorIsVisible: expectLoadingIndicatorIsVisible, expectLoadingErrorIsVisible: expectLoadingErrorIsVisible, expectElementContainsField: expectElementContainsField, 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 d9d464f3e2..fc92c267da 100644 --- a/lms/static/js/student_account/views/account_settings_factory.js +++ b/lms/static/js/student_account/views/account_settings_factory.js @@ -20,7 +20,7 @@ ) { var accountSettingsElement, userAccountModel, userPreferencesModel, aboutSectionsData, accountsSectionData, ordersSectionData, accountSettingsView, showAccountSettingsPage, - showLoadingError, orderNumber; + showLoadingError, orderNumber, getUserField, userFields, timeZoneDropdownField, countryDropdownField; accountSettingsElement = $('.wrapper-account-settings'); @@ -110,7 +110,7 @@ }) }, { - view: new AccountSettingsFieldViews.DropdownFieldView({ + view: new AccountSettingsFieldViews.TimeZoneFieldView({ model: userPreferencesModel, required: true, title: gettext('Time Zone'), @@ -120,7 +120,10 @@ 'time zone here, course dates, including assignment deadlines, are displayed in ' + 'Coordinated Universal Time (UTC).' ), - options: fieldsData.time_zone.options, + groupOptions: [{ + groupTitle: gettext('All Time Zones'), + selectOptions: fieldsData.time_zone.options + }], persistChanges: true }) } @@ -169,6 +172,19 @@ } ]; + // set TimeZoneField to listen to CountryField + getUserField = function(list, search) { + return _.find(list, function(field) { + return field.view.options.valueAttribute === search; + }).view; + }; + userFields = _.find(aboutSectionsData, function(section) { + return section.title === gettext('Basic Account Information'); + }).fields; + timeZoneDropdownField = getUserField(userFields, 'time_zone'); + countryDropdownField = getUserField(userFields, 'country'); + timeZoneDropdownField.listenToCountryView(countryDropdownField); + accountsSectionData = [ { title: gettext('Linked Accounts'), 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 4262fa7330..1e350b22cc 100644 --- a/lms/static/js/student_account/views/account_settings_fields.js +++ b/lms/static/js/student_account/views/account_settings_fields.js @@ -76,6 +76,67 @@ }); } + }), + TimeZoneFieldView: FieldViews.DropdownFieldView.extend({ + fieldTemplate: field_dropdown_account_template, + + initialize: function(options) { + this.options = _.extend({}, options); + _.bindAll(this, 'listenToCountryView', 'updateCountrySubheader', 'replaceOrAddGroupOption'); + this._super(options); // eslint-disable-line no-underscore-dangle + }, + + listenToCountryView: function(view) { + this.listenTo(view.model, 'change:country', this.updateCountrySubheader); + }, + + updateCountrySubheader: function(user) { + var view = this; + $.ajax({ + type: 'GET', + url: '/user_api/v1/preferences/time_zones/', + data: {country_code: user.attributes.country}, + success: function(data) { + var countryTimeZones = $.map(data, function(timeZoneInfo) { + return [[timeZoneInfo.time_zone, timeZoneInfo.description]]; + }); + view.replaceOrAddGroupOption( + 'Country Time Zones', + countryTimeZones + ); + view.render(); + } + }); + }, + + updateValueInField: function() { + var options; + if (this.modelValue()) { + options = [[this.modelValue(), this.displayValue(this.modelValue())]]; + this.replaceOrAddGroupOption( + 'Currently Selected Time Zone', + options + ); + } + this._super(); // eslint-disable-line no-underscore-dangle + }, + + replaceOrAddGroupOption: function(title, options) { + var groupOption = { + groupTitle: gettext(title), + selectOptions: options + }; + + var index = _.findIndex(this.options.groupOptions, function(group) { + return group.groupTitle === gettext(title); + }); + if (index >= 0) { + this.options.groupOptions[index] = groupOption; + } else { + this.options.groupOptions.unshift(groupOption); + } + } + }), PasswordFieldView: FieldViews.LinkFieldView.extend({ fieldType: 'button', diff --git a/lms/static/js/views/fields.js b/lms/static/js/views/fields.js index ccf44340f1..c67f185711 100644 --- a/lms/static/js/views/fields.js +++ b/lms/static/js/views/fields.js @@ -369,7 +369,8 @@ }, initialize: function(options) { - _.bindAll(this, 'render', 'optionForValue', 'fieldValue', 'displayValue', 'updateValueInField', 'saveValue'); + _.bindAll(this, 'render', 'optionForValue', 'fieldValue', 'displayValue', 'updateValueInField', + 'saveValue', 'createGroupOptions'); this._super(options); this.listenTo(this.model, 'change:' + this.options.valueAttribute, this.updateValueInField); @@ -385,7 +386,7 @@ titleVisible: this.options.titleVisible !== undefined ? this.options.titleVisible : true, iconName: this.options.iconName, showBlankOption: (!this.options.required || !this.modelValueIsSet()), - selectOptions: this.options.options, + groupOptions: this.createGroupOptions(), message: this.helpMessage })); this.delegateEvents(); @@ -407,7 +408,17 @@ }, optionForValue: function(value) { - return _.find(this.options.options, function(option) { return option[0] === value; }); + var options = []; + if (_.isUndefined(this.options.groupOptions)) { + return _.find(this.options.options, function(option) { return option[0] === value; }); + } else { + _.each(this.options.groupOptions, function(groupOption) { + options = options.concat(groupOption.selectOptions); + }); + return _.find(options, function(option) { + return option[0] === value; + }); + } }, fieldValue: function() { @@ -483,6 +494,14 @@ if (this.editable !== 'never') { this.$('.u-field-value select').prop('disabled', disable); } + }, + + createGroupOptions: function() { + return !(_.isUndefined(this.options.groupOptions)) ? this.options.groupOptions : + [{ + groupTitle: null, + selectOptions: this.options.options + }]; } }); diff --git a/lms/templates/fields/field_dropdown.underscore b/lms/templates/fields/field_dropdown.underscore index 8f0c61f5b2..097365c9dc 100644 --- a/lms/templates/fields/field_dropdown.underscore +++ b/lms/templates/fields/field_dropdown.underscore @@ -23,8 +23,13 @@ <% if (showBlankOption) { %> <% } %> - <% _.each(selectOptions, function(selectOption) { %> - + <% _.each(groupOptions, function(groupOption) { %> + <% if (groupOption.groupTitle != null) { %> + + <% } %> + <% _.each(groupOption.selectOptions, function(selectOption) { %> + + <% }); %> <% }); %>