diff --git a/core/lib/Drupal/Core/Datetime/Element/Datetime.php b/core/lib/Drupal/Core/Datetime/Element/Datetime.php index d005475..812087a 100644 --- a/core/lib/Drupal/Core/Datetime/Element/Datetime.php +++ b/core/lib/Drupal/Core/Datetime/Element/Datetime.php @@ -61,6 +61,7 @@ public function getInfo() { '#date_year_range' => '1900:2050', '#date_increment' => 1, '#date_timezone' => '', + '#expose_timezone' => FALSE, ]; } @@ -73,6 +74,11 @@ public static function valueCallback(&$element, $input, FormStateInterface $form $time_input = $element['#date_time_element'] != 'none' && !empty($input['time']) ? $input['time'] : ''; $date_format = $element['#date_date_element'] != 'none' ? static::getHtml5DateFormat($element) : ''; $time_format = $element['#date_time_element'] != 'none' ? static::getHtml5TimeFormat($element) : ''; + + // Timezone. + if ($element['#expose_timezone'] && $input['timezone']) { + $element['#date_timezone'] = $input['timezone']; + } $timezone = !empty($element['#date_timezone']) ? $element['#date_timezone'] : NULL; // Seconds will be omitted in a post in case there's no entry. @@ -91,6 +97,7 @@ public static function valueCallback(&$element, $input, FormStateInterface $form $input = [ 'date' => $date_input, 'time' => $time_input, + 'timezone' => $timezone, 'object' => $date, ]; } @@ -100,6 +107,7 @@ public static function valueCallback(&$element, $input, FormStateInterface $form $input = [ 'date' => $date->format($element['#date_date_format']), 'time' => $date->format($element['#date_time_format']), + 'timezone' => $date->getTimezone()->getName(), 'object' => $date, ]; } @@ -107,6 +115,7 @@ public static function valueCallback(&$element, $input, FormStateInterface $form $input = [ 'date' => '', 'time' => '', + 'timezone' => '', 'object' => NULL, ]; } @@ -190,6 +199,8 @@ public static function valueCallback(&$element, $input, FormStateInterface $form * provided, this value will be ignored, the timezone in the default date * takes precedence. Defaults to the value returned by * drupal_get_user_timezone(). + * - #expose_timezone: a boolean that if set to TRUE, will expose a timezone + * select list. Defaults to FALSE. * * Example usage: * @code @@ -308,6 +319,17 @@ public static function processDatetime(&$element, FormStateInterface $form_state } } + // Expose a timezone selector. + if ($element['#expose_timezone']) { + $element['timezone'] = array( + '#type' => 'select', + '#options' => array_combine(\DateTimeZone::listIdentifiers(), \DateTimeZone::listIdentifiers()), + // Default to user's timezone. + '#default_value' => $element['#date_timezone'], + '#required' => $element['#required'], + ); + } + return $element; } diff --git a/core/modules/datetime/config/schema/datetime.schema.yml b/core/modules/datetime/config/schema/datetime.schema.yml index a009011..7e01885 100644 --- a/core/modules/datetime/config/schema/datetime.schema.yml +++ b/core/modules/datetime/config/schema/datetime.schema.yml @@ -7,6 +7,9 @@ field.storage_settings.datetime: datetime_type: type: string label: 'Date type' + timezone_handling: + type: string + label: 'Timezone handling' field.field_settings.datetime: type: mapping @@ -26,6 +29,9 @@ field.value.datetime: field.formatter.settings.datetime_base: type: mapping mapping: + timezone_display: + type: string + label: 'Timezone display' timezone_override: type: string label: 'Time zone override' diff --git a/core/modules/datetime/datetime.install b/core/modules/datetime/datetime.install new file mode 100644 index 0000000..def4300 --- /dev/null +++ b/core/modules/datetime/datetime.install @@ -0,0 +1,101 @@ +getDefinitions() as $entity_type_id => $entity_type) { + $storage = \Drupal::entityTypeManager()->getStorage($entity_type_id); + if ($storage instanceof DynamicallyFieldableEntityStorageSchemaInterface) { + $field_changes = []; + $field_storage_definitions = \Drupal::service('entity_field.manager')->getFieldStorageDefinitions($entity_type_id); + $original_storage_definitions = \Drupal::service('entity.last_installed_schema.repository')->getLastInstalledFieldStorageDefinitions($entity_type_id); + foreach (array_intersect_key($field_storage_definitions, $original_storage_definitions) as $field_name => $storage_definition) { + if ($storage_definition->getType() === 'datetime' && $storage->requiresFieldStorageSchemaChanges($storage_definition, $original_storage_definitions[$field_name])) { + /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */ + $table_mapping = $storage->getTableMapping(); + $field_changes[$field_name] = $table_mapping->getFieldTableName($field_name); + } + } + + if (!empty($field_changes)) { + + $change_list[$entity_type_id] = [ + 'field_storage_definitions' => $field_changes, + 'revision_table' => $entity_type->getRevisionTable(), + ]; + } + } + } + + $sandbox['change_list'] = $change_list; + $sandbox['max'] = count($change_list); + } + + $remaining = array_diff(array_keys($sandbox['change_list']), $sandbox['processed']); + if (!empty($remaining)) { + $entity_type_id = array_pop($remaining); + $field_changes = $sandbox['change_list'][$entity_type_id]; + + $field_spec = [ + 'description' => 'The date timezone.', + 'type' => 'varchar', + 'length' => 50, + ]; + $schema = Database::getConnection()->schema(); + + foreach ($field_changes['field_storage_definitions'] as $field_name => $field_table) { + $timezone_field_name = $field_name . '_timezone'; + $field_schema = [ + 'fields' => [ + // Include the 'value' field in order to properly create the index. + $field_name . '_value' => [ + 'description' => 'The date value.', + 'type' => 'varchar', + 'length' => 20, + ], + $timezone_field_name => $field_spec, + ], + 'indexes' => [ + 'value_timezone' => [$field_name . '_value', $timezone_field_name], + ], + ]; + $schema->addField($field_table, $timezone_field_name, $field_spec); + $schema->addIndex($field_table, 'value_timezone', $field_schema['indexes']['value_timezone'], $field_schema); + + if ($field_changes['revision_table']) { + $revision_table = $field_changes['revision_table'] . '__' . $field_name; + $schema->addField($revision_table, $timezone_field_name, $field_spec); + $schema->addIndex($revision_table, 'value_timezone', $field_schema['indexes']['value_timezone'], $field_schema); + } + + // Set timezone handling to 'user', which is the behavior prior to the + // introduction of configurable timezone handling. + $field_storage = FieldStorageConfig::load($entity_type_id . '.' . $field_name); + $field_storage->setSetting('timezone_handling', DateTimeItem::TIMEZONE_USER); + $field_storage->save(); + \Drupal::entityDefinitionUpdateManager()->updateFieldStorageDefinition($field_storage); + } + $sandbox['processed'][] = $entity_type_id; + } + + $sandbox['#finished'] = empty($remaining); +} diff --git a/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimeCustomFormatter.php b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimeCustomFormatter.php index 984423d..75bbaff 100644 --- a/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimeCustomFormatter.php +++ b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimeCustomFormatter.php @@ -5,6 +5,7 @@ use Drupal\Core\Datetime\DrupalDateTime; use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Form\FormStateInterface; +use Drupal\datetime\Plugin\Field\FieldType\DateTimeItem; /** * Plugin implementation of the 'Custom' formatter for 'datetime' fields. @@ -42,7 +43,7 @@ public function viewElements(FieldItemListInterface $items, $langcode) { /** @var \Drupal\Core\Datetime\DrupalDateTime $date */ $date = $item->date; - $elements[$delta] = $this->buildDate($date); + $elements[$delta] = $this->buildDate($date, $item->timezone); } } @@ -54,8 +55,7 @@ public function viewElements(FieldItemListInterface $items, $langcode) { */ protected function formatDate($date) { $format = $this->getSetting('date_format'); - $timezone = $this->getSetting('timezone_override'); - return $this->dateFormatter->format($date->getTimestamp(), 'custom', $format, $timezone != '' ? $timezone : NULL); + return $this->dateFormatter->format($date->getTimestamp(), 'custom', $format, $date->getTimezone()->getName()); } /** @@ -82,7 +82,7 @@ public function settingsSummary() { $date = new DrupalDateTime(); $this->setTimeZone($date); - $summary[] = $date->format($this->getSetting('date_format'), $this->getFormatSettings()); + $summary[] = $date->format($this->getSetting('date_format')); return $summary; } diff --git a/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimeDefaultFormatter.php b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimeDefaultFormatter.php index 0b75b4e..3c9ac51 100644 --- a/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimeDefaultFormatter.php +++ b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimeDefaultFormatter.php @@ -4,6 +4,7 @@ use Drupal\Core\Datetime\DrupalDateTime; use Drupal\Core\Form\FormStateInterface; +use Drupal\datetime\Plugin\Field\FieldType\DateTimeItem; /** * Plugin implementation of the 'Default' formatter for 'datetime' fields. @@ -32,8 +33,7 @@ public static function defaultSettings() { */ protected function formatDate($date) { $format_type = $this->getSetting('format_type'); - $timezone = $this->getSetting('timezone_override') ?: $date->getTimezone()->getName(); - return $this->dateFormatter->format($date->getTimestamp(), $format_type, '', $timezone != '' ? $timezone : NULL); + return $this->dateFormatter->format($date->getTimestamp(), $format_type, '', $date->getTimezone()->getName()); } /** @@ -68,7 +68,8 @@ public function settingsSummary() { $summary = parent::settingsSummary(); $date = new DrupalDateTime(); - $summary[] = t('Format: @display', ['@display' => $this->formatDate($date, $this->getFormatSettings())]); + $this->setTimeZone($date); + $summary[] = t('Format: @display', ['@display' => $this->formatDate($date)]); return $summary; } diff --git a/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimeFormatterBase.php b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimeFormatterBase.php index 71b9467..aa700d2 100644 --- a/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimeFormatterBase.php +++ b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimeFormatterBase.php @@ -13,7 +13,6 @@ use Drupal\datetime\Plugin\Field\FieldType\DateTimeItem; use Symfony\Component\DependencyInjection\ContainerInterface; - /** * Base class for 'DateTime Field formatter' plugin implementations. */ @@ -84,6 +83,7 @@ public static function create(ContainerInterface $container, array $configuratio */ public static function defaultSettings() { return [ + 'timezone_display' => DateTimeItem::TIMEZONE_USER, 'timezone_override' => '', ] + parent::defaultSettings(); } @@ -94,13 +94,38 @@ public static function defaultSettings() { public function settingsForm(array $form, FormStateInterface $form_state) { $form = parent::settingsForm($form, $form_state); - $form['timezone_override'] = [ - '#type' => 'select', - '#title' => $this->t('Time zone override'), - '#description' => $this->t('The time zone selected here will always be used'), - '#options' => system_time_zones(TRUE), - '#default_value' => $this->getSetting('timezone_override'), - ]; + // Timezone display is only applicable to datetime items. + if ($this->fieldDefinition->getFieldStorageDefinition()->getSetting('datetime_type') === DateTimeItem::DATETIME_TYPE_DATETIME) { + $form['timezone_display'] = [ + '#type' => 'select', + '#title' => $this->t('Timezone display'), + '#description' => $this->t('The timezone to use when displaying this date.'), + '#options' => [ + DateTimeItem::TIMEZONE_USER => $this->t("The user's timezone"), + DateTimeItem::TIMEZONE_NONE => $this->t("Timezone override"), + ], + '#default_value' => $this->getSetting('timezone_display'), + ]; + + // If this field is using per-date timezone storage, add that as an + // option. + if ($this->fieldDefinition->getFieldStorageDefinition()->getSetting('timezone_handling') === DateTimeItem::TIMEZONE_DATE) { + $form['timezone_display']['#options'][DateTimeItem::TIMEZONE_DATE] = $this->t("The date's timezone"); + } + + $form['timezone_override'] = [ + '#type' => 'select', + '#title' => $this->t('Time zone override'), + '#description' => $this->t('The time zone selected here will always be used'), + '#options' => system_time_zones(TRUE), + '#default_value' => $this->getSetting('timezone_override'), + '#states' => [ + 'visible' => [ + ':input[name="fields[' . $this->fieldDefinition->getName() . '][settings_edit_form][settings][timezone_display]"]' => ['value' => DateTimeItem::TIMEZONE_NONE], + ], + ], + ]; + } return $form; } @@ -111,8 +136,16 @@ public function settingsForm(array $form, FormStateInterface $form_state) { public function settingsSummary() { $summary = parent::settingsSummary(); - if ($override = $this->getSetting('timezone_override')) { - $summary[] = $this->t('Time zone: @timezone', ['@timezone' => $override]); + if ($this->fieldDefinition->getFieldStorageDefinition()->getSetting('datetime_type') === DateTimeItem::DATETIME_TYPE_DATETIME) { + $timezone_display = $this->getSetting('timezone_display'); + $timezone_override = $this->getSetting('timezone_override'); + if ($timezone_display === DateTimeItem::TIMEZONE_NONE && $timezone_override) { + $summary[] = $this->t('Time zone: @timezone', ['@timezone' => $timezone_override]); + } + else { + // @todo Make human-readable. + $summary[] = $this->t('Time zone display: @timezone', ['@timezone' => $timezone_display]); + } } return $summary; @@ -128,7 +161,7 @@ public function viewElements(FieldItemListInterface $items, $langcode) { if ($item->date) { /** @var \Drupal\Core\Datetime\DrupalDateTime $date */ $date = $item->date; - $elements[$delta] = $this->buildDateWithIsoAttribute($date); + $elements[$delta] = $this->buildDateWithIsoAttribute($date, $item->timezone); if (!empty($item->_attributes)) { $elements[$delta]['#attributes'] += $item->_attributes; @@ -154,59 +187,57 @@ public function viewElements(FieldItemListInterface $items, $langcode) { abstract protected function formatDate($date); /** - * Sets the proper time zone on a DrupalDateTime object for the current user. + * Sets the proper time zone on a DrupalDateTime object. * * A DrupalDateTime object loaded from the database will have the UTC time - * zone applied to it. This method will apply the time zone for the current - * user, based on system and user settings. + * zone applied to it. This method applies the proper timezone based on + * the formatter configuration. * * @see drupal_get_user_timezone() * * @param \Drupal\Core\Datetime\DrupalDateTime $date * A DrupalDateTime object. + * @param string $date_instance_timezone + * (optional) The timezone associated with the specific date field instance. */ - protected function setTimeZone(DrupalDateTime $date) { + protected function setTimeZone(DrupalDateTime $date, $date_instance_timezone = NULL) { if ($this->getFieldSetting('datetime_type') === DateTimeItem::DATETIME_TYPE_DATE) { // A date without time has no timezone conversion. $timezone = DATETIME_STORAGE_TIMEZONE; } else { - $timezone = drupal_get_user_timezone(); + $timezone_display = $this->getSetting('timezone_display'); + $timezone_override = $this->getSetting('timezone_override'); + if ($timezone_display === DateTimeItem::TIMEZONE_DATE && !empty($date_instance_timezone)) { + $timezone = $date_instance_timezone; + } + elseif ($timezone_display === DateTimeItem::TIMEZONE_NONE && $timezone_override) { + $timezone = $timezone_override; + } + else { + $timezone = drupal_get_user_timezone(); + } } $date->setTimeZone(timezone_open($timezone)); } /** - * Gets a settings array suitable for DrupalDateTime::format(). - * - * @return array - * The settings array that can be passed to DrupalDateTime::format(). - */ - protected function getFormatSettings() { - $settings = []; - - if ($this->getSetting('timezone_override') != '') { - $settings['timezone'] = $this->getSetting('timezone_override'); - } - - return $settings; - } - - /** * Creates a render array from a date object. * * @param \Drupal\Core\Datetime\DrupalDateTime $date * A date object. + * @param string $timezone + * (optional) A timezone to explicitly set the date to. * * @return array * A render array. */ - protected function buildDate(DrupalDateTime $date) { + protected function buildDate(DrupalDateTime $date, $timezone = NULL) { 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); + $this->setTimeZone($date, $timezone); $build = [ '#markup' => $this->formatDate($date), @@ -225,11 +256,13 @@ protected function buildDate(DrupalDateTime $date) { * * @param \Drupal\Core\Datetime\DrupalDateTime $date * A date object. + * @param string $timezone + * (optional) A timezone to explicitly set the date to. * * @return array * A render array. */ - protected function buildDateWithIsoAttribute(DrupalDateTime $date) { + protected function buildDateWithIsoAttribute(DrupalDateTime $date, $timezone = NULL) { 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); @@ -238,7 +271,7 @@ protected function buildDateWithIsoAttribute(DrupalDateTime $date) { // Create the ISO date in Universal Time. $iso_date = $date->format("Y-m-d\TH:i:s") . 'Z'; - $this->setTimeZone($date); + $this->setTimeZone($date, $timezone); $build = [ '#theme' => 'time', diff --git a/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimePlainFormatter.php b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimePlainFormatter.php index 7f0dee2..10cc9fd 100644 --- a/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimePlainFormatter.php +++ b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimePlainFormatter.php @@ -29,7 +29,7 @@ public function viewElements(FieldItemListInterface $items, $langcode) { /** @var \Drupal\Core\Datetime\DrupalDateTime $date */ $date = $item->date; - $elements[$delta] = $this->buildDate($date); + $elements[$delta] = $this->buildDate($date, $item->timezone); } } @@ -41,8 +41,7 @@ public function viewElements(FieldItemListInterface $items, $langcode) { */ protected function formatDate($date) { $format = $this->getFieldSetting('datetime_type') == DateTimeItem::DATETIME_TYPE_DATE ? DATETIME_DATE_STORAGE_FORMAT : DATETIME_DATETIME_STORAGE_FORMAT; - $timezone = $this->getSetting('timezone_override'); - return $this->dateFormatter->format($date->getTimestamp(), 'custom', $format, $timezone != '' ? $timezone : NULL); + return $this->dateFormatter->format($date->getTimestamp(), 'custom', $format, $date->getTimezone()->getName()); } } diff --git a/core/modules/datetime/src/Plugin/Field/FieldType/DateTimeItem.php b/core/modules/datetime/src/Plugin/Field/FieldType/DateTimeItem.php index f5c06e6..e53467a 100644 --- a/core/modules/datetime/src/Plugin/Field/FieldType/DateTimeItem.php +++ b/core/modules/datetime/src/Plugin/Field/FieldType/DateTimeItem.php @@ -28,6 +28,7 @@ class DateTimeItem extends FieldItemBase { public static function defaultStorageSettings() { return [ 'datetime_type' => 'datetime', + 'timezone_handling' => static::TIMEZONE_USER, ] + parent::defaultStorageSettings(); } @@ -42,6 +43,28 @@ public static function defaultStorageSettings() { const DATETIME_TYPE_DATETIME = 'datetime'; /** + * Timezone uses the site's timezone, regardless of the user's timezone. + */ + const TIMEZONE_SITE = 'site'; + + /** + * Timezone uses the user's timezone. + * + * @see drupal_get_user_timezone() + */ + const TIMEZONE_USER = 'user'; + + /** + * Timezone is set per-date. + */ + const TIMEZONE_DATE = 'date'; + + /** + * No timezone conversion is performed. + */ + const TIMEZONE_NONE = 'none'; + + /** * {@inheritdoc} */ public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) { @@ -56,6 +79,9 @@ public static function propertyDefinitions(FieldStorageDefinitionInterface $fiel ->setClass('\Drupal\datetime\DateTimeComputed') ->setSetting('date source', 'value'); + $properties['timezone'] = DataDefinition::create('string') + ->setLabel(t('Timezone')); + return $properties; } @@ -63,18 +89,26 @@ public static function propertyDefinitions(FieldStorageDefinitionInterface $fiel * {@inheritdoc} */ public static function schema(FieldStorageDefinitionInterface $field_definition) { - return [ + $schema = [ 'columns' => [ 'value' => [ 'description' => 'The date value.', 'type' => 'varchar', 'length' => 20, ], + 'timezone' => [ + 'description' => 'The date timezone', + 'type' => 'varchar', + 'length' => 50, + ], ], 'indexes' => [ 'value' => ['value'], + 'value_timezone' => ['value', 'timezone'], ], ]; + + return $schema; } /** @@ -95,6 +129,29 @@ public function storageSettingsForm(array &$form, FormStateInterface $form_state '#disabled' => $has_data, ]; + $element['timezone_handling'] = [ + '#type' => 'select', + '#title' => t('Timezone handling'), + '#options' => [ + static::TIMEZONE_SITE => $this->t("Site's timezone"), + static::TIMEZONE_DATE => $this->t("Date's timezone"), + static::TIMEZONE_USER => $this->t("User's timezone"), + static::TIMEZONE_NONE => $this->t('No timezone conversion'), + ], + '#default_value' => $this->getSetting('timezone_handling'), + '#required' => TRUE, + '#states' => [ + // Hide the field if this is a date-only field. + 'visible' => [ + ':input[name="settings[datetime_type]"]' => ['value' => static::DATETIME_TYPE_DATETIME], + ], + 'disabled' => [ + ':input[name="settings[datetime_type]"]' => ['value' => static::DATETIME_TYPE_DATE], + ], + ], + '#disabled' => $has_data, + ]; + return $element; } diff --git a/core/modules/datetime/src/Plugin/Field/FieldWidget/DateTimeWidgetBase.php b/core/modules/datetime/src/Plugin/Field/FieldWidget/DateTimeWidgetBase.php index f965e21..59a8a11 100644 --- a/core/modules/datetime/src/Plugin/Field/FieldWidget/DateTimeWidgetBase.php +++ b/core/modules/datetime/src/Plugin/Field/FieldWidget/DateTimeWidgetBase.php @@ -21,7 +21,7 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen '#type' => 'datetime', '#default_value' => NULL, '#date_increment' => 1, - '#date_timezone' => drupal_get_user_timezone(), + '#date_timezone' => $items[$delta]->timezone ?: drupal_get_user_timezone(), '#required' => $element['#required'], ]; @@ -31,7 +31,12 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen $element['value']['#date_timezone'] = DATETIME_STORAGE_TIMEZONE; } + if ($this->getFieldSetting('timezone_handling') === DateTimeItem::TIMEZONE_DATE) { + $element['value']['#expose_timezone'] = TRUE; + } + if ($items[$delta]->date) { + /** @var \Drupal\Core\Datetime\DrupalDateTime $date */ $date = $items[$delta]->date; // The date was created and verified during field_load(), so it is safe to // use without further inspection. @@ -69,8 +74,20 @@ public function massageFormValues(array $values, array $form, FormStateInterface $format = DATETIME_DATETIME_STORAGE_FORMAT; break; } - // Adjust the date for storage. - $date->setTimezone(new \DateTimezone(DATETIME_STORAGE_TIMEZONE)); + + // Store the timezone if set. + if ($this->getFieldSetting('timezone_handling') === DateTimeItem::TIMEZONE_DATE) { + $item['timezone'] = $date->getTimezone()->getName(); + } + else { + $item['timezone'] = ''; + } + + // Adjust the date for storage once validation is complete. + if ($form_state->isValidationComplete()) { + $date->setTimezone(new \DateTimezone(DATETIME_STORAGE_TIMEZONE)); + } + $item['value'] = $date->format($format); } } diff --git a/core/modules/datetime/tests/src/Functional/DateTimeFieldTest.php b/core/modules/datetime/tests/src/Functional/DateTimeFieldTest.php index ab03ff5..399e3d2 100644 --- a/core/modules/datetime/tests/src/Functional/DateTimeFieldTest.php +++ b/core/modules/datetime/tests/src/Functional/DateTimeFieldTest.php @@ -7,6 +7,7 @@ use Drupal\Component\Utility\Unicode; use Drupal\Core\Datetime\DrupalDateTime; use Drupal\Core\Datetime\Entity\DateFormat; +use Drupal\datetime\Plugin\Field\FieldType\DateTimeItem; use Drupal\entity_test\Entity\EntityTest; use Drupal\field\Entity\FieldConfig; use Drupal\field\Entity\FieldStorageConfig; @@ -24,7 +25,10 @@ class DateTimeFieldTest extends DateTestBase { * * @var array */ - protected $defaultSettings = ['timezone_override' => '']; + protected $defaultSettings = [ + 'timezone_display' => DateTimeItem::TIMEZONE_USER, + 'timezone_override' => '', + ]; /** * {@inheritdoc} @@ -196,7 +200,7 @@ public function testDateField() { ->setComponent($field_name, $this->displayOptions) ->save(); $expected = SafeMarkup::format($this->displayOptions['settings']['future_format'], [ - '@interval' => $this->dateFormatter->formatTimeDiffUntil($timestamp, ['granularity' => $this->displayOptions['settings']['granularity']]) + '@interval' => $this->dateFormatter->formatTimeDiffUntil($timestamp, ['granularity' => $this->displayOptions['settings']['granularity'],]) ]); $output = $this->renderTestEntity($id); $this->assertContains((string) $expected, $output, SafeMarkup::format('Formatted date field using datetime_time_ago format displayed as %expected.', ['%expected' => $expected])); @@ -290,7 +294,11 @@ public function testDatetimeField() { // Verify that the 'timezone_override' setting works. $this->displayOptions['type'] = 'datetime_custom'; - $this->displayOptions['settings'] = ['date_format' => 'm/d/Y g:i:s A', 'timezone_override' => 'America/New_York'] + $this->defaultSettings; + $this->displayOptions['settings'] = [ + 'date_format' => 'm/d/Y g:i:s A', + 'timezone_override' => 'America/New_York', + 'timezone_display' => DateTimeItem::TIMEZONE_NONE, + ] + $this->defaultSettings; entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full') ->setComponent($field_name, $this->displayOptions) ->save(); @@ -345,6 +353,32 @@ public function testDatetimeField() { ]); $output = $this->renderTestEntity($id); $this->assertContains((string) $expected, $output, SafeMarkup::format('Formatted date field using datetime_time_ago format displayed as %expected.', ['%expected' => $expected])); + + // Verify timezone display settings. + $this->fieldStorage->setSetting('timezone_handling', DateTimeItem::TIMEZONE_DATE); + $this->fieldStorage->save(); + + $edit = [ + "{$field_name}[0][value][date]" => $date->format($date_format, ['timezone' => 'America/New_York']), + "{$field_name}[0][value][time]" => $date->format($time_format, ['timezone' => 'America/New_York']), + "{$field_name}[0][value][timezone]" => 'America/New_York', + ]; + $this->drupalPostForm('entity_test/add', $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->displayOptions['type'] = 'datetime_custom'; + $this->displayOptions['settings'] = [ + 'date_format' => 'm/d/Y g:i:s A e', + 'timezone_display' => DateTimeItem::TIMEZONE_DATE, + ] + $this->defaultSettings; + entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full') + ->setComponent($field_name, $this->displayOptions) + ->save(); + $output = $this->renderTestEntity($id); + $expected = $date->format($this->displayOptions['settings']['date_format'], ['timezone' => 'America/New_York']); + $this->assertContains((string) $expected, $output); } /** @@ -727,6 +761,7 @@ public function testInvalidField() { $this->drupalGet('entity_test/add'); $this->assertFieldByName("{$field_name}[0][value][date]", '', 'Date element found.'); $this->assertFieldByName("{$field_name}[0][value][time]", '', 'Time element found.'); + $this->assertNoFieldByName("{$field_name}[0][timezone]", '', 'No timezone field appears for dates that do not collect timezone information.'); // Submit invalid dates and ensure they is not accepted. $date_value = ''; diff --git a/core/modules/datetime/tests/src/Kernel/DateTimeItemTest.php b/core/modules/datetime/tests/src/Kernel/DateTimeItemTest.php index e28667f..2668ea6 100644 --- a/core/modules/datetime/tests/src/Kernel/DateTimeItemTest.php +++ b/core/modules/datetime/tests/src/Kernel/DateTimeItemTest.php @@ -46,7 +46,10 @@ protected function setUp() { 'field_name' => 'field_datetime', 'type' => 'datetime', 'entity_type' => 'entity_test', - 'settings' => ['datetime_type' => DateTimeItem::DATETIME_TYPE_DATETIME], + 'settings' => [ + 'datetime_type' => DateTimeItem::DATETIME_TYPE_DATETIME, + 'timezone_handling' => DateTimeItem::TIMEZONE_SITE, + ], ]); $this->fieldStorage->save(); $this->field = FieldConfig::create([ @@ -221,4 +224,28 @@ public function testSetValueProperty() { $this->assertEqual($entity->field_datetime[0]->value, $value, '"Value" property can be set directly.'); } + /** + * Tests DateTimeItem with per-date timezone handling. + */ + public function testTimezoneDate() { + /** @var \Drupal\field\FieldStorageConfigInterface $field_storage */ + $field_storage = FieldStorageConfig::load('entity_test.field_datetime'); + $field_storage->setSetting('timezone_handling', 'date'); + $field_storage->save(); + + // Use a non-UTC timezone. + $timezone = 'America/Yellowknife'; + + $entity = EntityTest::create(); + $value = '2014-01-01T20:00:00Z'; + + $entity->set('field_datetime', ['value' => $value, 'timezone' => $timezone]); + $entity->save(); + + // Load the entity. + $id = $entity->id(); + $entity = EntityTest::load($id); + $this->assertEqual($timezone, $entity->field_datetime[0]->timezone, '"timezone" property can be set.'); + } + } diff --git a/core/modules/datetime_range/src/Plugin/Field/FieldType/DateRangeItem.php b/core/modules/datetime_range/src/Plugin/Field/FieldType/DateRangeItem.php index 49c4dce..0d2b92e 100644 --- a/core/modules/datetime_range/src/Plugin/Field/FieldType/DateRangeItem.php +++ b/core/modules/datetime_range/src/Plugin/Field/FieldType/DateRangeItem.php @@ -54,6 +54,9 @@ public static function propertyDefinitions(FieldStorageDefinitionInterface $fiel ->setClass(DateTimeComputed::class) ->setSetting('date source', 'end_value'); + $properties['timezone'] = DataDefinition::create('string') + ->setLabel(t('Timezone')); + return $properties; } diff --git a/core/modules/datetime_range/tests/src/Functional/DateRangeFieldTest.php b/core/modules/datetime_range/tests/src/Functional/DateRangeFieldTest.php index 23ba8fa..415e5d2 100644 --- a/core/modules/datetime_range/tests/src/Functional/DateRangeFieldTest.php +++ b/core/modules/datetime_range/tests/src/Functional/DateRangeFieldTest.php @@ -6,6 +6,7 @@ use Drupal\Component\Utility\Unicode; use Drupal\Core\Datetime\DrupalDateTime; use Drupal\Core\Datetime\Entity\DateFormat; +use Drupal\datetime\Plugin\Field\FieldType\DateTimeItem; use Drupal\Tests\datetime\Functional\DateTestBase; use Drupal\datetime_range\Plugin\Field\FieldType\DateRangeItem; use Drupal\entity_test\Entity\EntityTest; @@ -347,7 +348,7 @@ public function testDatetimeRangeField() { // 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; + $this->displayOptions['settings'] = ['date_format' => 'm/d/Y g:i:s A', 'timezone_override' => 'America/New_York', 'timezone_display' => DateTimeItem::TIMEZONE_NONE] + $this->defaultSettings; entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full') ->setComponent($field_name, $this->displayOptions) ->save(); @@ -514,7 +515,7 @@ public function testAlldayRangeField() { // 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; + $this->displayOptions['settings'] = ['date_format' => 'm/d/Y g:i:s A', 'timezone_override' => 'America/New_York', 'timezone_display' => DateTimeItem::TIMEZONE_NONE] + $this->defaultSettings; entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full') ->setComponent($field_name, $this->displayOptions) ->save(); diff --git a/core/modules/datetime_range/tests/src/Kernel/DateRangeItemTest.php b/core/modules/datetime_range/tests/src/Kernel/DateRangeItemTest.php index 67e1b9d..a44623b 100644 --- a/core/modules/datetime_range/tests/src/Kernel/DateRangeItemTest.php +++ b/core/modules/datetime_range/tests/src/Kernel/DateRangeItemTest.php @@ -50,7 +50,10 @@ protected function setUp() { 'field_name' => Unicode::strtolower($this->randomMachineName()), 'entity_type' => 'entity_test', 'type' => 'daterange', - 'settings' => ['datetime_type' => DateRangeItem::DATETIME_TYPE_DATE], + 'settings' => [ + 'datetime_type' => DateRangeItem::DATETIME_TYPE_DATE, + 'timezone_handling' => DateRangeItem::TIMEZONE_SITE, + ], ]); $this->fieldStorage->save(); diff --git a/core/modules/field/tests/src/Kernel/Migrate/d6/MigrateFieldFormatterSettingsTest.php b/core/modules/field/tests/src/Kernel/Migrate/d6/MigrateFieldFormatterSettingsTest.php index fc81c49..c759252 100644 --- a/core/modules/field/tests/src/Kernel/Migrate/d6/MigrateFieldFormatterSettingsTest.php +++ b/core/modules/field/tests/src/Kernel/Migrate/d6/MigrateFieldFormatterSettingsTest.php @@ -3,6 +3,7 @@ namespace Drupal\Tests\field\Kernel\Migrate\d6; use Drupal\Core\Entity\Entity\EntityViewDisplay; +use Drupal\datetime\Plugin\Field\FieldType\DateTimeItem; use Drupal\Tests\migrate_drupal\Kernel\d6\MigrateDrupal6TestBase; /** @@ -88,10 +89,10 @@ public function testEntityDisplaySettings() { $expected['weight'] = 2; $expected['type'] = 'number_decimal'; $expected['settings'] = [ - 'scale' => 2, - 'decimal_separator' => '.', - 'thousand_separator' => ',', - 'prefix_suffix' => TRUE, + 'scale' => 2, + 'decimal_separator' => '.', + 'thousand_separator' => ',', + 'prefix_suffix' => TRUE, ]; $component = $display->getComponent('field_test_three'); $this->assertIdentical($expected, $component); @@ -151,7 +152,7 @@ public function testEntityDisplaySettings() { $this->assertIdentical($expected, $component); // Test date field. - $defaults = ['format_type' => 'fallback', 'timezone_override' => '']; + $defaults = ['format_type' => 'fallback', 'timezone_override' => '', 'timezone_display' => DateTimeItem::TIMEZONE_USER]; $expected['weight'] = 10; $expected['type'] = 'datetime_default'; $expected['settings'] = ['format_type' => 'fallback'] + $defaults;