From f5b7ad80fcfd1638cb7ea019a38871c3b8438e36 Mon Sep 17 00:00:00 2001 From: Adrien Dieudonne Date: Wed, 16 Aug 2017 14:49:08 +0200 Subject: [PATCH] [FIX] web: always use browser timezone With the new views, we stopped using the browser timezone to display the dates in Odoo, and we used the timezone defined on the User profile instead. When loading the webclient, the timezone offset was put into the session and used to display all dates. This wasn't a good idea. The given offset was computed for the current time, meaning that it may be incorrect for specific dates (e.g. with the daylight saving, the UTC offset of today is not the same as 6 months ago). Moreover (but less likely), as the offset was stored in the session, it wasn't recalculated afterwards. So if the offset actually changed during the session (e.g. from or to daylight saving time), the displayed dates were incorrect until the user reloaded the page. With this rev., we don't retrieve the offset from the server anymore and we use the browser timezone again (like before the new views). However, we keep the computation of the offset (on the fly) in the session, so that it can be mocked in the test environment. --- addons/web/models/ir_http.py | 3 -- addons/web/static/src/js/core/session.js | 15 ++++++--- .../web/static/src/js/fields/basic_fields.js | 6 ++-- .../web/static/src/js/fields/field_utils.js | 6 ++-- .../src/js/views/calendar/calendar_model.js | 12 +++---- .../static/tests/fields/basic_fields_tests.js | 32 ++++++++++++++----- .../static/tests/fields/field_utils_tests.js | 26 +++++++++++++++ .../tests/fields/relational_fields_tests.js | 4 ++- addons/web/static/tests/helpers/test_utils.js | 7 ++-- .../web/static/tests/views/calendar_tests.js | 16 +++++++--- addons/web/static/tests/views/form_tests.js | 4 ++- addons/web/static/tests/views/list_tests.js | 4 ++- 12 files changed, 99 insertions(+), 36 deletions(-) diff --git a/addons/web/models/ir_http.py b/addons/web/models/ir_http.py index 73a83664683..a1fa58ef81d 100644 --- a/addons/web/models/ir_http.py +++ b/addons/web/models/ir_http.py @@ -5,8 +5,6 @@ import json from odoo import models from odoo.http import request -import pytz -import datetime import odoo @@ -26,7 +24,6 @@ class Http(models.AbstractModel): version_info = odoo.service.common.exp_version() return { "session_id": request.session.sid, - "tzOffset": self._context.get('tz') and datetime.datetime.now(pytz.timezone(self._context['tz'])).utcoffset().total_seconds() / 60 or False, "uid": request.session.uid, "is_system": request.env.user._is_system(), "is_superuser": request.env.user._is_superuser(), diff --git a/addons/web/static/src/js/core/session.js b/addons/web/static/src/js/core/session.js index c98ff7fbe9a..033d7995c9c 100644 --- a/addons/web/static/src/js/core/session.js +++ b/addons/web/static/src/js/core/session.js @@ -285,10 +285,6 @@ var Session = core.Class.extend(mixins.EventDispatcherMixin, { var result = _.extend({}, window.odoo.session_info); delete result.session_id; _.extend(this, result); - if (this.tzOffset === false) { - // send by server to have the user timezone and not the browser timezone - this.tzOffset = -new Date().getTimezoneOffset(); - } return $.when(); }, check_session_id: function () { @@ -410,6 +406,17 @@ var Session = core.Class.extend(mixins.EventDispatcherMixin, { }) ? '' : this.prefix; return prefix + path + qs; }, + /** + * Returns the time zone difference (in minutes) from the current locale + * (host system settings) to UTC, for a given date. The offset is positive + * if the local timezone is behind UTC, and negative if it is ahead. + * + * @param {string || moment} date a valid string date or moment instance + * @returns {integer} + */ + getTZOffset: function (date) { + return -new Date(date).getTimezoneOffset(); + }, }); return Session; diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js index ec04f67e270..035d24dbdf2 100644 --- a/addons/web/static/src/js/fields/basic_fields.js +++ b/addons/web/static/src/js/fields/basic_fields.js @@ -450,7 +450,7 @@ var FieldDateTime = FieldDate.extend({ */ _getValue: function () { var value = this.datewidget.getValue(); - return value && value.add(-this.getSession().tzOffset, 'minutes'); + return value && value.add(-this.getSession().getTZOffset(value), 'minutes'); }, /** @@ -460,7 +460,7 @@ var FieldDateTime = FieldDate.extend({ * @private */ _makeDatePicker: function () { - var value = this.value && this.value.clone().add(this.getSession().tzOffset, 'minutes'); + var value = this.value && this.value.clone().add(this.getSession().getTZOffset(this.value), 'minutes'); return new datepicker.DateTimeWidget(this, {defaultDate: value}); }, @@ -471,7 +471,7 @@ var FieldDateTime = FieldDate.extend({ * @private */ _renderEdit: function () { - var value = this.value && this.value.clone().add(this.getSession().tzOffset, 'minutes'); + var value = this.value && this.value.clone().add(this.getSession().getTZOffset(this.value), 'minutes'); this.datewidget.setValue(value); this.$input = this.datewidget.$input; }, diff --git a/addons/web/static/src/js/fields/field_utils.js b/addons/web/static/src/js/fields/field_utils.js index 787f66080dd..db5cd34d8b9 100644 --- a/addons/web/static/src/js/fields/field_utils.js +++ b/addons/web/static/src/js/fields/field_utils.js @@ -97,7 +97,7 @@ function formatDate(value, field, options) { } if (field && field.type === 'datetime') { if (!options || !('timezone' in options) || options.timezone) { - value = value.clone().add(session.tzOffset, 'minutes'); + value = value.clone().add(session.getTZOffset(value), 'minutes'); } } var l10n = core._t.database.parameters; @@ -123,7 +123,7 @@ function formatDateTime(value, field, options) { return ""; } if (!options || !('timezone' in options) || options.timezone) { - value = value.clone().add(session.tzOffset, 'minutes'); + value = value.clone().add(session.getTZOffset(value), 'minutes'); } var l10n = core._t.database.parameters; var date_format = time.strftime_to_moment_format(l10n.date_format); @@ -404,7 +404,7 @@ function parseDateTime(value, field, options) { } else { datetime = moment.utc(value, [pattern1, pattern2, moment.ISO_8601], true); if (options && options.timezone) { - datetime.add(-session.tzOffset, 'minutes'); + datetime.add(-session.getTZOffset(datetime), 'minutes'); } } if (datetime.isValid()) { diff --git a/addons/web/static/src/js/views/calendar/calendar_model.js b/addons/web/static/src/js/views/calendar/calendar_model.js index d2b0c02c524..ed02983d957 100644 --- a/addons/web/static/src/js/views/calendar/calendar_model.js +++ b/addons/web/static/src/js/views/calendar/calendar_model.js @@ -65,12 +65,12 @@ return AbstractModel.extend({ end.startOf('day').add(-1, 'days'); } else { // default hours in the user's timezone - start.hours(7).add(-this.getSession().tzOffset, 'minutes'); - end.hours(19).add(-this.getSession().tzOffset, 'minutes'); + start.hours(7).add(-this.getSession().getTZOffset(start), 'minutes'); + end.hours(19).add(-this.getSession().getTZOffset(end), 'minutes'); } } else { - start.add(-this.getSession().tzOffset, 'minutes'); - end.add(-this.getSession().tzOffset, 'minutes'); + start.add(-this.getSession().getTZOffset(start), 'minutes'); + end.add(-this.getSession().getTZOffset(end), 'minutes'); } if (this.mapping.all_day) { @@ -599,8 +599,8 @@ return AbstractModel.extend({ date_stop = date_start.clone().add(date_delay,'hours'); } - date_start.add(this.getSession().tzOffset, 'minutes'); - date_stop.add(this.getSession().tzOffset, 'minutes'); + date_start.add(this.getSession().getTZOffset(date_start), 'minutes'); + date_stop.add(this.getSession().getTZOffset(date_stop), 'minutes'); if (this.mapping.all_day && evt[this.mapping.all_day]) { date_stop.add(1, 'days'); diff --git a/addons/web/static/tests/fields/basic_fields_tests.js b/addons/web/static/tests/fields/basic_fields_tests.js index a1aa5958009..35fa05a8eab 100644 --- a/addons/web/static/tests/fields/basic_fields_tests.js +++ b/addons/web/static/tests/fields/basic_fields_tests.js @@ -1589,7 +1589,9 @@ QUnit.module('basic_fields', { date_format: '%m/%d/%Y', }, session: { - tzOffset: 120 // Should be ignored by date fields + getTZOffset: function () { + return 120; // Should be ignored by date fields + }, }, }); @@ -1634,7 +1636,9 @@ QUnit.module('basic_fields', { date_format: '%m/%d/%Y', }, session: { - tzOffset: -120 // Should be ignored by date fields + getTZOffset: function () { + return -120; // Should be ignored by date fields + }, }, }); @@ -1663,7 +1667,9 @@ QUnit.module('basic_fields', { date_format: '%m/%d/%Y', }, session: { - tzOffset: 0 + getTZOffset: function () { + return 0; + }, }, }); @@ -1752,7 +1758,9 @@ QUnit.module('basic_fields', { time_format: '%H:%M:%S', }, session: { - tzOffset: 120 + getTZOffset: function () { + return 120; + }, }, }); @@ -1809,7 +1817,9 @@ QUnit.module('basic_fields', { time_format: '%H:%M:%S', }, session: { - tzOffset: 120 + getTZOffset: function () { + return 120; + }, }, }); @@ -1879,7 +1889,9 @@ QUnit.module('basic_fields', { time_format: '%H:%M:%S', }, session: { - tzOffset: 120 + getTZOffset: function () { + return 120; + }, }, }); @@ -1924,7 +1936,9 @@ QUnit.module('basic_fields', { time_format: '%H:%M:%S', }, session: { - tzOffset: -240, + getTZOffset: function () { + return -240; + }, }, }); @@ -1966,7 +1980,9 @@ QUnit.module('basic_fields', { time_format: '%H:%M:%S', }, session: { - tzOffset: -240, + getTZOffset: function () { + return -240; + }, }, }); diff --git a/addons/web/static/tests/fields/field_utils_tests.js b/addons/web/static/tests/fields/field_utils_tests.js index 922c9f3a79c..a0284798553 100644 --- a/addons/web/static/tests/fields/field_utils_tests.js +++ b/addons/web/static/tests/fields/field_utils_tests.js @@ -4,6 +4,7 @@ odoo.define('web.field_utils_tests', function (require) { var core = require('web.core'); var session = require('web.session'); var fieldUtils = require('web.field_utils'); +var testUtils = require('web.test_utils'); QUnit.module('fields', {}, function () { @@ -63,6 +64,31 @@ QUnit.test("format_datetime", function (assert) { assert.strictEqual(str, moment(date).format("MM/DD/YYYY HH:mm:ss")); }); +QUnit.test("format_datetime (with different timezone offset)", function (assert) { + assert.expect(2); + + // mock the date format to avoid issues due to localisation + var dateFormat = core._t.database.parameters.date_format; + core._t.database.parameters.date_format = '%m/%d/%Y'; + session.getTZOffset = function (date) { + // simulate daylight saving time + var startDate = new Date(2017, 2, 26); + var endDate = new Date(2017, 9, 29); + if (startDate < date && date < endDate) { + return 120; // UTC+2 + } else { + return 60; // UTC+1 + } + }; + + var str = fieldUtils.format.datetime(moment.utc('2017-01-01T10:00:00Z')); + assert.strictEqual(str, '01/01/2017 11:00:00'); + str = fieldUtils.format.datetime(moment.utc('2017-06-01T10:00:00Z')); + assert.strictEqual(str, '06/01/2017 12:00:00'); + + core._t.database.parameters.date_format = dateFormat; +}); + QUnit.test("format_many2one", function (assert) { assert.expect(2); diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js index fd872fe2ca7..3d6721a55df 100644 --- a/addons/web/static/tests/fields/relational_fields_tests.js +++ b/addons/web/static/tests/fields/relational_fields_tests.js @@ -1771,7 +1771,9 @@ QUnit.module('relational_fields', { '', res_id: 1, session: { - tzOffset: 120 + getTZOffset: function () { + return 120; + }, }, }); assert.strictEqual(form.$('td:eq(0)').text(), "01/25/2017", diff --git a/addons/web/static/tests/helpers/test_utils.js b/addons/web/static/tests/helpers/test_utils.js index 56070453e31..0115dfc9152 100644 --- a/addons/web/static/tests/helpers/test_utils.js +++ b/addons/web/static/tests/helpers/test_utils.js @@ -256,8 +256,11 @@ function addMockEnvironment(widget, params) { var initialDebounce = DebouncedField.prototype.DEBOUNCE; DebouncedField.prototype.DEBOUNCE = params.fieldDebounce || 0; var initialSession, initialConfig, initialParameters; + initialSession = _.extend({}, session); + session.getTZOffset = function () { + return 0; // by default, but may be overriden in specific tests + }; if ('session' in params) { - initialSession = _.extend({}, session); _.extend(session, params.session); } if ('config' in params) { @@ -282,8 +285,8 @@ function addMockEnvironment(widget, params) { for (key in session) { delete session[key]; } - _.extend(session, initialSession); } + _.extend(session, initialSession); if ('config' in params) { for (key in config) { delete config[key]; diff --git a/addons/web/static/tests/views/calendar_tests.js b/addons/web/static/tests/views/calendar_tests.js index 6832c71b170..29e76182bb4 100644 --- a/addons/web/static/tests/views/calendar_tests.js +++ b/addons/web/static/tests/views/calendar_tests.js @@ -440,7 +440,9 @@ QUnit.module('Views', { initialDate: initialDate, }, session: { - tzOffset: 120 + getTZOffset: function () { + return 120; + }, }, mockRPC: function (route, args) { if (args.method === "create") { @@ -541,7 +543,9 @@ QUnit.module('Views', { initialDate: initialDate, }, session: { - tzOffset: 120 + getTZOffset: function () { + return 120; + }, }, mockRPC: function (route, args) { if (args.method === "create") { @@ -664,7 +668,9 @@ QUnit.module('Views', { initialDate: initialDate, }, session: { - tzOffset: 120 + getTZOffset: function () { + return 120; + }, }, }); @@ -728,7 +734,9 @@ QUnit.module('Views', { initialDate: initialDate, }, session: { - tzOffset: 120 + getTZOffset: function () { + return 120; + }, }, }); diff --git a/addons/web/static/tests/views/form_tests.js b/addons/web/static/tests/views/form_tests.js index 3a1976592d7..ada05e5ed28 100644 --- a/addons/web/static/tests/views/form_tests.js +++ b/addons/web/static/tests/views/form_tests.js @@ -2849,7 +2849,9 @@ QUnit.module('Views', { '', res_id: 1, session: { - tzOffset: 120 + getTZOffset: function () { + return 120; + }, }, }); diff --git a/addons/web/static/tests/views/list_tests.js b/addons/web/static/tests/views/list_tests.js index 402b59194f8..083aa349516 100644 --- a/addons/web/static/tests/views/list_tests.js +++ b/addons/web/static/tests/views/list_tests.js @@ -1271,7 +1271,9 @@ QUnit.module('Views', { data: this.data, arch: '', session: { - tzOffset: 120 + getTZOffset: function () { + return 120; + }, }, });