diff --git a/core/MAINTAINERS.txt b/core/MAINTAINERS.txt index b6bfdd991fa59689a2ac9a17c14fd7ab02751119..a74afa629df501c4c2c033a596ab13e8e67fd2ba 100644 --- a/core/MAINTAINERS.txt +++ b/core/MAINTAINERS.txt @@ -324,6 +324,10 @@ DateTime module - Jonathan Hedstrom 'jhedstrom' https://www.drupal.org/u/jhedstrom - Matthew Donadio 'mpdonadio' https://www.drupal.org/u/mpdonadio +DateTime range module +- Jonathan Hedstrom 'jhedstrom' https://www.drupal.org/u/jhedstrom +- Matthew Donadio 'mpdonadio' https://www.drupal.org/u/mpdonadio + Dynamic Page Cache module - Fabian Franz 'Fabianx' https://www.drupal.org/u/fabianx - Wim Leers 'Wim Leers' https://www.drupal.org/u/wim-leers diff --git a/core/composer.json b/core/composer.json index de2feb8aadbd918ac901644713e419cee4844814..53ab8fa68aa36c37844cfadd3063752a0f46dc6a 100644 --- a/core/composer.json +++ b/core/composer.json @@ -88,6 +88,7 @@ "drupal/core-utility": "self.version", "drupal/core-uuid": "self.version", "drupal/datetime": "self.version", + "drupal/datetime_range": "self.version", "drupal/dblog": "self.version", "drupal/dynamic_page_cache": "self.version", "drupal/editor": "self.version", diff --git a/core/modules/datetime_range/config/schema/datetime_range.schema.yml b/core/modules/datetime_range/config/schema/datetime_range.schema.yml new file mode 100644 index 0000000000000000000000000000000000000000..90bbd38b2f2aa26d598b0af0fca165ac8b3c6c3d --- /dev/null +++ b/core/modules/datetime_range/config/schema/datetime_range.schema.yml @@ -0,0 +1,70 @@ +# Schema for the configuration files of the Datetime Range module. + +# Daterange field type. + +field.storage_settings.daterange: + type: field.storage_settings.datetime + label: 'Date range settings' + +field.field_settings.daterange: + type: field.field_settings.datetime + label: 'Date range settings' + +field.value.daterange: + type: mapping + label: 'Default value' + mapping: + default_date_type: + type: string + label: 'Default start date type' + default_date: + type: string + label: 'Default start date value' + default_end_date_type: + type: string + label: 'Default end date type' + default_end_date: + type: string + label: 'Default end date value' + +field.formatter.settings.daterange_default: + type: field.formatter.settings.datetime_default + label: 'Date range default display format settings' + mapping: + separator: + type: string + label: 'Separator' + +field.formatter.settings.daterange_plain: + type: field.formatter.settings.datetime_plain + label: 'Date range plain display format settings' + mapping: + separator: + type: string + label: 'Separator' + +field.formatter.settings.daterange_custom: + type: field.formatter.settings.datetime_custom + label: 'Date range custom display format settings' + mapping: + separator: + type: string + label: 'Separator' + +field.widget.settings.daterange_datelist: + type: mapping + label: 'Date range select list display format settings' + mapping: + increment: + type: integer + label: 'Time increments' + date_order: + type: string + label: 'Date part order' + time_type: + type: string + label: 'Time type' + +field.widget.settings.daterange_default: + type: mapping + label: 'Date range default display format settings' diff --git a/core/modules/datetime_range/datetime_range.info.yml b/core/modules/datetime_range/datetime_range.info.yml new file mode 100644 index 0000000000000000000000000000000000000000..dd22d9a369192aa9c4ff3bc89822b82db1c94268 --- /dev/null +++ b/core/modules/datetime_range/datetime_range.info.yml @@ -0,0 +1,8 @@ +name: 'Datetime Range' +type: module +description: 'Provides the ability to store end dates.' +package: Core (Experimental) +version: VERSION +core: 8.x +dependencies: + - datetime diff --git a/core/modules/datetime_range/datetime_range.module b/core/modules/datetime_range/datetime_range.module new file mode 100644 index 0000000000000000000000000000000000000000..ceeb99dd8a50a72bbe18e46088f6bed30a5497f4 --- /dev/null +++ b/core/modules/datetime_range/datetime_range.module @@ -0,0 +1,28 @@ +' . t('About') . ''; + $output .= '

' . t('The Datetime Range module provides a Date field that stores start dates and times, as well as end dates and times. See the Field module help and the Field UI module help pages for general information on fields and how to create and manage them. For more information, see the online documentation for the Datetime Range module.', array(':field' => \Drupal::url('help.page', array('name' => 'field')), ':field_ui' => (\Drupal::moduleHandler()->moduleExists('field_ui')) ? \Drupal::url('help.page', array('name' => 'field_ui')) : '#', ':datetime_do' => 'https://www.drupal.org/documentation/modules/datetime_range')) . '

'; + $output .= '

' . t('Uses') . '

'; + $output .= '
'; + $output .= '
' . t('Managing and displaying date fields') . '
'; + $output .= '
' . t('The settings and the display of the Date field can be configured separately. See the Field UI help for more information on how to manage fields and their display.', array(':field_ui' => (\Drupal::moduleHandler()->moduleExists('field_ui')) ? \Drupal::url('help.page', array('name' => 'field_ui')) : '#')) . '
'; + $output .= '
' . t('Displaying dates') . '
'; + $output .= '
' . t('Dates can be displayed using the Plain or the Default formatter. The Plain formatter displays the date in the ISO 8601 format. If you choose the Default formatter, you can choose a format from a predefined list that can be managed on the Date and time formats page.', array(':date_format_list' => \Drupal::url('entity.date_format.collection'))) . '
'; + $output .= '
'; + return $output; + } +} diff --git a/core/modules/datetime_range/src/DateTimeRangeTrait.php b/core/modules/datetime_range/src/DateTimeRangeTrait.php new file mode 100644 index 0000000000000000000000000000000000000000..5a34f2c1828345445fe4136fce724bdaef76146e --- /dev/null +++ b/core/modules/datetime_range/src/DateTimeRangeTrait.php @@ -0,0 +1,78 @@ +getFieldSetting('datetime_type') == DateTimeItem::DATETIME_TYPE_DATE) { + // A date without time will pick up the current time, use the default. + datetime_date_default_time($date); + } + $this->setTimeZone($date); + + $build = [ + '#plain_text' => $this->formatDate($date), + '#cache' => [ + 'contexts' => [ + 'timezone', + ], + ], + ]; + + return $build; + } + + /** + * Creates a render array from a date object with ISO date attribute. + * + * @param \Drupal\Core\Datetime\DrupalDateTime $date + * A date object. + * + * @return array + * A render array. + */ + protected function buildDateWithIsoAttribute(DrupalDateTime $date) { + if ($this->getFieldSetting('datetime_type') == DateTimeItem::DATETIME_TYPE_DATE) { + // A date without time will pick up the current time, use the default. + datetime_date_default_time($date); + } + + // Create the ISO date in Universal Time. + $iso_date = $date->format("Y-m-d\TH:i:s") . 'Z'; + + $this->setTimeZone($date); + + $build = [ + '#theme' => 'time', + '#text' => $this->formatDate($date), + '#html' => FALSE, + '#attributes' => [ + 'datetime' => $iso_date, + ], + '#cache' => [ + 'contexts' => [ + 'timezone', + ], + ], + ]; + + return $build; + } + +} diff --git a/core/modules/datetime_range/src/Plugin/Field/FieldFormatter/DateRangeCustomFormatter.php b/core/modules/datetime_range/src/Plugin/Field/FieldFormatter/DateRangeCustomFormatter.php new file mode 100644 index 0000000000000000000000000000000000000000..a23de512026ba302a45b4501d23c73e07eec5a80 --- /dev/null +++ b/core/modules/datetime_range/src/Plugin/Field/FieldFormatter/DateRangeCustomFormatter.php @@ -0,0 +1,96 @@ + '-', + ] + parent::defaultSettings(); + } + + /** + * {@inheritdoc} + */ + public function viewElements(FieldItemListInterface $items, $langcode) { + $elements = []; + $separator = $this->getSetting('separator'); + + foreach ($items as $delta => $item) { + if (!empty($item->start_date) && !empty($item->end_date)) { + /** @var \Drupal\Core\Datetime\DrupalDateTime $start_date */ + $start_date = $item->start_date; + /** @var \Drupal\Core\Datetime\DrupalDateTime $end_date */ + $end_date = $item->end_date; + + if ($start_date->format('U') !== $end_date->format('U')) { + $elements[$delta] = [ + 'start_date' => $this->buildDate($start_date), + 'separator' => ['#plain_text' => ' ' . $separator . ' '], + 'end_date' => $this->buildDate($end_date), + ]; + } + else { + $elements[$delta] = $this->buildDate($start_date); + } + } + } + + return $elements; + } + + /** + * {@inheritdoc} + */ + public function settingsForm(array $form, FormStateInterface $form_state) { + $form = parent::settingsForm($form, $form_state); + + $form['separator'] = [ + '#type' => 'textfield', + '#title' => $this->t('Date separator'), + '#description' => $this->t('The string to separate the start and end dates'), + '#default_value' => $this->getSetting('separator'), + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function settingsSummary() { + $summary = parent::settingsSummary(); + + if ($separator = $this->getSetting('separator')) { + $summary[] = $this->t('Separator: %separator', ['%separator' => $separator]); + } + + return $summary; + } + +} diff --git a/core/modules/datetime_range/src/Plugin/Field/FieldFormatter/DateRangeDefaultFormatter.php b/core/modules/datetime_range/src/Plugin/Field/FieldFormatter/DateRangeDefaultFormatter.php new file mode 100644 index 0000000000000000000000000000000000000000..f5ebb8ceaa8351eb8a43344fb277ac3b9882d5e2 --- /dev/null +++ b/core/modules/datetime_range/src/Plugin/Field/FieldFormatter/DateRangeDefaultFormatter.php @@ -0,0 +1,104 @@ + elements, with + * configurable date formats (from the list of configured formats) and a + * separator. + * + * @FieldFormatter( + * id = "daterange_default", + * label = @Translation("Default"), + * field_types = { + * "daterange" + * } + * ) + */ +class DateRangeDefaultFormatter extends DateTimeDefaultFormatter { + + use DateTimeRangeTrait; + + /** + * {@inheritdoc} + */ + public static function defaultSettings() { + return [ + 'separator' => '-', + ] + parent::defaultSettings(); + } + + /** + * {@inheritdoc} + */ + public function viewElements(FieldItemListInterface $items, $langcode) { + $elements = []; + $separator = $this->getSetting('separator'); + + foreach ($items as $delta => $item) { + if (!empty($item->start_date) && !empty($item->end_date)) { + /** @var \Drupal\Core\Datetime\DrupalDateTime $start_date */ + $start_date = $item->start_date; + /** @var \Drupal\Core\Datetime\DrupalDateTime $end_date */ + $end_date = $item->end_date; + + if ($start_date->format('U') !== $end_date->format('U')) { + $elements[$delta] = [ + 'start_date' => $this->buildDateWithIsoAttribute($start_date), + 'separator' => ['#plain_text' => ' ' . $separator . ' '], + 'end_date' => $this->buildDateWithIsoAttribute($end_date), + ]; + } + else { + $elements[$delta] = $this->buildDateWithIsoAttribute($start_date); + } + + if (!empty($item->_attributes)) { + $elements[$delta]['#attributes'] += $item->_attributes; + // Unset field item attributes since they have been included in the + // formatter output and should not be rendered in the field template. + unset($item->_attributes); + } + } + } + + return $elements; + } + + /** + * {@inheritdoc} + */ + public function settingsForm(array $form, FormStateInterface $form_state) { + $form = parent::settingsForm($form, $form_state); + + $form['separator'] = [ + '#type' => 'textfield', + '#title' => $this->t('Date separator'), + '#description' => $this->t('The string to separate the start and end dates'), + '#default_value' => $this->getSetting('separator'), + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function settingsSummary() { + $summary = parent::settingsSummary(); + + if ($separator = $this->getSetting('separator')) { + $summary[] = $this->t('Separator: %separator', ['%separator' => $separator]); + } + + return $summary; + } + +} diff --git a/core/modules/datetime_range/src/Plugin/Field/FieldFormatter/DateRangePlainFormatter.php b/core/modules/datetime_range/src/Plugin/Field/FieldFormatter/DateRangePlainFormatter.php new file mode 100644 index 0000000000000000000000000000000000000000..4842e1fd14d1b23c63859f6f5285cc5d318562e2 --- /dev/null +++ b/core/modules/datetime_range/src/Plugin/Field/FieldFormatter/DateRangePlainFormatter.php @@ -0,0 +1,96 @@ + '-', + ] + parent::defaultSettings(); + } + + /** + * {@inheritdoc} + */ + public function viewElements(FieldItemListInterface $items, $langcode) { + $elements = []; + $separator = $this->getSetting('separator'); + + foreach ($items as $delta => $item) { + if (!empty($item->start_date) && !empty($item->end_date)) { + /** @var \Drupal\Core\Datetime\DrupalDateTime $start_date */ + $start_date = $item->start_date; + /** @var \Drupal\Core\Datetime\DrupalDateTime $end_date */ + $end_date = $item->end_date; + + if ($start_date->format('U') !== $end_date->format('U')) { + $elements[$delta] = [ + 'start_date' => $this->buildDate($start_date), + 'separator' => ['#plain_text' => ' ' . $separator . ' '], + 'end_date' => $this->buildDate($end_date), + ]; + } + else { + $elements[$delta] = $this->buildDate($start_date); + } + } + } + + return $elements; + } + + /** + * {@inheritdoc} + */ + public function settingsForm(array $form, FormStateInterface $form_state) { + $form = parent::settingsForm($form, $form_state); + + $form['separator'] = [ + '#type' => 'textfield', + '#title' => $this->t('Date separator'), + '#description' => $this->t('The string to separate the start and end dates'), + '#default_value' => $this->getSetting('separator'), + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function settingsSummary() { + $summary = parent::settingsSummary(); + + if ($separator = $this->getSetting('separator')) { + $summary[] = $this->t('Separator: %separator', ['%separator' => $separator]); + } + + return $summary; + } + +} diff --git a/core/modules/datetime_range/src/Plugin/Field/FieldType/DateRangeFieldItemList.php b/core/modules/datetime_range/src/Plugin/Field/FieldType/DateRangeFieldItemList.php new file mode 100644 index 0000000000000000000000000000000000000000..e1450323ad660f13de51006fccc955d1b3d8e2ab --- /dev/null +++ b/core/modules/datetime_range/src/Plugin/Field/FieldType/DateRangeFieldItemList.php @@ -0,0 +1,131 @@ +getFieldDefinition()->getDefaultValueCallback())) { + $default_value = $this->getFieldDefinition()->getDefaultValueLiteral(); + + $element = parent::defaultValuesForm($form, $form_state); + + $element['default_date_type']['#title'] = $this->t('Default start date'); + $element['default_date_type']['#description'] = $this->t('Set a default value for the start date.'); + + $element['default_end_date_type'] = [ + '#type' => 'select', + '#title' => $this->t('Default end date'), + '#description' => $this->t('Set a default value for the end date.'), + '#default_value' => isset($default_value[0]['default_end_date_type']) ? $default_value[0]['default_end_date_type'] : '', + '#options' => [ + static::DEFAULT_VALUE_NOW => $this->t('Current date'), + static::DEFAULT_VALUE_CUSTOM => $this->t('Relative date'), + ], + '#empty_value' => '', + ]; + + $element['default_end_date'] = [ + '#type' => 'textfield', + '#title' => $this->t('Relative default value'), + '#description' => $this->t("Describe a time by reference to the current day, like '+90 days' (90 days from the day the field is created) or '+1 Saturday' (the next Saturday). See strtotime for more details."), + '#default_value' => (isset($default_value[0]['default_end_date_type']) && $default_value[0]['default_end_date_type'] == static::DEFAULT_VALUE_CUSTOM) ? $default_value[0]['default_end_date'] : '', + '#states' => [ + 'visible' => [ + ':input[id="edit-default-value-input-default-end-date-type"]' => ['value' => static::DEFAULT_VALUE_CUSTOM], + ], + ], + ]; + + return $element; + } + } + + /** + * {@inheritdoc} + */ + public function defaultValuesFormValidate(array $element, array &$form, FormStateInterface $form_state) { + if ($form_state->getValue(['default_value_input', 'default_date_type']) == static::DEFAULT_VALUE_CUSTOM) { + $is_strtotime = @strtotime($form_state->getValue(['default_value_input', 'default_date'])); + if (!$is_strtotime) { + $form_state->setErrorByName('default_value_input][default_date', $this->t('The relative start date value entered is invalid.')); + } + } + + if ($form_state->getValue(['default_value_input', 'default_end_date_type']) == static::DEFAULT_VALUE_CUSTOM) { + $is_strtotime = @strtotime($form_state->getValue(['default_value_input', 'default_end_date'])); + if (!$is_strtotime) { + $form_state->setErrorByName('default_value_input][default_end_date', $this->t('The relative end date value entered is invalid.')); + } + } + } + + /** + * {@inheritdoc} + */ + public function defaultValuesFormSubmit(array $element, array &$form, FormStateInterface $form_state) { + if ($form_state->getValue(['default_value_input', 'default_date_type']) || $form_state->getValue(['default_value_input', 'default_end_date_type'])) { + if ($form_state->getValue(['default_value_input', 'default_date_type']) == static::DEFAULT_VALUE_NOW) { + $form_state->setValueForElement($element['default_date'], static::DEFAULT_VALUE_NOW); + } + if ($form_state->getValue(['default_value_input', 'default_end_date_type']) == static::DEFAULT_VALUE_NOW) { + $form_state->setValueForElement($element['default_end_date'], static::DEFAULT_VALUE_NOW); + } + return [$form_state->getValue('default_value_input')]; + } + return []; + } + + /** + * {@inheritdoc} + */ + public static function processDefaultValue($default_value, FieldableEntityInterface $entity, FieldDefinitionInterface $definition) { + // Explicitly call the base class so that we can get the default value + // types. + $default_value = FieldItemList::processDefaultValue($default_value, $entity, $definition); + + // Allow either the start or end date to have a default, but not require + // defaults for both. + if (!empty($default_value[0]['default_date_type']) || !empty($default_value[0]['default_end_date_type'])) { + // A default value should be in the format and timezone used for date + // storage. All-day ranges are stored the same as date+time ranges. We + // only provide a default value for the first item, as do all fields. + // Otherwise, there is no way to clear out unwanted values on multiple + // value fields. + $storage_format = $definition->getSetting('datetime_type') == DateRangeItem::DATETIME_TYPE_DATE ? DATETIME_DATE_STORAGE_FORMAT : DATETIME_DATETIME_STORAGE_FORMAT; + $default_values = [[]]; + + if (!empty($default_value[0]['default_date_type'])) { + $start_date = new DrupalDateTime($default_value[0]['default_date'], DATETIME_STORAGE_TIMEZONE); + $start_value = $start_date->format($storage_format); + $default_values[0]['value'] = $start_value; + $default_values[0]['start_date'] = $start_date; + } + + if (!empty($default_value[0]['default_end_date_type'])) { + $end_date = new DrupalDateTime($default_value[0]['default_end_date'], DATETIME_STORAGE_TIMEZONE); + $end_value = $end_date->format($storage_format); + $default_values[0]['end_value'] = $end_value; + $default_values[0]['end_date'] = $end_date; + } + + $default_value = $default_values; + } + + return $default_value; + } + +} diff --git a/core/modules/datetime_range/src/Plugin/Field/FieldType/DateRangeItem.php b/core/modules/datetime_range/src/Plugin/Field/FieldType/DateRangeItem.php new file mode 100644 index 0000000000000000000000000000000000000000..49c4dce72a9b13aebff78d7aaf29a566ac9dcd6b --- /dev/null +++ b/core/modules/datetime_range/src/Plugin/Field/FieldType/DateRangeItem.php @@ -0,0 +1,132 @@ +setLabel(t('Start date value')) + ->setRequired(TRUE); + + $properties['start_date'] = DataDefinition::create('any') + ->setLabel(t('Computed start date')) + ->setDescription(t('The computed start DateTime object.')) + ->setComputed(TRUE) + ->setClass(DateTimeComputed::class) + ->setSetting('date source', 'value'); + + $properties['end_value'] = DataDefinition::create('datetime_iso8601') + ->setLabel(t('End date value')) + ->setRequired(TRUE); + + $properties['end_date'] = DataDefinition::create('any') + ->setLabel(t('Computed end date')) + ->setDescription(t('The computed end DateTime object.')) + ->setComputed(TRUE) + ->setClass(DateTimeComputed::class) + ->setSetting('date source', 'end_value'); + + return $properties; + } + + /** + * {@inheritdoc} + */ + public static function schema(FieldStorageDefinitionInterface $field_definition) { + $schema = parent::schema($field_definition); + + $schema['columns']['value']['description'] = 'The start date value.'; + + $schema['columns']['end_value'] = [ + 'description' => 'The end date value.', + ] + $schema['columns']['value']; + + $schema['indexes']['end_value'] = ['end_value']; + + return $schema; + } + + /** + * {@inheritdoc} + */ + public function storageSettingsForm(array &$form, FormStateInterface $form_state, $has_data) { + $element = parent::storageSettingsForm($form, $form_state, $has_data); + + $element['datetime_type']['#options'][static::DATETIME_TYPE_ALLDAY] = $this->t('All Day'); + + return $element; + } + + /** + * {@inheritdoc} + */ + public static function generateSampleValue(FieldDefinitionInterface $field_definition) { + $type = $field_definition->getSetting('datetime_type'); + + // Just pick a date in the past year. No guidance is provided by this Field + // type. + $start = REQUEST_TIME - mt_rand(0, 86400 * 365) - 86400; + $end = $start + 86400; + if ($type == static::DATETIME_TYPE_DATETIME) { + $values['value'] = gmdate(DATETIME_DATETIME_STORAGE_FORMAT, $start); + $values['end_value'] = gmdate(DATETIME_DATETIME_STORAGE_FORMAT, $end); + } + else { + $values['value'] = gmdate(DATETIME_DATE_STORAGE_FORMAT, $start); + $values['end_value'] = gmdate(DATETIME_DATE_STORAGE_FORMAT, $end); + } + return $values; + } + + /** + * {@inheritdoc} + */ + public function isEmpty() { + $start_value = $this->get('value')->getValue(); + $end_value = $this->get('end_value')->getValue(); + return ($start_value === NULL || $start_value === '') && ($end_value === NULL || $end_value === ''); + } + + /** + * {@inheritdoc} + */ + public function onChange($property_name, $notify = TRUE) { + // Enforce that the computed date is recalculated. + if ($property_name == 'value') { + $this->start_date = NULL; + } + elseif ($property_name == 'end_value') { + $this->end_date = NULL; + } + parent::onChange($property_name, $notify); + } + +} diff --git a/core/modules/datetime_range/src/Plugin/Field/FieldWidget/DateRangeDatelistWidget.php b/core/modules/datetime_range/src/Plugin/Field/FieldWidget/DateRangeDatelistWidget.php new file mode 100644 index 0000000000000000000000000000000000000000..3e0e6e4ce91ba737f321f25bb7e0657fe30e6ec2 --- /dev/null +++ b/core/modules/datetime_range/src/Plugin/Field/FieldWidget/DateRangeDatelistWidget.php @@ -0,0 +1,157 @@ + '15', + 'date_order' => 'YMD', + 'time_type' => '24', + ] + parent::defaultSettings(); + } + + /** + * {@inheritdoc} + */ + public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) { + $element = parent::formElement($items, $delta, $element, $form, $form_state); + + $date_order = $this->getSetting('date_order'); + + if ($this->getFieldSetting('datetime_type') == DateRangeItem::DATETIME_TYPE_DATETIME) { + $time_type = $this->getSetting('time_type'); + $increment = $this->getSetting('increment'); + } + else { + $time_type = ''; + $increment = ''; + } + + // Set up the date part order array. + switch ($date_order) { + default: + case 'YMD': + $date_part_order = ['year', 'month', 'day']; + break; + + case 'MDY': + $date_part_order = ['month', 'day', 'year']; + break; + + case 'DMY': + $date_part_order = ['day', 'month', 'year']; + break; + } + switch ($time_type) { + case '24': + $date_part_order = array_merge($date_part_order, ['hour', 'minute']); + break; + + case '12': + $date_part_order = array_merge($date_part_order, ['hour', 'minute', 'ampm']); + break; + + case 'none': + break; + } + + $element['value'] = [ + '#type' => 'datelist', + '#date_increment' => $increment, + '#date_part_order' => $date_part_order, + ] + $element['value']; + + $element['end_value'] = [ + '#type' => 'datelist', + '#date_increment' => $increment, + '#date_part_order' => $date_part_order, + ] + $element['end_value']; + + return $element; + } + + /** + * {@inheritdoc} + */ + public function settingsForm(array $form, FormStateInterface $form_state) { + $element = parent::settingsForm($form, $form_state); + + $element['date_order'] = [ + '#type' => 'select', + '#title' => $this->t('Date part order'), + '#default_value' => $this->getSetting('date_order'), + '#options' => ['MDY' => $this->t('Month/Day/Year'), 'DMY' => $this->t('Day/Month/Year'), 'YMD' => $this->t('Year/Month/Day')], + ]; + + if ($this->getFieldSetting('datetime_type') == DateRangeItem::DATETIME_TYPE_DATETIME) { + $element['time_type'] = [ + '#type' => 'select', + '#title' => $this->t('Time type'), + '#default_value' => $this->getSetting('time_type'), + '#options' => ['24' => $this->t('24 hour time'), '12' => $this->t('12 hour time')], + ]; + + $element['increment'] = [ + '#type' => 'select', + '#title' => $this->t('Time increments'), + '#default_value' => $this->getSetting('increment'), + '#options' => [ + 1 => $this->t('1 minute'), + 5 => $this->t('5 minute'), + 10 => $this->t('10 minute'), + 15 => $this->t('15 minute'), + 30 => $this->t('30 minute'), + ], + ]; + } + else { + $element['time_type'] = [ + '#type' => 'hidden', + '#value' => 'none', + ]; + + $element['increment'] = [ + '#type' => 'hidden', + '#value' => $this->getSetting('increment'), + ]; + } + + return $element; + } + + /** + * {@inheritdoc} + */ + public function settingsSummary() { + $summary = []; + + $summary[] = $this->t('Date part order: @order', ['@order' => $this->getSetting('date_order')]); + if ($this->getFieldSetting('datetime_type') == DateRangeItem::DATETIME_TYPE_DATETIME) { + $summary[] = $this->t('Time type: @time_type', ['@time_type' => $this->getSetting('time_type')]); + $summary[] = $this->t('Time increments: @increment', ['@increment' => $this->getSetting('increment')]); + } + + return $summary; + } + +} diff --git a/core/modules/datetime_range/src/Plugin/Field/FieldWidget/DateRangeDefaultWidget.php b/core/modules/datetime_range/src/Plugin/Field/FieldWidget/DateRangeDefaultWidget.php new file mode 100644 index 0000000000000000000000000000000000000000..79f799469250ee5b2b82bb41b4214ff86cb9515f --- /dev/null +++ b/core/modules/datetime_range/src/Plugin/Field/FieldWidget/DateRangeDefaultWidget.php @@ -0,0 +1,101 @@ +dateStorage = $date_storage; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $plugin_id, + $plugin_definition, + $configuration['field_definition'], + $configuration['settings'], + $configuration['third_party_settings'], + $container->get('entity_type.manager')->getStorage('date_format') + ); + } + + /** + * {@inheritdoc} + */ + public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) { + $element = parent::formElement($items, $delta, $element, $form, $form_state); + + // Identify the type of date and time elements to use. + switch ($this->getFieldSetting('datetime_type')) { + case DateRangeItem::DATETIME_TYPE_DATE: + case DateRangeItem::DATETIME_TYPE_ALLDAY: + $date_type = 'date'; + $time_type = 'none'; + $date_format = $this->dateStorage->load('html_date')->getPattern(); + $time_format = ''; + break; + + default: + $date_type = 'date'; + $time_type = 'time'; + $date_format = $this->dateStorage->load('html_date')->getPattern(); + $time_format = $this->dateStorage->load('html_time')->getPattern(); + break; + } + + $element['value'] += [ + '#date_date_format' => $date_format, + '#date_date_element' => $date_type, + '#date_date_callbacks' => [], + '#date_time_format' => $time_format, + '#date_time_element' => $time_type, + '#date_time_callbacks' => [], + ]; + + $element['end_value'] += [ + '#date_date_format' => $date_format, + '#date_date_element' => $date_type, + '#date_date_callbacks' => [], + '#date_time_format' => $time_format, + '#date_time_element' => $time_type, + '#date_time_callbacks' => [], + ]; + + return $element; + } + +} diff --git a/core/modules/datetime_range/src/Plugin/Field/FieldWidget/DateRangeWidgetBase.php b/core/modules/datetime_range/src/Plugin/Field/FieldWidget/DateRangeWidgetBase.php new file mode 100644 index 0000000000000000000000000000000000000000..ae9f24a32112bbb0c8c0c532e2eb860bde0053d3 --- /dev/null +++ b/core/modules/datetime_range/src/Plugin/Field/FieldWidget/DateRangeWidgetBase.php @@ -0,0 +1,167 @@ +t('Start'); + + $element['end_value'] = [ + '#title' => $this->t('End'), + ] + $element['value']; + + if ($items[$delta]->start_date) { + /** @var \Drupal\Core\Datetime\DrupalDateTime $start_date */ + $start_date = $items[$delta]->start_date; + $element['value']['#default_value'] = $this->createDefaultValue($start_date, $element['value']['#date_timezone']); + } + + if ($items[$delta]->end_date) { + /** @var \Drupal\Core\Datetime\DrupalDateTime $end_date */ + $end_date = $items[$delta]->end_date; + $element['end_value']['#default_value'] = $this->createDefaultValue($end_date, $element['end_value']['#date_timezone']); + } + + return $element; + } + + /** + * {@inheritdoc} + */ + public function massageFormValues(array $values, array $form, FormStateInterface $form_state) { + // The widget form element type has transformed the value to a + // DrupalDateTime object at this point. We need to convert it back to the + // storage timezone and format. + foreach ($values as &$item) { + if (!empty($item['value']) && $item['value'] instanceof DrupalDateTime) { + /** @var \Drupal\Core\Datetime\DrupalDateTime $start_date */ + $start_date = $item['value']; + switch ($this->getFieldSetting('datetime_type')) { + case DateRangeItem::DATETIME_TYPE_DATE: + // If this is a date-only field, set it to the default time so the + // timezone conversion can be reversed. + datetime_date_default_time($start_date); + $format = DATETIME_DATE_STORAGE_FORMAT; + break; + + case DateRangeItem::DATETIME_TYPE_ALLDAY: + // All day fields start at midnight on the starting date, but are + // stored like datetime fields, so we need to adjust the time. + // This function is called twice, so to prevent a double conversion + // we need to explicitly set the timezone. + $start_date->setTimeZone(timezone_open(drupal_get_user_timezone())); + $start_date->setTime(0, 0, 0); + $format = DATETIME_DATETIME_STORAGE_FORMAT; + break; + + default: + $format = DATETIME_DATETIME_STORAGE_FORMAT; + break; + } + // Adjust the date for storage. + $start_date->setTimezone(new \DateTimezone(DATETIME_STORAGE_TIMEZONE)); + $item['value'] = $start_date->format($format); + } + + if (!empty($item['end_value']) && $item['end_value'] instanceof DrupalDateTime) { + /** @var \Drupal\Core\Datetime\DrupalDateTime $end_date */ + $end_date = $item['end_value']; + switch ($this->getFieldSetting('datetime_type')) { + case DateRangeItem::DATETIME_TYPE_DATE: + // If this is a date-only field, set it to the default time so the + // timezone conversion can be reversed. + datetime_date_default_time($end_date); + $format = DATETIME_DATE_STORAGE_FORMAT; + break; + + case DateRangeItem::DATETIME_TYPE_ALLDAY: + // All day fields end at midnight on the end date, but are + // stored like datetime fields, so we need to adjust the time. + // This function is called twice, so to prevent a double conversion + // we need to explicitly set the timezone. + $end_date->setTimeZone(timezone_open(drupal_get_user_timezone())); + $end_date->setTime(23, 59, 59); + $format = DATETIME_DATETIME_STORAGE_FORMAT; + break; + + default: + $format = DATETIME_DATETIME_STORAGE_FORMAT; + break; + } + // Adjust the date for storage. + $end_date->setTimezone(new \DateTimezone(DATETIME_STORAGE_TIMEZONE)); + $item['end_value'] = $end_date->format($format); + } + } + + return $values; + } + + /** + * #element_validate callback to ensure that the start date <= the end date. + * + * @param array $element + * An associative array containing the properties and children of the + * generic form element. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * @param array $complete_form + * The complete form structure. + */ + public function validateStartEnd(array &$element, FormStateInterface $form_state, array &$complete_form) { + $start_date = $element['value']['#value']['object']; + $end_date = $element['end_value']['#value']['object']; + + if ($start_date instanceof DrupalDateTime && $end_date instanceof DrupalDateTime) { + if ($start_date->format('U') !== $end_date->format('U')) { + $interval = $start_date->diff($end_date); + if ($interval->invert === 1) { + $form_state->setError($element, $this->t('The @title end date cannot be before the start date', ['@title' => $element['#title']])); + } + } + } + } + + /** + * Creates a date object for use as a default value. + * + * This will take a default value, apply the proper timezone for display in + * a widget, and set the default time for date-only fields. + * + * @param \Drupal\Core\Datetime\DrupalDateTime $date + * The UTC default date. + * @param string $timezone + * The timezone to apply. + * + * @return \Drupal\Core\Datetime\DrupalDateTime + * A date object for use as a default value in a field widget. + */ + protected function createDefaultValue($date, $timezone) { + // The date was created and verified during field_load(), so it is safe to + // use without further inspection. + if ($this->getFieldSetting('datetime_type') == DateTimeItem::DATETIME_TYPE_DATE) { + // A date without time will pick up the current time, use the default + // time. + datetime_date_default_time($date); + } + $date->setTimezone(new \DateTimeZone($timezone)); + return $date; + } + +} diff --git a/core/modules/datetime_range/src/Tests/DateRangeFieldTest.php b/core/modules/datetime_range/src/Tests/DateRangeFieldTest.php new file mode 100644 index 0000000000000000000000000000000000000000..078a67e88160a0d6b993a40b4fc0661d20f6d366 --- /dev/null +++ b/core/modules/datetime_range/src/Tests/DateRangeFieldTest.php @@ -0,0 +1,1432 @@ +drupalCreateUser([ + 'access content', + 'view test entity', + 'administer entity_test content', + 'administer entity_test form display', + 'administer content types', + 'administer node fields', + ]); + $this->drupalLogin($web_user); + + // Create a field with settings to validate. + $field_name = Unicode::strtolower($this->randomMachineName()); + $this->fieldStorage = FieldStorageConfig::create([ + 'field_name' => $field_name, + 'entity_type' => 'entity_test', + 'type' => 'daterange', + 'settings' => ['datetime_type' => DateRangeItem::DATETIME_TYPE_DATE], + ]); + $this->fieldStorage->save(); + $this->field = FieldConfig::create([ + 'field_storage' => $this->fieldStorage, + 'bundle' => 'entity_test', + 'required' => TRUE, + ]); + $this->field->save(); + + entity_get_form_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'default') + ->setComponent($field_name, [ + 'type' => 'daterange_default', + ]) + ->save(); + + $this->defaultSettings = [ + 'separator' => '-', + 'timezone_override' => '', + ]; + + $this->displayOptions = [ + 'type' => 'daterange_default', + 'label' => 'hidden', + 'settings' => ['format_type' => 'medium'] + $this->defaultSettings, + ]; + entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full') + ->setComponent($field_name, $this->displayOptions) + ->save(); + + $this->dateFormatter = \Drupal::service('date.formatter'); + } + + /** + * Tests date field functionality. + */ + public function testDateRangeField() { + $field_name = $this->fieldStorage->getName(); + + // Loop through defined timezones to test that date-only fields work at the + // extremes. + foreach (static::$timezones as $timezone) { + + $this->setSiteTimezone($timezone); + + // Ensure field is set to a date-only field. + $this->fieldStorage->setSetting('datetime_type', DateRangeItem::DATETIME_TYPE_DATE); + $this->fieldStorage->save(); + + // Display creation form. + $this->drupalGet('entity_test/add'); + $this->assertFieldByName("{$field_name}[0][value][date]", '', 'Start date element found.'); + $this->assertFieldByName("{$field_name}[0][end_value][date]", '', 'End date element found.'); + $this->assertFieldByXPath('//*[@id="edit-' . $field_name . '-wrapper"]/h4[contains(@class, "js-form-required")]', TRUE, 'Required markup found'); + $this->assertNoFieldByName("{$field_name}[0][value][time]", '', 'Start time element not found.'); + $this->assertNoFieldByName("{$field_name}[0][end_value][time]", '', 'End time element not found.'); + + // Build up dates in the UTC timezone. + $value = '2012-12-31 00:00:00'; + $start_date = new DrupalDateTime($value, 'UTC'); + $end_value = '2013-06-06 00:00:00'; + $end_date = new DrupalDateTime($end_value, 'UTC'); + + // Submit a valid date and ensure it is accepted. + $date_format = DateFormat::load('html_date')->getPattern(); + $time_format = DateFormat::load('html_time')->getPattern(); + + $edit = array( + "{$field_name}[0][value][date]" => $start_date->format($date_format), + "{$field_name}[0][end_value][date]" => $end_date->format($date_format), + ); + $this->drupalPostForm(NULL, $edit, t('Save')); + preg_match('|entity_test/manage/(\d+)|', $this->url, $match); + $id = $match[1]; + $this->assertText(t('entity_test @id has been created.', array('@id' => $id))); + $this->assertRaw($start_date->format($date_format)); + $this->assertNoRaw($start_date->format($time_format)); + $this->assertRaw($end_date->format($date_format)); + $this->assertNoRaw($end_date->format($time_format)); + + // Verify the date doesn't change when entity is edited through the form. + $entity = EntityTest::load($id); + $this->assertEqual('2012-12-31', $entity->{$field_name}->value); + $this->assertEqual('2013-06-06', $entity->{$field_name}->end_value); + $this->drupalGet('entity_test/manage/' . $id . '/edit'); + $this->drupalPostForm(NULL, [], t('Save')); + $this->drupalGet('entity_test/manage/' . $id . '/edit'); + $this->drupalPostForm(NULL, [], t('Save')); + $this->drupalGet('entity_test/manage/' . $id . '/edit'); + $this->drupalPostForm(NULL, [], t('Save')); + $entity = EntityTest::load($id); + $this->assertEqual('2012-12-31', $entity->{$field_name}->value); + $this->assertEqual('2013-06-06', $entity->{$field_name}->end_value); + + // Formats that display a time component for date-only fields will display + // the default time, so that is applied before calculating the expected + // value. + datetime_date_default_time($start_date); + datetime_date_default_time($end_date); + + // Reset display options since these get changed below. + $this->displayOptions = [ + 'type' => 'daterange_default', + 'label' => 'hidden', + 'settings' => [ + 'format_type' => 'long', + 'separator' => 'THESEPARATOR', + ] + $this->defaultSettings, + ]; + + // Verify that the default formatter works. + entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full') + ->setComponent($field_name, $this->displayOptions) + ->save(); + + $start_expected = $this->dateFormatter->format($start_date->getTimestamp(), 'long', '', DATETIME_STORAGE_TIMEZONE); + $start_expected_iso = $this->dateFormatter->format($start_date->getTimestamp(), 'custom', 'Y-m-d\TH:i:s\Z', DATETIME_STORAGE_TIMEZONE); + $end_expected = $this->dateFormatter->format($end_date->getTimestamp(), 'long', '', DATETIME_STORAGE_TIMEZONE); + $end_expected_iso = $this->dateFormatter->format($end_date->getTimestamp(), 'custom', 'Y-m-d\TH:i:s\Z', DATETIME_STORAGE_TIMEZONE); + $this->renderTestEntity($id); + $this->assertFieldByXPath('//time[@datetime="' . $start_expected_iso . '"]', $start_expected, new FormattableMarkup('Formatted date field using %value format displayed as %expected with %expected_iso attribute.', [ + '%value' => 'long', + '%expected' => $start_expected, + '%expected_iso' => $start_expected_iso, + ])); + $this->assertFieldByXPath('//time[@datetime="' . $end_expected_iso . '"]', $end_expected, new FormattableMarkup('Formatted date field using %value format displayed as %expected with %expected_iso attribute.', [ + '%value' => 'long', + '%expected' => $end_expected, + '%expected_iso' => $end_expected_iso, + ])); + $this->assertText(' THESEPARATOR ', 'Found proper separator'); + + // Verify that the plain formatter works. + $this->displayOptions['type'] = 'daterange_plain'; + $this->displayOptions['settings'] = $this->defaultSettings; + entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full') + ->setComponent($field_name, $this->displayOptions) + ->save(); + $expected = $start_date->format(DATETIME_DATE_STORAGE_FORMAT) . ' - ' . $end_date->format(DATETIME_DATE_STORAGE_FORMAT); + $this->renderTestEntity($id); + $this->assertText($expected, new FormattableMarkup('Formatted date field using plain format displayed as %expected.', array('%expected' => $expected))); + + // Verify that the custom formatter works. + $this->displayOptions['type'] = 'daterange_custom'; + $this->displayOptions['settings'] = array('date_format' => 'm/d/Y') + $this->defaultSettings; + entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full') + ->setComponent($field_name, $this->displayOptions) + ->save(); + $expected = $start_date->format($this->displayOptions['settings']['date_format']) . ' - ' . $end_date->format($this->displayOptions['settings']['date_format']); + $this->renderTestEntity($id); + $this->assertText($expected, new FormattableMarkup('Formatted date field using daterange_custom format displayed as %expected.', array('%expected' => $expected))); + + // Test formatters when start date and end date are the same + $this->drupalGet('entity_test/add'); + $value = '2012-12-31 00:00:00'; + $start_date = new DrupalDateTime($value, 'UTC'); + + $date_format = DateFormat::load('html_date')->getPattern(); + $time_format = DateFormat::load('html_time')->getPattern(); + + $edit = array( + "{$field_name}[0][value][date]" => $start_date->format($date_format), + "{$field_name}[0][end_value][date]" => $start_date->format($date_format), + ); + + $this->drupalPostForm(NULL, $edit, t('Save')); + preg_match('|entity_test/manage/(\d+)|', $this->url, $match); + $id = $match[1]; + $this->assertText(t('entity_test @id has been created.', array('@id' => $id))); + + datetime_date_default_time($start_date); + + $this->displayOptions = [ + 'type' => 'daterange_default', + 'label' => 'hidden', + 'settings' => [ + 'format_type' => 'long', + 'separator' => 'THESEPARATOR', + ] + $this->defaultSettings, + ]; + + entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full') + ->setComponent($field_name, $this->displayOptions) + ->save(); + + $start_expected = $this->dateFormatter->format($start_date->getTimestamp(), 'long', '', DATETIME_STORAGE_TIMEZONE); + $start_expected_iso = $this->dateFormatter->format($start_date->getTimestamp(), 'custom', 'Y-m-d\TH:i:s\Z', DATETIME_STORAGE_TIMEZONE); + $this->renderTestEntity($id); + $this->assertFieldByXPath('//time[@datetime="' . $start_expected_iso . '"]', $start_expected, new FormattableMarkup('Formatted date field using %value format displayed as %expected with %expected_iso attribute.', [ + '%value' => 'long', + '%expected' => $start_expected, + '%expected_iso' => $start_expected_iso, + ])); + $this->assertNoText(' THESEPARATOR ', 'Separator not found on page'); + + $this->displayOptions['type'] = 'daterange_plain'; + $this->displayOptions['settings'] = $this->defaultSettings; + entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full') + ->setComponent($field_name, $this->displayOptions) + ->save(); + $expected = $start_date->format(DATETIME_DATE_STORAGE_FORMAT); + $this->renderTestEntity($id); + $this->assertText($expected, new FormattableMarkup('Formatted date field using plain format displayed as %expected.', array('%expected' => $expected))); + $this->assertNoText(' THESEPARATOR ', 'Separator not found on page'); + + $this->displayOptions['type'] = 'daterange_custom'; + $this->displayOptions['settings'] = array('date_format' => 'm/d/Y') + $this->defaultSettings; + entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full') + ->setComponent($field_name, $this->displayOptions) + ->save(); + $expected = $start_date->format($this->displayOptions['settings']['date_format']); + $this->renderTestEntity($id); + $this->assertText($expected, new FormattableMarkup('Formatted date field using daterange_custom format displayed as %expected.', array('%expected' => $expected))); + $this->assertNoText(' THESEPARATOR ', 'Separator not found on page'); + } + } + + /** + * Tests date and time field. + */ + public function testDatetimeRangeField() { + $field_name = $this->fieldStorage->getName(); + + // Ensure the field to a datetime field. + $this->fieldStorage->setSetting('datetime_type', DateRangeItem::DATETIME_TYPE_DATETIME); + $this->fieldStorage->save(); + + // Display creation form. + $this->drupalGet('entity_test/add'); + $this->assertFieldByName("{$field_name}[0][value][date]", '', 'Start date element found.'); + $this->assertFieldByName("{$field_name}[0][value][time]", '', 'Start time element found.'); + $this->assertFieldByName("{$field_name}[0][end_value][date]", '', 'End date element found.'); + $this->assertFieldByName("{$field_name}[0][end_value][time]", '', 'End time element found.'); + + // Build up dates in the UTC timezone. + $value = '2012-12-31 00:00:00'; + $start_date = new DrupalDateTime($value, 'UTC'); + $end_value = '2013-06-06 00:00:00'; + $end_date = new DrupalDateTime($end_value, 'UTC'); + + // Update the timezone to the system default. + $start_date->setTimezone(timezone_open(drupal_get_user_timezone())); + $end_date->setTimezone(timezone_open(drupal_get_user_timezone())); + + // Submit a valid date and ensure it is accepted. + $date_format = DateFormat::load('html_date')->getPattern(); + $time_format = DateFormat::load('html_time')->getPattern(); + + $edit = array( + "{$field_name}[0][value][date]" => $start_date->format($date_format), + "{$field_name}[0][value][time]" => $start_date->format($time_format), + "{$field_name}[0][end_value][date]" => $end_date->format($date_format), + "{$field_name}[0][end_value][time]" => $end_date->format($time_format), + ); + $this->drupalPostForm(NULL, $edit, t('Save')); + preg_match('|entity_test/manage/(\d+)|', $this->url, $match); + $id = $match[1]; + $this->assertText(t('entity_test @id has been created.', array('@id' => $id))); + $this->assertRaw($start_date->format($date_format)); + $this->assertRaw($start_date->format($time_format)); + $this->assertRaw($end_date->format($date_format)); + $this->assertRaw($end_date->format($time_format)); + + // Verify that the default formatter works. + $this->displayOptions['settings'] = [ + 'format_type' => 'long', + 'separator' => 'THESEPARATOR', + ] + $this->defaultSettings; + entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full') + ->setComponent($field_name, $this->displayOptions) + ->save(); + + $start_expected = $this->dateFormatter->format($start_date->getTimestamp(), 'long'); + $start_expected_iso = $this->dateFormatter->format($start_date->getTimestamp(), 'custom', 'Y-m-d\TH:i:s\Z', 'UTC'); + $end_expected = $this->dateFormatter->format($end_date->getTimestamp(), 'long'); + $end_expected_iso = $this->dateFormatter->format($end_date->getTimestamp(), 'custom', 'Y-m-d\TH:i:s\Z', 'UTC'); + $this->renderTestEntity($id); + $this->assertFieldByXPath('//time[@datetime="' . $start_expected_iso . '"]', $start_expected, new FormattableMarkup('Formatted date field using %value format displayed as %expected with %expected_iso attribute.', ['%value' => 'long', '%expected' => $start_expected, '%expected_iso' => $start_expected_iso])); + $this->assertFieldByXPath('//time[@datetime="' . $end_expected_iso . '"]', $end_expected, new FormattableMarkup('Formatted date field using %value format displayed as %expected with %expected_iso attribute.', ['%value' => 'long', '%expected' => $end_expected, '%expected_iso' => $end_expected_iso])); + $this->assertText(' THESEPARATOR ', 'Found proper separator'); + + // Verify that the plain formatter works. + $this->displayOptions['type'] = 'daterange_plain'; + $this->displayOptions['settings'] = $this->defaultSettings; + entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full') + ->setComponent($field_name, $this->displayOptions) + ->save(); + $expected = $start_date->format(DATETIME_DATETIME_STORAGE_FORMAT) . ' - ' . $end_date->format(DATETIME_DATETIME_STORAGE_FORMAT); + $this->renderTestEntity($id); + $this->assertText($expected, new FormattableMarkup('Formatted date field using plain format displayed as %expected.', array('%expected' => $expected))); + + // Verify that the 'datetime_custom' formatter works. + $this->displayOptions['type'] = 'daterange_custom'; + $this->displayOptions['settings'] = ['date_format' => 'm/d/Y g:i:s A'] + $this->defaultSettings; + entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full') + ->setComponent($field_name, $this->displayOptions) + ->save(); + $expected = $start_date->format($this->displayOptions['settings']['date_format']) . ' - ' . $end_date->format($this->displayOptions['settings']['date_format']); + $this->renderTestEntity($id); + $this->assertText($expected, new FormattableMarkup('Formatted date field using daterange_custom format displayed as %expected.', array('%expected' => $expected))); + + // Verify that the 'timezone_override' setting works. + $this->displayOptions['type'] = 'daterange_custom'; + $this->displayOptions['settings'] = ['date_format' => 'm/d/Y g:i:s A', 'timezone_override' => 'America/New_York'] + $this->defaultSettings; + entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full') + ->setComponent($field_name, $this->displayOptions) + ->save(); + $expected = $start_date->format($this->displayOptions['settings']['date_format'], ['timezone' => 'America/New_York']); + $expected .= ' - ' . $end_date->format($this->displayOptions['settings']['date_format'], ['timezone' => 'America/New_York']); + $this->renderTestEntity($id); + $this->assertText($expected, new FormattableMarkup('Formatted date field using daterange_custom format displayed as %expected.', array('%expected' => $expected))); + + // Test formatters when start date and end date are the same + $this->drupalGet('entity_test/add'); + $value = '2012-12-31 00:00:00'; + $start_date = new DrupalDateTime($value, 'UTC'); + $start_date->setTimezone(timezone_open(drupal_get_user_timezone())); + + $date_format = DateFormat::load('html_date')->getPattern(); + $time_format = DateFormat::load('html_time')->getPattern(); + + $edit = array( + "{$field_name}[0][value][date]" => $start_date->format($date_format), + "{$field_name}[0][value][time]" => $start_date->format($time_format), + "{$field_name}[0][end_value][date]" => $start_date->format($date_format), + "{$field_name}[0][end_value][time]" => $start_date->format($time_format), + ); + + $this->drupalPostForm(NULL, $edit, t('Save')); + preg_match('|entity_test/manage/(\d+)|', $this->url, $match); + $id = $match[1]; + $this->assertText(t('entity_test @id has been created.', array('@id' => $id))); + + $this->displayOptions = [ + 'type' => 'daterange_default', + 'label' => 'hidden', + 'settings' => [ + 'format_type' => 'long', + 'separator' => 'THESEPARATOR', + ] + $this->defaultSettings, + ]; + + entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full') + ->setComponent($field_name, $this->displayOptions) + ->save(); + + $start_expected = $this->dateFormatter->format($start_date->getTimestamp(), 'long'); + $start_expected_iso = $this->dateFormatter->format($start_date->getTimestamp(), 'custom', 'Y-m-d\TH:i:s\Z', 'UTC'); + $this->renderTestEntity($id); + $this->assertFieldByXPath('//time[@datetime="' . $start_expected_iso . '"]', $start_expected, new FormattableMarkup('Formatted date field using %value format displayed as %expected with %expected_iso attribute.', ['%value' => 'long', '%expected' => $start_expected, '%expected_iso' => $start_expected_iso])); + $this->assertNoText(' THESEPARATOR ', 'Separator not found on page'); + + $this->displayOptions['type'] = 'daterange_plain'; + $this->displayOptions['settings'] = $this->defaultSettings; + entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full') + ->setComponent($field_name, $this->displayOptions) + ->save(); + $expected = $start_date->format(DATETIME_DATETIME_STORAGE_FORMAT); + $this->renderTestEntity($id); + $this->assertText($expected, new FormattableMarkup('Formatted date field using plain format displayed as %expected.', array('%expected' => $expected))); + $this->assertNoText(' THESEPARATOR ', 'Separator not found on page'); + + $this->displayOptions['type'] = 'daterange_custom'; + $this->displayOptions['settings'] = ['date_format' => 'm/d/Y g:i:s A'] + $this->defaultSettings; + entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full') + ->setComponent($field_name, $this->displayOptions) + ->save(); + $expected = $start_date->format($this->displayOptions['settings']['date_format']); + $this->renderTestEntity($id); + $this->assertText($expected, new FormattableMarkup('Formatted date field using daterange_custom format displayed as %expected.', array('%expected' => $expected))); + $this->assertNoText(' THESEPARATOR ', 'Separator not found on page'); + } + + /** + * Tests all-day field. + */ + public function testAlldayRangeField() { + $field_name = $this->fieldStorage->getName(); + + // Ensure field is set to a all-day field. + $this->fieldStorage->setSetting('datetime_type', DateRangeItem::DATETIME_TYPE_ALLDAY); + $this->fieldStorage->save(); + + // Display creation form. + $this->drupalGet('entity_test/add'); + $this->assertFieldByName("{$field_name}[0][value][date]", '', 'Start date element found.'); + $this->assertFieldByName("{$field_name}[0][end_value][date]", '', 'End date element found.'); + $this->assertFieldByXPath('//*[@id="edit-' . $field_name . '-wrapper"]/h4[contains(@class, "js-form-required")]', TRUE, 'Required markup found'); + $this->assertNoFieldByName("{$field_name}[0][value][time]", '', 'Start time element not found.'); + $this->assertNoFieldByName("{$field_name}[0][end_value][time]", '', 'End time element not found.'); + + // Build up dates in the proper timezone. + $value = '2012-12-31 00:00:00'; + $start_date = new DrupalDateTime($value, timezone_open(drupal_get_user_timezone())); + $end_value = '2013-06-06 23:59:59'; + $end_date = new DrupalDateTime($end_value, timezone_open(drupal_get_user_timezone())); + + // Submit a valid date and ensure it is accepted. + $date_format = DateFormat::load('html_date')->getPattern(); + $time_format = DateFormat::load('html_time')->getPattern(); + + $edit = array( + "{$field_name}[0][value][date]" => $start_date->format($date_format), + "{$field_name}[0][end_value][date]" => $end_date->format($date_format), + ); + $this->drupalPostForm(NULL, $edit, t('Save')); + preg_match('|entity_test/manage/(\d+)|', $this->url, $match); + $id = $match[1]; + $this->assertText(t('entity_test @id has been created.', array('@id' => $id))); + $this->assertRaw($start_date->format($date_format)); + $this->assertNoRaw($start_date->format($time_format)); + $this->assertRaw($end_date->format($date_format)); + $this->assertNoRaw($end_date->format($time_format)); + + // Verify that the default formatter works. + $this->displayOptions['settings'] = [ + 'format_type' => 'long', + 'separator' => 'THESEPARATOR', + ] + $this->defaultSettings; + entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full') + ->setComponent($field_name, $this->displayOptions) + ->save(); + + $start_expected = $this->dateFormatter->format($start_date->getTimestamp(), 'long'); + $start_expected_iso = $this->dateFormatter->format($start_date->getTimestamp(), 'custom', 'Y-m-d\TH:i:s\Z', 'UTC'); + $end_expected = $this->dateFormatter->format($end_date->getTimestamp(), 'long'); + $end_expected_iso = $this->dateFormatter->format($end_date->getTimestamp(), 'custom', 'Y-m-d\TH:i:s\Z', 'UTC'); + $this->renderTestEntity($id); + $this->assertFieldByXPath('//time[@datetime="' . $start_expected_iso . '"]', $start_expected, new FormattableMarkup('Formatted date field using %value format displayed as %expected with %expected_iso attribute.', ['%value' => 'long', '%expected' => $start_expected, '%expected_iso' => $start_expected_iso])); + $this->assertFieldByXPath('//time[@datetime="' . $end_expected_iso . '"]', $end_expected, new FormattableMarkup('Formatted date field using %value format displayed as %expected with %expected_iso attribute.', ['%value' => 'long', '%expected' => $end_expected, '%expected_iso' => $end_expected_iso])); + $this->assertText(' THESEPARATOR ', 'Found proper separator'); + + // Verify that the plain formatter works. + $this->displayOptions['type'] = 'daterange_plain'; + $this->displayOptions['settings'] = $this->defaultSettings; + entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full') + ->setComponent($field_name, $this->displayOptions) + ->save(); + $expected = $start_date->format(DATETIME_DATETIME_STORAGE_FORMAT) . ' - ' . $end_date->format(DATETIME_DATETIME_STORAGE_FORMAT); + $this->renderTestEntity($id); + $this->assertText($expected, new FormattableMarkup('Formatted date field using plain format displayed as %expected.', array('%expected' => $expected))); + + // Verify that the custom formatter works. + $this->displayOptions['type'] = 'daterange_custom'; + $this->displayOptions['settings'] = array('date_format' => 'm/d/Y') + $this->defaultSettings; + entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full') + ->setComponent($field_name, $this->displayOptions) + ->save(); + $expected = $start_date->format($this->displayOptions['settings']['date_format']) . ' - ' . $end_date->format($this->displayOptions['settings']['date_format']); + $this->renderTestEntity($id); + $this->assertText($expected, new FormattableMarkup('Formatted date field using daterange_custom format displayed as %expected.', array('%expected' => $expected))); + + // Verify that the 'timezone_override' setting works. + $this->displayOptions['type'] = 'daterange_custom'; + $this->displayOptions['settings'] = ['date_format' => 'm/d/Y g:i:s A', 'timezone_override' => 'America/New_York'] + $this->defaultSettings; + entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full') + ->setComponent($field_name, $this->displayOptions) + ->save(); + $expected = $start_date->format($this->displayOptions['settings']['date_format'], ['timezone' => 'America/New_York']); + $expected .= ' - ' . $end_date->format($this->displayOptions['settings']['date_format'], ['timezone' => 'America/New_York']); + $this->renderTestEntity($id); + $this->assertText($expected, new FormattableMarkup('Formatted date field using daterange_custom format displayed as %expected.', array('%expected' => $expected))); + + // Test formatters when start date and end date are the same + $this->drupalGet('entity_test/add'); + + $value = '2012-12-31 00:00:00'; + $start_date = new DrupalDateTime($value, timezone_open(drupal_get_user_timezone())); + $end_value = '2012-12-31 23:59:59'; + $end_date = new DrupalDateTime($end_value, timezone_open(drupal_get_user_timezone())); + + $date_format = DateFormat::load('html_date')->getPattern(); + $time_format = DateFormat::load('html_time')->getPattern(); + + $edit = array( + "{$field_name}[0][value][date]" => $start_date->format($date_format), + "{$field_name}[0][end_value][date]" => $start_date->format($date_format), + ); + $this->drupalPostForm(NULL, $edit, t('Save')); + preg_match('|entity_test/manage/(\d+)|', $this->url, $match); + $id = $match[1]; + $this->assertText(t('entity_test @id has been created.', array('@id' => $id))); + + $this->displayOptions = [ + 'type' => 'daterange_default', + 'label' => 'hidden', + 'settings' => [ + 'format_type' => 'long', + 'separator' => 'THESEPARATOR', + ] + $this->defaultSettings, + ]; + + entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full') + ->setComponent($field_name, $this->displayOptions) + ->save(); + + $start_expected = $this->dateFormatter->format($start_date->getTimestamp(), 'long'); + $start_expected_iso = $this->dateFormatter->format($start_date->getTimestamp(), 'custom', 'Y-m-d\TH:i:s\Z', 'UTC'); + $end_expected = $this->dateFormatter->format($end_date->getTimestamp(), 'long'); + $end_expected_iso = $this->dateFormatter->format($end_date->getTimestamp(), 'custom', 'Y-m-d\TH:i:s\Z', 'UTC'); + $this->renderTestEntity($id); + $this->assertFieldByXPath('//time[@datetime="' . $start_expected_iso . '"]', $start_expected, new FormattableMarkup('Formatted date field using %value format displayed as %expected with %expected_iso attribute.', ['%value' => 'long', '%expected' => $start_expected, '%expected_iso' => $start_expected_iso])); + $this->assertFieldByXPath('//time[@datetime="' . $end_expected_iso . '"]', $end_expected, new FormattableMarkup('Formatted date field using %value format displayed as %expected with %expected_iso attribute.', ['%value' => 'long', '%expected' => $end_expected, '%expected_iso' => $end_expected_iso])); + $this->assertText(' THESEPARATOR ', 'Found proper separator'); + + $this->displayOptions['type'] = 'daterange_plain'; + entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full') + ->setComponent($field_name, $this->displayOptions) + ->save(); + $expected = $start_date->format(DATETIME_DATETIME_STORAGE_FORMAT) . ' THESEPARATOR ' . $end_date->format(DATETIME_DATETIME_STORAGE_FORMAT); + $this->renderTestEntity($id); + $this->assertText($expected, new FormattableMarkup('Formatted date field using plain format displayed as %expected.', array('%expected' => $expected))); + $this->assertText(' THESEPARATOR ', 'Found proper separator'); + + $this->displayOptions['type'] = 'daterange_custom'; + $this->displayOptions['settings']['date_format'] = 'm/d/Y'; + entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full') + ->setComponent($field_name, $this->displayOptions) + ->save(); + $expected = $start_date->format($this->displayOptions['settings']['date_format']) . ' THESEPARATOR ' . $end_date->format($this->displayOptions['settings']['date_format']); + $this->renderTestEntity($id); + $this->assertText($expected, new FormattableMarkup('Formatted date field using daterange_custom format displayed as %expected.', array('%expected' => $expected))); + $this->assertText(' THESEPARATOR ', 'Found proper separator'); + + } + + /** + * Tests Date Range List Widget functionality. + */ + public function testDatelistWidget() { + $field_name = $this->fieldStorage->getName(); + + // Ensure field is set to a date only field. + $this->fieldStorage->setSetting('datetime_type', DateRangeItem::DATETIME_TYPE_DATE); + $this->fieldStorage->save(); + + // Change the widget to a datelist widget. + entity_get_form_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'default') + ->setComponent($field_name, [ + 'type' => 'daterange_datelist', + 'settings' => [ + 'date_order' => 'YMD', + ], + ]) + ->save(); + \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions(); + + // Display creation form. + $this->drupalGet('entity_test/add'); + + // Assert that Hour and Minute Elements do not appear on Date Only. + $this->assertNoFieldByXPath("//*[@id=\"edit-$field_name-0-value-hour\"]", NULL, 'Hour element not found on Date Only.'); + $this->assertNoFieldByXPath("//*[@id=\"edit-$field_name-0-value-minute\"]", NULL, 'Minute element not found on Date Only.'); + $this->assertNoFieldByXPath("//*[@id=\"edit-$field_name-0-end-value-hour\"]", NULL, 'Hour element not found on Date Only.'); + $this->assertNoFieldByXPath("//*[@id=\"edit-$field_name-0-end-value-minute\"]", NULL, 'Minute element not found on Date Only.'); + + // Go to the form display page to assert that increment option does not + // appear on Date Only. + $fieldEditUrl = 'entity_test/structure/entity_test/form-display'; + $this->drupalGet($fieldEditUrl); + + // Click on the widget settings button to open the widget settings form. + $this->drupalPostAjaxForm(NULL, [], $field_name . "_settings_edit"); + $xpathIncr = "//select[starts-with(@id, \"edit-fields-$field_name-settings-edit-form-settings-increment\")]"; + $this->assertNoFieldByXPath($xpathIncr, NULL, 'Increment element not found for Date Only.'); + + // Change the field is set to an all day field. + $this->fieldStorage->setSetting('datetime_type', DateRangeItem::DATETIME_TYPE_ALLDAY); + $this->fieldStorage->save(); + + // Change the widget to a datelist widget. + entity_get_form_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'default') + ->setComponent($field_name, [ + 'type' => 'daterange_datelist', + 'settings' => [ + 'date_order' => 'YMD', + ], + ]) + ->save(); + \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions(); + + // Display creation form. + $this->drupalGet('entity_test/add'); + + // Assert that Hour and Minute Elements do not appear on Date Only. + $this->assertNoFieldByXPath("//*[@id=\"edit-$field_name-0-value-hour\"]", NULL, 'Hour element not found on Date Only.'); + $this->assertNoFieldByXPath("//*[@id=\"edit-$field_name-0-value-minute\"]", NULL, 'Minute element not found on Date Only.'); + $this->assertNoFieldByXPath("//*[@id=\"edit-$field_name-0-end-value-hour\"]", NULL, 'Hour element not found on Date Only.'); + $this->assertNoFieldByXPath("//*[@id=\"edit-$field_name-0-end-value-minute\"]", NULL, 'Minute element not found on Date Only.'); + + // Go to the form display page to assert that increment option does not + // appear on Date Only. + $fieldEditUrl = 'entity_test/structure/entity_test/form-display'; + $this->drupalGet($fieldEditUrl); + + // Click on the widget settings button to open the widget settings form. + $this->drupalPostAjaxForm(NULL, [], $field_name . "_settings_edit"); + $xpathIncr = "//select[starts-with(@id, \"edit-fields-$field_name-settings-edit-form-settings-increment\")]"; + $this->assertNoFieldByXPath($xpathIncr, NULL, 'Increment element not found for Date Only.'); + + // Change the field to a datetime field. + $this->fieldStorage->setSetting('datetime_type', DateRangeItem::DATETIME_TYPE_DATETIME); + $this->fieldStorage->save(); + + // Change the widget to a datelist widget. + entity_get_form_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'default') + ->setComponent($field_name, [ + 'type' => 'daterange_datelist', + 'settings' => [ + 'increment' => 1, + 'date_order' => 'YMD', + 'time_type' => '12', + ], + ]) + ->save(); + \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions(); + + // Go to the form display page to assert that increment option does appear + // on Date Time. + $fieldEditUrl = 'entity_test/structure/entity_test/form-display'; + $this->drupalGet($fieldEditUrl); + + // Click on the widget settings button to open the widget settings form. + $this->drupalPostAjaxForm(NULL, [], $field_name . "_settings_edit"); + $this->assertFieldByXPath($xpathIncr, NULL, 'Increment element found for Date and time.'); + + // Display creation form. + $this->drupalGet('entity_test/add'); + + foreach (['value', 'end-value'] as $column) { + foreach (['year', 'month', 'day', 'hour', 'minute', 'ampm'] as $element) { + $this->assertFieldByXPath("//*[@id=\"edit-$field_name-0-$column-$element\"]", NULL, $element . ' element found.'); + $this->assertOptionSelected("edit-$field_name-0-$column-$element", '', 'No ' . $element . ' selected.'); + } + } + + // Submit a valid date and ensure it is accepted. + $start_date_value = ['year' => 2012, 'month' => 12, 'day' => 31, 'hour' => 5, 'minute' => 15]; + $end_date_value = ['year' => 2013, 'month' => 1, 'day' => 15, 'hour' => 3, 'minute' => 30]; + + $edit = []; + // Add the ampm indicator since we are testing 12 hour time. + $start_date_value['ampm'] = 'am'; + $end_date_value['ampm'] = 'pm'; + foreach ($start_date_value as $part => $value) { + $edit["{$field_name}[0][value][$part]"] = $value; + } + foreach ($end_date_value as $part => $value) { + $edit["{$field_name}[0][end_value][$part]"] = $value; + } + + $this->drupalPostForm(NULL, $edit, t('Save')); + preg_match('|entity_test/manage/(\d+)|', $this->url, $match); + $id = $match[1]; + $this->assertText(t('entity_test @id has been created.', ['@id' => $id])); + + $this->assertOptionSelected("edit-$field_name-0-value-year", '2012', 'Correct year selected.'); + $this->assertOptionSelected("edit-$field_name-0-value-month", '12', 'Correct month selected.'); + $this->assertOptionSelected("edit-$field_name-0-value-day", '31', 'Correct day selected.'); + $this->assertOptionSelected("edit-$field_name-0-value-hour", '5', 'Correct hour selected.'); + $this->assertOptionSelected("edit-$field_name-0-value-minute", '15', 'Correct minute selected.'); + $this->assertOptionSelected("edit-$field_name-0-value-ampm", 'am', 'Correct ampm selected.'); + + $this->assertOptionSelected("edit-$field_name-0-end-value-year", '2013', 'Correct year selected.'); + $this->assertOptionSelected("edit-$field_name-0-end-value-month", '1', 'Correct month selected.'); + $this->assertOptionSelected("edit-$field_name-0-end-value-day", '15', 'Correct day selected.'); + $this->assertOptionSelected("edit-$field_name-0-end-value-hour", '3', 'Correct hour selected.'); + $this->assertOptionSelected("edit-$field_name-0-end-value-minute", '30', 'Correct minute selected.'); + $this->assertOptionSelected("edit-$field_name-0-end-value-ampm", 'pm', 'Correct ampm selected.'); + + // Test the widget using increment other than 1 and 24 hour mode. + entity_get_form_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'default') + ->setComponent($field_name, [ + 'type' => 'daterange_datelist', + 'settings' => [ + 'increment' => 15, + 'date_order' => 'YMD', + 'time_type' => '24', + ], + ]) + ->save(); + \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions(); + + // Display creation form. + $this->drupalGet('entity_test/add'); + + // Other elements are unaffected by the changed settings. + $this->assertFieldByXPath("//*[@id=\"edit-$field_name-0-value-hour\"]", NULL, 'Hour element found.'); + $this->assertOptionSelected("edit-$field_name-0-value-hour", '', 'No hour selected.'); + $this->assertNoFieldByXPath("//*[@id=\"edit-$field_name-0-value-ampm\"]", NULL, 'AMPM element not found.'); + $this->assertFieldByXPath("//*[@id=\"edit-$field_name-0-end-value-hour\"]", NULL, 'Hour element found.'); + $this->assertOptionSelected("edit-$field_name-0-end-value-hour", '', 'No hour selected.'); + $this->assertNoFieldByXPath("//*[@id=\"edit-$field_name-0-end-value-ampm\"]", NULL, 'AMPM element not found.'); + + // Submit a valid date and ensure it is accepted. + $start_date_value = ['year' => 2012, 'month' => 12, 'day' => 31, 'hour' => 17, 'minute' => 15]; + $end_date_value = ['year' => 2013, 'month' => 1, 'day' => 15, 'hour' => 3, 'minute' => 30]; + + $edit = []; + foreach ($start_date_value as $part => $value) { + $edit["{$field_name}[0][value][$part]"] = $value; + } + foreach ($end_date_value as $part => $value) { + $edit["{$field_name}[0][end_value][$part]"] = $value; + } + + $this->drupalPostForm(NULL, $edit, t('Save')); + preg_match('|entity_test/manage/(\d+)|', $this->url, $match); + $id = $match[1]; + $this->assertText(t('entity_test @id has been created.', ['@id' => $id])); + + $this->assertOptionSelected("edit-$field_name-0-value-year", '2012', 'Correct year selected.'); + $this->assertOptionSelected("edit-$field_name-0-value-month", '12', 'Correct month selected.'); + $this->assertOptionSelected("edit-$field_name-0-value-day", '31', 'Correct day selected.'); + $this->assertOptionSelected("edit-$field_name-0-value-hour", '17', 'Correct hour selected.'); + $this->assertOptionSelected("edit-$field_name-0-value-minute", '15', 'Correct minute selected.'); + + $this->assertOptionSelected("edit-$field_name-0-end-value-year", '2013', 'Correct year selected.'); + $this->assertOptionSelected("edit-$field_name-0-end-value-month", '1', 'Correct month selected.'); + $this->assertOptionSelected("edit-$field_name-0-end-value-day", '15', 'Correct day selected.'); + $this->assertOptionSelected("edit-$field_name-0-end-value-hour", '3', 'Correct hour selected.'); + $this->assertOptionSelected("edit-$field_name-0-end-value-minute", '30', 'Correct minute selected.'); + + // Test the widget for partial completion of fields. + entity_get_form_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'default') + ->setComponent($field_name, [ + 'type' => 'daterange_datelist', + 'settings' => [ + 'increment' => 1, + 'date_order' => 'YMD', + 'time_type' => '24', + ], + ]) + ->save(); + \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions(); + + // Test the widget for validation notifications. + foreach ($this->datelistDataProvider() as $data) { + list($start_date_value, $end_date_value, $expected) = $data; + + // Display creation form. + $this->drupalGet('entity_test/add'); + + // Submit a partial date and ensure and error message is provided. + $edit = []; + foreach ($start_date_value as $part => $value) { + $edit["{$field_name}[0][value][$part]"] = $value; + } + foreach ($end_date_value as $part => $value) { + $edit["{$field_name}[0][end_value][$part]"] = $value; + } + + $this->drupalPostForm(NULL, $edit, t('Save')); + $this->assertResponse(200); + foreach ($expected as $expected_text) { + $this->assertText(t($expected_text)); + } + } + + // Test the widget for complete input with zeros as part of selections. + $this->drupalGet('entity_test/add'); + + $start_date_value = ['year' => 2012, 'month' => 12, 'day' => 31, 'hour' => 0, 'minute' => 0]; + $end_date_value = ['year' => 2013, 'month' => 1, 'day' => 15, 'hour' => 3, 'minute' => 30]; + $edit = []; + foreach ($start_date_value as $part => $value) { + $edit["{$field_name}[0][value][$part]"] = $value; + } + foreach ($end_date_value as $part => $value) { + $edit["{$field_name}[0][end_value][$part]"] = $value; + } + + $this->drupalPostForm(NULL, $edit, t('Save')); + $this->assertResponse(200); + preg_match('|entity_test/manage/(\d+)|', $this->url, $match); + $id = $match[1]; + $this->assertText(t('entity_test @id has been created.', ['@id' => $id])); + + // Test the widget to ensure zeros are not deselected on validation. + $this->drupalGet('entity_test/add'); + + $start_date_value = ['year' => 2012, 'month' => 12, 'day' => 31, 'hour' => 0, 'minute' => 0]; + $end_date_value = ['year' => 2013, 'month' => 1, 'day' => 15, 'hour' => 3, 'minute' => 0]; + $edit = []; + foreach ($start_date_value as $part => $value) { + $edit["{$field_name}[0][value][$part]"] = $value; + } + foreach ($end_date_value as $part => $value) { + $edit["{$field_name}[0][end_value][$part]"] = $value; + } + + $this->drupalPostForm(NULL, $edit, t('Save')); + $this->assertResponse(200); + $this->assertOptionSelected("edit-$field_name-0-value-minute", '0', 'Correct minute selected.'); + $this->assertOptionSelected("edit-$field_name-0-end-value-minute", '0', 'Correct minute selected.'); + } + + /** + * The data provider for testing the validation of the datelist widget. + * + * @return array + * An array of datelist input permutations to test. + */ + protected function datelistDataProvider() { + return [ + // Year only selected, validation error on Month, Day, Hour, Minute. + [ + ['year' => 2012, 'month' => '', 'day' => '', 'hour' => '', 'minute' => ''], + ['year' => 2013, 'month' => '1', 'day' => '15', 'hour' => '3', 'minute' => '30'], [ + 'A value must be selected for month.', + 'A value must be selected for day.', + 'A value must be selected for hour.', + 'A value must be selected for minute.', + ], + ], + // Year and Month selected, validation error on Day, Hour, Minute. + [ + ['year' => 2012, 'month' => '12', 'day' => '', 'hour' => '', 'minute' => ''], + ['year' => 2013, 'month' => '1', 'day' => '15', 'hour' => '3', 'minute' => '30'], [ + 'A value must be selected for day.', + 'A value must be selected for hour.', + 'A value must be selected for minute.', + ], + ], + // Year, Month and Day selected, validation error on Hour, Minute. + [ + ['year' => 2012, 'month' => '12', 'day' => '31', 'hour' => '', 'minute' => ''], + ['year' => 2013, 'month' => '1', 'day' => '15', 'hour' => '3', 'minute' => '30'], [ + 'A value must be selected for hour.', + 'A value must be selected for minute.', + ], + ], + // Year, Month, Day and Hour selected, validation error on Minute only. + [ + ['year' => 2012, 'month' => '12', 'day' => '31', 'hour' => '0', 'minute' => ''], + ['year' => 2013, 'month' => '1', 'day' => '15', 'hour' => '3', 'minute' => '30'], [ + 'A value must be selected for minute.', + ], + ], + // Year selected, validation error on Month, Day, Hour, Minute. + [ + ['year' => 2012, 'month' => '12', 'day' => '31', 'hour' => '0', 'minute' => '0'], + ['year' => 2013, 'month' => '', 'day' => '', 'hour' => '', 'minute' => ''], [ + 'A value must be selected for month.', + 'A value must be selected for day.', + 'A value must be selected for hour.', + 'A value must be selected for minute.', + ], + ], + // Year and Month selected, validation error on Day, Hour, Minute. + [ + ['year' => 2012, 'month' => '12', 'day' => '31', 'hour' => '0', 'minute' => '0'], + ['year' => 2013, 'month' => '1', 'day' => '', 'hour' => '', 'minute' => ''], [ + 'A value must be selected for day.', + 'A value must be selected for hour.', + 'A value must be selected for minute.', + ], + ], + // Year, Month and Day selected, validation error on Hour, Minute. + [ + ['year' => 2012, 'month' => '12', 'day' => '31', 'hour' => '0', 'minute' => '0'], + ['year' => 2013, 'month' => '1', 'day' => '15', 'hour' => '', 'minute' => ''], [ + 'A value must be selected for hour.', + 'A value must be selected for minute.', + ], + ], + // Year, Month, Day and Hour selected, validation error on Minute only. + [ + ['year' => 2012, 'month' => '12', 'day' => '31', 'hour' => '0', 'minute' => '0'], + ['year' => 2013, 'month' => '1', 'day' => '15', 'hour' => '3', 'minute' => ''], [ + 'A value must be selected for minute.', + ], + ], + ]; + } + + /** + * Test default value functionality. + */ + public function testDefaultValue() { + // Create a test content type. + $this->drupalCreateContentType(['type' => 'date_content']); + + // Create a field storage with settings to validate. + $field_name = Unicode::strtolower($this->randomMachineName()); + $field_storage = FieldStorageConfig::create([ + 'field_name' => $field_name, + 'entity_type' => 'node', + 'type' => 'daterange', + 'settings' => ['datetime_type' => DateRangeItem::DATETIME_TYPE_DATE], + ]); + $field_storage->save(); + + $field = FieldConfig::create([ + 'field_storage' => $field_storage, + 'bundle' => 'date_content', + ]); + $field->save(); + + // Set now as default_value. + $field_edit = [ + 'default_value_input[default_date_type]' => 'now', + 'default_value_input[default_end_date_type]' => 'now', + ]; + $this->drupalPostForm('admin/structure/types/manage/date_content/fields/node.date_content.' . $field_name, $field_edit, t('Save settings')); + + // Check that default value is selected in default value form. + $this->drupalGet('admin/structure/types/manage/date_content/fields/node.date_content.' . $field_name); + $this->assertOptionSelected('edit-default-value-input-default-date-type', 'now', 'The default start value is selected in instance settings page'); + $this->assertFieldByName('default_value_input[default_date]', '', 'The relative start default value is empty in instance settings page'); + $this->assertOptionSelected('edit-default-value-input-default-end-date-type', 'now', 'The default end value is selected in instance settings page'); + $this->assertFieldByName('default_value_input[default_end_date]', '', 'The relative end default value is empty in instance settings page'); + + // Check if default_date has been stored successfully. + $config_entity = $this->config('field.field.node.date_content.' . $field_name)->get(); + $this->assertEqual($config_entity['default_value'][0], [ + 'default_date_type' => 'now', + 'default_date' => 'now', + 'default_end_date_type' => 'now', + 'default_end_date' => 'now', + ], 'Default value has been stored successfully'); + + // Clear field cache in order to avoid stale cache values. + \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions(); + + // Create a new node to check that datetime field default value is today. + $new_node = Node::create(['type' => 'date_content']); + $expected_date = new DrupalDateTime('now', DATETIME_STORAGE_TIMEZONE); + $this->assertEqual($new_node->get($field_name)->offsetGet(0)->value, $expected_date->format(DATETIME_DATE_STORAGE_FORMAT)); + $this->assertEqual($new_node->get($field_name)->offsetGet(0)->end_value, $expected_date->format(DATETIME_DATE_STORAGE_FORMAT)); + + // Set an invalid relative default_value to test validation. + $field_edit = [ + 'default_value_input[default_date_type]' => 'relative', + 'default_value_input[default_date]' => 'invalid date', + 'default_value_input[default_end_date_type]' => 'relative', + 'default_value_input[default_end_date]' => '+1 day', + ]; + $this->drupalPostForm('admin/structure/types/manage/date_content/fields/node.date_content.' . $field_name, $field_edit, t('Save settings')); + $this->assertText('The relative start date value entered is invalid.'); + + $field_edit = [ + 'default_value_input[default_date_type]' => 'relative', + 'default_value_input[default_date]' => '+1 day', + 'default_value_input[default_end_date_type]' => 'relative', + 'default_value_input[default_end_date]' => 'invalid date', + ]; + $this->drupalPostForm('admin/structure/types/manage/date_content/fields/node.date_content.' . $field_name, $field_edit, t('Save settings')); + $this->assertText('The relative end date value entered is invalid.'); + + // Set a relative default_value. + $field_edit = [ + 'default_value_input[default_date_type]' => 'relative', + 'default_value_input[default_date]' => '+45 days', + 'default_value_input[default_end_date_type]' => 'relative', + 'default_value_input[default_end_date]' => '+90 days', + ]; + $this->drupalPostForm('admin/structure/types/manage/date_content/fields/node.date_content.' . $field_name, $field_edit, t('Save settings')); + + // Check that default value is selected in default value form. + $this->drupalGet('admin/structure/types/manage/date_content/fields/node.date_content.' . $field_name); + $this->assertOptionSelected('edit-default-value-input-default-date-type', 'relative', 'The default start value is selected in instance settings page'); + $this->assertFieldByName('default_value_input[default_date]', '+45 days', 'The relative default start value is displayed in instance settings page'); + $this->assertOptionSelected('edit-default-value-input-default-end-date-type', 'relative', 'The default end value is selected in instance settings page'); + $this->assertFieldByName('default_value_input[default_end_date]', '+90 days', 'The relative default end value is displayed in instance settings page'); + + // Check if default_date has been stored successfully. + $config_entity = $this->config('field.field.node.date_content.' . $field_name)->get(); + $this->assertEqual($config_entity['default_value'][0], [ + 'default_date_type' => 'relative', + 'default_date' => '+45 days', + 'default_end_date_type' => 'relative', + 'default_end_date' => '+90 days', + ], 'Default value has been stored successfully'); + + // Clear field cache in order to avoid stale cache values. + \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions(); + + // Create a new node to check that datetime field default value is +90 days. + $new_node = Node::create(['type' => 'date_content']); + $expected_start_date = new DrupalDateTime('+45 days', DATETIME_STORAGE_TIMEZONE); + $expected_end_date = new DrupalDateTime('+90 days', DATETIME_STORAGE_TIMEZONE); + $this->assertEqual($new_node->get($field_name)->offsetGet(0)->value, $expected_start_date->format(DATETIME_DATE_STORAGE_FORMAT)); + $this->assertEqual($new_node->get($field_name)->offsetGet(0)->end_value, $expected_end_date->format(DATETIME_DATE_STORAGE_FORMAT)); + + // Remove default value. + $field_edit = [ + 'default_value_input[default_date_type]' => '', + 'default_value_input[default_end_date_type]' => '', + ]; + $this->drupalPostForm('admin/structure/types/manage/date_content/fields/node.date_content.' . $field_name, $field_edit, t('Save settings')); + + // Check that default value is selected in default value form. + $this->drupalGet('admin/structure/types/manage/date_content/fields/node.date_content.' . $field_name); + $this->assertOptionSelected('edit-default-value-input-default-date-type', '', 'The default start value is selected in instance settings page'); + $this->assertFieldByName('default_value_input[default_date]', '', 'The relative default start value is empty in instance settings page'); + $this->assertOptionSelected('edit-default-value-input-default-end-date-type', '', 'The default end value is selected in instance settings page'); + $this->assertFieldByName('default_value_input[default_end_date]', '', 'The relative default end value is empty in instance settings page'); + + // Check if default_date has been stored successfully. + $config_entity = $this->config('field.field.node.date_content.' . $field_name)->get(); + $this->assertTrue(empty($config_entity['default_value']), 'Empty default value has been stored successfully'); + + // Clear field cache in order to avoid stale cache values. + \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions(); + + // Create a new node to check that datetime field default value is not set. + $new_node = Node::create(['type' => 'date_content']); + $this->assertNull($new_node->get($field_name)->value, 'Default value is not set'); + + // Set now as default_value for start date only. + entity_get_form_display('node', 'date_content', 'default') + ->setComponent($field_name, [ + 'type' => 'datetime_default', + ]) + ->save(); + + $expected_date = new DrupalDateTime('now', DATETIME_STORAGE_TIMEZONE); + + $field_edit = [ + 'default_value_input[default_date_type]' => 'now', + 'default_value_input[default_end_date_type]' => '', + ]; + $this->drupalPostForm('admin/structure/types/manage/date_content/fields/node.date_content.' . $field_name, $field_edit, t('Save settings')); + + // Make sure only the start value is populated on node add page. + $this->drupalGet('node/add/date_content'); + $this->assertFieldByName("{$field_name}[0][value][date]", $expected_date->format(DATETIME_DATE_STORAGE_FORMAT), 'Start date element populated.'); + $this->assertFieldByName("{$field_name}[0][end_value][date]", '', 'End date element empty.'); + + // Set now as default_value for end date only. + $field_edit = [ + 'default_value_input[default_date_type]' => '', + 'default_value_input[default_end_date_type]' => 'now', + ]; + $this->drupalPostForm('admin/structure/types/manage/date_content/fields/node.date_content.' . $field_name, $field_edit, t('Save settings')); + + // Make sure only the start value is populated on node add page. + $this->drupalGet('node/add/date_content'); + $this->assertFieldByName("{$field_name}[0][value][date]", '', 'Start date element empty.'); + $this->assertFieldByName("{$field_name}[0][end_value][date]", $expected_date->format(DATETIME_DATE_STORAGE_FORMAT), 'End date element populated.'); + } + + /** + * Test that invalid values are caught and marked as invalid. + */ + public function testInvalidField() { + // Change the field to a datetime field. + $this->fieldStorage->setSetting('datetime_type', DateRangeItem::DATETIME_TYPE_DATETIME); + $this->fieldStorage->save(); + $field_name = $this->fieldStorage->getName(); + + $this->drupalGet('entity_test/add'); + $this->assertFieldByName("{$field_name}[0][value][date]", '', 'Start date element found.'); + $this->assertFieldByName("{$field_name}[0][value][time]", '', 'Start time element found.'); + $this->assertFieldByName("{$field_name}[0][end_value][date]", '', 'End date element found.'); + $this->assertFieldByName("{$field_name}[0][end_value][time]", '', 'End time element found.'); + + // Submit invalid start dates and ensure they is not accepted. + $date_value = ''; + $edit = [ + "{$field_name}[0][value][date]" => $date_value, + "{$field_name}[0][value][time]" => '12:00:00', + "{$field_name}[0][end_value][date]" => '2012-12-01', + "{$field_name}[0][end_value][time]" => '12:00:00', + ]; + $this->drupalPostForm(NULL, $edit, t('Save')); + $this->assertText('date is invalid', 'Empty start date value has been caught.'); + + $date_value = 'aaaa-12-01'; + $edit = [ + "{$field_name}[0][value][date]" => $date_value, + "{$field_name}[0][value][time]" => '00:00:00', + "{$field_name}[0][end_value][date]" => '2012-12-01', + "{$field_name}[0][end_value][time]" => '12:00:00', + ]; + $this->drupalPostForm(NULL, $edit, t('Save')); + $this->assertText('date is invalid', new FormattableMarkup('Invalid start year value %date has been caught.', ['%date' => $date_value])); + + $date_value = '2012-75-01'; + $edit = [ + "{$field_name}[0][value][date]" => $date_value, + "{$field_name}[0][value][time]" => '00:00:00', + "{$field_name}[0][end_value][date]" => '2012-12-01', + "{$field_name}[0][end_value][time]" => '12:00:00', + ]; + $this->drupalPostForm(NULL, $edit, t('Save')); + $this->assertText('date is invalid', new FormattableMarkup('Invalid start month value %date has been caught.', ['%date' => $date_value])); + + $date_value = '2012-12-99'; + $edit = [ + "{$field_name}[0][value][date]" => $date_value, + "{$field_name}[0][value][time]" => '00:00:00', + "{$field_name}[0][end_value][date]" => '2012-12-01', + "{$field_name}[0][end_value][time]" => '12:00:00', + ]; + $this->drupalPostForm(NULL, $edit, t('Save')); + $this->assertText('date is invalid', new FormattableMarkup('Invalid start day value %date has been caught.', ['%date' => $date_value])); + + // Submit invalid start times and ensure they is not accepted. + $time_value = ''; + $edit = [ + "{$field_name}[0][value][date]" => '2012-12-01', + "{$field_name}[0][value][time]" => $time_value, + "{$field_name}[0][end_value][date]" => '2012-12-01', + "{$field_name}[0][end_value][time]" => '12:00:00', + ]; + $this->drupalPostForm(NULL, $edit, t('Save')); + $this->assertText('date is invalid', 'Empty start time value has been caught.'); + + $time_value = '49:00:00'; + $edit = [ + "{$field_name}[0][value][date]" => '2012-12-01', + "{$field_name}[0][value][time]" => $time_value, + "{$field_name}[0][end_value][date]" => '2012-12-01', + "{$field_name}[0][end_value][time]" => '12:00:00', + ]; + $this->drupalPostForm(NULL, $edit, t('Save')); + $this->assertText('date is invalid', new FormattableMarkup('Invalid start hour value %time has been caught.', ['%time' => $time_value])); + + $time_value = '12:99:00'; + $edit = [ + "{$field_name}[0][value][date]" => '2012-12-01', + "{$field_name}[0][value][time]" => $time_value, + "{$field_name}[0][end_value][date]" => '2012-12-01', + "{$field_name}[0][end_value][time]" => '12:00:00', + ]; + $this->drupalPostForm(NULL, $edit, t('Save')); + $this->assertText('date is invalid', new FormattableMarkup('Invalid start minute value %time has been caught.', ['%time' => $time_value])); + + $time_value = '12:15:99'; + $edit = [ + "{$field_name}[0][value][date]" => '2012-12-01', + "{$field_name}[0][value][time]" => $time_value, + "{$field_name}[0][end_value][date]" => '2012-12-01', + "{$field_name}[0][end_value][time]" => '12:00:00', + ]; + $this->drupalPostForm(NULL, $edit, t('Save')); + $this->assertText('date is invalid', new FormattableMarkup('Invalid start second value %time has been caught.', ['%time' => $time_value])); + + // Submit invalid end dates and ensure they is not accepted. + $date_value = ''; + $edit = [ + "{$field_name}[0][value][date]" => '2012-12-01', + "{$field_name}[0][value][time]" => '12:00:00', + "{$field_name}[0][end_value][date]" => $date_value, + "{$field_name}[0][end_value][time]" => '12:00:00', + ]; + $this->drupalPostForm(NULL, $edit, t('Save')); + $this->assertText('date is invalid', 'Empty end date value has been caught.'); + + $date_value = 'aaaa-12-01'; + $edit = [ + "{$field_name}[0][value][date]" => '2012-12-01', + "{$field_name}[0][value][time]" => '12:00:00', + "{$field_name}[0][end_value][date]" => $date_value, + "{$field_name}[0][end_value][time]" => '00:00:00', + ]; + $this->drupalPostForm(NULL, $edit, t('Save')); + $this->assertText('date is invalid', new FormattableMarkup('Invalid end year value %date has been caught.', ['%date' => $date_value])); + + $date_value = '2012-75-01'; + $edit = [ + "{$field_name}[0][value][date]" => '2012-12-01', + "{$field_name}[0][value][time]" => '12:00:00', + "{$field_name}[0][end_value][date]" => $date_value, + "{$field_name}[0][end_value][time]" => '00:00:00', + ]; + $this->drupalPostForm(NULL, $edit, t('Save')); + $this->assertText('date is invalid', new FormattableMarkup('Invalid end month value %date has been caught.', ['%date' => $date_value])); + + $date_value = '2012-12-99'; + $edit = [ + "{$field_name}[0][value][date]" => '2012-12-01', + "{$field_name}[0][value][time]" => '12:00:00', + "{$field_name}[0][end_value][date]" => $date_value, + "{$field_name}[0][end_value][time]" => '00:00:00', + ]; + $this->drupalPostForm(NULL, $edit, t('Save')); + $this->assertText('date is invalid', new FormattableMarkup('Invalid end day value %date has been caught.', ['%date' => $date_value])); + + // Submit invalid start times and ensure they is not accepted. + $time_value = ''; + $edit = [ + "{$field_name}[0][value][date]" => '2012-12-01', + "{$field_name}[0][value][time]" => '12:00:00', + "{$field_name}[0][end_value][date]" => '2012-12-01', + "{$field_name}[0][end_value][time]" => $time_value, + ]; + $this->drupalPostForm(NULL, $edit, t('Save')); + $this->assertText('date is invalid', 'Empty end time value has been caught.'); + + $time_value = '49:00:00'; + $edit = [ + "{$field_name}[0][value][date]" => '2012-12-01', + "{$field_name}[0][value][time]" => '12:00:00', + "{$field_name}[0][end_value][date]" => '2012-12-01', + "{$field_name}[0][end_value][time]" => $time_value, + ]; + $this->drupalPostForm(NULL, $edit, t('Save')); + $this->assertText('date is invalid', new FormattableMarkup('Invalid end hour value %time has been caught.', ['%time' => $time_value])); + + $time_value = '12:99:00'; + $edit = [ + "{$field_name}[0][value][date]" => '2012-12-01', + "{$field_name}[0][value][time]" => '12:00:00', + "{$field_name}[0][end_value][date]" => '2012-12-01', + "{$field_name}[0][end_value][time]" => $time_value, + ]; + $this->drupalPostForm(NULL, $edit, t('Save')); + $this->assertText('date is invalid', new FormattableMarkup('Invalid end minute value %time has been caught.', ['%time' => $time_value])); + + $time_value = '12:15:99'; + $edit = [ + "{$field_name}[0][value][date]" => '2012-12-01', + "{$field_name}[0][value][time]" => '12:00:00', + "{$field_name}[0][end_value][date]" => '2012-12-01', + "{$field_name}[0][end_value][time]" => $time_value, + ]; + $this->drupalPostForm(NULL, $edit, t('Save')); + $this->assertText('date is invalid', new FormattableMarkup('Invalid end second value %time has been caught.', ['%time' => $time_value])); + + $edit = [ + "{$field_name}[0][value][date]" => '2012-12-01', + "{$field_name}[0][value][time]" => '12:00:00', + "{$field_name}[0][end_value][date]" => '2010-12-01', + "{$field_name}[0][end_value][time]" => '12:00:00', + ]; + $this->drupalPostForm(NULL, $edit, t('Save')); + $this->assertText(new FormattableMarkup('The @title end date cannot be before the start date', ['@title' => $field_name]), 'End date before start date has been caught.'); + + $edit = [ + "{$field_name}[0][value][date]" => '2012-12-01', + "{$field_name}[0][value][time]" => '12:00:00', + "{$field_name}[0][end_value][date]" => '2012-12-01', + "{$field_name}[0][end_value][time]" => '11:00:00', + ]; + $this->drupalPostForm(NULL, $edit, t('Save')); + $this->assertText(new FormattableMarkup('The @title end date cannot be before the start date', ['@title' => $field_name]), 'End time before start time has been caught.'); + } + + /** + * Tests that 'Date' field storage setting form is disabled if field has data. + */ + public function testDateStorageSettings() { + // Create a test content type. + $this->drupalCreateContentType(['type' => 'date_content']); + + // Create a field storage with settings to validate. + $field_name = Unicode::strtolower($this->randomMachineName()); + $field_storage = FieldStorageConfig::create([ + 'field_name' => $field_name, + 'entity_type' => 'node', + 'type' => 'daterange', + 'settings' => [ + 'datetime_type' => DateRangeItem::DATETIME_TYPE_DATE, + ], + ]); + $field_storage->save(); + $field = FieldConfig::create([ + 'field_storage' => $field_storage, + 'field_name' => $field_name, + 'bundle' => 'date_content', + ]); + $field->save(); + + entity_get_form_display('node', 'date_content', 'default') + ->setComponent($field_name, [ + 'type' => 'datetime_default', + ]) + ->save(); + $edit = [ + 'title[0][value]' => $this->randomString(), + 'body[0][value]' => $this->randomString(), + $field_name . '[0][value][date]' => '2016-04-01', + $field_name . '[0][end_value][date]' => '2016-04-02', + ]; + $this->drupalPostForm('node/add/date_content', $edit, t('Save')); + $this->drupalGet('admin/structure/types/manage/date_content/fields/node.date_content.' . $field_name . '/storage'); + $result = $this->xpath("//*[@id='edit-settings-datetime-type' and contains(@disabled, 'disabled')]"); + $this->assertEqual(count($result), 1, "Changing datetime setting is disabled."); + $this->assertText('There is data for this field in the database. The field settings can no longer be changed.'); + } + + /** + * Renders a entity_test and sets the output in the internal browser. + * + * @param int $id + * The entity_test ID to render. + * @param string $view_mode + * (optional) The view mode to use for rendering. Defaults to 'full'. + * @param bool $reset + * (optional) Whether to reset the entity_test controller cache. Defaults to + * TRUE to simplify testing. + */ + protected function renderTestEntity($id, $view_mode = 'full', $reset = TRUE) { + if ($reset) { + \Drupal::service('entity_type.manager')->getStorage('entity_test')->resetCache([$id]); + } + $entity = EntityTest::load($id); + $display = EntityViewDisplay::collectRenderDisplay($entity, $view_mode); + $build = $display->build($entity); + $output = \Drupal::service('renderer')->renderRoot($build); + $this->setRawContent($output); + $this->verbose($output); + } + + /** + * Sets the site timezone to a given timezone. + * + * @param string $timezone + * The timezone identifier to set. + */ + protected function setSiteTimezone($timezone) { + // Set an explicit site timezone, and disallow per-user timezones. + $this->config('system.date') + ->set('timezone.user.configurable', 0) + // A timezone with an offset greater than UTC+12 is used. + ->set('timezone.default', $timezone) + ->save(); + } + +}