From ba254be4fcee07de92d9905c488ff08f30cafa4e Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Thu, 21 Aug 2014 18:46:11 +0000 Subject: [PATCH] * eTemplate2/all apps: fixing various timezone related issues backport of Nathan and mine commits r47919, r48102, r48133, r48142, r48163, r48166 using now ISO time strings instead of timestamps for communication between client and server --- calendar/js/app.js | 20 +++--- .../inc/class.etemplate_widget_date.inc.php | 62 +++++++++++++++++-- etemplate/js/et2_widget_date.js | 49 +++++++++++---- 3 files changed, 103 insertions(+), 28 deletions(-) diff --git a/calendar/js/app.js b/calendar/js/app.js index 3c61281706..e2bb7749e6 100644 --- a/calendar/js/app.js +++ b/calendar/js/app.js @@ -119,7 +119,7 @@ app.classes.calendar = AppJS.extend( break; } }, - + /** * Observer method receives update notifications from all applications * @@ -155,7 +155,7 @@ app.classes.calendar = AppJS.extend( if (match[1]== _id) do_refresh = true; } }); - if (jQuery('div [id^="infolog'+_id+'"],div [id^="drag_infolog'+_id+'"]').length > 0) do_refresh = true; + if (jQuery('div [id^="infolog'+_id+'"],div [id^="drag_infolog'+_id+'"]').length > 0) do_refresh = true; switch (_type) { case 'add': @@ -175,9 +175,9 @@ app.classes.calendar = AppJS.extend( } } break; - } + } }, - + /** * Link hander for jDots template to just reload our iframe, instead of reloading whole admin app * @@ -602,7 +602,7 @@ app.classes.calendar = AppJS.extend( { //Get infologID if in case if it's an integrated infolog event var infolog_id = eventId.split('infolog')[1]; - + if (infolog_id) { // If it is an integrated infolog event we need to edit infolog entry @@ -720,7 +720,7 @@ app.classes.calendar = AppJS.extend( { this.egw.open_link(this.egw.link("/index.php",vars),'_blank','700x700'); }, - + /** * control delete_series popup visibility * @@ -755,7 +755,7 @@ app.classes.calendar = AppJS.extend( id: 'dialog[cancel]', image: 'cancel' } - + ]; var self = this; et2_dialog.show_dialog @@ -988,7 +988,7 @@ app.classes.calendar = AppJS.extend( { // nm action - show popup nm_open_popup(_action,_senders); - } + } return; } @@ -1244,7 +1244,6 @@ app.classes.calendar = AppJS.extend( var alarm_date = this.et2.getWidgetById('new_alarm[date]'); var alarm_options = _widget || this.et2.getWidgetById('new_alarm[options]'); var start = this.et2.getWidgetById('start'); - var date = 0; if (alarm_date && alarm_options && start) @@ -1260,7 +1259,8 @@ app.classes.calendar = AppJS.extend( var startDate = start.get_value(); if (startDate) { - date = startDate - parseInt(alarm_options.get_value()); + var date = new Date(startDate); + date.setTime(date.getTime() - 1000 * parseInt(alarm_options.get_value())); alarm_date.set_value(date); } } diff --git a/etemplate/inc/class.etemplate_widget_date.inc.php b/etemplate/inc/class.etemplate_widget_date.inc.php index 4fbfe57ef7..68ea86a57a 100644 --- a/etemplate/inc/class.etemplate_widget_date.inc.php +++ b/etemplate/inc/class.etemplate_widget_date.inc.php @@ -30,9 +30,10 @@ * * @todo validation of date-duration * - * @info beforeSendToClient is no longer neccessary, in order to handle date/time conversion, for this widget - * as we are handling both timestamp and string date/time formats on client side - * + * @info Communication between client and server is always done as a string in ISO8601/W3C + * format ("Y-m-d\TH:i:sP"). If the application specifies a different format + * for the field, the conversion is done as needed understand what the application + * sends, and to give the application what it wants when the form is submitted. */ class etemplate_widget_date extends etemplate_widget_transformer { @@ -48,9 +49,48 @@ class etemplate_widget_date extends etemplate_widget_transformer protected $legacy_options = 'dataformat,mode'; + /** + * Convert the provided date into the format needed for unambiguous communication + * with browsers (Javascript). We use W3C format to avoid timestamp issues. + */ + public function beforeSendToClient($cname) + { + if($this->type == 'date-houronly') + { + return parent::beforeSendToClient($cname); + } + + $form_name = self::form_name($cname, $this->id); + $value =& self::get_array(self::$request->content, $form_name, false, true); + + if($this->type != 'date-duration' && $value) + { + // string with formatting letters like for php's date() method + if ($this->attrs['dataformat'] && !is_numeric($value)) + { + $date = date_create_from_format($this->attrs['dataformat'], $value, new DateTimeZone('UTC')); + } + else + { + $date = new egw_time((int)$value, new DateTimeZone('UTC')); + } + if($date) + { + // Set timezone to UTC so javascript doesn't add/subtract anything + $date->setTimezone(new DateTimeZone('UTC')); + $value = $date->format(egw_time::W3C); + } + } + } + /** * Validate input * + * For dates (except duration), it is always a full timestamp in W3C format, + * which we then convert to the format the application is expecting. This can + * be either a unix timestamp, just a date, just time, or whatever is + * specified in the template. + * * @param string $cname current namespace * @param array $expand values for keys 'c', 'row', 'c_', 'row_', 'cont' * @param array $content @@ -65,7 +105,7 @@ class etemplate_widget_date extends etemplate_widget_transformer { $value = self::get_array($content, $form_name); $valid =& self::get_array($validated, $form_name, true); - + if ((string)$value === '' && $this->attrs['needed']) { self::set_validation_error($form_name,lang('Field must not be empty !!!')); @@ -78,12 +118,21 @@ class etemplate_widget_date extends etemplate_widget_transformer { $valid = (string)$value === '' ? '' : (int)$value; } + if($value) + { + $date = new egw_time($value); + } + if(!$value) + { + // Not null, blank + $value = ''; + } elseif (empty($this->attrs['dataformat'])) // integer timestamp { - $valid = (int)$value; + $valid = $date->format('ts'); } // string with formatting letters like for php's date() method - elseif (($valid = date($this->attrs['dataformat'], $value))) + elseif (($valid = $date->format($this->attrs['dataformat']))) { // Nothing to do here } @@ -92,6 +141,7 @@ class etemplate_widget_date extends etemplate_widget_transformer // this is not really a user error, but one of the clientside engine self::set_validation_error($form_name,lang("'%1' is not a valid date !!!", $value).' '.$this->dataformat); } + //error_log("$this : ($valid)" . egw_time::to($valid)); } } } diff --git a/etemplate/js/et2_widget_date.js b/etemplate/js/et2_widget_date.js index 6c83d61c42..284f4323c5 100644 --- a/etemplate/js/et2_widget_date.js +++ b/etemplate/js/et2_widget_date.js @@ -23,6 +23,9 @@ /** * Class which implements the "date" XET-Tag * + * Dates are passed to the server in ISO8601 format ("Y-m-d\TH:i:sP"), and data_format is + * handled server-side. + * * @augments et2_inputWidget */ var et2_date = et2_inputWidget.extend( @@ -35,7 +38,7 @@ var et2_date = et2_inputWidget.extend( "ignore": false }, "data_format": { - "ignore": false, + "ignore": true, "description": "Date/Time format. Can be set as an options to date widget", "default": '' } @@ -52,7 +55,7 @@ var et2_date = et2_inputWidget.extend( this._super.apply(this, arguments); this.date = new Date(); - this.date.setHours(0); + this.date.setUTCHours(0); this.date.setMinutes(0); this.date.setSeconds(0); this.input = null; @@ -121,6 +124,11 @@ var et2_date = et2_inputWidget.extend( return; } + // Check for full timestamp + if(typeof _value == 'string' && _value.match(/(\d{4})-(\d{2})-(\d{2})T(\d{2})\:(\d{2})\:(\d{2})[+-](\d{2})\:(\d{2})/)) + { + _value = new Date(_value); + } // Handle just time as a string in the form H:i if(typeof _value == 'string' && isNaN(_value)) { @@ -146,7 +154,8 @@ var et2_date = et2_inputWidget.extend( return; } this.set_validation_error(false); - this.date.setHours(parsed.hour); + // Avoid javascript timezone offset, hour is in 'user time' + this.date.setUTCHours(parsed.hour); this.date.setMinutes(parsed.minute); this.input_date.val(_value); if(old_value !== this.getValue()) @@ -157,7 +166,7 @@ var et2_date = et2_inputWidget.extend( this.change(this.input_date); } } - this._oldValue = _value; + this._oldValue = this.date.toJSON(); return; default: // Parse customfields's date with storage data_format to date object @@ -173,7 +182,6 @@ var et2_date = et2_inputWidget.extend( var DTformat = this.options.data_format.split(' '); var parsed = jQuery.datepicker.parseDateTime(this.egw().dateTimeFormat(DTformat[0]),this.egw().dateTimeFormat(DTformat[1]), _value); } - this.date = new Date(parsed); } else // Parse other date widgets date with timepicker date/time format to date onject { @@ -184,9 +192,13 @@ var et2_date = et2_inputWidget.extend( this.set_validation_error(this.egw().lang("%1' han an invalid format !!!",_value)); return; } - this.date = new Date(parsed); } - + // Update local variable, but remove the timezone offset that + // javascript adds when we parse + if(parsed) + { + this.date = new Date(parsed.valueOf() - parsed.getTimezoneOffset() * 60000); + } this.set_validation_error(false); } @@ -202,17 +214,21 @@ var et2_date = et2_inputWidget.extend( // Update input - popups do, but framework doesn't _value = ''; + // Add timezone offset back in, or formatDate will lose those hours + var formatDate = new Date(this.date.valueOf() + this.date.getTimezoneOffset() * 60 * 1000); if(this._type != 'date-timeonly') { - _value = jQuery.datepicker.formatDate(this.input_date.datepicker("option","dateFormat"),this.date); + _value = jQuery.datepicker.formatDate(this.input_date.datepicker("option","dateFormat"), + formatDate + ); } if(this._type != 'date') { if(this._type != 'date-timeonly') _value += ' '; _value += jQuery.datepicker.formatTime(this.input_date.datepicker("option","timeFormat"),{ - hour: this.date.getHours(), - minute: this.date.getMinutes(), + hour: formatDate.getHours(), + minute: formatDate.getMinutes(), seconds: 0, timezone: 0 }); @@ -240,11 +256,13 @@ var et2_date = et2_inputWidget.extend( } else if (this._type == 'date') { - this.date.setHours(12); + this.date.setUTCHours(0); + this.date.setUTCMinutes(0); } + // Convert to timestamp - no seconds this.date.setSeconds(0,0); - return Math.round(this.date.valueOf() / 1000); + return this.date.toJSON(); } }); et2_register_widget(et2_date, ["date", "date-time", "date-timeonly"]); @@ -643,6 +661,13 @@ var et2_date_ro = et2_valueWidget.extend([et2_IDetachedDOM], jQuery.datepicker.parseDateTime(this.egw().preference('dateformat'),this.egw().preference('timeformat') == '24' ? 'H:i' : 'g:i a', _value); var text = new Date(parsed); + + // Update local variable, but remove the timezone offset that javascript adds + if(parsed) + { + this.date = new Date(text.valueOf() - (text.getTimezoneOffset()*60*1000)); + } + // JS dates use milliseconds this.date.setTime(text.valueOf()); }