API Create SeparatedDateField

API Restrict allowed values parsed via DBDate::setValue
API Remove NumericField_Readonly
API Remove DBTime::Nice12 / Nice24
This commit is contained in:
Damian Mooyman 2017-02-14 18:19:09 +13:00
parent 029a8b9586
commit 014f0d23ed
No known key found for this signature in database
GPG Key ID: 78B823A10DE27D1A
20 changed files with 274 additions and 446 deletions

View File

@ -8859,11 +8859,16 @@ fieldset{
height:18px; height:18px;
} }
.field input.day,.field input.month,.field input.year{ .field input.day,.field input.month{
width:56px; width:56px;
display:inline; display:inline;
} }
.field input.year{
width:72px;
display:inline;
}
.field input.time{ .field input.time{
width:88px; width:88px;
} }

View File

@ -295,8 +295,12 @@ form.small .field, .field.small {
} }
/* Date Fields */ /* Date Fields */
input.month, input.day, input.year { input.month, input.day {
width: ($grid-x * 7); width: ($grid-x * 7);
display: inline;
}
input.year {
width: ($grid-x * 9);
display: inline; display: inline;
} }

View File

@ -68,11 +68,6 @@ Subclasses of `FormField` can define their own version of `validate` to provide
above example with the `Email` validation. The `validate` method on `FormField` takes a single argument of the current above example with the `Email` validation. The `validate` method on `FormField` takes a single argument of the current
`Validator` instance. `Validator` instance.
<div class="notice" markdown="1">
The data value of the `FormField` submitted is not passed into validate. It is stored in the `value` property through
the `setValue` method.
</div>
```php ```php
public function validate($validator) public function validate($validator)
{ {

View File

@ -3,8 +3,8 @@ summary: How to format and use the DateField class.
# DateField # DateField
This `FormField` subclass lets you display an editable date, either in a single text input field, or in three separate This `FormField` subclass lets you display an editable date, in a single text input field.
fields for day, month and year. It also provides a calendar date picker. It also provides a calendar date picker.
The following example will add a simple DateField to your Page, allowing you to enter a date manually. The following example will add a simple DateField to your Page, allowing you to enter a date manually.
@ -33,14 +33,14 @@ The following example will add a simple DateField to your Page, allowing you to
## Custom Date Format ## Custom Date Format
A custom date format for a [api:DateField] can be provided through `setConfig`. A custom date format for a [api:DateField] can be provided through `setDateFormat`.
:::php :::php
// will display a date in the following format: 31-06-2012 // will display a date in the following format: 31-06-2012
DateField::create('MyDate')->setDateFormat('dd-MM-yyyy'); DateField::create('MyDate')->setDateFormat('dd-MM-yyyy');
<div class="info" markdown="1"> <div class="info" markdown="1">
The formats are based on [Zend_Date constants](http://framework.zend.com/manual/1.12/en/zend.date.constants.html). The formats are based on [CLDR format](http://userguide.icu-project.org/formatparse/datetime).
</div> </div>
@ -51,19 +51,16 @@ strtotime()).
:::php :::php
DateField::create('MyDate') DateField::create('MyDate')
->setConfig('min', '-7 days') ->setMinDate('-7 days')
->setConfig('max', '2012-12-31') ->setMaxDate'2012-12-31')
## Separate Day / Month / Year Fields ## Separate Day / Month / Year Fields
The following setting will display your DateField as three input fields for day, month and year separately. HTML5 To display separate input fields for day, month and year separately you can use the `DateFieldSeparated` subclass`.
placeholders 'day', 'month' and 'year' are enabled by default. HTML5 placeholders 'day', 'month' and 'year' are enabled by default.
:::php :::php
DateField::create('MyDate') DateFieldSeparated::create('MyDate');
->setConfig('dmyfields', true)
->setConfig('dmyseparator', '/') // set the separator
->setConfig('dmyplaceholders', 'true'); // enable HTML 5 Placeholders
<div class="alert" markdown="1"> <div class="alert" markdown="1">
Any custom date format settings will be ignored. Any custom date format settings will be ignored.
@ -75,10 +72,13 @@ The following setting will add a Calendar to a single DateField, using the jQuer
:::php :::php
DateField::create('MyDate') DateField::create('MyDate')
->setConfig('showcalendar', true); ->setShowCalendar(true);
The jQuery DatePicker doesn't support every constant available for `Zend_Date`. If you choose to use the calendar, the The jQuery date picker will support most custom locale formats (if left as default).
following constants should at least be safe: 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 Constant | xxxxx
-------- | ----- -------- | -----
@ -94,15 +94,6 @@ y | year (4 digits)
yy | year (2 digits) yy | year (2 digits)
yyyy | year (4 digits) yyyy | year (4 digits)
Unfortunately the day- and monthname values in Zend Date do not always match those in the existing jQuery UI locale
files, so constants like `EEE` or `MMM`, for day and month names could break validation. To fix this we had to slightly
alter the jQuery locale files, situated in */framework/thirdparty/jquery-ui/datepicker/i18n/*, to match Zend_Date.
<div class="info">
At this moment not all locale files may be present. If a locale file is missing, the DatePicker calendar will fallback
to 'yyyy-MM-dd' whenever day - and/or monthnames are used. After saving, the correct format will be displayed.
</div>
## Formatting Hints ## Formatting Hints
It's often not immediate apparent which format a field accepts, and showing the technical format (e.g. `HH:mm:ss`) is It's often not immediate apparent which format a field accepts, and showing the technical format (e.g. `HH:mm:ss`) is
@ -113,13 +104,14 @@ field description as an example.
$dateField = DateField::create('MyDate'); $dateField = DateField::create('MyDate');
// Show long format as text below the field // Show long format as text below the field
$dateField->setDescription(sprintf( $dateField->setDescription(_t(
_t('FormField.Example', 'e.g. %s', 'Example format'), 'FormField.Example',
Convert::raw2xml(Zend_Date::now()->toString($dateField->getConfig('dateformat'))) 'e.g. {format}',
[ 'format' => $dateField->getDateFormat() ]
)); ));
// Alternatively, set short format as a placeholder in the field // Alternatively, set short format as a placeholder in the field
$dateField->setAttribute('placeholder', $dateField->getConfig('dateformat')); $dateField->setAttribute('placeholder', $dateField->getDateFormat());
<div class="notice" markdown="1"> <div class="notice" markdown="1">
Fields scaffolded through [api:DataObject::scaffoldCMSFields()] automatically have a description attached to them. Fields scaffolded through [api:DataObject::scaffoldCMSFields()] automatically have a description attached to them.

View File

@ -942,6 +942,9 @@ specific functions.
by the array key, or the `class` parameter value. by the array key, or the `class` parameter value.
* Uniqueness checks for `File.Name` is performed on write only (not in `setName()`) * Uniqueness checks for `File.Name` is performed on write only (not in `setName()`)
* Created `Resettable` interface to better declare objects which should be reset between tests. * Created `Resettable` interface to better declare objects which should be reset between tests.
* Added a server requirement for the php-intl extension (shipped by default with most PHP distributions)
* Replaced Zend_Date and Zend_Locale with the php-intl extension.
* Consistently use CLDR date formats (rather than a mix of CLDR and date() formats)
#### <a name="overview-general-removed"></a>General and Core Removed API #### <a name="overview-general-removed"></a>General and Core Removed API
@ -1052,17 +1055,23 @@ A very small number of methods were chosen for deprecation, and will be removed
* `ChangeSet` and `ChangeSetItem` have been added for batch publishing of versioned dataobjects. * `ChangeSet` and `ChangeSetItem` have been added for batch publishing of versioned dataobjects.
* `DataObject.table_name` config can now be used to customise the database table for any record. * `DataObject.table_name` config can now be used to customise the database table for any record.
* `DataObjectSchema` class added to assist with mapping between classes and tables. * `DataObjectSchema` class added to assist with mapping between classes and tables.
* `DBMoney` values are now treated as empty only Amount is null. Values without Currency * `DBMoney` values are now treated as empty only if `Amount` field is null. If an `Amount` value
will be formatted in the default locale. is provided without a `Currency` specified, it will be formatted as per the current locale.
The below methods have been added or had their functionality updated to `DBDate`, `DBTime` and `DBDatetime` The below methods have been added or had their functionality updated to `DBDate`, `DBTime` and `DBDatetime`
* `getTimestamp()` added to get the respective date / time as unix timestamp (seconds since 1970-01-01) * `getTimestamp()` added to get the respective date / time as unix timestamp (seconds since 1970-01-01)
* `Format()` method now use CLDR format strings, rather than PHP format strings. * `Format()` method now use [CLDR format strings](http://userguide.icu-project.org/formatparse/datetime),
See http://userguide.icu-project.org/formatparse/datetime. rather than [PHP format string](http://php.net/manual/en/function.date.php).
E.g. `d/m/Y H:i:s` (php format) should be replaced with to `dd/MM/y HH:mm:ss` (CLDR format).
* getISOFormat() added which returns the standard date/time ISO 8601 pattern in CLDR format. * getISOFormat() added which returns the standard date/time ISO 8601 pattern in CLDR format.
* Dates passed in m/d/y format will now raise a notice but will be parsed. * `setValue` method is now a lot more restrictive, and expects dates and times to be passed in
Dates passed to constructors should follow ISO 8601 (y-m-d). ISO 8601 format (y-MM-dd) or (HH:mm:ss). Certain date formats will attempt to parse with
* 2-digit years will raise a notice. the below restrictions:
- `/`, `.` or `-` are supported date separators, but will be replaced with `-` internally.
- US date formats (m-d-y / y-d-m) will not be supported and may be parsed incorrectly.
(Note: Date form fields will still support localised date formats).
- `dd-MM-y` will be converted to `y-MM-dd` internally.
- 2-digit values for year will now raise errors.
* `FormatFromSettings` will default to `Nice()` format if no member is logged in. * `FormatFromSettings` will default to `Nice()` format if no member is logged in.
* `Nice`, `Long` and `Full` methods will now follow standard formatting rules for the * `Nice`, `Long` and `Full` methods will now follow standard formatting rules for the
current locale, rather than pre-defined formats. current locale, rather than pre-defined formats.
@ -1071,7 +1080,6 @@ The below methods have been added or had their functionality updated to `DBDate`
`DBTime` specific changes: `DBTime` specific changes:
* Added `DBTime::FormatFromSettings` * Added `DBTime::FormatFromSettings`
* Added `DBTime::Nice12`
#### <a name="overview-orm-removed"></a>ORM Removed API #### <a name="overview-orm-removed"></a>ORM Removed API
@ -1115,7 +1123,9 @@ The below methods have been added or had their functionality updated to `DBDate`
- `days_between` - `days_between`
* `nice_format` has been removed from `DBDate` / `DBTime` / `DBDatetime` has been removed in favour of * `nice_format` has been removed from `DBDate` / `DBTime` / `DBDatetime` has been removed in favour of
locale-specific formatting for Nice() locale-specific formatting for Nice()
* Removed `DBTime::TwelveHour` * Removed several `DBTime` methods:
- `TwelveHour`
- `Nice24`
* Removed some `DBMoney` methods due to lack of support in php-intl. * Removed some `DBMoney` methods due to lack of support in php-intl.
- `NiceWithShortname` - `NiceWithShortname`
- `NiceWithName` - `NiceWithName`
@ -1235,6 +1245,9 @@ The following filesystem synchronisation methods and tasks are also removed
* Introduced `AssetAdmin\Forms\UploadField` as a react-friendly version of UploadField. This may also * Introduced `AssetAdmin\Forms\UploadField` as a react-friendly version of UploadField. This may also
be used in normal entwine forms for managing files in a similar way to UploadField. However, this be used in normal entwine forms for managing files in a similar way to UploadField. However, this
does not support inline editing of files. does not support inline editing of files.
* Added method `FormField::setSubmittedValue($value, $data)` to process input submitted from form
submission, in contrast to `FormField::setValue($value, $data)` which is intended to load its
value from the ORM. The second argument to setValue() has been added.
The following methods and properties on `Requirements_Backend` have been renamed: The following methods and properties on `Requirements_Backend` have been renamed:
@ -1320,6 +1333,7 @@ New `DatetimeField` methods replace `getConfig()` / `setConfig()`:
* `getTimezone()` / `setTimezone()` * `getTimezone()` / `setTimezone()`
* `getDateTimeOrder()` / `setDateTimeOrder()` * `getDateTimeOrder()` / `setDateTimeOrder()`
* `getLocale()` / `setLocale()` * `getLocale()` / `setLocale()`
* `datavaluefield` config is removed as internal data value is now fixed to ISO 8601 format
New `DateField` methods replace `getConfig()` / `setConfig()`: New `DateField` methods replace `getConfig()` / `setConfig()`:
@ -1328,9 +1342,9 @@ New `DateField` methods replace `getConfig()` / `setConfig()`:
* `getMinDate()` / `setMinDate()` * `getMinDate()` / `setMinDate()`
* `getMaxDate()` / `setMaxDate()` * `getMaxDate()` / `setMaxDate()`
* `getPlaceholders()` / `setPlaceholders()` * `getPlaceholders()` / `setPlaceholders()`
* `getSeparateDMYFields()` / `setSeparateDMYFields()`
* `getClientLocale` / `setClientLocale` * `getClientLocale` / `setClientLocale`
* `getLocale()` / `setLocale()` * `getLocale()` / `setLocale()`
* option `dmyfields` is now superceded with an `SeparatedDateField` class
New `TimeField` methods replace `getConfig()` / `setConfig()` New `TimeField` methods replace `getConfig()` / `setConfig()`
@ -1370,6 +1384,7 @@ New `TimeField` methods replace `getConfig()` / `setConfig()`
as they are obsolete. as they are obsolete.
* Removed `DatetimeField`, `DateField` and `TimeField` methods `getConfig` and `setConfig`. Individual * Removed `DatetimeField`, `DateField` and `TimeField` methods `getConfig` and `setConfig`. Individual
getters and setters for individual options are provided instead. See above for list of new methods. getters and setters for individual options are provided instead. See above for list of new methods.
* Removed `NumericField_Readonly`. Use `setReadonly(true)` instead.
### <a name="overview-i18n"></a>i18n API ### <a name="overview-i18n"></a>i18n API
@ -1383,20 +1398,20 @@ New `TimeField` methods replace `getConfig()` / `setConfig()`
for all DataObject subclasses, rather than just the basename without namespace. for all DataObject subclasses, rather than just the basename without namespace.
* i18n key for locale-respective pluralisation rules added as '.PLURALS'. These can be configured * i18n key for locale-respective pluralisation rules added as '.PLURALS'. These can be configured
within yaml in array format as per [ruby i18n pluralization rules](http://guides.rubyonrails.org/i18n.html#pluralization). within yaml in array format as per [ruby i18n pluralization rules](http://guides.rubyonrails.org/i18n.html#pluralization).
* `i18n.all_locales` config moved to `Locales.locales` * `i18n.all_locales` config moved to `SilverStripe\i18n\Data\Locales.locales`
* `i18n.common_languages` config moved to `Locales.languages` * `i18n.common_languages` config moved to `SilverStripe\i18n\Data\Locales.languages`
* `i18n.likely_subtags` config moved to `Locales.likely_subtags` * `i18n.likely_subtags` config moved to `SilverStripe\i18n\Data\Locales.likely_subtags`
* `i18n.tinymce_lang` config moved to `TinyMCEConfig.tinymce_lang` * `i18n.tinymce_lang` config moved to `SilverStripe\Forms\HTMLEditor\TinyMCEConfig.tinymce_lang`
* `i18n::get_tinymce_lang()` moved to `TinyMCEConfig::get_tinymce_lang()` * `i18n::get_tinymce_lang()` moved to `SilverStripe\Forms\HTMLEditor\TinyMCEConfig::get_tinymce_lang()`
* `i18n::get_locale_from_lang()` moved to `Locales::localeFromLang()` * `i18n::get_locale_from_lang()` moved to `SilverStripe\i18n\Data\Locales::localeFromLang()`
* `i18n::get_lange_from_locale()` moved to `Locales::langFromLocale()` * `i18n::get_lange_from_locale()` moved to `SilverStripe\i18n\Data\Locales::langFromLocale()`
* `i18n::validate_locale()` moved to `Locales::validate()` * `i18n::validate_locale()` moved to `SilverStripe\i18n\Data\Locales::validate()`
* `i18n::get_common_languages()` moved to `Locales::getLanguages()` * `i18n::get_common_languages()` moved to `SilverStripe\i18n\Data\Locales::getLanguages()`
* `i18n::get_locale_name()` moved to `Locales::localeName()` * `i18n::get_locale_name()` moved to `SilverStripe\i18n\Data\Locales::localeName()`
* `i18n::get_language_name()` moved to `Locales::languageName()` * `i18n::get_language_name()` moved to `SilverStripe\i18n\Data\Locales::languageName()`
* `i18n.module_priority` config moved to `Sources.module_priority` * `i18n.module_priority` config moved to `SilverStripe\i18n\Data\Sources.module_priority`
* `i18n::get_owner_module()` moved to `ClassManifest::getOwnerModule()` * `i18n::get_owner_module()` moved to `SilverStripe\Core\Manifest\ClassManifest::getOwnerModule()`
* `i18n::get_existing_translations()` moved to `Sources::getKnownLocales()` * `i18n::get_existing_translations()` moved to `SilverStripe\i18n\Data\Sources::getKnownLocales()`
#### <a name="overview-i18n-removed"></a>i18n API Removed API #### <a name="overview-i18n-removed"></a>i18n API Removed API

View File

@ -8,9 +8,7 @@ use InvalidArgumentException;
use SilverStripe\ORM\FieldType\DBDatetime; use SilverStripe\ORM\FieldType\DBDatetime;
/** /**
* Form field to display an editable date string, * Form used for editing a date stirng
* either in a single `<input type="text">` field,
* or in three separate fields for day, month and year.
* *
* Caution: The form field does not include any JavaScript or CSS when used outside of the CMS context, * Caution: The form field does not include any JavaScript or CSS when used outside of the CMS context,
* since the required frontend dependencies are included through CMS bundling. * since the required frontend dependencies are included through CMS bundling.
@ -29,11 +27,10 @@ use SilverStripe\ORM\FieldType\DBDatetime;
* *
* # Usage * # Usage
* *
* ## Example: German dates with separate fields for day, month, year * ## Example: Field localised with german date format
* *
* $f = new DateField('MyDate'); * $f = new DateField('MyDate');
* $f->setLocale('de_DE'); * $f->setLocale('de_DE');
* $f->setSeparateDMYFields(true);
* *
* # Validation * # Validation
* *
@ -95,14 +92,6 @@ class DateField extends TextField
*/ */
protected $placeholders = true; protected $placeholders = true;
/**
* Declare whether D, M and Y fields should be separate inputs.
* If set then only numeric values will be accepted.
*
* @var bool
*/
protected $separateDMYFields = false;
/** /**
* Override locale for client side. * Override locale for client side.
* *
@ -145,16 +134,11 @@ class DateField extends TextField
/** /**
* Set if calendar should be shown on the frontend. * Set if calendar should be shown on the frontend.
* *
* If set to true, disables separate DMY fields
*
* @param bool $show * @param bool $show
* @return $this * @return $this
*/ */
public function setShowCalendar($show) public function setShowCalendar($show)
{ {
if ($show && $this->getSeparateDMYFields()) {
throw new InvalidArgumentException("Can't separate DMY fields and show calendar popup");
}
$this->showCalendar = $show; $this->showCalendar = $show;
return $this; return $this;
} }
@ -316,39 +300,6 @@ class DateField extends TextField
return $attributes; return $attributes;
} }
public function Field($properties = array())
{
if (!$this->getSeparateDMYFields()) {
return parent::Field($properties);
}
// Three separate fields for day, month and year
$valArr = $this->iso8601ToArray($this->Value());
$fieldDay = NumericField::create($this->name . '[day]', false, $valArr ? $valArr['day'] : null)
->addExtraClass('day')
->setMaxLength(2);
$fieldMonth = NumericField::create($this->name . '[month]', false, $valArr ? $valArr['month'] : null)
->addExtraClass('month')
->setMaxLength(2);
$fieldYear = NumericField::create($this->name . '[year]', false, $valArr ? $valArr['year'] : null)
->addExtraClass('year')
->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'));
}
// Join all fields
// @todo custom ordering based on locale
$sep = '&nbsp;<span class="separator">/</span>&nbsp;';
return $fieldDay->Field() . $sep
. $fieldMonth->Field() . $sep
. $fieldYear->Field();
}
public function Type() public function Type()
{ {
return 'date text'; return 'date text';
@ -364,11 +315,7 @@ class DateField extends TextField
public function setSubmittedValue($value, $data = null) public function setSubmittedValue($value, $data = null)
{ {
// Save raw value for later validation // Save raw value for later validation
if ($this->isEmptyArray($value)) {
$this->rawValue = null;
} else {
$this->rawValue = $value; $this->rawValue = $value;
}
// Null case // Null case
if (!$value) { if (!$value) {
@ -376,12 +323,6 @@ class DateField extends TextField
return $this; return $this;
} }
// If loading from array convert
if (is_array($value)) {
$this->value = $this->arrayToISO8601($value);
return $this;
}
// Parse from submitted value // Parse from submitted value
$this->value = $this->localisedToISO8601($value); $this->value = $this->localisedToISO8601($value);
return $this; return $this;
@ -555,33 +496,6 @@ class DateField extends TextField
return $this; return $this;
} }
/**
* Declare whether D, M and Y fields should be separate inputs.
* If set then only numeric values will be accepted.
*
* @return bool
*/
public function getSeparateDMYFields()
{
return $this->separateDMYFields;
}
/**
* Set if we should separate D M and Y fields. If set to true, disabled calendar
* popup.
*
* @param bool $separate
* @return $this
*/
public function setSeparateDMYFields($separate)
{
if ($separate && $this->getShowCalendar()) {
throw new InvalidArgumentException("Can't separate DMY fields and show calendar popup");
}
$this->separateDMYFields = $separate;
return $this;
}
/** /**
* @return string * @return string
*/ */
@ -647,62 +561,6 @@ class DateField extends TextField
return $config; return $config;
} }
/**
* 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),
];
}
/**
* 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 date localised in the current locale to ISO 8601 date * Convert date localised in the current locale to ISO 8601 date
* *
@ -776,15 +634,4 @@ class DateField extends TextField
{ {
return DateField_View_JQuery::create($this); return DateField_View_JQuery::create($this);
} }
/**
* Check if this array is empty
*
* @param $value
* @return bool
*/
public function isEmptyArray($value)
{
return is_array($value) && !array_filter($value);
}
} }

View File

@ -2,7 +2,6 @@
namespace SilverStripe\Forms\HTMLEditor; namespace SilverStripe\Forms\HTMLEditor;
use SilverStripe\Core\Config\Configurable;
use SilverStripe\Core\Convert; use SilverStripe\Core\Convert;
use SilverStripe\Control\Controller; use SilverStripe\Control\Controller;
use SilverStripe\Control\Director; use SilverStripe\Control\Director;

View File

@ -159,7 +159,7 @@ class MemberDatetimeOptionsetField extends OptionsetField
public function setValue($value, $data = null) public function setValue($value, $data = null)
{ {
if (is_array($value)) { if (is_array($value)) {
throw new InvalidArgumentException("Invalid value"); throw new InvalidArgumentException("Invalid array value: Expected string");
} }
return parent::setValue($value, $data); return parent::setValue($value, $data);
} }

View File

@ -89,7 +89,7 @@ class MoneyField extends FormField
$currencyValue = $this->fieldCurrency ? $this->fieldCurrency->dataValue() : null; $currencyValue = $this->fieldCurrency ? $this->fieldCurrency->dataValue() : null;
$allowedCurrencies = $this->getAllowedCurrencies(); $allowedCurrencies = $this->getAllowedCurrencies();
if (count($allowedCurrencies) === 1) { if (count($allowedCurrencies) === 1) {
// Dropdown field for multiple currencies // Hidden field for single currency
$field = HiddenField::create("{$name}[Currency]"); $field = HiddenField::create("{$name}[Currency]");
reset($allowedCurrencies); reset($allowedCurrencies);
$currencyValue = key($allowedCurrencies); $currencyValue = key($allowedCurrencies);
@ -164,7 +164,7 @@ class MoneyField extends FormField
'Currency' => $value->getCurrency(), 'Currency' => $value->getCurrency(),
'Amount' => $value->getAmount(), 'Amount' => $value->getAmount(),
]; ];
} else { } elseif (!is_array($value)) {
throw new InvalidArgumentException("Invalid currency format"); throw new InvalidArgumentException("Invalid currency format");
} }
@ -232,8 +232,6 @@ class MoneyField extends FormField
public function performReadonlyTransformation() public function performReadonlyTransformation()
{ {
$clone = clone $this; $clone = clone $this;
$clone->fieldAmount = $clone->fieldAmount->performReadonlyTransformation();
$clone->fieldCurrency = $clone->fieldCurrency->performReadonlyTransformation();
$clone->setReadonly(true); $clone->setReadonly(true);
return $clone; return $clone;
} }

View File

@ -306,4 +306,11 @@ class NumericField extends TextField
$this->scale = $scale; $this->scale = $scale;
return $this; return $this;
} }
public function performReadonlyTransformation()
{
$field = clone $this;
$field->setReadonly(true);
return $field;
}
} }

View File

@ -1,30 +0,0 @@
<?php
namespace SilverStripe\Forms;
/**
* Readonly version of a numeric field.
*/
class NumericField_Readonly extends ReadonlyField
{
/**
* @return static
*/
public function performReadonlyTransformation()
{
return clone $this;
}
/**
* @return string
*/
public function Value()
{
return $this->value ?: '0';
}
public function getValueCast()
{
return 'Decimal';
}
}

View File

@ -0,0 +1,136 @@
<?php
namespace SilverStripe\Forms;
use SilverStripe\i18n\i18n;
/**
* Date field with separate inputs for d/m/y
*/
class SeparatedDateField extends DateField
{
public function Field($properties = array())
{
// Three separate fields for day, month and year
$valArr = $this->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'));
}
// Join all fields
// @todo custom ordering based on locale
$sep = '&nbsp;<span class="separator">/</span>&nbsp;';
return $fieldDay->Field() . $sep
. $fieldMonth->Field() . $sep
. $fieldYear->Field();
}
/**
* 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);
}
}

View File

@ -38,7 +38,7 @@ class DBDate extends DBField
$value = $this->parseDate($value); $value = $this->parseDate($value);
if ($value === false) { if ($value === false) {
throw new InvalidArgumentException( throw new InvalidArgumentException(
"Invalid date passed. Use " . self::ISO_DATE . " to prevent this error." "Invalid date: '$value'. Use " . self::ISO_DATE . " to prevent this error."
); );
} }
$this->value = $value; $this->value = $value;
@ -308,10 +308,7 @@ class DBDate extends DBField
*/ */
public function Rfc2822() public function Rfc2822()
{ {
if ($this->value) { return $this->Format('y-MM-dd HH:mm:ss');
return date('Y-m-d H:i:s', $this->getTimestamp());
}
return null;
} }
/** /**
@ -321,15 +318,13 @@ class DBDate extends DBField
*/ */
public function Rfc3339() public function Rfc3339()
{ {
if (!$this->value) { $date = $this->Format('y-MM-dd\\THH:mm:ss');
if (!$date) {
return null; return null;
} }
$timestamp = $this->getTimestamp();
$date = date('Y-m-d\TH:i:s', $timestamp);
$matches = array(); $matches = array();
if (preg_match('/^([\-+])(\d{2})(\d{2})$/', date('O', $timestamp), $matches)) { if (preg_match('/^([\-+])(\d{2})(\d{2})$/', date('O', $this->getTimestamp()), $matches)) {
$date .= $matches[1].$matches[2].':'.$matches[3]; $date .= $matches[1].$matches[2].':'.$matches[3];
} else { } else {
$date .= 'Z'; $date .= 'Z';
@ -541,23 +536,13 @@ class DBDate extends DBField
protected function fixInputDate($value) protected function fixInputDate($value)
{ {
// split // split
list($day, $month, $year, $time) = $this->explodeDateString($value); list($year, $month, $day, $time) = $this->explodeDateString($value);
// Detect invalid year order
if (!checkdate($month, $day, $year) && checkdate($month, $year, $day)) {
trigger_error(
"Unexpected date order. Use " . self::ISO_DATE . " to prevent this notice.",
E_USER_NOTICE
);
list($day, $year) = [$year, $day];
}
// Fix y2k year
$year = $this->guessY2kYear($year);
// Validate date // Validate date
if (!checkdate($month, $day, $year)) { if (!checkdate($month, $day, $year)) {
throw new InvalidArgumentException("Invalid date passed. Use " . self::ISO_DATE . " to prevent this error."); throw new InvalidArgumentException(
"Invalid date: '$value'. Use " . self::ISO_DATE . " to prevent this error."
);
} }
// Convert to y-m-d // Convert to y-m-d
@ -565,65 +550,39 @@ class DBDate extends DBField
} }
/** /**
* Attempt to split date string into day, month, year, and timestamp components. * Attempt to split date string into year, month, day, and timestamp components.
* Don't read this code without a drink in hand!
* *
* @param string $value * @param string $value
* @return array * @return array
*/ */
protected function explodeDateString($value) protected function explodeDateString($value)
{ {
// US date format with 4-digit year first // split on known delimiters (. / -)
if (preg_match('#^(?<year>\\d{4})/(?<day>\\d+)/(?<month>\\d+)(?<time>.*)$#', $value, $matches)) { if (!preg_match(
trigger_error( '#^(?<first>\\d+)[-/\\.](?<second>\\d+)[-/\\.](?<third>\\d+)(?<time>.*)$#',
"Implicit y/d/m conversion. Use " . self::ISO_DATE . " to prevent this notice.", $value,
E_USER_NOTICE $matches
); )) {
return [$matches['day'], $matches['month'], $matches['year'], $matches['time']];
}
// US date format without 4-digit year first: assume m/d/y
if (preg_match('#^(?<month>\\d+)/(?<day>\\d+)/(?<year>\\d+)(?<time>.*)$#', $value, $matches)) {
// Assume m/d/y
trigger_error(
"Implicit m/d/y conversion. Use " . self::ISO_DATE . " to prevent this notice.",
E_USER_NOTICE
);
return [$matches['day'], $matches['month'], $matches['year'], $matches['time']];
}
// check d.m.y
if (preg_match('#^(?<day>\\d+)\\.(?<month>\\d+)\\.(?<year>\\d+)(?<time>.*)$#', $value, $matches)) {
return [$matches['day'], $matches['month'], $matches['year'], $matches['time']];
}
// check y-m-d
if (preg_match('#^(?<year>\\d+)\\-(?<month>\\d+)\\-(?<day>\\d+)(?<time>.*)$#', $value, $matches)) {
return [$matches['day'], $matches['month'], $matches['year'], $matches['time']];
}
throw new InvalidArgumentException( throw new InvalidArgumentException(
"Invalid date passed. Use " . self::ISO_DATE . " to prevent this error." "Invalid date: '$value'. Use " . self::ISO_DATE . " to prevent this error."
); );
} }
/** $parts = [
* @param int $year $matches['first'],
* @return int Fixed year $matches['second'],
*/ $matches['third']
protected function guessY2kYear($year) ];
{ // Flip d-m-y to y-m-d
// Fix y2k if ($parts[0] < 1000 && $parts[2] > 1000) {
if ($year < 100) { $parts = array_reverse($parts);
trigger_error("Implicit y2k conversion. Please use full YYYY year for dates", E_USER_NOTICE); }
if ($year >= 70) { if ($parts[0] < 1000) {
// 70 -> 99 converted to 19(x) throw new InvalidArgumentException(
$year += 1900; "Invalid date: '$value'. Use " . self::ISO_DATE . " to prevent this error."
} else { );
// 0 -> 69 converted to 20(x) }
$year += 2000; $parts[] = $matches['time'];
} return $parts;
}
return $year;
} }
} }

View File

@ -110,26 +110,6 @@ class DBTime extends DBField
return $formatter->format($this->getTimestamp()); return $formatter->format($this->getTimestamp());
} }
/**
* Returns the time in 12-hour format using the format string 'h:mm a' e.g. '1:32 pm'.
*
* @return string Formatted time.
*/
public function Nice12()
{
return $this->Format('h:mm a');
}
/**
* Returns the time in 24-hour format using the format string 'H:mm' e.g. '13:32'.
*
* @return string Formatted time.
*/
public function Nice24()
{
return $this->Format('H:mm');
}
/** /**
* Return the time using a particular formatting string. * Return the time using a particular formatting string.
* *

View File

@ -1606,8 +1606,8 @@ class IntlLocales implements Locales, Resettable
return false; return false;
} }
return strcasecmp($lang, $region) return strcasecmp($lang, $region)
* strcasecmp($lang, $locale) && strcasecmp($lang, $locale)
* strcasecmp($region, $locale) !== 0; && strcasecmp($region, $locale);
} }
/** /**

View File

@ -4,6 +4,7 @@ namespace SilverStripe\Forms\Tests;
use SilverStripe\Dev\SapphireTest; use SilverStripe\Dev\SapphireTest;
use SilverStripe\Forms\DateField; use SilverStripe\Forms\DateField;
use SilverStripe\Forms\SeparatedDateField;
use SilverStripe\Forms\RequiredFields; use SilverStripe\Forms\RequiredFields;
use SilverStripe\i18n\i18n; use SilverStripe\i18n\i18n;
use SilverStripe\ORM\FieldType\DBDatetime; use SilverStripe\ORM\FieldType\DBDatetime;
@ -110,8 +111,7 @@ class DateFieldTest extends SapphireTest
public function testSetValueWithDateArray() public function testSetValueWithDateArray()
{ {
$f = new DateField('Date', 'Date'); $f = new SeparatedDateField('Date', 'Date');
$f->setSeparateDMYFields(true);
$f->setSubmittedValue([ $f->setSubmittedValue([
'day' => 29, 'day' => 29,
'month' => 03, 'month' => 03,
@ -147,10 +147,8 @@ class DateFieldTest extends SapphireTest
public function testEmptyValueValidation() public function testEmptyValueValidation()
{ {
$field = new DateField('Date');
$validator = new RequiredFields(); $validator = new RequiredFields();
$this->assertTrue($field->validate($validator)); $field = new SeparatedDateField('Date');
$field->setSeparateDMYFields(true);
$this->assertTrue($field->validate($validator)); $this->assertTrue($field->validate($validator));
$field->setSubmittedValue([ $field->setSubmittedValue([
'day' => '', 'day' => '',
@ -162,8 +160,7 @@ class DateFieldTest extends SapphireTest
public function testValidateArray() public function testValidateArray()
{ {
$f = new DateField('Date', 'Date'); $f = new SeparatedDateField('Date', 'Date');
$f->setSeparateDMYFields(true);
$f->setSubmittedValue([ $f->setSubmittedValue([
'day' => 29, 'day' => 29,
'month' => 03, 'month' => 03,
@ -193,9 +190,7 @@ class DateFieldTest extends SapphireTest
public function testValidateEmptyArrayValuesSetsNullForValueObject() public function testValidateEmptyArrayValuesSetsNullForValueObject()
{ {
$f = new DateField('Date', 'Date'); $f = new SeparatedDateField('Date', 'Date');
$f->setSeparateDMYFields(true);
$f->setSubmittedValue([ $f->setSubmittedValue([
'day' => '', 'day' => '',
'month' => '', 'month' => '',
@ -213,7 +208,7 @@ class DateFieldTest extends SapphireTest
public function testValidateArrayValue() public function testValidateArrayValue()
{ {
$f = new DateField('Date', 'Date'); $f = new SeparatedDateField('Date', 'Date');
$f->setSubmittedValue(['day' => 29, 'month' => 03, 'year' => 2003]); $f->setSubmittedValue(['day' => 29, 'month' => 03, 'year' => 2003]);
$this->assertTrue($f->validate(new RequiredFields())); $this->assertTrue($f->validate(new RequiredFields()));

View File

@ -7,6 +7,7 @@ use SilverStripe\Control\Controller;
use SilverStripe\Forms\DatetimeField; use SilverStripe\Forms\DatetimeField;
use SilverStripe\Forms\RequiredFields; use SilverStripe\Forms\RequiredFields;
use SilverStripe\Forms\DateField; use SilverStripe\Forms\DateField;
use SilverStripe\Forms\SeparatedDateField;
use SilverStripe\Forms\Tests\DatetimeFieldTest\Model; use SilverStripe\Forms\Tests\DatetimeFieldTest\Model;
use SilverStripe\Forms\TimeField; use SilverStripe\Forms\TimeField;
use SilverStripe\Forms\FieldList; use SilverStripe\Forms\FieldList;
@ -107,7 +108,7 @@ class DatetimeFieldTest extends SapphireTest
public function testSetValueWithDmyArray() public function testSetValueWithDmyArray()
{ {
$f = new DatetimeField('Datetime', 'Datetime'); $f = new DatetimeField('Datetime', 'Datetime');
$f->getDateField()->setSeparateDMYFields(true); $f->setDateField(new SeparatedDateField('Datetime[date]'));
$f->setSubmittedValue([ $f->setSubmittedValue([
'date' => ['day' => 29, 'month' => 03, 'year' => 2003], 'date' => ['day' => 29, 'month' => 03, 'year' => 2003],
'time' => '11:00:00 pm' 'time' => '11:00:00 pm'

View File

@ -107,9 +107,13 @@ class NumericFieldTest extends SapphireTest
public function testReadonly() public function testReadonly()
{ {
i18n::set_locale('en_US');
$field = new NumericField('Number'); $field = new NumericField('Number');
$this->assertRegExp("#<span[^>]+>\s*0\s*<\/span>#", "".$field->performReadonlyTransformation()->Field().""); $field->setLocale('de_DE');
$field->setScale(2);
$field->setValue(1001.3);
$html = $field->performReadonlyTransformation()->Field()->forTemplate();
$this->assertContains('value="1.001,30"', $html);
$this->assertContains('readonly="readonly"', $html);
} }
public function testNumberTypeOnInputHtml() public function testNumberTypeOnInputHtml()

View File

@ -94,96 +94,30 @@ class DBDateTest extends SapphireTest
public function testMDYConversion() public function testMDYConversion()
{ {
// Disable notices
$this->suppressNotices();
$this->assertEquals(
'4/03/2003',
DBField::create_field('Date', '3/4/2003')->Nice(),
"Date->Nice() works with M/D/YYYY format"
);
$this->setExpectedException( $this->setExpectedException(
PHPUnit_Framework_Error_Notice::class, \InvalidArgumentException::class,
"Implicit m/d/y conversion. Use " . DBDate::ISO_DATE . " to prevent this notice." "Invalid date: '3/16/2003'. Use " . DBDate::ISO_DATE . " to prevent this error."
); );
$this->restoreNotices(); DBField::create_field('Date', '3/16/2003');
DBField::create_field('Date', '3/4/2003');
}
public function testYDMConversion()
{
// Disable notices
$this->suppressNotices();
$this->assertEquals(
'4/03/2003',
DBField::create_field('Date', '2003/4/3')->Nice(),
"Date->Nice() works with YYYY/D/M format"
);
$this->setExpectedException(
PHPUnit_Framework_Error_Notice::class,
"Implicit y/d/m conversion. Use " . DBDate::ISO_DATE . " to prevent this notice."
);
$this->restoreNotices();
DBField::create_field('Date', '2003/4/3');
} }
public function testY2kCorrection() public function testY2kCorrection()
{ {
$this->suppressNotices();
$this->assertEquals(
'4/03/2003',
DBField::create_field('Date', '4.3.03')->Nice(),
"Date->Nice() works with D.M.YY format"
);
$this->assertEquals(
'4/03/2003',
DBField::create_field('Date', '04.03.03')->Nice(),
"Date->Nice() works with DD.MM.YY format"
);
$this->assertEquals(
'4/03/2003',
DBField::create_field('Date', '4.3.03')->Nice(),
"Date->Nice() works with D.M.YY format"
);
$this->assertEquals(
'4/03/2003',
DBField::create_field('Date', '4.03.03')->Nice(),
"Date->Nice() works with D.M.YY format"
);
$this->assertEquals(
'4/03/2003',
DBField::create_field('Date', '03-03-04')->Nice(),
"Date->Nice() works with Y-m-d format"
);
$this->setExpectedException( $this->setExpectedException(
PHPUnit_Framework_Error_Notice::class, \InvalidArgumentException::class,
"Implicit y2k conversion. Please use full YYYY year for dates" "Invalid date: '03-03-04'. Use " . DBDate::ISO_DATE . " to prevent this error."
); );
$this->restoreNotices();
DBField::create_field('Date', '03-03-04'); DBField::create_field('Date', '03-03-04');
} }
public function testInvertedYearCorrection() public function testInvertedYearCorrection()
{ {
$this->suppressNotices(); // iso8601 expects year first, but support year last
// iso8601 expects year first
$this->assertEquals( $this->assertEquals(
'4/03/2003', '4/03/2003',
DBField::create_field('Date', '04-03-2003')->Nice(), DBField::create_field('Date', '04-03-2003')->Nice(),
"Date->Nice() works with DD-MM-YYYY format" "Date->Nice() works with DD-MM-YYYY format"
); );
$this->setExpectedException(
PHPUnit_Framework_Error_Notice::class,
"Unexpected date order. Use " . DBDate::ISO_DATE . " to prevent this notice."
);
$this->restoreNotices();
DBField::create_field('Date', '04-03-2003');
} }
public function testYear() public function testYear()
@ -400,7 +334,6 @@ class DBDateTest extends SapphireTest
return [ return [
['2000-12-31', '31/12/2000'], ['2000-12-31', '31/12/2000'],
['31-12-2000', '31/12/2000'], ['31-12-2000', '31/12/2000'],
['12/31/2000', '31/12/2000'],
['2014-04-01', '01/04/2014'], ['2014-04-01', '01/04/2014'],
]; ];
} }

View File

@ -57,18 +57,6 @@ class DBTimeTest extends SapphireTest
$this->assertEquals('5:15 PM', $time->Short()); $this->assertEquals('5:15 PM', $time->Short());
} }
public function testNice12()
{
$time = DBTime::create_field('Time', '17:15:55');
$this->assertEquals('5:15 PM', $time->Nice12());
}
public function testNice24()
{
$time = DBTime::create_field('Time', '17:15:55');
$this->assertEquals('17:15', $time->Nice24());
}
public function dataTestFormatFromSettings() public function dataTestFormatFromSettings()
{ {
return [ return [