mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
Store $value in ISO and server timezone consistently, fix min/max timezone handling
This commit is contained in:
parent
628fd216ad
commit
60706c8efd
@ -12,6 +12,7 @@ use SilverStripe\ORM\FieldType\DBDatetime;
|
||||
* In the default HTML5 mode, the field expects form submissions
|
||||
* in normalised ISO 8601 format, for example 2017-04-26T23:59:59 (with a "T" separator).
|
||||
* Data is passed on via {@link dataValue()} with whitespace separators.
|
||||
* The {@link $value} property is always in ISO 8601 format, in the server timezone.
|
||||
*/
|
||||
class DatetimeField extends TextField
|
||||
{
|
||||
@ -31,14 +32,14 @@ class DatetimeField extends TextField
|
||||
/**
|
||||
* Min date time
|
||||
*
|
||||
* @var string ISO 8601 date time for min date time
|
||||
* @var string ISO 8601 date time in server timezone
|
||||
*/
|
||||
protected $minDatetime = null;
|
||||
|
||||
/**
|
||||
* Max date time
|
||||
*
|
||||
* @var string ISO 860 date time for max date time
|
||||
* @var string ISO 860 date time in server timezone
|
||||
*/
|
||||
protected $maxDatetime = null;
|
||||
|
||||
@ -100,8 +101,8 @@ class DatetimeField extends TextField
|
||||
|
||||
if ($this->getHTML5()) {
|
||||
$attributes['type'] = 'datetime-local';
|
||||
$attributes['min'] = $this->getMinDatetime();
|
||||
$attributes['max'] = $this->getMaxDatetime();
|
||||
$attributes['min'] = $this->internalToFrontend($this->getMinDatetime());
|
||||
$attributes['max'] = $this->internalToFrontend($this->getMaxDatetime());
|
||||
}
|
||||
|
||||
return $attributes;
|
||||
@ -117,8 +118,8 @@ class DatetimeField extends TextField
|
||||
'lang' => i18n::convert_rfc1766($this->getLocale()),
|
||||
'data' => array_merge($defaults['data'], [
|
||||
'html5' => $this->getHTML5(),
|
||||
'min' => $this->getMinDate(),
|
||||
'max' => $this->getMaxDate()
|
||||
'min' => $this->internalToFrontend($this->getMinDatetime()),
|
||||
'max' => $this->internalToFrontend($this->getMaxDatetime())
|
||||
])
|
||||
]);
|
||||
}
|
||||
@ -177,6 +178,8 @@ class DatetimeField extends TextField
|
||||
/**
|
||||
* Convert frontend date to the internal representation (ISO 8601).
|
||||
* The frontend date is also in ISO 8601 when $html5=true.
|
||||
* Assumes the value is in the defined {@link $timezone} (if one is set),
|
||||
* and adjusts for server timezone.
|
||||
*
|
||||
* @param string $datetime
|
||||
* @return string The formatted date, or null if not a valid date
|
||||
@ -284,59 +287,35 @@ class DatetimeField extends TextField
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get formatter for converting to the target timezone, if timezone is set
|
||||
* Can return null if no timezone set
|
||||
*
|
||||
* @return IntlDateFormatter|null
|
||||
*/
|
||||
protected function getTimezoneFormatter()
|
||||
{
|
||||
$timezone = $this->getTimezone();
|
||||
if (!$timezone) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Build new formatter with the altered timezone
|
||||
$formatter = clone $this->getInternalFormatter();
|
||||
$formatter->setTimeZone($timezone);
|
||||
|
||||
// ISO8601 date with a standard "T" separator (W3C standard).
|
||||
// Note we omit timezone from this format, and we assume server TZ always.
|
||||
$formatter->setPattern(DBDatetime::ISO_DATETIME_NORMALISED);
|
||||
|
||||
return $formatter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a date formatter for the ISO 8601 format
|
||||
*
|
||||
* @param String $timezone Optional timezone identifier (defaults to server timezone)
|
||||
* @return IntlDateFormatter
|
||||
*/
|
||||
protected function getInternalFormatter()
|
||||
protected function getInternalFormatter($timezone = null)
|
||||
{
|
||||
if (!$timezone) {
|
||||
$timezone = date_default_timezone_get(); // Default to server timezone
|
||||
}
|
||||
|
||||
$formatter = IntlDateFormatter::create(
|
||||
i18n::config()->uninherited('default_locale'),
|
||||
IntlDateFormatter::MEDIUM,
|
||||
IntlDateFormatter::MEDIUM,
|
||||
date_default_timezone_get() // Default to server timezone
|
||||
$timezone
|
||||
);
|
||||
$formatter->setLenient(false);
|
||||
|
||||
// Note we omit timezone from this format, and we always assume server TZ
|
||||
if ($this->getHTML5()) {
|
||||
// ISO8601 date with a standard "T" separator (W3C standard).
|
||||
$formatter->setPattern(DBDatetime::ISO_DATETIME_NORMALISED);
|
||||
} else {
|
||||
// ISO8601 date with a whitespace separator
|
||||
$formatter->setPattern(DBDatetime::ISO_DATETIME);
|
||||
}
|
||||
|
||||
return $formatter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign value based on {@link $datetimeFormat}, which might be localised.
|
||||
* The value needs to be in the server timezone.
|
||||
*
|
||||
* When $html5=true, assign value from ISO 8601 normalised string (with a "T" separator).
|
||||
* Falls back to an ISO 8601 string (with a whitespace separator).
|
||||
@ -393,41 +372,6 @@ class DatetimeField extends TextField
|
||||
return $this->internalToFrontend($this->value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the field value in the internal representation (ISO 8601),
|
||||
* suitable for insertion into the data object.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function dataValue()
|
||||
{
|
||||
return $this->internalToDataValue($this->value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an ISO 8601 localised datetime into a saveable representation.
|
||||
*
|
||||
* @param string $datetime
|
||||
* @return string The formatted date and time, or null if not a valid date and time
|
||||
*/
|
||||
public function internalToDataValue($datetime)
|
||||
{
|
||||
$fromFormatter = $this->getInternalFormatter();
|
||||
$toFormatter = $this->getFrontendFormatter();
|
||||
|
||||
// Set default timezone (avoid shifting data values into user timezone)
|
||||
$toFormatter->setTimezone(date_default_timezone_get());
|
||||
|
||||
// Send to data object without "T" separator
|
||||
$toFormatter->setPattern(DBDatetime::ISO_DATETIME);
|
||||
|
||||
$timestamp = $fromFormatter->parse($datetime);
|
||||
if ($timestamp === false) {
|
||||
return null;
|
||||
}
|
||||
return $toFormatter->format($timestamp) ?: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the internal date representation (ISO 8601) to a format used by the frontend,
|
||||
* as defined by {@link $dateFormat}. With $html5=true, the frontend date will also be
|
||||
@ -580,7 +524,7 @@ class DatetimeField extends TextField
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
* @return string Date in ISO 8601 format, in server timezone.
|
||||
*/
|
||||
public function getMinDatetime()
|
||||
{
|
||||
@ -588,7 +532,7 @@ class DatetimeField extends TextField
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $minDatetime A string in ISO 8601 format
|
||||
* @param string $minDatetime A string in ISO 8601 format, in server timezone.
|
||||
* @return $this
|
||||
*/
|
||||
public function setMinDatetime($minDatetime)
|
||||
@ -598,7 +542,7 @@ class DatetimeField extends TextField
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
* @return string Date in ISO 8601 format, in server timezone.
|
||||
*/
|
||||
public function getMaxDatetime()
|
||||
{
|
||||
@ -606,7 +550,7 @@ class DatetimeField extends TextField
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $maxDatetime
|
||||
* @param string $maxDatetime A string in ISO 8601 format, in server timezone.
|
||||
* @return $this
|
||||
*/
|
||||
public function setMaxDatetime($maxDatetime)
|
||||
@ -639,7 +583,7 @@ class DatetimeField extends TextField
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check min date
|
||||
// Check min date (in server timezone)
|
||||
$min = $this->getMinDatetime();
|
||||
if ($min) {
|
||||
$oops = strtotime($this->value) < strtotime($min);
|
||||
@ -656,7 +600,7 @@ class DatetimeField extends TextField
|
||||
}
|
||||
}
|
||||
|
||||
// Check max date
|
||||
// Check max date (in server timezone)
|
||||
$max = $this->getMaxDatetime();
|
||||
if ($max) {
|
||||
$oops = strtotime($this->value) > strtotime($max);
|
||||
@ -700,9 +644,9 @@ class DatetimeField extends TextField
|
||||
if ($this->value && $timezone !== $this->timezone) {
|
||||
throw new \BadMethodCallException("Can't change timezone after setting a value");
|
||||
}
|
||||
// Note: DateField has no timezone option, and TimeField::setTimezone
|
||||
// should be ignored
|
||||
|
||||
$this->timezone = $timezone;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
@ -74,6 +74,17 @@ class DatetimeFieldTest extends SapphireTest
|
||||
$this->assertEquals('2003-03-29 23:59:38', $f->dataValue(), 'From date/time string');
|
||||
}
|
||||
|
||||
public function testDataValueWithTimezone()
|
||||
{
|
||||
// Berlin and Auckland have 12h time difference in northern hemisphere winter
|
||||
date_default_timezone_set('Europe/Berlin');
|
||||
|
||||
$f = new DatetimeField('Datetime');
|
||||
$f->setTimezone('Pacific/Auckland');
|
||||
$f->setSubmittedValue('2003-01-30T23:59:38'); // frontend timezone (Auckland)
|
||||
$this->assertEquals('2003-01-30 11:59:38', $f->dataValue()); // server timezone (Berlin)
|
||||
}
|
||||
|
||||
public function testConstructorWithoutArgs()
|
||||
{
|
||||
$f = new DatetimeField('Datetime');
|
||||
@ -97,11 +108,11 @@ class DatetimeFieldTest extends SapphireTest
|
||||
{
|
||||
$f = new DatetimeField('Datetime', 'Datetime');
|
||||
$f->setValue('2003-03-29 23:59:38');
|
||||
$this->assertEquals($f->dataValue(), '2003-03-29 23:59:38');
|
||||
$this->assertEquals($f->dataValue(), '2003-03-29 23:59:38', 'Accepts ISO');
|
||||
|
||||
$f = new DatetimeField('Datetime', 'Datetime');
|
||||
$f->setValue('2003-03-29T23:59:38');
|
||||
$this->assertEquals($f->dataValue(), '2003-03-29 23:59:38', 'Normalised ISO');
|
||||
$this->assertNull($f->dataValue(), 'Rejects normalised ISO');
|
||||
}
|
||||
|
||||
public function testSubmittedValue()
|
||||
@ -140,13 +151,13 @@ class DatetimeFieldTest extends SapphireTest
|
||||
$this->assertTrue($f->validate(new RequiredFields()));
|
||||
|
||||
$f = new DatetimeField('Datetime', 'Datetime', '2003-03-29T23:59:38');
|
||||
$this->assertTrue($f->validate(new RequiredFields()), 'Normalised ISO');
|
||||
$this->assertFalse($f->validate(new RequiredFields()), 'Normalised ISO');
|
||||
|
||||
$f = new DatetimeField('Datetime', 'Datetime', '2003-03-29 00:00:00');
|
||||
$this->assertTrue($f->validate(new RequiredFields()));
|
||||
$f = new DatetimeField('Datetime', 'Datetime', '2003-03-29');
|
||||
$this->assertFalse($f->validate(new RequiredFields()), 'Leaving out time');
|
||||
|
||||
$f = (new DatetimeField('Datetime', 'Datetime'))
|
||||
->setSubmittedValue('2003-03-29 00:00');
|
||||
->setSubmittedValue('2003-03-29T00:00');
|
||||
$this->assertTrue($f->validate(new RequiredFields()), 'Leaving out seconds (like many browsers)');
|
||||
|
||||
$f = new DatetimeField('Datetime', 'Datetime', 'wrong');
|
||||
@ -156,10 +167,10 @@ class DatetimeFieldTest extends SapphireTest
|
||||
public function testSetMinDate()
|
||||
{
|
||||
$f = (new DatetimeField('Datetime'))->setMinDatetime('2009-03-31T23:00:00');
|
||||
$this->assertEquals($f->getMinDatetime(), '2009-03-31T23:00:00', 'Retains normalised ISO');
|
||||
$this->assertEquals($f->getMinDatetime(), '2009-03-31 23:00:00', 'Retains ISO');
|
||||
|
||||
$f = (new DatetimeField('Datetime'))->setMinDatetime('2009-03-31 23:00:00');
|
||||
$this->assertEquals($f->getMinDatetime(), '2009-03-31T23:00:00', 'Converts ISO to normalised ISO');
|
||||
$this->assertEquals($f->getMinDatetime(), '2009-03-31 23:00:00', 'Converts normalised ISO to ISO');
|
||||
|
||||
$f = (new DatetimeField('Datetime'))->setMinDatetime('invalid');
|
||||
$this->assertNull($f->getMinDatetime(), 'Ignores invalid values');
|
||||
@ -168,10 +179,10 @@ class DatetimeFieldTest extends SapphireTest
|
||||
public function testSetMaxDate()
|
||||
{
|
||||
$f = (new DatetimeField('Datetime'))->setMaxDatetime('2009-03-31T23:00:00');
|
||||
$this->assertEquals($f->getMaxDatetime(), '2009-03-31T23:00:00', 'Retains normalised ISO');
|
||||
$this->assertEquals($f->getMaxDatetime(), '2009-03-31 23:00:00', 'Retains ISO');
|
||||
|
||||
$f = (new DatetimeField('Datetime'))->setMaxDatetime('2009-03-31 23:00:00');
|
||||
$this->assertEquals($f->getMaxDatetime(), '2009-03-31T23:00:00', 'Converts ISO to normalised ISO');
|
||||
$this->assertEquals($f->getMaxDatetime(), '2009-03-31 23:00:00', 'Converts normalised ISO to ISO');
|
||||
|
||||
$f = (new DatetimeField('Datetime'))->setMaxDatetime('invalid');
|
||||
$this->assertNull($f->getMaxDatetime(), 'Ignores invalid values');
|
||||
@ -198,11 +209,36 @@ class DatetimeFieldTest extends SapphireTest
|
||||
$dateField->setMinDatetime('2009-03-31 23:00:00');
|
||||
$dateField->setValue('2008-03-31 23:00:00');
|
||||
$this->assertFalse($dateField->validate(new RequiredFields()), 'Date below min datetime');
|
||||
}
|
||||
|
||||
public function testValidateMinDateWithSubmittedValueAndTimezone()
|
||||
{
|
||||
// Berlin and Auckland have 12h time difference in northern hemisphere winter
|
||||
date_default_timezone_set('Europe/Berlin');
|
||||
|
||||
$dateField = new DatetimeField('Datetime');
|
||||
$dateField->setMinDatetime('2009-03-31T23:00:00');
|
||||
$dateField->setValue('2009-03-31T23:00:01');
|
||||
$this->assertTrue($dateField->validate(new RequiredFields()), 'Time above min datetime with normalised ISO');
|
||||
$dateField->setTimezone('Pacific/Auckland');
|
||||
$dateField->setMinDatetime('2009-01-30 23:00:00'); // server timezone (Berlin)
|
||||
$dateField->setSubmittedValue('2009-01-31T11:00:01'); // frontend timezone (Auckland)
|
||||
$this->assertTrue($dateField->validate(new RequiredFields()), 'Time above min datetime');
|
||||
|
||||
$dateField = new DatetimeField('Datetime');
|
||||
$dateField->setTimezone('Pacific/Auckland');
|
||||
$dateField->setMinDatetime('2009-01-30 23:00:00');
|
||||
$dateField->setSubmittedValue('2009-01-31T10:00:00');
|
||||
$this->assertFalse($dateField->validate(new RequiredFields()), 'Time below min datetime');
|
||||
|
||||
$dateField = new DatetimeField('Datetime');
|
||||
$dateField->setTimezone('Pacific/Auckland');
|
||||
$dateField->setMinDatetime('2009-01-30 23:00:00');
|
||||
$dateField->setSubmittedValue('2009-01-31T11:00:00');
|
||||
$this->assertTrue($dateField->validate(new RequiredFields()), 'Date and time matching min datetime');
|
||||
|
||||
$dateField = new DatetimeField('Datetime');
|
||||
$dateField->setTimezone('Pacific/Auckland');
|
||||
$dateField->setMinDatetime('2009-01-30 23:00:00');
|
||||
$dateField->setSubmittedValue('2008-01-31T11:00:00');
|
||||
$this->assertFalse($dateField->validate(new RequiredFields()), 'Date below min datetime');
|
||||
}
|
||||
|
||||
public function testValidateMinDateStrtotime()
|
||||
@ -252,11 +288,36 @@ class DatetimeFieldTest extends SapphireTest
|
||||
$f->setMaxDatetime('2009-03-31 23:00:00');
|
||||
$f->setValue('2010-03-31 23:00:00');
|
||||
$this->assertFalse($f->validate(new RequiredFields()), 'Date above max datetime');
|
||||
}
|
||||
|
||||
public function testValidateMaxDateWithSubmittedValueAndTimezone()
|
||||
{
|
||||
// Berlin and Auckland have 12h time difference in northern hemisphere winter
|
||||
date_default_timezone_set('Europe/Berlin');
|
||||
|
||||
$f = new DatetimeField('Datetime');
|
||||
$f->setMaxDatetime('2009-03-31T23:00:00');
|
||||
$f->setValue('2009-03-31T22:00:00');
|
||||
$this->assertTrue($f->validate(new RequiredFields()), 'Time below max datetime with normalised ISO');
|
||||
$f->setTimezone('Pacific/Auckland');
|
||||
$f->setMaxDatetime('2009-01-31 23:00:00'); // server timezone (Berlin)
|
||||
$f->setSubmittedValue('2009-01-31T10:00:00'); // frontend timezone (Auckland)
|
||||
$this->assertTrue($f->validate(new RequiredFields()), 'Time below max datetime');
|
||||
|
||||
$f = new DatetimeField('Datetime');
|
||||
$f->setTimezone('Pacific/Auckland');
|
||||
$f->setMaxDatetime('2009-01-31 23:00:00');
|
||||
$f->setSubmittedValue('2010-01-31T11:00:01');
|
||||
$this->assertFalse($f->validate(new RequiredFields()), 'Time above max datetime');
|
||||
|
||||
$f = new DatetimeField('Datetime');
|
||||
$f->setTimezone('Pacific/Auckland');
|
||||
$f->setMaxDatetime('2009-01-31 23:00:00');
|
||||
$f->setSubmittedValue('2009-01-31T11:00:00');
|
||||
$this->assertTrue($f->validate(new RequiredFields()), 'Date and time matching max datetime');
|
||||
|
||||
$f = new DatetimeField('Datetime');
|
||||
$f->setTimezone('Pacific/Auckland');
|
||||
$f->setMaxDatetime('2009-01-31 23:00:00');
|
||||
$f->setSubmittedValue('2010-01-31T11:00:00');
|
||||
$this->assertFalse($f->validate(new RequiredFields()), 'Date above max datetime');
|
||||
}
|
||||
|
||||
public function testTimezoneSetValueLocalised()
|
||||
@ -336,6 +397,54 @@ class DatetimeFieldTest extends SapphireTest
|
||||
$this->assertEquals('CustomDatetime', $field->getName());
|
||||
}
|
||||
|
||||
public function testSchemaDataDefaultsIncludesMinMax()
|
||||
{
|
||||
$field = new DatetimeField('Datetime');
|
||||
$field->setMinDatetime('2009-03-31 23:00:00');
|
||||
$field->setMaxDatetime('2010-03-31 23:00:00');
|
||||
$defaults = $field->getSchemaDataDefaults();
|
||||
$this->assertEquals($defaults['data']['min'], '2009-03-31T23:00:00');
|
||||
$this->assertEquals($defaults['data']['max'], '2010-03-31T23:00:00');
|
||||
}
|
||||
|
||||
public function testSchemaDataDefaultsAdjustsMinMaxToTimezone()
|
||||
{
|
||||
date_default_timezone_set('Europe/Berlin');
|
||||
// Berlin and Auckland have 12h time difference in northern hemisphere summer, but Berlin and Moscow only 2h.
|
||||
|
||||
$field = new DatetimeField('Datetime');
|
||||
$field->setTimezone('Pacific/Auckland');
|
||||
$field->setMinDatetime('2009-01-31 11:00:00'); // server timezone
|
||||
$field->setMaxDatetime('2010-01-31 11:00:00'); // server timezone
|
||||
$defaults = $field->getSchemaDataDefaults();
|
||||
$this->assertEquals($defaults['data']['min'], '2009-01-31T23:00:00'); // frontend timezone
|
||||
$this->assertEquals($defaults['data']['max'], '2010-01-31T23:00:00'); // frontend timezone
|
||||
}
|
||||
|
||||
public function testAttributesIncludesMinMax()
|
||||
{
|
||||
$field = new DatetimeField('Datetime');
|
||||
$field->setMinDatetime('2009-03-31 23:00:00');
|
||||
$field->setMaxDatetime('2010-03-31 23:00:00');
|
||||
$attrs = $field->getAttributes();
|
||||
$this->assertEquals($attrs['min'], '2009-03-31T23:00:00');
|
||||
$this->assertEquals($attrs['max'], '2010-03-31T23:00:00');
|
||||
}
|
||||
|
||||
public function testAttributesAdjustsMinMaxToTimezone()
|
||||
{
|
||||
date_default_timezone_set('Europe/Berlin');
|
||||
// Berlin and Auckland have 12h time difference in northern hemisphere summer, but Berlin and Moscow only 2h.
|
||||
|
||||
$field = new DatetimeField('Datetime');
|
||||
$field->setTimezone('Pacific/Auckland');
|
||||
$field->setMinDatetime('2009-01-31 11:00:00'); // server timezone
|
||||
$field->setMaxDatetime('2010-01-31 11:00:00'); // server timezone
|
||||
$attrs = $field->getAttributes();
|
||||
$this->assertEquals($attrs['min'], '2009-01-31T23:00:00'); // frontend timezone
|
||||
$this->assertEquals($attrs['max'], '2010-01-31T23:00:00'); // frontend timezone
|
||||
}
|
||||
|
||||
protected function getMockForm()
|
||||
{
|
||||
/** @skipUpgrade */
|
||||
|
Loading…
Reference in New Issue
Block a user