From 81a21f68cd6fd302efd0677b25964fa46cba8fcd Mon Sep 17 00:00:00 2001 From: Saophalkun Ponlu Date: Thu, 6 Apr 2017 17:23:52 +1200 Subject: [PATCH 01/26] Add 'lang' attribute to front-end date field schema --- src/Forms/DateField.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Forms/DateField.php b/src/Forms/DateField.php index b3da3be06..a20614356 100644 --- a/src/Forms/DateField.php +++ b/src/Forms/DateField.php @@ -290,6 +290,12 @@ class DateField extends TextField return $attributes; } + public function getSchemaDataDefaults() + { + $defaults = parent::getSchemaDataDefaults(); + return array_merge($defaults, [ 'lang' => i18n::convert_rfc1766($this->getLocale()) ]); + } + public function Type() { return 'date text'; From 9f8fe88eea52aeb7f649f2c0e253992ab2cd0d44 Mon Sep 17 00:00:00 2001 From: Saophalkun Ponlu Date: Wed, 12 Apr 2017 17:11:30 +1200 Subject: [PATCH 02/26] Refactor DateTimeField not to use DateField and TimeField --- src/Forms/DatetimeField.php | 516 ++++++++++++------ src/ORM/FieldType/DBDatetime.php | 2 +- templates/SilverStripe/Forms/DatetimeField.ss | 7 - 3 files changed, 347 insertions(+), 178 deletions(-) delete mode 100644 templates/SilverStripe/Forms/DatetimeField.ss diff --git a/src/Forms/DatetimeField.php b/src/Forms/DatetimeField.php index 0ae8b65c6..7cd183d49 100644 --- a/src/Forms/DatetimeField.php +++ b/src/Forms/DatetimeField.php @@ -5,49 +5,77 @@ namespace SilverStripe\Forms; use IntlDateFormatter; use InvalidArgumentException; use SilverStripe\i18n\i18n; +use SilverStripe\ORM\FieldType\DBDatetime; /** - * A composite field for date and time entry, - * based on {@link DateField} and {@link TimeField}. - * Usually saves into a single {@link DBDateTime} database column. - * If you want to save into {@link Date} or {@link Time} columns, - * please instanciate the fields separately. - * - * This field does not implement the HTML5 field, - * but can use date and time HTML5 inputs separately (through {@link DateField->setHTML5()} - * and {@link TimeField->setHTML5()}. + * Form field used for editing date time string * * # Configuration * - * Individual options are configured either on the DatetimeField, or on individual - * sub-fields accessed via getDateField() or getTimeField() - * - * Example: - * - * $field = new DatetimeField('Name', 'Label'); - * $field->getDateField()->setTitle('Select Date'); - * - * - * - setLocale(): Sets a custom locale for date / time formatting. - * - setTimezone(): Set a different timezone for viewing. {@link dataValue()} will still save + * - "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. - * - setDateTimeOrder(): An sprintf() template to determine in which order the date and time values will + * - "datetimeorder": An sprintf() template to determine in which order the date and time values will * be combined. This is necessary as those separate formats are set in their invididual fields. */ -class DatetimeField extends FormField +class DatetimeField extends TextField { /** - * @var DateField + * @var bool */ - protected $dateField = null; + protected $html5 = true; /** - * @var TimeField + * Override locale. If empty will default to current locale + * + * @var string */ - protected $timeField = null; + protected $locale = null; + + /** + * Min date time + * + * @var string ISO 8601 date time for min date time + */ + protected $minDatetime = null; + + /** + * Max date time + * + * @var string ISO 860 date time for max date time + */ + protected $maxDatetime = null; + + /** + * Override date format. If empty will default to that used by the current locale. + * + * @var null + */ + protected $datetimeFormat = null; + + /** + * Length of this date (full, short, etc). + * + * @see http://php.net/manual/en/class.intldateformatter.php#intl.intldateformatter-constants + * @var int + */ + protected $dateLength = null; + + /** + * Length of this time (full, short, etc). + * + * @see http://php.net/manual/en/class.intldateformatter.php#intl.intldateformatter-constants + * @var int + */ + protected $timeLength = null; + + /** + * Unparsed value, used exclusively for comparing with internal value + * to detect invalid values. + * + * @var mixed + */ + protected $rawValue = null; protected $schemaDataType = FormField::SCHEMA_DATA_TYPE_DATETIME; @@ -60,58 +88,124 @@ class DatetimeField extends FormField public function __construct($name, $title = null, $value = "") { - $this->timeField = TimeField::create($name . '[time]', false); - $this->dateField = DateField::create($name . '[date]', false); parent::__construct($name, $title, $value); } public function setForm($form) { parent::setForm($form); - $this->dateField->setForm($form); - $this->timeField->setForm($form); return $this; } public function setName($name) { parent::setName($name); - $this->dateField->setName($name . '[date]'); - $this->timeField->setName($name . '[time]'); + return $this; + } + + public function Type() + { + return 'text datetime'; + } + + public function getHTML5() + { + return $this->html5; + } + + public function setHTML5($bool) + { + $this->html5 = $bool; return $this; } /** - * Sets value from a submitted form array + * Assign value posted from form submission * - * @param array $value Expected submission value is either an empty value, - * or an array with the necessary components keyed against 'date' and 'time', each value - * localised according to each's localisation setting. + * @param mixed $value * @param mixed $data * @return $this */ public function setSubmittedValue($value, $data = null) { - // Empty value - if (empty($value)) { + // Save raw value for later validation + $this->rawValue = $value; + + // Null case + if (!$value) { $this->value = null; - $this->dateField->setValue(null); - $this->timeField->setValue(null); return $this; } - // Validate value is submitted in array format - if (!is_array($value)) { - throw new InvalidArgumentException("Value is not submitted array"); + // Parse from submitted value + $this->value = $this->localisedToISO8601($value); + return $this; + } + + /** + * Convert date localised in the current locale to ISO 8601 date + * + * @param string $date + * @return string The formatted date, or null if not a valid date + */ + public function localisedToISO8601($datetime) + { + if (!$datetime) { + return null; + } + $fromFormatter = $this->getFormatter(); + $toFormatter = $this->getISO8601Formatter(); + $timestamp = $fromFormatter->parse($datetime); + if ($timestamp === false) { + return null; + } + return $toFormatter->format($timestamp) ?: null; + } + + /** + * Get date formatter with the standard locale / date format + * + * @throws \LogicException + * @return IntlDateFormatter + */ + protected function getFormatter() + { + if ($this->getHTML5() && $this->datetimeFormat && $this->datetimeFormat !== DBDatetime::ISO_DATE) { + throw new \LogicException( + 'Please opt-out of HTML5 processing of ISO 8601 dates via setHTML5(false) if using setDateFormat()' + ); } - // Save each field, and convert from array to iso8601 string - $this->dateField->setSubmittedValue($value['date'], $value); - $this->timeField->setSubmittedValue($value['time'], $value); + if ($this->getHTML5() && $this->dateLength) { + throw new \LogicException( + 'Please opt-out of HTML5 processing of ISO 8601 dates via setHTML5(false) if using setDateLength()' + ); + } - // Combine date components back into iso8601 string for the root value - $this->value = $this->dataValue(); - return $this; + if ($this->getHTML5() && $this->locale) { + throw new \LogicException( + 'Please opt-out of HTML5 processing of ISO 8601 dates via setHTML5(false) if using setLocale()' + ); + } + + $formatter = IntlDateFormatter::create( + $this->getLocale(), + $this->getDateLength(), + $this->getTimeLength(), + $this->getTimezone() + ); + + if ($this->getHTML5()) { + // Browsers expect ISO 8601 dates, localisation is handled on the client + $formatter->setPattern(DBDatetime::ISO_DATETIME); + } elseif ($this->datetimeFormat) { + // Don't invoke getDateFormat() directly to avoid infinite loop + $ok = $formatter->setPattern($this->datetimeFormat); + if (!$ok) { + throw new InvalidArgumentException("Invalid date format {$this->datetimeFormat}"); + } + } + return $formatter; } /** @@ -165,8 +259,6 @@ class DatetimeField extends FormField // Empty value if (empty($value)) { $this->value = null; - $this->dateField->setValue(null); - $this->timeField->setValue(null); return $this; } if (is_array($value)) { @@ -178,8 +270,6 @@ class DatetimeField extends FormField $isoFormatter = $this->getISO8601Formatter(); $timestamp = $isoFormatter->parse($value); if ($timestamp === false) { - $this->dateField->setSubmittedValue($value); - $this->timeField->setValue(null); return $this; } @@ -197,135 +287,140 @@ class DatetimeField extends FormField // Set date / time components, which are unaware of their timezone list($date, $time) = explode(' ', $value); - $this->dateField->setValue($date, $data); - $this->timeField->setValue($time, $data); + return $this; + } + + public function Value() + { + return $this->iso8601ToLocalised($this->value); + } + + /** + * Convert an ISO 8601 localised date into the format specified by the + * current date format. + * + * @param string $date + * @return string The formatted date, or null if not a valid date + */ + public function iso8601ToLocalised($datetime) + { + $datetime = $this->tidyISO8601($datetime); + if (!$datetime) { + return null; + } + $fromFormatter = $this->getISO8601Formatter(); + $toFormatter = $this->getFormatter(); + $timestamp = $fromFormatter->parse($datetime); + if ($timestamp === false) { + return null; + } + return $toFormatter->format($timestamp) ?: null; + } + + /** + * Tidy up iso8601-ish date, or approximation + * + * @param string $date Date in iso8601 or approximate form + * @return string iso8601 date, or null if not valid + */ + public function tidyISO8601($datetime) + { + if (!$datetime) { + return null; + } + // Re-run through formatter to tidy up (e.g. remove time component) + $formatter = $this->getISO8601Formatter(); + $timestamp = $formatter->parse($datetime); + if ($timestamp === false) { + // Fallback to strtotime + $timestamp = strtotime($datetime, DBDatetime::now()->getTimestamp()); + if ($timestamp === false) { + return null; + } + } + return $formatter->format($timestamp); + } + + /** + * Get length of the date format to use. One of: + * + * - IntlDateFormatter::SHORT + * - IntlDateFormatter::MEDIUM + * - IntlDateFormatter::LONG + * - IntlDateFormatter::FULL + * + * @see http://php.net/manual/en/class.intldateformatter.php#intl.intldateformatter-constants + * @return int + */ + public function getDateLength() + { + if ($this->dateLength) { + return $this->dateLength; + } + return IntlDateFormatter::MEDIUM; + } + + /** + * Get length of the date format to use. + * Only applicable with {@link setHTML5(false)}. + * + * @see http://php.net/manual/en/class.intldateformatter.php#intl.intldateformatter-constants + * + * @param int $length + * @return $this + */ + public function setDateLength($length) + { + $this->dateLength = $length; return $this; } /** - * localised time value + * Get length of the date format to use. One of: * - * @return string + * - IntlDateFormatter::SHORT + * - IntlDateFormatter::MEDIUM + * - IntlDateFormatter::LONG + * - IntlDateFormatter::FULL + * + * @see http://php.net/manual/en/class.intldateformatter.php#intl.intldateformatter-constants + * @return int */ - public function Value() + public function getTimeLength() { - $date = $this->dateField->Value(); - $time = $this->timeField->Value(); - return $this->joinDateTime($date, $time); + if ($this->timeLength) { + return $this->timeLength; + } + return IntlDateFormatter::MEDIUM; } /** - * @param string $date - * @param string $time - * @return string - */ - protected function joinDateTime($date, $time) - { - $format = $this->getDateTimeOrder(); - return strtr($format, [ - '{date}' => $date, - '{time}' => $time - ]); - } - - /** - * Get ISO8601 formatted string in the local server timezone + * Get length of the date format to use. + * Only applicable with {@link setHTML5(false)}. * - * @return string|null + * @see http://php.net/manual/en/class.intldateformatter.php#intl.intldateformatter-constants + * + * @param int $length + * @return $this */ - public function dataValue() + public function setTimeLength($length) { - // No date means no value (even if time is specified) - $dateDataValue = $this->getDateField()->dataValue(); - if (empty($dateDataValue)) { - return null; - } - - // Build iso8601 timestamp from combined date and time - $timeDataValue = $this->getTimeField()->dataValue() ?: '00:00:00'; - $value = $dateDataValue . ' ' . $timeDataValue; - - // If necessary, convert timezone - $timezoneFormatter = $this->getTimezoneFormatter(); - if ($timezoneFormatter) { - $timestamp = $timezoneFormatter->parse($value); - $isoFormatter = $this->getISO8601Formatter(); - $value = $isoFormatter->format($timestamp); - } - - return $value; + $this->timeLength = $length; + return $this; } public function setDisabled($bool) { parent::setDisabled($bool); - $this->dateField->setDisabled($bool); - $this->timeField->setDisabled($bool); return $this; } public function setReadonly($bool) { parent::setReadonly($bool); - $this->dateField->setReadonly($bool); - $this->timeField->setReadonly($bool); return $this; } - /** - * @return DateField - */ - public function getDateField() - { - return $this->dateField; - } - - /** - * @param FormField $field - */ - public function setDateField($field) - { - $expected = $this->getName() . '[date]'; - if ($field->getName() != $expected) { - throw new InvalidArgumentException(sprintf( - 'Wrong name format for date field: "%s" (expected "%s")', - $field->getName(), - $expected - )); - } - - $field->setForm($this->getForm()); - $field->setValue($this->dateField->dataValue()); - $this->dateField = $field; - } - - /** - * @return TimeField - */ - public function getTimeField() - { - return $this->timeField; - } - - /** - * @param FormField $field - */ - public function setTimeField($field) - { - $expected = $this->getName() . '[time]'; - if ($field->getName() != $expected) { - throw new InvalidArgumentException(sprintf( - 'Wrong name format for time field: "%s" (expected "%s")', - $field->getName(), - $expected - )); - } - - $field->setForm($this->getForm()); - $field->setValue($this->timeField->dataValue()); - $this->timeField = $field; - } - /** * Set default locale for this field. If omitted will default to the current locale. * @@ -334,8 +429,7 @@ class DatetimeField extends FormField */ public function setLocale($locale) { - $this->dateField->setLocale($locale); - $this->timeField->setLocale($locale); + $this->locale = $locale; return $this; } @@ -346,16 +440,104 @@ class DatetimeField extends FormField */ public function getLocale() { - return $this->dateField->getLocale(); + return $this->locale ?: i18n::get_locale(); } + /** + * @return string + */ + public function getMinDatetime() + { + return $this->minDatetime; + } + + /** + * @param string $minDatetime + * @return $this + */ + public function setMinDatetime($minDatetime) + { + $this->minDatetime = $this->tidyISO8601($minDatetime); + return $this; + } + + /** + * @return string + */ + public function getMaxDatetime() + { + return $this->maxDatetime; + } + + /** + * @param string $maxDatetime + * @return $this + */ + public function setMaxDatetime($maxDatetime) + { + $this->maxDatetime = $this->tidyISO8601($maxDatetime); + return $this; + } + + /** + * @param Validator $validator + * @return bool + */ public function validate($validator) { - $dateValid = $this->dateField->validate($validator); - $timeValid = $this->timeField->validate($validator); + // Don't validate empty fields + if (empty($this->rawValue)) { + return true; + } - // Validate if both subfields are valid - return $dateValid && $timeValid; + // We submitted a value, but it couldn't be parsed + if (empty($this->value)) { + $validator->validationError( + $this->name, + _t( + 'DateField.VALIDDATEFORMAT2', + "Please enter a valid date format ({format})", + ['format' => $this->getDateFormat()] + ) + ); + return false; + } + + // Check min date + $min = $this->getMinDatetime(); + if ($min) { + $oops = strtotime($this->value) < strtotime($min); + if ($oops) { + $validator->validationError( + $this->name, + _t( + 'DateField.VALIDDATEMINDATE', + "Your date has to be newer or matching the minimum allowed date ({date})", + ['date' => $this->iso8601ToLocalised($min)] + ) + ); + return false; + } + } + + // Check max date + $max = $this->getMaxDatetime(); + if ($max) { + $oops = strtotime($this->value) > strtotime($max); + if ($oops) { + $validator->validationError( + $this->name, + _t( + 'DateField.VALIDDATEMAXDATE', + "Your date has to be older or matching the maximum allowed date ({date})", + ['date' => $this->iso8601ToLocalised($max)] + ) + ); + return false; + } + } + + return true; } public function performReadonlyTransformation() @@ -365,12 +547,6 @@ class DatetimeField extends FormField return $field; } - public function __clone() - { - $this->dateField = clone $this->dateField; - $this->timeField = clone $this->timeField; - } - /** * @return string */ diff --git a/src/ORM/FieldType/DBDatetime.php b/src/ORM/FieldType/DBDatetime.php index 291b31d46..159e43c8b 100644 --- a/src/ORM/FieldType/DBDatetime.php +++ b/src/ORM/FieldType/DBDatetime.php @@ -37,7 +37,7 @@ class DBDatetime extends DBDate implements TemplateGlobalProvider /** * Standard ISO format string for date and time in CLDR standard format */ - const ISO_DATETIME = 'y-MM-dd HH:mm:ss'; + const ISO_DATETIME = 'y-MM-dd\'T\'HH:mm:ss'; /** * Returns the standard localised date diff --git a/templates/SilverStripe/Forms/DatetimeField.ss b/templates/SilverStripe/Forms/DatetimeField.ss deleted file mode 100644 index 90a7836ea..000000000 --- a/templates/SilverStripe/Forms/DatetimeField.ss +++ /dev/null @@ -1,7 +0,0 @@ -
- $DateField.SmallFieldHolder - $TimeField.SmallFieldHolder - <% if $TimeZone %> - $TimezoneField.Field - <% end_if %> -
From 4a7066294028be2f65ab8ececae18efd441790fa Mon Sep 17 00:00:00 2001 From: Saophalkun Ponlu Date: Wed, 12 Apr 2017 22:39:23 +1200 Subject: [PATCH 03/26] Pass html5 flag to front-end --- src/Forms/DateField.php | 5 ++++- src/Forms/DatetimeField.php | 11 +++++++++++ src/Forms/TimeField.php | 8 ++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/Forms/DateField.php b/src/Forms/DateField.php index a20614356..adff7afb1 100644 --- a/src/Forms/DateField.php +++ b/src/Forms/DateField.php @@ -293,7 +293,10 @@ class DateField extends TextField public function getSchemaDataDefaults() { $defaults = parent::getSchemaDataDefaults(); - return array_merge($defaults, [ 'lang' => i18n::convert_rfc1766($this->getLocale()) ]); + return array_merge($defaults, [ + 'lang' => i18n::convert_rfc1766($this->getLocale()), + 'html5' => $this->getHTML5() + ]); } public function Type() diff --git a/src/Forms/DatetimeField.php b/src/Forms/DatetimeField.php index 7cd183d49..7e91300e8 100644 --- a/src/Forms/DatetimeField.php +++ b/src/Forms/DatetimeField.php @@ -103,6 +103,14 @@ class DatetimeField extends TextField return $this; } + public function getSchemaDataDefaults() + { + $defaults = parent::getSchemaDataDefaults(); + return array_merge($defaults, [ + 'html5' => $this->getHTML5() + ]); + } + public function Type() { return 'text datetime'; @@ -256,6 +264,9 @@ class DatetimeField extends TextField */ public function setValue($value, $data = null) { + // Save raw value for later validation + $this->rawValue = $value; + // Empty value if (empty($value)) { $this->value = null; diff --git a/src/Forms/TimeField.php b/src/Forms/TimeField.php index 94d480236..85c377e17 100644 --- a/src/Forms/TimeField.php +++ b/src/Forms/TimeField.php @@ -231,6 +231,14 @@ class TimeField extends TextField return $attributes; } + public function getSchemaDataDefaults() + { + $defaults = parent::getSchemaDataDefaults(); + return array_merge($defaults, [ + 'html5' => $this->getHTML5() + ]); + } + public function Type() { return 'time text'; From 9d7eef7cf3de62c5b1c50ee74b54c3ac521c3a2b Mon Sep 17 00:00:00 2001 From: Saophalkun Ponlu Date: Thu, 20 Apr 2017 10:54:41 +1200 Subject: [PATCH 04/26] Fix datetime field validation for the refactor --- src/Forms/DatetimeField.php | 59 +++++++++++++++++++++++++++++++++---- 1 file changed, 54 insertions(+), 5 deletions(-) diff --git a/src/Forms/DatetimeField.php b/src/Forms/DatetimeField.php index 7e91300e8..e14473770 100644 --- a/src/Forms/DatetimeField.php +++ b/src/Forms/DatetimeField.php @@ -103,6 +103,21 @@ class DatetimeField extends TextField return $this; } + public function getAttributes() + { + $attributes = parent::getAttributes(); + + $attributes['lang'] = i18n::convert_rfc1766($this->getLocale()); + + if ($this->getHTML5()) { + $attributes['type'] = 'datetime-local'; + $attributes['min'] = $this->getMinDatetime(); + $attributes['max'] = $this->getMaxDatetime(); + } + + return $attributes; + } + public function getSchemaDataDefaults() { $defaults = parent::getSchemaDataDefaults(); @@ -178,9 +193,9 @@ class DatetimeField extends TextField */ protected function getFormatter() { - if ($this->getHTML5() && $this->datetimeFormat && $this->datetimeFormat !== DBDatetime::ISO_DATE) { + if ($this->getHTML5() && $this->datetimeFormat && $this->datetimeFormat !== DBDatetime::ISO_DATETIME) { throw new \LogicException( - 'Please opt-out of HTML5 processing of ISO 8601 dates via setHTML5(false) if using setDateFormat()' + 'Please opt-out of HTML5 processing of ISO 8601 dates via setHTML5(false) if using setDatetimeFormat()' ); } @@ -216,6 +231,43 @@ class DatetimeField extends TextField return $formatter; } + /** + * Get date format in CLDR standard format + * + * This can be set explicitly. If not, this will be generated from the current locale + * with the current date length. + * + * @see http://userguide.icu-project.org/formatparse/datetime#TOC-Date-Field-Symbol-Table + */ + public function getDateFormat() + { + if ($this->getHTML5()) { + // Browsers expect ISO 8601 dates, localisation is handled on the client + $this->setDatetimeFormat(DBDatetime::ISO_DATETIME); + } + + if ($this->datetimeFormat) { + return $this->datetimeFormat; + } + + // Get from locale + return $this->getFormatter()->getPattern(); + } + + /** + * Set date format in CLDR standard format. + * Only applicable with {@link setHTML5(false)}. + * + * @see http://userguide.icu-project.org/formatparse/datetime#TOC-Date-Field-Symbol-Table + * @param string $format + * @return $this + */ + public function setDatetimeFormat($format) + { + $this->datetimeFormat = $format; + return $this; + } + /** * Get formatter for converting to the target timezone, if timezone is set * Can return null if no timezone set @@ -295,9 +347,6 @@ class DatetimeField extends TextField if ($timezoneFormatter) { $value = $timezoneFormatter->format($timestamp); } - - // Set date / time components, which are unaware of their timezone - list($date, $time) = explode(' ', $value); return $this; } From dba1f61f133f22cba1ecd1fe07f4838a61a1486d Mon Sep 17 00:00:00 2001 From: Saophalkun Ponlu Date: Thu, 20 Apr 2017 15:08:44 +1200 Subject: [PATCH 05/26] Fix tests related to date time --- src/Forms/DatetimeField.php | 33 ++------ src/ORM/FieldType/DBDatetime.php | 20 +---- tests/php/Forms/DatetimeFieldTest.php | 117 ++++---------------------- tests/php/Forms/FormSchemaTest.php | 2 + tests/php/ORM/DBDatetimeTest.php | 8 +- 5 files changed, 32 insertions(+), 148 deletions(-) diff --git a/src/Forms/DatetimeField.php b/src/Forms/DatetimeField.php index e14473770..a1db12a64 100644 --- a/src/Forms/DatetimeField.php +++ b/src/Forms/DatetimeField.php @@ -10,12 +10,14 @@ use SilverStripe\ORM\FieldType\DBDatetime; /** * Form field used for editing date time string * + * # Localization + * + * See {@link DateField} + * * # Configuration * * - "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. - * - "datetimeorder": An sprintf() template to determine in which order the date and time values will - * be combined. This is necessary as those separate formats are set in their invididual fields. */ class DatetimeField extends TextField { @@ -162,6 +164,7 @@ class DatetimeField extends TextField // Parse from submitted value $this->value = $this->localisedToISO8601($value); + return $this; } @@ -222,7 +225,7 @@ class DatetimeField extends TextField // Browsers expect ISO 8601 dates, localisation is handled on the client $formatter->setPattern(DBDatetime::ISO_DATETIME); } elseif ($this->datetimeFormat) { - // Don't invoke getDateFormat() directly to avoid infinite loop + // Don't invoke getDatetimeFormat() directly to avoid infinite loop $ok = $formatter->setPattern($this->datetimeFormat); if (!$ok) { throw new InvalidArgumentException("Invalid date format {$this->datetimeFormat}"); @@ -239,7 +242,7 @@ class DatetimeField extends TextField * * @see http://userguide.icu-project.org/formatparse/datetime#TOC-Date-Field-Symbol-Table */ - public function getDateFormat() + public function getDatetimeFormat() { if ($this->getHTML5()) { // Browsers expect ISO 8601 dates, localisation is handled on the client @@ -557,7 +560,7 @@ class DatetimeField extends TextField _t( 'DateField.VALIDDATEFORMAT2', "Please enter a valid date format ({format})", - ['format' => $this->getDateFormat()] + ['format' => $this->getDatetimeFormat()] ) ); return false; @@ -636,24 +639,4 @@ class DatetimeField extends TextField $this->timezone = $timezone; return $this; } - - /** - * @return string - */ - public function getDateTimeOrder() - { - return $this->dateTimeOrder; - } - - /** - * Set date time order format string. Use {date} and {time} as placeholders. - * - * @param string $dateTimeOrder - * @return $this - */ - public function setDateTimeOrder($dateTimeOrder) - { - $this->dateTimeOrder = $dateTimeOrder; - return $this; - } } diff --git a/src/ORM/FieldType/DBDatetime.php b/src/ORM/FieldType/DBDatetime.php index 159e43c8b..1630c1ebc 100644 --- a/src/ORM/FieldType/DBDatetime.php +++ b/src/ORM/FieldType/DBDatetime.php @@ -132,32 +132,18 @@ class DBDatetime extends DBDate implements TemplateGlobalProvider public function scaffoldFormField($title = null, $params = null) { $field = DatetimeField::create($this->name, $title); - $dateFormat = $field->getDateField()->getDateFormat(); - $timeFormat = $field->getTimeField()->getTimeFormat(); + $dateTimeFormat = $field->getDatetimeFormat(); // Set date formatting hints and example - $date = static::now()->Format($dateFormat); + $date = static::now()->Format($dateTimeFormat); $field - ->getDateField() ->setDescription(_t( 'FormField.EXAMPLE', 'e.g. {format}', 'Example format', [ 'format' => $date ] )) - ->setAttribute('placeholder', $dateFormat); - - // Set time formatting hints and example - $time = static::now()->Format($timeFormat); - $field - ->getTimeField() - ->setDescription(_t( - 'FormField.EXAMPLE', - 'e.g. {format}', - 'Example format', - [ 'format' => $time ] - )) - ->setAttribute('placeholder', $timeFormat); + ->setAttribute('placeholder', $dateTimeFormat); return $field; } diff --git a/tests/php/Forms/DatetimeFieldTest.php b/tests/php/Forms/DatetimeFieldTest.php index f0736f44a..02dccca14 100644 --- a/tests/php/Forms/DatetimeFieldTest.php +++ b/tests/php/Forms/DatetimeFieldTest.php @@ -37,26 +37,18 @@ class DatetimeFieldTest extends SapphireTest $form = $this->getMockForm(); $form->Fields()->push($dateTimeField); - $dateTimeField->setSubmittedValue([ - 'date' => '2003-03-29', - 'time' => '23:59:38' - ]); + $dateTimeField->setSubmittedValue('2003-03-29T23:59:38'); $validator = new RequiredFields(); $this->assertTrue($dateTimeField->validate($validator)); $m = new Model(); $form->saveInto($m); - $this->assertEquals('2003-03-29 23:59:38', $m->MyDatetime); + $this->assertEquals('2003-03-29T23:59:38', $m->MyDatetime); } public function testFormSaveIntoLocalised() { $dateTimeField = new DatetimeField('MyDatetime'); - - $dateTimeField->getDateField() - ->setHTML5(false) - ->setLocale('en_NZ'); - - $dateTimeField->getTimeField() + $dateTimeField ->setHTML5(false) ->setLocale('en_NZ'); @@ -64,15 +56,12 @@ class DatetimeFieldTest extends SapphireTest $form->Fields()->push($dateTimeField); // en_NZ standard format - $dateTimeField->setSubmittedValue([ - 'date' => '29/03/2003', - 'time' => '11:59:38 pm' - ]); + $dateTimeField->setSubmittedValue('29/03/2003 11:59:38 pm'); $validator = new RequiredFields(); $this->assertTrue($dateTimeField->validate($validator)); $m = new Model(); $form->saveInto($m); - $this->assertEquals('2003-03-29 23:59:38', $m->MyDatetime); + $this->assertEquals('2003-03-29T23:59:38', $m->MyDatetime); } public function testDataValue() @@ -119,33 +108,23 @@ class DatetimeFieldTest extends SapphireTest $this->assertEquals($f->dataValue(), '2003-03-29 23:59:38'); } - public function testSetValueWithArray() + public function testSetValue() { $datetimeField = new DatetimeField('Datetime', 'Datetime'); - $datetimeField->setSubmittedValue([ - 'date' => '2003-03-29', - 'time' => '23:00:00' - ]); + $datetimeField->setSubmittedValue('2003-03-29 23:00:00'); $this->assertEquals($datetimeField->dataValue(), '2003-03-29 23:00:00'); } - public function testSetValueWithArrayLocalised() + public function testSetValueWithLocalised() { $datetimeField = new DatetimeField('Datetime', 'Datetime'); - $datetimeField->getDateField() - ->setHTML5(false) - ->setLocale('en_NZ'); - - $datetimeField->getTimeField() + $datetimeField ->setHTML5(false) ->setLocale('en_NZ'); // Values can only be localized (= non-ISO) in array notation - $datetimeField->setSubmittedValue([ - 'date' => '29/03/2003', - 'time' => '11:00:00 pm' - ]); + $datetimeField->setSubmittedValue('29/03/2003 11:00:00 pm'); $this->assertEquals($datetimeField->dataValue(), '2003-03-29 23:00:00'); } @@ -167,11 +146,7 @@ class DatetimeFieldTest extends SapphireTest // Berlin and Auckland have 12h time difference in northern hemisphere winter $datetimeField = new DatetimeField('Datetime', 'Datetime'); - $datetimeField->getDateField() - ->setHTML5(false) - ->setLocale('en_NZ'); - - $datetimeField->getTimeField() + $datetimeField ->setHTML5(false) ->setLocale('en_NZ'); @@ -182,8 +157,7 @@ class DatetimeFieldTest extends SapphireTest $datetimeField->Value(), 'User value is formatted, and in user timezone' ); - $this->assertEquals('25/12/2003', $datetimeField->getDateField()->Value()); - $this->assertEquals('11:59:59 AM', $datetimeField->getTimeField()->Value()); + $this->assertEquals( '2003-12-24 23:59:59', $datetimeField->dataValue(), @@ -197,81 +171,22 @@ class DatetimeFieldTest extends SapphireTest // Berlin and Auckland have 12h time difference in northern hemisphere summer, but Berlin and Moscow only 2h. $datetimeField = new DatetimeField('Datetime', 'Datetime'); - $datetimeField->getDateField() - ->setHTML5(false) - ->setLocale('en_NZ'); - - $datetimeField->getTimeField() + $datetimeField ->setHTML5(false) ->setLocale('en_NZ'); $datetimeField->setTimezone('Europe/Moscow'); - $datetimeField->setSubmittedValue([ - // pass in default format, at user time (Moscow) - 'date' => '24/06/2003', - 'time' => '11:59:59 pm', - ]); + // pass in default format, at user time (Moscow) + $datetimeField->setSubmittedValue('24/06/2003 11:59:59 pm'); $this->assertTrue($datetimeField->validate(new RequiredFields())); $this->assertEquals('2003-06-24 21:59:59', $datetimeField->dataValue(), 'Data value matches server timezone'); } - public function testSetDateField() - { - $form = $this->getMockForm(); - $field = new DatetimeField('Datetime', 'Datetime'); - $field->setForm($form); - $field->setSubmittedValue([ - 'date' => '2003-06-24', - 'time' => '23:59:59', - ]); - $dateField = new DateField('Datetime[date]'); - $field->setDateField($dateField); - - $this->assertEquals( - $dateField->getForm(), - $form, - 'Sets form on new field' - ); - - $this->assertEquals( - '2003-06-24', - $dateField->dataValue(), - 'Sets existing value on new field' - ); - } - - public function testSetTimeField() - { - $form = $this->getMockForm(); - $field = new DatetimeField('Datetime', 'Datetime'); - $field->setForm($form); - $field->setSubmittedValue([ - 'date' => '2003-06-24', - 'time' => '23:59:59', - ]); - $timeField = new TimeField('Datetime[time]'); - $field->setTimeField($timeField); - - $this->assertEquals( - $timeField->getForm(), - $form, - 'Sets form on new field' - ); - - $this->assertEquals( - '23:59:59', - $timeField->dataValue(), - 'Sets existing value on new field' - ); - } - public function testGetName() { $field = new DatetimeField('Datetime'); $this->assertEquals('Datetime', $field->getName()); - $this->assertEquals('Datetime[date]', $field->getDateField()->getName()); - $this->assertEquals('Datetime[time]', $field->getTimeField()->getName()); } public function testSetName() @@ -279,8 +194,6 @@ class DatetimeFieldTest extends SapphireTest $field = new DatetimeField('Datetime', 'Datetime'); $field->setName('CustomDatetime'); $this->assertEquals('CustomDatetime', $field->getName()); - $this->assertEquals('CustomDatetime[date]', $field->getDateField()->getName()); - $this->assertEquals('CustomDatetime[time]', $field->getTimeField()->getName()); } protected function getMockForm() diff --git a/tests/php/Forms/FormSchemaTest.php b/tests/php/Forms/FormSchemaTest.php index 3073482f2..fb5b8659a 100644 --- a/tests/php/Forms/FormSchemaTest.php +++ b/tests/php/Forms/FormSchemaTest.php @@ -462,6 +462,8 @@ class FormSchemaTest extends SapphireTest 'extraClass' => 'date text', 'description' => null, 'rightTitle' => null, + 'html5' => true, + 'lang' => 'en-US', 'leftTitle' => null, 'readOnly' => false, 'disabled' => false, diff --git a/tests/php/ORM/DBDatetimeTest.php b/tests/php/ORM/DBDatetimeTest.php index 428e56b03..bfb2ab032 100644 --- a/tests/php/ORM/DBDatetimeTest.php +++ b/tests/php/ORM/DBDatetimeTest.php @@ -29,7 +29,7 @@ class DBDatetimeTest extends SapphireTest public function testNowWithMockDate() { // Test setting - $mockDate = '2001-12-31 22:10:59'; + $mockDate = '2001-12-31T22:10:59'; DBDatetime::set_mock_now($mockDate); $systemDatetime = DBDatetime::create_field('Datetime', date('Y-m-d H:i:s')); $nowDatetime = DBDatetime::now(); @@ -55,10 +55,10 @@ class DBDatetimeTest extends SapphireTest $this->assertNull($date->getValue(), 'Boolean FALSE evaluates to NULL'); $date = DBDatetime::create_field('Datetime', '0'); - $this->assertEquals('1970-01-01 00:00:00', $date->getValue(), 'String zero is UNIX epoch time'); + $this->assertEquals('1970-01-01T00:00:00', $date->getValue(), 'String zero is UNIX epoch time'); $date = DBDatetime::create_field('Datetime', 0); - $this->assertEquals('1970-01-01 00:00:00', $date->getValue(), 'Numeric zero is UNIX epoch time'); + $this->assertEquals('1970-01-01T00:00:00', $date->getValue(), 'Numeric zero is UNIX epoch time'); } public function testExtendedDateTimes() @@ -98,7 +98,7 @@ class DBDatetimeTest extends SapphireTest public function testURLDateTime() { $date = DBDatetime::create_field('Datetime', '2001-12-31 22:10:59'); - $this->assertEquals('2001-12-31%2022%3A10%3A59', $date->URLDateTime()); + $this->assertEquals('2001-12-31T22%3A10%3A59', $date->URLDateTime()); } public function testAgoInPast() From 507add8566a480a59201e40ba271d06fcdcfcb89 Mon Sep 17 00:00:00 2001 From: Saophalkun Ponlu Date: Fri, 21 Apr 2017 12:04:44 +1200 Subject: [PATCH 06/26] Update changelogs --- docs/en/04_Changelogs/4.0.0.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/en/04_Changelogs/4.0.0.md b/docs/en/04_Changelogs/4.0.0.md index c0266346a..f2cc7ea88 100644 --- a/docs/en/04_Changelogs/4.0.0.md +++ b/docs/en/04_Changelogs/4.0.0.md @@ -1397,8 +1397,13 @@ The below methods have been added or had their functionality updated to `DBDate` * `getFormatter` method added, which returns a locale-specific date/time formatter. `DBTime` specific changes: + * Added `DBTime::FormatFromSettings` +`DBDatetim` specific changes: + +* Changed `ISO_DATETIME`'s pattern from '`y-MM-dd HH:mm:ss`' to '`y-MM-dd\'T\'HH:mm:ss`' + #### ORM Removed API * `DataObject::db` removed and replaced with `DataObjectSchema::fieldSpec` and `DataObjectSchema::fieldSpecs` @@ -1670,6 +1675,12 @@ New `DatetimeField` methods replace `getConfig()` / `setConfig()`: * `getLocale()` / `setLocale()` * `datavaluefield` config is removed as internal data value is now fixed to ISO 8601 format +The `DatetimeField` has changed behaviour: + +* It no longer relies on `DateField` and `TimeField` +* Anything related to `DateField` and `TimeField` have been removed, e.g. `DatetimeField::getDateField()` +* Added `getHTML5()` / `setHTML5()` + New `DateField` methods replace `getConfig()` / `setConfig()`: * `getDateFormat()` / `setDateFormat()` From d2132e85db8c906b57c258ee5909ca8a61cd2e96 Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Wed, 26 Apr 2017 13:48:48 +1200 Subject: [PATCH 07/26] More specific localisations --- lang/en.yml | 4 ++++ src/Forms/DatetimeField.php | 16 ++++++++-------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/lang/en.yml b/lang/en.yml index 58f12af3e..384af5f54 100644 --- a/lang/en.yml +++ b/lang/en.yml @@ -57,6 +57,10 @@ en: VALIDDATEFORMAT2: 'Please enter a valid date format ({format})' VALIDDATEMAXDATE: 'Your date has to be older or matching the maximum allowed date ({date})' VALIDDATEMINDATE: 'Your date has to be newer or matching the minimum allowed date ({date})' + DatetimeField: + VALIDDATETIMEFORMAT: 'Please enter a valid date and time format ({format})' + VALIDDATEMAXDATETIME: 'Your date has to be older or matching the maximum allowed date and time ({datetime})' + VALIDDATEMINDATETIME: 'Your date has to be newer or matching the minimum allowed date and time ({datetime})' Director: INVALID_REQUEST: 'Invalid request' DropdownField: diff --git a/src/Forms/DatetimeField.php b/src/Forms/DatetimeField.php index a1db12a64..9c9b6b8b7 100644 --- a/src/Forms/DatetimeField.php +++ b/src/Forms/DatetimeField.php @@ -558,8 +558,8 @@ class DatetimeField extends TextField $validator->validationError( $this->name, _t( - 'DateField.VALIDDATEFORMAT2', - "Please enter a valid date format ({format})", + 'DatetimeField.VALIDDATETIMEFORMAT', + "Please enter a valid date and time format ({format})", ['format' => $this->getDatetimeFormat()] ) ); @@ -574,9 +574,9 @@ class DatetimeField extends TextField $validator->validationError( $this->name, _t( - 'DateField.VALIDDATEMINDATE', - "Your date has to be newer or matching the minimum allowed date ({date})", - ['date' => $this->iso8601ToLocalised($min)] + 'DatetimeField.VALIDDATETIMEMINDATE', + "Your date has to be newer or matching the minimum allowed date and time ({datetime})", + ['datetime' => $this->iso8601ToLocalised($min)] ) ); return false; @@ -591,9 +591,9 @@ class DatetimeField extends TextField $validator->validationError( $this->name, _t( - 'DateField.VALIDDATEMAXDATE', - "Your date has to be older or matching the maximum allowed date ({date})", - ['date' => $this->iso8601ToLocalised($max)] + 'DatetimeField.VALIDDATEMAXDATETIME', + "Your date has to be older or matching the maximum allowed date and time ({datetime})", + ['datetime' => $this->iso8601ToLocalised($max)] ) ); return false; From e97783b05708fd72449605f41b7e7226b4e90a28 Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Wed, 26 Apr 2017 13:49:29 +1200 Subject: [PATCH 08/26] Better second handling --- src/Forms/DatetimeField.php | 8 ++++++++ src/Forms/TimeField.php | 2 +- tests/php/Forms/DatetimeFieldTest.php | 4 ++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/Forms/DatetimeField.php b/src/Forms/DatetimeField.php index 9c9b6b8b7..9557e6689 100644 --- a/src/Forms/DatetimeField.php +++ b/src/Forms/DatetimeField.php @@ -182,6 +182,14 @@ class DatetimeField extends TextField $fromFormatter = $this->getFormatter(); $toFormatter = $this->getISO8601Formatter(); $timestamp = $fromFormatter->parse($datetime); + + // Try to parse time without seconds, since that's a valid HTML5 submission format + // See https://html.spec.whatwg.org/multipage/infrastructure.html#times + if ($timestamp === false && $this->getHTML5()) { + $fromFormatter->setPattern(str_replace(':ss', '', $fromFormatter->getPattern())); + $timestamp = $fromFormatter->parse($datetime); + } + if ($timestamp === false) { return null; } diff --git a/src/Forms/TimeField.php b/src/Forms/TimeField.php index 85c377e17..26085cb6a 100644 --- a/src/Forms/TimeField.php +++ b/src/Forms/TimeField.php @@ -389,7 +389,7 @@ class TimeField extends TextField // Try to parse time without seconds, since that's a valid HTML5 submission format // See https://html.spec.whatwg.org/multipage/infrastructure.html#times if ($timestamp === false && $this->getHTML5()) { - $fromFormatter->setPattern('HH:mm'); + $fromFormatter->setPattern(str_replace(':ss', '', DBTime::ISO_TIME)); $timestamp = $fromFormatter->parse($time); } diff --git a/tests/php/Forms/DatetimeFieldTest.php b/tests/php/Forms/DatetimeFieldTest.php index 02dccca14..13e31619a 100644 --- a/tests/php/Forms/DatetimeFieldTest.php +++ b/tests/php/Forms/DatetimeFieldTest.php @@ -136,6 +136,10 @@ class DatetimeFieldTest extends SapphireTest $f = new DatetimeField('Datetime', 'Datetime', '2003-03-29 00:00:00'); $this->assertTrue($f->validate(new RequiredFields())); + $f = (new DatetimeField('Datetime', 'Datetime')) + ->setSubmittedValue('2003-03-29 00:00'); + $this->assertTrue($f->validate(new RequiredFields()), 'Leaving out seconds (like many browsers)'); + $f = new DatetimeField('Datetime', 'Datetime', 'wrong'); $this->assertFalse($f->validate(new RequiredFields())); } From a279d00e81432ca253315c59aab82dafe07074ca Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Wed, 26 Apr 2017 13:49:40 +1200 Subject: [PATCH 09/26] Min/max validations --- tests/php/Forms/DatetimeFieldTest.php | 73 +++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/tests/php/Forms/DatetimeFieldTest.php b/tests/php/Forms/DatetimeFieldTest.php index 13e31619a..fa685ad4f 100644 --- a/tests/php/Forms/DatetimeFieldTest.php +++ b/tests/php/Forms/DatetimeFieldTest.php @@ -13,6 +13,7 @@ use SilverStripe\Forms\FieldList; use SilverStripe\Forms\FormAction; use SilverStripe\Forms\Form; use SilverStripe\i18n\i18n; +use SilverStripe\ORM\FieldType\DBDatetime; class DatetimeFieldTest extends SapphireTest { @@ -144,6 +145,78 @@ class DatetimeFieldTest extends SapphireTest $this->assertFalse($f->validate(new RequiredFields())); } + public function testValidateMinDate() + { + $dateField = new DatetimeField('Datetime'); + $dateField->setMinDatetime('2009-03-31 23:00:00'); + $dateField->setValue('2009-03-31 23:00:01'); + $this->assertTrue($dateField->validate(new RequiredFields()), 'Time above min datetime'); + + $dateField = new DatetimeField('Datetime'); + $dateField->setMinDatetime('2009-03-31 23:00:00'); + $dateField->setValue('2009-03-31 22:00:00'); + $this->assertFalse($dateField->validate(new RequiredFields()), 'Time below min datetime'); + + $dateField = new DatetimeField('Datetime'); + $dateField->setMinDatetime('2009-03-31 23:00:00'); + $dateField->setValue('2009-03-31 23:00:00'); + $this->assertTrue($dateField->validate(new RequiredFields()), 'Date and time matching min datetime'); + + $dateField = new DatetimeField('Datetime'); + $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 testValidateMinDateStrtotime() + { + $f = new DatetimeField('Datetime'); + $f->setMinDatetime('-7 days'); + $f->setValue(strftime('%Y-%m-%d %T', strtotime('-8 days', DBDatetime::now()->getTimestamp()))); + $this->assertFalse($f->validate(new RequiredFields()), 'Date below min datetime, with strtotime'); + + $f = new DatetimeField('Datetime'); + $f->setMinDatetime('-7 days'); + $f->setValue(strftime('%Y-%m-%d %T', strtotime('-7 days', DBDatetime::now()->getTimestamp()))); + $this->assertTrue($f->validate(new RequiredFields()), 'Date matching min datetime, with strtotime'); + } + + public function testValidateMaxDateStrtotime() + { + $f = new DatetimeField('Datetime'); + $f->setMaxDatetime('7 days'); + $f->setValue(strftime('%Y-%m-%d %T', strtotime('8 days', DBDatetime::now()->getTimestamp()))); + $this->assertFalse($f->validate(new RequiredFields()), 'Date above max date, with strtotime'); + + $f = new DatetimeField('Datetime'); + $f->setMaxDatetime('7 days'); + $f->setValue(strftime('%Y-%m-%d %T', strtotime('7 days', DBDatetime::now()->getTimestamp()))); + $this->assertTrue($f->validate(new RequiredFields()), 'Date matching max date, with strtotime'); + } + + public function testValidateMaxDate() + { + $f = new DatetimeField('Datetime'); + $f->setMaxDatetime('2009-03-31 23:00:00'); + $f->setValue('2009-03-31 22:00:00'); + $this->assertTrue($f->validate(new RequiredFields()), 'Time below max datetime'); + + $f = new DatetimeField('Datetime'); + $f->setMaxDatetime('2009-03-31 23:00:00'); + $f->setValue('2010-03-31 23:00:01'); + $this->assertFalse($f->validate(new RequiredFields()), 'Time above max datetime'); + + $f = new DatetimeField('Datetime'); + $f->setMaxDatetime('2009-03-31 23:00:00'); + $f->setValue('2009-03-31 23:00:00'); + $this->assertTrue($f->validate(new RequiredFields()), 'Date and time matching max datetime'); + + $f = new DatetimeField('Datetime'); + $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 testTimezoneSetLocalised() { date_default_timezone_set('Europe/Berlin'); From 1ec2abe75f9f3541fa8866361e20a3d939249358 Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Wed, 26 Apr 2017 13:49:59 +1200 Subject: [PATCH 10/26] Fixed timezone and normalised ISO handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A few observations: - ISO says “T” is optional (https://en.wikipedia.org/wiki/ISO_8601#cite_note-21), - WHATWG says in the HTML5 spec that it’s optional (https://html.spec.whatwg.org/multipage/infrastructure.html#local-dates-and-times) - W3C says it’s reqiured in 1997 (https://www.w3.org/TR/NOTE-datetime), but then later says it’s optional in its HTML5 spec (https://www.w3.org/TR/html5/infrastructure.html#floating-dates-and-times). - Chrome doesn’t parse values with whitespace separators (requires "T") - DataObject DBDatetime values and database columns use whitespace separators (and will have many devs relying on this format) - MySQL only supports whitespace separators (https://dev.mysql.com/doc/refman/5.7/en/datetime.html) - SQLite can parse both ways (https://sqlite.org/lang_datefunc.html) So the goal here is to retain ORM/database compatibility with 3.x (whitespace separator), while exposing "T" separators to the browser in HTML5 mode. Regarding timezones, this fixes a regression where setValue() would not actually apply the timezone (last $value assignment is ineffective now that sub fields are removed). --- docs/en/04_Changelogs/4.0.0.md | 12 +-- src/Forms/DatetimeField.php | 119 +++++++++++++++++--------- src/ORM/FieldType/DBDatetime.php | 11 ++- tests/php/Forms/DatetimeFieldTest.php | 57 ++++++++++-- 4 files changed, 144 insertions(+), 55 deletions(-) diff --git a/docs/en/04_Changelogs/4.0.0.md b/docs/en/04_Changelogs/4.0.0.md index f2cc7ea88..32549d376 100644 --- a/docs/en/04_Changelogs/4.0.0.md +++ b/docs/en/04_Changelogs/4.0.0.md @@ -1400,10 +1400,6 @@ The below methods have been added or had their functionality updated to `DBDate` * Added `DBTime::FormatFromSettings` -`DBDatetim` specific changes: - -* Changed `ISO_DATETIME`'s pattern from '`y-MM-dd HH:mm:ss`' to '`y-MM-dd\'T\'HH:mm:ss`' - #### ORM Removed API * `DataObject::db` removed and replaced with `DataObjectSchema::fieldSpec` and `DataObjectSchema::fieldSpecs` @@ -1677,8 +1673,12 @@ New `DatetimeField` methods replace `getConfig()` / `setConfig()`: The `DatetimeField` has changed behaviour: -* It no longer relies on `DateField` and `TimeField` -* Anything related to `DateField` and `TimeField` have been removed, e.g. `DatetimeField::getDateField()` +* It uses a combined input instead of a composite from `DateField` and `TimeField` + Consequently, `getDateField()` and `getTimeField()` have been removed. +* It returns [ISO 8601 normalised dates](https://html.spec.whatwg.org/multipage/infrastructure.html#local-dates-and-times) + by default in `Value()`, which include a "T" separator between date and time. + This is required to allow HTML5 input. Either use `setHTML5(false)` to set your custom format, + or use `dataValue()` to retrieve a whitespace separated representation. * Added `getHTML5()` / `setHTML5()` New `DateField` methods replace `getConfig()` / `setConfig()`: diff --git a/src/Forms/DatetimeField.php b/src/Forms/DatetimeField.php index 9557e6689..86e00209d 100644 --- a/src/Forms/DatetimeField.php +++ b/src/Forms/DatetimeField.php @@ -8,16 +8,10 @@ use SilverStripe\i18n\i18n; use SilverStripe\ORM\FieldType\DBDatetime; /** - * Form field used for editing date time string - * - * # Localization - * - * See {@link DateField} - * - * # Configuration - * - * - "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. + * Form field used for editing date time strings. + * By default, the field handles strings in normalised ISO 8601 format, + * for example 2017-04-26T23:59:59. The "T" separator can be replaced with a whitespace for value setting. + * Data is passed on via {@link dataValue()} with whitespace separators. */ class DatetimeField extends TextField { @@ -79,6 +73,9 @@ class DatetimeField extends TextField */ protected $rawValue = null; + /** + * @inheritDoc + */ protected $schemaDataType = FormField::SCHEMA_DATA_TYPE_DATETIME; /** @@ -88,10 +85,12 @@ class DatetimeField extends TextField */ protected $dateTimeOrder = '{date} {time}'; - public function __construct($name, $title = null, $value = "") - { - parent::__construct($name, $title, $value); - } + /** + * Custom timezone + * + * @var string + */ + protected $timezone = null; public function setForm($form) { @@ -171,7 +170,7 @@ class DatetimeField extends TextField /** * Convert date localised in the current locale to ISO 8601 date * - * @param string $date + * @param string $datetime * @return string The formatted date, or null if not a valid date */ public function localisedToISO8601($datetime) @@ -181,6 +180,13 @@ class DatetimeField extends TextField } $fromFormatter = $this->getFormatter(); $toFormatter = $this->getISO8601Formatter(); + + // Remove 'T' date and time separator before parsing (required by W3C HTML5 fields) + if ($this->getHTML5()) { + $datetime = str_replace('T', ' ', $datetime); + } + + // Try to parse time with seconds $timestamp = $fromFormatter->parse($datetime); // Try to parse time without seconds, since that's a valid HTML5 submission format @@ -204,7 +210,7 @@ class DatetimeField extends TextField */ protected function getFormatter() { - if ($this->getHTML5() && $this->datetimeFormat && $this->datetimeFormat !== DBDatetime::ISO_DATETIME) { + if ($this->getHTML5() && $this->datetimeFormat && $this->datetimeFormat !== DBDatetime::ISO_DATETIME_NORMALISED) { throw new \LogicException( 'Please opt-out of HTML5 processing of ISO 8601 dates via setHTML5(false) if using setDatetimeFormat()' ); @@ -230,8 +236,9 @@ class DatetimeField extends TextField ); if ($this->getHTML5()) { - // Browsers expect ISO 8601 dates, localisation is handled on the client - $formatter->setPattern(DBDatetime::ISO_DATETIME); + // Browsers expect ISO 8601 dates, localisation is handled on the client. + // Add 'T' date and time separator to create W3C compliant format + $formatter->setPattern(DBDatetime::ISO_DATETIME_NORMALISED); } elseif ($this->datetimeFormat) { // Don't invoke getDatetimeFormat() directly to avoid infinite loop $ok = $formatter->setPattern($this->datetimeFormat); @@ -254,7 +261,7 @@ class DatetimeField extends TextField { if ($this->getHTML5()) { // Browsers expect ISO 8601 dates, localisation is handled on the client - $this->setDatetimeFormat(DBDatetime::ISO_DATETIME); + $this->setDatetimeFormat(DBDatetime::ISO_DATETIME_NORMALISED); } if ($this->datetimeFormat) { @@ -295,6 +302,11 @@ class DatetimeField extends TextField // Build new formatter with the altered timezone $formatter = clone $this->getISO8601Formatter(); $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; } @@ -312,9 +324,11 @@ class DatetimeField extends TextField date_default_timezone_get() // Default to server timezone ); $formatter->setLenient(false); - // CLDR iso8601 date. - // Note we omit timezone from this format, and we assume server TZ always. - $formatter->setPattern('y-MM-dd HH:mm:ss'); + + // Note we omit timezone from this format, and we always assume server TZ + // ISO8601 date with a standard "T" separator (W3C standard). + $formatter->setPattern(DBDatetime::ISO_DATETIME_NORMALISED); + return $formatter; } @@ -343,6 +357,14 @@ class DatetimeField extends TextField // If invalid, assign for later validation failure $isoFormatter = $this->getISO8601Formatter(); $timestamp = $isoFormatter->parse($value); + + // Retry without "T" separator + if (!$timestamp) { + $isoFallbackFormatter = $this->getISO8601Formatter(); + $isoFallbackFormatter->setPattern(DBDatetime::ISO_DATETIME); + $timestamp = $isoFallbackFormatter->parse($value); + } + if ($timestamp === false) { return $this; } @@ -353,11 +375,6 @@ class DatetimeField extends TextField // Save value $this->value = $value; - // Shift iso date into timezone before assignment to subfields - $timezoneFormatter = $this->getTimezoneFormatter(); - if ($timezoneFormatter) { - $value = $timezoneFormatter->format($timestamp); - } return $this; } @@ -366,12 +383,40 @@ class DatetimeField extends TextField return $this->iso8601ToLocalised($this->value); } + public function dataValue() + { + return $this->iso8601ToDataValue($this->value); + } + /** - * Convert an ISO 8601 localised date into the format specified by the - * current date format. + * Convert an ISO 8601 localised datetime into a saveable representation. * - * @param string $date - * @return string The formatted date, or null if not a valid date + * @param string $datetime + * @return string The formatted date and time, or null if not a valid date and time + */ + public function iso8601ToDataValue($datetime) + { + $fromFormatter = $this->getISO8601Formatter(); + $toFormatter = $this->getFormatter(); + + // 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 an ISO 8601 localised datetime into the format specified by the current format. + * + * @param string $datetime + * @return string The formatted date and time, or null if not a valid date and time */ public function iso8601ToLocalised($datetime) { @@ -385,13 +430,14 @@ class DatetimeField extends TextField if ($timestamp === false) { return null; } + return $toFormatter->format($timestamp) ?: null; } /** * Tidy up iso8601-ish date, or approximation * - * @param string $date Date in iso8601 or approximate form + * @param string $date Date in ISO 8601 or approximate form * @return string iso8601 date, or null if not valid */ public function tidyISO8601($datetime) @@ -523,7 +569,7 @@ class DatetimeField extends TextField } /** - * @param string $minDatetime + * @param string $minDatetime A string in ISO 8601 format * @return $this */ public function setMinDatetime($minDatetime) @@ -626,13 +672,6 @@ class DatetimeField extends TextField return $this->timezone; } - /** - * Custom timezone - * - * @var string - */ - protected $timezone = null; - /** * @param string $timezone * @return $this diff --git a/src/ORM/FieldType/DBDatetime.php b/src/ORM/FieldType/DBDatetime.php index 1630c1ebc..c52dbc8d8 100644 --- a/src/ORM/FieldType/DBDatetime.php +++ b/src/ORM/FieldType/DBDatetime.php @@ -35,9 +35,16 @@ use InvalidArgumentException; class DBDatetime extends DBDate implements TemplateGlobalProvider { /** - * Standard ISO format string for date and time in CLDR standard format + * Standard ISO format string for date and time in CLDR standard format, + * with a whitespace separating date and time (common database representation, e.g. in MySQL). */ - const ISO_DATETIME = 'y-MM-dd\'T\'HH:mm:ss'; + const ISO_DATETIME = 'y-MM-dd HH:mm:ss'; + + /** + * Standard ISO format string for date and time in CLDR standard format, + * with a "T" separator between date and time (W3C standard, e.g. for HTML5 datetime-local fields). + */ + const ISO_DATETIME_NORMALISED = 'y-MM-dd\'T\'HH:mm:ss'; /** * Returns the standard localised date diff --git a/tests/php/Forms/DatetimeFieldTest.php b/tests/php/Forms/DatetimeFieldTest.php index fa685ad4f..df4e517f8 100644 --- a/tests/php/Forms/DatetimeFieldTest.php +++ b/tests/php/Forms/DatetimeFieldTest.php @@ -43,7 +43,7 @@ class DatetimeFieldTest extends SapphireTest $this->assertTrue($dateTimeField->validate($validator)); $m = new Model(); $form->saveInto($m); - $this->assertEquals('2003-03-29T23:59:38', $m->MyDatetime); + $this->assertEquals('2003-03-29 23:59:38', $m->MyDatetime); } public function testFormSaveIntoLocalised() @@ -62,7 +62,7 @@ class DatetimeFieldTest extends SapphireTest $this->assertTrue($dateTimeField->validate($validator)); $m = new Model(); $form->saveInto($m); - $this->assertEquals('2003-03-29T23:59:38', $m->MyDatetime); + $this->assertEquals('2003-03-29 23:59:38', $m->MyDatetime); } public function testDataValue() @@ -107,13 +107,21 @@ 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'); + + $f = new DatetimeField('Datetime', 'Datetime'); + $f->setValue('2003-03-29T23:59:38'); + $this->assertEquals($f->dataValue(), '2003-03-29 23:59:38', 'Normalised ISO'); } - public function testSetValue() + public function testSubmittedValue() { $datetimeField = new DatetimeField('Datetime', 'Datetime'); $datetimeField->setSubmittedValue('2003-03-29 23:00:00'); $this->assertEquals($datetimeField->dataValue(), '2003-03-29 23:00:00'); + + $datetimeField = new DatetimeField('Datetime', 'Datetime'); + $datetimeField->setSubmittedValue('2003-03-29T23:00:00'); + $this->assertEquals($datetimeField->dataValue(), '2003-03-29 23:00:00', 'Normalised ISO'); } public function testSetValueWithLocalised() @@ -134,6 +142,9 @@ class DatetimeFieldTest extends SapphireTest $f = new DatetimeField('Datetime', 'Datetime', '2003-03-29 23:59:38'); $this->assertTrue($f->validate(new RequiredFields())); + $f = new DatetimeField('Datetime', 'Datetime', '2003-03-29T23:59:38'); + $this->assertTrue($f->validate(new RequiredFields()), 'Normalised ISO'); + $f = new DatetimeField('Datetime', 'Datetime', '2003-03-29 00:00:00'); $this->assertTrue($f->validate(new RequiredFields())); @@ -166,6 +177,11 @@ 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'); + + $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'); } public function testValidateMinDateStrtotime() @@ -215,9 +231,14 @@ 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'); + + $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'); } - public function testTimezoneSetLocalised() + public function testTimezoneSetValueLocalised() { date_default_timezone_set('Europe/Berlin'); // Berlin and Auckland have 12h time difference in northern hemisphere winter @@ -230,7 +251,7 @@ class DatetimeFieldTest extends SapphireTest $datetimeField->setTimezone('Pacific/Auckland'); $datetimeField->setValue('2003-12-24 23:59:59'); $this->assertEquals( - '25/12/2003 11:59:59 AM', + '25/12/2003, 11:59:59 AM', $datetimeField->Value(), 'User value is formatted, and in user timezone' ); @@ -238,11 +259,32 @@ class DatetimeFieldTest extends SapphireTest $this->assertEquals( '2003-12-24 23:59:59', $datetimeField->dataValue(), - 'Data value is unformatted, and in server timezone' + 'Data value is in ISO format, and in server timezone' ); } - public function testTimezoneFromConfigLocalised() + public function testTimezoneSetValueWithHtml5() + { + date_default_timezone_set('Europe/Berlin'); + // Berlin and Auckland have 12h time difference in northern hemisphere winter + $datetimeField = new DatetimeField('Datetime', 'Datetime'); + + $datetimeField->setTimezone('Pacific/Auckland'); + $datetimeField->setValue('2003-12-24 23:59:59'); + $this->assertEquals( + '2003-12-25T11:59:59', + $datetimeField->Value(), + 'User value is in normalised ISO format and in user timezone' + ); + + $this->assertEquals( + '2003-12-24 23:59:59', + $datetimeField->dataValue(), + 'Data value is in ISO format, and in server timezone' + ); + } + + public function testTimezoneSetSubmittedValueLocalised() { date_default_timezone_set('Europe/Berlin'); // Berlin and Auckland have 12h time difference in northern hemisphere summer, but Berlin and Moscow only 2h. @@ -285,4 +327,5 @@ class DatetimeFieldTest extends SapphireTest ) ); } + } From a73abbfcb834dc740207d86bab20d7009ee60beb Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Thu, 27 Apr 2017 09:18:38 +1200 Subject: [PATCH 11/26] unit test cleanup --- docs/en/04_Changelogs/4.0.0.md | 1 + tests/php/Forms/DatetimeFieldTest.php | 31 ++++++++++++--------------- tests/php/ORM/DBDatetimeTest.php | 8 +++---- 3 files changed, 19 insertions(+), 21 deletions(-) diff --git a/docs/en/04_Changelogs/4.0.0.md b/docs/en/04_Changelogs/4.0.0.md index 32549d376..ea1a357bc 100644 --- a/docs/en/04_Changelogs/4.0.0.md +++ b/docs/en/04_Changelogs/4.0.0.md @@ -1679,6 +1679,7 @@ The `DatetimeField` has changed behaviour: by default in `Value()`, which include a "T" separator between date and time. This is required to allow HTML5 input. Either use `setHTML5(false)` to set your custom format, or use `dataValue()` to retrieve a whitespace separated representation. +* It no longer accepts `setValue()` as an array with 'date' and 'time' keys * Added `getHTML5()` / `setHTML5()` New `DateField` methods replace `getConfig()` / `setConfig()`: diff --git a/tests/php/Forms/DatetimeFieldTest.php b/tests/php/Forms/DatetimeFieldTest.php index df4e517f8..08dfdab88 100644 --- a/tests/php/Forms/DatetimeFieldTest.php +++ b/tests/php/Forms/DatetimeFieldTest.php @@ -80,12 +80,11 @@ class DatetimeFieldTest extends SapphireTest $this->assertEquals($f->dataValue(), null); } - // /** - // * @expectedException InvalidArgumentException - // */ - // public function testConstructorWithLocalizedDateString() { - // $f = new DatetimeField('Datetime', 'Datetime', '29/03/2003 23:59:38'); - // } + public function testConstructorWithLocalizedDateSetsNullValue() + { + $f = new DatetimeField('Datetime', 'Datetime', '29/03/2003 23:59:38'); + $this->assertNull($f->Value()); + } public function testConstructorWithIsoDate() { @@ -94,14 +93,6 @@ class DatetimeFieldTest extends SapphireTest $this->assertEquals($f->dataValue(), '2003-03-29 23:59:38'); } - // /** - // * @expectedException InvalidArgumentException - // */ - // public function testSetValueWithDateString() { - // $f = new DatetimeField('Datetime', 'Datetime'); - // $f->setValue('29/03/2003'); - // } - public function testSetValueWithDateTimeString() { $f = new DatetimeField('Datetime', 'Datetime'); @@ -132,9 +123,15 @@ class DatetimeFieldTest extends SapphireTest ->setHTML5(false) ->setLocale('en_NZ'); - // Values can only be localized (= non-ISO) in array notation $datetimeField->setSubmittedValue('29/03/2003 11:00:00 pm'); $this->assertEquals($datetimeField->dataValue(), '2003-03-29 23:00:00'); + + // Some localisation packages exclude the ',' in default medium format + $this->assertRegExp( + '#29/03/2003(,)? 11:00:00 (AM|am)#', + $datetimeField->Value(), + 'User value is formatted, and in user timezone' + ); } public function testValidate() @@ -246,12 +243,12 @@ class DatetimeFieldTest extends SapphireTest $datetimeField ->setHTML5(false) - ->setLocale('en_NZ'); + ->setDatetimeFormat('dd/MM/y HH:mm:ss'); $datetimeField->setTimezone('Pacific/Auckland'); $datetimeField->setValue('2003-12-24 23:59:59'); $this->assertEquals( - '25/12/2003, 11:59:59 AM', + '25/12/2003 11:59:59', $datetimeField->Value(), 'User value is formatted, and in user timezone' ); diff --git a/tests/php/ORM/DBDatetimeTest.php b/tests/php/ORM/DBDatetimeTest.php index bfb2ab032..428e56b03 100644 --- a/tests/php/ORM/DBDatetimeTest.php +++ b/tests/php/ORM/DBDatetimeTest.php @@ -29,7 +29,7 @@ class DBDatetimeTest extends SapphireTest public function testNowWithMockDate() { // Test setting - $mockDate = '2001-12-31T22:10:59'; + $mockDate = '2001-12-31 22:10:59'; DBDatetime::set_mock_now($mockDate); $systemDatetime = DBDatetime::create_field('Datetime', date('Y-m-d H:i:s')); $nowDatetime = DBDatetime::now(); @@ -55,10 +55,10 @@ class DBDatetimeTest extends SapphireTest $this->assertNull($date->getValue(), 'Boolean FALSE evaluates to NULL'); $date = DBDatetime::create_field('Datetime', '0'); - $this->assertEquals('1970-01-01T00:00:00', $date->getValue(), 'String zero is UNIX epoch time'); + $this->assertEquals('1970-01-01 00:00:00', $date->getValue(), 'String zero is UNIX epoch time'); $date = DBDatetime::create_field('Datetime', 0); - $this->assertEquals('1970-01-01T00:00:00', $date->getValue(), 'Numeric zero is UNIX epoch time'); + $this->assertEquals('1970-01-01 00:00:00', $date->getValue(), 'Numeric zero is UNIX epoch time'); } public function testExtendedDateTimes() @@ -98,7 +98,7 @@ class DBDatetimeTest extends SapphireTest public function testURLDateTime() { $date = DBDatetime::create_field('Datetime', '2001-12-31 22:10:59'); - $this->assertEquals('2001-12-31T22%3A10%3A59', $date->URLDateTime()); + $this->assertEquals('2001-12-31%2022%3A10%3A59', $date->URLDateTime()); } public function testAgoInPast() From 958736502a9b29145d9726b7077d2a959d837ab6 Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Thu, 27 Apr 2017 10:32:22 +1200 Subject: [PATCH 12/26] =?UTF-8?q?Removed=20=E2=80=9CT=E2=80=9D=20str=5Frep?= =?UTF-8?q?lace,=20more=20comments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Forms/DatetimeField.php | 20 ++++++++++---------- tests/php/Forms/DatetimeFieldTest.php | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Forms/DatetimeField.php b/src/Forms/DatetimeField.php index 86e00209d..aeda8cbcf 100644 --- a/src/Forms/DatetimeField.php +++ b/src/Forms/DatetimeField.php @@ -9,8 +9,8 @@ use SilverStripe\ORM\FieldType\DBDatetime; /** * Form field used for editing date time strings. - * By default, the field handles strings in normalised ISO 8601 format, - * for example 2017-04-26T23:59:59. The "T" separator can be replaced with a whitespace for value setting. + * 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. */ class DatetimeField extends TextField @@ -144,7 +144,8 @@ class DatetimeField extends TextField } /** - * Assign value posted from form submission + * Assign value posted from form submission, based on {@link $datetimeFormat}. + * When $html5=true, this needs to be normalised ISO format (with "T" separator). * * @param mixed $value * @param mixed $data @@ -168,7 +169,8 @@ class DatetimeField extends TextField } /** - * Convert date localised in the current locale to ISO 8601 date + * Convert date localised in the current locale to ISO 8601 date. + * Note that "localised" could also mean ISO format when $html5=true. * * @param string $datetime * @return string The formatted date, or null if not a valid date @@ -181,11 +183,6 @@ class DatetimeField extends TextField $fromFormatter = $this->getFormatter(); $toFormatter = $this->getISO8601Formatter(); - // Remove 'T' date and time separator before parsing (required by W3C HTML5 fields) - if ($this->getHTML5()) { - $datetime = str_replace('T', ' ', $datetime); - } - // Try to parse time with seconds $timestamp = $fromFormatter->parse($datetime); @@ -333,7 +330,10 @@ class DatetimeField extends TextField } /** - * Assign value from iso8601 string + * Assign value based on {@link $datetimeFormat}. + * + * 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). * * @param mixed $value * @param mixed $data diff --git a/tests/php/Forms/DatetimeFieldTest.php b/tests/php/Forms/DatetimeFieldTest.php index 08dfdab88..6499a2914 100644 --- a/tests/php/Forms/DatetimeFieldTest.php +++ b/tests/php/Forms/DatetimeFieldTest.php @@ -128,7 +128,7 @@ class DatetimeFieldTest extends SapphireTest // Some localisation packages exclude the ',' in default medium format $this->assertRegExp( - '#29/03/2003(,)? 11:00:00 (AM|am)#', + '#29/03/2003(,)? 11:00:00 (PM|pm)#', $datetimeField->Value(), 'User value is formatted, and in user timezone' ); From d3afa0c3b5211db2b70afc55c3961ba0174e4559 Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Thu, 27 Apr 2017 10:59:44 +1200 Subject: [PATCH 13/26] Remove array check since setSubmittedValue() no longer supports it --- src/Forms/DateField.php | 4 ---- src/Forms/DatetimeField.php | 3 --- 2 files changed, 7 deletions(-) diff --git a/src/Forms/DateField.php b/src/Forms/DateField.php index adff7afb1..dd86aed6f 100644 --- a/src/Forms/DateField.php +++ b/src/Forms/DateField.php @@ -338,10 +338,6 @@ class DateField extends TextField return $this; } - if (is_array($value)) { - throw new InvalidArgumentException("Use setSubmittedValue to assign by array"); - } - // Re-run through formatter to tidy up (e.g. remove time component) $this->value = $this->tidyISO8601($value); return $this; diff --git a/src/Forms/DatetimeField.php b/src/Forms/DatetimeField.php index aeda8cbcf..52b802dc3 100644 --- a/src/Forms/DatetimeField.php +++ b/src/Forms/DatetimeField.php @@ -349,9 +349,6 @@ class DatetimeField extends TextField $this->value = null; return $this; } - if (is_array($value)) { - throw new InvalidArgumentException("Use setSubmittedValue to assign by array"); - }; // Validate iso 8601 date // If invalid, assign for later validation failure From 655b047d8057f1187e66a1dcfc0fd99f007ec3c9 Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Thu, 27 Apr 2017 11:09:43 +1200 Subject: [PATCH 14/26] Removed superfluous methods --- src/Forms/DatetimeField.php | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/Forms/DatetimeField.php b/src/Forms/DatetimeField.php index 52b802dc3..e4d50f2c7 100644 --- a/src/Forms/DatetimeField.php +++ b/src/Forms/DatetimeField.php @@ -92,18 +92,6 @@ class DatetimeField extends TextField */ protected $timezone = null; - public function setForm($form) - { - parent::setForm($form); - return $this; - } - - public function setName($name) - { - parent::setName($name); - return $this; - } - public function getAttributes() { $attributes = parent::getAttributes(); From 14b3468eeee992835a0d7df972c1f32f5821d66e Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Thu, 27 Apr 2017 11:09:59 +1200 Subject: [PATCH 15/26] Removed setting format in getter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit That’s already handled in getFormatter() --- src/Forms/DatetimeField.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Forms/DatetimeField.php b/src/Forms/DatetimeField.php index e4d50f2c7..836a3ae82 100644 --- a/src/Forms/DatetimeField.php +++ b/src/Forms/DatetimeField.php @@ -244,11 +244,6 @@ class DatetimeField extends TextField */ public function getDatetimeFormat() { - if ($this->getHTML5()) { - // Browsers expect ISO 8601 dates, localisation is handled on the client - $this->setDatetimeFormat(DBDatetime::ISO_DATETIME_NORMALISED); - } - if ($this->datetimeFormat) { return $this->datetimeFormat; } From b852a76334d17ea60a37456c972760ddaab5124c Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Thu, 27 Apr 2017 11:47:04 +1200 Subject: [PATCH 16/26] Consistent schema keys --- src/Forms/DateField.php | 6 +++++- src/Forms/DatetimeField.php | 7 ++++++- src/Forms/TimeField.php | 5 ++++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/Forms/DateField.php b/src/Forms/DateField.php index dd86aed6f..33e4a9c22 100644 --- a/src/Forms/DateField.php +++ b/src/Forms/DateField.php @@ -295,7 +295,11 @@ class DateField extends TextField $defaults = parent::getSchemaDataDefaults(); return array_merge($defaults, [ 'lang' => i18n::convert_rfc1766($this->getLocale()), - 'html5' => $this->getHTML5() + 'data' => array_merge($defaults['data'], [ + 'html5' => $this->getHTML5(), + 'min' => $this->getMinDate(), + 'max' => $this->getMaxDate() + ]) ]); } diff --git a/src/Forms/DatetimeField.php b/src/Forms/DatetimeField.php index 836a3ae82..9003b4f58 100644 --- a/src/Forms/DatetimeField.php +++ b/src/Forms/DatetimeField.php @@ -111,7 +111,12 @@ class DatetimeField extends TextField { $defaults = parent::getSchemaDataDefaults(); return array_merge($defaults, [ - 'html5' => $this->getHTML5() + 'lang' => i18n::convert_rfc1766($this->getLocale()), + 'data' => array_merge($defaults['data'], [ + 'html5' => $this->getHTML5(), + 'min' => $this->getMinDate(), + 'max' => $this->getMaxDate() + ]) ]); } diff --git a/src/Forms/TimeField.php b/src/Forms/TimeField.php index 26085cb6a..ff430031b 100644 --- a/src/Forms/TimeField.php +++ b/src/Forms/TimeField.php @@ -235,7 +235,10 @@ class TimeField extends TextField { $defaults = parent::getSchemaDataDefaults(); return array_merge($defaults, [ - 'html5' => $this->getHTML5() + 'lang' => i18n::convert_rfc1766($this->getLocale()), + 'data' => array_merge($defaults['data'], [ + 'html5' => $this->getHTML5(), + ]) ]); } From 9b41350f64cbfceb7f4c9037ffe3de67371fef1a Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Thu, 27 Apr 2017 11:53:34 +1200 Subject: [PATCH 17/26] More min/max setter tests --- tests/php/Forms/DatetimeFieldTest.php | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/php/Forms/DatetimeFieldTest.php b/tests/php/Forms/DatetimeFieldTest.php index 6499a2914..9d33b39f3 100644 --- a/tests/php/Forms/DatetimeFieldTest.php +++ b/tests/php/Forms/DatetimeFieldTest.php @@ -153,6 +153,30 @@ class DatetimeFieldTest extends SapphireTest $this->assertFalse($f->validate(new RequiredFields())); } + 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'); + + $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'); + + $f = (new DatetimeField('Datetime'))->setMinDatetime('invalid'); + $this->assertNull($f->getMinDatetime(), 'Ignores invalid values'); + } + + 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'); + + $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'); + + $f = (new DatetimeField('Datetime'))->setMaxDatetime('invalid'); + $this->assertNull($f->getMaxDatetime(), 'Ignores invalid values'); + } + public function testValidateMinDate() { $dateField = new DatetimeField('Datetime'); From de8abe11675941ee9c5bfa49f491155dd8f22293 Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Thu, 27 Apr 2017 11:53:43 +1200 Subject: [PATCH 18/26] API rename --- src/Forms/DateField.php | 62 +++++++++++++--------- src/Forms/DatetimeField.php | 87 ++++++++++++++++++------------- src/Forms/TimeField.php | 46 ++++++++-------- tests/php/Forms/DateFieldTest.php | 19 ++++--- 4 files changed, 126 insertions(+), 88 deletions(-) diff --git a/src/Forms/DateField.php b/src/Forms/DateField.php index 33e4a9c22..9d698e012 100644 --- a/src/Forms/DateField.php +++ b/src/Forms/DateField.php @@ -194,7 +194,7 @@ class DateField extends TextField } // Get from locale - return $this->getFormatter()->getPattern(); + return $this->getFrontendFormatter()->getPattern(); } /** @@ -217,7 +217,7 @@ class DateField extends TextField * @throws \LogicException * @return IntlDateFormatter */ - protected function getFormatter() + protected function getFrontendFormatter() { if ($this->getHTML5() && $this->dateFormat && $this->dateFormat !== DBDate::ISO_DATE) { throw new \LogicException( @@ -261,7 +261,7 @@ class DateField extends TextField * * @return IntlDateFormatter */ - protected function getISO8601Formatter() + protected function getInternalFormatter() { $locale = i18n::config()->uninherited('default_locale'); $formatter = IntlDateFormatter::create( @@ -327,10 +327,19 @@ class DateField extends TextField } // Parse from submitted value - $this->value = $this->localisedToISO8601($value); + $this->value = $this->frontendToInternal($value); return $this; } + /** + * Assign value based on {@link $datetimeFormat}, which might be localised. + * + * When $html5=true, assign value from ISO 8601 string. + * + * @param mixed $value + * @param mixed $data + * @return $this + */ public function setValue($value, $data = null) { // Save raw value for later validation @@ -343,13 +352,13 @@ class DateField extends TextField } // Re-run through formatter to tidy up (e.g. remove time component) - $this->value = $this->tidyISO8601($value); + $this->value = $this->tidyInternal($value); return $this; } public function Value() { - return $this->iso8601ToLocalised($this->value); + return $this->internalToFrontend($this->value); } public function performReadonlyTransformation() @@ -394,7 +403,7 @@ class DateField extends TextField _t( 'DateField.VALIDDATEMINDATE', "Your date has to be newer or matching the minimum allowed date ({date})", - ['date' => $this->iso8601ToLocalised($min)] + ['date' => $this->internalToFrontend($min)] ) ); return false; @@ -411,7 +420,7 @@ class DateField extends TextField _t( 'DateField.VALIDDATEMAXDATE', "Your date has to be older or matching the maximum allowed date ({date})", - ['date' => $this->iso8601ToLocalised($max)] + ['date' => $this->internalToFrontend($max)] ) ); return false; @@ -466,7 +475,7 @@ class DateField extends TextField */ public function setMinDate($minDate) { - $this->minDate = $this->tidyISO8601($minDate); + $this->minDate = $this->tidyInternal($minDate); return $this; } @@ -484,23 +493,24 @@ class DateField extends TextField */ public function setMaxDate($maxDate) { - $this->maxDate = $this->tidyISO8601($maxDate); + $this->maxDate = $this->tidyInternal($maxDate); return $this; } /** - * Convert date localised in the current locale to ISO 8601 date + * Convert frontend date to the internal representation (ISO 8601). + * The frontend date is also in ISO 8601 when $html5=true. * * @param string $date * @return string The formatted date, or null if not a valid date */ - public function localisedToISO8601($date) + protected function frontendToInternal($date) { if (!$date) { return null; } - $fromFormatter = $this->getFormatter(); - $toFormatter = $this->getISO8601Formatter(); + $fromFormatter = $this->getFrontendFormatter(); + $toFormatter = $this->getInternalFormatter(); $timestamp = $fromFormatter->parse($date); if ($timestamp === false) { return null; @@ -509,20 +519,21 @@ class DateField extends TextField } /** - * Convert an ISO 8601 localised date into the format specified by the - * current date format. + * 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 + * in ISO 8601. * * @param string $date * @return string The formatted date, or null if not a valid date */ - public function iso8601ToLocalised($date) + protected function internalToFrontend($date) { - $date = $this->tidyISO8601($date); + $date = $this->tidyInternal($date); if (!$date) { return null; } - $fromFormatter = $this->getISO8601Formatter(); - $toFormatter = $this->getFormatter(); + $fromFormatter = $this->getInternalFormatter(); + $toFormatter = $this->getFrontendFormatter(); $timestamp = $fromFormatter->parse($date); if ($timestamp === false) { return null; @@ -531,18 +542,19 @@ class DateField extends TextField } /** - * Tidy up iso8601-ish date, or approximation + * Tidy up the internal date representation (ISO 8601), + * and fall back to strtotime() if there's parsing errors. * - * @param string $date Date in iso8601 or approximate form - * @return string iso8601 date, or null if not valid + * @param string $date Date in ISO 8601 or approximate form + * @return string ISO 8601 date, or null if not valid */ - public function tidyISO8601($date) + protected function tidyInternal($date) { if (!$date) { return null; } // Re-run through formatter to tidy up (e.g. remove time component) - $formatter = $this->getISO8601Formatter(); + $formatter = $this->getInternalFormatter(); $timestamp = $formatter->parse($date); if ($timestamp === false) { // Fallback to strtotime diff --git a/src/Forms/DatetimeField.php b/src/Forms/DatetimeField.php index 9003b4f58..c551f1a31 100644 --- a/src/Forms/DatetimeField.php +++ b/src/Forms/DatetimeField.php @@ -156,25 +156,25 @@ class DatetimeField extends TextField } // Parse from submitted value - $this->value = $this->localisedToISO8601($value); + $this->value = $this->frontendToInternal($value); return $this; } /** - * Convert date localised in the current locale to ISO 8601 date. - * Note that "localised" could also mean ISO format when $html5=true. + * Convert frontend date to the internal representation (ISO 8601). + * The frontend date is also in ISO 8601 when $html5=true. * * @param string $datetime * @return string The formatted date, or null if not a valid date */ - public function localisedToISO8601($datetime) + public function frontendToInternal($datetime) { if (!$datetime) { return null; } - $fromFormatter = $this->getFormatter(); - $toFormatter = $this->getISO8601Formatter(); + $fromFormatter = $this->getFrontendFormatter(); + $toFormatter = $this->getInternalFormatter(); // Try to parse time with seconds $timestamp = $fromFormatter->parse($datetime); @@ -198,7 +198,7 @@ class DatetimeField extends TextField * @throws \LogicException * @return IntlDateFormatter */ - protected function getFormatter() + protected function getFrontendFormatter() { if ($this->getHTML5() && $this->datetimeFormat && $this->datetimeFormat !== DBDatetime::ISO_DATETIME_NORMALISED) { throw new \LogicException( @@ -254,7 +254,7 @@ class DatetimeField extends TextField } // Get from locale - return $this->getFormatter()->getPattern(); + return $this->getFrontendFormatter()->getPattern(); } /** @@ -285,7 +285,7 @@ class DatetimeField extends TextField } // Build new formatter with the altered timezone - $formatter = clone $this->getISO8601Formatter(); + $formatter = clone $this->getInternalFormatter(); $formatter->setTimeZone($timezone); // ISO8601 date with a standard "T" separator (W3C standard). @@ -300,7 +300,7 @@ class DatetimeField extends TextField * * @return IntlDateFormatter */ - protected function getISO8601Formatter() + protected function getInternalFormatter() { $formatter = IntlDateFormatter::create( i18n::config()->uninherited('default_locale'), @@ -311,14 +311,13 @@ class DatetimeField extends TextField $formatter->setLenient(false); // Note we omit timezone from this format, and we always assume server TZ - // ISO8601 date with a standard "T" separator (W3C standard). $formatter->setPattern(DBDatetime::ISO_DATETIME_NORMALISED); return $formatter; } /** - * Assign value based on {@link $datetimeFormat}. + * Assign value based on {@link $datetimeFormat}, which might be localised. * * 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). @@ -340,14 +339,14 @@ class DatetimeField extends TextField // Validate iso 8601 date // If invalid, assign for later validation failure - $isoFormatter = $this->getISO8601Formatter(); - $timestamp = $isoFormatter->parse($value); + $internalFormatter = $this->getInternalFormatter(); + $timestamp = $internalFormatter->parse($value); // Retry without "T" separator if (!$timestamp) { - $isoFallbackFormatter = $this->getISO8601Formatter(); - $isoFallbackFormatter->setPattern(DBDatetime::ISO_DATETIME); - $timestamp = $isoFallbackFormatter->parse($value); + $fallbackFormatter = $this->getInternalFormatter(); + $fallbackFormatter->setPattern(DBDatetime::ISO_DATETIME); + $timestamp = $fallbackFormatter->parse($value); } if ($timestamp === false) { @@ -355,7 +354,7 @@ class DatetimeField extends TextField } // Cleanup date - $value = $isoFormatter->format($timestamp); + $value = $internalFormatter->format($timestamp); // Save value $this->value = $value; @@ -363,14 +362,27 @@ class DatetimeField extends TextField return $this; } + /** + * Returns the frontend representation of the field value, + * according to the defined {@link dateFormat}. + * With $html5=true, this will be in ISO 8601 format. + * + * @return string + */ public function Value() { - return $this->iso8601ToLocalised($this->value); + 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->iso8601ToDataValue($this->value); + return $this->internalToDataValue($this->value); } /** @@ -379,10 +391,10 @@ class DatetimeField extends TextField * @param string $datetime * @return string The formatted date and time, or null if not a valid date and time */ - public function iso8601ToDataValue($datetime) + public function internalToDataValue($datetime) { - $fromFormatter = $this->getISO8601Formatter(); - $toFormatter = $this->getFormatter(); + $fromFormatter = $this->getInternalFormatter(); + $toFormatter = $this->getFrontendFormatter(); // Set default timezone (avoid shifting data values into user timezone) $toFormatter->setTimezone(date_default_timezone_get()); @@ -398,19 +410,21 @@ class DatetimeField extends TextField } /** - * Convert an ISO 8601 localised datetime into the format specified by the current format. + * 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 + * in ISO 8601. * * @param string $datetime * @return string The formatted date and time, or null if not a valid date and time */ - public function iso8601ToLocalised($datetime) + public function internalToFrontend($datetime) { - $datetime = $this->tidyISO8601($datetime); + $datetime = $this->tidyInternal($datetime); if (!$datetime) { return null; } - $fromFormatter = $this->getISO8601Formatter(); - $toFormatter = $this->getFormatter(); + $fromFormatter = $this->getInternalFormatter(); + $toFormatter = $this->getFrontendFormatter(); $timestamp = $fromFormatter->parse($datetime); if ($timestamp === false) { return null; @@ -420,18 +434,19 @@ class DatetimeField extends TextField } /** - * Tidy up iso8601-ish date, or approximation + * Tidy up the internal date representation (ISO 8601), + * and fall back to strtotime() if there's parsing errors. * * @param string $date Date in ISO 8601 or approximate form - * @return string iso8601 date, or null if not valid + * @return string ISO 8601 date, or null if not valid */ - public function tidyISO8601($datetime) + public function tidyInternal($datetime) { if (!$datetime) { return null; } // Re-run through formatter to tidy up (e.g. remove time component) - $formatter = $this->getISO8601Formatter(); + $formatter = $this->getInternalFormatter(); $timestamp = $formatter->parse($datetime); if ($timestamp === false) { // Fallback to strtotime @@ -559,7 +574,7 @@ class DatetimeField extends TextField */ public function setMinDatetime($minDatetime) { - $this->minDatetime = $this->tidyISO8601($minDatetime); + $this->minDatetime = $this->tidyInternal($minDatetime); return $this; } @@ -577,7 +592,7 @@ class DatetimeField extends TextField */ public function setMaxDatetime($maxDatetime) { - $this->maxDatetime = $this->tidyISO8601($maxDatetime); + $this->maxDatetime = $this->tidyInternal($maxDatetime); return $this; } @@ -615,7 +630,7 @@ class DatetimeField extends TextField _t( 'DatetimeField.VALIDDATETIMEMINDATE', "Your date has to be newer or matching the minimum allowed date and time ({datetime})", - ['datetime' => $this->iso8601ToLocalised($min)] + ['datetime' => $this->internalToFrontend($min)] ) ); return false; @@ -632,7 +647,7 @@ class DatetimeField extends TextField _t( 'DatetimeField.VALIDDATEMAXDATETIME', "Your date has to be older or matching the maximum allowed date and time ({datetime})", - ['datetime' => $this->iso8601ToLocalised($max)] + ['datetime' => $this->internalToFrontend($max)] ) ); return false; diff --git a/src/Forms/TimeField.php b/src/Forms/TimeField.php index ff430031b..4cfa187ac 100644 --- a/src/Forms/TimeField.php +++ b/src/Forms/TimeField.php @@ -103,7 +103,7 @@ class TimeField extends TextField } // Get from locale - return $this->getFormatter()->getPattern(); + return $this->getFrontendFormatter()->getPattern(); } /** @@ -159,7 +159,7 @@ class TimeField extends TextField * * @return IntlDateFormatter */ - protected function getFormatter() + protected function getFrontendFormatter() { if ($this->getHTML5() && $this->timeFormat && $this->timeFormat !== DBTime::ISO_TIME) { throw new \LogicException( @@ -204,7 +204,7 @@ class TimeField extends TextField * * @return IntlDateFormatter */ - protected function getISO8601Formatter() + protected function getInternalFormatter() { $formatter = IntlDateFormatter::create( i18n::config()->uninherited('default_locale'), @@ -260,7 +260,7 @@ class TimeField extends TextField $this->rawValue = $value; // Parse from submitted value - $this->value = $this->localisedToISO8601($value); + $this->value = $this->frontendToInternal($value); return $this; } @@ -283,13 +283,13 @@ class TimeField extends TextField } // Re-run through formatter to tidy up (e.g. remove date component) - $this->value = $this->tidyISO8601($value); + $this->value = $this->tidyInternal($value); return $this; } public function Value() { - $localised = $this->iso8601ToLocalised($this->value); + $localised = $this->internalToFrontend($this->value); if ($localised) { return $localised; } @@ -305,7 +305,7 @@ class TimeField extends TextField */ public function getMidnight() { - $formatter = $this->getFormatter(); + $formatter = $this->getFrontendFormatter(); $timestamp = $this->withTimezone($this->getTimezone(), function () { return strtotime('midnight'); }); @@ -375,18 +375,19 @@ class TimeField extends TextField } /** - * Convert time localised in the current locale to ISO 8601 time + * Convert frontend time to the internal representation (ISO 8601). + * The frontend time is also in ISO 8601 when $html5=true. * * @param string $time * @return string The formatted time, or null if not a valid time */ - public function localisedToISO8601($time) + protected function frontendToInternal($time) { if (!$time) { return null; } - $fromFormatter = $this->getFormatter(); - $toFormatter = $this->getISO8601Formatter(); + $fromFormatter = $this->getFrontendFormatter(); + $toFormatter = $this->getInternalFormatter(); $timestamp = $fromFormatter->parse($time); // Try to parse time without seconds, since that's a valid HTML5 submission format @@ -405,19 +406,21 @@ class TimeField extends TextField } /** - * Format iso time to localised form + * Convert the internal time representation (ISO 8601) to a format used by the frontend, + * as defined by {@link $timeFormat}. With $html5=true, the frontend time will also be + * in ISO 8601. * * @param string $time * @return string */ - public function iso8601ToLocalised($time) + protected function internalToFrontend($time) { - $time = $this->tidyISO8601($time); + $time = $this->tidyInternal($time); if (!$time) { return null; } - $fromFormatter = $this->getISO8601Formatter(); - $toFormatter = $this->getFormatter(); + $fromFormatter = $this->getInternalFormatter(); + $toFormatter = $this->getFrontendFormatter(); $timestamp = $fromFormatter->parse($time); if ($timestamp === false) { return null; @@ -428,18 +431,19 @@ class TimeField extends TextField /** - * Tidy up iso8601-ish time, or approximation + * Tidy up the internal time representation (ISO 8601), + * and fall back to strtotime() if there's parsing errors. * - * @param string $time Time in iso8601 or approximate form - * @return string iso8601 time, or null if not valid + * @param string $time Time in ISO 8601 or approximate form + * @return string ISO 8601 time, or null if not valid */ - public function tidyISO8601($time) + protected function tidyInternal($time) { if (!$time) { return null; } // Re-run through formatter to tidy up (e.g. remove date component) - $formatter = $this->getISO8601Formatter(); + $formatter = $this->getInternalFormatter(); $timestamp = $formatter->parse($time); if ($timestamp === false) { // Fallback to strtotime diff --git a/tests/php/Forms/DateFieldTest.php b/tests/php/Forms/DateFieldTest.php index 676ae58eb..7843a02c0 100644 --- a/tests/php/Forms/DateFieldTest.php +++ b/tests/php/Forms/DateFieldTest.php @@ -94,15 +94,22 @@ class DateFieldTest extends SapphireTest $this->assertEquals('2003-03-29', $f->dataValue()); } - public function testTidyISO8601() + public function testSetValue() { - $f = new DateField('Date', 'Date'); - $this->assertEquals(null, $f->tidyISO8601('notadate')); - $this->assertEquals('2011-01-31', $f->tidyISO8601('-1 day')); - $this->assertEquals(null, $f->tidyISO8601('29/03/2003')); + $f = (new DateField('Date', 'Date'))->setValue('notadate'); + $this->assertNull($f->Value(), 'Invalid input ignored'); + + $f = (new DateField('Date', 'Date'))->setValue('-1 day'); + $this->assertEquals($f->Value(), '2011-01-31', 'Relative dates accepted'); + + $f = (new DateField('Date', 'Date'))->setValue('2011-01-31'); + $this->assertEquals($f->Value(), '2011-01-31', 'ISO format accepted'); + + $f = (new DateField('Date', 'Date'))->setValue('2011-01-31 23:59:59'); + $this->assertEquals($f->Value(), '2011-01-31', 'ISO format with time accepted'); } - public function testSetValueWithDateString() + public function testSetValueWithLocalisedDateString() { $f = new DateField('Date', 'Date'); $f->setHTML5(false); From f01a20d5c4b46715e07ddcaa673aa8025f9890a0 Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Thu, 27 Apr 2017 11:56:18 +1200 Subject: [PATCH 19/26] Only used normalised ISO on HTML5 --- src/Forms/DatetimeField.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Forms/DatetimeField.php b/src/Forms/DatetimeField.php index c551f1a31..e97679927 100644 --- a/src/Forms/DatetimeField.php +++ b/src/Forms/DatetimeField.php @@ -311,7 +311,13 @@ class DatetimeField extends TextField $formatter->setLenient(false); // Note we omit timezone from this format, and we always assume server TZ - $formatter->setPattern(DBDatetime::ISO_DATETIME_NORMALISED); + 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; } From 628fd216adce2fff3bbf136f15c1435e5cf670c6 Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Thu, 27 Apr 2017 11:56:23 +1200 Subject: [PATCH 20/26] PHPDoc fixes --- src/Forms/DatetimeField.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/Forms/DatetimeField.php b/src/Forms/DatetimeField.php index e97679927..a5520bdc6 100644 --- a/src/Forms/DatetimeField.php +++ b/src/Forms/DatetimeField.php @@ -107,6 +107,9 @@ class DatetimeField extends TextField return $attributes; } + /** + * @inheritDoc + */ public function getSchemaDataDefaults() { $defaults = parent::getSchemaDataDefaults(); @@ -120,16 +123,26 @@ class DatetimeField extends TextField ]); } + /** + * @inheritDoc + */ public function Type() { return 'text datetime'; } + /** + * @return bool + */ public function getHTML5() { return $this->html5; } + /** + * @param $bool + * @return $this + */ public function setHTML5($bool) { $this->html5 = $bool; From 60706c8efd2ea22884eaede36a6deaee3829902e Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Thu, 27 Apr 2017 14:59:11 +1200 Subject: [PATCH 21/26] Store $value in ISO and server timezone consistently, fix min/max timezone handling --- src/Forms/DatetimeField.php | 108 +++++--------------- tests/php/Forms/DatetimeFieldTest.php | 141 +++++++++++++++++++++++--- 2 files changed, 151 insertions(+), 98 deletions(-) diff --git a/src/Forms/DatetimeField.php b/src/Forms/DatetimeField.php index a5520bdc6..03da92f68 100644 --- a/src/Forms/DatetimeField.php +++ b/src/Forms/DatetimeField.php @@ -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); - } + $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; } } diff --git a/tests/php/Forms/DatetimeFieldTest.php b/tests/php/Forms/DatetimeFieldTest.php index 9d33b39f3..e8bcdcc69 100644 --- a/tests/php/Forms/DatetimeFieldTest.php +++ b/tests/php/Forms/DatetimeFieldTest.php @@ -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 */ From 94b49e3e28b66295e15137b4ffb97ec021a389ab Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Thu, 27 Apr 2017 15:36:11 +1200 Subject: [PATCH 22/26] Removed unused field --- src/Forms/DatetimeField.php | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/Forms/DatetimeField.php b/src/Forms/DatetimeField.php index 03da92f68..7a7ff97fd 100644 --- a/src/Forms/DatetimeField.php +++ b/src/Forms/DatetimeField.php @@ -79,13 +79,6 @@ class DatetimeField extends TextField */ protected $schemaDataType = FormField::SCHEMA_DATA_TYPE_DATETIME; - /** - * Date time order - * - * @var string - */ - protected $dateTimeOrder = '{date} {time}'; - /** * Custom timezone * From cbe534c67523bb5e0bdb019e353a1efa6c35b345 Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Thu, 27 Apr 2017 15:36:18 +1200 Subject: [PATCH 23/26] Fixed component capitalisation --- src/Forms/FormField.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Forms/FormField.php b/src/Forms/FormField.php index ac60eb425..076fb0f52 100644 --- a/src/Forms/FormField.php +++ b/src/Forms/FormField.php @@ -70,7 +70,7 @@ class FormField extends RequestHandler const SCHEMA_DATA_TYPE_DATE = 'Date'; /** @see $schemaDataType */ - const SCHEMA_DATA_TYPE_DATETIME = 'DateTime'; + const SCHEMA_DATA_TYPE_DATETIME = 'Datetime'; /** @see $schemaDataType */ const SCHEMA_DATA_TYPE_TIME = 'Time'; From a2ee6a76a0dfed5bb738852c932d04a91545614c Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Thu, 27 Apr 2017 21:44:28 +1200 Subject: [PATCH 24/26] Fixed formschematest --- tests/php/Forms/FormSchemaTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/php/Forms/FormSchemaTest.php b/tests/php/Forms/FormSchemaTest.php index fb5b8659a..fbc56b050 100644 --- a/tests/php/Forms/FormSchemaTest.php +++ b/tests/php/Forms/FormSchemaTest.php @@ -462,7 +462,6 @@ class FormSchemaTest extends SapphireTest 'extraClass' => 'date text', 'description' => null, 'rightTitle' => null, - 'html5' => true, 'lang' => 'en-US', 'leftTitle' => null, 'readOnly' => false, @@ -474,8 +473,9 @@ class FormSchemaTest extends SapphireTest ], 'attributes' => [], - 'data' => - [], + 'data' => [ + 'html5' => true, + ], ], [ 'name' => 'Number', From 22f232ed4d6c82d649b02d0fea26978726659646 Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Thu, 27 Apr 2017 21:44:52 +1200 Subject: [PATCH 25/26] Mark up