Merge pull request #13140 from edx/kkim/country_tz_pref
Time Zone Field Changes Based on Country
This commit is contained in:
@@ -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'),
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user