Commit 3af769b4 by Kevin Kim

Update time zone dropdown in account settings based on user country

parent 95b16dba
...@@ -108,6 +108,11 @@ define(['backbone', ...@@ -108,6 +108,11 @@ define(['backbone',
request = requests[1]; request = requests[1];
expect(request.method).toBe('GET'); 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); expect(request.url).toBe(Helpers.USER_PREFERENCES_API_URL);
AjaxHelpers.respondWithError(requests, 500); AjaxHelpers.respondWithError(requests, 500);
...@@ -126,6 +131,7 @@ define(['backbone', ...@@ -126,6 +131,7 @@ define(['backbone',
Helpers.expectSettingsSectionsButNotFieldsToBeRendered(accountSettingsView); Helpers.expectSettingsSectionsButNotFieldsToBeRendered(accountSettingsView);
AjaxHelpers.respondWithJson(requests, Helpers.createAccountSettingsData()); AjaxHelpers.respondWithJson(requests, Helpers.createAccountSettingsData());
AjaxHelpers.respondWithJson(requests, Helpers.TIME_ZONE_RESPONSE);
AjaxHelpers.respondWithJson(requests, Helpers.createUserPreferencesData()); AjaxHelpers.respondWithJson(requests, Helpers.createUserPreferencesData());
Helpers.expectLoadingIndicatorIsVisible(accountSettingsView, false); Helpers.expectLoadingIndicatorIsVisible(accountSettingsView, false);
...@@ -141,6 +147,7 @@ define(['backbone', ...@@ -141,6 +147,7 @@ define(['backbone',
var accountSettingsView = createAccountSettingsPage(); var accountSettingsView = createAccountSettingsPage();
AjaxHelpers.respondWithJson(requests, Helpers.createAccountSettingsData()); AjaxHelpers.respondWithJson(requests, Helpers.createAccountSettingsData());
AjaxHelpers.respondWithJson(requests, Helpers.TIME_ZONE_RESPONSE);
AjaxHelpers.respondWithJson(requests, Helpers.createUserPreferencesData()); AjaxHelpers.respondWithJson(requests, Helpers.createUserPreferencesData());
AjaxHelpers.respondWithJson(requests, {}); // Page viewed analytics event AjaxHelpers.respondWithJson(requests, {}); // Page viewed analytics event
......
...@@ -45,7 +45,74 @@ define(['backbone', ...@@ -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); requests = AjaxHelpers.requests(this);
var selector = '.u-field-value > select'; var selector = '.u-field-value > select';
......
...@@ -49,6 +49,11 @@ define(['underscore'], function(_) { ...@@ -49,6 +49,11 @@ define(['underscore'], function(_) {
['3', 'Option 3'] ['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_MAX_BYTES = 1024 * 1024;
var IMAGE_MIN_BYTES = 100; var IMAGE_MIN_BYTES = 100;
...@@ -123,6 +128,7 @@ define(['underscore'], function(_) { ...@@ -123,6 +128,7 @@ define(['underscore'], function(_) {
createAccountSettingsData: createAccountSettingsData, createAccountSettingsData: createAccountSettingsData,
createUserPreferencesData: createUserPreferencesData, createUserPreferencesData: createUserPreferencesData,
FIELD_OPTIONS: FIELD_OPTIONS, FIELD_OPTIONS: FIELD_OPTIONS,
TIME_ZONE_RESPONSE: TIME_ZONE_RESPONSE,
expectLoadingIndicatorIsVisible: expectLoadingIndicatorIsVisible, expectLoadingIndicatorIsVisible: expectLoadingIndicatorIsVisible,
expectLoadingErrorIsVisible: expectLoadingErrorIsVisible, expectLoadingErrorIsVisible: expectLoadingErrorIsVisible,
expectElementContainsField: expectElementContainsField, expectElementContainsField: expectElementContainsField,
......
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
) { ) {
var accountSettingsElement, userAccountModel, userPreferencesModel, aboutSectionsData, var accountSettingsElement, userAccountModel, userPreferencesModel, aboutSectionsData,
accountsSectionData, ordersSectionData, accountSettingsView, showAccountSettingsPage, accountsSectionData, ordersSectionData, accountSettingsView, showAccountSettingsPage,
showLoadingError, orderNumber; showLoadingError, orderNumber, getUserField, userFields, timeZoneDropdownField, countryDropdownField;
accountSettingsElement = $('.wrapper-account-settings'); accountSettingsElement = $('.wrapper-account-settings');
...@@ -110,7 +110,7 @@ ...@@ -110,7 +110,7 @@
}) })
}, },
{ {
view: new AccountSettingsFieldViews.DropdownFieldView({ view: new AccountSettingsFieldViews.TimeZoneFieldView({
model: userPreferencesModel, model: userPreferencesModel,
required: true, required: true,
title: gettext('Time Zone'), title: gettext('Time Zone'),
...@@ -120,7 +120,10 @@ ...@@ -120,7 +120,10 @@
'time zone here, course dates, including assignment deadlines, are displayed in ' + 'time zone here, course dates, including assignment deadlines, are displayed in ' +
'Coordinated Universal Time (UTC).' 'Coordinated Universal Time (UTC).'
), ),
options: fieldsData.time_zone.options, groupOptions: [{
groupTitle: gettext('All Time Zones'),
selectOptions: fieldsData.time_zone.options
}],
persistChanges: true persistChanges: true
}) })
} }
...@@ -169,6 +172,19 @@ ...@@ -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 = [ accountsSectionData = [
{ {
title: gettext('Linked Accounts'), title: gettext('Linked Accounts'),
......
...@@ -77,6 +77,67 @@ ...@@ -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({ PasswordFieldView: FieldViews.LinkFieldView.extend({
fieldType: 'button', fieldType: 'button',
fieldTemplate: field_link_account_template, fieldTemplate: field_link_account_template,
......
...@@ -369,7 +369,8 @@ ...@@ -369,7 +369,8 @@
}, },
initialize: function(options) { initialize: function(options) {
_.bindAll(this, 'render', 'optionForValue', 'fieldValue', 'displayValue', 'updateValueInField', 'saveValue'); _.bindAll(this, 'render', 'optionForValue', 'fieldValue', 'displayValue', 'updateValueInField',
'saveValue', 'createGroupOptions');
this._super(options); this._super(options);
this.listenTo(this.model, 'change:' + this.options.valueAttribute, this.updateValueInField); this.listenTo(this.model, 'change:' + this.options.valueAttribute, this.updateValueInField);
...@@ -385,7 +386,7 @@ ...@@ -385,7 +386,7 @@
titleVisible: this.options.titleVisible !== undefined ? this.options.titleVisible : true, titleVisible: this.options.titleVisible !== undefined ? this.options.titleVisible : true,
iconName: this.options.iconName, iconName: this.options.iconName,
showBlankOption: (!this.options.required || !this.modelValueIsSet()), showBlankOption: (!this.options.required || !this.modelValueIsSet()),
selectOptions: this.options.options, groupOptions: this.createGroupOptions(),
message: this.helpMessage message: this.helpMessage
})); }));
this.delegateEvents(); this.delegateEvents();
...@@ -407,7 +408,17 @@ ...@@ -407,7 +408,17 @@
}, },
optionForValue: function(value) { 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() { fieldValue: function() {
...@@ -483,6 +494,14 @@ ...@@ -483,6 +494,14 @@
if (this.editable !== 'never') { if (this.editable !== 'never') {
this.$('.u-field-value select').prop('disabled', disable); 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 @@ ...@@ -23,8 +23,13 @@
<% if (showBlankOption) { %> <% if (showBlankOption) { %>
<option value=""></option> <option value=""></option>
<% } %> <% } %>
<% _.each(selectOptions, function(selectOption) { %> <% _.each(groupOptions, function(groupOption) { %>
<option value="<%- selectOption[0] %>"><%- selectOption[1] %></option> <% if (groupOption.groupTitle != null) { %>
<optgroup label="<%- groupOption.groupTitle %>">
<% } %>
<% _.each(groupOption.selectOptions, function(selectOption) { %>
<option value="<%- selectOption[0] %>"><%- selectOption[1] %></option>
<% }); %>
<% }); %> <% }); %>
</select> </select>
<button class="u-field-value-display"> <button class="u-field-value-display">
......
...@@ -23,8 +23,13 @@ ...@@ -23,8 +23,13 @@
<% if (showBlankOption) { %> <% if (showBlankOption) { %>
<option value=""></option> <option value=""></option>
<% } %> <% } %>
<% _.each(selectOptions, function(selectOption) { %> <% _.each(groupOptions, function(groupOption) { %>
<option value="<%- selectOption[0] %>"><%- selectOption[1] %></option> <% if (groupOption.groupTitle != null) { %>
<optgroup label="<%- groupOption.groupTitle %>">
<% } %>
<% _.each(groupOption.selectOptions, function(selectOption) { %>
<option value="<%- selectOption[0] %>"><%- selectOption[1] %></option>
<% }); %>
<% }); %> <% }); %>
</select> </select>
<span class="icon-caret-down" aria-hidden="true"></span> <span class="icon-caret-down" aria-hidden="true"></span>
......
...@@ -12,6 +12,7 @@ from django.db import IntegrityError ...@@ -12,6 +12,7 @@ from django.db import IntegrityError
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.utils.translation import ugettext_noop 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 pytz import common_timezones, common_timezones_set, country_timezones
from student.models import User, UserProfile from student.models import User, UserProfile
from request_cache import get_request_or_stub from request_cache import get_request_or_stub
...@@ -422,8 +423,8 @@ def _create_preference_update_error(preference_key, preference_value, error): ...@@ -422,8 +423,8 @@ def _create_preference_update_error(preference_key, preference_value, error):
def get_country_time_zones(country_code=None): def get_country_time_zones(country_code=None):
""" """
Returns a list of time zones commonly used in given country Returns a sorted list of time zones commonly used in given
or list of all time zones, if country code is None. country or list of all time zones, if country code is None.
Arguments: Arguments:
country_code (str): ISO 3166-1 Alpha-2 country code country_code (str): ISO 3166-1 Alpha-2 country code
...@@ -432,7 +433,34 @@ def get_country_time_zones(country_code=None): ...@@ -432,7 +433,34 @@ def get_country_time_zones(country_code=None):
CountryCodeError: the given country code is invalid CountryCodeError: the given country code is invalid
""" """
if country_code is None: 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): 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 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 ...@@ -15,6 +15,7 @@ from django.test import TestCase
from django.test.utils import override_settings from django.test.utils import override_settings
from dateutil.parser import parse as parse_datetime 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 student.tests.factories import UserFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
...@@ -445,13 +446,20 @@ class CountryTimeZoneTest(TestCase): ...@@ -445,13 +446,20 @@ class CountryTimeZoneTest(TestCase):
Test cases to validate country code api functionality Test cases to validate country code api functionality
""" """
@ddt.data(('NZ', ['Pacific/Auckland', 'Pacific/Chatham']), @ddt.data(('ES', ['Africa/Ceuta', 'Atlantic/Canary', 'Europe/Madrid']),
(None, common_timezones)) (None, common_timezones[:10]))
@ddt.unpack @ddt.unpack
def test_get_country_time_zones(self, country_code, expected_time_zones): def test_get_country_time_zones(self, country_code, expected_time_zones):
"""Verify that list of common country time zones are returned""" """Verify that list of common country time zones dictionaries is returned"""
country_time_zones = get_country_time_zones(country_code) expected_dict = [
self.assertEqual(country_time_zones, expected_time_zones) {
'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): def test_country_code_errors(self):
"""Verify that country code error is raised for invalid country code""" """Verify that country code error is raised for invalid country code"""
......
...@@ -4,7 +4,6 @@ Django REST Framework serializers for the User API application ...@@ -4,7 +4,6 @@ Django REST Framework serializers for the User API application
from django.contrib.auth.models import User from django.contrib.auth.models import User
from rest_framework import serializers from rest_framework import serializers
from openedx.core.lib.time_zone_utils import get_display_time_zone
from student.models import UserProfile from student.models import UserProfile
from .models import UserPreference from .models import UserPreference
...@@ -89,17 +88,5 @@ class CountryTimeZoneSerializer(serializers.Serializer): # pylint: disable=abst ...@@ -89,17 +88,5 @@ class CountryTimeZoneSerializer(serializers.Serializer): # pylint: disable=abst
""" """
Serializer that generates a list of common time zones for a country Serializer that generates a list of common time zones for a country
""" """
time_zone = serializers.SerializerMethodField() time_zone = serializers.CharField()
description = serializers.SerializerMethodField() description = serializers.CharField()
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)
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