From 69ae6e2912aaf6190d9856f04069d725ef5ad5a4 Mon Sep 17 00:00:00 2001 From: Christophe Matthieu Date: Tue, 18 Apr 2017 14:24:04 +0200 Subject: [PATCH] [FIX] web: use correct timezone for date fields before this commit when toggling allday flag in calendar form view, it was possible to have one day error. The input datetime is wrong when the user don't use the datepicker widget (insert date by keypress instead of click on datepicker) sometimes, the value could be null (crash because try to clone false) --- .../web/static/src/js/fields/basic_fields.js | 18 ++ .../web/static/src/js/fields/field_utils.js | 58 +++-- .../web/static/src/js/widgets/date_picker.js | 7 +- .../static/tests/fields/basic_fields_tests.js | 5 +- .../web/static/tests/views/calendar_tests.js | 208 ++++++++++++------ 5 files changed, 204 insertions(+), 92 deletions(-) diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js index 9f37240e0cc..85cd0382ac2 100644 --- a/addons/web/static/src/js/fields/basic_fields.js +++ b/addons/web/static/src/js/fields/basic_fields.js @@ -282,6 +282,14 @@ var FieldDate = InputField.extend({ // Private //-------------------------------------------------------------------------- + /** + * @override + * @private + */ + _doDebouncedAction: function () { + this.datewidget.change_datetime(); + }, + /** * return the datepicker value * @@ -311,6 +319,16 @@ var FieldDate = InputField.extend({ this.$input = this.datewidget.$input; }, + /** + * FieldDateTime overrides _formatValue to use the session timezone + * + * @override + * @private + * @param {Moment} value + */ + _formatValue: function (value) { + return field_utils.format.date(value, {timezone: true}); + }, }); var FieldDateTime = FieldDate.extend({ diff --git a/addons/web/static/src/js/fields/field_utils.js b/addons/web/static/src/js/fields/field_utils.js index d6677291d47..b00fca0048a 100644 --- a/addons/web/static/src/js/fields/field_utils.js +++ b/addons/web/static/src/js/fields/field_utils.js @@ -60,12 +60,20 @@ function formatChar(value) { * an empty string. Note that this is dependant on the localization settings * * @param {Moment|false} + * @param {Object} [field] + * a description of the field (note: this parameter is ignored) + * @param {Object} [options] additional options + * @param {boolean} [options.timezone=true] use the user timezone when formating the + * date * @returns {string} */ -function formatDate(value) { +function formatDate(value, field, options) { if (!value) { return ""; } + if (!options || !('timezone' in options) || options.timezone) { + value = value.clone().add(session.tzOffset < 0 ? -1 : 0, 'days'); + } var l10n = core._t.database.parameters; var date_format = time.strftime_to_moment_format(l10n.date_format); return value.format(date_format); @@ -77,9 +85,14 @@ function formatDate(value) { * settings * * @params {Moment|false} + * @param {Object} [field] + * a description of the field (note: this parameter is ignored) + * @param {Object} [options] additional options + * @param {boolean} [options.timezone=true] use the user timezone when formating the + * date * @returns {string} */ -function formatDateTime(value, options) { +function formatDateTime(value, field, options) { if (!value) { return ""; } @@ -264,23 +277,32 @@ function formatSelection(value, field) { //////////////////////////////////////////////////////////////////////////////// /** - * create an Date object + * Create an Date object * The method toJSON return the formated value to send value server side * - * @params {string} + * @param {string} + * @param {Object} [field] + * a description of the field (note: this parameter is ignored) + * @param {Object} [options] additional options + * @param {boolean} [options.isUTC] the formatted date is utc + * @param {boolean} [options.timezone=false] format the date after apply the timezone + * offset * @returns {Moment|false} Moment date object */ function parseDate(value, field, options) { if (!value) { return false; } - var date_pattern = time.strftime_to_moment_format(core._t.database.parameters.date_format); - var date_pattern_wo_zero = date_pattern.replace('MM','M').replace('DD','D'); + var datePattern = time.strftime_to_moment_format(core._t.database.parameters.date_format); + var datePatternWoZero = datePattern.replace('MM','M').replace('DD','D'); var date; if (options && options.isUTC) { date = moment.utc(value); } else { - date = moment.utc(value, [date_pattern, date_pattern_wo_zero, moment.ISO_8601], true); + date = moment.utc(value, [datePattern, datePatternWoZero, moment.ISO_8601], true); + if (options && options.timezone) { + date.add(session.tzOffset > 0 ? -1 : 0, 'days'); + } } if (date.isValid() && date.year() >= 1900) { if (date.year() === 0) { @@ -297,22 +319,28 @@ function parseDate(value, field, options) { } /** - * create an Date object + * Create an Date object * The method toJSON return the formated value to send value server side * - * @params {string} + * @param {string} + * @param {Object} [field] + * a description of the field (note: this parameter is ignored) + * @param {Object} [options] additional options + * @param {boolean} [options.isUTC] the formatted date is utc + * @param {boolean} [options.timezone=false] format the date after apply the timezone + * offset * @returns {Moment|false} Moment date object */ function parseDateTime(value, field, options) { if (!value) { return false; } - var date_pattern = time.strftime_to_moment_format(core._t.database.parameters.date_format), - time_pattern = time.strftime_to_moment_format(core._t.database.parameters.time_format); - var date_pattern_wo_zero = date_pattern.replace('MM','M').replace('DD','D'), - time_pattern_wo_zero = time_pattern.replace('HH','H').replace('mm','m').replace('ss','s'); - var pattern1 = date_pattern + ' ' + time_pattern; - var pattern2 = date_pattern_wo_zero + ' ' + time_pattern_wo_zero; + var datePattern = time.strftime_to_moment_format(core._t.database.parameters.date_format), + timePattern = time.strftime_to_moment_format(core._t.database.parameters.time_format); + var datePatternWoZero = datePattern.replace('MM','M').replace('DD','D'), + timePatternWoZero = timePattern.replace('HH','H').replace('mm','m').replace('ss','s'); + var pattern1 = datePattern + ' ' + timePattern; + var pattern2 = datePatternWoZero + ' ' + timePatternWoZero; var datetime; if (options && options.isUTC) { // phatomjs crash if we don't use this format diff --git a/addons/web/static/src/js/widgets/date_picker.js b/addons/web/static/src/js/widgets/date_picker.js index 1641f13f634..1aaf05ee11e 100644 --- a/addons/web/static/src/js/widgets/date_picker.js +++ b/addons/web/static/src/js/widgets/date_picker.js @@ -60,7 +60,8 @@ var DateWidget = Widget.extend({ } }, get_value: function() { - return this.get('value') && this.get('value').clone(); + var value = this.get('value'); + return value && value.clone(); }, set_value_from_ui: function() { var value = this.$input.val() || false; @@ -84,10 +85,10 @@ var DateWidget = Widget.extend({ } }, parse_client: function(v) { - return field_utils.parse[this.type_of_date](v, {timezone: false}); + return field_utils.parse[this.type_of_date](v, null, {timezone: false}); }, format_client: function(v) { - return field_utils.format[this.type_of_date](v, {timezone: false}); + return field_utils.format[this.type_of_date](v, null, {timezone: false}); }, set_datetime_default: function() { //when opening datetimepicker the date and time by default should be the one from diff --git a/addons/web/static/tests/fields/basic_fields_tests.js b/addons/web/static/tests/fields/basic_fields_tests.js index 7d7aabb5617..1dd84ce3dd4 100644 --- a/addons/web/static/tests/fields/basic_fields_tests.js +++ b/addons/web/static/tests/fields/basic_fields_tests.js @@ -1041,7 +1041,10 @@ QUnit.module('basic_fields', { '', translateParameters: { // Avoid issues due to localization formats date_format: '%m/%d/%Y', - } + }, + session: { + tzOffset: 0 + }, }); var $cell = list.$('tr.o_data_row td:not(.o_list_record_selector)').first(); diff --git a/addons/web/static/tests/views/calendar_tests.js b/addons/web/static/tests/views/calendar_tests.js index 7813320af1d..a2347ad801b 100644 --- a/addons/web/static/tests/views/calendar_tests.js +++ b/addons/web/static/tests/views/calendar_tests.js @@ -33,8 +33,10 @@ QUnit.module('Views', { user_id: {string: "user", type: "many2one", relation: 'user'}, parnter_id: {string: "user", type: "many2one", relation: 'partner', related: 'user_id.parnter_id'}, name: {string: "name", type: "char"}, - start: {string: "start", type: "datetime"}, - stop: {string: "stop", type: "datetime"}, + start_date: {string: "start date", type: "date"}, + stop_date: {string: "stop date", type: "date"}, + start: {string: "start datetime", type: "datetime"}, + stop: {string: "stop datetime", type: "datetime"}, allday: {string: "allday", type: "boolean"}, partner_ids: {string: "attendees", type: "one2many", relation: 'partner'}, type: {string: "type", type: "integer"}, @@ -95,32 +97,19 @@ QUnit.module('Views', { QUnit.module('CalendarView'); var archs = { - "event,false,form": { - attrs: {}, - children: [ - { - attrs: {name: "name"}, - children: [], - tag: 'field' - }, - { - attrs: {name: "allday"}, - children: [], - tag: 'field' - }, - { - attrs: {name: "start"}, - children: [], - tag: 'field' - }, - { - attrs: {name: "stop"}, - children: [], - tag: 'field' - } - ], - tag: "form" - }, + "event,false,form": + '
'+ + ''+ + ''+ + ''+ + ''+ + ''+ + ''+ + ''+ + ''+ + '', "event,1,form": { attrs: {}, children: [ @@ -337,10 +326,92 @@ QUnit.module('Views', { }); QUnit.test('create event with timezone in week mode', function (assert) { - assert.expect(8); + assert.expect(4); this.data.event.records = []; + var calendar = createView({ + View: CalendarView, + model: 'event', + data: this.data, + arch: + ''+ + ''+ + '', + archs: archs, + viewOptions: { + initialDate: initialDate, + }, + session: { + tzOffset: 120 + }, + }); + + + var $view = $('#qunit-fixture').contents(); + $view.prependTo('body'); // => select with click position + + var top = calendar.$('.fc-axis:contains(8am)').offset().top + 5; + var left = calendar.$('.fc-day:eq(2)').offset().left + 5; + + testUtils.triggerPositionalMouseEvent(left, top, "mousedown"); + testUtils.triggerPositionalMouseEvent(left, top + 60, "mousemove"); + + assert.strictEqual(calendar.$('.fc-content .fc-time').text(), "08:00 - 10:00", + "should display the time in the calendar sticker"); + + testUtils.triggerPositionalMouseEvent(left, top + 60, "mouseup"); + $('.modal input:first').val('new event').trigger('input'); + $('.modal button.btn:contains(Create)').trigger('click'); + var $newevent = calendar.$('.fc-event:contains(new event)'); + + assert.strictEqual($newevent.text().replace(/[\s\n\r]+/g, ''), "08:00-10:00newevent", + "should display the new event with time and title"); + + assert.deepEqual($newevent.data('fcSeg').event.record, + { + display_name: "new event", + start: "2016-12-13 06:00:00", + stop: "2016-12-13 08:00:00", + allday: false, + name: "new event", + id: 1 + }, + "the new record should have the utc datetime (quickCreate)"); + + // delete record + + $newevent.trigger('click'); + $('.modal button.btn-default:contains(Delete)').trigger('click'); + $('.modal button.btn-primary:contains(Ok)').trigger('click'); + assert.strictEqual(calendar.$('.fc-content').length, 0, "should delete the record"); + + calendar.destroy(); + $view.remove(); + }); + + QUnit.test('create event with timezone in week mode with formViewDialog', function (assert) { + assert.expect(6); + + this.data.event.records = []; + this.data.event.onchanges = { + allday: function (obj) { + if (obj.allday) { + obj.start_date = obj.start && obj.start.split(' ')[0] || obj.start_date; + obj.stop_date = obj.stop && obj.stop.split(' ')[0] || obj.stop_date || obj.start_date; + } else { + obj.start = obj.start_date && (obj.start_date + ' 00:00:00') || obj.start; + obj.stop = obj.stop_date && (obj.stop_date + ' 00:00:00') || obj.stop || obj.start; + } + } + }; + var calendar = createView({ View: CalendarView, model: 'event', @@ -367,7 +438,7 @@ QUnit.module('Views', { assert.deepEqual(args.args[1], { "allday": false, "start": "2016-12-12 06:00:00", - "stop": "2016-12-12 10:00:00" + "stop": "2016-12-12 08:00:00" }, "should move the event"); } @@ -375,75 +446,66 @@ QUnit.module('Views', { }, }); - var $view = $('#qunit-fixture').contents(); $view.prependTo('body'); // => select with click position var top = calendar.$('.fc-axis:contains(8am)').offset().top + 5; var left = calendar.$('.fc-day:eq(2)').offset().left + 5; - testUtils.triggerPositionalMouseEvent(left, top, "mousedown"); - testUtils.triggerPositionalMouseEvent(left, top + 140, "mousemove"); - - assert.strictEqual(calendar.$('.fc-content .fc-time').text(), "08:00 - 12:00", - "should display the time in the calendar sticker"); - - testUtils.triggerPositionalMouseEvent(left, top + 140, "mouseup"); - $('.modal input:first').val('new event').trigger('input'); - $('.modal button.btn:contains(Create)').trigger('click'); - var $newevent = calendar.$('.fc-event:contains(new event)'); - - assert.strictEqual($newevent.text().replace(/[\s\n\r]+/g, ''), "08:00-12:00newevent", - "should display the new event with time and title"); - - assert.deepEqual($newevent.data('fcSeg').event.record, - { - display_name: "new event", - start: "2016-12-13 06:00:00", - stop: "2016-12-13 10:00:00", - allday: false, - name: "new event", - id: 1 - }, - "the new record should have the utc datetime (quickCreate)"); - - // delete record - - $newevent.trigger('click'); - $('.modal button.btn-default:contains(Delete)').trigger('click'); - $('.modal button.btn-primary:contains(Ok)').trigger('click'); - assert.strictEqual(calendar.$('.fc-content').length, 0, "should delete the record"); - - - // create again - - testUtils.triggerPositionalMouseEvent(left, top, "mousedown"); - testUtils.triggerPositionalMouseEvent(left, top + 140, "mousemove"); - testUtils.triggerPositionalMouseEvent(left, top + 140, "mouseup"); + testUtils.triggerPositionalMouseEvent(left, top + 60, "mousemove"); + testUtils.triggerPositionalMouseEvent(left, top + 60, "mouseup"); $('.modal input:first').val('new event').trigger('input'); $('.modal button.btn:contains(Edit)').trigger('click'); assert.strictEqual($('.o_form_field[name="start"] input').val(), "12/13/2016 08:00:00", "should display the datetime"); - $('.modal-lg button.btn:contains(Save)').trigger('click'); - $newevent = calendar.$('.fc-event:contains(new event)'); + $('.modal-lg .o_field_boolean[name="allday"] input').trigger('click'); - assert.strictEqual($newevent.text().replace(/[\s\n\r]+/g, ''), "08:00-12:00newevent", + assert.strictEqual($('.o_form_field[name="start_date"] input').val(), "12/13/2016", + "should display the date"); + + $('.modal-lg .o_field_boolean[name="allday"] input').trigger('click'); + + assert.strictEqual($('.o_form_field[name="start"] input').val(), "12/13/2016 02:00:00", + "should display the datetime from the date with the timezone"); + + // use datepicker to enter a date: 12/13/2016 08:00:00 + $('.o_form_field[name="start"] input').trigger('click'); + $('.bootstrap-datetimepicker-widget .picker-switch a[data-action="togglePicker"]').trigger('click'); + $('.bootstrap-datetimepicker-widget .timepicker .timepicker-hour').trigger('click'); + $('.bootstrap-datetimepicker-widget .timepicker-hours td.hour:contains(08)').trigger('click'); + $('.bootstrap-datetimepicker-widget .picker-switch a[data-action="close"]').trigger('click'); + + // use datepicker to enter a date: 12/13/2016 10:00:00 + $('.o_form_field[name="stop"] input').trigger('click'); + $('.bootstrap-datetimepicker-widget .picker-switch a[data-action="togglePicker"]').trigger('click'); + $('.bootstrap-datetimepicker-widget .timepicker .timepicker-hour').trigger('click'); + $('.bootstrap-datetimepicker-widget .timepicker-hours td.hour:contains(10)').trigger('click'); + $('.bootstrap-datetimepicker-widget .picker-switch a[data-action="close"]').trigger('click'); + + $('.modal-lg button.btn:contains(Save)').trigger('click'); + var $newevent = calendar.$('.fc-event:contains(new event)'); + + assert.strictEqual($newevent.text().replace(/[\s\n\r]+/g, ''), "08:00-10:00newevent", "should display the new event with time and title"); assert.deepEqual($newevent.data('fcSeg').event.record, { display_name: "new event", start: "2016-12-13 06:00:00", - stop: "2016-12-13 10:00:00", + stop: "2016-12-13 08:00:00", allday: false, name: "new event", id: 1 }, "the new record should have the utc datetime (formViewDialog)"); + var pos = calendar.$('.fc-content').offset(); + left = pos.left + 5; + top = pos.top + 5; + testUtils.triggerPositionalMouseEvent(left, top, "mousedown"); left = calendar.$('.fc-day:eq(1)').offset().left + 5; testUtils.triggerPositionalMouseEvent(left, top, "mousemove");