Commit 059985b9 by Kevin Kim

Added time zone field to user account settings, currently hidden behind a feature flag.

parent 1d09f787
......@@ -272,4 +272,4 @@ Brian Jacobel <bjacobel@edx.org>
Sigberto Alarcon <salarcon@stanford.edu>
Sofiya Semenova <ssemenova@edx.org>
Alisan Tang <atang@edx.org>
Kevin Kim <kkim@edx.org>
......@@ -5,8 +5,9 @@ Convenience methods for working with datetime objects
from datetime import datetime, timedelta
import re
from pytz import timezone, UTC, UnknownTimeZoneError
from django.utils.timezone import now
from django.utils.translation import pgettext, ugettext
from pytz import timezone, utc, UnknownTimeZoneError
def get_default_time_display(dtime):
......@@ -52,7 +53,7 @@ def get_time_display(dtime, format_string=None, coerce_tz=None):
try:
to_tz = timezone(coerce_tz)
except UnknownTimeZoneError:
to_tz = UTC
to_tz = utc
dtime = to_tz.normalize(dtime.astimezone(to_tz))
if dtime is None or format_string is None:
return get_default_time_display(dtime)
......@@ -62,6 +63,15 @@ def get_time_display(dtime, format_string=None, coerce_tz=None):
return get_default_time_display(dtime)
def get_formatted_time_zone(time_zone):
"""
Returns a formatted time zone (e.g. 'Asia/Tokyo (JST +0900)') for user account settings time zone drop down
"""
abbr = get_time_display(now(), '%Z', time_zone)
offset = get_time_display(now(), '%z', time_zone)
return "{name} ({abbr}, UTC{offset})".format(name=time_zone, abbr=abbr, offset=offset).replace("_", " ")
def almost_same_datetime(dt1, dt2, allowed_delta=timedelta(minutes=1)):
"""
Returns true if these are w/in a minute of each other. (in case secs saved to db
......@@ -78,7 +88,7 @@ def to_timestamp(datetime_value):
Convert a datetime into a timestamp, represented as the number
of seconds since January 1, 1970 UTC.
"""
return int((datetime_value - datetime(1970, 1, 1, tzinfo=UTC)).total_seconds())
return int((datetime_value - datetime(1970, 1, 1, tzinfo=utc)).total_seconds())
def from_timestamp(timestamp):
......@@ -89,7 +99,7 @@ def from_timestamp(timestamp):
If the timestamp cannot be converted, returns None instead.
"""
try:
return datetime.utcfromtimestamp(int(timestamp)).replace(tzinfo=UTC)
return datetime.utcfromtimestamp(int(timestamp)).replace(tzinfo=utc)
except (ValueError, TypeError):
return None
......
......@@ -165,7 +165,8 @@ class AccountSettingsPageTest(AccountSettingsTestMixin, WebAppTest):
'Email Address',
'Password',
'Language',
'Country or Region'
'Country or Region',
'Time Zone',
]
},
{
......
# -*- coding: utf-8 -*-
""" Tests for student account views. """
from copy import copy
import re
from nose.plugins.attrib import attr
from unittest import skipUnless
from urllib import urlencode
......@@ -18,22 +18,23 @@ from django.test import TestCase
from django.test.utils import override_settings
from django.http import HttpRequest
from edx_rest_api_client import exceptions
from nose.plugins.attrib import attr
from course_modes.models import CourseMode
from commerce.models import CommerceConfiguration
from commerce.tests import TEST_API_URL, TEST_API_SIGNING_KEY, factories
from commerce.tests.mocks import mock_get_orders
from course_modes.models import CourseMode
from openedx.core.djangoapps.programs.tests.mixins import ProgramsApiConfigMixin
from openedx.core.djangoapps.user_api.accounts.api import activate_account, create_account
from openedx.core.djangoapps.user_api.accounts import EMAIL_MAX_LENGTH
from openedx.core.djangolib.js_utils import dump_js_escaped_json
from openedx.core.djangolib.testing.utils import CacheIsolationTestCase
from openedx.core.djangoapps.theming.tests.test_util import with_edx_domain_context
from student.tests.factories import UserFactory
from student_account.views import account_settings_context, get_user_orders
from third_party_auth.tests.testutil import simulate_running_pipeline, ThirdPartyAuthTestMixin
from util.testing import UrlResetMixin
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from openedx.core.djangoapps.theming.tests.test_util import with_edx_domain_context
@ddt.ddt
......@@ -463,6 +464,10 @@ class AccountSettingsViewTest(ThirdPartyAuthTestMixin, TestCase, ProgramsApiConf
'preferred_language',
]
HIDDEN_FIELDS = [
'time_zone',
]
@mock.patch("django.conf.settings.MESSAGE_STORAGE", 'django.contrib.messages.storage.cookie.CookieStorage')
def setUp(self):
super(AccountSettingsViewTest, self).setUp()
......@@ -507,13 +512,35 @@ class AccountSettingsViewTest(ThirdPartyAuthTestMixin, TestCase, ProgramsApiConf
self.assertEqual(context['auth']['providers'][0]['name'], 'Facebook')
self.assertEqual(context['auth']['providers'][1]['name'], 'Google')
def test_view(self):
view_path = reverse('account_settings')
response = self.client.get(path=view_path)
for attribute in self.FIELDS:
self.assertIn(attribute, response.content)
def test_hidden_fields_not_visible(self):
"""
Test that hidden fields are not visible when disabled.
"""
temp_features = copy(settings.FEATURES)
temp_features['ENABLE_TIME_ZONE_PREFERENCE'] = False
with self.settings(FEATURES=temp_features):
view_path = reverse('account_settings')
response = self.client.get(path=view_path)
for attribute in self.FIELDS:
self.assertIn(attribute, response.content)
for attribute in self.HIDDEN_FIELDS:
self.assertIn('"%s": {"enabled": false' % (attribute), response.content)
def test_hidden_fields_are_visible(self):
"""
Test that hidden fields are visible when enabled.
"""
temp_features = copy(settings.FEATURES)
temp_features['ENABLE_TIME_ZONE_PREFERENCE'] = True
with self.settings(FEATURES=temp_features):
view_path = reverse('account_settings')
response = self.client.get(path=view_path)
for attribute in self.FIELDS:
self.assertIn(attribute, response.content)
for attribute in self.HIDDEN_FIELDS:
self.assertIn('"%s": {"enabled": true' % (attribute), response.content)
def test_header_with_programs_listing_enabled(self):
"""
......
......@@ -31,6 +31,7 @@ from openedx.core.djangoapps.programs.models import ProgramsApiConfig
from openedx.core.djangoapps.theming.helpers import is_request_in_themed_site, get_value as get_themed_value
from openedx.core.djangoapps.user_api.accounts.api import request_password_change
from openedx.core.djangoapps.user_api.errors import UserNotFound
from openedx.core.djangoapps.user_api.models import UserPreference
from openedx.core.lib.edx_api_utils import get_edx_api_data
from student.models import UserProfile
from student.views import (
......@@ -42,12 +43,8 @@ import third_party_auth
from third_party_auth import pipeline
from third_party_auth.decorators import xframe_allow_whitelisted
from util.bad_request_rate_limiter import BadRequestRateLimiter
from openedx.core.djangoapps.theming.helpers import is_request_in_themed_site, get_value as get_themed_value
from openedx.core.djangoapps.user_api.accounts.api import request_password_change
from openedx.core.djangoapps.user_api.errors import UserNotFound
from util.date_utils import strftime_localized
AUDIT_LOG = logging.getLogger("audit")
log = logging.getLogger(__name__)
......@@ -451,6 +448,9 @@ def account_settings_context(request):
'options': year_of_birth_options,
}, 'preferred_language': {
'options': all_languages(),
}, 'time_zone': {
'options': UserPreference.TIME_ZONE_CHOICES,
'enabled': settings.FEATURES.get('ENABLE_TIME_ZONE_PREFERENCE'),
}
},
'platform_name': get_themed_value('PLATFORM_NAME', settings.PLATFORM_NAME),
......
......@@ -164,6 +164,9 @@ FEATURES['ENABLE_DASHBOARD_SEARCH'] = True
# Enable support for OpenBadges accomplishments
FEATURES['ENABLE_OPENBADGES'] = True
# Enable time zone field in account settings. Will be removed in Ticket #TNL-4750.
FEATURES['ENABLE_TIME_ZONE_PREFERENCE'] = True
# Use MockSearchEngine as the search engine for test scenario
SEARCH_ENGINE = "search.tests.mock_search_engine.MockSearchEngine"
# Path at which to store the mock index
......
......@@ -363,6 +363,9 @@ FEATURES = {
# lives in the Extended table, saving the frontend from
# making multiple queries.
'ENABLE_READING_FROM_MULTIPLE_HISTORY_TABLES': True,
# WIP -- will be removed in Ticket #TNL-4750.
'ENABLE_TIME_ZONE_PREFERENCE': False,
}
# Ignore static asset files on import which match this pattern
......
......@@ -30,6 +30,9 @@ define(['backbone',
'options': Helpers.FIELD_OPTIONS
}, 'preferred_language': {
'options': Helpers.FIELD_OPTIONS
}, 'time_zone': {
'options': Helpers.FIELD_OPTIONS,
'enabled': false
}
};
......@@ -148,7 +151,7 @@ define(['backbone',
var sectionsData = accountSettingsView.options.tabSections.aboutTabSections;
expect(sectionsData[0].fields.length).toBe(6);
expect(sectionsData[0].fields.length).toBe(7);
var textFields = [sectionsData[0].fields[1], sectionsData[0].fields[2]];
for (i = 0; i < textFields.length ; i++) {
......
......@@ -34,7 +34,8 @@ define(['underscore'], function(_) {
};
var DEFAULT_USER_PREFERENCES_DATA = {
'pref-lang': '2'
'pref-lang': '2',
'time_zone': null
};
var createUserPreferencesData = function(options) {
......@@ -100,7 +101,14 @@ define(['underscore'], function(_) {
if (fieldsAreRendered === false) {
expect(sectionFieldElements.length).toBe(0);
} else {
expect(sectionFieldElements.length).toBe(sectionsData[sectionIndex].fields.length);
var visible_count = 0;
_.each(sectionsData[sectionIndex].fields, function(field) {
if (field.view.enabled) {
visible_count++;
}
});
expect(sectionFieldElements.length).toBe(visible_count);
_.each(sectionFieldElements, function (sectionFieldElement, fieldIndex) {
expectElementContainsField(sectionFieldElement, sectionsData[sectionIndex].fields[fieldIndex]);
......
define(['backbone',
'jquery',
'underscore',
'edx-ui-toolkit/js/utils/html-utils',
'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers',
'common/js/spec_helpers/template_helpers',
'js/views/fields',
'string_utils'],
function (Backbone, $, _, AjaxHelpers, TemplateHelpers, FieldViews) {
function (Backbone, $, _, HtmlUtils, AjaxHelpers, TemplateHelpers, FieldViews) {
'use strict';
var API_URL = '/api/end_point/v1';
......
......@@ -109,6 +109,22 @@
options: fieldsData.country.options,
persistChanges: true
})
},
{
view: new AccountSettingsFieldViews.DropdownFieldView({
model: userPreferencesModel,
required: true,
title: gettext('Time Zone'),
valueAttribute: 'time_zone',
enabled: fieldsData.time_zone.enabled,
helpMessage: gettext(
'Select the time zone for displaying course dates. If you do not specify a ' +
'time zone here, course dates, including assignment deadlines, are displayed in ' +
'Coordinated Universal Time (UTC).'
),
options: fieldsData.time_zone.options,
persistChanges: true
})
}
]
},
......
......@@ -41,10 +41,13 @@
EmailFieldView: FieldViews.TextFieldView.extend({
fieldTemplate: field_text_account_template,
successMessage: function () {
return this.indicators.success + StringUtils.interpolate(
return HtmlUtils.joinHtml(
this.indicators.success,
StringUtils.interpolate(
gettext('We\'ve sent a confirmation message to {new_email_address}. Click the link in the message to update your email address.'), /* jshint ignore:line */
{'new_email_address': this.fieldValue()}
);
)
);
}
}),
LanguagePreferenceFieldView: FieldViews.DropdownFieldView.extend({
......@@ -65,8 +68,10 @@
},
error: function () {
view.showNotificationMessage(
view.indicators.error +
gettext('You must sign out and sign back in before your language changes take effect.')
HtmlUtils.joinHtml(
view.indicators.error,
gettext('You must sign out and sign back in before your language changes take effect.') // jshint ignore:line
)
);
}
});
......@@ -106,10 +111,13 @@
});
},
successMessage: function () {
return this.indicators.success + StringUtils.interpolate(
return HtmlUtils.joinHtml(
this.indicators.success,
StringUtils.interpolate(
gettext('We\'ve sent a message to {email_address}. Click the link in the message to reset your password.'), /* jshint ignore:line */
{'email_address': this.model.get(this.options.emailAttribute)}
);
)
);
}
}),
LanguageProficienciesFieldView: FieldViews.DropdownFieldView.extend({
......@@ -169,7 +177,7 @@
);
}
this.$el.html(this.template({
HtmlUtils.setHtml(this.$el, HtmlUtils.template(this.fieldTemplate)({
id: this.options.valueAttribute,
title: this.options.title,
screenReaderTitle: screenReaderTitle,
......@@ -220,12 +228,12 @@
});
},
inProgressMessage: function () {
return this.indicators.inProgress + (
return HtmlUtils.joinHtml(this.indicators.inProgress, (
this.options.connected ? gettext('Unlinking') : gettext('Linking')
);
));
},
successMessage: function () {
return this.indicators.success + gettext('Successfully unlinked.');
return HtmlUtils.joinHtml(this.indicators.success, gettext('Successfully unlinked.'));
}
}),
......
......@@ -5,9 +5,10 @@
'jquery',
'underscore',
'backbone',
'edx-ui-toolkit/js/utils/html-utils',
'js/student_account/views/account_section_view',
'text!templates/student_account/account_settings.underscore'
], function (gettext, $, _, Backbone, AccountSectionView, accountSettingsTemplate) {
], function (gettext, $, _, Backbone, HtmlUtils, AccountSectionView, accountSettingsTemplate) {
var AccountSettingsView = Backbone.View.extend({
......@@ -28,7 +29,7 @@
},
render: function () {
this.$el.html(_.template(accountSettingsTemplate)({
HtmlUtils.setHtml(this.$el, HtmlUtils.template(accountSettingsTemplate)({
accountSettingsTabs: this.accountSettingsTabs
}));
this.renderSection(this.options.tabSections[this.activeTab]);
......@@ -67,7 +68,9 @@
_.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);
if (field.view.enabled) {
$(sectionEl).append(field.view.render().el);
}
});
});
return this;
......
;(function (define, undefined) {
'use strict';
define([
'gettext', 'jquery', 'underscore', 'backbone', 'js/views/fields', 'js/views/image_field', 'backbone-super'
], function (gettext, $, _, Backbone, FieldViews, ImageFieldView) {
'gettext', 'jquery', 'underscore', 'backbone', 'edx-ui-toolkit/js/utils/string-utils',
'edx-ui-toolkit/js/utils/html-utils', 'js/views/fields', 'js/views/image_field', 'backbone-super'
], function (gettext, $, _, Backbone, StringUtils, HtmlUtils, FieldViews, ImageFieldView) {
var LearnerProfileFieldViews = {};
......@@ -16,22 +17,31 @@
},
showNotificationMessage: function () {
var accountSettingsLink = '<a href="' + this.options.accountSettingsPageUrl + '">' + gettext('Account Settings page.') + '</a>';
var accountSettingsLink = HtmlUtils.joinHtml(
HtmlUtils.interpolateHtml(
HtmlUtils.HTML('<a href="{settings_url}">'), {settings_url: this.options.accountSettingsPageUrl}
),
gettext('Account Settings page.'),
HtmlUtils.HTML('</a>')
);
if (this.profileIsPrivate) {
this._super(interpolate_text(
gettext("You must specify your birth year before you can share your full profile. To specify your birth year, go to the {account_settings_page_link}"),
{'account_settings_page_link': accountSettingsLink}
));
this._super(
HtmlUtils.interpolateHtml(
gettext("You must specify your birth year before you can share your full profile. To specify your birth year, go to the {account_settings_page_link}"), // jshint ignore:line
{'account_settings_page_link':accountSettingsLink}
)
);
} else if (this.requiresParentalConsent) {
this._super(interpolate_text(
gettext('You must be over 13 to share a full profile. If you are over 13, make sure that you have specified a birth year on the {account_settings_page_link}'),
{'account_settings_page_link': accountSettingsLink}
));
this._super(
HtmlUtils.interpolateHtml(
gettext('You must be over 13 to share a full profile. If you are over 13, make sure that you have specified a birth year on the {account_settings_page_link}'), // jshint ignore:line
{'account_settings_page_link': accountSettingsLink}
)
);
}
else {
this._super('');
}
return this._super();
}
},
updateFieldValue: function() {
......
;(function (define, undefined) {
'use strict';
define([
'gettext', 'jquery', 'underscore', 'backbone',
'gettext', 'jquery', 'underscore', 'backbone', 'edx-ui-toolkit/js/utils/html-utils',
'common/js/components/views/tabbed_view',
'js/student_profile/views/section_two_tab',
'text!templates/student_profile/learner_profile.underscore'],
function (gettext, $, _, Backbone, TabbedView, SectionTwoTab, learnerProfileTemplate) {
function (gettext, $, _, Backbone, HtmlUtils, TabbedView, SectionTwoTab, learnerProfileTemplate) {
var LearnerProfileView = Backbone.View.extend({
......@@ -52,7 +52,7 @@
{view: this.sectionTwoView, title: gettext("About Me"), url: "about_me"}
];
this.$el.html(this.template({
HtmlUtils.setHtml(this.$el, HtmlUtils.template(learnerProfileTemplate)({
username: self.options.accountSettingsModel.get('username'),
ownProfile: self.options.ownProfile,
showFullProfile: self.showFullProfile()
......
......@@ -2,13 +2,14 @@
'use strict';
define([
'gettext', 'jquery', 'underscore', 'backbone',
'edx-ui-toolkit/js/utils/html-utils',
'text!templates/fields/field_readonly.underscore',
'text!templates/fields/field_dropdown.underscore',
'text!templates/fields/field_link.underscore',
'text!templates/fields/field_text.underscore',
'text!templates/fields/field_textarea.underscore',
'backbone-super'
], function (gettext, $, _, Backbone,
], function (gettext, $, _, Backbone, HtmlUtils,
field_readonly_template,
field_dropdown_template,
field_link_template,
......@@ -30,12 +31,36 @@
tagName: 'div',
indicators: {
'canEdit': '<span class="icon fa fa-pencil message-can-edit" aria-hidden="true"></span><span class="sr">' + gettext("Editable") + '</span>', // jshint ignore:line
'error': '<span class="fa fa-exclamation-triangle message-error" aria-hidden="true"></span><span class="sr">' + gettext("Error") + '</span>', // jshint ignore:line
'validationError': '<span class="fa fa-exclamation-triangle message-validation-error" aria-hidden="true"></span><span class="sr">' + gettext("Validation Error") + '</span>', // jshint ignore:line
'inProgress': '<span class="fa fa-spinner fa-pulse message-in-progress" aria-hidden="true"></span><span class="sr">' + gettext("In Progress") + '</span>', // jshint ignore:line
'success': '<span class="fa fa-check message-success" aria-hidden="true"></span><span class="sr">' + gettext("Success") + '</span>', // jshint ignore:line
'plus': '<span class="fa fa-plus placeholder" aria-hidden="true"></span><span class="sr">' + gettext("Placeholder")+ '</span>' // jshint ignore:line
'canEdit': HtmlUtils.joinHtml(
HtmlUtils.HTML('<span class="icon fa fa-pencil message-can-edit" aria-hidden="true"></span><span class="sr">'), // jshint ignore:line
gettext("Editable"),
HtmlUtils.HTML('</span>')
),
'error': HtmlUtils.joinHtml(
HtmlUtils.HTML('<span class="fa fa-exclamation-triangle message-error" aria-hidden="true"></span><span class="sr">'), // jshint ignore:line
gettext("Error"),
HtmlUtils.HTML('</span>')
),
'validationError': HtmlUtils.joinHtml(
HtmlUtils.HTML('<span class="fa fa-exclamation-triangle message-validation-error" aria-hidden="true"></span><span class="sr">'), // jshint ignore:line
gettext("Validation Error"),
HtmlUtils.HTML('</span>')
),
'inProgress': HtmlUtils.joinHtml(
HtmlUtils.HTML('<span class="fa fa-spinner fa-pulse message-in-progress" aria-hidden="true"></span><span class="sr">'), // jshint ignore:line
gettext("In Progress"),
HtmlUtils.HTML('</span>')
),
'success': HtmlUtils.joinHtml(
HtmlUtils.HTML('<span class="fa fa-check message-success" aria-hidden="true"></span><span class="sr">'), // jshint ignore:line
gettext("Success"),
HtmlUtils.HTML('</span>')
),
'plus': HtmlUtils.joinHtml(
HtmlUtils.HTML('<span class="fa fa-plus placeholder" aria-hidden="true"></span><span class="sr">'),
gettext("Placeholder"),
HtmlUtils.HTML('</span>')
)
},
messages: {
......@@ -57,6 +82,7 @@
this.helpMessage = this.options.helpMessage || '';
this.showMessages = _.isUndefined(this.options.showMessages) ? true : this.options.showMessages;
this.enabled = _.isUndefined(this.options.enabled) ? true: this.options.enabled;
_.bindAll(this, 'modelValue', 'modelValueIsSet', 'showNotificationMessage','getNotificationMessage',
'getMessage', 'title', 'showHelpMessage', 'showInProgressMessage', 'showSuccessMessage',
......@@ -73,14 +99,14 @@
},
title: function (text) {
return this.$('.u-field-title').html(text);
return this.$('.u-field-title').text(text);
},
getMessage: function(message_status) {
if ((message_status + 'Message') in this) {
return this[message_status + 'Message'].call(this);
} else if (this.showMessages) {
return this.indicators[message_status] + this.messages[message_status];
return HtmlUtils.joinHtml(this.indicators[message_status], this.messages[message_status]);
}
return this.indicators[message_status];
},
......@@ -90,16 +116,16 @@
message = this.helpMessage;
}
this.$('.u-field-message-notification').html('');
this.$('.u-field-message-help').html(message);
HtmlUtils.setHtml(this.$('.u-field-message-help'), message);
},
getNotificationMessage: function() {
return this.$('.u-field-message-notification').html();
return HtmlUtils.HTML(this.$('.u-field-message-notification').html());
},
showNotificationMessage: function(message) {
this.$('.u-field-message-help').html('');
this.$('.u-field-message-notification').html(message);
HtmlUtils.setHtml(this.$('.u-field-message-notification'), message);
},
showCanEditMessage: function(show) {
......@@ -128,7 +154,8 @@
this.lastSuccessMessageContext = context;
setTimeout(function () {
if ((context === view.lastSuccessMessageContext) && (view.getNotificationMessage() === successMessage)) {
if ((context === view.lastSuccessMessageContext) &&
(view.getNotificationMessage().toString() === successMessage.toString())) {
if (view.editable === 'toggle') {
view.showCanEditMessage(true);
} else {
......@@ -142,10 +169,8 @@
if (xhr.status === 400) {
try {
var errors = JSON.parse(xhr.responseText),
validationErrorMessage = _.escape(
errors.field_errors[this.options.valueAttribute].user_message
),
message = this.indicators.validationError + validationErrorMessage;
validationErrorMessage = errors.field_errors[this.options.valueAttribute].user_message,
message = HtmlUtils.joinHtml(this.indicators.validationError, validationErrorMessage);
this.showNotificationMessage(message);
} catch (error) {
this.showNotificationMessage(this.getMessage('error'));
......@@ -271,7 +296,7 @@
},
render: function () {
this.$el.html(this.template({
HtmlUtils.setHtml(this.$el, HtmlUtils.template(this.fieldTemplate)({
id: this.options.valueAttribute,
title: this.options.title,
screenReaderTitle: this.options.screenReaderTitle || this.options.title,
......@@ -287,7 +312,7 @@
},
updateValueInField: function () {
this.$('.u-field-value ').html(_.escape(this.modelValue()));
this.$('.u-field-value ').text(this.modelValue());
}
});
......@@ -308,7 +333,7 @@
},
render: function () {
this.$el.html(this.template({
HtmlUtils.setHtml(this.$el, HtmlUtils.template(this.fieldTemplate)({
id: this.options.valueAttribute,
title: this.options.title,
value: this.modelValue(),
......@@ -354,7 +379,7 @@
},
render: function () {
this.$el.html(this.template({
HtmlUtils.setHtml(this.$el, HtmlUtils.template(this.fieldTemplate)({
id: this.options.valueAttribute,
mode: this.mode,
editable: this.editable,
......@@ -418,7 +443,7 @@
value = this.options.placeholderValue || '';
}
this.$('.u-field-value').attr('aria-label', this.options.title);
this.$('.u-field-value-readonly').html(_.escape(value));
this.$('.u-field-value-readonly').text(value);
if (this.mode === 'display') {
this.updateDisplayModeClass();
......@@ -492,7 +517,7 @@
if (this.mode === 'display') {
value = value || this.options.placeholderValue;
}
this.$el.html(this.template({
HtmlUtils.setHtml(this.$el, HtmlUtils.template(this.fieldTemplate)({
id: this.options.valueAttribute,
screenReaderTitle: this.options.screenReaderTitle || this.options.title,
mode: this.mode,
......@@ -587,7 +612,7 @@
},
render: function () {
this.$el.html(this.template({
HtmlUtils.setHtml(this.$el, HtmlUtils.template(this.fieldTemplate)({
id: this.options.valueAttribute,
title: this.options.title,
screenReaderTitle: this.options.screenReaderTitle || this.options.title,
......
......@@ -85,7 +85,6 @@ class UserReadOnlySerializer(serializers.Serializer):
user,
self.context.get('request')
),
"time_zone": None,
"language_proficiencies": LanguageProficiencySerializer(
profile.language_proficiencies.all(),
many=True
......
......@@ -8,6 +8,9 @@ from django.db.models.signals import post_delete, pre_save, post_save
from django.dispatch import receiver
from model_utils.models import TimeStampedModel
from pytz import common_timezones
from util.date_utils import get_formatted_time_zone
from util.model_utils import get_changed_fields_dict, emit_setting_changed_event
from xmodule_django.models import CourseKeyField
......@@ -27,6 +30,10 @@ class UserPreference(models.Model):
key = models.CharField(max_length=255, db_index=True, validators=[RegexValidator(KEY_REGEX)])
value = models.TextField()
TIME_ZONE_CHOICES = [
(tz, get_formatted_time_zone(tz)) for tz in common_timezones
]
class Meta(object):
unique_together = ("user", "key")
......
......@@ -21,6 +21,8 @@ from ..helpers import intercept_errors
from ..models import UserOrgTag, UserPreference
from ..serializers import UserSerializer, RawUserPreferenceSerializer
from pytz import common_timezones_set
log = logging.getLogger(__name__)
......@@ -392,6 +394,17 @@ def validate_user_preference_serializer(serializer, preference_key, preference_v
"user_message": user_message,
}
})
if preference_key == "time_zone" and preference_value not in common_timezones_set:
developer_message = ugettext_noop(u"Value '{preference_value}' not valid for preference '{preference_key}': Not in timezone set.") # pylint: disable=line-too-long
user_message = ugettext_noop(u"Value '{preference_value}' is not valid for user preference '{preference_key}'.")
raise PreferenceValidationError({
preference_key: {
"developer_message": developer_message.format(
preference_key=preference_key, preference_value=preference_value
),
"user_message": user_message.format(preference_key=preference_key, preference_value=preference_value)
}
})
def _create_preference_update_error(preference_key, preference_value, error):
......
......@@ -90,11 +90,13 @@ class TestPreferencesAPI(UserAPITestCase):
# Create some test preferences values.
set_user_preference(self.user, "dict_pref", {"int_key": 10})
set_user_preference(self.user, "string_pref", "value")
set_user_preference(self.user, "time_zone", "Asia/Tokyo")
# Log in the client and do the GET.
client = self.login_client(api_client, user)
response = self.send_get(client)
self.assertEqual({"dict_pref": "{'int_key': 10}", "string_pref": "value"}, response.data)
self.assertEqual({"dict_pref": "{'int_key': 10}", "string_pref": "value", "time_zone": "Asia/Tokyo"},
response.data)
@ddt.data(
("client", "user"),
......@@ -178,6 +180,7 @@ class TestPreferencesAPI(UserAPITestCase):
set_user_preference(self.user, "dict_pref", {"int_key": 10})
set_user_preference(self.user, "string_pref", "value")
set_user_preference(self.user, "extra_pref", "extra_value")
set_user_preference(self.user, "time_zone", "Asia/Macau")
# Send the patch request
self.client.login(username=self.user.username, password=self.test_password)
......@@ -187,6 +190,7 @@ class TestPreferencesAPI(UserAPITestCase):
"string_pref": "updated_value",
"new_pref": "new_value",
"extra_pref": None,
"time_zone": "Europe/London",
},
expected_status=204
)
......@@ -197,6 +201,7 @@ class TestPreferencesAPI(UserAPITestCase):
"dict_pref": "{'int_key': 10}",
"string_pref": "updated_value",
"new_pref": "new_value",
"time_zone": "Europe/London",
}
self.assertEqual(expected_preferences, response.data)
......@@ -208,6 +213,7 @@ class TestPreferencesAPI(UserAPITestCase):
set_user_preference(self.user, "dict_pref", {"int_key": 10})
set_user_preference(self.user, "string_pref", "value")
set_user_preference(self.user, "extra_pref", "extra_value")
set_user_preference(self.user, "time_zone", "Pacific/Midway")
# Send the patch request
self.client.login(username=self.user.username, password=self.test_password)
......@@ -218,6 +224,7 @@ class TestPreferencesAPI(UserAPITestCase):
TOO_LONG_PREFERENCE_KEY: "new_value",
"new_pref": "new_value",
u"empty_pref_ȻħȺɍłɇs": "",
"time_zone": "Asia/Africa",
},
expected_status=400
)
......@@ -238,6 +245,11 @@ class TestPreferencesAPI(UserAPITestCase):
"developer_message": u"Preference 'empty_pref_ȻħȺɍłɇs' cannot be set to an empty value.",
"user_message": u"Preference 'empty_pref_ȻħȺɍłɇs' cannot be set to an empty value.",
},
"time_zone": {
"developer_message": u"Value 'Asia/Africa' not valid for preference 'time_zone': Not in "
u"timezone set.",
"user_message": u"Value 'Asia/Africa' is not valid for user preference 'time_zone'."
},
}
)
......@@ -247,6 +259,7 @@ class TestPreferencesAPI(UserAPITestCase):
u"dict_pref": u"{'int_key': 10}",
u"string_pref": u"value",
u"extra_pref": u"extra_value",
u"time_zone": u"Pacific/Midway",
}
self.assertEqual(expected_preferences, response.data)
......
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