Commit 3af769b4 by Kevin Kim

Update time zone dropdown in account settings based on user country

parent 95b16dba
......@@ -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
......
......@@ -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';
......
......@@ -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,
......
......@@ -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'),
......
......@@ -77,6 +77,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',
fieldTemplate: field_link_account_template,
......
......@@ -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
}];
}
});
......
......@@ -23,8 +23,13 @@
<% if (showBlankOption) { %>
<option value=""></option>
<% } %>
<% _.each(selectOptions, function(selectOption) { %>
<option value="<%- selectOption[0] %>"><%- selectOption[1] %></option>
<% _.each(groupOptions, function(groupOption) { %>
<% if (groupOption.groupTitle != null) { %>
<optgroup label="<%- groupOption.groupTitle %>">
<% } %>
<% _.each(groupOption.selectOptions, function(selectOption) { %>
<option value="<%- selectOption[0] %>"><%- selectOption[1] %></option>
<% }); %>
<% }); %>
</select>
<button class="u-field-value-display">
......
......@@ -23,8 +23,13 @@
<% if (showBlankOption) { %>
<option value=""></option>
<% } %>
<% _.each(selectOptions, function(selectOption) { %>
<option value="<%- selectOption[0] %>"><%- selectOption[1] %></option>
<% _.each(groupOptions, function(groupOption) { %>
<% if (groupOption.groupTitle != null) { %>
<optgroup label="<%- groupOption.groupTitle %>">
<% } %>
<% _.each(groupOption.selectOptions, function(selectOption) { %>
<option value="<%- selectOption[0] %>"><%- selectOption[1] %></option>
<% }); %>
<% }); %>
</select>
<span class="icon-caret-down" aria-hidden="true"></span>
......
......@@ -12,6 +12,7 @@ from django.db import IntegrityError
from django.utils.translation import ugettext as _
from django.utils.translation import ugettext_noop
from openedx.core.lib.time_zone_utils import get_display_time_zone
from pytz import common_timezones, common_timezones_set, country_timezones
from student.models import User, UserProfile
from request_cache import get_request_or_stub
......@@ -422,8 +423,8 @@ def _create_preference_update_error(preference_key, preference_value, error):
def get_country_time_zones(country_code=None):
"""
Returns a list of time zones commonly used in given country
or list of all time zones, if country code is None.
Returns a sorted list of time zones commonly used in given
country or list of all time zones, if country code is None.
Arguments:
country_code (str): ISO 3166-1 Alpha-2 country code
......@@ -432,7 +433,34 @@ def get_country_time_zones(country_code=None):
CountryCodeError: the given country code is invalid
"""
if country_code is None:
return common_timezones
return _get_sorted_time_zone_list(common_timezones)
if country_code.upper() in set(countries.alt_codes):
return country_timezones(country_code)
return _get_sorted_time_zone_list(country_timezones(country_code))
raise CountryCodeError
def _get_sorted_time_zone_list(time_zone_list):
"""
Returns a list of time zone dictionaries sorted by their display values
:param time_zone_list (list): pytz time zone list
"""
return sorted(
[_get_time_zone_dictionary(time_zone) for time_zone in time_zone_list],
key=lambda tz_dict: tz_dict['description']
)
def _get_time_zone_dictionary(time_zone_name):
"""
Returns a dictionary of time zone information:
* time_zone: Name of pytz time zone
* description: Display version of time zone [e.g. US/Pacific (PST, UTC-0800)]
:param time_zone_name (str): Name of pytz time zone
"""
return {
'time_zone': time_zone_name,
'description': get_display_time_zone(time_zone_name),
}
......@@ -15,6 +15,7 @@ from django.test import TestCase
from django.test.utils import override_settings
from dateutil.parser import parse as parse_datetime
from openedx.core.lib.time_zone_utils import get_display_time_zone
from student.tests.factories import UserFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
......@@ -445,13 +446,20 @@ class CountryTimeZoneTest(TestCase):
Test cases to validate country code api functionality
"""
@ddt.data(('NZ', ['Pacific/Auckland', 'Pacific/Chatham']),
(None, common_timezones))
@ddt.data(('ES', ['Africa/Ceuta', 'Atlantic/Canary', 'Europe/Madrid']),
(None, common_timezones[:10]))
@ddt.unpack
def test_get_country_time_zones(self, country_code, expected_time_zones):
"""Verify that list of common country time zones are returned"""
country_time_zones = get_country_time_zones(country_code)
self.assertEqual(country_time_zones, expected_time_zones)
"""Verify that list of common country time zones dictionaries is returned"""
expected_dict = [
{
'time_zone': time_zone,
'description': get_display_time_zone(time_zone)
}
for time_zone in expected_time_zones
]
country_time_zones_dicts = get_country_time_zones(country_code)[:10]
self.assertEqual(country_time_zones_dicts, expected_dict)
def test_country_code_errors(self):
"""Verify that country code error is raised for invalid country code"""
......
......@@ -4,7 +4,6 @@ Django REST Framework serializers for the User API application
from django.contrib.auth.models import User
from rest_framework import serializers
from openedx.core.lib.time_zone_utils import get_display_time_zone
from student.models import UserProfile
from .models import UserPreference
......@@ -89,17 +88,5 @@ class CountryTimeZoneSerializer(serializers.Serializer): # pylint: disable=abst
"""
Serializer that generates a list of common time zones for a country
"""
time_zone = serializers.SerializerMethodField()
description = serializers.SerializerMethodField()
def get_time_zone(self, time_zone_name):
"""
Returns inputted time zone name
"""
return time_zone_name
def get_description(self, time_zone_name):
"""
Returns the display version of time zone [e.g. US/Pacific (PST, UTC-0800)]
"""
return get_display_time_zone(time_zone_name)
time_zone = serializers.CharField()
description = serializers.CharField()
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment