diff --git a/docs/en/02_Developer_Guides/03_Forms/Field_types/02_DateField.md b/docs/en/02_Developer_Guides/03_Forms/Field_types/02_DateField.md index 40d761fb0..5f2f188a1 100644 --- a/docs/en/02_Developer_Guides/03_Forms/Field_types/02_DateField.md +++ b/docs/en/02_Developer_Guides/03_Forms/Field_types/02_DateField.md @@ -4,7 +4,11 @@ summary: How to format and use the DateField class. # DateField This `FormField` subclass lets you display an editable date, in a single text input field. -It also provides a calendar date picker. +It implements the [HTML5 input date type](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/date) +(with `type=date`). In supported browsers, this will cause a localised date picker to appear for users. +HTML5 date fields present and save ISO 8601 date formats (`y-MM-dd`), +since the browser takes care of converting to/from a localised presentation. +Browsers without support receive an `` based polyfill. The following example will add a simple DateField to your Page, allowing you to enter a date manually. @@ -34,65 +38,28 @@ The following example will add a simple DateField to your Page, allowing you to ## Custom Date Format A custom date format for a [api:DateField] can be provided through `setDateFormat`. +This is only necessary if you want to opt-out of the built-in browser localisation via `type=date`. :::php - // will display a date in the following format: 31-06-2012 - DateField::create('MyDate')->setDateFormat('dd-MM-yyyy'); + // will display a date in the following format: 31/06/2012 + DateField::create('MyDate') + ->setHTML5(false) + ->setDateFormat('dd/MM/yyyy');
-The formats are based on [CLDR format](http://userguide.icu-project.org/formatparse/datetime). +The formats are based on [ICU format](http://www.icu-project.org/apiref/icu4c/classSimpleDateFormat.html#details).
## Min and Max Dates Sets the minimum and maximum allowed date values using the `min` and `max` configuration settings (in ISO format or -strtotime()). +`strtotime()`). :::php DateField::create('MyDate') ->setMinDate('-7 days') - ->setMaxDate'2012-12-31') - -## Separate Day / Month / Year Fields - -To display separate input fields for day, month and year separately you can use the `SeparatedDateField` subclass`. -HTML5 placeholders 'day', 'month' and 'year' are enabled by default. - - :::php - SeparatedDateField::create('MyDate'); - -
-Any custom date format settings will be ignored. -
- -## Calendar Picker - -The following setting will add a Calendar to a single DateField, using the jQuery UI DatePicker widget. - - :::php - DateField::create('MyDate') - ->setShowCalendar(true); - -The jQuery date picker will support most custom locale formats (if left as default). -If setting an explicit date format via setDateFormat() then the below table of supported -characters should be used. - -It is recommended to use numeric format, as `MMM` or `MMMM` month names may not always pass validation. - -Constant | xxxxx --------- | ----- -d | numeric day of the month (without leading zero) -dd | numeric day of the month (with leading zero) -EEE | dayname, abbreviated -EEEE | dayname -M | numeric month of the year (without leading zero) -MM | numeric month of the year (with leading zero) -MMM | monthname, abbreviated -MMMM | monthname -y | year (4 digits) -yy | year (2 digits) -yyyy | year (4 digits) + ->setMaxDate('2012-12-31') ## Formatting Hints diff --git a/docs/en/02_Developer_Guides/13_i18n/index.md b/docs/en/02_Developer_Guides/13_i18n/index.md index 46023817f..d7efc7ef9 100644 --- a/docs/en/02_Developer_Guides/13_i18n/index.md +++ b/docs/en/02_Developer_Guides/13_i18n/index.md @@ -71,18 +71,24 @@ and default alignment of paragraphs and tables to browsers. ### Date and time formats -Formats can be set globally in the i18n class. These settings are currently only picked up by the CMS, you'll need -to write your own logic for any frontend output. +Formats can be set globally in the i18n class. +You can use these settings for your own view logic. :::php Config::inst()->update('i18n', 'date_format', 'dd.MM.YYYY'); Config::inst()->update('i18n', 'time_format', 'HH:mm'); -Most localization routines in SilverStripe use the [Zend_Date API](http://framework.zend.com/manual/1.12/en/zend.date.overview.html). -This means all formats are defined in -[ISO date format](http://framework.zend.com/manual/1.12/en/zend.date.constants.html), +Localization in SilverStripe uses PHP's [intl extension](http://php.net/intl). +Formats for it's [IntlDateFormatter](http://php.net/manual/en/class.intldateformatter.php) +are defined in [ICU format](http://www.icu-project.org/apiref/icu4c/classSimpleDateFormat.html#details), not PHP's built-in [date()](http://nz.php.net/manual/en/function.date.php). +These settings are not used for CMS presentation. +Users can choose their own locale, which determines the date format +that gets presented to them. Currently this is a mix of PHP defaults (for readonly `DateField` and `TimeField`), +browser defaults (for `DateField` on browsers supporting HTML5), and [Moment.JS](http://momentjs.com/) +client-side logic (for `DateField` polyfills and other readonly dates and times). + ### Language Names SilverStripe comes with a built-in list of common languages, listed by locale and region. @@ -126,32 +132,17 @@ Please refer to [W3C: Introduction to IDN and IRI](http://www.w3.org/Internation ### i18n in Form Fields -Date- and time related form fields support i18n ([api:DateField], [api:TimeField], [api:DatetimeField]). +Date and time related form fields are automatically localised ([api:DateField], [api:TimeField], [api:DatetimeField]). +Since they use HTML5 `type=date` and `type=time` fields by default, these fields will present dates +in a localised format chosen by the browser and operating system. - :::php - i18n::set_locale('ca_AD'); - $field = new DateField(); // will automatically set date format defaults for 'ca_AD' - $field->setLocale('de_DE'); // will not update the date formats - $field->setConfig('dateformat', 'dd. MMMM YYYY'); // sets typical 'de_DE' date format, shows as "23. Juni 1982" - -Defaults can be applied globally for all field instances through the `DateField.default_config` -and `TimeField.default_config` [configuration arrays](/developer_guides/configuration). -If no 'locale' default is set on the field, [api:i18n::get_locale()] will be used. - -**Important:** Form fields in the CMS are automatically configured according to the profile settings for the logged-in user (`Member->Locale`, `Member->DateFormat` and `Member->TimeFormat`). This means that in most cases, -fields created through [api:DataObject::getCMSFields()] will get their i18n settings from a specific member - -The [api:DateField] API can be enhanced by JavaScript, and comes with -[jQuery UI datepicker](http://jqueryui.com/demos/datepicker/) capabilities built-in. -The field tries to translate the date formats and locales into a format compatible with jQuery UI -(see [api:DateField_View_JQuery::$locale_map_] and [api:DateField_View_JQuery::convert_iso_to_jquery_format()]). +Fields can be forced to use a certain locale and date/time format by calling `setHTML5(false)`, +followed by `setLocale()` or `setDateFormat()`/`setTimeFormat()`. :::php $field = new DateField(); - $field->setLocale('de_AT'); // set Austrian/German locale - $field->setConfig('showcalendar', true); - $field->setConfig('jslocale', 'de'); // jQuery UI only has a generic German localization - $field->setConfig('dateformat', 'dd. MMMM YYYY'); // will be transformed to 'dd. MM yy' for jQuery + $field->setLocale('de_AT'); // set Austrian/German locale, defaulting format to dd.MM.y + $field->setDateFormat('d.M.y'); // set a more specific date format (single digit day/month) ## Translating text diff --git a/docs/en/04_Changelogs/4.0.0.md b/docs/en/04_Changelogs/4.0.0.md index 5db6936df..09e3343ae 100644 --- a/docs/en/04_Changelogs/4.0.0.md +++ b/docs/en/04_Changelogs/4.0.0.md @@ -398,6 +398,19 @@ In templates this can also be invoked as below: <%t MyObject.PLURALS 'An item|{count} items' count=$Count %> +#### Removed Member.DateFormat and Member.TimeFormat database settings + +We're using [native HTML5 date and time pickers](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/date) +in `DateField` and `TimeField` now ([discussion](https://github.com/silverstripe/silverstripe-framework/issues/6626)), +where the browser localises the output based on the browser/system preferences. +In this context it no longer makes sense to give users control over their own +date and time formats in their CMS profile. +Consequently, we've also removed `MemberDatetimeOptionsetField`. + +`Member->getDateFormat()` and `Member->getTimeFormat()` still exist, and default to +the [IntlDateFormatter defaults](http://php.net/manual/en/class.intldateformatter.php) for the selected locale. + + #### New asset storage mechanism File system has been abstracted into an abstract interface. By default, the out of the box filesystem @@ -1495,19 +1508,25 @@ New `DatetimeField` methods replace `getConfig()` / `setConfig()`: New `DateField` methods replace `getConfig()` / `setConfig()`: -* `getShowCalendar()` / `setShowCalendar()` -* `getDateFormat()` / `setShowCalendar()` +* `getDateFormat()` / `setDateFormat()` * `getMinDate()` / `setMinDate()` * `getMaxDate()` / `setMaxDate()` -* `getPlaceholders()` / `setPlaceholders()` -* `getClientLocale` / `setClientLocale` * `getLocale()` / `setLocale()` -* option `dmyfields` is now superceded with an `SeparatedDateField` class + +The `DateField` has changed behavior: + +* `DateField` no longer provides a jQuery UI date picker (`showcalendar` option), + and uses [HTML5 date pickers](https://www.wufoo.com/html5/types/4-date.html) by default instead. +* `DateField` provides an optional polyfill for + [browsers without HTML5 date picker support](http://caniuse.com/#feat=input-datetime) +* The `dmyfields` option has been replced with native HTML5 behaviour (as one single ``). +* `getClientLocale` / `setClientLocale` have been removed (handled by `DateField->locale` and browser settings) New `TimeField` methods replace `getConfig()` / `setConfig()` * `getTimeFormat()` / `setTimeFormat()` * `getLocale()` / `setLocale()` +* `getClientConfig()` has been removed (in favour of `setHTML5()`) #### Template and Form Removed API @@ -1549,7 +1568,9 @@ New `TimeField` methods replace `getConfig()` / `setConfig()` * `set_source_file_comments()` * `get_source_file_comments()` * `getOption` - * `setOption` + * `setOption` +* Removed `MemberDatetimeOptionsetField` (no replacement) +* Removed `DateField_View_JQuery` (replaced with native HTML5 support in `DateField`) ### i18n API diff --git a/src/Forms/DateField.php b/src/Forms/DateField.php index ea3e3a7dc..b3da3be06 100644 --- a/src/Forms/DateField.php +++ b/src/Forms/DateField.php @@ -5,6 +5,7 @@ namespace SilverStripe\Forms; use IntlDateFormatter; use SilverStripe\i18n\i18n; use InvalidArgumentException; +use SilverStripe\ORM\FieldType\DBDate; use SilverStripe\ORM\FieldType\DBDatetime; /** @@ -85,13 +86,6 @@ class DateField extends TextField */ protected $dateLength = null; - /** - * Set whether to show placeholders - * - * @var bool - */ - protected $placeholders = true; - /** * Override locale for client side. * @@ -122,25 +116,27 @@ class DateField extends TextField protected $rawValue = null; /** - * Check if calendar should be shown on the frontend + * Use HTML5-based input fields (and force ISO 8601 date formats). * + * @var bool + */ + protected $html5 = true; + + /** * @return bool */ - public function getShowCalendar() + public function getHTML5() { - return $this->showCalendar; + return $this->html5; } /** - * Set if calendar should be shown on the frontend. - * @internal WARNING: Experimental and volatile API. - * - * @param bool $show + * @param boolean $bool * @return $this */ - public function setShowCalendar($show) + public function setHTML5($bool) { - $this->showCalendar = $show; + $this->html5 = $bool; return $this; } @@ -164,12 +160,8 @@ class DateField extends TextField } /** - * Get length of the date format to use. One of: - * - * - IntlDateFormatter::SHORT - * - IntlDateFormatter::MEDIUM - * - IntlDateFormatter::LONG - * - IntlDateFormatter::FULL + * 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 * @@ -192,6 +184,11 @@ class DateField extends TextField */ public function getDateFormat() { + if ($this->getHTML5()) { + // Browsers expect ISO 8601 dates, localisation is handled on the client + $this->setDateFormat(DBDate::ISO_DATE); + } + if ($this->dateFormat) { return $this->dateFormat; } @@ -202,6 +199,7 @@ class DateField extends TextField /** * 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 @@ -216,18 +214,40 @@ class DateField extends TextField /** * Get date formatter with the standard locale / date format * + * @throws \LogicException * @return IntlDateFormatter */ protected function getFormatter() { + if ($this->getHTML5() && $this->dateFormat && $this->dateFormat !== DBDate::ISO_DATE) { + throw new \LogicException( + 'Please opt-out of HTML5 processing of ISO 8601 dates via setHTML5(false) if using setDateFormat()' + ); + } + + if ($this->getHTML5() && $this->dateLength) { + throw new \LogicException( + 'Please opt-out of HTML5 processing of ISO 8601 dates via setHTML5(false) if using setDateLength()' + ); + } + + 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(), IntlDateFormatter::NONE ); - // Don't invoke getDateFormat() directly to avoid infinite loop - if ($this->dateFormat) { + if ($this->getHTML5()) { + // Browsers expect ISO 8601 dates, localisation is handled on the client + $formatter->setPattern(DBDate::ISO_DATE); + } elseif ($this->dateFormat) { + // Don't invoke getDateFormat() directly to avoid infinite loop $ok = $formatter->setPattern($this->dateFormat); if (!$ok) { throw new InvalidArgumentException("Invalid date format {$this->dateFormat}"); @@ -243,59 +263,28 @@ class DateField extends TextField */ protected function getISO8601Formatter() { + $locale = i18n::config()->uninherited('default_locale'); $formatter = IntlDateFormatter::create( i18n::config()->uninherited('default_locale'), IntlDateFormatter::MEDIUM, IntlDateFormatter::NONE ); $formatter->setLenient(false); - // CLDR iso8601 date. - $formatter->setPattern('y-MM-dd'); + // CLDR ISO 8601 date. + $formatter->setPattern(DBDate::ISO_DATE); return $formatter; } - public function FieldHolder($properties = array()) - { - return $this->renderWithClientView(function () use ($properties) { - return parent::FieldHolder($properties); - }); - } - - public function SmallFieldHolder($properties = array()) - { - return $this->renderWithClientView(function () use ($properties) { - return parent::SmallFieldHolder($properties); - }); - } - - /** - * Generate field with client view enabled - * - * @param callable $callback - * @return string - */ - protected function renderWithClientView($callback) - { - $clientView = null; - if ($this->getShowCalendar()) { - $clientView = $this->getClientView(); - $clientView->onBeforeRender(); - } - $html = $callback(); - if ($clientView) { - $html = $clientView->onAfterRender($html); - } - return $html; - } - public function getAttributes() { $attributes = parent::getAttributes(); - // Merge with client config - $config = $this->getClientConfig(); - foreach ($config as $key => $value) { - $attributes["data-{$key}"] = $value; + $attributes['lang'] = i18n::convert_rfc1766($this->getLocale()); + + if ($this->getHTML5()) { + $attributes['type'] = 'date'; + $attributes['min'] = $this->getMinDate(); + $attributes['max'] = $this->getMaxDate(); } return $attributes; @@ -434,7 +423,9 @@ class DateField extends TextField } /** - * Caution: Will not update the 'dateformat' config value. + * Determines the presented/processed format based on locale defaults, + * instead of explicitly setting {@link setDateFormat()}. + * Only applicable with {@link setHTML5(false)}. * * @param string $locale * @return $this @@ -445,29 +436,6 @@ class DateField extends TextField return $this; } - /** - * Get locale code for client-side. Will default to getLocale() if omitted. - * - * @return string - */ - public function getClientLocale() - { - if ($this->clientLocale) { - return $this->clientLocale; - } - return $this->getLocale(); - } - - /** - * @param string $clientLocale - * @return DateField - */ - public function setClientLocale($clientLocale) - { - $this->clientLocale = $clientLocale; - return $this; - } - public function getSchemaValidation() { $rules = parent::getSchemaValidation(); @@ -475,28 +443,6 @@ class DateField extends TextField return $rules; } - /** - * If placeholders are shown - * - * @return bool - */ - public function getPlaceholders() - { - return $this->placeholders; - } - - /** - * Set if placeholders are shown - * - * @param bool $placeholders - * @return $this - */ - public function setPlaceholders($placeholders) - { - $this->placeholders = $placeholders; - return $this; - } - /** * @return string */ @@ -533,35 +479,6 @@ class DateField extends TextField return $this; } - /** - * Get client data properties for this field - * - * @return array - */ - public function getClientConfig() - { - $view = $this->getClientView(); - $config = [ - 'showcalendar' => $this->getShowCalendar() ? 'true' : null, - 'date-format' => $view->getDateFormat(), // https://api.jqueryui.com/datepicker/#option-dateFormat - 'locale' => $view->getLocale(), - ]; - - // Format min/maxDate in format expected by jquery datepicker - $min = $this->getMinDate(); - if ($min) { - // https://api.jqueryui.com/datepicker/#option-minDate - $config['min-date'] = $this->iso8601ToLocalised($min); - } - $max = $this->getMaxDate(); - if ($max) { - // https://api.jqueryui.com/datepicker/#option-maxDate - $config['max-date'] = $this->iso8601ToLocalised($max); - } - - return $config; - } - /** * Convert date localised in the current locale to ISO 8601 date * @@ -627,12 +544,4 @@ class DateField extends TextField } return $formatter->format($timestamp); } - - /** - * @return DateField_View_JQuery - */ - protected function getClientView() - { - return DateField_View_JQuery::create($this); - } } diff --git a/src/Forms/DateField_View_JQuery.php b/src/Forms/DateField_View_JQuery.php deleted file mode 100644 index b00539b80..000000000 --- a/src/Forms/DateField_View_JQuery.php +++ /dev/null @@ -1,197 +0,0 @@ - 'en-GB', - 'en_US' => 'en', - 'en_NZ' => 'en-GB', - 'fr_CH' => 'fr', - 'pt_BR' => 'pt-BR', - 'sr_SR' => 'sr-SR', - 'zh_CN' => 'zh-CN', - 'zh_HK' => 'zh-HK', - 'zh_TW' => 'zh-TW', - ); - - /** - * @param DateField $field - */ - public function __construct($field) - { - $this->field = $field; - - // Health check - if (!$this->localePath('en')) { - throw new InvalidArgumentException("Missing jquery config"); - } - } - - /** - * @return DateField - */ - public function getField() - { - return $this->field; - } - - /** - * Get path to localisation file for a given locale, if it exists - * - * @param string $lang - * @return string Relative path to file, or null if it isn't available - */ - protected function localePath($lang) - { - $path = ADMIN_THIRDPARTY_DIR . "/jquery-ui/datepicker/i18n/jquery.ui.datepicker-{$lang}.js"; - if (file_exists(BASE_PATH . '/' . $path)) { - return $path; - } - return null; - } - - public function onBeforeRender() - { - } - - /** - * @param String $html - * @return string - */ - public function onAfterRender($html) - { - if ($this->getField()->getShowCalendar()) { - // Load config for this locale if available - $locale = $this->getLocale(); - $localeFile = $this->localePath($locale); - if ($localeFile) { - Requirements::javascript($localeFile); - } - } - - return $html; - } - - /** - * Determines which language to use for jQuery UI, which - * can be different from the value set in i18n. - * - * @return string - */ - public function getLocale() - { - $locale = $this->getField()->getClientLocale(); - - // Check standard mappings - $map = Config::inst()->get(__CLASS__, 'locale_map'); - if (array_key_exists($locale, $map)) { - return $map[$locale]; - } - - // Fall back to default lang (meaning "en_US" turns into "en") - return i18n::getData()->langFromLocale($locale); - } - - /** - * Convert iso to jquery UI date format. - * Needs to be consistent with Zend formatting, otherwise validation will fail. - * Removes all time settings like hour/minute/second from the format. - * See http://docs.jquery.com/UI/Datepicker/formatDate - * From http://userguide.icu-project.org/formatparse/datetime - * - * @param string $format - * @return string - */ - public static function convert_iso_to_jquery_format($format) - { - $convert = array( - '/([^d])d([^d])/' => '$1d$2', - '/^d([^d])/' => 'd$1', - '/([^d])d$/' => '$1d', - '/dd/' => 'dd', - '/SS/' => '', - '/eee/' => 'd', - '/e/' => 'N', - '/D/' => '', - '/EEEE/' => 'DD', - '/EEE/' => 'D', - '/w/' => '', - // make single "M" lowercase - '/([^M])M([^M])/' => '$1m$2', - // make single "M" at start of line lowercase - '/^M([^M])/' => 'm$1', - // make single "M" at end of line lowercase - '/([^M])M$/' => '$1m', - // match exactly three capital Ms not preceeded or followed by an M - '/(? 'M', - // match exactly two capital Ms not preceeded or followed by an M - '/(? 'mm', - // match four capital Ms (maximum allowed) - '/MMMM/' => 'MM', - '/l/' => '', - '/YYYY/' => 'yy', - '/yyyy/' => 'yy', - // See http://open.silverstripe.org/ticket/7669 - '/y{1,3}/' => 'yy', - '/a/' => '', - '/B/' => '', - '/hh/' => '', - '/h/' => '', - '/([^H])H([^H])/' => '', - '/^H([^H])/' => '', - '/([^H])H$/' => '', - '/HH/' => '', - // '/mm/' => '', - '/ss/' => '', - '/zzzz/' => '', - '/I/' => '', - '/ZZZZ/' => '', - '/Z/' => '', - '/z/' => '', - '/X/' => '', - '/r/' => '', - '/U/' => '', - ); - $patterns = array_keys($convert); - $replacements = array_values($convert); - - return preg_replace($patterns, $replacements, $format); - } - - /** - * Get client date format - * - * @return string - */ - public function getDateFormat() - { - return static::convert_iso_to_jquery_format($this->getField()->getDateFormat()); - } -} diff --git a/src/Forms/DatetimeField.php b/src/Forms/DatetimeField.php index 4190e2d3e..b47933faa 100644 --- a/src/Forms/DatetimeField.php +++ b/src/Forms/DatetimeField.php @@ -13,6 +13,10 @@ use SilverStripe\i18n\i18n; * 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()}. + * * # Configuration * * Individual options are configured either on the DatetimeField, or on individual diff --git a/src/Forms/SeparatedDateField.php b/src/Forms/SeparatedDateField.php deleted file mode 100644 index 8580a1e48..000000000 --- a/src/Forms/SeparatedDateField.php +++ /dev/null @@ -1,177 +0,0 @@ -iso8601ToArray($this->dataValue()); - $fieldDay = NumericField::create($this->name . '[day]', false, $valArr ? $valArr['day'] : null) - ->addExtraClass('day') - ->setHTML5(true) - ->setMaxLength(2); - $fieldMonth = NumericField::create($this->name . '[month]', false, $valArr ? $valArr['month'] : null) - ->addExtraClass('month') - ->setHTML5(true) - ->setMaxLength(2); - $fieldYear = NumericField::create($this->name . '[year]', false, $valArr ? $valArr['year'] : null) - ->addExtraClass('year') - ->setHTML5(true) - ->setMaxLength(4); - - // Set placeholders - if ($this->getPlaceholders()) { - $fieldDay->setAttribute('placeholder', _t(__CLASS__ . '.DAY', 'Day')); - $fieldMonth->setAttribute('placeholder', _t(__CLASS__ . '.MONTH', 'Month')); - $fieldYear->setAttribute('placeholder', _t(__CLASS__ . '.YEAR', 'Year')); - } - - $format = $this->getDateFormat(); - $validFormat = ( - stripos($format, 'd') !== false - && stripos($format, 'm') !== false - && stripos($format, 'y') !== false - ); - if (!$validFormat) { - throw new \InvalidArgumentException( - 'Invalid date format for field ordering: ' . $format - . '. Requires "d", "m", and "y" values to determine order' - ); - } - - $fields = array(); - $fields[stripos($format, 'd')] = $fieldDay->Field(); - $fields[stripos($format, 'm')] = $fieldMonth->Field(); - $fields[stripos($format, 'y')] = $fieldYear->Field(); - ksort($fields); - - - // Join all fields - $sep = ' ' . $this->getSeparator() . ' '; - return implode($sep, $fields); - } - - /** - * @param $string - * @return self - */ - public function setSeparator($separator) - { - $this->separator = $separator; - return $this; - } - - /** - * @return string - */ - public function getSeparator() - { - return $this->separator; - } - - /** - * Convert array to timestamp - * - * @param array $value - * @return string - */ - public function arrayToISO8601($value) - { - if ($this->isEmptyArray($value)) { - return null; - } - - // ensure all keys are specified - if (!isset($value['month']) || !isset($value['day']) || !isset($value['year'])) { - return null; - } - - // Ensure valid range - if (!checkdate($value['month'], $value['day'], $value['year'])) { - return null; - } - - // Note: Set formatter to strict for array input - $formatter = $this->getISO8601Formatter(); - $timestamp = mktime(0, 0, 0, $value['month'], $value['day'], $value['year']); - if ($timestamp === false) { - return null; - } - return $formatter->format($timestamp); - } - - /** - * Convert iso 8601 date to array (day / month / year) - * - * @param string $date - * @return array|null Array form, or null if not valid - */ - public function iso8601ToArray($date) - { - if (!$date) { - return null; - } - $formatter = $this->getISO8601Formatter(); - $timestamp = $formatter->parse($date); - if ($timestamp === false) { - return null; - } - - // Format time manually into an array - return [ - 'day' => date('j', $timestamp), - 'month' => date('n', $timestamp), - 'year' => date('Y', $timestamp), - ]; - } - - /** - * Assign value posted from form submission - * - * @param mixed $value - * @param mixed $data - * @return $this - */ - public function setSubmittedValue($value, $data = null) - { - // Filter out empty arrays - if ($this->isEmptyArray($value)) { - $value = null; - } - $this->rawValue = $value; - - // Null case - if (!$value || !is_array($value)) { - $this->value = null; - return $this; - } - - // Parse - $this->value = $this->arrayToISO8601($value); - return $this; - } - - /** - * Check if this array is empty - * - * @param $value - * @return bool - */ - public function isEmptyArray($value) - { - return is_array($value) && !array_filter($value); - } -} diff --git a/src/Forms/TimeField.php b/src/Forms/TimeField.php index ee5427510..94d480236 100644 --- a/src/Forms/TimeField.php +++ b/src/Forms/TimeField.php @@ -6,6 +6,7 @@ use IntlDateFormatter; use InvalidArgumentException; use SilverStripe\i18n\i18n; use SilverStripe\ORM\FieldType\DBDatetime; +use SilverStripe\ORM\FieldType\DBTime; /** * Form field to display editable time values in an field. @@ -57,6 +58,31 @@ class TimeField extends TextField */ protected $timezone = null; + /** + * Use HTML5-based input fields (and force ISO 8601 time formats). + * + * @var bool + */ + protected $html5 = true; + + /** + * @return bool + */ + public function getHTML5() + { + return $this->html5; + } + + /** + * @param boolean $bool + * @return $this + */ + public function setHTML5($bool) + { + $this->html5 = $bool; + return $this; + } + /** * Get time format in CLDR standard format * @@ -67,6 +93,11 @@ class TimeField extends TextField */ public function getTimeFormat() { + if ($this->getHTML5()) { + // Browsers expect ISO 8601 times, localisation is handled on the client + $this->setTimeFormat(DBTime::ISO_TIME); + } + if ($this->timeFormat) { return $this->timeFormat; } @@ -77,6 +108,7 @@ class TimeField extends TextField /** * Set time 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 @@ -108,12 +140,8 @@ class TimeField extends TextField } /** - * Get length of the time format to use. One of: - * - * - IntlDateFormatter::SHORT E.g. '6:31 PM' - * - IntlDateFormatter::MEDIUM E.g. '6:30:48 PM' - * - IntlDateFormatter::LONG E.g. '6:32:09 PM NZDT' - * - IntlDateFormatter::FULL E.g. '6:32:24 PM New Zealand Daylight Time' + * Get length of the time format to use. + * Only applicable with {@link setHTML5(false)}. * * @see http://php.net/manual/en/class.intldateformatter.php#intl.intldateformatter-constants * @@ -133,6 +161,24 @@ class TimeField extends TextField */ protected function getFormatter() { + if ($this->getHTML5() && $this->timeFormat && $this->timeFormat !== DBTime::ISO_TIME) { + throw new \LogicException( + 'Please opt-out of HTML5 processing of ISO 8601 times via setHTML5(false) if using setTimeFormat()' + ); + } + + if ($this->getHTML5() && $this->timeLength) { + throw new \LogicException( + 'Please opt-out of HTML5 processing of ISO 8601 times via setHTML5(false) if using setTimeLength()' + ); + } + + if ($this->getHTML5() && $this->locale) { + throw new \LogicException( + 'Please opt-out of HTML5 processing of ISO 8601 times via setHTML5(false) if using setLocale()' + ); + } + $formatter = IntlDateFormatter::create( $this->getLocale(), IntlDateFormatter::NONE, @@ -140,8 +186,11 @@ class TimeField extends TextField $this->getTimezone() ); - // Don't invoke getDateFormat() directly to avoid infinite loop - if ($this->timeFormat) { + if ($this->getHTML5()) { + // Browsers expect ISO 8601 times, localisation is handled on the client + $formatter->setPattern(DBTime::ISO_TIME); + // Don't invoke getTimeFormat() directly to avoid infinite loop + } elseif ($this->timeFormat) { $ok = $formatter->setPattern($this->timeFormat); if (!$ok) { throw new InvalidArgumentException("Invalid time format {$this->timeFormat}"); @@ -164,35 +213,22 @@ class TimeField extends TextField date_default_timezone_get() // Default to server timezone ); $formatter->setLenient(false); - // CLDR iso8601 time + // Note we omit timezone from this format, and we assume server TZ always. - $formatter->setPattern('HH:mm:ss'); + $formatter->setPattern(DBTime::ISO_TIME); + return $formatter; } - public function getAttribute($name) + public function getAttributes() { $attributes = parent::getAttributes(); - // Merge with client config - $config = $this->getClientConfig(); - foreach ($config as $key => $value) { - $attributes["data-{$key}"] = $value; + if ($this->getHTML5()) { + $attributes['type'] = 'time'; } - return $attributes; - } - /** - * Get client config options for this field - * - * @return array - */ - public function getClientConfig() - { - return [ - // @todo - Support javascript time picker - 'timeformat' => $this->getTimeFormat(), - ]; + return $attributes; } public function Type() @@ -302,6 +338,10 @@ class TimeField extends TextField } /** + * Determines the presented/processed format based on locale defaults, + * instead of explicitly setting {@link setTimeFormat()}. + * Only applicable with {@link setHTML5(false)}. + * * @param string $locale * @return $this */ @@ -337,9 +377,19 @@ class TimeField extends TextField $fromFormatter = $this->getFormatter(); $toFormatter = $this->getISO8601Formatter(); $timestamp = $fromFormatter->parse($time); + + // 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'); + $timestamp = $fromFormatter->parse($time); + } + + // If timestamp still can't be detected, we've got an invalid time if ($timestamp === false) { return null; } + return $toFormatter->format($timestamp); } diff --git a/src/ORM/FieldType/DBDate.php b/src/ORM/FieldType/DBDate.php index 1e1fd26e8..4eeb6a0a7 100644 --- a/src/ORM/FieldType/DBDate.php +++ b/src/ORM/FieldType/DBDate.php @@ -512,17 +512,7 @@ class DBDate extends DBField public function scaffoldFormField($title = null, $params = null) { $field = DateField::create($this->name, $title); - $format = $field->getDateFormat(); - - // Show formatting hints for better usability - $now = DBDatetime::now()->Format($format); - $field->setDescription(_t( - 'FormField.EXAMPLE', - 'e.g. {format}', - 'Example format', - [ 'format' => $now ] - )); - $field->setAttribute('placeholder', $format); + $field->setHTML5(true); return $field; } diff --git a/src/ORM/FieldType/DBTime.php b/src/ORM/FieldType/DBTime.php index d6eac7f06..b0e596956 100644 --- a/src/ORM/FieldType/DBTime.php +++ b/src/ORM/FieldType/DBTime.php @@ -141,19 +141,7 @@ class DBTime extends DBField public function scaffoldFormField($title = null, $params = null) { - $field = TimeField::create($this->name, $title); - $format = $field->getTimeFormat(); - - // Show formatting hints for better usability - $now = DBDatetime::now()->Format($format); - $field->setDescription(_t( - 'FormField.Example', - 'e.g. {format}', - 'Example format', - [ 'format' => $now ] - )); - $field->setAttribute('placeholder', $format); - return $field; + return TimeField::create($this->name, $title); } /** diff --git a/src/Security/Member.php b/src/Security/Member.php index e7090a706..22174dcc2 100644 --- a/src/Security/Member.php +++ b/src/Security/Member.php @@ -19,7 +19,6 @@ use SilverStripe\Forms\DropdownField; use SilverStripe\Forms\FieldList; use SilverStripe\Forms\HTMLEditor\HTMLEditorConfig; use SilverStripe\Forms\ListboxField; -use SilverStripe\Forms\MemberDatetimeOptionsetField; use SilverStripe\i18n\i18n; use SilverStripe\MSSQL\MSSQLDatabase; use SilverStripe\ORM\ArrayList; @@ -81,9 +80,6 @@ class Member extends DataObject implements TemplateGlobalProvider 'Locale' => 'Varchar(6)', // handled in registerFailedLogin(), only used if $lock_out_after_incorrect_logins is set 'FailedLoginCount' => 'Int', - // In ISO format - 'DateFormat' => 'Varchar(30)', - 'TimeFormat' => 'Varchar(30)', ); private static $belongs_many_many = array( @@ -1315,19 +1311,23 @@ class Member extends DataObject implements TemplateGlobalProvider } /** - * Override the default getter for DateFormat so the - * default format for the user's locale is used - * if the user has not defined their own. + * Return the date format based on the user's chosen locale, + * falling back to the default format defined by the {@link i18n.get_locale()} setting. * * @return string ISO date format */ public function getDateFormat() { - $format = $this->getField('DateFormat'); - if ($format) { - return $format; - } - return $this->getDefaultDateFormat(); + $formatter = new IntlDateFormatter( + $this->getLocale(), + IntlDateFormatter::MEDIUM, + IntlDateFormatter::NONE + ); + $format = $formatter->getPattern(); + + $this->extend('updateDateFormat', $format); + + return $format; } /** @@ -1343,19 +1343,23 @@ class Member extends DataObject implements TemplateGlobalProvider } /** - * Override the default getter for TimeFormat so the - * default format for the user's locale is used - * if the user has not defined their own. + * Return the time format based on the user's chosen locale, + * falling back to the default format defined by the {@link i18n.get_locale()} setting. * * @return string ISO date format */ public function getTimeFormat() { - $timeFormat = $this->getField('TimeFormat'); - if ($timeFormat) { - return $timeFormat; - } - return $this->getDefaultTimeFormat(); + $formatter = new IntlDateFormatter( + $this->getLocale(), + IntlDateFormatter::NONE, + IntlDateFormatter::MEDIUM + ); + $format = $formatter->getPattern(); + + $this->extend('updateTimeFormat', $format); + + return $format; } //---------------------------------------------------------------------// @@ -1592,112 +1596,11 @@ class Member extends DataObject implements TemplateGlobalProvider if ($permissionsTab) { $permissionsTab->addExtraClass('readonly'); } - - // Date format selecter - $mainFields->push( - $dateFormatField = new MemberDatetimeOptionsetField( - 'DateFormat', - $this->fieldLabel('DateFormat'), - $this->getDateFormats() - ) - ); - $formatClass = get_class($dateFormatField); - $dateFormatField->setValue($this->DateFormat); - $dateTemplate = SSViewer::get_templates_by_class($formatClass, '_description_date', $formatClass); - $dateFormatField->setDescriptionTemplate($dateTemplate); - - // Time format selector - $mainFields->push( - $timeFormatField = new MemberDatetimeOptionsetField( - 'TimeFormat', - $this->fieldLabel('TimeFormat'), - $this->getTimeFormats() - ) - ); - $timeFormatField->setValue($this->TimeFormat); - $timeTemplate = SSViewer::get_templates_by_class($formatClass, '_description_time', $formatClass); - $timeFormatField->setDescriptionTemplate($timeTemplate); }); return parent::getCMSFields(); } - /** - * Get list of date formats with example values - * - * @return array - */ - protected function getDateFormats() - { - $defaultDateFormat = $this->getDefaultDateFormat(); - $formats = [ - 'MMM d, y' => null, - 'yyyy/MM/dd' => null, - 'MM/dd/y' => null, - 'dd/MM/y' => null, - ]; - unset($formats[$defaultDateFormat]); - $formats[$defaultDateFormat] = null; - // Fill in each format with example - foreach (array_keys($formats) as $format) { - $formats[$format] = DBDatetime::now()->Format($format); - } - // Mark default format - $formats[$defaultDateFormat] .= sprintf(' (%s)', _t('Member.DefaultDateTime', 'default')); - return $formats; - } - - /** - * @return string - */ - public function getDefaultDateFormat() - { - $formatter = new IntlDateFormatter( - $this->getLocale(), - IntlDateFormatter::MEDIUM, - IntlDateFormatter::NONE - ); - return $formatter->getPattern(); - } - - /** - * @return string - */ - public function getDefaultTimeFormat() - { - $formatter = new IntlDateFormatter( - $this->getLocale(), - IntlDateFormatter::NONE, - IntlDateFormatter::MEDIUM - ); - $defaultTimeFormat = $formatter->getPattern(); - return $defaultTimeFormat; - } - - - /** - * Get list of date formats with example values - * - * @return array - */ - protected function getTimeFormats() - { - $defaultTimeFormat = $this->getDefaultTimeFormat(); - $formats = [ - 'h:mm a' => null, - 'H:mm' => null, - ]; - unset($formats[$defaultTimeFormat]); - $formats[$defaultTimeFormat] = null; - // Fill in each format with example - foreach (array_keys($formats) as $format) { - $formats[$format] = DBDatetime::now()->Format($format); - } - // Mark default format - $formats[$defaultTimeFormat] .= sprintf(' (%s)', _t('Member.DefaultDateTime', 'default')); - return $formats; - } - /** * @param bool $includerelations Indicate if the labels returned include relation fields * @return array @@ -1714,8 +1617,6 @@ class Member extends DataObject implements TemplateGlobalProvider $labels['PasswordExpiry'] = _t('Member.db_PasswordExpiry', 'Password Expiry Date', 'Password expiry date'); $labels['LockedOutUntil'] = _t('Member.db_LockedOutUntil', 'Locked out until', 'Security related date'); $labels['Locale'] = _t('Member.db_Locale', 'Interface Locale'); - $labels['DateFormat'] = _t('Member.DATEFORMAT', 'Date format'); - $labels['TimeFormat'] = _t('Member.TIMEFORMAT', 'Time format'); if ($includerelations) { $labels['Groups'] = _t( 'Member.belongs_many_many_Groups', diff --git a/src/i18n/i18n.php b/src/i18n/i18n.php index ee7c5fcea..732eb3922 100644 --- a/src/i18n/i18n.php +++ b/src/i18n/i18n.php @@ -85,12 +85,20 @@ class i18n implements TemplateGlobalProvider private static $default_locale = 'en_US'; /** + * System-wide date format. Will be overruled for CMS UI display + * by the format defaults inferred from the browser as well as + * any user-specific locale preferences. + * * @config * @var string */ private static $date_format = 'yyyy-MM-dd'; /** + * System-wide time format. Will be overruled for CMS UI display + * by the format defaults inferred from the browser as well as + * any user-specific locale preferences. + * * @config * @var string */ diff --git a/tests/php/Forms/DateFieldTest.php b/tests/php/Forms/DateFieldTest.php index fbfe18e89..676ae58eb 100644 --- a/tests/php/Forms/DateFieldTest.php +++ b/tests/php/Forms/DateFieldTest.php @@ -2,9 +2,9 @@ namespace SilverStripe\Forms\Tests; +use IntlDateFormatter; use SilverStripe\Dev\SapphireTest; use SilverStripe\Forms\DateField; -use SilverStripe\Forms\SeparatedDateField; use SilverStripe\Forms\RequiredFields; use SilverStripe\i18n\i18n; use SilverStripe\ORM\FieldType\DBDatetime; @@ -105,21 +105,11 @@ class DateFieldTest extends SapphireTest public function testSetValueWithDateString() { $f = new DateField('Date', 'Date'); + $f->setHTML5(false); $f->setSubmittedValue('29/03/2003'); $this->assertEquals($f->dataValue(), '2003-03-29'); } - public function testSetValueWithDateArray() - { - $f = new SeparatedDateField('Date', 'Date'); - $f->setSubmittedValue([ - 'day' => 29, - 'month' => 03, - 'year' => 2003 - ]); - $this->assertEquals($f->dataValue(), '2003-03-29'); - } - public function testConstructorWithIsoDate() { // used by Form->loadDataFrom() @@ -145,84 +135,11 @@ class DateFieldTest extends SapphireTest $this->assertFalse($f->validate(new RequiredFields())); } - public function testEmptyValueValidation() - { - $validator = new RequiredFields(); - $field = new SeparatedDateField('Date'); - $this->assertTrue($field->validate($validator)); - $field->setSubmittedValue([ - 'day' => '', - 'month' => '', - 'year' => '', - ]); - $this->assertTrue($field->validate($validator)); - } - - public function testValidateArray() - { - $f = new SeparatedDateField('Date', 'Date'); - $f->setSubmittedValue([ - 'day' => 29, - 'month' => 03, - 'year' => 2003 - ]); - $this->assertTrue($f->validate(new RequiredFields())); - - $f->setValue(null); - $this->assertTrue($f->validate(new RequiredFields()), 'NULL values are validating TRUE'); - - $f->setSubmittedValue(array()); - $this->assertTrue($f->validate(new RequiredFields()), 'Empty array values are validating TRUE'); - - $f->setSubmittedValue([ - 'day' => null, - 'month' => null, - 'year' => null - ]); - $this->assertTrue($f->validate(new RequiredFields()), 'Empty array values with keys are validating TRUE'); - $f->setSubmittedValue([ - 'day' => 9999, - 'month' => 9999, - 'year' => 9999 - ]); - $this->assertFalse($f->validate(new RequiredFields())); - } - - public function testValidateEmptyArrayValuesSetsNullForValueObject() - { - $f = new SeparatedDateField('Date', 'Date'); - $f->setSubmittedValue([ - 'day' => '', - 'month' => '', - 'year' => '' - ]); - $this->assertNull($f->dataValue()); - - $f->setSubmittedValue([ - 'day' => null, - 'month' => null, - 'year' => null - ]); - $this->assertNull($f->dataValue()); - } - - public function testValidateArrayValue() - { - $f = new SeparatedDateField('Date', 'Date'); - $f->setSubmittedValue(['day' => 29, 'month' => 03, 'year' => 2003]); - $this->assertTrue($f->validate(new RequiredFields())); - - $f->setSubmittedValue(['month' => 03, 'year' => 2003]); - $this->assertFalse($f->validate(new RequiredFields())); - - $f->setSubmittedValue(array('day' => 99, 'month' => 99, 'year' => 2003)); - $this->assertFalse($f->validate(new RequiredFields())); - } - public function testFormatEnNz() { /* We get YYYY-MM-DD format as the data value for DD/MM/YYYY input value */ $f = new DateField('Date', 'Date'); + $f->setHTML5(false); $f->setSubmittedValue('29/03/2003'); $this->assertEquals($f->dataValue(), '2003-03-29'); } @@ -232,6 +149,7 @@ class DateFieldTest extends SapphireTest // should get en_NZ by default through setUp() i18n::set_locale('de_DE'); $f = new DateField('Date', 'Date', '29/03/2003'); + $f->setHTML5(false); $f->setValue('29.06.2006'); $this->assertEquals($f->dataValue(), '2006-06-29'); } @@ -242,6 +160,7 @@ class DateFieldTest extends SapphireTest public function testMDYFormat() { $dateField = new DateField('Date', 'Date'); + $dateField->setHTML5(false); $dateField->setDateFormat('d/M/y'); $dateField->setSubmittedValue('31/03/2003'); $this->assertEquals( @@ -251,6 +170,7 @@ class DateFieldTest extends SapphireTest ); $dateField2 = new DateField('Date', 'Date'); + $dateField2->setHTML5(false); $dateField2->setDateFormat('d/M/y'); $dateField2->setSubmittedValue('04/3/03'); $this->assertEquals( @@ -259,4 +179,40 @@ class DateFieldTest extends SapphireTest "Even if input value hasn't got leading 0's in it we still get the correct data value" ); } + + /** + * @expectedException \LogicException + * @expectedExceptionMessageRegExp /Please opt-out .* if using setDateFormat/ + */ + public function testHtml5WithCustomFormatThrowsException() + { + $dateField = new DateField('Date', 'Date'); + $dateField->setValue('2010-03-31'); + $dateField->setDateFormat('d/M/y'); + $dateField->Value(); + } + + /** + * @expectedException \LogicException + * @expectedExceptionMessageRegExp /Please opt-out .* if using setDateLength/ + */ + public function testHtml5WithCustomDateLengthThrowsException() + { + $dateField = new DateField('Date', 'Date'); + $dateField->setValue('2010-03-31'); + $dateField->setDateLength(IntlDateFormatter::MEDIUM); + $dateField->Value(); + } + + /** + * @expectedException \LogicException + * @expectedExceptionMessageRegExp /Please opt-out .* if using setLocale/ + */ + public function testHtml5WithCustomLocaleThrowsException() + { + $dateField = new DateField('Date', 'Date'); + $dateField->setValue('2010-03-31'); + $dateField->setLocale('de_DE'); + $dateField->Value(); + } } diff --git a/tests/php/Forms/DatefieldViewJQueryTest.php b/tests/php/Forms/DatefieldViewJQueryTest.php deleted file mode 100644 index 83b7a931a..000000000 --- a/tests/php/Forms/DatefieldViewJQueryTest.php +++ /dev/null @@ -1,47 +0,0 @@ -assertEquals( - 'M d, yy', - DateField_View_JQuery::convert_iso_to_jquery_format('MMM d, yyyy') - ); - - $this->assertEquals( - 'd/mm/yy', - DateField_View_JQuery::convert_iso_to_jquery_format('d/MM/yyyy') - ); - - $this->assertEquals( - 'dd.m.yy', - DateField_View_JQuery::convert_iso_to_jquery_format('dd.M.yyyy'), - 'Month, no leading zero' - ); - - $this->assertEquals( - 'dd.mm.yy', - DateField_View_JQuery::convert_iso_to_jquery_format('dd.MM.yyyy'), - 'Month, two digit' - ); - - $this->assertEquals( - 'dd.M.yy', - DateField_View_JQuery::convert_iso_to_jquery_format('dd.MMM.yyyy'), - 'Abbreviated month name' - ); - - $this->assertEquals( - 'dd.MM.yy', - DateField_View_JQuery::convert_iso_to_jquery_format('dd.MMMM.yyyy'), - 'Full month name' - ); - } -} diff --git a/tests/php/Forms/DatetimeFieldTest.php b/tests/php/Forms/DatetimeFieldTest.php index 9969bb24f..f0736f44a 100644 --- a/tests/php/Forms/DatetimeFieldTest.php +++ b/tests/php/Forms/DatetimeFieldTest.php @@ -7,7 +7,6 @@ use SilverStripe\Control\Controller; use SilverStripe\Forms\DatetimeField; use SilverStripe\Forms\RequiredFields; use SilverStripe\Forms\DateField; -use SilverStripe\Forms\SeparatedDateField; use SilverStripe\Forms\Tests\DatetimeFieldTest\Model; use SilverStripe\Forms\TimeField; use SilverStripe\Forms\FieldList; @@ -38,6 +37,32 @@ class DatetimeFieldTest extends SapphireTest $form = $this->getMockForm(); $form->Fields()->push($dateTimeField); + $dateTimeField->setSubmittedValue([ + 'date' => '2003-03-29', + 'time' => '23: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); + } + + public function testFormSaveIntoLocalised() + { + $dateTimeField = new DatetimeField('MyDatetime'); + + $dateTimeField->getDateField() + ->setHTML5(false) + ->setLocale('en_NZ'); + + $dateTimeField->getTimeField() + ->setHTML5(false) + ->setLocale('en_NZ'); + + $form = $this->getMockForm(); + $form->Fields()->push($dateTimeField); + // en_NZ standard format $dateTimeField->setSubmittedValue([ 'date' => '29/03/2003', @@ -97,6 +122,25 @@ class DatetimeFieldTest extends SapphireTest public function testSetValueWithArray() { $datetimeField = new DatetimeField('Datetime', 'Datetime'); + $datetimeField->setSubmittedValue([ + 'date' => '2003-03-29', + 'time' => '23:00:00' + ]); + $this->assertEquals($datetimeField->dataValue(), '2003-03-29 23:00:00'); + } + + public function testSetValueWithArrayLocalised() + { + $datetimeField = new DatetimeField('Datetime', 'Datetime'); + + $datetimeField->getDateField() + ->setHTML5(false) + ->setLocale('en_NZ'); + + $datetimeField->getTimeField() + ->setHTML5(false) + ->setLocale('en_NZ'); + // Values can only be localized (= non-ISO) in array notation $datetimeField->setSubmittedValue([ 'date' => '29/03/2003', @@ -105,17 +149,6 @@ class DatetimeFieldTest extends SapphireTest $this->assertEquals($datetimeField->dataValue(), '2003-03-29 23:00:00'); } - public function testSetValueWithDmyArray() - { - $f = new DatetimeField('Datetime', 'Datetime'); - $f->setDateField(new SeparatedDateField('Datetime[date]')); - $f->setSubmittedValue([ - 'date' => ['day' => 29, 'month' => 03, 'year' => 2003], - 'time' => '11:00:00 pm' - ]); - $this->assertEquals($f->dataValue(), '2003-03-29 23:00:00'); - } - public function testValidate() { $f = new DatetimeField('Datetime', 'Datetime', '2003-03-29 23:59:38'); @@ -128,11 +161,20 @@ class DatetimeFieldTest extends SapphireTest $this->assertFalse($f->validate(new RequiredFields())); } - public function testTimezoneSet() + public function testTimezoneSetLocalised() { date_default_timezone_set('Europe/Berlin'); // 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() + ->setHTML5(false) + ->setLocale('en_NZ'); + $datetimeField->setTimezone('Pacific/Auckland'); $datetimeField->setValue('2003-12-24 23:59:59'); $this->assertEquals( @@ -149,11 +191,20 @@ class DatetimeFieldTest extends SapphireTest ); } - public function testTimezoneFromConfig() + public function testTimezoneFromConfigLocalised() { date_default_timezone_set('Europe/Berlin'); // 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() + ->setHTML5(false) + ->setLocale('en_NZ'); + $datetimeField->setTimezone('Europe/Moscow'); $datetimeField->setSubmittedValue([ // pass in default format, at user time (Moscow) @@ -170,7 +221,7 @@ class DatetimeFieldTest extends SapphireTest $field = new DatetimeField('Datetime', 'Datetime'); $field->setForm($form); $field->setSubmittedValue([ - 'date' => '24/06/2003', + 'date' => '2003-06-24', 'time' => '23:59:59', ]); $dateField = new DateField('Datetime[date]'); @@ -195,8 +246,8 @@ class DatetimeFieldTest extends SapphireTest $field = new DatetimeField('Datetime', 'Datetime'); $field->setForm($form); $field->setSubmittedValue([ - 'date' => '24/06/2003', - 'time' => '11:59:59 pm', + 'date' => '2003-06-24', + 'time' => '23:59:59', ]); $timeField = new TimeField('Datetime[time]'); $field->setTimeField($timeField); diff --git a/tests/php/Forms/MemberDatetimeOptionsetFieldTest.php b/tests/php/Forms/MemberDatetimeOptionsetFieldTest.php deleted file mode 100644 index 5755b4fa8..000000000 --- a/tests/php/Forms/MemberDatetimeOptionsetFieldTest.php +++ /dev/null @@ -1,177 +0,0 @@ -getDefaultDateFormat(); - $dateFormatMap = array( - 'yyyy-MM-dd' => DBDatetime::now()->Format('yyyy-MM-dd'), - 'yyyy/MM/dd' => DBDatetime::now()->Format('yyyy/MM/dd'), - 'MM/dd/yyyy' => DBDatetime::now()->Format('MM/dd/yyyy'), - 'dd/MM/yyyy' => DBDatetime::now()->Format('dd/MM/yyyy'), - ); - $dateFormatMap[$defaultDateFormat] = DBDatetime::now()->Format($defaultDateFormat) . ' (default)'; - $field = new MemberDatetimeOptionsetField( - 'DateFormat', - 'Date format', - $dateFormatMap - ); - $field->setValue($member->getDateFormat()); - return $field; - } - - /** - * @param Member $member - * @return MemberDatetimeOptionsetField - */ - protected function createTimeFormatFieldForMember($member) - { - $defaultTimeFormat = $member->getDefaultTimeFormat(); - $timeFormatMap = array( - 'h:mm a' => DBDatetime::now()->Format('h:mm a'), - 'H:mm' => DBDatetime::now()->Format('H:mm'), - ); - $timeFormatMap[$defaultTimeFormat] = DBDatetime::now()->Format($defaultTimeFormat) . ' (default)'; - $field = new MemberDatetimeOptionsetField( - 'TimeFormat', - 'Time format', - $timeFormatMap - ); - $field->setValue($member->getTimeFormat()); - return $field; - } - - public function testDateFormatDefaultCheckedInFormField() - { - /** @var Member $member */ - $member = $this->objFromFixture(Member::class, 'noformatmember'); - $field = $this->createDateFormatFieldForMember($member); - /** @skipUpgrade */ - $field->setForm( - new Form( - new Controller(), - 'Form', - new FieldList(), - new FieldList() - ) - ); // fake form - // `MMM d, y` is default format for default locale (en_US) - $parser = new CSSContentParser($field->Field()); - $xmlArr = $parser->getBySelector('#Form_Form_DateFormat_MMM_d_y'); - $this->assertEquals('checked', (string) $xmlArr[0]['checked']); - } - - public function testTimeFormatDefaultCheckedInFormField() - { - /** @var Member $member */ - $member = $this->objFromFixture(Member::class, 'noformatmember'); - $field = $this->createTimeFormatFieldForMember($member); - /** @skipUpgrade */ - $field->setForm( - new Form( - new Controller(), - 'Form', - new FieldList(), - new FieldList() - ) - ); // fake form - // `h:mm:ss a` is the default for en_US locale - $parser = new CSSContentParser($field->Field()); - $xmlArr = $parser->getBySelector('#Form_Form_TimeFormat_h:mm:ss_a'); - $this->assertEquals('checked', (string) $xmlArr[0]['checked']); - } - - public function testDateFormatChosenIsCheckedInFormField() - { - /** @var Member $member */ - $member = $this->objFromFixture(Member::class, 'noformatmember'); - $member->setField('DateFormat', 'MM/dd/yyyy'); - $field = $this->createDateFormatFieldForMember($member); - /** @skipUpgrade */ - $field->setForm( - new Form( - new Controller(), - 'Form', - new FieldList(), - new FieldList() - ) - ); // fake form - $parser = new CSSContentParser($field->Field()); - $xmlArr = $parser->getBySelector('#Form_Form_DateFormat_MM_dd_yyyy'); - $this->assertEquals('checked', (string) $xmlArr[0]['checked']); - } - - public function testDateFormatCustomFormatAppearsInCustomInputInField() - { - /** @var Member $member */ - $member = $this->objFromFixture(Member::class, 'noformatmember'); - $member->setField('DateFormat', 'dd MM yy'); - $field = $this->createDateFormatFieldForMember($member); - /** @skipUpgrade */ - $field->setForm( - new Form( - new Controller(), - 'Form', - new FieldList(), - new FieldList() - ) - ); // fake form - $parser = new CSSContentParser($field->Field()); - $xmlInputArr = $parser->getBySelector('.valcustom input'); - $this->assertEquals('checked', (string) $xmlInputArr[0]['checked']); - $this->assertEquals('dd MM yy', (string) $xmlInputArr[1]['value']); - } - - public function testDateFormValid() - { - $field = new MemberDatetimeOptionsetField('DateFormat', 'DateFormat'); - $validator = new RequiredFields(); - $this->assertTrue($field->validate($validator)); - $field->setSubmittedValue([ - 'Options' => '__custom__', - 'Custom' => 'dd MM yyyy' - ]); - $this->assertTrue($field->validate($validator)); - $field->setSubmittedValue([ - 'Options' => '__custom__', - 'Custom' => 'sdfdsfdfd1244' - ]); - // @todo - Be less forgiving of invalid CLDR date format strings - $this->assertTrue($field->validate($validator)); - } - - public function testDescriptionTemplate() - { - $field = new MemberDatetimeOptionsetField('DateFormat', 'DateFormat'); - - $this->assertEmpty($field->getDescription()); - - $field->setDescription('Test description'); - $this->assertEquals('Test description', $field->getDescription()); - - $field->setDescriptionTemplate(get_class($field).'_description_time'); - $this->assertNotEmpty($field->getDescription()); - $this->assertNotEquals('Test description', $field->getDescription()); - } -} diff --git a/tests/php/Forms/MemberDatetimeOptionsetFieldTest.yml b/tests/php/Forms/MemberDatetimeOptionsetFieldTest.yml deleted file mode 100644 index 655d400df..000000000 --- a/tests/php/Forms/MemberDatetimeOptionsetFieldTest.yml +++ /dev/null @@ -1,6 +0,0 @@ -SilverStripe\Security\Member: - noformatmember: - Email: noformat@test.com - delocalemember: - Email: delocalemember@test.com - Locale: de_DE diff --git a/tests/php/Forms/SeparatedDateFieldTest.php b/tests/php/Forms/SeparatedDateFieldTest.php deleted file mode 100644 index 096503f8e..000000000 --- a/tests/php/Forms/SeparatedDateFieldTest.php +++ /dev/null @@ -1,53 +0,0 @@ -setLocale('en_NZ'); - $this->assertRegExp('/.*[day].*[month].*[year]/', $dateField->Field()); - } - - public function testFieldOrderingBasedOnDateFormat() - { - $dateField = new SeparatedDateField('Date'); - $dateField->setDateFormat('y/MM/dd'); - $this->assertRegExp('/.*[year].*[month].*[day]/', $dateField->Field()); - } - - public function testCustomSeparator() - { - $dateField = new SeparatedDateField('Date'); - $dateField->setDateFormat('dd/MM/y'); - $dateField->setSeparator('###'); - $this->assertRegExp('/.*[day].*###.*[month].*###.*[day]/', $dateField->Field()); - } - - /** - * @expectedException \InvalidArgumentException - * @expectedExceptionMessage Invalid date format - */ - public function testInvalidDateFormat() - { - $dateField = new SeparatedDateField('Date'); - $dateField->setDateFormat('y/MM'); - $dateField->Field(); - } -} diff --git a/tests/php/Forms/TimeFieldTest.php b/tests/php/Forms/TimeFieldTest.php index 58d2e31dc..65e40fb22 100644 --- a/tests/php/Forms/TimeFieldTest.php +++ b/tests/php/Forms/TimeFieldTest.php @@ -2,6 +2,7 @@ namespace SilverStripe\Forms\Tests; +use IntlDateFormatter; use SilverStripe\Core\Config\Config; use SilverStripe\Dev\SapphireTest; use SilverStripe\Forms\TimeField; @@ -40,11 +41,23 @@ class TimeFieldTest extends SapphireTest $this->assertFalse($f->validate(new RequiredFields())); } + public function testValidateLenientWithHtml5() + { + $f = new TimeField('Time', 'Time', '23:59:59'); + $f->setHTML5(true); + $this->assertTrue($f->validate(new RequiredFields())); + + $f = new TimeField('Time', 'Time', '23:59'); // leave out seconds + $f->setHTML5(true); + $this->assertTrue($f->validate(new RequiredFields())); + } + public function testSetLocale() { // should get en_NZ by default through setUp() $f = new TimeField('Time', 'Time'); - $f->setLocale('de_DE'); + $f->setHTML5(false); + $f->setLocale('fr_FR'); // TODO Find a hour format thats actually different $f->setValue('23:59'); $this->assertEquals($f->dataValue(), '23:59:00'); @@ -85,8 +98,7 @@ class TimeFieldTest extends SapphireTest public function testOverrideWithNull() { $field = new TimeField('Time', 'Time'); - - $field->setValue('11:00pm'); + $field->setValue('11:00:00'); $field->setValue(''); $this->assertEquals($field->dataValue(), ''); } @@ -94,33 +106,80 @@ class TimeFieldTest extends SapphireTest /** * Test that AM/PM is preserved correctly in various situations */ - public function testPreserveAMPM() + public function testSetTimeFormat() { // Test with timeformat that includes hour // Check pm $f = new TimeField('Time', 'Time'); + $f->setHTML5(false); $f->setTimeFormat('h:mm:ss a'); $f->setValue('3:59 pm'); $this->assertEquals($f->dataValue(), '15:59:00'); // Check am $f = new TimeField('Time', 'Time'); + $f->setHTML5(false); $f->setTimeFormat('h:mm:ss a'); $f->setValue('3:59 am'); $this->assertEquals($f->dataValue(), '03:59:00'); // Check with ISO date/time $f = new TimeField('Time', 'Time'); + $f->setHTML5(false); $f->setTimeFormat('h:mm:ss a'); $f->setValue('15:59:00'); $this->assertEquals($f->dataValue(), '15:59:00'); // ISO am $f = new TimeField('Time', 'Time'); + $f->setHTML5(false); $f->setTimeFormat('h:mm:ss a'); $f->setValue('03:59:00'); $this->assertEquals($f->dataValue(), '03:59:00'); } + + public function testLenientSubmissionParseWithoutSecondsOnHtml5() + { + $f = new TimeField('Time', 'Time'); + $f->setSubmittedValue('23:59'); + $this->assertEquals($f->Value(), '23:59:00'); + } + + /** + * @expectedException \LogicException + * @expectedExceptionMessageRegExp /Please opt-out .* if using setTimeFormat/ + */ + public function testHtml5WithCustomFormatThrowsException() + { + $f = new TimeField('Time', 'Time'); + $f->setValue('15:59:00'); + $f->setTimeFormat('mm:HH'); + $f->Value(); + } + + /** + * @expectedException \LogicException + * @expectedExceptionMessageRegExp /Please opt-out .* if using setTimeLength/ + */ + public function testHtml5WithCustomDateLengthThrowsException() + { + $f = new TimeField('Time', 'Time'); + $f->setValue('15:59:00'); + $f->setTimeLength(IntlDateFormatter::MEDIUM); + $f->Value(); + } + + /** + * @expectedException \LogicException + * @expectedExceptionMessageRegExp /Please opt-out .* if using setLocale/ + */ + public function testHtml5WithCustomLocaleThrowsException() + { + $f = new TimeField('Time', 'Time'); + $f->setValue('15:59:00'); + $f->setLocale('de_DE'); + $f->Value(); + } } diff --git a/tests/php/ORM/DBDateTest.php b/tests/php/ORM/DBDateTest.php index cb550e765..30040a159 100644 --- a/tests/php/ORM/DBDateTest.php +++ b/tests/php/ORM/DBDateTest.php @@ -324,41 +324,4 @@ class DBDateTest extends SapphireTest DBDatetime::clear_mock_now(); } - - /** - * @see testFormatFromSettings - * @return array - */ - public function dataTestFormatFromSettings() - { - return [ - ['2000-12-31', '31/12/2000'], - ['31-12-2000', '31/12/2000'], - ['2014-04-01', '01/04/2014'], - ]; - } - - /** - * @dataProvider dataTestFormatFromSettings - * @param string $from - * @param string $to - */ - public function testFormatFromSettings($from, $to) - { - $this->suppressNotices(); - $member = new Member(); - $member->DateFormat = 'dd/MM/y'; - - $date = DBField::create_field('Date', $from); - $this->assertEquals($to, $date->FormatFromSettings($member)); - } - - /** - * Test that FormatFromSettings without a member defaults to Nice() - */ - public function testFormatFromSettingsEmpty() - { - $date = DBfield::create_field('Date', '2000-12-31'); - $this->assertEquals('31/12/2000', $date->FormatFromSettings()); - } } diff --git a/tests/php/ORM/DBDatetimeTest.php b/tests/php/ORM/DBDatetimeTest.php index 9f3cb8e02..428e56b03 100644 --- a/tests/php/ORM/DBDatetimeTest.php +++ b/tests/php/ORM/DBDatetimeTest.php @@ -204,41 +204,4 @@ class DBDatetimeTest extends SapphireTest DBDatetime::clear_mock_now(); } - - public function dataTestFormatFromSettings() - { - return [ - ['2000-12-31 10:11:01', '31/12/2000 10:11:01'], - ['2000-12-31 1:11:01', '31/12/2000 01:11:01'], - ['2000-12-12 1:11:01', '12/12/2000 01:11:01'], - ['2000-12-31', '31/12/2000 00:00:00'], - ['2014-04-01 10:11:01', '01/04/2014 10:11:01'] - ]; - } - - /** - * @dataProvider dataTestFormatFromSettings - * @param string $from - * @param string $to - */ - public function testFormatFromSettings($from, $to) - { - $member = new Member(); - $member->DateFormat = 'dd/MM/y'; - $member->TimeFormat = 'HH:mm:ss'; - - $date = DBDatetime::create_field('Datetime', $from); - $this->assertEquals($to, $date->FormatFromSettings($member)); - } - - /** - * Test that FormatFromSettings without a member defaults to Nice() - */ - public function testFormatFromSettingsEmpty() - { - $date = DBDatetime::create_field('Datetime', '2000-12-31 10:11:01'); - - // note: Some localisation packages exclude the ',' in default medium format - $this->assertRegExp('#31/12/2000(,)? 10:11:01 AM#', $date->FormatFromSettings()); - } } diff --git a/tests/php/ORM/DBTimeTest.php b/tests/php/ORM/DBTimeTest.php index 64f0640ae..8f68f5017 100644 --- a/tests/php/ORM/DBTimeTest.php +++ b/tests/php/ORM/DBTimeTest.php @@ -56,35 +56,4 @@ class DBTimeTest extends SapphireTest $time = DBTime::create_field('Time', '17:15:55'); $this->assertEquals('5:15 PM', $time->Short()); } - - public function dataTestFormatFromSettings() - { - return [ - ['10:11:01', '10:11:01 (AM)'], - ['21:11:01', '9:11:01 (PM)'], - ]; - } - - /** - * @dataProvider dataTestFormatFromSettings - * @param string $from - * @param string $to - */ - public function testFormatFromSettings($from, $to) - { - $member = new Member(); - $member->TimeFormat = 'h:mm:ss (a)'; - - $date = DBTime::create_field('Time', $from); - $this->assertEquals($to, $date->FormatFromSettings($member)); - } - - /** - * Test that FormatFromSettings without a member defaults to Nice() - */ - public function testFormatFromSettingsEmpty() - { - $date = DBTime::create_field('Time', '10:11:01'); - $this->assertEquals('10:11:01 AM', $date->FormatFromSettings()); - } } diff --git a/tests/php/Security/MemberTest.php b/tests/php/Security/MemberTest.php index 153eab2dc..80c2ea1df 100644 --- a/tests/php/Security/MemberTest.php +++ b/tests/php/Security/MemberTest.php @@ -395,15 +395,6 @@ class MemberTest extends FunctionalTest $member->PasswordExpiry = date('Y-m-d', time() + 86400); $this->assertFalse($member->isPasswordExpired()); } - - public function testMemberWithNoDateFormatFallsbackToGlobalLocaleDefaultFormat() - { - // Note: All default strings are based on locale defaults for en_US - $member = $this->objFromFixture(Member::class, 'noformatmember'); - $this->assertEquals('MMM d, y', $member->DateFormat); - $this->assertEquals('h:mm:ss a', $member->TimeFormat); - } - public function testInGroups() { $staffmember = $this->objFromFixture(Member::class, 'staffmember');