ENHANCEMENT Added user timezone support to DatetimeField

This commit is contained in:
Ingo Schommer 2011-09-26 09:19:42 +02:00
parent 0601384cda
commit c89bdbb268
3 changed files with 148 additions and 36 deletions

View File

@ -18,6 +18,11 @@
* $field->getDateField()->setConfig('showcalendar', 1); // field-specific setting * $field->getDateField()->setConfig('showcalendar', 1); // field-specific setting
* </code> * </code>
* *
* - "timezone": Set a different timezone for viewing. {@link dataValue()} will still save
* the time in PHP's default timezone (date_default_timezone_get()), its only a view setting.
* Note that the sub-fields ({@link getDateField()} and {@link getTimeField()})
* are not timezone aware, and will have their values set in local time, rather than server time.
*
* @package sapphire * @package sapphire
* @subpackage forms * @subpackage forms
*/ */
@ -37,12 +42,14 @@ class DatetimeField extends FormField {
* @var array * @var array
*/ */
protected $config = array( protected $config = array(
'datavalueformat' => 'YYYY-MM-dd HH:mm:ss' 'datavalueformat' => 'YYYY-MM-dd HH:mm:ss',
'usertimezone' => null,
); );
function __construct($name, $title = null, $value = ""){ function __construct($name, $title = null, $value = ""){
$this->dateField = new DateField($name . '[date]', false); $this->dateField = new DateField($name . '[date]', false);
$this->timeField = new TimeField($name . '[time]', false); $this->timeField = new TimeField($name . '[time]', false);
$this->timezoneField = new HiddenField($this->Name() . '[timezone]');
parent::__construct($name, $title, $value); parent::__construct($name, $title, $value);
} }
@ -52,62 +59,107 @@ class DatetimeField extends FormField {
$this->dateField->setForm($form); $this->dateField->setForm($form);
$this->timeField->setForm($form); $this->timeField->setForm($form);
$this->timezoneField->setForm($form);
} }
function Field() { function Field() {
Requirements::css(SAPPHIRE_DIR . '/css/DatetimeField.css'); Requirements::css(SAPPHIRE_DIR . '/css/DatetimeField.css');
return $this->dateField->FieldHolder() . $this->timeField->FieldHolder() . '<div class="clear"><!-- --></div>'; $tzField = ($this->getConfig('usertimezone')) ? $this->timezoneField->FieldHolder() : '';
return $this->dateField->FieldHolder() .
$this->timeField->FieldHolder() .
$tzField .
'<div class="clear"><!-- --></div>';
} }
/** /**
* Sets the internal value to ISO date format. * Sets the internal value to ISO date format, based on either a database value in ISO date format,
* or a form submssion in the user date format. Uses the individual date and time fields
* to take care of the actual formatting and value conversion.
*
* Value setting happens *before* validation, so we have to set the value even if its not valid.
*
* Caution: Only converts user timezones when value is passed as array data (= form submission).
* Weak indication, but unfortunately the framework doesn't support a distinction between
* setting a value from the database, application logic, and user input.
* *
* @param string|array $val String expects an ISO date format. Array notation with 'date' and 'time' * @param string|array $val String expects an ISO date format. Array notation with 'date' and 'time'
* keys can contain localized strings. If the 'dmyfields' option is used for {@link DateField}, * keys can contain localized strings. If the 'dmyfields' option is used for {@link DateField},
* the 'date' value may contain array notation was well (see {@link DateField->setValue()}). * the 'date' value may contain array notation was well (see {@link DateField->setValue()}).
*/ */
function setValue($val) { function setValue($val) {
// If timezones are enabled, assume user data needs to be reverted to server timezone
if($this->getConfig('usertimezone')) {
// Accept user input on timezone, but only when timezone support is enabled
$userTz = (is_array($val) && array_key_exists('timezone', $val)) ? $val['timezone'] : null;
if(!$userTz) $userTz = $this->getConfig('usertimezone'); // fall back to defined timezone
} else {
$userTz = null;
}
if(empty($val)) { if(empty($val)) {
$this->value = null;
$this->dateField->setValue(null); $this->dateField->setValue(null);
$this->timeField->setValue(null); $this->timeField->setValue(null);
} else { } else {
// String setting is only possible from the database, so we don't allow anything but ISO format // Case 1: String setting from database, in ISO date format
if(is_string($val) && Zend_Date::isDate($val, $this->getConfig('datavalueformat'), $this->locale)) { if(is_string($val) && Zend_Date::isDate($val, $this->getConfig('datavalueformat'), $this->locale)) {
// split up in date and time string values. $this->value = $val;
$valueObj = new Zend_Date($val, $this->getConfig('datavalueformat'), $this->locale); }
// set date either as array, or as string // Case 2: Array form submission with user date format
elseif(is_array($val) && array_key_exists('date', $val) && array_key_exists('time', $val)) {
$dataTz = date_default_timezone_get();
// If timezones are enabled, assume user data needs to be converted to server timezone
if($userTz) date_default_timezone_set($userTz);
// Uses sub-fields to temporarily write values and delegate dealing with their normalization,
// actual sub-field value setting happens later
$this->dateField->setValue($val['date']);
$this->timeField->setValue($val['time']);
if($this->dateField->dataValue() && $this->timeField->dataValue()) {
$userValueObj = new Zend_Date(null, null, $this->locale);
$userValueObj->setDate($this->dateField->dataValue(), $this->dateField->getConfig('datavalueformat'));
$userValueObj->setTime($this->timeField->dataValue(), $this->timeField->getConfig('datavalueformat'));
if($userTz) $userValueObj->setTimezone($dataTz);
$this->value = $userValueObj->get($this->getConfig('datavalueformat'), $this->locale);
unset($userValueObj);
} else {
// Validation happens later, so set the raw string in case Zend_Date doesn't accept it
$this->value = $val['date'] . ' ' . $val['time'];
}
if($userTz) date_default_timezone_set($dataTz);
}
// Case 3: Value is invalid, but set it anyway to allow validation by the fields later on
else {
$this->dateField->setValue($val);
if(is_string($val) )$this->timeField->setValue($val);
$this->value = $val;
}
// view settings (dates might differ from $this->value based on user timezone settings)
if (Zend_Date::isDate($this->value, $this->getConfig('datavalueformat'), $this->locale)) {
$valueObj = new Zend_Date($this->value, $this->getConfig('datavalueformat'), $this->locale);
if($userTz) $valueObj->setTimezone($userTz);
// Set view values in sub-fields
if($this->dateField->getConfig('dmyfields')) { if($this->dateField->getConfig('dmyfields')) {
$this->dateField->setValue($valueObj->toArray()); $this->dateField->setValue($valueObj->toArray());
} else { } else {
$this->dateField->setValue($valueObj->get($this->dateField->getConfig('dateformat'), $this->locale)); $this->dateField->setValue($valueObj->get($this->dateField->getConfig('dateformat'), $this->locale));
} }
// set time
$this->timeField->setValue($valueObj->get($this->timeField->getConfig('timeformat'), $this->locale)); $this->timeField->setValue($valueObj->get($this->timeField->getConfig('timeformat'), $this->locale));
} }
// Setting from form submission
elseif(is_array($val) && array_key_exists('date', $val) && array_key_exists('time', $val)) {
$this->dateField->setValue($val['date']);
$this->timeField->setValue($val['time']);
} else {
$this->dateField->setValue($val);
$this->timeField->setValue($val);
}
} }
} }
function dataValue() { function Value() {
$valDate = $this->dateField->dataValue(); $valDate = $this->dateField->Value();
$valTime = $this->timeField->dataValue(); $valTime = $this->timeField->Value();
// Only date is actually required, time is optional
if($valDate) {
if(!$valTime) $valTime = '00:00:00'; if(!$valTime) $valTime = '00:00:00';
return $valDate . ' ' . $valTime; return $valDate . ' ' . $valTime;
} else {
// TODO
return null;
}
} }
/** /**
@ -142,6 +194,11 @@ class DatetimeField extends FormField {
*/ */
function setConfig($name, $val) { function setConfig($name, $val) {
$this->config[$name] = $val; $this->config[$name] = $val;
if($name == 'usertimezone') {
$this->timezoneField->setValue($val);
$this->setValue($this->dataValue());
}
} }
/** /**

View File

@ -118,10 +118,14 @@ class TimeField extends TextField {
$this->value = $this->valueObj->get($this->getConfig('timeformat')); $this->value = $this->valueObj->get($this->getConfig('timeformat'));
} }
// Fallback: Set incorrect value so validate() can pick it up // Fallback: Set incorrect value so validate() can pick it up
else { elseif(is_string($val)) {
$this->value = $val; $this->value = $val;
$this->valueObj = null; $this->valueObj = null;
} }
else {
$this->value = null;
$this->valueObj = null;
}
} }
/** /**

View File

@ -44,12 +44,6 @@ class DatetimeFieldTest extends SapphireTest {
$f = new DatetimeField('Datetime', null, '2003-03-29 23:59:38'); $f = new DatetimeField('Datetime', null, '2003-03-29 23:59:38');
$this->assertEquals('2003-03-29 23:59:38', $f->dataValue(), 'From date/time string'); $this->assertEquals('2003-03-29 23:59:38', $f->dataValue(), 'From date/time string');
$f = new DatetimeField('Datetime', null, '2003-03-29');
$this->assertEquals('2003-03-29 00:00:00', $f->dataValue(), 'From date string (no time)');
$f = new DatetimeField('Datetime', null, array('date' => '2003-03-29', 'time' => null));
$this->assertEquals('2003-03-29 00:00:00', $f->dataValue(), 'From date array (no time)');
} }
function testConstructorWithoutArgs() { function testConstructorWithoutArgs() {
@ -108,9 +102,66 @@ class DatetimeFieldTest extends SapphireTest {
$f = new DatetimeField('Datetime', 'Datetime', '2003-03-29 23:59:38'); $f = new DatetimeField('Datetime', 'Datetime', '2003-03-29 23:59:38');
$this->assertTrue($f->validate(new RequiredFields())); $this->assertTrue($f->validate(new RequiredFields()));
$f = new DatetimeField('Datetime', 'Datetime', '2003-03-29');
$this->assertTrue($f->validate(new RequiredFields()));
$f = new DatetimeField('Datetime', 'Datetime', 'wrong'); $f = new DatetimeField('Datetime', 'Datetime', 'wrong');
$this->assertFalse($f->validate(new RequiredFields())); $this->assertFalse($f->validate(new RequiredFields()));
} }
function testTimezone() {
$oldTz = date_default_timezone_get();
date_default_timezone_set('Europe/Berlin');
// Berlin and Auckland have 12h time difference in northern hemisphere winter
$f = new DatetimeField('Datetime', 'Datetime', '2003-12-24 23:59:59');
$f->setConfig('usertimezone', 'Pacific/Auckland');
$this->assertEquals('25/12/2003 11:59:59', $f->Value(), 'User value is formatted, and in user timezone');
$this->assertEquals('25/12/2003', $f->getDateField()->Value());
$this->assertEquals('11:59:59', $f->getTimeField()->Value());
$this->assertEquals('2003-12-24 23:59:59', $f->dataValue(), 'Data value is unformatted, and in server timezone');
date_default_timezone_set($oldTz);
}
function testTimezoneFromFormSubmission() {
$oldTz = date_default_timezone_get();
date_default_timezone_set('Europe/Berlin');
// Berlin and Auckland have 12h time difference in northern hemisphere summer, but Berlin and Moscow only 2h.
$f = new DatetimeField('Datetime', 'Datetime');
$f->setConfig('usertimezone', 'Pacific/Auckland'); // should be overridden by form submission
$f->setValue(array(
// pass in default format, at user time (Moscow)
'date' => '24/06/2003',
'time' => '23:59:59',
'timezone' => 'Europe/Moscow'
));
$this->assertEquals('24/06/2003 23:59:59', $f->Value(), 'View composite value matches user timezone');
$this->assertEquals('24/06/2003', $f->getDateField()->Value(), 'View date part matches user timezone');
$this->assertEquals('23:59:59', $f->getTimeField()->Value(), 'View time part matches user timezone');
// 2h difference to Moscow
$this->assertEquals('2003-06-24 21:59:59', $f->dataValue(), 'Data value matches server timezone');
date_default_timezone_set($oldTz);
}
function testTimezoneFromConfig() {
$oldTz = date_default_timezone_get();
date_default_timezone_set('Europe/Berlin');
// Berlin and Auckland have 12h time difference in northern hemisphere summer, but Berlin and Moscow only 2h.
$f = new DatetimeField('Datetime', 'Datetime');
$f->setConfig('usertimezone', 'Europe/Moscow');
$f->setValue(array(
// pass in default format, at user time (Moscow)
'date' => '24/06/2003',
'time' => '23:59:59',
));
$this->assertEquals('2003-06-24 21:59:59', $f->dataValue(), 'Data value matches server timezone');
date_default_timezone_set($oldTz);
}
} }
/** /**