summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorxjm2016-08-25 20:56:23 -0500
committerxjm2016-08-25 20:56:23 -0500
commitb1656002753f0f01a72d5a0b390eaaadae3b1e3a (patch)
tree467c9bc1c411e9ee28f53560c0030cde40c2a8e3
parentb4d89a9ec4dbdc53e2b3675b564c97f77fb4d047 (diff)
Issue #2161337 by mpdonadio, darrick, swentel, effulgentsia, YesCT, xjm, Berdir, tim.plunkett, pguillard, SKAUGHT, sylus, jonathanjfshaw, FluxSauce, DuneBL, jhedstrom, dawehner, generalredneck, pjonckiere, borisson_, Gábor Hojtsy, webchick, miwayha: Add a Date Range field type with support for end date
-rw-r--r--core/MAINTAINERS.txt4
-rw-r--r--core/composer.json1
-rw-r--r--core/modules/datetime_range/config/schema/datetime_range.schema.yml70
-rw-r--r--core/modules/datetime_range/datetime_range.info.yml8
-rw-r--r--core/modules/datetime_range/datetime_range.module28
-rw-r--r--core/modules/datetime_range/src/DateTimeRangeTrait.php78
-rw-r--r--core/modules/datetime_range/src/Plugin/Field/FieldFormatter/DateRangeCustomFormatter.php96
-rw-r--r--core/modules/datetime_range/src/Plugin/Field/FieldFormatter/DateRangeDefaultFormatter.php104
-rw-r--r--core/modules/datetime_range/src/Plugin/Field/FieldFormatter/DateRangePlainFormatter.php96
-rw-r--r--core/modules/datetime_range/src/Plugin/Field/FieldType/DateRangeFieldItemList.php131
-rw-r--r--core/modules/datetime_range/src/Plugin/Field/FieldType/DateRangeItem.php132
-rw-r--r--core/modules/datetime_range/src/Plugin/Field/FieldWidget/DateRangeDatelistWidget.php157
-rw-r--r--core/modules/datetime_range/src/Plugin/Field/FieldWidget/DateRangeDefaultWidget.php101
-rw-r--r--core/modules/datetime_range/src/Plugin/Field/FieldWidget/DateRangeWidgetBase.php167
-rw-r--r--core/modules/datetime_range/src/Tests/DateRangeFieldTest.php1432
15 files changed, 2605 insertions, 0 deletions
diff --git a/core/MAINTAINERS.txt b/core/MAINTAINERS.txt
index b6bfdd9..a74afa6 100644
--- a/core/MAINTAINERS.txt
+++ b/core/MAINTAINERS.txt
@@ -324,6 +324,10 @@ DateTime module
- Jonathan Hedstrom 'jhedstrom' https://www.drupal.org/u/jhedstrom
- Matthew Donadio 'mpdonadio' https://www.drupal.org/u/mpdonadio
+DateTime range module
+- Jonathan Hedstrom 'jhedstrom' https://www.drupal.org/u/jhedstrom
+- Matthew Donadio 'mpdonadio' https://www.drupal.org/u/mpdonadio
+
Dynamic Page Cache module
- Fabian Franz 'Fabianx' https://www.drupal.org/u/fabianx
- Wim Leers 'Wim Leers' https://www.drupal.org/u/wim-leers
diff --git a/core/composer.json b/core/composer.json
index de2feb8..53ab8fa 100644
--- a/core/composer.json
+++ b/core/composer.json
@@ -88,6 +88,7 @@
"drupal/core-utility": "self.version",
"drupal/core-uuid": "self.version",
"drupal/datetime": "self.version",
+ "drupal/datetime_range": "self.version",
"drupal/dblog": "self.version",
"drupal/dynamic_page_cache": "self.version",
"drupal/editor": "self.version",
diff --git a/core/modules/datetime_range/config/schema/datetime_range.schema.yml b/core/modules/datetime_range/config/schema/datetime_range.schema.yml
new file mode 100644
index 0000000..90bbd38
--- /dev/null
+++ b/core/modules/datetime_range/config/schema/datetime_range.schema.yml
@@ -0,0 +1,70 @@
+# Schema for the configuration files of the Datetime Range module.
+
+# Daterange field type.
+
+field.storage_settings.daterange:
+ type: field.storage_settings.datetime
+ label: 'Date range settings'
+
+field.field_settings.daterange:
+ type: field.field_settings.datetime
+ label: 'Date range settings'
+
+field.value.daterange:
+ type: mapping
+ label: 'Default value'
+ mapping:
+ default_date_type:
+ type: string
+ label: 'Default start date type'
+ default_date:
+ type: string
+ label: 'Default start date value'
+ default_end_date_type:
+ type: string
+ label: 'Default end date type'
+ default_end_date:
+ type: string
+ label: 'Default end date value'
+
+field.formatter.settings.daterange_default:
+ type: field.formatter.settings.datetime_default
+ label: 'Date range default display format settings'
+ mapping:
+ separator:
+ type: string
+ label: 'Separator'
+
+field.formatter.settings.daterange_plain:
+ type: field.formatter.settings.datetime_plain
+ label: 'Date range plain display format settings'
+ mapping:
+ separator:
+ type: string
+ label: 'Separator'
+
+field.formatter.settings.daterange_custom:
+ type: field.formatter.settings.datetime_custom
+ label: 'Date range custom display format settings'
+ mapping:
+ separator:
+ type: string
+ label: 'Separator'
+
+field.widget.settings.daterange_datelist:
+ type: mapping
+ label: 'Date range select list display format settings'
+ mapping:
+ increment:
+ type: integer
+ label: 'Time increments'
+ date_order:
+ type: string
+ label: 'Date part order'
+ time_type:
+ type: string
+ label: 'Time type'
+
+field.widget.settings.daterange_default:
+ type: mapping
+ label: 'Date range default display format settings'
diff --git a/core/modules/datetime_range/datetime_range.info.yml b/core/modules/datetime_range/datetime_range.info.yml
new file mode 100644
index 0000000..dd22d9a
--- /dev/null
+++ b/core/modules/datetime_range/datetime_range.info.yml
@@ -0,0 +1,8 @@
+name: 'Datetime Range'
+type: module
+description: 'Provides the ability to store end dates.'
+package: Core (Experimental)
+version: VERSION
+core: 8.x
+dependencies:
+ - datetime
diff --git a/core/modules/datetime_range/datetime_range.module b/core/modules/datetime_range/datetime_range.module
new file mode 100644
index 0000000..ceeb99d
--- /dev/null
+++ b/core/modules/datetime_range/datetime_range.module
@@ -0,0 +1,28 @@
+<?php
+
+/**
+ * @file
+ * Field hooks to implement a datetime field that stores a start and end date.
+ */
+
+use Drupal\Core\Routing\RouteMatchInterface;
+
+/**
+ * Implements hook_help().
+ */
+function datetime_range_help($route_name, RouteMatchInterface $route_match) {
+ switch ($route_name) {
+ case 'help.page.datetime_range':
+ $output = '';
+ $output .= '<h3>' . t('About') . '</h3>';
+ $output .= '<p>' . t('The Datetime Range module provides a Date field that stores start dates and times, as well as end dates and times. See the <a href=":field">Field module help</a> and the <a href=":field_ui">Field UI module help</a> pages for general information on fields and how to create and manage them. For more information, see the <a href=":datetime_do">online documentation for the Datetime Range module</a>.', array(':field' => \Drupal::url('help.page', array('name' => 'field')), ':field_ui' => (\Drupal::moduleHandler()->moduleExists('field_ui')) ? \Drupal::url('help.page', array('name' => 'field_ui')) : '#', ':datetime_do' => 'https://www.drupal.org/documentation/modules/datetime_range')) . '</p>';
+ $output .= '<h3>' . t('Uses') . '</h3>';
+ $output .= '<dl>';
+ $output .= '<dt>' . t('Managing and displaying date fields') . '</dt>';
+ $output .= '<dd>' . t('The <em>settings</em> and the <em>display</em> of the Date field can be configured separately. See the <a href=":field_ui">Field UI help</a> for more information on how to manage fields and their display.', array(':field_ui' => (\Drupal::moduleHandler()->moduleExists('field_ui')) ? \Drupal::url('help.page', array('name' => 'field_ui')) : '#')) . '</dd>';
+ $output .= '<dt>' . t('Displaying dates') . '</dt>';
+ $output .= '<dd>' . t('Dates can be displayed using the <em>Plain</em> or the <em>Default</em> formatter. The <em>Plain</em> formatter displays the date in the <a href="http://en.wikipedia.org/wiki/ISO_8601">ISO 8601</a> format. If you choose the <em>Default</em> formatter, you can choose a format from a predefined list that can be managed on the <a href=":date_format_list">Date and time formats</a> page.', array(':date_format_list' => \Drupal::url('entity.date_format.collection'))) . '</dd>';
+ $output .= '</dl>';
+ return $output;
+ }
+}
diff --git a/core/modules/datetime_range/src/DateTimeRangeTrait.php b/core/modules/datetime_range/src/DateTimeRangeTrait.php
new file mode 100644
index 0000000..5a34f2c
--- /dev/null
+++ b/core/modules/datetime_range/src/DateTimeRangeTrait.php
@@ -0,0 +1,78 @@
+<?php
+
+namespace Drupal\datetime_range;
+
+use Drupal\Core\Datetime\DrupalDateTime;
+use Drupal\datetime\Plugin\Field\FieldType\DateTimeItem;
+
+/**
+ * Provides friendly methods for datetime range.
+ */
+trait DateTimeRangeTrait {
+
+ /**
+ * Creates a render array from a date object.
+ *
+ * @param \Drupal\Core\Datetime\DrupalDateTime $date
+ * A date object.
+ *
+ * @return array
+ * A render array.
+ */
+ protected function buildDate(DrupalDateTime $date) {
+ if ($this->getFieldSetting('datetime_type') == DateTimeItem::DATETIME_TYPE_DATE) {
+ // A date without time will pick up the current time, use the default.
+ datetime_date_default_time($date);
+ }
+ $this->setTimeZone($date);
+
+ $build = [
+ '#plain_text' => $this->formatDate($date),
+ '#cache' => [
+ 'contexts' => [
+ 'timezone',
+ ],
+ ],
+ ];
+
+ return $build;
+ }
+
+ /**
+ * Creates a render array from a date object with ISO date attribute.
+ *
+ * @param \Drupal\Core\Datetime\DrupalDateTime $date
+ * A date object.
+ *
+ * @return array
+ * A render array.
+ */
+ protected function buildDateWithIsoAttribute(DrupalDateTime $date) {
+ if ($this->getFieldSetting('datetime_type') == DateTimeItem::DATETIME_TYPE_DATE) {
+ // A date without time will pick up the current time, use the default.
+ datetime_date_default_time($date);
+ }
+
+ // Create the ISO date in Universal Time.
+ $iso_date = $date->format("Y-m-d\TH:i:s") . 'Z';
+
+ $this->setTimeZone($date);
+
+ $build = [
+ '#theme' => 'time',
+ '#text' => $this->formatDate($date),
+ '#html' => FALSE,
+ '#attributes' => [
+ 'datetime' => $iso_date,
+ ],
+ '#cache' => [
+ 'contexts' => [
+ 'timezone',
+ ],
+ ],
+ ];
+
+ return $build;
+ }
+
+}
diff --git a/core/modules/datetime_range/src/Plugin/Field/FieldFormatter/DateRangeCustomFormatter.php b/core/modules/datetime_range/src/Plugin/Field/FieldFormatter/DateRangeCustomFormatter.php
new file mode 100644
index 0000000..a23de51
--- /dev/null
+++ b/core/modules/datetime_range/src/Plugin/Field/FieldFormatter/DateRangeCustomFormatter.php
@@ -0,0 +1,96 @@
+<?php
+
+namespace Drupal\datetime_range\Plugin\Field\FieldFormatter;
+
+use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\datetime\Plugin\Field\FieldFormatter\DateTimeCustomFormatter;
+use Drupal\datetime_range\DateTimeRangeTrait;
+
+/**
+ * Plugin implementation of the 'Custom' formatter for 'daterange' fields.
+ *
+ * This formatter renders the data range as plain text, with a fully
+ * configurable date format using the PHP date syntax and separator.
+ *
+ * @FieldFormatter(
+ * id = "daterange_custom",
+ * label = @Translation("Custom"),
+ * field_types = {
+ * "daterange"
+ * }
+ * )
+ */
+class DateRangeCustomFormatter extends DateTimeCustomFormatter {
+
+ use DateTimeRangeTrait;
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function defaultSettings() {
+ return [
+ 'separator' => '-',
+ ] + parent::defaultSettings();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function viewElements(FieldItemListInterface $items, $langcode) {
+ $elements = [];
+ $separator = $this->getSetting('separator');
+
+ foreach ($items as $delta => $item) {
+ if (!empty($item->start_date) && !empty($item->end_date)) {
+ /** @var \Drupal\Core\Datetime\DrupalDateTime $start_date */
+ $start_date = $item->start_date;
+ /** @var \Drupal\Core\Datetime\DrupalDateTime $end_date */
+ $end_date = $item->end_date;
+
+ if ($start_date->format('U') !== $end_date->format('U')) {
+ $elements[$delta] = [
+ 'start_date' => $this->buildDate($start_date),
+ 'separator' => ['#plain_text' => ' ' . $separator . ' '],
+ 'end_date' => $this->buildDate($end_date),
+ ];
+ }
+ else {
+ $elements[$delta] = $this->buildDate($start_date);
+ }
+ }
+ }
+
+ return $elements;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function settingsForm(array $form, FormStateInterface $form_state) {
+ $form = parent::settingsForm($form, $form_state);
+
+ $form['separator'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Date separator'),
+ '#description' => $this->t('The string to separate the start and end dates'),
+ '#default_value' => $this->getSetting('separator'),
+ ];
+
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function settingsSummary() {
+ $summary = parent::settingsSummary();
+
+ if ($separator = $this->getSetting('separator')) {
+ $summary[] = $this->t('Separator: %separator', ['%separator' => $separator]);
+ }
+
+ return $summary;
+ }
+
+}
diff --git a/core/modules/datetime_range/src/Plugin/Field/FieldFormatter/DateRangeDefaultFormatter.php b/core/modules/datetime_range/src/Plugin/Field/FieldFormatter/DateRangeDefaultFormatter.php
new file mode 100644
index 0000000..f5ebb8c
--- /dev/null
+++ b/core/modules/datetime_range/src/Plugin/Field/FieldFormatter/DateRangeDefaultFormatter.php
@@ -0,0 +1,104 @@
+<?php
+
+namespace Drupal\datetime_range\Plugin\Field\FieldFormatter;
+
+use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\datetime\Plugin\Field\FieldFormatter\DateTimeDefaultFormatter;
+use Drupal\datetime_range\DateTimeRangeTrait;
+
+/**
+ * Plugin implementation of the 'Default' formatter for 'daterange' fields.
+ *
+ * This formatter renders the data range using <time> elements, with
+ * configurable date formats (from the list of configured formats) and a
+ * separator.
+ *
+ * @FieldFormatter(
+ * id = "daterange_default",
+ * label = @Translation("Default"),
+ * field_types = {
+ * "daterange"
+ * }
+ * )
+ */
+class DateRangeDefaultFormatter extends DateTimeDefaultFormatter {
+
+ use DateTimeRangeTrait;
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function defaultSettings() {
+ return [
+ 'separator' => '-',
+ ] + parent::defaultSettings();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function viewElements(FieldItemListInterface $items, $langcode) {
+ $elements = [];
+ $separator = $this->getSetting('separator');
+
+ foreach ($items as $delta => $item) {
+ if (!empty($item->start_date) && !empty($item->end_date)) {
+ /** @var \Drupal\Core\Datetime\DrupalDateTime $start_date */
+ $start_date = $item->start_date;
+ /** @var \Drupal\Core\Datetime\DrupalDateTime $end_date */
+ $end_date = $item->end_date;
+
+ if ($start_date->format('U') !== $end_date->format('U')) {
+ $elements[$delta] = [
+ 'start_date' => $this->buildDateWithIsoAttribute($start_date),
+ 'separator' => ['#plain_text' => ' ' . $separator . ' '],
+ 'end_date' => $this->buildDateWithIsoAttribute($end_date),
+ ];
+ }
+ else {
+ $elements[$delta] = $this->buildDateWithIsoAttribute($start_date);
+ }
+
+ if (!empty($item->_attributes)) {
+ $elements[$delta]['#attributes'] += $item->_attributes;
+ // Unset field item attributes since they have been included in the
+ // formatter output and should not be rendered in the field template.
+ unset($item->_attributes);
+ }
+ }
+ }
+
+ return $elements;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function settingsForm(array $form, FormStateInterface $form_state) {
+ $form = parent::settingsForm($form, $form_state);
+
+ $form['separator'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Date separator'),
+ '#description' => $this->t('The string to separate the start and end dates'),
+ '#default_value' => $this->getSetting('separator'),
+ ];
+
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function settingsSummary() {
+ $summary = parent::settingsSummary();
+
+ if ($separator = $this->getSetting('separator')) {
+ $summary[] = $this->t('Separator: %separator', ['%separator' => $separator]);
+ }
+
+ return $summary;
+ }
+
+}
diff --git a/core/modules/datetime_range/src/Plugin/Field/FieldFormatter/DateRangePlainFormatter.php b/core/modules/datetime_range/src/Plugin/Field/FieldFormatter/DateRangePlainFormatter.php
new file mode 100644
index 0000000..4842e1f
--- /dev/null
+++ b/core/modules/datetime_range/src/Plugin/Field/FieldFormatter/DateRangePlainFormatter.php
@@ -0,0 +1,96 @@
+<?php
+
+namespace Drupal\datetime_range\Plugin\Field\FieldFormatter;
+
+use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\datetime\Plugin\Field\FieldFormatter\DateTimePlainFormatter;
+use Drupal\datetime_range\DateTimeRangeTrait;
+
+/**
+ * Plugin implementation of the 'Plain' formatter for 'daterange' fields.
+ *
+ * This formatter renders the data range as a plain text string, with a
+ * configurable separator using an ISO-like date format string.
+ *
+ * @FieldFormatter(
+ * id = "daterange_plain",
+ * label = @Translation("Plain"),
+ * field_types = {
+ * "daterange"
+ * }
+ * )
+ */
+class DateRangePlainFormatter extends DateTimePlainFormatter {
+
+ use DateTimeRangeTrait;
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function defaultSettings() {
+ return [
+ 'separator' => '-',
+ ] + parent::defaultSettings();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function viewElements(FieldItemListInterface $items, $langcode) {
+ $elements = [];
+ $separator = $this->getSetting('separator');
+
+ foreach ($items as $delta => $item) {
+ if (!empty($item->start_date) && !empty($item->end_date)) {
+ /** @var \Drupal\Core\Datetime\DrupalDateTime $start_date */
+ $start_date = $item->start_date;
+ /** @var \Drupal\Core\Datetime\DrupalDateTime $end_date */
+ $end_date = $item->end_date;
+
+ if ($start_date->format('U') !== $end_date->format('U')) {
+ $elements[$delta] = [
+ 'start_date' => $this->buildDate($start_date),
+ 'separator' => ['#plain_text' => ' ' . $separator . ' '],
+ 'end_date' => $this->buildDate($end_date),
+ ];
+ }
+ else {
+ $elements[$delta] = $this->buildDate($start_date);
+ }
+ }
+ }
+
+ return $elements;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function settingsForm(array $form, FormStateInterface $form_state) {
+ $form = parent::settingsForm($form, $form_state);
+
+ $form['separator'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Date separator'),
+ '#description' => $this->t('The string to separate the start and end dates'),
+ '#default_value' => $this->getSetting('separator'),
+ ];
+
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function settingsSummary() {
+ $summary = parent::settingsSummary();
+
+ if ($separator = $this->getSetting('separator')) {
+ $summary[] = $this->t('Separator: %separator', ['%separator' => $separator]);
+ }
+
+ return $summary;
+ }
+
+}
diff --git a/core/modules/datetime_range/src/Plugin/Field/FieldType/DateRangeFieldItemList.php b/core/modules/datetime_range/src/Plugin/Field/FieldType/DateRangeFieldItemList.php
new file mode 100644
index 0000000..e145032
--- /dev/null
+++ b/core/modules/datetime_range/src/Plugin/Field/FieldType/DateRangeFieldItemList.php
@@ -0,0 +1,131 @@
+<?php
+
+namespace Drupal\datetime_range\Plugin\Field\FieldType;
+
+use Drupal\Core\Datetime\DrupalDateTime;
+use Drupal\Core\Entity\FieldableEntityInterface;
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\Field\FieldItemList;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\datetime\Plugin\Field\FieldType\DateTimeFieldItemList;
+
+/**
+ * Represents a configurable entity daterange field.
+ */
+class DateRangeFieldItemList extends DateTimeFieldItemList {
+
+ /**
+ * {@inheritdoc}
+ */
+ public function defaultValuesForm(array &$form, FormStateInterface $form_state) {
+ if (empty($this->getFieldDefinition()->getDefaultValueCallback())) {
+ $default_value = $this->getFieldDefinition()->getDefaultValueLiteral();
+
+ $element = parent::defaultValuesForm($form, $form_state);
+
+ $element['default_date_type']['#title'] = $this->t('Default start date');
+ $element['default_date_type']['#description'] = $this->t('Set a default value for the start date.');
+
+ $element['default_end_date_type'] = [
+ '#type' => 'select',
+ '#title' => $this->t('Default end date'),
+ '#description' => $this->t('Set a default value for the end date.'),
+ '#default_value' => isset($default_value[0]['default_end_date_type']) ? $default_value[0]['default_end_date_type'] : '',
+ '#options' => [
+ static::DEFAULT_VALUE_NOW => $this->t('Current date'),
+ static::DEFAULT_VALUE_CUSTOM => $this->t('Relative date'),
+ ],
+ '#empty_value' => '',
+ ];
+
+ $element['default_end_date'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Relative default value'),
+ '#description' => $this->t("Describe a time by reference to the current day, like '+90 days' (90 days from the day the field is created) or '+1 Saturday' (the next Saturday). See <a href=\"http://php.net/manual/function.strtotime.php\">strtotime</a> for more details."),
+ '#default_value' => (isset($default_value[0]['default_end_date_type']) && $default_value[0]['default_end_date_type'] == static::DEFAULT_VALUE_CUSTOM) ? $default_value[0]['default_end_date'] : '',
+ '#states' => [
+ 'visible' => [
+ ':input[id="edit-default-value-input-default-end-date-type"]' => ['value' => static::DEFAULT_VALUE_CUSTOM],
+ ],
+ ],
+ ];
+
+ return $element;
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function defaultValuesFormValidate(array $element, array &$form, FormStateInterface $form_state) {
+ if ($form_state->getValue(['default_value_input', 'default_date_type']) == static::DEFAULT_VALUE_CUSTOM) {
+ $is_strtotime = @strtotime($form_state->getValue(['default_value_input', 'default_date']));
+ if (!$is_strtotime) {
+ $form_state->setErrorByName('default_value_input][default_date', $this->t('The relative start date value entered is invalid.'));
+ }
+ }
+
+ if ($form_state->getValue(['default_value_input', 'default_end_date_type']) == static::DEFAULT_VALUE_CUSTOM) {
+ $is_strtotime = @strtotime($form_state->getValue(['default_value_input', 'default_end_date']));
+ if (!$is_strtotime) {
+ $form_state->setErrorByName('default_value_input][default_end_date', $this->t('The relative end date value entered is invalid.'));
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function defaultValuesFormSubmit(array $element, array &$form, FormStateInterface $form_state) {
+ if ($form_state->getValue(['default_value_input', 'default_date_type']) || $form_state->getValue(['default_value_input', 'default_end_date_type'])) {
+ if ($form_state->getValue(['default_value_input', 'default_date_type']) == static::DEFAULT_VALUE_NOW) {
+ $form_state->setValueForElement($element['default_date'], static::DEFAULT_VALUE_NOW);
+ }
+ if ($form_state->getValue(['default_value_input', 'default_end_date_type']) == static::DEFAULT_VALUE_NOW) {
+ $form_state->setValueForElement($element['default_end_date'], static::DEFAULT_VALUE_NOW);
+ }
+ return [$form_state->getValue('default_value_input')];
+ }
+ return [];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function processDefaultValue($default_value, FieldableEntityInterface $entity, FieldDefinitionInterface $definition) {
+ // Explicitly call the base class so that we can get the default value
+ // types.
+ $default_value = FieldItemList::processDefaultValue($default_value, $entity, $definition);
+
+ // Allow either the start or end date to have a default, but not require
+ // defaults for both.
+ if (!empty($default_value[0]['default_date_type']) || !empty($default_value[0]['default_end_date_type'])) {
+ // A default value should be in the format and timezone used for date
+ // storage. All-day ranges are stored the same as date+time ranges. We
+ // only provide a default value for the first item, as do all fields.
+ // Otherwise, there is no way to clear out unwanted values on multiple
+ // value fields.
+ $storage_format = $definition->getSetting('datetime_type') == DateRangeItem::DATETIME_TYPE_DATE ? DATETIME_DATE_STORAGE_FORMAT : DATETIME_DATETIME_STORAGE_FORMAT;
+ $default_values = [[]];
+
+ if (!empty($default_value[0]['default_date_type'])) {
+ $start_date = new DrupalDateTime($default_value[0]['default_date'], DATETIME_STORAGE_TIMEZONE);
+ $start_value = $start_date->format($storage_format);
+ $default_values[0]['value'] = $start_value;
+ $default_values[0]['start_date'] = $start_date;
+ }
+
+ if (!empty($default_value[0]['default_end_date_type'])) {
+ $end_date = new DrupalDateTime($default_value[0]['default_end_date'], DATETIME_STORAGE_TIMEZONE);
+ $end_value = $end_date->format($storage_format);
+ $default_values[0]['end_value'] = $end_value;
+ $default_values[0]['end_date'] = $end_date;
+ }
+
+ $default_value = $default_values;
+ }
+
+ return $default_value;
+ }
+
+}
diff --git a/core/modules/datetime_range/src/Plugin/Field/FieldType/DateRangeItem.php b/core/modules/datetime_range/src/Plugin/Field/FieldType/DateRangeItem.php
new file mode 100644
index 0000000..49c4dce
--- /dev/null
+++ b/core/modules/datetime_range/src/Plugin/Field/FieldType/DateRangeItem.php
@@ -0,0 +1,132 @@
+<?php
+
+namespace Drupal\datetime_range\Plugin\Field\FieldType;
+
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\TypedData\DataDefinition;
+use Drupal\datetime\DateTimeComputed;
+use Drupal\datetime\Plugin\Field\FieldType\DateTimeItem;
+
+/**
+ * Plugin implementation of the 'daterange' field type.
+ *
+ * @FieldType(
+ * id = "daterange",
+ * label = @Translation("Date range"),
+ * description = @Translation("Create and store date ranges."),
+ * default_widget = "daterange_default",
+ * default_formatter = "daterange_default",
+ * list_class = "\Drupal\datetime_range\Plugin\Field\FieldType\DateRangeFieldItemList"
+ * )
+ */
+class DateRangeItem extends DateTimeItem {
+
+ /**
+ * Value for the 'datetime_type' setting: store a date and time.
+ */
+ const DATETIME_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(DateTimeComputed::class)
+ ->setSetting('date source', 'value');
+
+ $properties['end_value'] = DataDefinition::create('datetime_iso8601')
+ ->setLabel(t('End date value'))
+ ->setRequired(TRUE);
+
+ $properties['end_date'] = DataDefinition::create('any')
+ ->setLabel(t('Computed end date'))
+ ->setDescription(t('The computed end DateTime object.'))
+ ->setComputed(TRUE)
+ ->setClass(DateTimeComputed::class)
+ ->setSetting('date source', 'end_value');
+
+ return $properties;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function schema(FieldStorageDefinitionInterface $field_definition) {
+ $schema = parent::schema($field_definition);
+
+ $schema['columns']['value']['description'] = 'The start date value.';
+
+ $schema['columns']['end_value'] = [
+ 'description' => 'The end date value.',
+ ] + $schema['columns']['value'];
+
+ $schema['indexes']['end_value'] = ['end_value'];
+
+ return $schema;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function storageSettingsForm(array &$form, FormStateInterface $form_state, $has_data) {
+ $element = parent::storageSettingsForm($form, $form_state, $has_data);
+
+ $element['datetime_type']['#options'][static::DATETIME_TYPE_ALLDAY] = $this->t('All Day');
+
+ return $element;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function generateSampleValue(FieldDefinitionInterface $field_definition) {
+ $type = $field_definition->getSetting('datetime_type');
+
+ // Just pick a date in the past year. No guidance is provided by this Field
+ // type.
+ $start = REQUEST_TIME - mt_rand(0, 86400 * 365) - 86400;
+ $end = $start + 86400;
+ if ($type == static::DATETIME_TYPE_DATETIME) {
+ $values['value'] = gmdate(DATETIME_DATETIME_STORAGE_FORMAT, $start);
+ $values['end_value'] = gmdate(DATETIME_DATETIME_STORAGE_FORMAT, $end);
+ }
+ else {
+ $values['value'] = gmdate(DATETIME_DATE_STORAGE_FORMAT, $start);
+ $values['end_value'] = gmdate(DATETIME_DATE_STORAGE_FORMAT, $end);
+ }
+ return $values;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isEmpty() {
+ $start_value = $this->get('value')->getValue();
+ $end_value = $this->get('end_value')->getValue();
+ return ($start_value === NULL || $start_value === '') && ($end_value === NULL || $end_value === '');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function onChange($property_name, $notify = TRUE) {
+ // Enforce that the computed date is recalculated.
+ if ($property_name == 'value') {
+ $this->start_date = NULL;
+ }
+ elseif ($property_name == 'end_value') {
+ $this->end_date = NULL;
+ }
+ parent::onChange($property_name, $notify);
+ }
+
+}
diff --git a/core/modules/datetime_range/src/Plugin/Field/FieldWidget/DateRangeDatelistWidget.php b/core/modules/datetime_range/src/Plugin/Field/FieldWidget/DateRangeDatelistWidget.php
new file mode 100644
index 0000000..3e0e6e4
--- /dev/null
+++ b/core/modules/datetime_range/src/Plugin/Field/FieldWidget/DateRangeDatelistWidget.php
@@ -0,0 +1,157 @@
+<?php
+
+namespace Drupal\datetime_range\Plugin\Field\FieldWidget;
+
+use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\datetime_range\Plugin\Field\FieldType\DateRangeItem;
+
+/**
+ * Plugin implementation of the 'daterange_datelist' widget.
+ *
+ * @FieldWidget(
+ * id = "daterange_datelist",
+ * label = @Translation("Select list"),
+ * field_types = {
+ * "daterange"
+ * }
+ * )
+ */
+class DateRangeDatelistWidget extends DateRangeWidgetBase {
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function defaultSettings() {
+ return [
+ 'increment' => '15',
+ 'date_order' => 'YMD',
+ 'time_type' => '24',
+ ] + parent::defaultSettings();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
+ $element = parent::formElement($items, $delta, $element, $form, $form_state);
+
+ $date_order = $this->getSetting('date_order');
+
+ if ($this->getFieldSetting('datetime_type') == DateRangeItem::DATETIME_TYPE_DATETIME) {
+ $time_type = $this->getSetting('time_type');
+ $increment = $this->getSetting('increment');
+ }
+ else {
+ $time_type = '';
+ $increment = '';
+ }
+
+ // Set up the date part order array.
+ switch ($date_order) {
+ default:
+ case 'YMD':
+ $date_part_order = ['year', 'month', 'day'];
+ break;
+
+ case 'MDY':
+ $date_part_order = ['month', 'day', 'year'];
+ break;
+
+ case 'DMY':
+ $date_part_order = ['day', 'month', 'year'];
+ break;
+ }
+ switch ($time_type) {
+ case '24':
+ $date_part_order = array_merge($date_part_order, ['hour', 'minute']);
+ break;
+
+ case '12':
+ $date_part_order = array_merge($date_part_order, ['hour', 'minute', 'ampm']);
+ break;
+
+ case 'none':
+ break;
+ }
+
+ $element['value'] = [
+ '#type' => 'datelist',
+ '#date_increment' => $increment,
+ '#date_part_order' => $date_part_order,
+ ] + $element['value'];
+
+ $element['end_value'] = [
+ '#type' => 'datelist',
+ '#date_increment' => $increment,
+ '#date_part_order' => $date_part_order,
+ ] + $element['end_value'];
+
+ return $element;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function settingsForm(array $form, FormStateInterface $form_state) {
+ $element = parent::settingsForm($form, $form_state);
+
+ $element['date_order'] = [
+ '#type' => 'select',
+ '#title' => $this->t('Date part order'),
+ '#default_value' => $this->getSetting('date_order'),
+ '#options' => ['MDY' => $this->t('Month/Day/Year'), 'DMY' => $this->t('Day/Month/Year'), 'YMD' => $this->t('Year/Month/Day')],
+ ];
+
+ if ($this->getFieldSetting('datetime_type') == DateRangeItem::DATETIME_TYPE_DATETIME) {
+ $element['time_type'] = [
+ '#type' => 'select',
+ '#title' => $this->t('Time type'),
+ '#default_value' => $this->getSetting('time_type'),
+ '#options' => ['24' => $this->t('24 hour time'), '12' => $this->t('12 hour time')],
+ ];
+
+ $element['increment'] = [
+ '#type' => 'select',
+ '#title' => $this->t('Time increments'),
+ '#default_value' => $this->getSetting('increment'),
+ '#options' => [
+ 1 => $this->t('1 minute'),
+ 5 => $this->t('5 minute'),
+ 10 => $this->t('10 minute'),
+ 15 => $this->t('15 minute'),
+ 30 => $this->t('30 minute'),
+ ],
+ ];
+ }
+ else {
+ $element['time_type'] = [
+ '#type' => 'hidden',
+ '#value' => 'none',
+ ];
+
+ $element['increment'] = [
+ '#type' => 'hidden',
+ '#value' => $this->getSetting('increment'),
+ ];
+ }
+
+ return $element;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function settingsSummary() {
+ $summary = [];
+
+ $summary[] = $this->t('Date part order: @order', ['@order' => $this->getSetting('date_order')]);
+ if ($this->getFieldSetting('datetime_type') == DateRangeItem::DATETIME_TYPE_DATETIME) {
+ $summary[] = $this->t('Time type: @time_type', ['@time_type' => $this->getSetting('time_type')]);
+ $summary[] = $this->t('Time increments: @increment', ['@increment' => $this->getSetting('increment')]);
+ }
+
+ return $summary;
+ }
+
+}
diff --git a/core/modules/datetime_range/src/Plugin/Field/FieldWidget/DateRangeDefaultWidget.php b/core/modules/datetime_range/src/Plugin/Field/FieldWidget/DateRangeDefaultWidget.php
new file mode 100644
index 0000000..79f7994
--- /dev/null
+++ b/core/modules/datetime_range/src/Plugin/Field/FieldWidget/DateRangeDefaultWidget.php
@@ -0,0 +1,101 @@
+<?php
+
+namespace Drupal\datetime_range\Plugin\Field\FieldWidget;
+
+use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\datetime_range\Plugin\Field\FieldType\DateRangeItem;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Plugin implementation of the 'daterange_default' widget.
+ *
+ * @FieldWidget(
+ * id = "daterange_default",
+ * label = @Translation("Date and time range"),
+ * field_types = {
+ * "daterange"
+ * }
+ * )
+ */
+class DateRangeDefaultWidget extends DateRangeWidgetBase implements ContainerFactoryPluginInterface {
+
+ /**
+ * The date format storage.
+ *
+ * @var \Drupal\Core\Entity\EntityStorageInterface
+ */
+ protected $dateStorage;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, EntityStorageInterface $date_storage) {
+ parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings);
+
+ $this->dateStorage = $date_storage;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+ return new static(
+ $plugin_id,
+ $plugin_definition,
+ $configuration['field_definition'],
+ $configuration['settings'],
+ $configuration['third_party_settings'],
+ $container->get('entity_type.manager')->getStorage('date_format')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
+ $element = parent::formElement($items, $delta, $element, $form, $form_state);
+
+ // Identify the type of date and time elements to use.
+ switch ($this->getFieldSetting('datetime_type')) {
+ case DateRangeItem::DATETIME_TYPE_DATE:
+ case DateRangeItem::DATETIME_TYPE_ALLDAY:
+ $date_type = 'date';
+ $time_type = 'none';
+ $date_format = $this->dateStorage->load('html_date')->getPattern();
+ $time_format = '';
+ break;
+
+ default:
+ $date_type = 'date';
+ $time_type = 'time';
+ $date_format = $this->dateStorage->load('html_date')->getPattern();
+ $time_format = $this->dateStorage->load('html_time')->getPattern();
+ break;
+ }
+
+ $element['value'] += [
+ '#date_date_format' => $date_format,
+ '#date_date_element' => $date_type,
+ '#date_date_callbacks' => [],
+ '#date_time_format' => $time_format,
+ '#date_time_element' => $time_type,
+ '#date_time_callbacks' => [],
+ ];
+
+ $element['end_value'] += [
+ '#date_date_format' => $date_format,
+ '#date_date_element' => $date_type,
+ '#date_date_callbacks' => [],
+ '#date_time_format' => $time_format,
+ '#date_time_element' => $time_type,
+ '#date_time_callbacks' => [],
+ ];
+
+ return $element;
+ }
+
+}
diff --git a/core/modules/datetime_range/src/Plugin/Field/FieldWidget/DateRangeWidgetBase.php b/core/modules/datetime_range/src/Plugin/Field/FieldWidget/DateRangeWidgetBase.php
new file mode 100644
index 0000000..ae9f24a
--- /dev/null
+++ b/core/modules/datetime_range/src/Plugin/Field/FieldWidget/DateRangeWidgetBase.php
@@ -0,0 +1,167 @@
+<?php
+
+namespace Drupal\datetime_range\Plugin\Field\FieldWidget;
+
+use Drupal\Core\Datetime\DrupalDateTime;
+use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\datetime\Plugin\Field\FieldType\DateTimeItem;
+use Drupal\datetime\Plugin\Field\FieldWidget\DateTimeWidgetBase;
+use Drupal\datetime_range\Plugin\Field\FieldType\DateRangeItem;
+
+/**
+ * Base class for the 'daterange_*' widgets.
+ */
+class DateRangeWidgetBase extends DateTimeWidgetBase {
+
+ /**
+ * {@inheritdoc}
+ */
+ public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
+ $element = parent::formElement($items, $delta, $element, $form, $form_state);
+ $element['#element_validate'][] = [$this, 'validateStartEnd'];
+ $element['value']['#title'] = $this->t('Start');
+
+ $element['end_value'] = [
+ '#title' => $this->t('End'),
+ ] + $element['value'];
+
+ if ($items[$delta]->start_date) {
+ /** @var \Drupal\Core\Datetime\DrupalDateTime $start_date */
+ $start_date = $items[$delta]->start_date;
+ $element['value']['#default_value'] = $this->createDefaultValue($start_date, $element['value']['#date_timezone']);
+ }
+
+ if ($items[$delta]->end_date) {
+ /** @var \Drupal\Core\Datetime\DrupalDateTime $end_date */
+ $end_date = $items[$delta]->end_date;
+ $element['end_value']['#default_value'] = $this->createDefaultValue($end_date, $element['end_value']['#date_timezone']);
+ }
+
+ return $element;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function massageFormValues(array $values, array $form, FormStateInterface $form_state) {
+ // The widget form element type has transformed the value to a
+ // DrupalDateTime object at this point. We need to convert it back to the
+ // storage timezone and format.
+ foreach ($values as &$item) {
+ if (!empty($item['value']) && $item['value'] instanceof DrupalDateTime) {
+ /** @var \Drupal\Core\Datetime\DrupalDateTime $start_date */
+ $start_date = $item['value'];
+ switch ($this->getFieldSetting('datetime_type')) {
+ case DateRangeItem::DATETIME_TYPE_DATE:
+ // If this is a date-only field, set it to the default time so the
+ // timezone conversion can be reversed.
+ datetime_date_default_time($start_date);
+ $format = DATETIME_DATE_STORAGE_FORMAT;
+ break;
+
+ case DateRangeItem::DATETIME_TYPE_ALLDAY:
+ // All day fields start at midnight on the starting date, but are
+ // stored like datetime fields, so we need to adjust the time.
+ // This function is called twice, so to prevent a double conversion
+ // we need to explicitly set the timezone.
+ $start_date->setTimeZone(timezone_open(drupal_get_user_timezone()));
+ $start_date->setTime(0, 0, 0);
+ $format = DATETIME_DATETIME_STORAGE_FORMAT;
+ break;
+
+ default:
+ $format = DATETIME_DATETIME_STORAGE_FORMAT;
+ break;
+ }
+ // Adjust the date for storage.
+ $start_date->setTimezone(new \DateTimezone(DATETIME_STORAGE_TIMEZONE));
+ $item['value'] = $start_date->format($format);
+ }
+
+ if (!empty($item['end_value']) && $item['end_value'] instanceof DrupalDateTime) {
+ /** @var \Drupal\Core\Datetime\DrupalDateTime $end_date */
+ $end_date = $item['end_value'];
+ switch ($this->getFieldSetting('datetime_type')) {
+ case DateRangeItem::DATETIME_TYPE_DATE:
+ // If this is a date-only field, set it to the default time so the
+ // timezone conversion can be reversed.
+ datetime_date_default_time($end_date);
+ $format = DATETIME_DATE_STORAGE_FORMAT;
+ break;
+
+ case DateRangeItem::DATETIME_TYPE_ALLDAY:
+ // All day fields end at midnight on the end date, but are
+ // stored like datetime fields, so we need to adjust the time.
+ // This function is called twice, so to prevent a double conversion
+ // we need to explicitly set the timezone.
+ $end_date->setTimeZone(timezone_open(drupal_get_user_timezone()));
+ $end_date->setTime(23, 59, 59);
+ $format = DATETIME_DATETIME_STORAGE_FORMAT;
+ break;
+
+ default:
+ $format = DATETIME_DATETIME_STORAGE_FORMAT;
+ break;
+ }
+ // Adjust the date for storage.
+ $end_date->setTimezone(new \DateTimezone(DATETIME_STORAGE_TIMEZONE));
+ $item['end_value'] = $end_date->format($format);
+ }
+ }
+
+ return $values;
+ }
+
+ /**
+ * #element_validate callback to ensure that the start date <= the end date.
+ *
+ * @param array $element
+ * An associative array containing the properties and children of the
+ * generic form element.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current state of the form.
+ * @param array $complete_form
+ * The complete form structure.
+ */
+ public function validateStartEnd(array &$element, FormStateInterface $form_state, array &$complete_form) {
+ $start_date = $element['value']['#value']['object'];
+ $end_date = $element['end_value']['#value']['object'];
+
+ if ($start_date instanceof DrupalDateTime && $end_date instanceof DrupalDateTime) {
+ if ($start_date->format('U') !== $end_date->format('U')) {
+ $interval = $start_date->diff($end_date);
+ if ($interval->invert === 1) {
+ $form_state->setError($element, $this->t('The @title end date cannot be before the start date', ['@title' => $element['#title']]));
+ }
+ }
+ }
+ }
+
+ /**
+ * Creates a date object for use as a default value.
+ *
+ * This will take a default value, apply the proper timezone for display in
+ * a widget, and set the default time for date-only fields.
+ *
+ * @param \Drupal\Core\Datetime\DrupalDateTime $date
+ * The UTC default date.
+ * @param string $timezone
+ * The timezone to apply.
+ *
+ * @return \Drupal\Core\Datetime\DrupalDateTime
+ * A date object for use as a default value in a field widget.
+ */
+ protected function createDefaultValue($date, $timezone) {
+ // The date was created and verified during field_load(), so it is safe to
+ // use without further inspection.
+ if ($this->getFieldSetting('datetime_type') == DateTimeItem::DATETIME_TYPE_DATE) {
+ // A date without time will pick up the current time, use the default
+ // time.
+ datetime_date_default_time($date);
+ }
+ $date->setTimezone(new \DateTimeZone($timezone));
+ return $date;
+ }
+
+}
diff --git a/core/modules/datetime_range/src/Tests/DateRangeFieldTest.php b/core/modules/datetime_range/src/Tests/DateRangeFieldTest.php
new file mode 100644
index 0000000..078a67e
--- /dev/null
+++ b/core/modules/datetime_range/src/Tests/DateRangeFieldTest.php
@@ -0,0 +1,1432 @@
+<?php
+
+namespace Drupal\datetime_range\Tests;
+
+use Drupal\Component\Render\FormattableMarkup;
+use Drupal\Component\Utility\Unicode;
+use Drupal\Core\Datetime\DrupalDateTime;
+use Drupal\Core\Datetime\Entity\DateFormat;
+use Drupal\Core\Entity\Entity\EntityViewDisplay;
+use Drupal\datetime_range\Plugin\Field\FieldType\DateRangeItem;
+use Drupal\entity_test\Entity\EntityTest;
+use Drupal\field\Entity\FieldConfig;
+use Drupal\field\Entity\FieldStorageConfig;
+use Drupal\node\Entity\Node;
+use Drupal\simpletest\WebTestBase;
+
+/**
+ * Tests Daterange field functionality.
+ *
+ * @group datetime
+ */
+class DateRangeFieldTest extends WebTestBase {
+
+ /**
+ * Modules to enable.
+ *
+ * @var array
+ */
+ public static $modules = ['node', 'entity_test', 'datetime', 'datetime_range', 'field_ui'];
+
+ /**
+ * The default display settings to use for the formatters.
+ *
+ * @var array
+ */
+ protected $defaultSettings;
+
+ /**
+ * An array of display options to pass to entity_get_display()
+ *
+ * @var array
+ */
+ protected $displayOptions;
+
+ /**
+ * A field storage to use in this test class.
+ *
+ * @var \Drupal\field\Entity\FieldStorageConfig
+ */
+ protected $fieldStorage;
+
+ /**
+ * The field used in this test class.
+ *
+ * @var \Drupal\field\Entity\FieldConfig
+ */
+ protected $field;
+
+ /**
+ * An array of timezone extremes to test.
+ *
+ * @var string[]
+ */
+ protected static $timezones = [
+ // UTC-12, no DST.
+ 'Pacific/Kwajalein',
+ // UTC-11, no DST.
+ 'Pacific/Midway',
+ // UTC-7, no DST.
+ 'America/Phoenix',
+ // UTC.
+ 'UTC',
+ // UTC+5:30, no DST.
+ 'Asia/Kolkata',
+ // UTC+12, no DST.
+ 'Pacific/Funafuti',
+ // UTC+13, no DST.
+ 'Pacific/Tongatapu',
+ ];
+
+ /**
+ * The date formatter service.
+ *
+ * @var \Drupal\Core\Datetime\DateFormatterInterface
+ */
+ protected $dateFormatter;
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function setUp() {
+ parent::setUp();
+
+ $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' => ['datetime_type' => DateRangeItem::DATETIME_TYPE_DATE],
+ ]);
+ $this->fieldStorage->save();
+ $this->field = FieldConfig::create([
+ 'field_storage' => $this->fieldStorage,
+ 'bundle' => 'entity_test',
+ 'required' => TRUE,
+ ]);
+ $this->field->save();
+
+ entity_get_form_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'default')
+ ->setComponent($field_name, [
+ 'type' => 'daterange_default',
+ ])
+ ->save();
+
+ $this->defaultSettings = [
+ 'separator' => '-',
+ 'timezone_override' => '',
+ ];
+
+ $this->displayOptions = [
+ 'type' => 'daterange_default',
+ 'label' => 'hidden',
+ 'settings' => ['format_type' => 'medium'] + $this->defaultSettings,
+ ];
+ entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
+ ->setComponent($field_name, $this->displayOptions)
+ ->save();
+
+ $this->dateFormatter = \Drupal::service('date.formatter');
+ }
+
+ /**
+ * Tests date field functionality.
+ */
+ public function testDateRangeField() {
+ $field_name = $this->fieldStorage->getName();
+
+ // Loop through defined timezones to test that date-only fields work at the
+ // extremes.
+ foreach (static::$timezones as $timezone) {
+
+ $this->setSiteTimezone($timezone);
+
+ // Ensure field is set to a date-only field.
+ $this->fieldStorage->setSetting('datetime_type', DateRangeItem::DATETIME_TYPE_DATE);
+ $this->fieldStorage->save();
+
+ // Display creation form.
+ $this->drupalGet('entity_test/add');
+ $this->assertFieldByName("{$field_name}[0][value][date]", '', 'Start date element found.');
+ $this->assertFieldByName("{$field_name}[0][end_value][date]", '', 'End date element found.');
+ $this->assertFieldByXPath('//*[@id="edit-' . $field_name . '-wrapper"]/h4[contains(@class, "js-form-required")]', TRUE, 'Required markup found');
+ $this->assertNoFieldByName("{$field_name}[0][value][time]", '', 'Start time element not found.');
+ $this->assertNoFieldByName("{$field_name}[0][end_value][time]", '', 'End time element not found.');
+
+ // Build up dates in the UTC timezone.
+ $value = '2012-12-31 00:00:00';
+ $start_date = new DrupalDateTime($value, 'UTC');
+ $end_value = '2013-06-06 00:00:00';
+ $end_date = new DrupalDateTime($end_value, 'UTC');
+
+ // Submit a valid date and ensure it is accepted.
+ $date_format = DateFormat::load('html_date')->getPattern();
+ $time_format = DateFormat::load('html_time')->getPattern();
+
+ $edit = array(
+ "{$field_name}[0][value][date]" => $start_date->format($date_format),
+ "{$field_name}[0][end_value][date]" => $end_date->format($date_format),
+ );
+ $this->drupalPostForm(NULL, $edit, t('Save'));
+ preg_match('|entity_test/manage/(\d+)|', $this->url, $match);
+ $id = $match[1];
+ $this->assertText(t('entity_test @id has been created.', array('@id' => $id)));
+ $this->assertRaw($start_date->format($date_format));
+ $this->assertNoRaw($start_date->format($time_format));
+ $this->assertRaw($end_date->format($date_format));
+ $this->assertNoRaw($end_date->format($time_format));
+
+ // Verify the date doesn't change when entity is edited through the form.
+ $entity = EntityTest::load($id);
+ $this->assertEqual('2012-12-31', $entity->{$field_name}->value);
+ $this->assertEqual('2013-06-06', $entity->{$field_name}->end_value);
+ $this->drupalGet('entity_test/manage/' . $id . '/edit');
+ $this->drupalPostForm(NULL, [], t('Save'));
+ $this->drupalGet('entity_test/manage/' . $id . '/edit');
+ $this->drupalPostForm(NULL, [], t('Save'));
+ $this->drupalGet('entity_test/manage/' . $id . '/edit');
+ $this->drupalPostForm(NULL, [], t('Save'));
+ $entity = EntityTest::load($id);
+ $this->assertEqual('2012-12-31', $entity->{$field_name}->value);
+ $this->assertEqual('2013-06-06', $entity->{$field_name}->end_value);
+
+ // Formats that display a time component for date-only fields will display
+ // the default time, so that is applied before calculating the expected
+ // value.
+ datetime_date_default_time($start_date);
+ datetime_date_default_time($end_date);
+
+ // Reset display options since these get changed below.
+ $this->displayOptions = [
+ 'type' => 'daterange_default',
+ 'label' => 'hidden',
+ 'settings' => [
+ 'format_type' => 'long',
+ 'separator' => 'THESEPARATOR',
+ ] + $this->defaultSettings,
+ ];
+
+ // Verify that the default formatter works.
+ entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
+ ->setComponent($field_name, $this->displayOptions)
+ ->save();
+
+ $start_expected = $this->dateFormatter->format($start_date->getTimestamp(), 'long', '', DATETIME_STORAGE_TIMEZONE);
+ $start_expected_iso = $this->dateFormatter->format($start_date->getTimestamp(), 'custom', 'Y-m-d\TH:i:s\Z', DATETIME_STORAGE_TIMEZONE);
+ $end_expected = $this->dateFormatter->format($end_date->getTimestamp(), 'long', '', DATETIME_STORAGE_TIMEZONE);
+ $end_expected_iso = $this->dateFormatter->format($end_date->getTimestamp(), 'custom', 'Y-m-d\TH:i:s\Z', DATETIME_STORAGE_TIMEZONE);
+ $this->renderTestEntity($id);
+ $this->assertFieldByXPath('//time[@datetime="' . $start_expected_iso . '"]', $start_expected, new FormattableMarkup('Formatted date field using %value format displayed as %expected with %expected_iso attribute.', [
+ '%value' => 'long',
+ '%expected' => $start_expected,
+ '%expected_iso' => $start_expected_iso,
+ ]));
+ $this->assertFieldByXPath('//time[@datetime="' . $end_expected_iso . '"]', $end_expected, new FormattableMarkup('Formatted date field using %value format displayed as %expected with %expected_iso attribute.', [
+ '%value' => 'long',
+ '%expected' => $end_expected,
+ '%expected_iso' => $end_expected_iso,
+ ]));
+ $this->assertText(' THESEPARATOR ', 'Found proper separator');
+
+ // Verify that the plain formatter works.
+ $this->displayOptions['type'] = 'daterange_plain';
+ $this->displayOptions['settings'] = $this->defaultSettings;
+ entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
+ ->setComponent($field_name, $this->displayOptions)
+ ->save();
+ $expected = $start_date->format(DATETIME_DATE_STORAGE_FORMAT) . ' - ' . $end_date->format(DATETIME_DATE_STORAGE_FORMAT);
+ $this->renderTestEntity($id);
+ $this->assertText($expected, new FormattableMarkup('Formatted date field using plain format displayed as %expected.', array('%expected' => $expected)));
+
+ // Verify that the custom formatter works.
+ $this->displayOptions['type'] = 'daterange_custom';
+ $this->displayOptions['settings'] = array('date_format' => 'm/d/Y') + $this->defaultSettings;
+ entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
+ ->setComponent($field_name, $this->displayOptions)
+ ->save();
+ $expected = $start_date->format($this->displayOptions['settings']['date_format']) . ' - ' . $end_date->format($this->displayOptions['settings']['date_format']);
+ $this->renderTestEntity($id);
+ $this->assertText($expected, new FormattableMarkup('Formatted date field using daterange_custom format displayed as %expected.', array('%expected' => $expected)));
+
+ // Test formatters when start date and end date are the same
+ $this->drupalGet('entity_test/add');
+ $value = '2012-12-31 00:00:00';
+ $start_date = new DrupalDateTime($value, 'UTC');
+
+ $date_format = DateFormat::load('html_date')->getPattern();
+ $time_format = DateFormat::load('html_time')->getPattern();
+
+ $edit = array(
+ "{$field_name}[0][value][date]" => $start_date->format($date_format),
+ "{$field_name}[0][end_value][date]" => $start_date->format($date_format),
+ );
+
+ $this->drupalPostForm(NULL, $edit, t('Save'));
+ preg_match('|entity_test/manage/(\d+)|', $this->url, $match);
+ $id = $match[1];
+ $this->assertText(t('entity_test @id has been created.', array('@id' => $id)));
+
+ datetime_date_default_time($start_date);
+
+ $this->displayOptions = [
+ 'type' => 'daterange_default',
+ 'label' => 'hidden',
+ 'settings' => [
+ 'format_type' => 'long',
+ 'separator' => 'THESEPARATOR',
+ ] + $this->defaultSettings,
+ ];
+
+ entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
+ ->setComponent($field_name, $this->displayOptions)
+ ->save();
+
+ $start_expected = $this->dateFormatter->format($start_date->getTimestamp(), 'long', '', DATETIME_STORAGE_TIMEZONE);
+ $start_expected_iso = $this->dateFormatter->format($start_date->getTimestamp(), 'custom', 'Y-m-d\TH:i:s\Z', DATETIME_STORAGE_TIMEZONE);
+ $this->renderTestEntity($id);
+ $this->assertFieldByXPath('//time[@datetime="' . $start_expected_iso . '"]', $start_expected, new FormattableMarkup('Formatted date field using %value format displayed as %expected with %expected_iso attribute.', [
+ '%value' => 'long',
+ '%expected' => $start_expected,
+ '%expected_iso' => $start_expected_iso,
+ ]));
+ $this->assertNoText(' THESEPARATOR ', 'Separator not found on page');
+
+ $this->displayOptions['type'] = 'daterange_plain';
+ $this->displayOptions['settings'] = $this->defaultSettings;
+ entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
+ ->setComponent($field_name, $this->displayOptions)
+ ->save();
+ $expected = $start_date->format(DATETIME_DATE_STORAGE_FORMAT);
+ $this->renderTestEntity($id);
+ $this->assertText($expected, new FormattableMarkup('Formatted date field using plain format displayed as %expected.', array('%expected' => $expected)));
+ $this->assertNoText(' THESEPARATOR ', 'Separator not found on page');
+
+ $this->displayOptions['type'] = 'daterange_custom';
+ $this->displayOptions['settings'] = array('date_format' => 'm/d/Y') + $this->defaultSettings;
+ entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
+ ->setComponent($field_name, $this->displayOptions)
+ ->save();
+ $expected = $start_date->format($this->displayOptions['settings']['date_format']);
+ $this->renderTestEntity($id);
+ $this->assertText($expected, new FormattableMarkup('Formatted date field using daterange_custom format displayed as %expected.', array('%expected' => $expected)));
+ $this->assertNoText(' THESEPARATOR ', 'Separator not found on page');
+ }
+ }
+
+ /**
+ * Tests date and time field.
+ */
+ public function testDatetimeRangeField() {
+ $field_name = $this->fieldStorage->getName();
+
+ // Ensure the field to a datetime field.
+ $this->fieldStorage->setSetting('datetime_type', DateRangeItem::DATETIME_TYPE_DATETIME);
+ $this->fieldStorage->save();
+
+ // Display creation form.
+ $this->drupalGet('entity_test/add');
+ $this->assertFieldByName("{$field_name}[0][value][date]", '', 'Start date element found.');
+ $this->assertFieldByName("{$field_name}[0][value][time]", '', 'Start time element found.');
+ $this->assertFieldByName("{$field_name}[0][end_value][date]", '', 'End date element found.');
+ $this->assertFieldByName("{$field_name}[0][end_value][time]", '', 'End time element found.');
+
+ // Build up dates in the UTC timezone.
+ $value = '2012-12-31 00:00:00';
+ $start_date = new DrupalDateTime($value, 'UTC');
+ $end_value = '2013-06-06 00:00:00';
+ $end_date = new DrupalDateTime($end_value, 'UTC');
+
+ // Update the timezone to the system default.
+ $start_date->setTimezone(timezone_open(drupal_get_user_timezone()));
+ $end_date->setTimezone(timezone_open(drupal_get_user_timezone()));
+
+ // Submit a valid date and ensure it is accepted.
+ $date_format = DateFormat::load('html_date')->getPattern();
+ $time_format = DateFormat::load('html_time')->getPattern();
+
+ $edit = array(
+ "{$field_name}[0][value][date]" => $start_date->format($date_format),
+ "{$field_name}[0][value][time]" => $start_date->format($time_format),
+ "{$field_name}[0][end_value][date]" => $end_date->format($date_format),
+ "{$field_name}[0][end_value][time]" => $end_date->format($time_format),
+ );
+ $this->drupalPostForm(NULL, $edit, t('Save'));
+ preg_match('|entity_test/manage/(\d+)|', $this->url, $match);
+ $id = $match[1];
+ $this->assertText(t('entity_test @id has been created.', array('@id' => $id)));
+ $this->assertRaw($start_date->format($date_format));
+ $this->assertRaw($start_date->format($time_format));
+ $this->assertRaw($end_date->format($date_format));
+ $this->assertRaw($end_date->format($time_format));
+
+ // Verify that the default formatter works.
+ $this->displayOptions['settings'] = [
+ 'format_type' => 'long',
+ 'separator' => 'THESEPARATOR',
+ ] + $this->defaultSettings;
+ entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
+ ->setComponent($field_name, $this->displayOptions)
+ ->save();
+
+ $start_expected = $this->dateFormatter->format($start_date->getTimestamp(), 'long');
+ $start_expected_iso = $this->dateFormatter->format($start_date->getTimestamp(), 'custom', 'Y-m-d\TH:i:s\Z', 'UTC');
+ $end_expected = $this->dateFormatter->format($end_date->getTimestamp(), 'long');
+ $end_expected_iso = $this->dateFormatter->format($end_date->getTimestamp(), 'custom', 'Y-m-d\TH:i:s\Z', 'UTC');
+ $this->renderTestEntity($id);
+ $this->assertFieldByXPath('//time[@datetime="' . $start_expected_iso . '"]', $start_expected, new FormattableMarkup('Formatted date field using %value format displayed as %expected with %expected_iso attribute.', ['%value' => 'long', '%expected' => $start_expected, '%expected_iso' => $start_expected_iso]));
+ $this->assertFieldByXPath('//time[@datetime="' . $end_expected_iso . '"]', $end_expected, new FormattableMarkup('Formatted date field using %value format displayed as %expected with %expected_iso attribute.', ['%value' => 'long', '%expected' => $end_expected, '%expected_iso' => $end_expected_iso]));
+ $this->assertText(' THESEPARATOR ', 'Found proper separator');
+
+ // Verify that the plain formatter works.
+ $this->displayOptions['type'] = 'daterange_plain';
+ $this->displayOptions['settings'] = $this->defaultSettings;
+ entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
+ ->setComponent($field_name, $this->displayOptions)
+ ->save();
+ $expected = $start_date->format(DATETIME_DATETIME_STORAGE_FORMAT) . ' - ' . $end_date->format(DATETIME_DATETIME_STORAGE_FORMAT);
+ $this->renderTestEntity($id);
+ $this->assertText($expected, new FormattableMarkup('Formatted date field using plain format displayed as %expected.', array('%expected' => $expected)));
+
+ // Verify that the 'datetime_custom' formatter works.
+ $this->displayOptions['type'] = 'daterange_custom';
+ $this->displayOptions['settings'] = ['date_format' => 'm/d/Y g:i:s A'] + $this->defaultSettings;
+ entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
+ ->setComponent($field_name, $this->displayOptions)
+ ->save();
+ $expected = $start_date->format($this->displayOptions['settings']['date_format']) . ' - ' . $end_date->format($this->displayOptions['settings']['date_format']);
+ $this->renderTestEntity($id);
+ $this->assertText($expected, new FormattableMarkup('Formatted date field using daterange_custom format displayed as %expected.', array('%expected' => $expected)));
+
+ // Verify that the 'timezone_override' setting works.
+ $this->displayOptions['type'] = 'daterange_custom';
+ $this->displayOptions['settings'] = ['date_format' => 'm/d/Y g:i:s A', 'timezone_override' => 'America/New_York'] + $this->defaultSettings;
+ entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
+ ->setComponent($field_name, $this->displayOptions)
+ ->save();
+ $expected = $start_date->format($this->displayOptions['settings']['date_format'], ['timezone' => 'America/New_York']);
+ $expected .= ' - ' . $end_date->format($this->displayOptions['settings']['date_format'], ['timezone' => 'America/New_York']);
+ $this->renderTestEntity($id);
+ $this->assertText($expected, new FormattableMarkup('Formatted date field using daterange_custom format displayed as %expected.', array('%expected' => $expected)));
+
+ // Test formatters when start date and end date are the same
+ $this->drupalGet('entity_test/add');
+ $value = '2012-12-31 00:00:00';
+ $start_date = new DrupalDateTime($value, 'UTC');
+ $start_date->setTimezone(timezone_open(drupal_get_user_timezone()));
+
+ $date_format = DateFormat::load('html_date')->getPattern();
+ $time_format = DateFormat::load('html_time')->getPattern();
+
+ $edit = array(
+ "{$field_name}[0][value][date]" => $start_date->format($date_format),
+ "{$field_name}[0][value][time]" => $start_date->format($time_format),
+ "{$field_name}[0][end_value][date]" => $start_date->format($date_format),
+ "{$field_name}[0][end_value][time]" => $start_date->format($time_format),
+ );
+
+ $this->drupalPostForm(NULL, $edit, t('Save'));
+ preg_match('|entity_test/manage/(\d+)|', $this->url, $match);
+ $id = $match[1];
+ $this->assertText(t('entity_test @id has been created.', array('@id' => $id)));
+
+ $this->displayOptions = [
+ 'type' => 'daterange_default',
+ 'label' => 'hidden',
+ 'settings' => [
+ 'format_type' => 'long',
+ 'separator' => 'THESEPARATOR',
+ ] + $this->defaultSettings,
+ ];
+
+ entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
+ ->setComponent($field_name, $this->displayOptions)
+ ->save();
+
+ $start_expected = $this->dateFormatter->format($start_date->getTimestamp(), 'long');
+ $start_expected_iso = $this->dateFormatter->format($start_date->getTimestamp(), 'custom', 'Y-m-d\TH:i:s\Z', 'UTC');
+ $this->renderTestEntity($id);
+ $this->assertFieldByXPath('//time[@datetime="' . $start_expected_iso . '"]', $start_expected, new FormattableMarkup('Formatted date field using %value format displayed as %expected with %expected_iso attribute.', ['%value' => 'long', '%expected' => $start_expected, '%expected_iso' => $start_expected_iso]));
+ $this->assertNoText(' THESEPARATOR ', 'Separator not found on page');
+
+ $this->displayOptions['type'] = 'daterange_plain';
+ $this->displayOptions['settings'] = $this->defaultSettings;
+ entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
+ ->setComponent($field_name, $this->displayOptions)
+ ->save();
+ $expected = $start_date->format(DATETIME_DATETIME_STORAGE_FORMAT);
+ $this->renderTestEntity($id);
+ $this->assertText($expected, new FormattableMarkup('Formatted date field using plain format displayed as %expected.', array('%expected' => $expected)));
+ $this->assertNoText(' THESEPARATOR ', 'Separator not found on page');
+
+ $this->displayOptions['type'] = 'daterange_custom';
+ $this->displayOptions['settings'] = ['date_format' => 'm/d/Y g:i:s A'] + $this->defaultSettings;
+ entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
+ ->setComponent($field_name, $this->displayOptions)
+ ->save();
+ $expected = $start_date->format($this->displayOptions['settings']['date_format']);
+ $this->renderTestEntity($id);
+ $this->assertText($expected, new FormattableMarkup('Formatted date field using daterange_custom format displayed as %expected.', array('%expected' => $expected)));
+ $this->assertNoText(' THESEPARATOR ', 'Separator not found on page');
+ }
+
+ /**
+ * Tests all-day field.
+ */
+ public function testAlldayRangeField() {
+ $field_name = $this->fieldStorage->getName();
+
+ // Ensure field is set to a all-day field.
+ $this->fieldStorage->setSetting('datetime_type', DateRangeItem::DATETIME_TYPE_ALLDAY);
+ $this->fieldStorage->save();
+
+ // Display creation form.
+ $this->drupalGet('entity_test/add');
+ $this->assertFieldByName("{$field_name}[0][value][date]", '', 'Start date element found.');
+ $this->assertFieldByName("{$field_name}[0][end_value][date]", '', 'End date element found.');
+ $this->assertFieldByXPath('//*[@id="edit-' . $field_name . '-wrapper"]/h4[contains(@class, "js-form-required")]', TRUE, 'Required markup found');
+ $this->assertNoFieldByName("{$field_name}[0][value][time]", '', 'Start time element not found.');
+ $this->assertNoFieldByName("{$field_name}[0][end_value][time]", '', 'End time element not found.');
+
+ // Build up dates in the proper timezone.
+ $value = '2012-12-31 00:00:00';
+ $start_date = new DrupalDateTime($value, timezone_open(drupal_get_user_timezone()));
+ $end_value = '2013-06-06 23:59:59';
+ $end_date = new DrupalDateTime($end_value, timezone_open(drupal_get_user_timezone()));
+
+ // Submit a valid date and ensure it is accepted.
+ $date_format = DateFormat::load('html_date')->getPattern();
+ $time_format = DateFormat::load('html_time')->getPattern();
+
+ $edit = array(
+ "{$field_name}[0][value][date]" => $start_date->format($date_format),
+ "{$field_name}[0][end_value][date]" => $end_date->format($date_format),
+ );
+ $this->drupalPostForm(NULL, $edit, t('Save'));
+ preg_match('|entity_test/manage/(\d+)|', $this->url, $match);
+ $id = $match[1];
+ $this->assertText(t('entity_test @id has been created.', array('@id' => $id)));
+ $this->assertRaw($start_date->format($date_format));
+ $this->assertNoRaw($start_date->format($time_format));
+ $this->assertRaw($end_date->format($date_format));
+ $this->assertNoRaw($end_date->format($time_format));
+
+ // Verify that the default formatter works.
+ $this->displayOptions['settings'] = [
+ 'format_type' => 'long',
+ 'separator' => 'THESEPARATOR',
+ ] + $this->defaultSettings;
+ entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
+ ->setComponent($field_name, $this->displayOptions)
+ ->save();
+
+ $start_expected = $this->dateFormatter->format($start_date->getTimestamp(), 'long');
+ $start_expected_iso = $this->dateFormatter->format($start_date->getTimestamp(), 'custom', 'Y-m-d\TH:i:s\Z', 'UTC');
+ $end_expected = $this->dateFormatter->format($end_date->getTimestamp(), 'long');
+ $end_expected_iso = $this->dateFormatter->format($end_date->getTimestamp(), 'custom', 'Y-m-d\TH:i:s\Z', 'UTC');
+ $this->renderTestEntity($id);
+ $this->assertFieldByXPath('//time[@datetime="' . $start_expected_iso . '"]', $start_expected, new FormattableMarkup('Formatted date field using %value format displayed as %expected with %expected_iso attribute.', ['%value' => 'long', '%expected' => $start_expected, '%expected_iso' => $start_expected_iso]));
+ $this->assertFieldByXPath('//time[@datetime="' . $end_expected_iso . '"]', $end_expected, new FormattableMarkup('Formatted date field using %value format displayed as %expected with %expected_iso attribute.', ['%value' => 'long', '%expected' => $end_expected, '%expected_iso' => $end_expected_iso]));
+ $this->assertText(' THESEPARATOR ', 'Found proper separator');
+
+ // Verify that the plain formatter works.
+ $this->displayOptions['type'] = 'daterange_plain';
+ $this->displayOptions['settings'] = $this->defaultSettings;
+ entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
+ ->setComponent($field_name, $this->displayOptions)
+ ->save();
+ $expected = $start_date->format(DATETIME_DATETIME_STORAGE_FORMAT) . ' - ' . $end_date->format(DATETIME_DATETIME_STORAGE_FORMAT);
+ $this->renderTestEntity($id);
+ $this->assertText($expected, new FormattableMarkup('Formatted date field using plain format displayed as %expected.', array('%expected' => $expected)));
+
+ // Verify that the custom formatter works.
+ $this->displayOptions['type'] = 'daterange_custom';
+ $this->displayOptions['settings'] = array('date_format' => 'm/d/Y') + $this->defaultSettings;
+ entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
+ ->setComponent($field_name, $this->displayOptions)
+ ->save();
+ $expected = $start_date->format($this->displayOptions['settings']['date_format']) . ' - ' . $end_date->format($this->displayOptions['settings']['date_format']);
+ $this->renderTestEntity($id);
+ $this->assertText($expected, new FormattableMarkup('Formatted date field using daterange_custom format displayed as %expected.', array('%expected' => $expected)));
+
+ // Verify that the 'timezone_override' setting works.
+ $this->displayOptions['type'] = 'daterange_custom';
+ $this->displayOptions['settings'] = ['date_format' => 'm/d/Y g:i:s A', 'timezone_override' => 'America/New_York'] + $this->defaultSettings;
+ entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
+ ->setComponent($field_name, $this->displayOptions)
+ ->save();
+ $expected = $start_date->format($this->displayOptions['settings']['date_format'], ['timezone' => 'America/New_York']);
+ $expected .= ' - ' . $end_date->format($this->displayOptions['settings']['date_format'], ['timezone' => 'America/New_York']);
+ $this->renderTestEntity($id);
+ $this->assertText($expected, new FormattableMarkup('Formatted date field using daterange_custom format displayed as %expected.', array('%expected' => $expected)));
+
+ // Test formatters when start date and end date are the same
+ $this->drupalGet('entity_test/add');
+
+ $value = '2012-12-31 00:00:00';
+ $start_date = new DrupalDateTime($value, timezone_open(drupal_get_user_timezone()));
+ $end_value = '2012-12-31 23:59:59';
+ $end_date = new DrupalDateTime($end_value, timezone_open(drupal_get_user_timezone()));
+
+ $date_format = DateFormat::load('html_date')->getPattern();
+ $time_format = DateFormat::load('html_time')->getPattern();
+
+ $edit = array(
+ "{$field_name}[0][value][date]" => $start_date->format($date_format),
+ "{$field_name}[0][end_value][date]" => $start_date->format($date_format),
+ );
+ $this->drupalPostForm(NULL, $edit, t('Save'));
+ preg_match('|entity_test/manage/(\d+)|', $this->url, $match);
+ $id = $match[1];
+ $this->assertText(t('entity_test @id has been created.', array('@id' => $id)));
+
+ $this->displayOptions = [
+ 'type' => 'daterange_default',
+ 'label' => 'hidden',
+ 'settings' => [
+ 'format_type' => 'long',
+ 'separator' => 'THESEPARATOR',
+ ] + $this->defaultSettings,
+ ];
+
+ entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
+ ->setComponent($field_name, $this->displayOptions)
+ ->save();
+
+ $start_expected = $this->dateFormatter->format($start_date->getTimestamp(), 'long');
+ $start_expected_iso = $this->dateFormatter->format($start_date->getTimestamp(), 'custom', 'Y-m-d\TH:i:s\Z', 'UTC');
+ $end_expected = $this->dateFormatter->format($end_date->getTimestamp(), 'long');
+ $end_expected_iso = $this->dateFormatter->format($end_date->getTimestamp(), 'custom', 'Y-m-d\TH:i:s\Z', 'UTC');
+ $this->renderTestEntity($id);
+ $this->assertFieldByXPath('//time[@datetime="' . $start_expected_iso . '"]', $start_expected, new FormattableMarkup('Formatted date field using %value format displayed as %expected with %expected_iso attribute.', ['%value' => 'long', '%expected' => $start_expected, '%expected_iso' => $start_expected_iso]));
+ $this->assertFieldByXPath('//time[@datetime="' . $end_expected_iso . '"]', $end_expected, new FormattableMarkup('Formatted date field using %value format displayed as %expected with %expected_iso attribute.', ['%value' => 'long', '%expected' => $end_expected, '%expected_iso' => $end_expected_iso]));
+ $this->assertText(' THESEPARATOR ', 'Found proper separator');
+
+ $this->displayOptions['type'] = 'daterange_plain';
+ entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
+ ->setComponent($field_name, $this->displayOptions)
+ ->save();
+ $expected = $start_date->format(DATETIME_DATETIME_STORAGE_FORMAT) . ' THESEPARATOR ' . $end_date->format(DATETIME_DATETIME_STORAGE_FORMAT);
+ $this->renderTestEntity($id);
+ $this->assertText($expected, new FormattableMarkup('Formatted date field using plain format displayed as %expected.', array('%expected' => $expected)));
+ $this->assertText(' THESEPARATOR ', 'Found proper separator');
+
+ $this->displayOptions['type'] = 'daterange_custom';
+ $this->displayOptions['settings']['date_format'] = 'm/d/Y';
+ entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
+ ->setComponent($field_name, $this->displayOptions)
+ ->save();
+ $expected = $start_date->format($this->displayOptions['settings']['date_format']) . ' THESEPARATOR ' . $end_date->format($this->displayOptions['settings']['date_format']);
+ $this->renderTestEntity($id);
+ $this->assertText($expected, new FormattableMarkup('Formatted date field using daterange_custom format displayed as %expected.', array('%expected' => $expected)));
+ $this->assertText(' THESEPARATOR ', 'Found proper separator');
+
+ }
+
+ /**
+ * Tests Date Range List Widget functionality.
+ */
+ public function testDatelistWidget() {
+ $field_name = $this->fieldStorage->getName();
+
+ // Ensure field is set to a date only field.
+ $this->fieldStorage->setSetting('datetime_type', DateRangeItem::DATETIME_TYPE_DATE);
+ $this->fieldStorage->save();
+
+ // Change the widget to a datelist widget.
+ entity_get_form_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'default')
+ ->setComponent($field_name, [
+ 'type' => 'daterange_datelist',
+ 'settings' => [
+ 'date_order' => 'YMD',
+ ],
+ ])
+ ->save();
+ \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
+
+ // Display creation form.
+ $this->drupalGet('entity_test/add');
+
+ // Assert that Hour and Minute Elements do not appear on Date Only.
+ $this->assertNoFieldByXPath("//*[@id=\"edit-$field_name-0-value-hour\"]", NULL, 'Hour element not found on Date Only.');
+ $this->assertNoFieldByXPath("//*[@id=\"edit-$field_name-0-value-minute\"]", NULL, 'Minute element not found on Date Only.');
+ $this->assertNoFieldByXPath("//*[@id=\"edit-$field_name-0-end-value-hour\"]", NULL, 'Hour element not found on Date Only.');
+ $this->assertNoFieldByXPath("//*[@id=\"edit-$field_name-0-end-value-minute\"]", NULL, 'Minute element not found on Date Only.');
+
+ // Go to the form display page to assert that increment option does not
+ // appear on Date Only.
+ $fieldEditUrl = 'entity_test/structure/entity_test/form-display';
+ $this->drupalGet($fieldEditUrl);
+
+ // Click on the widget settings button to open the widget settings form.
+ $this->drupalPostAjaxForm(NULL, [], $field_name . "_settings_edit");
+ $xpathIncr = "//select[starts-with(@id, \"edit-fields-$field_name-settings-edit-form-settings-increment\")]";
+ $this->assertNoFieldByXPath($xpathIncr, NULL, 'Increment element not found for Date Only.');
+
+ // Change the field is set to an all day field.
+ $this->fieldStorage->setSetting('datetime_type', DateRangeItem::DATETIME_TYPE_ALLDAY);
+ $this->fieldStorage->save();
+
+ // Change the widget to a datelist widget.
+ entity_get_form_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'default')
+ ->setComponent($field_name, [
+ 'type' => 'daterange_datelist',
+ 'settings' => [
+ 'date_order' => 'YMD',
+ ],
+ ])
+ ->save();
+ \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
+
+ // Display creation form.
+ $this->drupalGet('entity_test/add');
+
+ // Assert that Hour and Minute Elements do not appear on Date Only.
+ $this->assertNoFieldByXPath("//*[@id=\"edit-$field_name-0-value-hour\"]", NULL, 'Hour element not found on Date Only.');
+ $this->assertNoFieldByXPath("//*[@id=\"edit-$field_name-0-value-minute\"]", NULL, 'Minute element not found on Date Only.');
+ $this->assertNoFieldByXPath("//*[@id=\"edit-$field_name-0-end-value-hour\"]", NULL, 'Hour element not found on Date Only.');
+ $this->assertNoFieldByXPath("//*[@id=\"edit-$field_name-0-end-value-minute\"]", NULL, 'Minute element not found on Date Only.');
+
+ // Go to the form display page to assert that increment option does not
+ // appear on Date Only.
+ $fieldEditUrl = 'entity_test/structure/entity_test/form-display';
+ $this->drupalGet($fieldEditUrl);
+
+ // Click on the widget settings button to open the widget settings form.
+ $this->drupalPostAjaxForm(NULL, [], $field_name . "_settings_edit");
+ $xpathIncr = "//select[starts-with(@id, \"edit-fields-$field_name-settings-edit-form-settings-increment\")]";
+ $this->assertNoFieldByXPath($xpathIncr, NULL, 'Increment element not found for Date Only.');
+
+ // Change the field to a datetime field.
+ $this->fieldStorage->setSetting('datetime_type', DateRangeItem::DATETIME_TYPE_DATETIME);
+ $this->fieldStorage->save();
+
+ // Change the widget to a datelist widget.
+ entity_get_form_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'default')
+ ->setComponent($field_name, [
+ 'type' => 'daterange_datelist',
+ 'settings' => [
+ 'increment' => 1,
+ 'date_order' => 'YMD',
+ 'time_type' => '12',
+ ],
+ ])
+ ->save();
+ \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
+
+ // Go to the form display page to assert that increment option does appear
+ // on Date Time.
+ $fieldEditUrl = 'entity_test/structure/entity_test/form-display';
+ $this->drupalGet($fieldEditUrl);
+
+ // Click on the widget settings button to open the widget settings form.
+ $this->drupalPostAjaxForm(NULL, [], $field_name . "_settings_edit");
+ $this->assertFieldByXPath($xpathIncr, NULL, 'Increment element found for Date and time.');
+
+ // Display creation form.
+ $this->drupalGet('entity_test/add');
+
+ foreach (['value', 'end-value'] as $column) {
+ foreach (['year', 'month', 'day', 'hour', 'minute', 'ampm'] as $element) {
+ $this->assertFieldByXPath("//*[@id=\"edit-$field_name-0-$column-$element\"]", NULL, $element . ' element found.');
+ $this->assertOptionSelected("edit-$field_name-0-$column-$element", '', 'No ' . $element . ' selected.');
+ }
+ }
+
+ // Submit a valid date and ensure it is accepted.
+ $start_date_value = ['year' => 2012, 'month' => 12, 'day' => 31, 'hour' => 5, 'minute' => 15];
+ $end_date_value = ['year' => 2013, 'month' => 1, 'day' => 15, 'hour' => 3, 'minute' => 30];
+
+ $edit = [];
+ // Add the ampm indicator since we are testing 12 hour time.
+ $start_date_value['ampm'] = 'am';
+ $end_date_value['ampm'] = 'pm';
+ foreach ($start_date_value as $part => $value) {
+ $edit["{$field_name}[0][value][$part]"] = $value;
+ }
+ foreach ($end_date_value as $part => $value) {
+ $edit["{$field_name}[0][end_value][$part]"] = $value;
+ }
+
+ $this->drupalPostForm(NULL, $edit, t('Save'));
+ preg_match('|entity_test/manage/(\d+)|', $this->url, $match);
+ $id = $match[1];
+ $this->assertText(t('entity_test @id has been created.', ['@id' => $id]));
+
+ $this->assertOptionSelected("edit-$field_name-0-value-year", '2012', 'Correct year selected.');
+ $this->assertOptionSelected("edit-$field_name-0-value-month", '12', 'Correct month selected.');
+ $this->assertOptionSelected("edit-$field_name-0-value-day", '31', 'Correct day selected.');
+ $this->assertOptionSelected("edit-$field_name-0-value-hour", '5', 'Correct hour selected.');
+ $this->assertOptionSelected("edit-$field_name-0-value-minute", '15', 'Correct minute selected.');
+ $this->assertOptionSelected("edit-$field_name-0-value-ampm", 'am', 'Correct ampm selected.');
+
+ $this->assertOptionSelected("edit-$field_name-0-end-value-year", '2013', 'Correct year selected.');
+ $this->assertOptionSelected("edit-$field_name-0-end-value-month", '1', 'Correct month selected.');
+ $this->assertOptionSelected("edit-$field_name-0-end-value-day", '15', 'Correct day selected.');
+ $this->assertOptionSelected("edit-$field_name-0-end-value-hour", '3', 'Correct hour selected.');
+ $this->assertOptionSelected("edit-$field_name-0-end-value-minute", '30', 'Correct minute selected.');
+ $this->assertOptionSelected("edit-$field_name-0-end-value-ampm", 'pm', 'Correct ampm selected.');
+
+ // Test the widget using increment other than 1 and 24 hour mode.
+ entity_get_form_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'default')
+ ->setComponent($field_name, [
+ 'type' => 'daterange_datelist',
+ 'settings' => [
+ 'increment' => 15,
+ 'date_order' => 'YMD',
+ 'time_type' => '24',
+ ],
+ ])
+ ->save();
+ \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
+
+ // Display creation form.
+ $this->drupalGet('entity_test/add');
+
+ // Other elements are unaffected by the changed settings.
+ $this->assertFieldByXPath("//*[@id=\"edit-$field_name-0-value-hour\"]", NULL, 'Hour element found.');
+ $this->assertOptionSelected("edit-$field_name-0-value-hour", '', 'No hour selected.');
+ $this->assertNoFieldByXPath("//*[@id=\"edit-$field_name-0-value-ampm\"]", NULL, 'AMPM element not found.');
+ $this->assertFieldByXPath("//*[@id=\"edit-$field_name-0-end-value-hour\"]", NULL, 'Hour element found.');
+ $this->assertOptionSelected("edit-$field_name-0-end-value-hour", '', 'No hour selected.');
+ $this->assertNoFieldByXPath("//*[@id=\"edit-$field_name-0-end-value-ampm\"]", NULL, 'AMPM element not found.');
+
+ // Submit a valid date and ensure it is accepted.
+ $start_date_value = ['year' => 2012, 'month' => 12, 'day' => 31, 'hour' => 17, 'minute' => 15];
+ $end_date_value = ['year' => 2013, 'month' => 1, 'day' => 15, 'hour' => 3, 'minute' => 30];
+
+ $edit = [];
+ foreach ($start_date_value as $part => $value) {
+ $edit["{$field_name}[0][value][$part]"] = $value;
+ }
+ foreach ($end_date_value as $part => $value) {
+ $edit["{$field_name}[0][end_value][$part]"] = $value;
+ }
+
+ $this->drupalPostForm(NULL, $edit, t('Save'));
+ preg_match('|entity_test/manage/(\d+)|', $this->url, $match);
+ $id = $match[1];
+ $this->assertText(t('entity_test @id has been created.', ['@id' => $id]));
+
+ $this->assertOptionSelected("edit-$field_name-0-value-year", '2012', 'Correct year selected.');
+ $this->assertOptionSelected("edit-$field_name-0-value-month", '12', 'Correct month selected.');
+ $this->assertOptionSelected("edit-$field_name-0-value-day", '31', 'Correct day selected.');
+ $this->assertOptionSelected("edit-$field_name-0-value-hour", '17', 'Correct hour selected.');
+ $this->assertOptionSelected("edit-$field_name-0-value-minute", '15', 'Correct minute selected.');
+
+ $this->assertOptionSelected("edit-$field_name-0-end-value-year", '2013', 'Correct year selected.');
+ $this->assertOptionSelected("edit-$field_name-0-end-value-month", '1', 'Correct month selected.');
+ $this->assertOptionSelected("edit-$field_name-0-end-value-day", '15', 'Correct day selected.');
+ $this->assertOptionSelected("edit-$field_name-0-end-value-hour", '3', 'Correct hour selected.');
+ $this->assertOptionSelected("edit-$field_name-0-end-value-minute", '30', 'Correct minute selected.');
+
+ // Test the widget for partial completion of fields.
+ entity_get_form_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'default')
+ ->setComponent($field_name, [
+ 'type' => 'daterange_datelist',
+ 'settings' => [
+ 'increment' => 1,
+ 'date_order' => 'YMD',
+ 'time_type' => '24',
+ ],
+ ])
+ ->save();
+ \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
+
+ // Test the widget for validation notifications.
+ foreach ($this->datelistDataProvider() as $data) {
+ list($start_date_value, $end_date_value, $expected) = $data;
+
+ // Display creation form.
+ $this->drupalGet('entity_test/add');
+
+ // Submit a partial date and ensure and error message is provided.
+ $edit = [];
+ foreach ($start_date_value as $part => $value) {
+ $edit["{$field_name}[0][value][$part]"] = $value;
+ }
+ foreach ($end_date_value as $part => $value) {
+ $edit["{$field_name}[0][end_value][$part]"] = $value;
+ }
+
+ $this->drupalPostForm(NULL, $edit, t('Save'));
+ $this->assertResponse(200);
+ foreach ($expected as $expected_text) {
+ $this->assertText(t($expected_text));
+ }
+ }
+
+ // Test the widget for complete input with zeros as part of selections.
+ $this->drupalGet('entity_test/add');
+
+ $start_date_value = ['year' => 2012, 'month' => 12, 'day' => 31, 'hour' => 0, 'minute' => 0];
+ $end_date_value = ['year' => 2013, 'month' => 1, 'day' => 15, 'hour' => 3, 'minute' => 30];
+ $edit = [];
+ foreach ($start_date_value as $part => $value) {
+ $edit["{$field_name}[0][value][$part]"] = $value;
+ }
+ foreach ($end_date_value as $part => $value) {
+ $edit["{$field_name}[0][end_value][$part]"] = $value;
+ }
+
+ $this->drupalPostForm(NULL, $edit, t('Save'));
+ $this->assertResponse(200);
+ preg_match('|entity_test/manage/(\d+)|', $this->url, $match);
+ $id = $match[1];
+ $this->assertText(t('entity_test @id has been created.', ['@id' => $id]));
+
+ // Test the widget to ensure zeros are not deselected on validation.
+ $this->drupalGet('entity_test/add');
+
+ $start_date_value = ['year' => 2012, 'month' => 12, 'day' => 31, 'hour' => 0, 'minute' => 0];
+ $end_date_value = ['year' => 2013, 'month' => 1, 'day' => 15, 'hour' => 3, 'minute' => 0];
+ $edit = [];
+ foreach ($start_date_value as $part => $value) {
+ $edit["{$field_name}[0][value][$part]"] = $value;
+ }
+ foreach ($end_date_value as $part => $value) {
+ $edit["{$field_name}[0][end_value][$part]"] = $value;
+ }
+
+ $this->drupalPostForm(NULL, $edit, t('Save'));
+ $this->assertResponse(200);
+ $this->assertOptionSelected("edit-$field_name-0-value-minute", '0', 'Correct minute selected.');
+ $this->assertOptionSelected("edit-$field_name-0-end-value-minute", '0', 'Correct minute selected.');
+ }
+
+ /**
+ * The data provider for testing the validation of the datelist widget.
+ *
+ * @return array
+ * An array of datelist input permutations to test.
+ */
+ protected function datelistDataProvider() {
+ return [
+ // Year only selected, validation error on Month, Day, Hour, Minute.
+ [
+ ['year' => 2012, 'month' => '', 'day' => '', 'hour' => '', 'minute' => ''],
+ ['year' => 2013, 'month' => '1', 'day' => '15', 'hour' => '3', 'minute' => '30'], [
+ 'A value must be selected for month.',
+ 'A value must be selected for day.',
+ 'A value must be selected for hour.',
+ 'A value must be selected for minute.',
+ ],
+ ],
+ // Year and Month selected, validation error on Day, Hour, Minute.
+ [
+ ['year' => 2012, 'month' => '12', 'day' => '', 'hour' => '', 'minute' => ''],
+ ['year' => 2013, 'month' => '1', 'day' => '15', 'hour' => '3', 'minute' => '30'], [
+ 'A value must be selected for day.',
+ 'A value must be selected for hour.',
+ 'A value must be selected for minute.',
+ ],
+ ],
+ // Year, Month and Day selected, validation error on Hour, Minute.
+ [
+ ['year' => 2012, 'month' => '12', 'day' => '31', 'hour' => '', 'minute' => ''],
+ ['year' => 2013, 'month' => '1', 'day' => '15', 'hour' => '3', 'minute' => '30'], [
+ 'A value must be selected for hour.',
+ 'A value must be selected for minute.',
+ ],
+ ],
+ // Year, Month, Day and Hour selected, validation error on Minute only.
+ [
+ ['year' => 2012, 'month' => '12', 'day' => '31', 'hour' => '0', 'minute' => ''],
+ ['year' => 2013, 'month' => '1', 'day' => '15', 'hour' => '3', 'minute' => '30'], [
+ 'A value must be selected for minute.',
+ ],
+ ],
+ // Year selected, validation error on Month, Day, Hour, Minute.
+ [
+ ['year' => 2012, 'month' => '12', 'day' => '31', 'hour' => '0', 'minute' => '0'],
+ ['year' => 2013, 'month' => '', 'day' => '', 'hour' => '', 'minute' => ''], [
+ 'A value must be selected for month.',
+ 'A value must be selected for day.',
+ 'A value must be selected for hour.',
+ 'A value must be selected for minute.',
+ ],
+ ],
+ // Year and Month selected, validation error on Day, Hour, Minute.
+ [
+ ['year' => 2012, 'month' => '12', 'day' => '31', 'hour' => '0', 'minute' => '0'],
+ ['year' => 2013, 'month' => '1', 'day' => '', 'hour' => '', 'minute' => ''], [
+ 'A value must be selected for day.',
+ 'A value must be selected for hour.',
+ 'A value must be selected for minute.',
+ ],
+ ],
+ // Year, Month and Day selected, validation error on Hour, Minute.
+ [
+ ['year' => 2012, 'month' => '12', 'day' => '31', 'hour' => '0', 'minute' => '0'],
+ ['year' => 2013, 'month' => '1', 'day' => '15', 'hour' => '', 'minute' => ''], [
+ 'A value must be selected for hour.',
+ 'A value must be selected for minute.',
+ ],
+ ],
+ // Year, Month, Day and Hour selected, validation error on Minute only.
+ [
+ ['year' => 2012, 'month' => '12', 'day' => '31', 'hour' => '0', 'minute' => '0'],
+ ['year' => 2013, 'month' => '1', 'day' => '15', 'hour' => '3', 'minute' => ''], [
+ 'A value must be selected for minute.',
+ ],
+ ],
+ ];
+ }
+
+ /**
+ * Test default value functionality.
+ */
+ public function testDefaultValue() {
+ // Create a test content type.
+ $this->drupalCreateContentType(['type' => 'date_content']);
+
+ // Create a field storage with settings to validate.
+ $field_name = Unicode::strtolower($this->randomMachineName());
+ $field_storage = FieldStorageConfig::create([
+ 'field_name' => $field_name,
+ 'entity_type' => 'node',
+ 'type' => 'daterange',
+ 'settings' => ['datetime_type' => DateRangeItem::DATETIME_TYPE_DATE],
+ ]);
+ $field_storage->save();
+
+ $field = FieldConfig::create([
+ 'field_storage' => $field_storage,
+ 'bundle' => 'date_content',
+ ]);
+ $field->save();
+
+ // Set now as default_value.
+ $field_edit = [
+ 'default_value_input[default_date_type]' => 'now',
+ 'default_value_input[default_end_date_type]' => 'now',
+ ];
+ $this->drupalPostForm('admin/structure/types/manage/date_content/fields/node.date_content.' . $field_name, $field_edit, t('Save settings'));
+
+ // Check that default value is selected in default value form.
+ $this->drupalGet('admin/structure/types/manage/date_content/fields/node.date_content.' . $field_name);
+ $this->assertOptionSelected('edit-default-value-input-default-date-type', 'now', 'The default start value is selected in instance settings page');
+ $this->assertFieldByName('default_value_input[default_date]', '', 'The relative start default value is empty in instance settings page');
+ $this->assertOptionSelected('edit-default-value-input-default-end-date-type', 'now', 'The default end value is selected in instance settings page');
+ $this->assertFieldByName('default_value_input[default_end_date]', '', 'The relative end default value is empty in instance settings page');
+
+ // Check if default_date has been stored successfully.
+ $config_entity = $this->config('field.field.node.date_content.' . $field_name)->get();
+ $this->assertEqual($config_entity['default_value'][0], [
+ 'default_date_type' => 'now',
+ 'default_date' => 'now',
+ 'default_end_date_type' => 'now',
+ 'default_end_date' => 'now',
+ ], 'Default value has been stored successfully');
+
+ // Clear field cache in order to avoid stale cache values.
+ \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
+
+ // Create a new node to check that datetime field default value is today.
+ $new_node = Node::create(['type' => 'date_content']);
+ $expected_date = new DrupalDateTime('now', DATETIME_STORAGE_TIMEZONE);
+ $this->assertEqual($new_node->get($field_name)->offsetGet(0)->value, $expected_date->format(DATETIME_DATE_STORAGE_FORMAT));
+ $this->assertEqual($new_node->get($field_name)->offsetGet(0)->end_value, $expected_date->format(DATETIME_DATE_STORAGE_FORMAT));
+
+ // Set an invalid relative default_value to test validation.
+ $field_edit = [
+ 'default_value_input[default_date_type]' => 'relative',
+ 'default_value_input[default_date]' => 'invalid date',
+ 'default_value_input[default_end_date_type]' => 'relative',
+ 'default_value_input[default_end_date]' => '+1 day',
+ ];
+ $this->drupalPostForm('admin/structure/types/manage/date_content/fields/node.date_content.' . $field_name, $field_edit, t('Save settings'));
+ $this->assertText('The relative start date value entered is invalid.');
+
+ $field_edit = [
+ 'default_value_input[default_date_type]' => 'relative',
+ 'default_value_input[default_date]' => '+1 day',
+ 'default_value_input[default_end_date_type]' => 'relative',
+ 'default_value_input[default_end_date]' => 'invalid date',
+ ];
+ $this->drupalPostForm('admin/structure/types/manage/date_content/fields/node.date_content.' . $field_name, $field_edit, t('Save settings'));
+ $this->assertText('The relative end date value entered is invalid.');
+
+ // Set a relative default_value.
+ $field_edit = [
+ 'default_value_input[default_date_type]' => 'relative',
+ 'default_value_input[default_date]' => '+45 days',
+ 'default_value_input[default_end_date_type]' => 'relative',
+ 'default_value_input[default_end_date]' => '+90 days',
+ ];
+ $this->drupalPostForm('admin/structure/types/manage/date_content/fields/node.date_content.' . $field_name, $field_edit, t('Save settings'));
+
+ // Check that default value is selected in default value form.
+ $this->drupalGet('admin/structure/types/manage/date_content/fields/node.date_content.' . $field_name);
+ $this->assertOptionSelected('edit-default-value-input-default-date-type', 'relative', 'The default start value is selected in instance settings page');
+ $this->assertFieldByName('default_value_input[default_date]', '+45 days', 'The relative default start value is displayed in instance settings page');
+ $this->assertOptionSelected('edit-default-value-input-default-end-date-type', 'relative', 'The default end value is selected in instance settings page');
+ $this->assertFieldByName('default_value_input[default_end_date]', '+90 days', 'The relative default end value is displayed in instance settings page');
+
+ // Check if default_date has been stored successfully.
+ $config_entity = $this->config('field.field.node.date_content.' . $field_name)->get();
+ $this->assertEqual($config_entity['default_value'][0], [
+ 'default_date_type' => 'relative',
+ 'default_date' => '+45 days',
+ 'default_end_date_type' => 'relative',
+ 'default_end_date' => '+90 days',
+ ], 'Default value has been stored successfully');
+
+ // Clear field cache in order to avoid stale cache values.
+ \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
+
+ // Create a new node to check that datetime field default value is +90 days.
+ $new_node = Node::create(['type' => 'date_content']);
+ $expected_start_date = new DrupalDateTime('+45 days', DATETIME_STORAGE_TIMEZONE);
+ $expected_end_date = new DrupalDateTime('+90 days', DATETIME_STORAGE_TIMEZONE);
+ $this->assertEqual($new_node->get($field_name)->offsetGet(0)->value, $expected_start_date->format(DATETIME_DATE_STORAGE_FORMAT));
+ $this->assertEqual($new_node->get($field_name)->offsetGet(0)->end_value, $expected_end_date->format(DATETIME_DATE_STORAGE_FORMAT));
+
+ // Remove default value.
+ $field_edit = [
+ 'default_value_input[default_date_type]' => '',
+ 'default_value_input[default_end_date_type]' => '',
+ ];
+ $this->drupalPostForm('admin/structure/types/manage/date_content/fields/node.date_content.' . $field_name, $field_edit, t('Save settings'));
+
+ // Check that default value is selected in default value form.
+ $this->drupalGet('admin/structure/types/manage/date_content/fields/node.date_content.' . $field_name);
+ $this->assertOptionSelected('edit-default-value-input-default-date-type', '', 'The default start value is selected in instance settings page');
+ $this->assertFieldByName('default_value_input[default_date]', '', 'The relative default start value is empty in instance settings page');
+ $this->assertOptionSelected('edit-default-value-input-default-end-date-type', '', 'The default end value is selected in instance settings page');
+ $this->assertFieldByName('default_value_input[default_end_date]', '', 'The relative default end value is empty in instance settings page');
+
+ // Check if default_date has been stored successfully.
+ $config_entity = $this->config('field.field.node.date_content.' . $field_name)->get();
+ $this->assertTrue(empty($config_entity['default_value']), 'Empty default value has been stored successfully');
+
+ // Clear field cache in order to avoid stale cache values.
+ \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
+
+ // Create a new node to check that datetime field default value is not set.
+ $new_node = Node::create(['type' => 'date_content']);
+ $this->assertNull($new_node->get($field_name)->value, 'Default value is not set');
+
+ // Set now as default_value for start date only.
+ entity_get_form_display('node', 'date_content', 'default')
+ ->setComponent($field_name, [
+ 'type' => 'datetime_default',
+ ])
+ ->save();
+
+ $expected_date = new DrupalDateTime('now', DATETIME_STORAGE_TIMEZONE);
+
+ $field_edit = [
+ 'default_value_input[default_date_type]' => 'now',
+ 'default_value_input[default_end_date_type]' => '',
+ ];
+ $this->drupalPostForm('admin/structure/types/manage/date_content/fields/node.date_content.' . $field_name, $field_edit, t('Save settings'));
+
+ // Make sure only the start value is populated on node add page.
+ $this->drupalGet('node/add/date_content');
+ $this->assertFieldByName("{$field_name}[0][value][date]", $expected_date->format(DATETIME_DATE_STORAGE_FORMAT), 'Start date element populated.');
+ $this->assertFieldByName("{$field_name}[0][end_value][date]", '', 'End date element empty.');
+
+ // Set now as default_value for end date only.
+ $field_edit = [
+ 'default_value_input[default_date_type]' => '',
+ 'default_value_input[default_end_date_type]' => 'now',
+ ];
+ $this->drupalPostForm('admin/structure/types/manage/date_content/fields/node.date_content.' . $field_name, $field_edit, t('Save settings'));
+
+ // Make sure only the start value is populated on node add page.
+ $this->drupalGet('node/add/date_content');
+ $this->assertFieldByName("{$field_name}[0][value][date]", '', 'Start date element empty.');
+ $this->assertFieldByName("{$field_name}[0][end_value][date]", $expected_date->format(DATETIME_DATE_STORAGE_FORMAT), 'End date element populated.');
+ }
+
+ /**
+ * Test that invalid values are caught and marked as invalid.
+ */
+ public function testInvalidField() {
+ // Change the field to a datetime field.
+ $this->fieldStorage->setSetting('datetime_type', DateRangeItem::DATETIME_TYPE_DATETIME);
+ $this->fieldStorage->save();
+ $field_name = $this->fieldStorage->getName();
+
+ $this->drupalGet('entity_test/add');
+ $this->assertFieldByName("{$field_name}[0][value][date]", '', 'Start date element found.');
+ $this->assertFieldByName("{$field_name}[0][value][time]", '', 'Start time element found.');
+ $this->assertFieldByName("{$field_name}[0][end_value][date]", '', 'End date element found.');
+ $this->assertFieldByName("{$field_name}[0][end_value][time]", '', 'End time element found.');
+
+ // Submit invalid start dates and ensure they is not accepted.
+ $date_value = '';
+ $edit = [
+ "{$field_name}[0][value][date]" => $date_value,
+ "{$field_name}[0][value][time]" => '12:00:00',
+ "{$field_name}[0][end_value][date]" => '2012-12-01',
+ "{$field_name}[0][end_value][time]" => '12:00:00',
+ ];
+ $this->drupalPostForm(NULL, $edit, t('Save'));
+ $this->assertText('date is invalid', 'Empty start date value has been caught.');
+
+ $date_value = 'aaaa-12-01';
+ $edit = [
+ "{$field_name}[0][value][date]" => $date_value,
+ "{$field_name}[0][value][time]" => '00:00:00',
+ "{$field_name}[0][end_value][date]" => '2012-12-01',
+ "{$field_name}[0][end_value][time]" => '12:00:00',
+ ];
+ $this->drupalPostForm(NULL, $edit, t('Save'));
+ $this->assertText('date is invalid', new FormattableMarkup('Invalid start year value %date has been caught.', ['%date' => $date_value]));
+
+ $date_value = '2012-75-01';
+ $edit = [
+ "{$field_name}[0][value][date]" => $date_value,
+ "{$field_name}[0][value][time]" => '00:00:00',
+ "{$field_name}[0][end_value][date]" => '2012-12-01',
+ "{$field_name}[0][end_value][time]" => '12:00:00',
+ ];
+ $this->drupalPostForm(NULL, $edit, t('Save'));
+ $this->assertText('date is invalid', new FormattableMarkup('Invalid start month value %date has been caught.', ['%date' => $date_value]));
+
+ $date_value = '2012-12-99';
+ $edit = [
+ "{$field_name}[0][value][date]" => $date_value,
+ "{$field_name}[0][value][time]" => '00:00:00',
+ "{$field_name}[0][end_value][date]" => '2012-12-01',
+ "{$field_name}[0][end_value][time]" => '12:00:00',
+ ];
+ $this->drupalPostForm(NULL, $edit, t('Save'));
+ $this->assertText('date is invalid', new FormattableMarkup('Invalid start day value %date has been caught.', ['%date' => $date_value]));
+
+ // Submit invalid start times and ensure they is not accepted.
+ $time_value = '';
+ $edit = [
+ "{$field_name}[0][value][date]" => '2012-12-01',
+ "{$field_name}[0][value][time]" => $time_value,
+ "{$field_name}[0][end_value][date]" => '2012-12-01',
+ "{$field_name}[0][end_value][time]" => '12:00:00',
+ ];
+ $this->drupalPostForm(NULL, $edit, t('Save'));
+ $this->assertText('date is invalid', 'Empty start time value has been caught.');
+
+ $time_value = '49:00:00';
+ $edit = [
+ "{$field_name}[0][value][date]" => '2012-12-01',
+ "{$field_name}[0][value][time]" => $time_value,
+ "{$field_name}[0][end_value][date]" => '2012-12-01',
+ "{$field_name}[0][end_value][time]" => '12:00:00',
+ ];
+ $this->drupalPostForm(NULL, $edit, t('Save'));
+ $this->assertText('date is invalid', new FormattableMarkup('Invalid start hour value %time has been caught.', ['%time' => $time_value]));
+
+ $time_value = '12:99:00';
+ $edit = [
+ "{$field_name}[0][value][date]" => '2012-12-01',
+ "{$field_name}[0][value][time]" => $time_value,
+ "{$field_name}[0][end_value][date]" => '2012-12-01',
+ "{$field_name}[0][end_value][time]" => '12:00:00',
+ ];
+ $this->drupalPostForm(NULL, $edit, t('Save'));
+ $this->assertText('date is invalid', new FormattableMarkup('Invalid start minute value %time has been caught.', ['%time' => $time_value]));
+
+ $time_value = '12:15:99';
+ $edit = [
+ "{$field_name}[0][value][date]" => '2012-12-01',
+ "{$field_name}[0][value][time]" => $time_value,
+ "{$field_name}[0][end_value][date]" => '2012-12-01',
+ "{$field_name}[0][end_value][time]" => '12:00:00',
+ ];
+ $this->drupalPostForm(NULL, $edit, t('Save'));
+ $this->assertText('date is invalid', new FormattableMarkup('Invalid start second value %time has been caught.', ['%time' => $time_value]));
+
+ // Submit invalid end dates and ensure they is not accepted.
+ $date_value = '';
+ $edit = [
+ "{$field_name}[0][value][date]" => '2012-12-01',
+ "{$field_name}[0][value][time]" => '12:00:00',
+ "{$field_name}[0][end_value][date]" => $date_value,
+ "{$field_name}[0][end_value][time]" => '12:00:00',
+ ];
+ $this->drupalPostForm(NULL, $edit, t('Save'));
+ $this->assertText('date is invalid', 'Empty end date value has been caught.');
+
+ $date_value = 'aaaa-12-01';
+ $edit = [
+ "{$field_name}[0][value][date]" => '2012-12-01',
+ "{$field_name}[0][value][time]" => '12:00:00',
+ "{$field_name}[0][end_value][date]" => $date_value,
+ "{$field_name}[0][end_value][time]" => '00:00:00',
+ ];
+ $this->drupalPostForm(NULL, $edit, t('Save'));
+ $this->assertText('date is invalid', new FormattableMarkup('Invalid end year value %date has been caught.', ['%date' => $date_value]));
+
+ $date_value = '2012-75-01';
+ $edit = [
+ "{$field_name}[0][value][date]" => '2012-12-01',
+ "{$field_name}[0][value][time]" => '12:00:00',
+ "{$field_name}[0][end_value][date]" => $date_value,
+ "{$field_name}[0][end_value][time]" => '00:00:00',
+ ];
+ $this->drupalPostForm(NULL, $edit, t('Save'));
+ $this->assertText('date is invalid', new FormattableMarkup('Invalid end month value %date has been caught.', ['%date' => $date_value]));
+
+ $date_value = '2012-12-99';
+ $edit = [
+ "{$field_name}[0][value][date]" => '2012-12-01',
+ "{$field_name}[0][value][time]" => '12:00:00',
+ "{$field_name}[0][end_value][date]" => $date_value,
+ "{$field_name}[0][end_value][time]" => '00:00:00',
+ ];
+ $this->drupalPostForm(NULL, $edit, t('Save'));
+ $this->assertText('date is invalid', new FormattableMarkup('Invalid end day value %date has been caught.', ['%date' => $date_value]));
+
+ // Submit invalid start times and ensure they is not accepted.
+ $time_value = '';
+ $edit = [
+ "{$field_name}[0][value][date]" => '2012-12-01',
+ "{$field_name}[0][value][time]" => '12:00:00',
+ "{$field_name}[0][end_value][date]" => '2012-12-01',
+ "{$field_name}[0][end_value][time]" => $time_value,
+ ];
+ $this->drupalPostForm(NULL, $edit, t('Save'));
+ $this->assertText('date is invalid', 'Empty end time value has been caught.');
+
+ $time_value = '49:00:00';
+ $edit = [
+ "{$field_name}[0][value][date]" => '2012-12-01',
+ "{$field_name}[0][value][time]" => '12:00:00',
+ "{$field_name}[0][end_value][date]" => '2012-12-01',
+ "{$field_name}[0][end_value][time]" => $time_value,
+ ];
+ $this->drupalPostForm(NULL, $edit, t('Save'));
+ $this->assertText('date is invalid', new FormattableMarkup('Invalid end hour value %time has been caught.', ['%time' => $time_value]));
+
+ $time_value = '12:99:00';
+ $edit = [
+ "{$field_name}[0][value][date]" => '2012-12-01',
+ "{$field_name}[0][value][time]" => '12:00:00',
+ "{$field_name}[0][end_value][date]" => '2012-12-01',
+ "{$field_name}[0][end_value][time]" => $time_value,
+ ];
+ $this->drupalPostForm(NULL, $edit, t('Save'));
+ $this->assertText('date is invalid', new FormattableMarkup('Invalid end minute value %time has been caught.', ['%time' => $time_value]));
+
+ $time_value = '12:15:99';
+ $edit = [
+ "{$field_name}[0][value][date]" => '2012-12-01',
+ "{$field_name}[0][value][time]" => '12:00:00',
+ "{$field_name}[0][end_value][date]" => '2012-12-01',
+ "{$field_name}[0][end_value][time]" => $time_value,
+ ];
+ $this->drupalPostForm(NULL, $edit, t('Save'));
+ $this->assertText('date is invalid', new FormattableMarkup('Invalid end second value %time has been caught.', ['%time' => $time_value]));
+
+ $edit = [
+ "{$field_name}[0][value][date]" => '2012-12-01',
+ "{$field_name}[0][value][time]" => '12:00:00',
+ "{$field_name}[0][end_value][date]" => '2010-12-01',
+ "{$field_name}[0][end_value][time]" => '12:00:00',
+ ];
+ $this->drupalPostForm(NULL, $edit, t('Save'));
+ $this->assertText(new FormattableMarkup('The @title end date cannot be before the start date', ['@title' => $field_name]), 'End date before start date has been caught.');
+
+ $edit = [
+ "{$field_name}[0][value][date]" => '2012-12-01',
+ "{$field_name}[0][value][time]" => '12:00:00',
+ "{$field_name}[0][end_value][date]" => '2012-12-01',
+ "{$field_name}[0][end_value][time]" => '11:00:00',
+ ];
+ $this->drupalPostForm(NULL, $edit, t('Save'));
+ $this->assertText(new FormattableMarkup('The @title end date cannot be before the start date', ['@title' => $field_name]), 'End time before start time has been caught.');
+ }
+
+ /**
+ * Tests that 'Date' field storage setting form is disabled if field has data.
+ */
+ public function testDateStorageSettings() {
+ // Create a test content type.
+ $this->drupalCreateContentType(['type' => 'date_content']);
+
+ // Create a field storage with settings to validate.
+ $field_name = Unicode::strtolower($this->randomMachineName());
+ $field_storage = FieldStorageConfig::create([
+ 'field_name' => $field_name,
+ 'entity_type' => 'node',
+ 'type' => 'daterange',
+ 'settings' => [
+ 'datetime_type' => DateRangeItem::DATETIME_TYPE_DATE,
+ ],
+ ]);
+ $field_storage->save();
+ $field = FieldConfig::create([
+ 'field_storage' => $field_storage,
+ 'field_name' => $field_name,
+ 'bundle' => 'date_content',
+ ]);
+ $field->save();
+
+ entity_get_form_display('node', 'date_content', 'default')
+ ->setComponent($field_name, [
+ 'type' => 'datetime_default',
+ ])
+ ->save();
+ $edit = [
+ 'title[0][value]' => $this->randomString(),
+ 'body[0][value]' => $this->randomString(),
+ $field_name . '[0][value][date]' => '2016-04-01',
+ $field_name . '[0][end_value][date]' => '2016-04-02',
+ ];
+ $this->drupalPostForm('node/add/date_content', $edit, t('Save'));
+ $this->drupalGet('admin/structure/types/manage/date_content/fields/node.date_content.' . $field_name . '/storage');
+ $result = $this->xpath("//*[@id='edit-settings-datetime-type' and contains(@disabled, 'disabled')]");
+ $this->assertEqual(count($result), 1, "Changing datetime setting is disabled.");
+ $this->assertText('There is data for this field in the database. The field settings can no longer be changed.');
+ }
+
+ /**
+ * Renders a entity_test and sets the output in the internal browser.
+ *
+ * @param int $id
+ * The entity_test ID to render.
+ * @param string $view_mode
+ * (optional) The view mode to use for rendering. Defaults to 'full'.
+ * @param bool $reset
+ * (optional) Whether to reset the entity_test controller cache. Defaults to
+ * TRUE to simplify testing.
+ */
+ protected function renderTestEntity($id, $view_mode = 'full', $reset = TRUE) {
+ if ($reset) {
+ \Drupal::service('entity_type.manager')->getStorage('entity_test')->resetCache([$id]);
+ }
+ $entity = EntityTest::load($id);
+ $display = EntityViewDisplay::collectRenderDisplay($entity, $view_mode);
+ $build = $display->build($entity);
+ $output = \Drupal::service('renderer')->renderRoot($build);
+ $this->setRawContent($output);
+ $this->verbose($output);
+ }
+
+ /**
+ * Sets the site timezone to a given timezone.
+ *
+ * @param string $timezone
+ * The timezone identifier to set.
+ */
+ protected function setSiteTimezone($timezone) {
+ // Set an explicit site timezone, and disallow per-user timezones.
+ $this->config('system.date')
+ ->set('timezone.user.configurable', 0)
+ // A timezone with an offset greater than UTC+12 is used.
+ ->set('timezone.default', $timezone)
+ ->save();
+ }
+
+}