diff --git a/core/lib/Drupal/Core/Datetime/Element/Datetime.php b/core/lib/Drupal/Core/Datetime/Element/Datetime.php index 44a3949..51a2b08 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 = array( 'date' => $date_input, 'time' => $time_input, + 'timezone' => $timezone, 'object' => $date, ); } @@ -100,6 +107,7 @@ public static function valueCallback(&$element, $input, FormStateInterface $form $input = array( '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 = array( '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 406a2fd..5da1701 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/src/Plugin/Field/FieldFormatter/DateTimeCustomFormatter.php b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimeCustomFormatter.php index 678ddc8..31bd1d1 100644 --- a/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimeCustomFormatter.php +++ b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimeCustomFormatter.php @@ -44,7 +44,7 @@ public function viewElements(FieldItemListInterface $items, $langcode) { // A date without time will pick up the current time, use the default. datetime_date_default_time($date); } - $this->setTimeZone($date); + $this->setTimeZone($date, $item->timezone); $output = $this->formatDate($date); } @@ -66,7 +66,7 @@ public function viewElements(FieldItemListInterface $items, $langcode) { */ protected function formatDate($date) { $format = $this->getSetting('date_format'); - $timezone = $this->getSetting('timezone_override'); + $timezone = $this->getSetting('timezone_override') ?: $date->getTimezone()->getName(); return $this->dateFormatter->format($date->getTimestamp(), 'custom', $format, $timezone != '' ? $timezone : NULL); } diff --git a/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimeDefaultFormatter.php b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimeDefaultFormatter.php index 03c92ae..b330179 100644 --- a/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimeDefaultFormatter.php +++ b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimeDefaultFormatter.php @@ -50,7 +50,7 @@ public function viewElements(FieldItemListInterface $items, $langcode) { // Create the ISO date in Universal Time. $iso_date = $date->format("Y-m-d\TH:i:s") . 'Z'; - $this->setTimeZone($date); + $this->setTimeZone($date, $item->timezone); $output = $this->formatDate($date); } diff --git a/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimeFormatterBase.php b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimeFormatterBase.php index be9df38..9b8a670 100644 --- a/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimeFormatterBase.php +++ b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimeFormatterBase.php @@ -83,6 +83,7 @@ public static function create(ContainerInterface $container, array $configuratio */ public static function defaultSettings() { return array( + 'timezone_display' => DateTimeItem::TIMEZONE_USER, 'timezone_override' => '', ) + parent::defaultSettings(); } @@ -93,13 +94,36 @@ public static function defaultSettings() { public function settingsForm(array $form, FormStateInterface $form_state) { $form = parent::settingsForm($form, $form_state); - $form['timezone_override'] = array( - '#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'] = array( + '#type' => 'select', + '#title' => $this->t('Timezone display'), + '#description' => $this->t('The timezone to use when displaying this date.'), + '#options' => array( + DateTimeItem::TIMEZONE_USER => $this->t("The user's timezone"), + DateTimeItem::TIMEZONE_NONE => $this->t("Timezone override") + ), + ); + + // 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'] = array( + '#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' => array( + 'visible' => array( + ':input[name="fields[' . $this->fieldDefinition->getName() . '][settings_edit_form][settings][timezone_display]"]' => ['value' => DateTimeItem::TIMEZONE_NONE], + ), + ) + ); + } return $form; } @@ -110,8 +134,15 @@ 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', array('@timezone' => $override)); + if ($this->fieldDefinition->getFieldStorageDefinition()->getSetting('datetime_type') === DateTimeItem::DATETIME_TYPE_DATETIME) { + if ($override = $this->getSetting('timezone_override')) { + $summary[] = $this->t('Time zone: @timezone', array('@timezone' => $override)); + } + else { + // @todo Make human-readable. + $summary[] = $this->t('Time zone display: @timezone', array('@timezone' => $this->getSetting('timezone_display'))); + + } } return $summary; @@ -139,13 +170,15 @@ public function settingsSummary() { * * @param \Drupal\Core\Datetime\DrupalDateTime $date * A DrupalDateTime object. + * @param string $timezone + * (optional) A timezone to explicitly set the date to. */ - protected function setTimeZone(DrupalDateTime $date) { + protected function setTimeZone(DrupalDateTime $date, $timezone = NULL) { if ($this->getFieldSetting('datetime_type') === DateTimeItem::DATETIME_TYPE_DATE) { // A date without time has no timezone conversion. $timezone = DATETIME_STORAGE_TIMEZONE; } - else { + elseif (empty($timezone)) { $timezone = drupal_get_user_timezone(); } $date->setTimeZone(timezone_open($timezone)); diff --git a/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimePlainFormatter.php b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimePlainFormatter.php index 683d75c..6b80133 100644 --- a/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimePlainFormatter.php +++ b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimePlainFormatter.php @@ -34,7 +34,7 @@ public function viewElements(FieldItemListInterface $items, $langcode) { // A date without time will pick up the current time, use the default. datetime_date_default_time($date); } - $this->setTimeZone($date); + $this->setTimeZone($date, $item->timezone); $output = $this->formatDate($date); } @@ -56,7 +56,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'); + $timezone = $this->getSetting('timezone_override') ?: $date->getTimezone()->getName(); return $this->dateFormatter->format($date->getTimestamp(), 'custom', $format, $timezone != '' ? $timezone : NULL); } diff --git a/core/modules/datetime/src/Plugin/Field/FieldType/DateTimeItem.php b/core/modules/datetime/src/Plugin/Field/FieldType/DateTimeItem.php index f07d564..5fa252b 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 array( '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 array( + $schema = array( 'columns' => array( 'value' => array( 'description' => 'The date value.', 'type' => 'varchar', 'length' => 20, ), + 'timezone' => array( + 'description' => 'The date timezone', + 'type' => 'varchar', + 'length' => 50, + ), ), 'indexes' => array( 'value' => array('value'), + 'value_timezone' => array('value', 'timezone'), ), ); + + return $schema; } /** @@ -95,6 +129,29 @@ public function storageSettingsForm(array &$form, FormStateInterface $form_state '#disabled' => $has_data, ); + $element['timezone_handling'] = array( + '#type' => 'select', + '#title' => t('Timezone handling'), + '#options' => array( + 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' => array( + // Hide the field if this is a date-only field. + 'visible' => array( + ':input[name="settings[datetime_type]"]' => ['value' => static::DATETIME_TYPE_DATETIME], + ), + 'disabled' => array( + ':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 aa6175a..65326f2 100644 --- a/core/modules/datetime/src/Plugin/Field/FieldWidget/DateTimeWidgetBase.php +++ b/core/modules/datetime/src/Plugin/Field/FieldWidget/DateTimeWidgetBase.php @@ -30,7 +30,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'], ); @@ -40,7 +40,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. @@ -78,8 +83,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/src/Tests/DateTimeFieldTest.php b/core/modules/datetime/src/Tests/DateTimeFieldTest.php index ce598bf..b7984f1 100644 --- a/core/modules/datetime/src/Tests/DateTimeFieldTest.php +++ b/core/modules/datetime/src/Tests/DateTimeFieldTest.php @@ -7,6 +7,7 @@ use Drupal\Core\Datetime\DrupalDateTime; use Drupal\Core\Datetime\Entity\DateFormat; use Drupal\Core\Entity\Entity\EntityViewDisplay; +use Drupal\datetime\Plugin\Field\FieldType\DateTimeItem; use Drupal\entity_test\Entity\EntityTest; use Drupal\field\Entity\FieldConfig; use Drupal\field\Entity\FieldStorageConfig; @@ -114,6 +115,7 @@ protected function setUp() { ->save(); $this->defaultSettings = array( + 'timezone_display' => DateTimeItem::TIMEZONE_USER, 'timezone_override' => '', ); @@ -297,6 +299,7 @@ function testDatetimeField() { $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.'); // Build up a date in the UTC timezone. $value = '2012-12-31 00:00:00'; @@ -422,6 +425,32 @@ function testDatetimeField() { ]); $this->renderTestEntity($id); $this->assertText($expected, SafeMarkup::format('Formatted date field using datetime_time_ago format displayed as %expected.', array('%expected' => $expected))); + + // Verify timezone display settings. + $this->fieldStorage->setSetting('timezone_handling', DateTimeItem::TIMEZONE_DATE); + $this->fieldStorage->save(); + + $edit = array( + "{$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.', array('@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(); + $this->renderTestEntity($id); + $expected = $date->format($this->displayOptions['settings']['date_format'], ['timezone' => 'America/New_York']); + $this->assertText($expected); } /** diff --git a/core/modules/datetime/tests/src/Kernel/DateTimeItemTest.php b/core/modules/datetime/tests/src/Kernel/DateTimeItemTest.php index d26434b..e452e6c 100644 --- a/core/modules/datetime/tests/src/Kernel/DateTimeItemTest.php +++ b/core/modules/datetime/tests/src/Kernel/DateTimeItemTest.php @@ -31,7 +31,10 @@ protected function setUp() { 'field_name' => 'field_datetime', 'type' => 'datetime', 'entity_type' => 'entity_test', - 'settings' => array('datetime_type' => 'date'), + 'settings' => array( + 'datetime_type' => 'date', + 'timezone_handling' => 'site', + ), )); $field_storage->save(); $field = FieldConfig::create([ @@ -120,4 +123,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.'); + } + }