diff --git a/core/modules/datetime/config/schema/datetime.schema.yml b/core/modules/datetime/config/schema/datetime.schema.yml index 406a2fd..a5c50b1 100644 --- a/core/modules/datetime/config/schema/datetime.schema.yml +++ b/core/modules/datetime/config/schema/datetime.schema.yml @@ -1,5 +1,7 @@ # Schema for the configuration files of the Datetime module. +# Datetime file type. + field.storage_settings.datetime: type: mapping label: 'Datetime settings' @@ -83,3 +85,84 @@ field.widget.settings.datetime_datelist: field.widget.settings.datetime_default: type: mapping label: 'Datetime default display format settings' + +# Daterange field type. + +field.storage_settings.daterange: + type: mapping + label: 'Daterange settings' + mapping: + daterange_type: + type: string + label: 'Date type' + +field.field_settings.daterange: + type: mapping + label: 'Daterange settings' + +field.value.daterange: + type: mapping + label: 'Default value' + mapping: + default_start_date_type: + type: string + label: 'Default start date type' + default_start_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_base: + type: mapping + mapping: + separator: + type: string + label: 'Separator' + timezone_override: + type: string + label: 'Time zone override' + +field.formatter.settings.daterange_default: + type: field.formatter.settings.daterange_base + label: 'Daterange default display format settings' + mapping: + format_type: + type: string + label: 'Date format' + +field.formatter.settings.daterange_plain: + type: field.formatter.settings.daterange_base + label: 'Daterange plain display format settings' + +field.formatter.settings.daterange_custom: + type: field.formatter.settings.daterange_base + label: 'Daterange custom display format settings' + mapping: + date_format: + type: string + label: 'Date/time format' + translatable: true + translation context: 'PHP date format' + +field.widget.settings.daterange_datelist: + type: mapping + label: 'Daterange 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: 'Daterange default display format settings' diff --git a/core/modules/datetime/src/DateTimeComputed.php b/core/modules/datetime/src/DateTimeComputed.php index 6939994..49f0512 100644 --- a/core/modules/datetime/src/DateTimeComputed.php +++ b/core/modules/datetime/src/DateTimeComputed.php @@ -40,10 +40,12 @@ public function getValue($langcode = NULL) { return $this->date; } + /** @var \Drupal\Core\Field\FieldItemBase $item */ $item = $this->getParent(); $value = $item->{($this->definition->getSetting('date source'))}; + $type = $item->getFieldDefinition()->getType(); - $storage_format = $item->getFieldDefinition()->getSetting('datetime_type') == 'date' ? DATETIME_DATE_STORAGE_FORMAT : DATETIME_DATETIME_STORAGE_FORMAT; + $storage_format = $item->getFieldDefinition()->getSetting($type . '_type') == 'date' ? DATETIME_DATE_STORAGE_FORMAT : DATETIME_DATETIME_STORAGE_FORMAT; try { $date = DrupalDateTime::createFromFormat($storage_format, $value, DATETIME_STORAGE_TIMEZONE); if ($date instanceof DrupalDateTime && !$date->hasErrors()) { diff --git a/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateRangeCustomFormatter.php b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateRangeCustomFormatter.php new file mode 100644 index 0000000..c45c24d --- /dev/null +++ b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateRangeCustomFormatter.php @@ -0,0 +1,116 @@ + DATETIME_DATETIME_STORAGE_FORMAT, + ] + parent::defaultSettings(); + } + + /** + * {@inheritdoc} + */ + public function viewElements(FieldItemListInterface $items, $langcode) { + $elements = []; + $separator = $this->getSetting('separator'); + + foreach ($items as $delta => $item) { + if ($item->start_date && $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 ($this->getFieldSetting('daterange_type') == DateRangeItem::DATERANGE_TYPE_DATE) { + // A date without time will pick up the current time, use the default. + datetime_date_default_time($start_date); + datetime_date_default_time($end_date); + } + + $this->setTimeZone($start_date); + $this->setTimeZone($end_date); + + $start = $this->formatDate($start_date); + $end = $this->formatDate($end_date); + if ($start !== $end) { + $output = $this->formatDate($start_date) . ' ' . $separator . ' ' . $this->formatDate($end_date); + } + else { + $output = $start; + } + + $elements[$delta] = [ + '#cache' => [ + 'contexts' => [ + 'timezone', + ], + ], + '#plain_text' => $output, + ]; + } + } + + return $elements; + } + + /** + * {@inheritdoc} + */ + protected function formatDate($date) { + $format = $this->getSetting('date_format'); + $timezone = $this->getSetting('timezone_override') ?: $date->getTimezone()->getName(); + return $this->dateFormatter->format($date->getTimestamp(), 'custom', $format, $timezone != '' ? $timezone : NULL); + } + + /** + * {@inheritdoc} + */ + public function settingsForm(array $form, FormStateInterface $form_state) { + $form = parent::settingsForm($form, $form_state); + + $form['date_format'] = [ + '#type' => 'textfield', + '#title' => $this->t('Date/time format'), + '#description' => $this->t('See the documentation for PHP date formats.'), + '#default_value' => $this->getSetting('date_format'), + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function settingsSummary() { + $summary = parent::settingsSummary(); + + $date = DrupalDateTime::createFromTimestamp($this->requestStack->getCurrentRequest()->server->get('REQUEST_TIME')); + $this->setTimeZone($date); + $summary[] = $this->t('Format: @display', ['@display' => $this->formatDate($date)]); + + return $summary; + } + +} diff --git a/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateRangeDefaultFormatter.php b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateRangeDefaultFormatter.php new file mode 100644 index 0000000..1469bd4 --- /dev/null +++ b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateRangeDefaultFormatter.php @@ -0,0 +1,160 @@ + 'medium', + ] + parent::defaultSettings(); + } + + /** + * {@inheritdoc} + */ + public function viewElements(FieldItemListInterface $items, $langcode) { + $elements = []; + $separator = $this->getSetting('separator'); + + foreach ($items as $delta => $item) { + if ($item->start_date && $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 ($this->getFieldSetting('daterange_type') == DateRangeItem::DATERANGE_TYPE_DATE) { + // A date without time will pick up the current time, use the default. + datetime_date_default_time($start_date); + datetime_date_default_time($end_date); + } + + // Create the ISO dates in Universal Time. + $start_iso_date = $start_date->format("Y-m-d\TH:i:s") . 'Z'; + $end_iso_date = $end_date->format("Y-m-d\TH:i:s") . 'Z'; + + $this->setTimeZone($start_date); + $this->setTimeZone($end_date); + + // Display the dates using theme datetime. + $elements[$delta] = [ + '#cache' => [ + 'contexts' => [ + 'timezone', + ], + ], + ]; + + $start = $this->formatDate($start_date); + $end = $this->formatDate($end_date); + + if ($start !== $end) { + $elements[$delta][] = [ + '#theme' => 'time', + '#text' => $start, + '#html' => FALSE, + '#attributes' => [ + 'datetime' => $start_iso_date, + ] + ]; + $elements[$delta][] = ['#plain_text' => ' ' . $separator . ' ']; + $elements[$delta][] = [ + '#theme' => 'time', + '#text' => $end, + '#html' => FALSE, + '#attributes' => [ + 'datetime' => $end_iso_date, + ] + ]; + } + else { + $elements[$delta][] = [ + '#theme' => 'time', + '#text' => $start, + '#html' => FALSE, + '#attributes' => [ + 'datetime' => $start_iso_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} + */ + protected function formatDate($date) { + $format_type = $this->getSetting('format_type'); + $timezone = $this->getSetting('timezone_override') ?: $date->getTimezone()->getName(); + return $this->dateFormatter->format($date->getTimestamp(), $format_type, '', $timezone != '' ? $timezone : NULL); + } + + /** + * {@inheritdoc} + */ + public function settingsForm(array $form, FormStateInterface $form_state) { + $form = parent::settingsForm($form, $form_state); + + $format_types = $this->dateFormatStorage->loadMultiple(); + $options = []; + + foreach ($format_types as $type => $type_info) { + $format = $this->dateFormatter->format(REQUEST_TIME, $type); + $options[$type] = $type_info->label() . ' (' . $format . ')'; + } + + $form['format_type'] = [ + '#type' => 'select', + '#title' => t('Date format'), + '#description' => $this->t('Choose a format for displaying the dates. Be sure to set a format appropriate for the field, i.e. omitting time for a field that only has a date.'), + '#options' => $options, + '#default_value' => $this->getSetting('format_type'), + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function settingsSummary() { + $summary = parent::settingsSummary(); + + $date = DrupalDateTime::createFromTimestamp($this->requestStack->getCurrentRequest()->server->get('REQUEST_TIME')); + $this->setTimeZone($date); + $summary[] = $this->t('Format: @display', ['@display' => $this->formatDate($date)]); + + return $summary; + } + +} diff --git a/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateRangeFormatterBase.php b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateRangeFormatterBase.php new file mode 100644 index 0000000..f25ecf1 --- /dev/null +++ b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateRangeFormatterBase.php @@ -0,0 +1,194 @@ +dateFormatter = $date_formatter; + $this->dateFormatStorage = $date_format_storage; + $this->requestStack = $request_stack; + } + + /** + * {@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['label'], + $configuration['view_mode'], + $configuration['third_party_settings'], + $container->get('date.formatter'), + $container->get('entity.manager')->getStorage('date_format'), + $container->get('request_stack') + ); + } + + /** + * {@inheritdoc} + */ + public static function defaultSettings() { + return [ + 'separator' => '-', + 'timezone_override' => '', + ] + parent::defaultSettings(); + } + + /** + * {@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'), + ]; + + $form['timezone_override'] = [ + '#type' => 'select', + '#title' => $this->t('Time zone override'), + '#description' => $this->t('The time zone selected here will always be used'), + '#options' => system_time_zones(TRUE), + '#default_value' => $this->getSetting('timezone_override'), + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function settingsSummary() { + $summary = parent::settingsSummary(); + + if ($separator = $this->getSetting('separator')) { + $summary[] = $this->t('Separator: %separator', ['%separator' => $separator]); + } + + if ($override = $this->getSetting('timezone_override')) { + $summary[] = $this->t('Time zone: @timezone', ['@timezone' => $override]); + } + + return $summary; + } + + /** + * Creates a formatted date as a string. + * + * @param \Drupal\Core\Datetime\DrupalDateTime $date + * The date. + * + * @return string + * A formatted date range string using the chosen format. + */ + abstract protected function formatDate($date); + + /** + * Sets the proper time zone on a DrupalDateTime object for the current user. + * + * A DrupalDateTime object loaded from the database will have the UTC time + * zone applied to it. This method will apply the time zone for the current + * user, based on system and user settings. + * + * @see drupal_get_user_timezone() + * + * @param \Drupal\Core\Datetime\DrupalDateTime $date + * A DrupalDateTime object. + */ + protected function setTimeZone(DrupalDateTime $date) { + if ($this->getFieldSetting('datetime_type') === DateRangeItem::DATERANGE_TYPE_DATE) { + // A date without time has no timezone conversion. + $timezone = DATETIME_STORAGE_TIMEZONE; + } + else { + $timezone = drupal_get_user_timezone(); + } + $date->setTimeZone(timezone_open($timezone)); + } + + /** + * Gets a settings array suitable for DrupalDateTime::format(). + * + * @return array + * The settings array that can be passed to DrupalDateTime::format(). + */ + protected function getFormatSettings() { + $settings = []; + + if ($this->getSetting('timezone_override') != '') { + $settings['timezone'] = $this->getSetting('timezone_override'); + } + + return $settings; + } + +} diff --git a/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateRangePlainFormatter.php b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateRangePlainFormatter.php new file mode 100644 index 0000000..a2a1b13 --- /dev/null +++ b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateRangePlainFormatter.php @@ -0,0 +1,76 @@ +getSetting('separator'); + + foreach ($items as $delta => $item) { + if ($item->start_date && $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 ($this->getFieldSetting('daterange_type') == DateRangeItem::DATERANGE_TYPE_DATE) { + // A date without time will pick up the current time, use the default. + datetime_date_default_time($start_date); + datetime_date_default_time($end_date); + } + + $this->setTimeZone($start_date); + $this->setTimeZone($end_date); + + $start = $this->formatDate($start_date); + $end = $this->formatDate($end_date); + if ($start !== $end) { + $output = $this->formatDate($start_date) . ' ' . $separator . ' ' . $this->formatDate($end_date); + } + else { + $output = $start; + } + + $elements[$delta] = [ + '#cache' => [ + 'contexts' => [ + 'timezone', + ], + ], + '#plain_text' => $output, + ]; + } + } + + return $elements; + } + + /** + * {@inheritdoc} + */ + protected function formatDate($date) { + $format = $this->getFieldSetting('daterange_type') == DateRangeItem::DATERANGE_TYPE_DATE ? DATETIME_DATE_STORAGE_FORMAT : DATETIME_DATETIME_STORAGE_FORMAT; + $timezone = $this->getSetting('timezone_override') ?: $date->getTimezone()->getName(); + return $this->dateFormatter->format($date->getTimestamp(), 'custom', $format, $timezone != '' ? $timezone : NULL); + } + +} diff --git a/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimeCustomFormatter.php b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimeCustomFormatter.php index 678ddc8..c4abd2d 100644 --- a/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimeCustomFormatter.php +++ b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimeCustomFormatter.php @@ -15,7 +15,7 @@ * field_types = { * "datetime" * } - *) + * ) */ class DateTimeCustomFormatter extends DateTimeFormatterBase { diff --git a/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimePlainFormatter.php b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimePlainFormatter.php index 683d75c..a74810a 100644 --- a/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimePlainFormatter.php +++ b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimePlainFormatter.php @@ -14,7 +14,7 @@ * field_types = { * "datetime" * } - *) + * ) */ class DateTimePlainFormatter extends DateTimeFormatterBase { diff --git a/core/modules/datetime/src/Plugin/Field/FieldType/DateRangeFieldItemList.php b/core/modules/datetime/src/Plugin/Field/FieldType/DateRangeFieldItemList.php new file mode 100644 index 0000000..d6dbf7d --- /dev/null +++ b/core/modules/datetime/src/Plugin/Field/FieldType/DateRangeFieldItemList.php @@ -0,0 +1,149 @@ +getFieldDefinition()->getDefaultValueCallback())) { + $default_value = $this->getFieldDefinition()->getDefaultValueLiteral(); + + $element = [ + '#parents' => ['default_value_input'], + 'default_start_date_type' => [ + '#type' => 'select', + '#title' => $this->t('Default start date'), + '#description' => $this->t('Set a default value for the start date.'), + '#default_value' => isset($default_value[0]['default_start_date_type']) ? $default_value[0]['default_start_date_type'] : '', + '#options' => [ + static::DEFAULT_VALUE_NOW => $this->t('Current date'), + static::DEFAULT_VALUE_CUSTOM => $this->t('Relative date'), + ], + '#empty_value' => '', + ], + 'default_start_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_start_date_type']) && $default_value[0]['default_start_date_type'] == static::DEFAULT_VALUE_CUSTOM) ? $default_value[0]['default_start_date'] : '', + '#states' => [ + 'visible' => [ + ':input[id="edit-default-value-input-default-start-date-type"]' => ['value' => static::DEFAULT_VALUE_CUSTOM], + ], + ], + ], + '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' => '', + ], + '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_start_date_type']) == static::DEFAULT_VALUE_CUSTOM) { + $is_strtotime = @strtotime($form_state->getValue(['default_value_input', 'default_start_date'])); + if (!$is_strtotime) { + $form_state->setErrorByName('default_value_input][default_start_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_start_date_type']) && $form_state->getValue(['default_value_input', 'default_end_date_type'])) { + if ($form_state->getValue(['default_value_input', 'default_start_date_type']) == static::DEFAULT_VALUE_NOW) { + $form_state->setValueForElement($element['default_start_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) { + $default_value = parent::processDefaultValue($default_value, $entity, $definition); + + if (isset($default_value[0]['default_start_date_type']) && isset($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. + $storage_format = $definition->getSetting('daterange_type') == DateRangeItem::DATERANGE_TYPE_DATE ? DATETIME_DATE_STORAGE_FORMAT : DATETIME_DATETIME_STORAGE_FORMAT; + $start_date = new DrupalDateTime($default_value[0]['default_start_date'], DATETIME_STORAGE_TIMEZONE); + $start_value = $start_date->format($storage_format); + $end_date = new DrupalDateTime($default_value[0]['default_end_date'], DATETIME_STORAGE_TIMEZONE); + $end_value = $end_date->format($storage_format); + // 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. + $default_value = [ + [ + 'value' => $start_value, + 'start_date' => $start_date, + 'value2' => $end_value, + 'end_date' => $end_date, + ], + ]; + } + return $default_value; + } + +} diff --git a/core/modules/datetime/src/Plugin/Field/FieldType/DateRangeItem.php b/core/modules/datetime/src/Plugin/Field/FieldType/DateRangeItem.php new file mode 100644 index 0000000..78e2cb8 --- /dev/null +++ b/core/modules/datetime/src/Plugin/Field/FieldType/DateRangeItem.php @@ -0,0 +1,172 @@ + 'datetime', + ] + parent::defaultStorageSettings(); + } + + /** + * Value for the 'daterange_type' setting: store only a date. + */ + const DATERANGE_TYPE_DATE = 'date'; + + /** + * Value for the 'daterange_type' setting: store a date and time. + */ + const DATERANGE_TYPE_DATETIME = 'datetime'; + + /** + * Value for the 'daterange_type' setting: store a date and time. + */ + const DATERANGE_TYPE_ALLDAY = 'allday'; + + /** + * {@inheritdoc} + */ + public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) { + $properties['value'] = DataDefinition::create('datetime_iso8601') + ->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('\Drupal\datetime\DateTimeComputed') + ->setSetting('date source', 'value'); + + $properties['value2'] = 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('\Drupal\datetime\DateTimeComputed') + ->setSetting('date source', 'value2'); + + return $properties; + } + + /** + * {@inheritdoc} + */ + public static function schema(FieldStorageDefinitionInterface $field_definition) { + return [ + 'columns' => [ + 'value' => [ + 'description' => 'The start date value.', + 'type' => 'varchar', + 'length' => 20, + ], + 'value2' => [ + 'description' => 'The end date value.', + 'type' => 'varchar', + 'length' => 20, + ], + ], + 'indexes' => [ + 'value' => ['value'], + 'value2' => ['value2'], + ], + ]; + } + + /** + * {@inheritdoc} + */ + public function storageSettingsForm(array &$form, FormStateInterface $form_state, $has_data) { + $element = []; + + $element['daterange_type'] = [ + '#type' => 'select', + '#title' => $this->t('Date type'), + '#description' => $this->t('Choose the type of date to create.'), + '#default_value' => $this->getSetting('daterange_type'), + '#options' => [ + static::DATERANGE_TYPE_DATETIME => t('Date and time'), + static::DATERANGE_TYPE_DATE => t('Date only'), + static::DATERANGE_TYPE_ALLDAY => t('All Day'), + ], + '#disabled' => $has_data, + ]; + + return $element; + } + + /** + * {@inheritdoc} + */ + public static function generateSampleValue(FieldDefinitionInterface $field_definition) { + $type = $field_definition->getSetting('daterange_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::DATERANGE_TYPE_DATE) { + $values['value'] = gmdate(DATETIME_DATE_STORAGE_FORMAT, $start); + $values['value2'] = gmdate(DATETIME_DATE_STORAGE_FORMAT, $end); + } + elseif ($type == static::DATERANGE_TYPE_ALLDAY) { + $values['value'] = gmdate(DATETIME_DATE_STORAGE_FORMAT, $start); + $values['value2'] = gmdate(DATETIME_DATE_STORAGE_FORMAT, $end); + } + else { + $values['value'] = gmdate(DATETIME_DATETIME_STORAGE_FORMAT, $start); + $values['value2'] = gmdate(DATETIME_DATETIME_STORAGE_FORMAT, $end); + } + return $values; + } + + /** + * {@inheritdoc} + */ + public function isEmpty() { + $start_value = $this->get('value')->getValue(); + $end_value = $this->get('value2')->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 == 'value2') { + $this->end_date = NULL; + } + parent::onChange($property_name, $notify); + } + +} diff --git a/core/modules/datetime/src/Plugin/Field/FieldWidget/DateRangeDatelistWidget.php b/core/modules/datetime/src/Plugin/Field/FieldWidget/DateRangeDatelistWidget.php new file mode 100644 index 0000000..f92955c --- /dev/null +++ b/core/modules/datetime/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('daterange_type') == DateRangeItem::DATERANGE_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 = array('year', 'month', 'day'); + break; + + case 'MDY': + $date_part_order = array('month', 'day', 'year'); + break; + + case 'DMY': + $date_part_order = array('day', 'month', 'year'); + break; + } + switch ($time_type) { + case '24': + $date_part_order = array_merge($date_part_order, array('hour', 'minute')); + break; + + case '12': + $date_part_order = array_merge($date_part_order, array('hour', 'minute', 'ampm')); + break; + + case 'none': + break; + } + + $element['value'] = [ + '#type' => 'datelist', + '#date_increment' => $increment, + '#date_part_order' => $date_part_order, + ] + $element['value']; + + $element['value2'] = [ + '#type' => 'datelist', + '#date_increment' => $increment, + '#date_part_order' => $date_part_order, + ] + $element['value2']; + + return $element; + } + + /** + * {@inheritdoc} + */ + function settingsForm(array $form, FormStateInterface $form_state) { + $element = parent::settingsForm($form, $form_state); + + $element['date_order'] = array( + '#type' => 'select', + '#title' => t('Date part order'), + '#default_value' => $this->getSetting('date_order'), + '#options' => array('MDY' => t('Month/Day/Year'), 'DMY' => t('Day/Month/Year'), 'YMD' => t('Year/Month/Day')), + ); + + if ($this->getFieldSetting('daterange_type') == DateRangeItem::DATERANGE_TYPE_DATETIME) { + $element['time_type'] = array( + '#type' => 'select', + '#title' => t('Time type'), + '#default_value' => $this->getSetting('time_type'), + '#options' => array('24' => t('24 hour time'), '12' => t('12 hour time')), + ); + + $element['increment'] = [ + '#type' => 'select', + '#title' => t('Time increments'), + '#default_value' => $this->getSetting('increment'), + '#options' => [ + 1 => t('1 minute'), + 5 => t('5 minute'), + 10 => t('10 minute'), + 15 => t('15 minute'), + 30 => t('30 minute'), + ], + ]; + } + else { + $element['time_type'] = array( + '#type' => 'hidden', + '#value' => 'none', + ); + + $element['increment'] = [ + '#type' => 'hidden', + '#value' => $this->getSetting('increment'), + ]; + } + + return $element; + } + + /** + * {@inheritdoc} + */ + public function settingsSummary() { + $summary = array(); + + $summary[] = t('Date part order: @order', array('@order' => $this->getSetting('date_order'))); + if ($this->getFieldSetting('daterange_type') == DateRangeItem::DATERANGE_TYPE_DATETIME) { + $summary[] = t('Time type: @time_type', array('@time_type' => $this->getSetting('time_type'))); + $summary[] = t('Time increments: @increment', array('@increment' => $this->getSetting('increment'))); + } + + return $summary; + } + +} diff --git a/core/modules/datetime/src/Plugin/Field/FieldWidget/DateRangeDefaultWidget.php b/core/modules/datetime/src/Plugin/Field/FieldWidget/DateRangeDefaultWidget.php new file mode 100644 index 0000000..24af1ce --- /dev/null +++ b/core/modules/datetime/src/Plugin/Field/FieldWidget/DateRangeDefaultWidget.php @@ -0,0 +1,107 @@ +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.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('daterange_type')) { + case DateRangeItem::DATERANGE_TYPE_DATE: + $date_type = 'date'; + $time_type = 'none'; + $date_format = $this->dateStorage->load('html_date')->getPattern(); + $time_format = ''; + break; + + case DateRangeItem::DATERANGE_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'] += array( + '#date_date_format' => $date_format, + '#date_date_element' => $date_type, + '#date_date_callbacks' => array(), + '#date_time_format' => $time_format, + '#date_time_element' => $time_type, + '#date_time_callbacks' => array(), + ); + + $element['value2'] += array( + '#date_date_format' => $date_format, + '#date_date_element' => $date_type, + '#date_date_callbacks' => array(), + '#date_time_format' => $time_format, + '#date_time_element' => $time_type, + '#date_time_callbacks' => array(), + ); + + return $element; + } + +} diff --git a/core/modules/datetime/src/Plugin/Field/FieldWidget/DateRangeWidgetBase.php b/core/modules/datetime/src/Plugin/Field/FieldWidget/DateRangeWidgetBase.php new file mode 100644 index 0000000..49c5564 --- /dev/null +++ b/core/modules/datetime/src/Plugin/Field/FieldWidget/DateRangeWidgetBase.php @@ -0,0 +1,178 @@ + $this->t('Start'), + '#type' => 'datetime', + '#default_value' => NULL, + '#date_increment' => 1, + '#date_timezone' => drupal_get_user_timezone(), + '#required' => $element['#required'], + ); + + $element['value2'] = array( + '#title' => $this->t('End'), + '#type' => 'datetime', + '#default_value' => NULL, + '#date_increment' => 1, + '#date_timezone' => drupal_get_user_timezone(), + '#required' => $element['#required'], + ); + + if ($this->getFieldSetting('datetime_type') == DateRangeItem::DATERANGE_TYPE_DATE) { + // A date-only field should have no timezone conversion performed, so + // use the same timezone as for storage. + $element['value']['#date_timezone'] = DATETIME_STORAGE_TIMEZONE; + $element['value2']['#date_timezone'] = DATETIME_STORAGE_TIMEZONE; + } + + if ($items[$delta]->start_date) { + /** @var \Drupal\Core\Datetime\DrupalDateTime $start_date */ + $start_date = $items[$delta]->start_date; + // The date was created and verified during field_load(), so it is safe to + // use without further inspection. + if ($this->getFieldSetting('daterange_type') == DateRangeItem::DATERANGE_TYPE_DATE) { + // A date without time will pick up the current time, use the default + // time. + datetime_date_default_time($start_date); + } + $start_date->setTimezone(new \DateTimeZone($element['value']['#date_timezone'])); + $element['value']['#default_value'] = $start_date; + } + + if ($items[$delta]->end_date) { + /** @var \Drupal\Core\Datetime\DrupalDateTime $start_date */ + $end_date = $items[$delta]->end_date; + // The date was created and verified during field_load(), so it is safe to + // use without further inspection. + if ($this->getFieldSetting('daterange_type') == DateRangeItem::DATERANGE_TYPE_DATE) { + // A date without time will pick up the current time, use the default + // time. + datetime_date_default_time($end_date); + } + $end_date->setTimezone(new \DateTimeZone($element['value2']['#date_timezone'])); + $element['value2']['#default_value'] = $end_date; + } + + 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('daterange_type')) { + case DateRangeItem::DATERANGE_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::DATERANGE_TYPE_ALLDAY: + // All day field 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['value2']) && $item['value2'] instanceof DrupalDateTime) { + /** @var \Drupal\Core\Datetime\DrupalDateTime $end_date */ + $end_date = $item['value2']; + switch ($this->getFieldSetting('daterange_type')) { + case DateRangeItem::DATERANGE_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::DATERANGE_TYPE_ALLDAY: + // All day field 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['value2'] = $end_date->format($format); + } + } + + return $values; + } + + /** + * Validates that the start <= the end date. + */ + public function validateStartEnd($element, FormStateInterface $form_state) { + $start_date = $element['value']['#value']['object']; + $end_date = $element['value2']['#value']['object']; + + if ($start_date instanceof DrupalDateTime && $end_date instanceof DrupalDateTime) { + /** @var \Drupal\Core\Datetime\DrupalDateTime $start_date */ + /** @var \Drupal\Core\Datetime\DrupalDateTime $end_date */ + + 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('Start date should be equal to, or before, end date')); + } + } + } + } + +} diff --git a/core/modules/datetime/src/Tests/DateRangeFieldTest.php b/core/modules/datetime/src/Tests/DateRangeFieldTest.php new file mode 100644 index 0000000..7d1e97b --- /dev/null +++ b/core/modules/datetime/src/Tests/DateRangeFieldTest.php @@ -0,0 +1,1192 @@ +config('system.date') + ->set('timezone.user.configurable', 0) + ->set('timezone.default', 'Asia/Tokyo') + ->save(); + + $web_user = $this->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' => ['daterange_type' => DateRangeItem::DATERANGE_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(); + } + + /** + * Tests date field functionality. + */ + function testDateRangeField() { + $field_name = $this->fieldStorage->getName(); + + // Ensure field is set to a date-only field. + $this->fieldStorage->setSetting('daterange_type', DateRangeItem::DATERANGE_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][value2][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][value2][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'); + $value2 = '2013-06-06 00:00:00'; + $end_date = new DrupalDateTime($value2, 'UTC'); + + // The expected values will use the default time. + datetime_date_default_time($start_date); + datetime_date_default_time($end_date); + + // 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][value2][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 = format_date($start_date->getTimestamp(), 'long'); + $start_expected_iso = format_date($start_date->getTimestamp(), 'custom', 'Y-m-d\TH:i:s\Z', 'UTC'); + $end_expected = format_date($end_date->getTimestamp(), 'long'); + $end_expected_iso = format_date($end_date->getTimestamp(), 'custom', 'Y-m-d\TH:i:s\Z', 'UTC'); + $this->renderTestEntity($id); + $this->assertFieldByXPath('//time[@datetime="' . $start_expected_iso . '"]', $start_expected, SafeMarkup::format('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, SafeMarkup::format('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, SafeMarkup::format('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, SafeMarkup::format('Formatted date field using daterange_custom format displayed as %expected.', array('%expected' => $expected))); + } + + /** + * Tests date and time field. + */ + function testDatetimeRangeField() { + $field_name = $this->fieldStorage->getName(); + + // Ensure the field to a datetime field. + $this->fieldStorage->setSetting('daterange_type', DateRangeItem::DATERANGE_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][value2][date]", '', 'End date element found.'); + $this->assertFieldByName("{$field_name}[0][value2][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'); + $value2 = '2013-06-06 00:00:00'; + $end_date = new DrupalDateTime($value2, '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][value2][date]" => $end_date->format($date_format), + "{$field_name}[0][value2][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 = format_date($start_date->getTimestamp(), 'long'); + $start_expected_iso = format_date($start_date->getTimestamp(), 'custom', 'Y-m-d\TH:i:s\Z', 'UTC'); + $end_expected = format_date($end_date->getTimestamp(), 'long'); + $end_expected_iso = format_date($end_date->getTimestamp(), 'custom', 'Y-m-d\TH:i:s\Z', 'UTC'); + $this->renderTestEntity($id); + $this->assertFieldByXPath('//time[@datetime="' . $start_expected_iso . '"]', $start_expected, SafeMarkup::format('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, SafeMarkup::format('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, SafeMarkup::format('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, SafeMarkup::format('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, SafeMarkup::format('Formatted date field using daterange_custom format displayed as %expected.', array('%expected' => $expected))); + } + + /** + * Tests all-day field. + */ + function testAlldayRangeField() { + $field_name = $this->fieldStorage->getName(); + + // Ensure field is set to a all-day field. + $this->fieldStorage->setSetting('daterange_type', DateRangeItem::DATERANGE_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][value2][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][value2][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())); + $value2 = '2013-06-06 23:59:59'; + $end_date = new DrupalDateTime($value2, 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][value2][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 = format_date($start_date->getTimestamp(), 'long'); + $start_expected_iso = format_date($start_date->getTimestamp(), 'custom', 'Y-m-d\TH:i:s\Z', 'UTC'); + $end_expected = format_date($end_date->getTimestamp(), 'long'); + $end_expected_iso = format_date($end_date->getTimestamp(), 'custom', 'Y-m-d\TH:i:s\Z', 'UTC'); + $this->renderTestEntity($id); + $this->assertFieldByXPath('//time[@datetime="' . $start_expected_iso . '"]', $start_expected, SafeMarkup::format('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, SafeMarkup::format('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, SafeMarkup::format('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, SafeMarkup::format('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, SafeMarkup::format('Formatted date field using daterange_custom format displayed as %expected.', array('%expected' => $expected))); + } + + /** + * Tests Date Range List Widget functionality. + */ + function testDatelistWidget() { + $field_name = $this->fieldStorage->getName(); + + // Ensure field is set to a date only field. + $this->fieldStorage->setSetting('daterange_type', DateRangeItem::DATERANGE_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-value2-hour\"]", NULL, 'Hour element not found on Date Only.'); + $this->assertNoFieldByXPath("//*[@id=\"edit-$field_name-0-value2-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('daterange_type', DateRangeItem::DATERANGE_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-value2-hour\"]", NULL, 'Hour element not found on Date Only.'); + $this->assertNoFieldByXPath("//*[@id=\"edit-$field_name-0-value2-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('daterange_type', DateRangeItem::DATERANGE_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'); + + $this->assertFieldByXPath("//*[@id=\"edit-$field_name-0-value-year\"]", NULL, 'Year element found.'); + $this->assertOptionSelected("edit-$field_name-0-value-year", '', 'No year selected.'); + $this->assertOptionByText("edit-$field_name-0-value-year", t('Year')); + $this->assertFieldByXPath("//*[@id=\"edit-$field_name-0-value-month\"]", NULL, 'Month element found.'); + $this->assertOptionSelected("edit-$field_name-0-value-month", '', 'No month selected.'); + $this->assertOptionByText("edit-$field_name-0-value-month", t('Month')); + $this->assertFieldByXPath("//*[@id=\"edit-$field_name-0-value-day\"]", NULL, 'Day element found.'); + $this->assertOptionSelected("edit-$field_name-0-value-day", '', 'No day selected.'); + $this->assertOptionByText("edit-$field_name-0-value-day", t('Day')); + $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->assertOptionByText("edit-$field_name-0-value-hour", t('Hour')); + $this->assertFieldByXPath("//*[@id=\"edit-$field_name-0-value-minute\"]", NULL, 'Minute element found.'); + $this->assertOptionSelected("edit-$field_name-0-value-minute", '', 'No minute selected.'); + $this->assertOptionByText("edit-$field_name-0-value-minute", t('Minute')); + $this->assertNoFieldByXPath("//*[@id=\"edit-$field_name-0-value-second\"]", NULL, 'Second element not found.'); + $this->assertFieldByXPath("//*[@id=\"edit-$field_name-0-value-ampm\"]", NULL, 'AMPM element found.'); + $this->assertOptionSelected("edit-$field_name-0-value-ampm", '', 'No ampm selected.'); + $this->assertOptionByText("edit-$field_name-0-value-ampm", t('AM/PM')); + + $this->assertFieldByXPath("//*[@id=\"edit-$field_name-0-value2-year\"]", NULL, 'Year element found.'); + $this->assertOptionSelected("edit-$field_name-0-value2-year", '', 'No year selected.'); + $this->assertOptionByText("edit-$field_name-0-value2-year", t('Year')); + $this->assertFieldByXPath("//*[@id=\"edit-$field_name-0-value2-month\"]", NULL, 'Month element found.'); + $this->assertOptionSelected("edit-$field_name-0-value2-month", '', 'No month selected.'); + $this->assertOptionByText("edit-$field_name-0-value2-month", t('Month')); + $this->assertFieldByXPath("//*[@id=\"edit-$field_name-0-value2-day\"]", NULL, 'Day element found.'); + $this->assertOptionSelected("edit-$field_name-0-value2-day", '', 'No day selected.'); + $this->assertOptionByText("edit-$field_name-0-value2-day", t('Day')); + $this->assertFieldByXPath("//*[@id=\"edit-$field_name-0-value2-hour\"]", NULL, 'Hour element found.'); + $this->assertOptionSelected("edit-$field_name-0-value2-hour", '', 'No hour selected.'); + $this->assertOptionByText("edit-$field_name-0-value2-hour", t('Hour')); + $this->assertFieldByXPath("//*[@id=\"edit-$field_name-0-value2-minute\"]", NULL, 'Minute element found.'); + $this->assertOptionSelected("edit-$field_name-0-value2-minute", '', 'No minute selected.'); + $this->assertOptionByText("edit-$field_name-0-value2-minute", t('Minute')); + $this->assertNoFieldByXPath("//*[@id=\"edit-$field_name-0-value2-second\"]", NULL, 'Second element not found.'); + $this->assertFieldByXPath("//*[@id=\"edit-$field_name-0-value2-ampm\"]", NULL, 'AMPM element found.'); + $this->assertOptionSelected("edit-$field_name-0-value2-ampm", '', 'No ampm selected.'); + $this->assertOptionByText("edit-$field_name-0-value2-ampm", t('AM/PM')); + + // 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][value2][$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-value2-year", '2013', 'Correct year selected.'); + $this->assertOptionSelected("edit-$field_name-0-value2-month", '1', 'Correct month selected.'); + $this->assertOptionSelected("edit-$field_name-0-value2-day", '15', 'Correct day selected.'); + $this->assertOptionSelected("edit-$field_name-0-value2-hour", '3', 'Correct hour selected.'); + $this->assertOptionSelected("edit-$field_name-0-value2-minute", '30', 'Correct minute selected.'); + $this->assertOptionSelected("edit-$field_name-0-value2-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-value2-hour\"]", NULL, 'Hour element found.'); + $this->assertOptionSelected("edit-$field_name-0-value2-hour", '', 'No hour selected.'); + $this->assertNoFieldByXPath("//*[@id=\"edit-$field_name-0-value2-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][value2][$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-value2-year", '2013', 'Correct year selected.'); + $this->assertOptionSelected("edit-$field_name-0-value2-month", '1', 'Correct month selected.'); + $this->assertOptionSelected("edit-$field_name-0-value2-day", '15', 'Correct day selected.'); + $this->assertOptionSelected("edit-$field_name-0-value2-hour", '3', 'Correct hour selected.'); + $this->assertOptionSelected("edit-$field_name-0-value2-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][value2][$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][value2][$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][value2][$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-value2-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' => 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. + */ + 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' => ['daterange_type' => DateRangeItem::DATERANGE_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_start_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-start-date-type', 'now', 'The default start value is selected in instance settings page'); + $this->assertFieldByName('default_value_input[default_start_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_start_date_type' => 'now', + 'default_start_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)->value2, $expected_date->format(DATETIME_DATE_STORAGE_FORMAT)); + + // Set an invalid relative default_value to test validation. + $field_edit = [ + 'default_value_input[default_start_date_type]' => 'relative', + 'default_value_input[default_start_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_start_date_type]' => 'relative', + 'default_value_input[default_start_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_start_date_type]' => 'relative', + 'default_value_input[default_start_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-start-date-type', 'relative', 'The default start value is selected in instance settings page'); + $this->assertFieldByName('default_value_input[default_start_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_start_date_type' => 'relative', + 'default_start_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)->value2, $expected_end_date->format(DATETIME_DATE_STORAGE_FORMAT)); + + // Remove default value. + $field_edit = [ + 'default_value_input[default_start_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-start-date-type', '', 'The default start value is selected in instance settings page'); + $this->assertFieldByName('default_value_input[default_start_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'); + } + + /** + * Test that invalid values are caught and marked as invalid. + */ + function testInvalidField() { + // Change the field to a datetime field. + $this->fieldStorage->setSetting('daterange_type', DateRangeItem::DATERANGE_TYPE_DATETIME); + $this->fieldStorage->save(); + $field_name = $this->fieldStorage->getName(); + + // 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][value2][date]", '', 'End date element found.'); + $this->assertFieldByName("{$field_name}[0][value2][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][value2][date]" => '2012-12-01', + "{$field_name}[0][value2][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][value2][date]" => '2012-12-01', + "{$field_name}[0][value2][time]" => '12:00:00', + ]; + $this->drupalPostForm(NULL, $edit, t('Save')); + $this->assertText('date is invalid', format_string('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][value2][date]" => '2012-12-01', + "{$field_name}[0][value2][time]" => '12:00:00', + ]; + $this->drupalPostForm(NULL, $edit, t('Save')); + $this->assertText('date is invalid', format_string('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][value2][date]" => '2012-12-01', + "{$field_name}[0][value2][time]" => '12:00:00', + ]; + $this->drupalPostForm(NULL, $edit, t('Save')); + $this->assertText('date is invalid', format_string('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][value2][date]" => '2012-12-01', + "{$field_name}[0][value2][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][value2][date]" => '2012-12-01', + "{$field_name}[0][value2][time]" => '12:00:00', + ]; + $this->drupalPostForm(NULL, $edit, t('Save')); + $this->assertText('date is invalid', format_string('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][value2][date]" => '2012-12-01', + "{$field_name}[0][value2][time]" => '12:00:00', + ]; + $this->drupalPostForm(NULL, $edit, t('Save')); + $this->assertText('date is invalid', format_string('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][value2][date]" => '2012-12-01', + "{$field_name}[0][value2][time]" => '12:00:00', + ]; + $this->drupalPostForm(NULL, $edit, t('Save')); + $this->assertText('date is invalid', format_string('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][value2][date]" => $date_value, + "{$field_name}[0][value2][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][value2][date]" => $date_value, + "{$field_name}[0][value2][time]" => '00:00:00', + ]; + $this->drupalPostForm(NULL, $edit, t('Save')); + $this->assertText('date is invalid', format_string('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][value2][date]" => $date_value, + "{$field_name}[0][value2][time]" => '00:00:00', + ]; + $this->drupalPostForm(NULL, $edit, t('Save')); + $this->assertText('date is invalid', format_string('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][value2][date]" => $date_value, + "{$field_name}[0][value2][time]" => '00:00:00', + ]; + $this->drupalPostForm(NULL, $edit, t('Save')); + $this->assertText('date is invalid', format_string('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][value2][date]" => '2012-12-01', + "{$field_name}[0][value2][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][value2][date]" => '2012-12-01', + "{$field_name}[0][value2][time]" => $time_value, + ]; + $this->drupalPostForm(NULL, $edit, t('Save')); + $this->assertText('date is invalid', format_string('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][value2][date]" => '2012-12-01', + "{$field_name}[0][value2][time]" => $time_value, + ]; + $this->drupalPostForm(NULL, $edit, t('Save')); + $this->assertText('date is invalid', format_string('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][value2][date]" => '2012-12-01', + "{$field_name}[0][value2][time]" => $time_value, + ]; + $this->drupalPostForm(NULL, $edit, t('Save')); + $this->assertText('date is invalid', format_string('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][value2][date]" => '2010-12-01', + "{$field_name}[0][value2][time]" => '12:00:00', + ]; + $this->drupalPostForm(NULL, $edit, t('Save')); + $this->assertText('Start date should be equal to, or before, end date', '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][value2][date]" => '2012-12-01', + "{$field_name}[0][value2][time]" => '11:00:00', + ]; + $this->drupalPostForm(NULL, $edit, t('Save')); + $this->assertText('Start date should be equal to, or before, end date', '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' => [ + 'daterange_type' => DateRangeItem::DATERANGE_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][value2][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-daterange-type' and contains(@disabled, 'disabled')]"); + $this->assertEqual(count($result), 1, "Changing daterange 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(); + } + +}