From 7659dcc4c5ed3ceeddda589304b2de093aa75fc7 Mon Sep 17 00:00:00 2001 From: catch <catch@35733.no-reply.drupal.org> Date: Mon, 1 Apr 2024 09:20:15 +0100 Subject: [PATCH] Issue #2827055 by yash.rode, srishtiiee, mrshowerman, rodrigoaguilera, herved, omkar.podey, MegaChriz, Lukas von Blarer, ravi.shankar, lauriii, c_archer, ifrik, froboy, Rudi Teschner, smustgrave, liquidcms, catch, quietone, mpdonadio, alexpott: Add option to show only start or end date in the DateTime Range custom formatter --- .../config/schema/datetime_range.schema.yml | 24 +++ .../datetime_range/datetime_range.module | 38 ++++ .../datetime_range.post_update.php | 43 ++++ .../src/DateTimeRangeConstantsInterface.php | 20 ++ .../datetime_range/src/DateTimeRangeTrait.php | 175 ++++++++++++++- .../DateRangeCustomFormatter.php | 27 +-- .../DateRangeDefaultFormatter.php | 20 +- .../DateRangePlainFormatter.php | 27 +-- ...l.daterange-formatter-settings-2827055.php | 120 +++++++++++ .../src/Functional/DateRangeFieldTest.php | 201 +++++++++++++++++- .../DateRangeFormatterSettingsUpdateTest.php | 57 +++++ .../DateRangeFieldTest.php | 97 +++++++++ 12 files changed, 772 insertions(+), 77 deletions(-) create mode 100644 core/modules/datetime_range/src/DateTimeRangeConstantsInterface.php create mode 100644 core/modules/datetime_range/tests/fixtures/update/drupal.daterange-formatter-settings-2827055.php create mode 100644 core/modules/datetime_range/tests/src/Functional/DateRangeFormatterSettingsUpdateTest.php create mode 100644 core/modules/datetime_range/tests/src/FunctionalJavascript/DateRangeFieldTest.php diff --git a/core/modules/datetime_range/config/schema/datetime_range.schema.yml b/core/modules/datetime_range/config/schema/datetime_range.schema.yml index fa17461be651..bb244553edc1 100644 --- a/core/modules/datetime_range/config/schema/datetime_range.schema.yml +++ b/core/modules/datetime_range/config/schema/datetime_range.schema.yml @@ -28,6 +28,14 @@ field.formatter.settings.daterange_default: type: field.formatter.settings.datetime_default label: 'Date range default display format settings' mapping: + from_to: + type: string + label: 'Display' + constraints: + Choice: + - both + - start_date + - end_date separator: type: label label: 'Separator' @@ -37,6 +45,14 @@ field.formatter.settings.daterange_plain: type: field.formatter.settings.datetime_plain label: 'Date range plain display format settings' mapping: + from_to: + type: string + label: 'Display' + constraints: + Choice: + - both + - start_date + - end_date separator: type: label label: 'Separator' @@ -46,6 +62,14 @@ field.formatter.settings.daterange_custom: type: field.formatter.settings.datetime_custom label: 'Date range custom display format settings' mapping: + from_to: + type: string + label: 'Display' + constraints: + Choice: + - both + - start_date + - end_date separator: type: label label: 'Separator' diff --git a/core/modules/datetime_range/datetime_range.module b/core/modules/datetime_range/datetime_range.module index c0be2a2089d8..0a505c30c75b 100644 --- a/core/modules/datetime_range/datetime_range.module +++ b/core/modules/datetime_range/datetime_range.module @@ -5,8 +5,13 @@ * Field hooks to implement a datetime field that stores a start and end date. */ +use Drupal\Core\Entity\Display\EntityViewDisplayInterface; use Drupal\Core\Url; use Drupal\Core\Routing\RouteMatchInterface; +use Drupal\datetime_range\DateTimeRangeConstantsInterface; +use Drupal\datetime_range\Plugin\Field\FieldFormatter\DateRangeCustomFormatter; +use Drupal\datetime_range\Plugin\Field\FieldFormatter\DateRangeDefaultFormatter; +use Drupal\datetime_range\Plugin\Field\FieldFormatter\DateRangePlainFormatter; /** * Implements hook_help(). @@ -27,3 +32,36 @@ function datetime_range_help($route_name, RouteMatchInterface $route_match) { return $output; } } + +/** + * Implements hook_ENTITY_TYPE_presave() for entity_view_display entities. + * + * @todo Remove this when datetime_range_post_update_from_to_configuration is removed. + */ +function datetime_range_entity_view_display_presave(EntityViewDisplayInterface $entity_view_display): void { + /** @var \Drupal\Core\Field\FormatterPluginManager $field_formatter_manager */ + $field_formatter_manager = \Drupal::service('plugin.manager.field.formatter'); + + foreach ($entity_view_display->getComponents() as $name => $component) { + if (empty($component['type'])) { + continue; + } + + $plugin_definition = $field_formatter_manager->getDefinition($component['type'], FALSE); + $daterange_formatter_classes = [ + DateRangeCustomFormatter::class, + DateRangeDefaultFormatter::class, + DateRangePlainFormatter::class, + ]; + + if (!in_array($plugin_definition['class'], $daterange_formatter_classes, FALSE)) { + continue; + } + + if (!isset($component['settings']['from_to'])) { + // Existing daterange formatters don't have 'from_to'. + $component['settings']['from_to'] = DateTimeRangeConstantsInterface::BOTH; + $entity_view_display->setComponent($name, $component); + } + } +} diff --git a/core/modules/datetime_range/datetime_range.post_update.php b/core/modules/datetime_range/datetime_range.post_update.php index 83df8ca9b1c8..9dfa5156eb26 100644 --- a/core/modules/datetime_range/datetime_range.post_update.php +++ b/core/modules/datetime_range/datetime_range.post_update.php @@ -5,6 +5,12 @@ * Post-update functions for Datetime Range module. */ +use Drupal\Core\Config\Entity\ConfigEntityUpdater; +use Drupal\Core\Entity\Display\EntityViewDisplayInterface; +use Drupal\datetime_range\Plugin\Field\FieldFormatter\DateRangeCustomFormatter; +use Drupal\datetime_range\Plugin\Field\FieldFormatter\DateRangeDefaultFormatter; +use Drupal\datetime_range\Plugin\Field\FieldFormatter\DateRangePlainFormatter; + /** * Implements hook_removed_post_updates(). */ @@ -14,3 +20,40 @@ function datetime_range_removed_post_updates() { 'datetime_range_post_update_views_string_plugin_id' => '9.0.0', ]; } + +/** + * Adds 'from_to' in flagged entity view date range formatter. + * + * @see \datetime_range_entity_view_display_presave + */ +function datetime_range_post_update_from_to_configuration(array &$sandbox = NULL): void { + /** @var \Drupal\Core\Field\FormatterPluginManager $field_formatter_manager */ + $field_formatter_manager = \Drupal::service('plugin.manager.field.formatter'); + $config_entity_updater = \Drupal::classResolver(ConfigEntityUpdater::class); + + $callback = function (EntityViewDisplayInterface $entity_view_display) use ($field_formatter_manager) { + foreach (array_values($entity_view_display->getComponents()) as $component) { + if (empty($component['type'])) { + continue; + } + + $plugin_definition = $field_formatter_manager->getDefinition($component['type'], FALSE); + $daterange_formatter_classes = [ + DateRangeCustomFormatter::class, + DateRangeDefaultFormatter::class, + DateRangePlainFormatter::class, + ]; + + if (!in_array($plugin_definition['class'], $daterange_formatter_classes, FALSE)) { + continue; + } + + if (!isset($component['settings']['from_to'])) { + return TRUE; + } + } + return FALSE; + }; + + $config_entity_updater->update($sandbox, 'entity_view_display', $callback); +} diff --git a/core/modules/datetime_range/src/DateTimeRangeConstantsInterface.php b/core/modules/datetime_range/src/DateTimeRangeConstantsInterface.php new file mode 100644 index 000000000000..9d97b6e4a798 --- /dev/null +++ b/core/modules/datetime_range/src/DateTimeRangeConstantsInterface.php @@ -0,0 +1,20 @@ +<?php + +namespace Drupal\datetime_range; + +/** + * Declares constants used in the datetime range module. + * + * @todo Convert this to an enum in 11.0. + * @see https://www.drupal.org/project/drupal/issues/3425141. + */ +interface DateTimeRangeConstantsInterface { + + /** + * Values for the 'from_to' formatter setting. + */ + const BOTH = 'both'; + const START_DATE = 'start_date'; + const END_DATE = 'end_date'; + +} diff --git a/core/modules/datetime_range/src/DateTimeRangeTrait.php b/core/modules/datetime_range/src/DateTimeRangeTrait.php index 3f05b82189b7..0563f7ff722b 100644 --- a/core/modules/datetime_range/src/DateTimeRangeTrait.php +++ b/core/modules/datetime_range/src/DateTimeRangeTrait.php @@ -2,6 +2,7 @@ namespace Drupal\datetime_range; +use Drupal\Core\Datetime\DrupalDateTime; use Drupal\Core\Field\FieldItemListInterface; /** @@ -9,6 +10,19 @@ */ trait DateTimeRangeTrait { + /** + * Get the default settings for a date and time range display. + * + * @return array + * An array containing default settings. + */ + protected static function dateTimeRangeDefaultSettings(): array { + return [ + 'from_to' => DateTimeRangeConstantsInterface::BOTH, + 'separator' => '-', + ]; + } + /** * {@inheritdoc} */ @@ -24,11 +38,7 @@ public function viewElements(FieldItemListInterface $items, $langcode) { $end_date = $item->end_date; if ($start_date->getTimestamp() !== $end_date->getTimestamp()) { - $elements[$delta] = [ - 'start_date' => $this->buildDateWithIsoAttribute($start_date), - 'separator' => ['#plain_text' => ' ' . $separator . ' '], - 'end_date' => $this->buildDateWithIsoAttribute($end_date), - ]; + $elements[$delta] = $this->renderStartEndWithIsoAttribute($start_date, $separator, $end_date); } else { $elements[$delta] = $this->buildDateWithIsoAttribute($start_date); @@ -46,4 +56,159 @@ public function viewElements(FieldItemListInterface $items, $langcode) { return $elements; } + /** + * Configuration form for date time range. + * + * @param array $form + * The form array. + * + * @return array + * Modified form array. + */ + protected function dateTimeRangeSettingsForm(array $form): array { + $form['from_to'] = [ + '#type' => 'select', + '#title' => $this->t('Display'), + '#options' => $this->getFromToOptions(), + '#default_value' => $this->getSetting('from_to'), + ]; + + $field_name = $this->fieldDefinition->getName(); + $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'), + '#states' => [ + 'visible' => [ + 'select[name="fields[' . $field_name . '][settings_edit_form][settings][from_to]"]' => ['value' => DateTimeRangeConstantsInterface::BOTH], + ], + ], + ]; + + return $form; + } + + /** + * Gets the date time range settings summary. + * + * @return array + * An array of summary messages. + */ + protected function dateTimeRangeSettingsSummary(): array { + $summary = []; + if ($from_to = $this->getSetting('from_to')) { + $from_to_options = $this->getFromToOptions(); + if (isset($from_to_options[$from_to])) { + $summary[] = $from_to_options[$from_to]; + } + } + + if (($separator = $this->getSetting('separator')) && $this->getSetting('from_to') === DateTimeRangeConstantsInterface::BOTH) { + $summary[] = $this->t('Separator: %separator', ['%separator' => $separator]); + } + + return $summary; + } + + /** + * Returns a list of possible values for the 'from_to' setting. + * + * @return array + * A list of 'from_to' options. + */ + protected function getFromToOptions(): array { + return [ + DateTimeRangeConstantsInterface::BOTH => $this->t('Display both start and end dates'), + DateTimeRangeConstantsInterface::START_DATE => $this->t('Display start date only'), + DateTimeRangeConstantsInterface::END_DATE => $this->t('Display end date only'), + ]; + } + + /** + * Gets whether the start date should be displayed. + * + * @return bool + * True if the start date should be displayed. False otherwise. + */ + protected function startDateIsDisplayed(): bool { + switch ($this->getSetting('from_to')) { + case DateTimeRangeConstantsInterface::BOTH: + case DateTimeRangeConstantsInterface::START_DATE: + return TRUE; + } + + return FALSE; + } + + /** + * Gets whether the end date should be displayed. + * + * @return bool + * True if the end date should be displayed. False otherwise. + */ + protected function endDateIsDisplayed(): bool { + switch ($this->getSetting('from_to')) { + case DateTimeRangeConstantsInterface::BOTH: + case DateTimeRangeConstantsInterface::END_DATE: + return TRUE; + } + + return FALSE; + } + + /** + * Creates a render array given start/end dates. + * + * @param \Drupal\Core\Datetime\DrupalDateTime $start_date + * The start date to be rendered. + * @param string $separator + * The separator string. + * @param \Drupal\Core\Datetime\DrupalDateTime $end_date + * The end date to be rendered. + * + * @return array + * A renderable array for a single date time range. + */ + protected function renderStartEnd(DrupalDateTime $start_date, string $separator, DrupalDateTime $end_date): array { + $element = []; + if ($this->startDateIsDisplayed()) { + $element[DateTimeRangeConstantsInterface::START_DATE] = $this->buildDate($start_date); + } + if ($this->startDateIsDisplayed() && $this->endDateIsDisplayed()) { + $element['separator'] = ['#plain_text' => ' ' . $separator . ' ']; + } + if ($this->endDateIsDisplayed()) { + $element[DateTimeRangeConstantsInterface::END_DATE] = $this->buildDate($end_date); + } + return $element; + } + + /** + * Creates a render array with ISO attributes given start/end dates. + * + * @param \Drupal\Core\Datetime\DrupalDateTime $start_date + * The start date to be rendered. + * @param string $separator + * The separator string. + * @param \Drupal\Core\Datetime\DrupalDateTime $end_date + * The end date to be rendered. + * + * @return array + * A renderable array for a single date time range. + */ + protected function renderStartEndWithIsoAttribute(DrupalDateTime $start_date, string $separator, DrupalDateTime $end_date): array { + $element = []; + if ($this->startDateIsDisplayed()) { + $element[DateTimeRangeConstantsInterface::START_DATE] = $this->buildDateWithIsoAttribute($start_date); + } + if ($this->startDateIsDisplayed() && $this->endDateIsDisplayed()) { + $element['separator'] = ['#plain_text' => ' ' . $separator . ' ']; + } + if ($this->endDateIsDisplayed()) { + $element[DateTimeRangeConstantsInterface::END_DATE] = $this->buildDateWithIsoAttribute($end_date); + } + return $element; + } + } diff --git a/core/modules/datetime_range/src/Plugin/Field/FieldFormatter/DateRangeCustomFormatter.php b/core/modules/datetime_range/src/Plugin/Field/FieldFormatter/DateRangeCustomFormatter.php index b9facc709661..1cf94249be68 100644 --- a/core/modules/datetime_range/src/Plugin/Field/FieldFormatter/DateRangeCustomFormatter.php +++ b/core/modules/datetime_range/src/Plugin/Field/FieldFormatter/DateRangeCustomFormatter.php @@ -30,9 +30,7 @@ class DateRangeCustomFormatter extends DateTimeCustomFormatter { * {@inheritdoc} */ public static function defaultSettings() { - return [ - 'separator' => '-', - ] + parent::defaultSettings(); + return static::dateTimeRangeDefaultSettings() + parent::defaultSettings(); } /** @@ -53,11 +51,7 @@ public function viewElements(FieldItemListInterface $items, $langcode) { $end_date = $item->end_date; if ($start_date->getTimestamp() !== $end_date->getTimestamp()) { - $elements[$delta] = [ - 'start_date' => $this->buildDate($start_date), - 'separator' => ['#plain_text' => ' ' . $separator . ' '], - 'end_date' => $this->buildDate($end_date), - ]; + $elements[$delta] = $this->renderStartEnd($start_date, $separator, $end_date); } else { $elements[$delta] = $this->buildDate($start_date); @@ -73,14 +67,7 @@ public function viewElements(FieldItemListInterface $items, $langcode) { */ public function settingsForm(array $form, FormStateInterface $form_state) { $form = parent::settingsForm($form, $form_state); - - $form['separator'] = [ - '#type' => 'textfield', - '#title' => $this->t('Date separator'), - '#description' => $this->t('The string to separate the start and end dates'), - '#default_value' => $this->getSetting('separator'), - ]; - + $form = $this->dateTimeRangeSettingsForm($form); return $form; } @@ -88,13 +75,7 @@ public function settingsForm(array $form, FormStateInterface $form_state) { * {@inheritdoc} */ public function settingsSummary() { - $summary = parent::settingsSummary(); - - if ($separator = $this->getSetting('separator')) { - $summary[] = $this->t('Separator: %separator', ['%separator' => $separator]); - } - - return $summary; + return array_merge(parent::settingsSummary(), $this->dateTimeRangeSettingsSummary()); } } diff --git a/core/modules/datetime_range/src/Plugin/Field/FieldFormatter/DateRangeDefaultFormatter.php b/core/modules/datetime_range/src/Plugin/Field/FieldFormatter/DateRangeDefaultFormatter.php index 76655da33a57..401fca6395de 100644 --- a/core/modules/datetime_range/src/Plugin/Field/FieldFormatter/DateRangeDefaultFormatter.php +++ b/core/modules/datetime_range/src/Plugin/Field/FieldFormatter/DateRangeDefaultFormatter.php @@ -30,9 +30,7 @@ class DateRangeDefaultFormatter extends DateTimeDefaultFormatter { * {@inheritdoc} */ public static function defaultSettings() { - return [ - 'separator' => '-', - ] + parent::defaultSettings(); + return static::dateTimeRangeDefaultSettings() + parent::defaultSettings(); } /** @@ -40,13 +38,7 @@ public static function defaultSettings() { */ public function settingsForm(array $form, FormStateInterface $form_state) { $form = parent::settingsForm($form, $form_state); - - $form['separator'] = [ - '#type' => 'textfield', - '#title' => $this->t('Date separator'), - '#description' => $this->t('The string to separate the start and end dates'), - '#default_value' => $this->getSetting('separator'), - ]; + $form = $this->dateTimeRangeSettingsForm($form); return $form; } @@ -55,13 +47,7 @@ public function settingsForm(array $form, FormStateInterface $form_state) { * {@inheritdoc} */ public function settingsSummary() { - $summary = parent::settingsSummary(); - - if ($separator = $this->getSetting('separator')) { - $summary[] = $this->t('Separator: %separator', ['%separator' => $separator]); - } - - return $summary; + return array_merge(parent::settingsSummary(), $this->dateTimeRangeSettingsSummary()); } } diff --git a/core/modules/datetime_range/src/Plugin/Field/FieldFormatter/DateRangePlainFormatter.php b/core/modules/datetime_range/src/Plugin/Field/FieldFormatter/DateRangePlainFormatter.php index 76a3799a2d46..960bd5e8ca7d 100644 --- a/core/modules/datetime_range/src/Plugin/Field/FieldFormatter/DateRangePlainFormatter.php +++ b/core/modules/datetime_range/src/Plugin/Field/FieldFormatter/DateRangePlainFormatter.php @@ -30,9 +30,7 @@ class DateRangePlainFormatter extends DateTimePlainFormatter { * {@inheritdoc} */ public static function defaultSettings() { - return [ - 'separator' => '-', - ] + parent::defaultSettings(); + return static::dateTimeRangeDefaultSettings() + parent::defaultSettings(); } /** @@ -50,11 +48,7 @@ public function viewElements(FieldItemListInterface $items, $langcode) { $end_date = $item->end_date; if ($start_date->getTimestamp() !== $end_date->getTimestamp()) { - $elements[$delta] = [ - 'start_date' => $this->buildDate($start_date), - 'separator' => ['#plain_text' => ' ' . $separator . ' '], - 'end_date' => $this->buildDate($end_date), - ]; + $elements[$delta] = $this->renderStartEnd($start_date, $separator, $end_date); } else { $elements[$delta] = $this->buildDate($start_date); @@ -77,14 +71,7 @@ public function viewElements(FieldItemListInterface $items, $langcode) { */ public function settingsForm(array $form, FormStateInterface $form_state) { $form = parent::settingsForm($form, $form_state); - - $form['separator'] = [ - '#type' => 'textfield', - '#title' => $this->t('Date separator'), - '#description' => $this->t('The string to separate the start and end dates'), - '#default_value' => $this->getSetting('separator'), - ]; - + $form = $this->dateTimeRangeSettingsForm($form); return $form; } @@ -92,13 +79,7 @@ public function settingsForm(array $form, FormStateInterface $form_state) { * {@inheritdoc} */ public function settingsSummary() { - $summary = parent::settingsSummary(); - - if ($separator = $this->getSetting('separator')) { - $summary[] = $this->t('Separator: %separator', ['%separator' => $separator]); - } - - return $summary; + return array_merge(parent::settingsSummary(), $this->dateTimeRangeSettingsSummary()); } } diff --git a/core/modules/datetime_range/tests/fixtures/update/drupal.daterange-formatter-settings-2827055.php b/core/modules/datetime_range/tests/fixtures/update/drupal.daterange-formatter-settings-2827055.php new file mode 100644 index 000000000000..f8552a5018bd --- /dev/null +++ b/core/modules/datetime_range/tests/fixtures/update/drupal.daterange-formatter-settings-2827055.php @@ -0,0 +1,120 @@ +<?php + +/** + * @file + * Provides database changes for testing the daterange formatter upgrade path. + * + * @see \Drupal\Tests\datetime_range\Functional\DateRangeFormatterSettingsUpdateTest + */ + +use Drupal\Core\Database\Database; +use Drupal\field\Entity\FieldStorageConfig; + +$connection = Database::getConnection(); + +// Add all datetime_range_removed_post_updates() as existing updates. +require_once __DIR__ . '/../../../../datetime_range/datetime_range.post_update.php'; +$existing_updates = $connection->select('key_value') + ->fields('key_value', ['value']) + ->condition('collection', 'post_update') + ->condition('name', 'existing_updates') + ->execute() + ->fetchField(); +$existing_updates = unserialize($existing_updates); +$existing_updates = array_merge( + $existing_updates, + array_keys(datetime_range_removed_post_updates()) +); +$connection->update('key_value') + ->fields(['value' => serialize($existing_updates)]) + ->condition('collection', 'post_update') + ->condition('name', 'existing_updates') + ->execute(); + +// Add a new timestamp field 'field_datetime_range'. +$connection->insert('config') + ->fields(['collection', 'name', 'data'])->values([ + 'collection' => '', + 'name' => 'field.storage.node.field_datetime_range', + 'data' => $field_storage = 'a:16:{s:4:"uuid";s:36:"a01264e6-2821-4b94-bc79-ba2b346795bb";s:8:"langcode";s:2:"en";s:6:"status";b:1;s:12:"dependencies";a:1:{s:6:"module";a:2:{i:0;s:14:"datetime_range";i:1;s:4:"node";}}s:2:"id";s:25:"node.field_datetime_range";s:10:"field_name";s:20:"field_datetime_range";s:11:"entity_type";s:4:"node";s:4:"type";s:9:"daterange";s:8:"settings";a:1:{s:13:"datetime_type";s:8:"datetime";}s:6:"module";s:14:"datetime_range";s:6:"locked";b:0;s:11:"cardinality";i:1;s:12:"translatable";b:1;s:7:"indexes";a:0:{}s:22:"persist_with_no_fields";b:0;s:14:"custom_storage";b:0;}', + ])->values([ + 'collection' => '', + 'name' => 'field.field.node.page.field_datetime_range', + 'data' => 'a:16:{s:4:"uuid";s:36:"678b9e68-cff5-4b2e-9111-43e5d9d6c826";s:8:"langcode";s:2:"en";s:6:"status";b:1;s:12:"dependencies";a:2:{s:6:"config";a:2:{i:0;s:39:"field.storage.node.field_datetime_range";i:1;s:14:"node.type.page";}s:6:"module";a:1:{i:0;s:14:"datetime_range";}}s:2:"id";s:30:"node.page.field_datetime_range";s:10:"field_name";s:20:"field_datetime_range";s:11:"entity_type";s:4:"node";s:6:"bundle";s:4:"page";s:5:"label";s:14:"datetime range";s:11:"description";s:0:"";s:8:"required";b:0;s:12:"translatable";b:0;s:13:"default_value";a:0:{}s:22:"default_value_callback";s:0:"";s:8:"settings";a:0:{}s:10:"field_type";s:9:"daterange";}', + ])->execute(); + +$connection->insert('key_value') + ->fields(['collection', 'name', 'value']) + ->values([ + 'collection' => 'config.entity.key_store.field_config', + 'name' => 'uuid:678b9e68-cff5-4b2e-9111-43e5d9d6c826', + 'value' => 'a:1:{i:0;s:42:"field.field.node.page.field_datetime_range";}', + ]) + ->values([ + 'collection' => 'config.entity.key_store.field_storage_config', + 'name' => 'uuid:a01264e6-2821-4b94-bc79-ba2b346795bb', + 'value' => 'a:1:{i:0;s:39:"field.storage.node.field_datetime_range";}', + ]) + ->values([ + 'collection' => 'entity.storage_schema.sql', + 'name' => 'node.field_schema_data.field_datetime_range', + 'value' => 'a:2:{s:26:"node__field_datetime_range";a:4:{s:11:"description";s:49:"Data storage for node field field_datetime_range.";s:6:"fields";a:8:{s:6:"bundle";a:5:{s:4:"type";s:13:"varchar_ascii";s:6:"length";i:128;s:8:"not null";b:1;s:7:"default";s:0:"";s:11:"description";s:88:"The field instance bundle to which this row belongs, used when deleting a field instance";}s:7:"deleted";a:5:{s:4:"type";s:3:"int";s:4:"size";s:4:"tiny";s:8:"not null";b:1;s:7:"default";i:0;s:11:"description";s:60:"A boolean indicating whether this data item has been deleted";}s:9:"entity_id";a:4:{s:4:"type";s:3:"int";s:8:"unsigned";b:1;s:8:"not null";b:1;s:11:"description";s:38:"The entity id this data is attached to";}s:11:"revision_id";a:4:{s:4:"type";s:3:"int";s:8:"unsigned";b:1;s:8:"not null";b:1;s:11:"description";s:47:"The entity revision id this data is attached to";}s:8:"langcode";a:5:{s:4:"type";s:13:"varchar_ascii";s:6:"length";i:32;s:8:"not null";b:1;s:7:"default";s:0:"";s:11:"description";s:37:"The language code for this data item.";}s:5:"delta";a:4:{s:4:"type";s:3:"int";s:8:"unsigned";b:1;s:8:"not null";b:1;s:11:"description";s:67:"The sequence number for this data item, used for multi-value fields";}s:26:"field_datetime_range_value";a:4:{s:11:"description";s:21:"The start date value.";s:4:"type";s:7:"varchar";s:6:"length";i:20;s:8:"not null";b:1;}s:30:"field_datetime_range_end_value";a:4:{s:11:"description";s:19:"The end date value.";s:4:"type";s:7:"varchar";s:6:"length";i:20;s:8:"not null";b:1;}}s:11:"primary key";a:4:{i:0;s:9:"entity_id";i:1;s:7:"deleted";i:2;s:5:"delta";i:3;s:8:"langcode";}s:7:"indexes";a:4:{s:6:"bundle";a:1:{i:0;s:6:"bundle";}s:11:"revision_id";a:1:{i:0;s:11:"revision_id";}s:26:"field_datetime_range_value";a:1:{i:0;s:26:"field_datetime_range_value";}s:30:"field_datetime_range_end_value";a:1:{i:0;s:30:"field_datetime_range_end_value";}}}s:35:"node_revision__field_datetime_range";a:4:{s:11:"description";s:61:"Revision archive storage for node field field_datetime_range.";s:6:"fields";a:8:{s:6:"bundle";a:5:{s:4:"type";s:13:"varchar_ascii";s:6:"length";i:128;s:8:"not null";b:1;s:7:"default";s:0:"";s:11:"description";s:88:"The field instance bundle to which this row belongs, used when deleting a field instance";}s:7:"deleted";a:5:{s:4:"type";s:3:"int";s:4:"size";s:4:"tiny";s:8:"not null";b:1;s:7:"default";i:0;s:11:"description";s:60:"A boolean indicating whether this data item has been deleted";}s:9:"entity_id";a:4:{s:4:"type";s:3:"int";s:8:"unsigned";b:1;s:8:"not null";b:1;s:11:"description";s:38:"The entity id this data is attached to";}s:11:"revision_id";a:4:{s:4:"type";s:3:"int";s:8:"unsigned";b:1;s:8:"not null";b:1;s:11:"description";s:47:"The entity revision id this data is attached to";}s:8:"langcode";a:5:{s:4:"type";s:13:"varchar_ascii";s:6:"length";i:32;s:8:"not null";b:1;s:7:"default";s:0:"";s:11:"description";s:37:"The language code for this data item.";}s:5:"delta";a:4:{s:4:"type";s:3:"int";s:8:"unsigned";b:1;s:8:"not null";b:1;s:11:"description";s:67:"The sequence number for this data item, used for multi-value fields";}s:26:"field_datetime_range_value";a:4:{s:11:"description";s:21:"The start date value.";s:4:"type";s:7:"varchar";s:6:"length";i:20;s:8:"not null";b:1;}s:30:"field_datetime_range_end_value";a:4:{s:11:"description";s:19:"The end date value.";s:4:"type";s:7:"varchar";s:6:"length";i:20;s:8:"not null";b:1;}}s:11:"primary key";a:5:{i:0;s:9:"entity_id";i:1;s:11:"revision_id";i:2;s:7:"deleted";i:3;s:5:"delta";i:4;s:8:"langcode";}s:7:"indexes";a:4:{s:6:"bundle";a:1:{i:0;s:6:"bundle";}s:11:"revision_id";a:1:{i:0;s:11:"revision_id";}s:26:"field_datetime_range_value";a:1:{i:0;s:26:"field_datetime_range_value";}s:30:"field_datetime_range_end_value";a:1:{i:0;s:30:"field_datetime_range_end_value";}}}}', + ]) + ->execute(); + +$data = $connection->select('key_value') + ->fields('key_value', ['value']) + ->condition('collection', 'entity.definitions.installed') + ->condition('name', 'node.field_storage_definitions') + ->execute() + ->fetchField(); +$data = unserialize($data); +$data['field_datetime_range'] = new FieldStorageConfig(unserialize($field_storage)); +$connection->update('key_value') + ->fields(['value' => serialize($data)]) + ->condition('collection', 'entity.definitions.installed') + ->condition('name', 'node.field_storage_definitions') + ->execute(); + +$data = $connection->select('config') + ->fields('config', ['data']) + ->condition('collection', '') + ->condition('name', 'core.entity_view_display.node.page.default') + ->execute() + ->fetchField(); +$data = unserialize($data); +$data['content']['field_datetime_range'] = [ + 'type' => 'daterange_default', + 'label' => 'above', + 'settings' => [ + 'timezone_override' => '', + 'format_type' => 'medium', + 'separator' => '-', + ], + 'third_party_settings' => [], + 'weight' => 102, + 'region' => 'content', +]; +$connection->update('config') + ->fields([ + 'data' => serialize($data), + ]) + ->condition('collection', '') + ->condition('name', 'core.entity_view_display.node.page.default') + ->execute(); + +$extensions = $connection->select('config') + ->fields('config', ['data']) + ->condition('collection', '') + ->condition('name', 'core.extension') + ->execute() + ->fetchField(); +$extensions = unserialize($extensions); +$extensions['module']['datetime_range'] = 0; +$connection->update('config') + ->fields([ + 'data' => serialize($extensions), + ]) + ->condition('collection', '') + ->condition('name', 'core.extension') + ->execute(); diff --git a/core/modules/datetime_range/tests/src/Functional/DateRangeFieldTest.php b/core/modules/datetime_range/tests/src/Functional/DateRangeFieldTest.php index f5de3a28ba1b..fcec7f3e1054 100644 --- a/core/modules/datetime_range/tests/src/Functional/DateRangeFieldTest.php +++ b/core/modules/datetime_range/tests/src/Functional/DateRangeFieldTest.php @@ -7,12 +7,13 @@ use Drupal\Core\Datetime\DrupalDateTime; use Drupal\Core\Datetime\Entity\DateFormat; use Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface; -use Drupal\Tests\datetime\Functional\DateTestBase; +use Drupal\datetime_range\DateTimeRangeConstantsInterface; 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\Tests\datetime\Functional\DateTestBase; /** * Tests Daterange field functionality. @@ -39,7 +40,11 @@ class DateRangeFieldTest extends DateTestBase { * * @var array */ - protected $defaultSettings = ['timezone_override' => '', 'separator' => '-']; + protected $defaultSettings = [ + 'timezone_override' => '', + 'separator' => '-', + 'from_to' => DateTimeRangeConstantsInterface::BOTH, + ]; /** * {@inheritdoc} @@ -185,7 +190,7 @@ public function testDateRangeField() { $output = $this->renderTestEntity($id); $this->assertStringContainsString($expected, $output, "Formatted date field using daterange_custom format displayed as $expected in $timezone."); - // Test formatters when start date and end date are the same + // 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'); @@ -363,7 +368,7 @@ public function testDatetimeRangeField() { $output = $this->renderTestEntity($id); $this->assertStringContainsString($expected, $output, "Formatted date field using daterange_custom format displayed as $expected."); - // Test formatters when start date and end date are the same + // 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'); @@ -536,7 +541,7 @@ public function testAlldayRangeField() { $output = $this->renderTestEntity($id); $this->assertStringContainsString($expected, $output, "Formatted date field using daterange_custom format displayed as $expected."); - // Test formatters when start date and end date are the same + // Test formatters when start date and end date are the same. $this->drupalGet('entity_test/add'); $value = '2012-12-31 00:00:00'; @@ -825,7 +830,7 @@ public function testDatelistWidget() { \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions(); // Test the widget for validation notifications. - foreach ($this->datelistDataProvider() as $data) { + foreach (static::datelistDataProvider() as $data) { [$start_date_value, $end_date_value, $expected] = $data; // Display creation form. @@ -891,7 +896,7 @@ public function testDatelistWidget() { * @return array * An array of datelist input permutations to test. */ - protected function datelistDataProvider() { + protected static function datelistDataProvider() { return [ // Year only selected, validation error on Month, Day, Hour, Minute. [ @@ -1007,7 +1012,13 @@ public function testDefaultValue() { // Check if default_date has been stored successfully. $config_entity = $this->config('field.field.node.date_content.' . $field_name)->get(); - $this->assertEquals(['default_date_type' => 'now', 'default_date' => 'now', 'default_end_date_type' => 'now', 'default_end_date' => 'now'], $config_entity['default_value'][0], 'Default value has been stored successfully'); + $this->assertEquals([ + 'default_date_type' => 'now', + 'default_date' => 'now', + 'default_end_date_type' => 'now', + 'default_end_date' => 'now', + ], + $config_entity['default_value'][0], 'Default value has been stored successfully'); // Clear field cache in order to avoid stale cache values. \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions(); @@ -1063,7 +1074,13 @@ public function testDefaultValue() { // Check if default_date has been stored successfully. $config_entity = $this->config('field.field.node.date_content.' . $field_name)->get(); - $this->assertEquals(['default_date_type' => 'relative', 'default_date' => '+45 days', 'default_end_date_type' => 'relative', 'default_end_date' => '+90 days'], $config_entity['default_value'][0], 'Default value has been stored successfully'); + $this->assertEquals([ + 'default_date_type' => 'relative', + 'default_date' => '+45 days', + 'default_end_date_type' => 'relative', + 'default_end_date' => '+90 days', + ], + $config_entity['default_value'][0], 'Default value has been stored successfully'); // Clear field cache in order to avoid stale cache values. \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions(); @@ -1398,4 +1415,170 @@ public function testDateStorageSettings() { $this->assertSession()->elementsCount('xpath', "//*[@name='field_storage[subform][settings][datetime_type]' and contains(@disabled, 'disabled')]", 1); } + /** + * Tests displaying dates with the 'from_to' setting. + * + * @dataProvider fromToSettingDataProvider + */ + public function testFromToSetting(array $expected, string $datetime_type, string $field_formatter_type, array $display_settings = []): void { + $field_name = $this->fieldStorage->getName(); + + // Create a test content type. + $this->drupalCreateContentType(['type' => 'date_content']); + + // Ensure the field to a datetime field. + $this->fieldStorage->setSetting('datetime_type', $datetime_type); + $this->fieldStorage->save(); + + // 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(); + + $edit = [ + "{$field_name}[0][value][date]" => $start_date->format($date_format), + "{$field_name}[0][end_value][date]" => $end_date->format($date_format), + ]; + + // Supply time as well when field is a datetime field. + if ($datetime_type === DateRangeItem::DATETIME_TYPE_DATETIME) { + $time_format = DateFormat::load('html_time')->getPattern(); + $edit["{$field_name}[0][value][time]"] = $start_date->format($time_format); + $edit["{$field_name}[0][end_value][time]"] = $end_date->format($time_format); + } + + $this->drupalGet('entity_test/add'); + $this->submitForm($edit, t('Save')); + preg_match('|entity_test/manage/(\d+)|', $this->getUrl(), $match); + $id = $match[1]; + $this->assertSession()->pageTextContains(t('entity_test @id has been created.', ['@id' => $id])); + + // Now set display options. + $this->displayOptions = [ + 'type' => $field_formatter_type, + 'label' => 'hidden', + 'settings' => $display_settings + [ + 'format_type' => 'short', + 'separator' => 'THE_SEPARATOR', + ] + $this->defaultSettings, + ]; + + \Drupal::service('entity_display.repository')->getViewDisplay( + $this->field->getTargetEntityTypeId(), + $this->field->getTargetBundle(), + 'full') + ->setComponent($field_name, $this->displayOptions) + ->save(); + + $output = $this->renderTestEntity($id); + foreach ($expected as $content => $is_expected) { + if ($is_expected) { + $this->assertStringContainsString($content, $output); + } + else { + $this->assertStringNotContainsString($content, $output); + } + } + } + + /** + * The data provider for testing the 'from_to' setting. + * + * @return array + * An array of date settings to test the behavior of the 'from_to' setting. + */ + public static function fromToSettingDataProvider(): array { + $datetime_types = [ + DateRangeItem::DATETIME_TYPE_DATE => [ + 'daterange_default' => [ + DateTimeRangeConstantsInterface::START_DATE => '12/31/2012', + DateTimeRangeConstantsInterface::END_DATE => '06/06/2013', + ], + 'daterange_plain' => [ + DateTimeRangeConstantsInterface::START_DATE => '2012-12-31', + DateTimeRangeConstantsInterface::END_DATE => '2013-06-06', + ], + 'daterange_custom' => [ + DateTimeRangeConstantsInterface::START_DATE => '2012-12-31', + DateTimeRangeConstantsInterface::END_DATE => '2013-06-06', + ], + ], + DateRangeItem::DATETIME_TYPE_DATETIME => [ + 'daterange_default' => [ + DateTimeRangeConstantsInterface::START_DATE => '12/31/2012 - 00:00', + DateTimeRangeConstantsInterface::END_DATE => '06/06/2013 - 00:00', + ], + 'daterange_plain' => [ + DateTimeRangeConstantsInterface::START_DATE => '2012-12-31T00:00:00', + DateTimeRangeConstantsInterface::END_DATE => '2013-06-06T00:00:00', + ], + 'daterange_custom' => [ + DateTimeRangeConstantsInterface::START_DATE => '2012-12-31T00:00:00', + DateTimeRangeConstantsInterface::END_DATE => '2013-06-06T00:00:00', + ], + ], + DateRangeItem::DATETIME_TYPE_ALLDAY => [ + 'daterange_default' => [ + DateTimeRangeConstantsInterface::START_DATE => '12/31/2012', + DateTimeRangeConstantsInterface::END_DATE => '06/06/2013', + ], + 'daterange_plain' => [ + DateTimeRangeConstantsInterface::START_DATE => '2012-12-31', + DateTimeRangeConstantsInterface::END_DATE => '2013-06-06', + ], + 'daterange_custom' => [ + DateTimeRangeConstantsInterface::START_DATE => '2012-12-31', + DateTimeRangeConstantsInterface::END_DATE => '2013-06-06', + ], + ], + ]; + + $return = []; + $separator = ' THE_SEPARATOR '; + foreach ($datetime_types as $datetime_type => $field_formatters) { + foreach ($field_formatters as $field_formatter_type => $dates) { + // Both start and end date. + $return[$datetime_type . '-' . $field_formatter_type . '-both'] = [ + 'expected' => [ + $dates[DateTimeRangeConstantsInterface::START_DATE] => TRUE, + $separator => TRUE, + $dates[DateTimeRangeConstantsInterface::END_DATE] => TRUE, + ], + 'datetime_type' => $datetime_type, + 'field_formatter_type' => $field_formatter_type, + ]; + + // Only start date. + $return[$datetime_type . '-' . $field_formatter_type . '-start_date'] = [ + 'expected' => [ + $dates[DateTimeRangeConstantsInterface::START_DATE] => TRUE, + $separator => FALSE, + $dates[DateTimeRangeConstantsInterface::END_DATE] => FALSE, + ], + 'datetime_type' => $datetime_type, + 'field_formatter_type' => $field_formatter_type, + ['from_to' => DateTimeRangeConstantsInterface::START_DATE], + ]; + + // Only end date. + $return[$datetime_type . '-' . $field_formatter_type . '-end_date'] = [ + 'expected' => [ + $dates[DateTimeRangeConstantsInterface::START_DATE] => FALSE, + $separator => FALSE, + $dates[DateTimeRangeConstantsInterface::END_DATE] => TRUE, + ], + 'datetime_type' => $datetime_type, + 'field_formatter_type' => $field_formatter_type, + ['from_to' => DateTimeRangeConstantsInterface::END_DATE], + ]; + } + } + + return $return; + } + } diff --git a/core/modules/datetime_range/tests/src/Functional/DateRangeFormatterSettingsUpdateTest.php b/core/modules/datetime_range/tests/src/Functional/DateRangeFormatterSettingsUpdateTest.php new file mode 100644 index 000000000000..841eb93bc28c --- /dev/null +++ b/core/modules/datetime_range/tests/src/Functional/DateRangeFormatterSettingsUpdateTest.php @@ -0,0 +1,57 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Tests\datetime_range\Functional; + +use Drupal\FunctionalTests\Update\UpdatePathTestBase; + +/** + * Tests the update path for daterange formatter settings. + * + * @group datetime + */ +class DateRangeFormatterSettingsUpdateTest extends UpdatePathTestBase { + + /** + * {@inheritdoc} + */ + protected static $modules = [ + 'node', + 'datetime', + 'datetime_range', + ]; + + /** + * {@inheritdoc} + */ + protected $defaultTheme = 'stark'; + + /** + * {@inheritdoc} + */ + protected function setDatabaseDumpFiles(): void { + $this->databaseDumpFiles = [ + __DIR__ . '/../../../../system/tests/fixtures/update/drupal-9.4.0.bare.standard.php.gz', + __DIR__ . '/../../fixtures/update/drupal.daterange-formatter-settings-2827055.php', + ]; + } + + /** + * Tests update path for the 'from_to' formatter setting. + * + * @covers \datetime_range_post_update_from_to_configuration + */ + public function testPostUpdateDateRangeFormatter(): void { + $config_factory = \Drupal::configFactory(); + // Check that 'from_to' is missing before update. + $settings = $config_factory->get('core.entity_view_display.node.page.default')->get('content.field_datetime_range.settings'); + $this->assertArrayNotHasKey('from_to', $settings); + + $this->runUpdates(); + + $settings = $config_factory->get('core.entity_view_display.node.page.default')->get('content.field_datetime_range.settings'); + $this->assertArrayHasKey('from_to', $settings); + } + +} diff --git a/core/modules/datetime_range/tests/src/FunctionalJavascript/DateRangeFieldTest.php b/core/modules/datetime_range/tests/src/FunctionalJavascript/DateRangeFieldTest.php new file mode 100644 index 000000000000..db42c26ec593 --- /dev/null +++ b/core/modules/datetime_range/tests/src/FunctionalJavascript/DateRangeFieldTest.php @@ -0,0 +1,97 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Tests\datetime_range\FunctionalJavascript; + +use Drupal\datetime_range\DateTimeRangeConstantsInterface; +use Drupal\datetime_range\Plugin\Field\FieldType\DateRangeItem; +use Drupal\field\Entity\FieldConfig; +use Drupal\field\Entity\FieldStorageConfig; +use Drupal\FunctionalJavascriptTests\WebDriverTestBase; + +/** + * Tests Daterange field. + * + * @group datetime + */ +class DateRangeFieldTest extends WebDriverTestBase { + + /** + * {@inheritdoc} + */ + protected static $modules = ['node', 'entity_test', 'field_ui', 'datetime', 'datetime_range']; + + /** + * {@inheritdoc} + */ + protected $defaultTheme = 'stark'; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + + $this->drupalLogin($this->drupalCreateUser([ + 'view test entity', + 'administer entity_test content', + 'administer content types', + 'administer node fields', + 'administer node display', + 'bypass node access', + 'administer entity_test fields', + ])); + } + + /** + * Tests the conditional visibility of the 'Date separator' field. + */ + public function testFromToSeparatorState(): void { + $field_name = $this->randomMachineName(); + $this->drupalCreateContentType(['type' => 'date_content']); + $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(); + \Drupal::service('entity_display.repository')->getViewDisplay('node', 'date_content') + ->setComponent($field_name, [ + 'type' => 'daterange_default', + 'label' => 'hidden', + 'settings' => [ + 'format_type' => 'short', + 'separator' => 'THE_SEPARATOR', + ], + ]) + ->save(); + $this->drupalGet("admin/structure/types/manage/date_content/display"); + + $page = $this->getSession()->getPage(); + $page->pressButton("{$field_name}_settings_edit"); + $this->assertSession()->waitForElement('css', '.ajax-new-content'); + + $from_to_locator = 'fields[' . $field_name . '][settings_edit_form][settings][from_to]'; + $separator = $page->findField('Date separator'); + + // Assert that date separator field is visible if 'from_to' is set to + // BOTH. + $this->assertSession()->fieldValueEquals($from_to_locator, DateTimeRangeConstantsInterface::BOTH); + $this->assertTrue($separator->isVisible()); + // Assert that the date separator is not visible if 'from_to' is set to + // START_DATE or END_DATE. + $page->selectFieldOption($from_to_locator, DateTimeRangeConstantsInterface::START_DATE); + $this->assertFalse($separator->isVisible()); + $page->selectFieldOption($from_to_locator, DateTimeRangeConstantsInterface::END_DATE); + $this->assertFalse($separator->isVisible()); + } + +} -- GitLab