Problem/Motivation

Imagine an organisation WidgetCo with editors and users in a variety of different time zones. They commonly face 2 problems trying to use the Date module. Support for some of this was present in D7's date but lost as it went to D8 core.

A. When creating session entities as part of their annual conference in New York, the times of the sessions as input by the editor are interpreted by Drupal in terms of the time zone of the editor who is creating or updating it, not New York. In this case their sitebuilder needs to be able to control the time zone used by the widget to interpret the times the editors input.

B. When creating meeting entities to describe meetings held at various locations across the world, the same problem happens. But in this case the intended time zone needs to specified for each meeting by the editor, and that preference needs to be stored with the date/time.

Proposed resolution

This effort has become too large to be accomplished in a single issue.

#3185747: [META] Add timezone support to core date fields is the active plan issue to actually coordinate this effort in smaller pieces.

We should stop posting "composer-friendly" giant patches in here, too. It's important we have a place for folks to put the patches they're using on real sites while the dust settles on doing this properly, but we don't want those patches either in this issue, nor in the new meta. Those patches should now live at #3185750: Composer friendly patches adding timezones to core date fields

Ideally, no one else should comment here until #3185747 is marked fixed and we can close this.

General commentary on the new plan should happen at #3185747
Comments about specific pieces of the plan should go in the appropriate child issue.
Comments about the monster patches, re-rolls, conflicts, etc, should happen in #3185750

Remaining tasks

  1. Complete the plan described in #3185747: [META] Add timezone support to core date fields
  2. Mark this issue fixed to notify everyone.

User interface changes

TBD, see child issues.

API changes

TBD, see child issues.

Data model changes

TBD, see child issues.

CommentFileSizeAuthor
#305 2632040-305.patch679 bytesjoseph.olstad
#296 interdiff-296-full--296-no-updates.txt8.53 KBrecrit
#296 2632040-D9.2.x-296--no-updates.patch90.12 KBrecrit
#296 interdiff-2632040-296-full-293.txt670 bytesrecrit
#296 2632040-D9.2.x-296--full.patch98.64 KBrecrit
#293 2632040-D9.2.x-293.patch98.38 KBrecrit
#284 2632040-284.patch98.43 KBhussainweb
#284 interdiff-283-284.txt1.95 KBhussainweb
#283 2632040-283.patch98.24 KBhussainweb
#280 2632040-280-9.0.x.patch98.12 KBdrewfranz
#279 2632040-279-9.0.x.patch98.33 KBiamdroid
#277 2632040-276--8.8.x-277.patch121.37 KBjaperry
#276 2632040-276--8.8.x.patch100.24 KBawolfey
#275 2632040-275--8.8.x.patch100.4 KBKasey_MK
#273 2632040-254--8.7.x--full-index-with-binaries.patch111.06 KBrecrit
#272 2632040-254--8.7.x--full-index.patch102.97 KBrecrit
#269 2632040-254--8.7.x.patch100.51 KBrecrit
#107 2632040-94-rerolled-again.patch37.86 KBjonathanshaw
#107 2632040-105-testonly.patch20.63 KBjonathanshaw
#107 2632040-105.patch37.86 KBjonathanshaw
#107 interdiff-2632040-105-94.txt65.96 KBjonathanshaw
#105 timezone_selector.png20.72 KBjonathanshaw
#105 widget_store_setting.png53.61 KBjonathanshaw
#105 formatter_store_setting.png45 KBjonathanshaw
#105 field_setting.png45.1 KBjonathanshaw
#105 setting_fixed.png38.72 KBjonathanshaw
#105 setting_when_not_enabled.png36.49 KBjonathanshaw
#97 interdiff-2632040-94-97.txt19.79 KBjonathanshaw
#97 2632040-97.patch48.22 KBjonathanshaw
#94 2632040-94.patch38.78 KBjonathanshaw
#91 2632040-91.patch36.34 KBbkosborne
#91 interdiff.txt9.92 KBbkosborne
#88 2632040-88.patch33.45 KBbkosborne
#88 interdiff.txt6.24 KBbkosborne
#85 interdiff.txt5.66 KBbkosborne
#85 2632040-85.patch32.5 KBbkosborne
#83 interdiff-2632040-72-82-rebase.txt6.07 KBjhedstrom
#82 2632040-82.patch29.91 KBjhedstrom
#82 interdiff-2632040-72-82.txt2.05 KBjhedstrom
#80 email_field-test.png29.31 KBSKAUGHT
#73 interdiff.txt1.9 KBbkosborne
#72 2632040-72.patch26.96 KBbkosborne
#67 add_ability_to_select_a-2632040-67.patch26.88 KBnlisgo
#61 2632040-61.patch26.78 KBjhedstrom
#61 interdiff.txt1.13 KBjhedstrom
#59 2632040-59.patch25.66 KBjhedstrom
#59 interdiff.txt1.21 KBjhedstrom
#57 2632040-57.patch25.71 KBjhedstrom
#57 interdiff.txt4.39 KBjhedstrom
#54 2632040-54.patch21.31 KBjhedstrom
#54 interdiff.txt3.98 KBjhedstrom
#50 2632040-50.patch21.06 KBjhedstrom
#50 interdiff.txt5.06 KBjhedstrom
#45 2632040-45.patch17.91 KBjhedstrom
#45 interdiff.txt5.8 KBjhedstrom
#40 interdiff.txt17.29 KBanemirovsky
#40 2632040-40.patch28.6 KBanemirovsky
#38 2632040-37.patch12.38 KBjhedstrom
#38 interdiff.txt8.48 KBjhedstrom
#21 2632040-21.patch7.32 KBjhedstrom
#21 interdiff.txt4.69 KBjhedstrom
#13 2632040-06.patch5.76 KBmpdonadio
#6 2632040-06.patch5.76 KBjhedstrom
#6 interdiff.txt2.4 KBjhedstrom
#4 2632040-04.patch3.36 KBjhedstrom
#110 2632040-108.patch77.62 KBjonathanshaw
#112 2632040-111.patch66.27 KBrvillan
#115 2632040-115.patch77.93 KBrvillan
#115 interdiff-2632040-108-115.txt702 bytesrvillan
#131 2632040-131.patch77.68 KBbenjy
#135 2632040-135.patch77.57 KBTim Bozeman
#135 interdiff.txt702 bytesTim Bozeman
#139 2632040-139.patch75.3 KBjofitz
#141 interdiff-139-141.txt7.44 KBjofitz
#141 2632040-141.patch77.88 KBjofitz
#150 2632040-150.patch76.57 KBekes
#151 2632040-151.patch79.19 KBekes
#151 interdiff-150-151.txt4.22 KBekes
#154 2632040-153.patch98.5 KBekes
#154 interdiff-151-153.patch.txt11.89 KBekes
#157 drupal-2632040-157.patch92.92 KBDane Powell
#161 2632040-161.patch87 KBekes
#161 interdiff-153-161.patch.txt15.88 KBekes
#165 2632040-165.patch95.15 KBekes
#167 interdiff-165-167.patch.txt2.36 KBekes
#167 2632040-167.patch95.27 KBekes
#169 problem_ui.png123.2 KBarsoman
#169 problem_code.png107.74 KBarsoman
#169 problem_ui_solution.png112.8 KBarsoman
#169 problem_code_solution.png154.19 KBarsoman
#171 align_timezone_names.patch1.1 KBarsoman
#175 2632040-175.patch87.18 KBjofitz
#177 2632040-177.patch95.33 KBjofitz
#179 interdiff-2632040-177-179.txt1.17 KBjofitz
#179 2632040-179.patch95.31 KBjofitz
#180 interdiff-2632040-179-180.txt2.92 KBjofitz
#180 2632040-180.patch95.34 KBjofitz
#184 2632040-184.patch87.14 KBJohn Cook
#184 interdiff-180-184.txt2.86 KBJohn Cook
#187 2632040-187.patch95.29 KBJohn Cook
#188 2632040-188.patch90.05 KBwengerk
#188 interdiff-2632040-187-188.txt6.02 KBwengerk
#191 2632040-191.patch98.2 KBwengerk
#198 drupal-2632040-198.patch92.05 KBrgristroph
#199 drupal-2632040-199.patch94.96 KBrgristroph
#203 drupal-2632040-203.patch100.45 KBrecrit
#203 interdiff-2632040-199-203.txt6.7 KBrecrit
#204 drupal-2632040-204.patch100.88 KBrecrit
#204 interdiff-2632040-203-204.txt7.62 KBrecrit
#207 drupal-2632040-207.patch101.03 KBrecrit
#207 interdiff-2632040-204-207.txt2.94 KBrecrit
#209 2632040-209.patch98.1 KBakalata
#210 2632040-210.patch97.14 KBakalata
#211 2632040-211.patch101.5 KBakalata
#211 interdiff_210-211.txt5 KBakalata
#213 2632040-212.patch101.26 KBakalata
#213 interdiff_211-212.txt933 bytesakalata
#215 2632040-215.patch102.09 KBakalata
#215 interdiff_212-215.txt1.09 KBakalata
#218 2632040-218.patch106.39 KBakalata
#218 interdiff_215-218.txt6.3 KBakalata
#223 drupal-2632040-223.patch93.18 KBkoppie
#225 drupal-2632040-225.patch133.48 KBkoppie
#234 2632040-234.patch108.65 KBWidgetsBurritos
#236 2632040-236.patch99.02 KBWidgetsBurritos
#236 interdiff-234-236.txt5.91 KBWidgetsBurritos
#237 2632040-237.patch110.63 KBWidgetsBurritos
#237 interdiff-234-237.txt5.91 KBWidgetsBurritos
#241 2632040-241.patch111.9 KBWidgetsBurritos
#241 interdiff-237-241.txt6.51 KBWidgetsBurritos
#242 2632040-242.patch115.49 KBWidgetsBurritos
#242 interdiff-241-242.txt545 bytesWidgetsBurritos
#244 interdiff-242-244.txt6.96 KBWidgetsBurritos
#244 2632040-244.patch111.7 KBWidgetsBurritos
#245 2632040-245.patch112.34 KBWidgetsBurritos
#245 interdiff-244-245.txt1.33 KBWidgetsBurritos
#250 interdiff-245-250.txt26.22 KBWidgetsBurritos
#250 2632040-250.patch110.55 KBWidgetsBurritos
#251 2632040-251.patch108.25 KBWidgetsBurritos
#251 interdiff-250-251.txt25.77 KBWidgetsBurritos
#252 2632040-252.patch108.22 KBWidgetsBurritos
#252 interdiff-251-252.txt1.04 KBWidgetsBurritos
#253 interdiff-252-253.txt5.04 KBWidgetsBurritos
#253 2632040-253.patch108.53 KBWidgetsBurritos
#254 interdiff-253-254.txt732 bytesWidgetsBurritos
#254 2632040-254.patch108.55 KBWidgetsBurritos
#262 reroll-diff-254-262.txt224.43 KBltrain
#262 2632040-262.patch111.03 KBltrain

Issue fork drupal-2632040

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

arknoll created an issue. See original summary.

mpdonadio’s picture

+1 from me, but I'm not sure if we would be able to get this in 8.0.x (regressions for contrib that made it into core don't automatically count as bugs; it's up to the core maintainer's discretion).

andrewmacpherson’s picture

Version: 8.0.x-dev » 8.1.x-dev

I agree this is a feature request, so bumping version.

jhedstrom’s picture

Status: Active » Needs work
Issue tags: +Needs tests
FileSize
3.36 KB

I was hoping to get further on this but ran out of time. Here's a very rough start. Current work takes heavily from the 7.x date module's timezone handling.

mpdonadio’s picture

Thanks. This will need upgrade path, but it will be easy to just add the default setting to all of the field instances.

jhedstrom’s picture

FileSize
2.4 KB
5.76 KB

A start at tests.

swentel’s picture

+++ b/core/modules/datetime/src/Plugin/Field/FieldType/DateTimeItem.php
@@ -61,6 +62,10 @@ public static function propertyDefinitions(FieldStorageDefinitionInterface $fiel
+    // @todo Shouldn't this only be needed if `$field_definition->getSetting('timezone_handling') === TRUE`?
+    $properties['timezone'] = DataDefinition::create('string')
+      ->setLabel(t('Timezone'));
+

Just a thought here: wouldn't it be more interesting to add a new field type that stores a date + timezone.
That way we have a clear separation between two fields:

- one that stores dates always stores in UTC
- one that allows storing a date with timezone

That makes the

- the storage per field type very clear, no extra empty column - or worse having to update the tables in case you turn on/off timezone handling.
- it makes the upgrade path easier, as in, there shouldn't be one at all.

jhedstrom’s picture

Issue summary: View changes

Updating IS.

jhedstrom’s picture

Cross post there. I like the idea of #7 a lot.

mpdonadio’s picture

Status: Needs work » Needs review

Not sure if I like the idea of a different field type. Not totally against it, but would have to sleep on that one. The Drupal 7 Date module also stores the offset in the field table for the selected timezone. I believe it does this to allow date calculation to be handled at the SQL level when necessary.

swentel’s picture

tldr: I now also think single field type is fine

----

So I thought some more about the split after I found #2161337: Add a Date Range field type with support for end date - theoretically we might end up with following scenario if we'd create different field types for all of them:

- single date
- single date with timezone
- single + end date
- single + end date with timezone

Now, more or less the same scenario exists for text (see #2313757: Remove text_processing option from text fields, expose existing string field types as plain text in UI) (which is where I got my 'inspiration' from) : plain vs formatted vs small vs long etc. In analogy with that patch it /could/ mean we'd expose TimestampItem as well in field UI some day (there is no reason to hide it imo) which would give us following field types exposed in Field UI

- single date (timestamp)
- single date + end date (timestamp)
- single date (iso8601)
- single date with timezone (iso8601)
- single + end date (iso8601)
- single + end date with timezone (iso8601)

Now, that looks a little silly, but then again, Field UI isn't perfect (ahum), but the field types and the API would be very clean, especially on the storage level, which in the end is probably my main concern as in: why have one (or more) column(s) that wouldn't store anything, right ? And with class inheritance, you call the parent, add a new column and property in the new class and you're done.

However, looking at the current datetime widgets and formatters, it'd especially mean a lot of new formatters and widgets per new field type, which on a first glance will lead to many duplicate code, or a lot of calls to the parent class in case of extending, let alone verbose names as well for the classes/files.

So, in the end, I think adding timezone in the same field type is fine, and the same for the end date too in that other issue.

mpdonadio’s picture

What @swentel said. I spent a while thinking and working this out on paper yesterday, as well as reading the Drupal 7 Date module and issues, but didn't get a chance to write it up. It gets worse once you consider adding recurrences (which I think need to be in core, too; ugh). I just don't see how you can accomplish this w/o a really bad class hierarchy and bad (or less than proper) OO implementations.

mpdonadio’s picture

Just reuploading the patch from #6 to get a TestBot run.

swentel’s picture

+++ b/core/modules/datetime/src/Plugin/Field/FieldType/DateTimeItem.php
@@ -80,6 +85,17 @@ public static function schema(FieldStorageDefinitionInterface $field_definition)
+    if ($field_definition->getSetting('timezone_handling') === 'date') {
+      $schema['columns']['timezone'] = array(
+        'description' => 'The date timezone',
+        'type' => 'varchar',
+        'length' => 50,
+      );
+    }

I'm not sure anymore what happens in case you'd change the setting when there's no data yet and go from datetime to date: will the db column be added than or not, that's something to consider.

mikemiles86’s picture

mpdonadio’s picture

Status: Needs review » Postponed
Related issues: +#2161337: Add a Date Range field type with support for end date

Thanks for tagging this to be worked on this weekend, but we are going to postpone this on #2161337: Add a Date Range field type with support for end date. TZ support will be needed in some manner when we add end dates, and both patches are similar and touch the exact same portions of code.

mikemiles86’s picture

Removing sprint tags, as this is postponed.

Version: 8.1.x-dev » 8.2.x-dev

Drupal 8.1.0-beta1 was released on March 2, 2016, which means new developments and disruptive changes should now be targeted against the 8.2.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

mpdonadio’s picture

Status: Postponed » Needs work

Given the approach now in #2161337: Add a Date Range field type with support for end date, lets unpostpone this and see if we can work on both in parallel.

Let's go with the approach of adding a timezone property to the schema and always saving it in the field data. This will eliminate the dynamic schema.

?

jhedstrom’s picture

Assigned: Unassigned » jhedstrom

Going to work on this a bit.

jhedstrom’s picture

Assigned: jhedstrom » Unassigned
Status: Needs work » Needs review
FileSize
4.69 KB
7.32 KB

Still more to do here. I added some @todo's in the patch, but additionally we will need more tests, and an upgrade path still.

mpdonadio’s picture

Was thinking about this today. Why do we need to do a schema update for storage? If this is really just about user entry, then why don't we just

- add the setting for the widget
- when we process the entry, warp the date to UTC based on the setting for storage; this is basically what we do now, we just don't give the user the option on how it happens per instance
- when the widget is used for editing, then we just need to use the setting to warp UTC back to what the setting uses

The upgrade path then just needs to add the default setting to add existing field instances (ie, just config), and not have to touch storage.

This has the added benefit that date storage is still sortable w/ simple string comparisons If we either update the storage string to add TZ or add another column for TZ, then things will get messy (especially for the Views plugins).

?

SKAUGHT’s picture

just to share my common use case for needing a timezone being collected (which almost always because a client 'wants visual timezone selection'):

content editor: -- event is going to be a video meeting.

  • i'm a content editor, in Ontario. [time zone 0]
  • I'm entering adding event content type
  • i pick a location (seperate field) -- field not specifically needed, but client asked for field.. IE: not all events are Video..
  • --i pick a date AND time ("probably" with end time on same day, 2-4pm..)
  • --i want to clarify that this location is in alberta, [time zone3] and that is a different timezone.

end-user reviewing events (personalization)

  • i live in Nova Scotia [time zone -1]
  • i see a view of events
  • i need reminder that event time to be displayed by my user timezone setting -- so i don't miss video meeting [+4hours from me]

@mpdonadio.
i will agree -- shouldn't need separate storage of the field as it should be a whole date stored. that can/will be easily converted come render time with somekind of personalizing to the end user.

it's always an issue of converting an original saved date/time in relation to location of the event it's being connected to... and the end user who's trying to match his schedule for it.

---------------------
or even (on the far side): date field should always have a location widget attached. HAHA.

SKAUGHT’s picture

mpdonadio’s picture

#24, thanks for outlining a use case. We may want to edit the authoring experience into the IS.

Just to clarify, we are talking about just the content authoring experience in this issue. I don't think there is a question of whether this is needed; we are just at the minutiae stage right now for how it gets implemented. We had originally thought that we would need to update how things get stored (which would bring it close to how the D7 Date module is implemented). I'm thinking now that we always store UTC, and that this new option just handles conversion before we store.

As for the user experience, we already have support for that. The field formatters support TZ overrides to force one for a field, else system default behavior is used (from how admin/config/regional/settings is set up).

IOW, handle entry and display as true inverses.

tstoeckler’s picture

Don't want to hijack this issue (and maybe it is in fact a separate issue, but I don't think so), but I would argue that this is actually a bug report.

If you look into \Drupal\Core\TypedData\Plugin\DataType\DateTimeIso8601::setDateTime() you see that core itself uses PHP's c date format to store a date time. That, though, includes the timezone offest in the 2004-02-12T15:19:21+00:00 format as can be seen on the linked PHP documentation page. So while it does not store the actual timezone identifier (which the patches here are about) it does store information that is in many cases (albeit not all) completely sufficient for timezone-aware date handling.

However, the storage of the datestring is limited to 20 characters, so the timezone makes the string to long. So in fact using DateTimeIso8601::setDateTime() leads to unsaveable entities. Try it yourself:
Given an entity with a datetime field with the ID date:

$datetime = new DrupalDateTime();
// Get a field item, regardless of whether one exits already.
$date_item = $entity->get('date')->first() ?: $this->get('date')->appendItem();
$date_item->get('value')->setDateTime($datetime);

$entity->save();

Seems like proper API usage to me, but it yields:
Drupal\Core\Database\DatabaseExceptionWrapper: SQLSTATE[22001]: String data, right truncated: 1406 Data too long for column 'date'

That's what makes this a bug report. And solving the bug would involve supporting time zones as part of datetime fields - at least in the ISO8601-conform offset form. That's why I didn't open a new issue for this.

Thoughts?

tstoeckler’s picture

Additional problem:
Even setting aside the column length problem, if you have a date of the form 2004-02-12T15:19:21+00:00 in the database \Drupal\datetime\DateTimeComputed::getValue() will never return a proper datetime object because it expects the format Y-m-d\TH:i:s or Y-m-d depending on the storage setting of the date time field.

mpdonadio’s picture

I'm not convinced we have a problem, more of a happy accident? Maybe this was by design (before my time with this). Not ruling out a problem, thought, considering what I have dug up with TZ handling. However, with the changes we made to testing to run tests in non-UTC, I think we are OK.

Storage uses the ISO date w/o TZ suffix, w/ the implicit assumption that it is UTC.

The data type uses proper ISO dates w/ the TZ suffix as the value.

The field item uses the data type as the value, and then has a computed value for the actual DrupalDateTime object.

DateTimeComputed brokers this, and all is well.

I don't think this would be a problem with the approach I am proposing. I am thinking that we can take the settings form changes from the existing patch and then adjust the logic in DateTimeWidgetBase::formElement()

  $element['value'] = array(
      '#type' => 'datetime',
      '#default_value' => NULL,
      '#date_increment' => 1,
      '#date_timezone' => drupal_get_user_timezone(),
      '#required' => $element['#required'],
    );

Instead of calling `drupal_get_user_timezone()` we would use the setting logic, and keep the non-widget code as-is.

?

tstoeckler’s picture

OK, I will open a separate issue then. I still think #27 is a bug, but then I'll let you continue here without any further interruption. :-)

mpdonadio’s picture

#30, I re-read your use case while looking core. It's a bug, and it looks like we are missing test coverage. It looks like all of the test are explicitly using the storage format string when setting values. That's on the public API, so that should work properly.

tstoeckler’s picture

jhedstrom’s picture

- when we process the entry, warp the date to UTC based on the setting for storage; this is basically what we do now, we just don't give the user the option on how it happens per instance

The issue with this is that converting from a specified timezone (eg, America/Los_Angeles) to UTC loses data since it would just be stored with -08:00. The if the user creates a date formatter where they want the actual selected timezone to display, they cannot do so reliably. I verified the D7 module stores the textual representation of the timezone.

mpdonadio’s picture

Summary of IRC discussion.

- Keep value column in implicit UTC
- Add new widget setting for TZ entry handling
- Add new column for TZ name that was selected or computed by system behavior during input
- Keep logic that converts user entry to UTC for storage to allow simple string handling
- Add new field formatter option to TZ setting to allow for output using value at entry
- Possibly add some predefined date formatter strings that include TZ

Think that summarizes it?

jhedstrom’s picture

Status: Needs work » Postponed

I think further work on this should hold off until #2739290: UTC+12 is broken on date only fields is in.

mpdonadio’s picture

Status: Postponed » Needs work
jhedstrom’s picture

Assigned: Unassigned » jhedstrom
jhedstrom’s picture

Assigned: jhedstrom » Unassigned
Status: Needs work » Needs review
FileSize
8.48 KB
12.38 KB

I made a bit of progress. This adds some options to the formatter configuration for which timezone to use.

anemirovsky’s picture

This adds support for date range fields as added by #2161337: Add a Date Range field type with support for end date, which looks close to completion. If we want to include support in this same patch, we should probably wait until that gets committed.

daffie’s picture

Status: Needs work » Needs review

For the testbot.

mpdonadio’s picture

A bot run of #40 doesn't make sense yet, because it references files that are only added by #2161337: Add a Date Range field type with support for end date.

jhedstrom’s picture

Assigned: Unassigned » jhedstrom

Thanks @anemirovsky, but unfortunately since that issue isn't in yet, it will be nearly impossible to iterate with those changes included.

I'm working on some tests for this now, and hopefully #2161337: Add a Date Range field type with support for end date gets in soon so we can include support here (it could also be done in a follow-up since those are new field plugins).

jhedstrom’s picture

Status: Needs work » Needs review
FileSize
5.8 KB
17.91 KB

This adds some additional testing and a few fixes. They are failing because there is still an issue where the timzone submitted by the user needs to be set before the \Drupal\Core\Datetime\Element\Datetime::processDatetime() method sets the timezone. As it is now, that gets called and sets the timezone to the site's timezone, which then gets converted to UTC on storage, and then when formatting, that gets converted to the submitted timezone, causing the date move around each time it is loaded into the form and saved.

jhedstrom’s picture

Assigned: jhedstrom » Unassigned

Unassigning in case somebody else gets to this before I do again.

jhedstrom’s picture

We somehow need to hook in before Datetime::valueCallback is called to set $element['#date_timezone'] with the user-selected timezone, since this is then used to construct the date object:

      $timezone = !empty($element['#date_timezone']) ? $element['#date_timezone'] : NULL;

As this patch stands now, that is set to NULL on new field values, and set to the previously selected timezone when editing.

jhedstrom’s picture

Assigned: Unassigned » jhedstrom
Issue summary: View changes

I had an idea that will resolve the issue detailed above, and also simplify implementation: we can add a #expose_timezone to the datetime form element itself. This will also make the reroll cleaner when #2161337: Add a Date Range field type with support for end date lands.

Working on this now.

jhedstrom’s picture

Assigned: jhedstrom » Unassigned
Status: Needs work » Needs review
FileSize
5.06 KB
21.06 KB

The new test is still failing, but I think this I've just overlooked something. I think this approach will work well, and it will have the added bonus of letting anybody using the core datetime form element to collect a timezone as needed.

jhedstrom’s picture

Issue tags: +beta target
+++ b/core/modules/datetime/src/Plugin/Field/FieldWidget/DateTimeWidgetBase.php
@@ -78,6 +84,15 @@ public function massageFormValues(array $values, array $form, FormStateInterface
+        // Store the timezone if set.
+        if ($this->getFieldSetting('timezone_handling') === DateTimeItem::TIMEZONE_DATE) {
+          $item['timezone'] = $date->getTimezone()->getName();
+        }
+        else {
+          $item['timezone'] = '';
+        }

This is why the date keeps processing. Since messageFormValues() is called twice (on form validate, and on form submit), the second time it is called, the timzeone on the date object has been set to UTC.

I didn't immediately see a way to set this elsewhere since the only value passed in is the datetime object.

Once this detail is sorted, I think the only thing left here is to write the relatively simple upgrade hook.

I'm optimistically tagging this as a beta target since if we could knock this and the date range issue out, that would allow contrib to make calendars, etc.

mpdonadio’s picture

Few quick comments, but not an in-depth look yet.

  1. +++ b/core/modules/datetime/src/Plugin/Field/FieldType/DateTimeItem.php
    @@ -63,18 +89,25 @@ public static function propertyDefinitions(FieldStorageDefinitionInterface $fiel
    -        'value' => array('value'),
    +        'value' => array('value', 'timezone'),
           ),
    

    Probably want to keep the 'value' index and add a 'value_timezone' index instead.

  2. +++ b/core/modules/datetime/src/Plugin/Field/FieldType/DateTimeItem.php
    @@ -95,6 +128,29 @@ public function storageSettingsForm(array &$form, FormStateInterface $form_state
    +      '#title' => t('Timezone handling'),
    +      '#options' => array(
    +        static::TIMEZONE_SITE => t("Site's timezone"),
    +        static::TIMEZONE_DATE => t("Date's timezone"),
    +        static::TIMEZONE_USER => t("User's timezone"),
    +        static::TIMEZONE_NONE => t('No timezone conversion'),
    +      ),
    +      '#default_value' => $this->getSetting('timezone_handling'),
    

    Use $this->t() ?

  3. +++ b/core/modules/datetime/src/Plugin/Field/FieldWidget/DateTimeWidgetBase.php
    @@ -78,6 +84,15 @@ public function massageFormValues(array $values, array $form, FormStateInterface
    +
    +        // 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.
    

    Maybe use $form_state->isValidationComplete() to check the phase to avoid the problem you mention in #52?

This doesn't look too hard to merge into #2161337: Add a Date Range field type with support for end date this if lands first, but if that does happen the end date issue will need an upgrade path. Maybe we can get these added at same time to avoid that?

jhedstrom’s picture

Status: Needs work » Needs review
Issue tags: -Needs tests
FileSize
3.98 KB
21.31 KB

Thanks @mpdonadio for the isValidationComplete() suggestion! This gets the new test passing, and addresses #53. I think the remaining fails are upgrade path related.

xjm’s picture

Issue tags: -beta target

Only committers should add the beta target tag. In the case of this issue unfortunately it missed the deadline, and we generally avoid changes requiring upgrade paths in beta except for major and critical bugs. Thanks!

jhedstrom’s picture

Status: Needs work » Needs review
FileSize
4.39 KB
25.71 KB

This update hook seems way too complex, but I couldn't find any other helpers that made it easier. If the update schema tests pass, I'll work on an final test for the upgrade path.

jhedstrom’s picture

Status: Needs work » Needs review
FileSize
1.21 KB
25.66 KB

This will fix the undefined index tests.

jhedstrom’s picture

Status: Needs work » Needs review
Issue tags: -Needs upgrade path
FileSize
1.13 KB
26.78 KB

This should fix that last failure. Will still need a dedicated upgrade path test, but this is close I think.

Version: 8.2.x-dev » 8.3.x-dev

Drupal 8.2.0-beta1 was released on August 3, 2016, which means new developments and disruptive changes should now be targeted against the 8.3.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

jhedstrom’s picture

Adding some related issues that are blocking this. Once those are in, I think this can be next.

nlisgo’s picture

Status: Needs review » Needs work
Issue tags: +Needs reroll
nlisgo’s picture

nlisgo’s picture

Assigned: Unassigned » nlisgo
nlisgo’s picture

Assigned: nlisgo » Unassigned
Status: Needs work » Needs review
Issue tags: -Needs reroll
FileSize
26.88 KB
nlisgo’s picture

The additional work that needs to be carried out is to support timezone for the datetime_range field.

bkosborne’s picture

I think the issue summary can use some clarification as to the purpose of this feature, because as someone who's not super familiar with how timezone handling has worked in D7 with the Date module, I'm a little lost.

Here's what I've gathered:

WITHOUT this feature, any date/times entered into a date field are assumed to be in the user's timezone, or if that's not set, the site's default timezone. The date/time is converted to UTC before being stored in the database. When displaying the date/time, it will be converted to either the user's timezone or the site's default timezone, depending on how the field formatter is configured.

WITH this feature, a timezone can be selected when entering a date/time into a date field. Drupal will assume that the date/time entered is for that selected timezone. The date/time is still converted to UTC before being stored in the database. The timezone is also stored in the database. When displaying the date/time, the formatter can be configured to convert the date/time to the specific timezone that was selected when it was entered, or just convert it to the user's timezone or site's default.

===

It seems like the "win" that this brings is that it allows users to enter date/times for worldwide events in that event locations native timezone. For instance, let's say someone wanted to enter an event taking place at 7:00PM in Chicago (-6). The editor's timezone in Drupal is New York (-5). Without this patch, the editor would have to enter the time as 8:00PM to account for that difference, whereas with this patch, the editor can enter 7:00PM and also select the America/Chicago timezone.

Is that all correct?

mpdonadio’s picture

#70, yes, that pretty much sums up the user experience when this lands.

bkosborne’s picture

Status: Needs work » Needs review
FileSize
26.96 KB

There was a missing default_value for the widget settings form. I think there will be test failures since there were a bunch from the last re-roll that weren't fixed.

bkosborne’s picture

FileSize
1.9 KB

Here's the interdiff from that last patch

bkosborne’s picture

Two problems found in manual testing:

1) The settings summary for the display formatter is not always accurate. If you select a timezone override, it will always display information relating to that override, even if you edit the settings and select a different display type.

2) Similarly, when the actual field data is rendered, the timezone override always takes precedence if you've previously configured one.

We could solve both problems by clearing the value set for "timezone_override" if the user de-selects timezone override from the display types, but probably a better fix it to adjust the logic that determines if the timezone override should be in effect so that it checks for the configured display type, instead of just checking if a value for override is set.

bkosborne’s picture

Another bug I found is that when a date field has a specific timezone filled in for it, and you configure the formatter to display it using the USER's timezone instead, it ignores that setting and still displays it with the date item's specific timezone.

I have a fix for that, and #74 above, but I'm going to wait until #2775669: Clean up redundant methods in datetime field formatters is committed since it all touches the same code.

Version: 8.3.x-dev » 8.4.x-dev

Drupal 8.3.0-alpha1 will be released the week of January 30, 2017, which means new developments and disruptive changes should now be targeted against the 8.4.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

mpdonadio’s picture

Status: Needs work » Postponed

Officially postponing to avoid re-reroll purgatory.

mpdonadio’s picture

Status: Postponed » Needs work

#2775669: Clean up redundant methods in datetime field formatters is in, so I think we can work on this again.

SKAUGHT’s picture

FileSize
29.31 KB

omit: sorry, wrong thread.

jhedstrom’s picture

Assigned: Unassigned » jhedstrom

I'll work on rebasing #72.

jhedstrom’s picture

Assigned: jhedstrom » Unassigned
Status: Needs work » Needs review
FileSize
2.05 KB
29.91 KB

This is a reroll of #72 (the rebase diff is attached as interdiff-2632040-72-82-rebase.txt). I also fixed the php notice errors that were causing the daterange tests to fail. It doesn't address any of the other issues noted above.

jhedstrom’s picture

FileSize
6.07 KB

Oops, here's the rebase resolution diff noted above

bkosborne’s picture

Assigned: Unassigned » bkosborne

Let me see if I can fix the issues I brought up before.

bkosborne’s picture

Assigned: bkosborne » Unassigned
FileSize
32.5 KB
5.66 KB

Here's the fixes to address #74.

I think the timezone logic in all the formatters can be refactored somehow to reduce this code duplication. I think as it stands, the timezone handling in the formatters is pretty confusing to understand. We have a method on DateTimeFormatterBase called "setTimeZone", but timezone overrides are not handled in this method, they are instead handled in each of the formatters formatDate method. I don't think those methods should need to deal with timezones at all.

May take a stab at that...

bkosborne’s picture

Also the name of the constant we use for specifying a timezone override is unfortunate. It makes code like this really confusing: if ($this->getSetting('timezone_display') === DateTimeItem::TIMEZONE_NONE && $timezone_override) {, but I don't think we can change the constant.

mpdonadio’s picture

#85, I haven't looked at this patch in a while, but yeah, ideally this should have minimal impact on formatters. We should all try to review what we have and what we need to do now that we are finally unblocked.

bkosborne’s picture

Here's some refactoring based on reasoning I mentioned above.

bkosborne’s picture

Status: Needs work » Needs review
FileSize
9.92 KB
36.34 KB

Ignore #88.

This fixes my test failures.

mpdonadio’s picture

Quick look; I need to read this applied an play with it for a proper review.

  1. +++ b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimeCustomFormatter.php
    @@ -54,8 +55,7 @@ public function viewElements(FieldItemListInterface $items, $langcode) {
    -    $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());
    

    Hmmm. I think we need to rethink this logic. Even if someone enters a timezone upon input, they may need a different timezone on output?

  2. +++ b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimeDefaultFormatter.php
    @@ -32,8 +33,7 @@ public static function defaultSettings() {
    -    $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());
    

    Ditto.

  3. +++ b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimeFormatterBase.php
    @@ -94,13 +95,38 @@ public static function defaultSettings() {
    +          DateTimeItem::TIMEZONE_USER => $this->t("The user's timezone"),
    +          DateTimeItem::TIMEZONE_NONE => $this->t("Timezone override"),
    

    Not in love with these labels, but don't have better suggestions. We also have four constants; shouldn't all four be options here?

  4. +++ b/core/modules/datetime/src/Plugin/Field/FieldType/DateTimeItem.php
    @@ -42,6 +43,28 @@ public static function defaultStorageSettings() {
    +   * 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';
    +
    +  /**
    

    If the DateTimeItemInterface patch gets in first, we probably want to move these there.

  5. +++ b/core/modules/datetime/src/Plugin/Field/FieldType/DateTimeItem.php
    @@ -63,18 +89,26 @@ public static function propertyDefinitions(FieldStorageDefinitionInterface $fiel
    +          'length' => 50,
    

    Can we double check this against the TZ database for max length?

bkosborne’s picture

RE: 1 and 2, the $date field there already has the proper timezone logic applied to it by the time that method is called. Your example of entering the time in a specific timezone and wanting it output in a different one works.

RE: 3, I think we can add some descriptions to both the widget settings and the field settings form that describes the impact of each choice a bit better. And yea, we probably should have all 4 constants there.

jonathanshaw’s picture

FileSize
38.78 KB

Rerolled, a lot of collisions with the short array syntax changes. I hope to address #92 tomorrow.

jonathanshaw’s picture

Assigned: Unassigned » jonathanshaw
Status: Needs review » Needs work

I'm deep into working on this (reworking formatter settings and tests), hold off on reviewing.

mpdonadio’s picture

Thanks. Was #94 a straight reroll to account for the array issue, or was there anything else? When you wrap up, make sure you post an interdiff. This touches a lot of places, so seeing the incremental changes really helps.

jonathanshaw’s picture

Status: Needs work » Needs review
FileSize
48.22 KB
19.79 KB

#94 is a straight reroll PLUS applying the short array syntax to the new patch code. There's 1 unrelated CS issue I fixed by mistake, reverted here.

The latest patch attached is a somewhat rough work in progress.

Responding to @mpdonadio's quick review #92:
#92.1 & 92.2: @bkosborne is right, the patch already allows for what you want.
#92.3 I added 'Site' which was the missing option.
#92.5 Various SO source demonstrate that the longest current timezone name is about 31 characters, the longest ever has been 38, so the specified storage length of 50 looks safe.

ALSO FIXED IN THIS PATCH

Site's default timezone
Issue: Per #92.3 'Site's default timezone' should be offered as an option alongside user's timezone or specific timezone.

Null per-date timezone values
Issue: The formatter needs robust handling for cases where per-date timezones are enabled in the field, but null for a particular date value. This mainly happens to a field with existing values (e.g. in a migration or when updating a field to have per-date timezones), but could also be set that way via the API or a widget.

Previously the patch used the user's timezone if there was no per-date timezone value. But this creates unexpected and undesired results when transitioning from using a manually specified timezone override to using per-date timezones - all existing dates would switch from the previously specified timezone to user timezone, until such time as they were edited and a timezone set for each one.

Solution: The current patch version splits the formatter timezone settings into 3:
- 'Default timezone': a select offering site, user or specified
- a select of timezones, if specifed is selected as the default
- a checkbox for 'Use stored timezone preference'
If the checkbox is checked, then the stored per-date timezone is used unless it is null, in which case it falls back to the default selected.

Need thorough tests of formatter's timezone logic
I added helper methods and used these to systematically test all combinations of settings and circumstances

WHAT'S STILL TO DO:

We face 2 unavoidably entangled issues:

(A) Interpreting user-inputted times in the widget. Previously we always assumed the user's timezone, but that can be problematic.
(B) Storing a timezone preference for each date This is a natural outflow of A), because one you allow people to specify a timezone that should be used to interpret their input of a certain time, it is unhelpful if you don't save that timezone. And once you've stored it, you need to allow for it in the formatter's rendering of the time too.

We've done a good job of (B), storing per-date timezones. But we've not yet fully worked through (A), the basics of how to determine the timezone used to interpret user-entered times.

What we need to do next are:

1) Add to the form element a 'timezone label' (the assumed timezone for time input) to the content editor, that is displayed when the timezone chooser is not. Have this always shown, but make sure it can be easily overridden by themers. With all this flexibility around timezones, it gets dangerously easy to trip up editors.

2) Change the field's per-date storage setting into a boolean 'Allow times to have a preferred timezone'.

3) Offer a setting on the widget, same as on the formatter: 'Default timezone' with site, user, and manually specified as the 3 options.

4) If per-date storage is set in the field's settings, offer a setting on the widget 'Allow users to choose a timezone'.

5) Pass to the form element 2 arguments:
-- '#expose_timezone'. Sets whether the element should show a timezone selector or timezone label, based on widget setting 'Allow user to choose'.
-- '#date_timezone'. The default value of the timezone selector, or the value of the timezone label. The value of this is based on the stored timezone value if present, or the default configured in the widget if the per-date is null or not enabled.

6) Exhaustively test the combinations of settings and circumstances.

7) Sort the terminology: create consistency between the settings labels, settings descriptions, variable names and code comments.

Current patch is good for reviewing the overall approach so far, but not for reviewing the fine details.

I will work on the todo's over the weekend.

mpdonadio’s picture

@jonathanshaw, thanks for moving this forward. We are making good progress on this.

I took a quick look at this. I am concerned about a few things right now.

We are starting to change a decent amount of strings (labels, titles, etc). If possible, we need to minimize this.

The user facing term and in comments needs to be "time zone", two words. That is the preferred language from the docs team.

My main concern (which I could be wrong about, this is a little hard to review as a patch) is that we are starting to couple the intended output intent (ie, formatter behaviour) upon input, the (A) and (B) above. IOW, we are implying what the formatters should do based on widget input. We need to avoid that.

I will review this applied and play with this over the weekend.

jonathanshaw’s picture

We are starting to change a decent amount of strings (labels, titles, etc). If possible, we need to minimize this.

Understood. I will think in terms of minimising this. I think it's only an issue in the formatters.

My bigger concern though is that it's very difficult to describe things clearly and set user expectations correctly, even with changing strings. I'm leaning towards coining the concept of a 'preferred time zone' or 'stored time zone preference' to refer to the time zone stored per-date in the field, but I need to play with this more, looking at all 3 settings contexts (field, widget and formatter) side by side.

My main concern (which I could be wrong about, this is a little hard to review as a patch) is that we are starting to couple the intended output intent (ie, formatter behaviour) upon input, the (A) and (B) above. IOW, we are implying what the formatters should do based on widget input. We need to avoid that.

Recent iterations of the patch make that coupling an optional (and non-default) behavior. If a timezone is set in the widget, you can choose in the formatter whether to use it or ignore it.

jonathanshaw’s picture

Assigned: jonathanshaw » Unassigned
jonathanshaw’s picture

I'm close to having a heavily-revised patch ready, hold off on reviews on this issue for now.

mpdonadio’s picture

#102, do you still think this is too coupled to 2799987? Should we postpone this properly until we figure that out? I haven't had much time to look into either of these issues this week.

jonathanshaw’s picture

Re #103, it's better to do #2799987: Datetime and Datelist element's timezone handling is fragile, buggy and wrongly documented for 2 reasons:
1) to have a good foundation before we add in more complexity
and
2) because of problem 2 in that issue, what I want to do in the widget here doesn't work: you can't get an exposed timezone selector to default (for new values) to something other the user timezone.

However, I don't suggest we need to formally postpone. This patch touches a lot of places, the element is just one small part of it, so there's a lot of reviewing and polishing that can be done here anyway even while that one is still being nailed down.

jonathanshaw’s picture

I have done a bunch more on this, and will upload new patch tomorrow once I've unpicked the merge conflicts with #2780063: Convert web tests to browser tests for datetime and datetime_range modules.

What I've done in this latest patch

A. Added widget settings for timezones, almost identical to those for the formatter and sharing a trait.

B. Reduce the field type setting down to a boolean (store / don't store), as the fancier question of what default you want to assume etc now lives on the widget/formatter.

C. Added widget timezone tests, and tidied up the formatter tests.

D. Mostly got all this timezone handling working for date range fields. The hard part of this was that the exposed timezone selector should only show up once (not on both date elements) but its value needs to be used to interpret the contents of both elements.

E. Harmonised the terminology of the UI text, code comments, array keys, constants and variables.

F. Moved the constants into an interface.

What still needs doing

1. Fix the date range formatters.

2. Extend the current tests to cover the daterange field/formatter/widget.

3. Extend the current tests to cover all widgets and formatters extending the DatetimeBase widget/formatter.

4. Check whether the logic in ConfigurableTimezoneTrait::getDefaultTimezone() is adequate for DateRange fields of the All day type.

5. Check whether we have adequate tests for all day and date-only fields, so that we know if our timezone changes are accidentally impacting them.

6. Add an update path for the formatter's timezone_override setting.

7. Remove -None- as an exposed value in the timezone_override selector (now called "fixed time zone").

8. Default timezone_override to the site's default timezone if there is the stored config setting is ''.

9. Add tests for all update paths.

10. Finish the datetime element once #2799987: Datetime and Datelist element's timezone handling is fragile, buggy and wrongly documented gets fixed.

11. Consider renaming the constant TIMEZONE_NONE to TIMEZONE_FIXED; consider changing its value to 'fixed' not 'none'. This may require an upgrade path if 'none' gets stored anywhere in config, but I don't think it does.

12. Bug: the field setting form allows you to change the timezone storage setting even after values are in the database, but it won;t save your change of setting. Make a test for this.

13. Make sure the date range validation that ensures the start time is before the end time is timezone intelligent - in particular it needs to handle the case where the exposed timezone on the start time is changed and that change needs to be applied to both start and end. Make sure we have tests for this.

14. Widgets and formatters seem to be forgetting their previous settings when disabled and then re-enabled. I'm not sure if this is the standard behavior or a bug in this patch.

15. Bug?: Make sure all the widgets and formatters are available in the UI when they should be. I've noticed an oddity here.

16. Bug: The formatter/widget timezone settings summary are not properly translatable, I think they don't use $this->t() correctly.

17. Bug: The formatter/widget timezone settings summaries are not behaving properly. The per-date settings summary doesn't show up at all for date_range fields, and only refreshes for normal date fields when the "Save" button is pressed, not when the "Update" button is pressed.

18. The field's timezone column description needs updating to reflect the new terminology, for the normal field and the daterange field.

19. The formatter/widget settings summaries may need updating to reflect new terminology.

20. Add tests for the formatter & widgets settings forms and summaries, and apply them to all widgets and formatters for both date and daterange fields.

Most of all, it's tests, tests, tests, tests. Timezones are really hard, and there are a lot of logic paths we're tampering with here all over the place. Unfortunately, I can't undertake to fix all this. I will focus on #2799987: Datetime and Datelist element's timezone handling is fragile, buggy and wrongly documented.

mpdonadio’s picture

@jonathanshaw, thanks for your work with this, the TZ stuff is a pain. I think we need to see your in progress patch here to assess things (even if it doesn't apply b/c the BTB issue). It looks like you uncovered some new bugs, and identified some new ones. It also looks you may have done some needed work (eg, the interface thing) that we already have an issue for.

I think we need to look new stuff you uncovered, along with the queue, to see what we need to tackle first to make this more manageable.

The biggest thing would probably be refactoring the functional field tests to make them manageable. I also think we need to be sure we aren't making things overly complex; I don't want to get rid of any work that would benefit end users; we need to provide the essential building blocks in core (I think #70 is our MVP), and extra functionality can be punted to datetime_extras. I am still not sure what 'timezone_per_date' is providing us here, especially since exposes more test surface area.

jonathanshaw’s picture

Title: Add ability to select a timezone for datetime field. » Add ability to select a timezone for datetime field
FileSize
37.86 KB
20.63 KB
37.86 KB
65.96 KB

Here's the rebased patch, and a further reroll of #94.

I think we need to look new stuff you uncovered, along with the queue, to see what we need to tackle first to make this more manageable.
The biggest thing would probably be refactoring the functional field tests to make them manageable

If you reckon this patch has become too sprawling too handle, I suggest the way to split it up would be into different issues like this:

1) Create a new TimezoneFieldTest class, which contains test methods that can be applied to any widget/formatter datetime/date_range to test timezone handling. So if we add some new timezone logic, we only have to touch a couple of places in the test and it magically gets tested on all widgets/formatters.

2) Refactor the formatters as suggested by @bkosborne in #88.

3) Move the formatters timezone settings into a trait, with the ['timezone_default] options of user, site and fixed; fixed allows you to choose (what we call for BC reasons) the ['timezone_override'].

4) Apply the trait to the widgets too. All the same options make sense for them. This addressed WidgetCo's first problem from the IS:

A. When creating session entities as part of their annual conference in New York, the times of the sessions as input by the editor are interpreted by Drupal in terms of the time zone of the editor who is creating or updating it, not New York. In this case their sitebuilder needs to be able to control the time zone used by the widget to interpret the times the editors input.

5) Add per-date time zone storage, to solve WidgetCo's second problem:

B. When creating meeting entities to describe meetings held at various locations across the world, the same problem happens. But in this case the intended time zone needs to specified for each meeting by the editor, and that preference needs to be stored with the date/time.

Trying to do 5) before 4) gives us earlier iterations of this patch which have things that should be formatter / widget settings being put as field settings, a mess that would be hard to unpick or build on. Trying to do 4) before 1) and 2) and 3) is painful and error-prone.

This would be 4 new issues and a meta to keep track of it all.

I am still not sure what 'timezone_per_date' is providing us here, especially since exposes more test surface area.

I don't think it's a sure thing either. It would be OK for core to default to always using the per-date if one existed. Especially if we put on the trait

protected function shouldUsePerDateTimezone() {
  return TRUE;
}

as this would make it trivial to change this behavior.

However, there area few reasons I think it may be worth having the setting:

a) It will always be desirable to have the formatter setting to choose the default time zone, even if per-date is enabled in the field, because we have to handle fields with existing values that will have a NULL stored time zone. And having this setting, which will then be ignored most of the time, is slightly misleading for sitebuilders. It would make it more bluntly obvious how things worked if you have the additional checkbox for "If a preferred time zone is stored, use it instead of the default". This drives home the idea there are two different things at work here, a default and something that can override it per-date. The screenshots say it better than I can.

b) I suspect it's quite common to want to sometimes render a time in its canonical (venue) time zone and sometimes in the time zone of the user.

c) I imagine it might not be uncommon to want to hide the time zone selector on a "basic" form but have it exposed on an "advanced" form for superusers.

jonathanshaw’s picture

Status: Needs work » Needs review
jonathanshaw’s picture

FileSize
77.62 KB

The reroll, interdiff and test-only in #105 are correct, but the main patch 2632040-105.patch only has about half of the work. Here is the full patch. Sorry about this.

rvillan’s picture

#110 had a print_r statement that was causing AJAX errors when trying to add entities using a complex inline entity form. Here it is with it removed.

jonathanshaw’s picture

#112 is 10% smaller than #110, isn't that suspicious? Maybe a file is missing.

shawn_smiley’s picture

I ran a diff on the 108 and 111 patches. The 111 patch is missing the updates to the files datetime.install, ConfigurableTimezoneInterface.php, and ConfigurableTimezoneTrait.php

rvillan’s picture

Yeah, that was my fault -- I didn't create the patch correctly. Here it is fixed along with an interdiff against #108.

jdleonard’s picture

I'm not sure whether the screenshot below (stolen from the issue summary) is still representative of this issue's direction, but in case this hasn't already been considered, I wanted to throw out the possibility that the date range field should provide the ability for the start date and end date to have different timezones from one another (e.g. a travel site needs to store the departure and arrival times of a flight that crosses timezones).

Daterange field

tacituseu’s picture

Status: Needs work » Needs review
jonathanshaw’s picture

Re #116 and the possibility of different timezones in date ranges I'm mindful of @mpdonadio's frequent injunction that core should provide the basics, not cover every use case.

The truth is that the 2nd timezone selector is naturally present in the widget and I actually had to do some ugly work to suppress it. So supporting it in the widget is easy.

But there's nothing at the moment to support it in the storage and formatter. Worst of all, because most sites would want to use the same timezone for both, we'd have to have setting and be constantly checking it and doing conditional logic based on it. The number of combinations of settings and formatters etc. is already painful, adding another permutation is not a very happy prospect.

On balance this seems like something that should be in datetime_extras, not core. But by all means review the patch and indicate if there is something that makes it unnecessarily hard to do this in contrib, if there's something that would make extending easier.

SKAUGHT’s picture

RE@116. different end timezone.

That is an interesting use case. but could covered by simply using 2 separate combo date fields and a custom formatter..or even a secondary field capturing the expected length of flight..

Certainly an idea that would deserve it's own ticket.

jdleonard’s picture

I completely understand the points raised in #119 and #120. I can see how the permutations could be getting a bit nuts.

While this use case could be solved per #120, it does seem cleaner to solve this in core. It is logical to me to defer the additional UI complication to a later date (or contrib) even if it's easy to expose the widget, but would it not make sense to address the storage aspect here to provide contrib with a simpler path to full implementation?

I'm not invested in this use case; I just thought it would be appropriate to address here.

jonathanshaw’s picture

Having a field column sitting there that we never used would be very confusing to anyone looking at the code. If it's in the storage we need to use it.
Let's see what @mpdonadio thinks.

jhedstrom’s picture

Nice progress here! A few items I found in reviewing the latest patch:

  1. +++ b/core/modules/field_ui/src/Form/FieldStorageAddForm.php
    @@ -410,7 +410,7 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
    +   * Configures the newly created field for the default view and form modes.
    
    @@ -428,7 +428,7 @@ protected function configureEntityFormDisplay($field_name, $widget_id = NULL) {
    +   * Configures the newly created field for the default view and form modes.
    

    These changes look unrelated?

  2. +++ b/core/modules/datetime/src/Plugin/Field/ConfigurableTimezoneTrait.php
    @@ -0,0 +1,133 @@
    +  ¶
    ...
    +    ¶
    

    Whitespace errors.

  3. +++ b/core/modules/field/tests/src/Kernel/Migrate/d6/MigrateFieldFormatterSettingsTest.php
    @@ -156,7 +157,7 @@ public function testEntityDisplaySettings() {
    +    $defaults = ['format_type' => 'fallback', 'timezone_override' => '', 'timezone_default' => DateTimeFormatterBase::TIMEZONE_USER];
    

    While technically ok, this constant should probably reference the ConfigurableTimezoneInterface rather than the base class...

mpdonadio’s picture

@jhedstrom, what are your thoughts on separate TZ for start/end for daterange? I'm torn on this. I think it is an edge case, but don't want to design ourselves into a corner and end up with a nasty update path (though this wouldn't probably be that hard). My gut says that we should push that feature to contrib.

jhedstrom’s picture

what are your thoughts on separate TZ for start/end for daterange?

Sine we already have the widget forced to a single TZ selection, even though that isn't the default widget behavior, I think we can push the separate TZ per start/end to contrib and/or a follow-up. There would be additional formatter, widget options etc that we don't need to complicate this already complex issue further with I think. Folks making a site for multi-timezone races will have to wait ;)

jonathanshaw’s picture

@mpdonadio / @jhedstrom I'm waiting for your take on #107, whether you want me to split this up or to proceed in just this issue?

mpdonadio’s picture

I am not sure if we are going down the right path with this right now.

I am still not convinced about the timezone_storage setting on storage. I am leaning toward always storing the TZ; I have a feeling that the empty column is going to cause problems with REST and API usage, and possibly Views (not 100% sure, but empty values make me uneasy).

For the widgets, I think we can have an option to expose a TZ select, that defaults to drupal_get_user_timezone(). If the option is false, the just store drupal_get_user_timezone() with the field.

And then there would really not need to be any change to the formatters.

I think the current version couples storage / input / output too closely. I think the approach outlined in #34, which I think was was was the blueprint up to #94 or so is still the best bet.

@jhedstrom, thoughts?

jonathanshaw’s picture

I am leaning toward always storing the TZ; I have a feeling that the empty column is going to cause problems with REST and API usage, and possibly Views (not 100% sure, but empty values make me uneasy).

How would you handle existing fields? What value would you have the update path insert into the timezone column in these cases? If you don't want to have empty values presumably you'll have to force it to be the system's default timezone, but have the formatters default to not using the stored timezone?

Even if we don't allow this, perhaps there is still merit in having the timezone default (user/site/fixed) be a setting of the widgets, rather than a setting of the field and so forced to be the same in all widgets.

jhedstrom’s picture

I am still in favor of the approach outlined in #34.

Existing fields would get the site's timezone set I think, and since the prior to this, formatters only displayed in the site, or in the current user's timezone, this would be ignored by the formatters.

jonathanshaw’s picture

Always storing the timezone

I'm understanding you guys to want the timezone column never empty. This requires AFAICS:
1) In the update hook, create a timezone column for all datetime fields,
2) In the update hook, put the site's default timezone as the value in all existing rows
3) Always store a timezone, including when saving via the API, regardless of the field's settings.
4) The FieldType needs to implement the getDefaultValue() method, so as to provide drupal_get_user_timezone() as the default value for timezone column when saving via the API.

This all seems fine to me. It requires some additions to the patch because so far no one has made the API or update path store a timezone.

The settings

#34 is not explicit about the details of what settings there are on the field, widgets and formatters.
Given you've specified the timezone column will never be empty, we now have 2 possible approaches:

The minimal option:
FieldType: have a checkbox for "Use per-date timezones"
Widget: If "Use per-date timezones" is checked on the FieldType, always have timezone selectors exposed by the widget, and always have these default to drupal_get_user_timezone()
Formatter: If "Use per-date timezones" is checked on the FieldType, always display dates using the stored timezone, do not allow overriding (hide the overrides setting).

More flexible options, which can be adopted in various combinations:

FieldType: Allow the user to choose the default timezone from "Site", "User", or "Fixed".
Widget: Have a select setting called "Timezone to use" with values "User's timezone", "Site default timezone", "Selected by user", and "Fixed". Timezone selectors are only exposed to the editor if the "Selected by User" option is shown. The "Fixed" option allows the sitebuilder to specify a particular timezone.
Formatter: Have a select setting called "Timezone to use" with values "User's timezone", "Site default timezone", "Stored timezone", and "Fixed".
If you have settings like this for the widgets and formatters, then you don't need a "Use per-date timezone" checkbox setting on the FieldType.

I think the idea of a default setting on the FieldType is worth considering seriously (and present in #94), because this affects the API behavior and can't easily be left to datetime_extras.

Overall when it comes to the widgets and formatters we have a choice between:
A) having a checkbox setting "Use per date timezones" on the FieldType, a (sometimes bypassed) "Timezone override" setting in the formatter, and nothing on the widget; OR
B) not having the checkbox setting on the FieldType, on the formatter replacing Timezone override with a broader selection of choices ("User's timezone", "Site default timezone", "Stored timezone", and "Fixed"), and reusing the same logic on widgets.

My sense is that B gives more power to sitebuilders while being not much more complex than A (possibly cleaner, depending on your tastes). But I can see there is a case for keeping core simple and leaving the formatter and widget settings to some more flexible widgets/formatters in datetime_extras.

benjy’s picture

Status: Needs work » Needs review
FileSize
77.68 KB

Re-rolled against 8.4.x head.

Version: 8.4.x-dev » 8.5.x-dev

Drupal 8.4.0-alpha1 will be released the week of July 31, 2017, which means new developments and disruptive changes should now be targeted against the 8.5.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

BrianLewisDesign’s picture

The 2632040-131.patch doesn't apply on core 8.4.2.

Looks like the patch is failing in core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimeFormatterBase.php, Here is the rejected file I get from composer:

$ cat core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimeFormatterBase.php.rej
--- modules/datetime/src/Plugin/Field/FieldFormatter/DateTimeFormatterBase.php
+++ modules/datetime/src/Plugin/Field/FieldFormatter/DateTimeFormatterBase.php
@@ -13,12 +14,15 @@
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
 use Drupal\datetime\Plugin\Field\FieldType\DateTimeItem;
 use Symfony\Component\DependencyInjection\ContainerInterface;
-
+use Drupal\datetime\Plugin\Field\ConfigurableTimezoneTrait;
+use Drupal\datetime\Plugin\Field\ConfigurableTimezoneInterface;
 
 /**
  * Base class for 'DateTime Field formatter' plugin implementations.
  */
-abstract class DateTimeFormatterBase extends FormatterBase implements ContainerFactoryPluginInterface {
+abstract class DateTimeFormatterBase extends FormatterBase implements ConfigurableTimezoneInterface, ContainerFactoryPluginInterface {
+
+  use ConfigurableTimezoneTrait;
 
   /**
    * The date formatter service.
Tim Bozeman’s picture

Re-rolled against 8.4.x head for people using this in production already and fixed an undefined index error.

mpdonadio’s picture

Status: Needs work » Needs review

Not sure if this will apply, but since this is a Feature Request, development should be against 8.5.x.

mpdonadio’s picture

Issue tags: +Needs reroll

Looks like we need a reroll for 8.5.x.

jofitz’s picture

Status: Needs work » Needs review
Issue tags: -Needs reroll
FileSize
75.3 KB

Re-rolled for 8.5.x.
Corrected some coding standard errors.

jofitz’s picture

Corrected a few test failures.
Removed a few more coding standards errors.

Version: 8.5.x-dev » 8.6.x-dev

Drupal 8.5.0-alpha1 will be released the week of January 17, 2018, which means new developments and disruptive changes should now be targeted against the 8.6.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

GoZ’s picture

+++ b/core/modules/datetime/datetime.install
@@ -0,0 +1,100 @@
+          $field_name . '_value' => [

Field name in database is not necessary suffixed by _value.

In case this field is created in an Entity thanks to BaseFieldDefinition::create(), the field name column will be named as defined in the key of array returned by baseFieldDefinitions.

Example:

  public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
    $fields = parent::baseFieldDefinitions($entity_type);

    $fields['start_at'] = BaseFieldDefinition::create('datetime')
      ->setLabel(t('Start at'))
      ->setDescription(t('Start time for this entity'))
      ->setDisplayOptions('view', [
        'label' => 'above',
        'type' => 'datetime_default',
        'settings' => ['format_type' => 'default'],
        'weight' => 10,
      ])
      ->setDisplayOptions('form', [
        'type' => 'date',
        'weight' => 10,
      ])
      ->setDisplayConfigurable('form', TRUE)
      ->setDisplayConfigurable('view', TRUE);

    return $fields;
  }
HiMyNameIsSeb’s picture

I'm wondering if saving all date and time fields as UTC is as designed. (I don't know enough about that in particular to know for sure).

We have a site that is used around the world, users can create events in their region. They are created using standard Drupal nodes with a date range field. Dates seem to all be stored in UTC within the database, and when rendered are formatted for the current users timezone, or defaults to UTC if the user has no timezone.

This works as expected in all cases where we are using the default node forms and rendering the fields using the default node field settings.

There is however 1 case where our events are created outside of the default Drupal forms. We considered our options, and they were;
- To store dates as the users timezone and flag this against the field value (as the patch above).
- To convert the date to UTC for storage in the database.

We decided to go with storing all dates in the database as UTC.

This is not necessarily a better way of doing this, it purely depends on your needs. However, I hope it helps someone in the future.

For our needs, it works nicely. Below is how we reverted the date selected by the user to UTC before saving the node.

// Get the users timezone.
$user = \Drupal::currentUser();
$user_timezone = $user->getTimeZone();

// Calculate the user timezone vs UTC offset.
$time = new \DateTime('now', new \DateTimeZone($user_timezone));

// Get the dates from the save array (in this case a form array).
$from_object = new \DateTime($event['start']);
$to_object = new \DateTime($event['end']);

// Convert the offset value so that we can use it.
$timezone_offset = $time->format('Z');

// Convert the dates to UTC and format.
$from_object->modify("- $timezone_offset seconds");
$to_object->modify("- $timezone_offset seconds");
$from = $from_object->format('Y-m-d\TH:i:s');
$to = $to_object->format('Y-m-d\TH:i:s');

// Save the node.
$node = Node::create([
      'type' => 'event',
      'langcode' => 'en',
      'field_event_date' => [
        'value' => $from,
        'end_value' => $to,
      ],
      'field_event_location' => $event['location'],
      'field_group_id' => $event['group_id'],
      'title' => $event['title'],
]);
$node->save();
mpdonadio’s picture

I'm wondering if saving all date and time fields as UTC is as designed. (I don't know enough about that in particular to know for sure).

Yes, there are a few constants for this:

DateTimeItemInterface::STORAGE_TIMEZONE
DateTimeItemInterface::DATETIME_STORAGE_FORMAT
DateTimeItemInterface::DATE_STORAGE_FORMAT

Re, your code, you can just create your date objects and use the `->setTimezone(DateTimeItemInterface::STORAGE_TIMEZONE)`, and then use `->format(DateTimeItemInterface::DATETIME_STORAGE_FORMAT)`.

brian.reese’s picture

I had an issue using #141 when I had pre-existing datetime base fields defined. Drupal\field\Entity\FieldStorageConfig::load() used in the update hook won't load Drupal\Core\Field\BaseFieldDefinition and the update hook fails. A fresh site install doesn't have the same issue since the update hook doesn't run

sjancich’s picture

With the patch in #141, I can confirm that the formatter does not respect the "Preferred time zone for each date" setting under Manage Display for date range fields. Date fields seem to respect it fine, though.

ekes’s picture

Issue tags: +DevDaysLisbon

Just noting I'm looking at this at Dev Days.

ekes’s picture

Straight re-roll.

ekes’s picture

Checking first work done. Fixes some tests, and other small items. Setting 'needs review' for tests.

Notes:-

Change to datetime_range.schema.yml: Fixes issue confirmed in #148. "Preferred time zone for each date" setting under Manage Display (widget), as it didn't have schema.

In DateTimeFieldTest: Adds the configuration required Timezone fixed. This is also not obvious in the UI. Maybe wants javascript to make the Timezone override only appear when fixed is selected?

-    $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_default' => ConfigurableTimezoneInterface::TIMEZONE_FIXED, 'timezone_override' => 'America/New_York'] + $this->d
efaultSettings;

In EntityTestDateonlyTest::getExpectedNormalizedEntity(): As expected there is additional output for the timezone. I assume for the API there could also be tests for setting via the API?

+          'timezone' => $this->entity->get(static::$fieldName)->value_timezone,
Dane Powell’s picture

Have you tested the upgrade path on a site with existing datetime fields? Patch #151 does not appear to address the concerns in #147 about the upgrade path.

ekes’s picture

Status: Needs work » Needs review
FileSize
98.5 KB
11.89 KB

Have you tested the upgrade path on a site with existing datetime fields? Patch #151 does not appear to address the concerns in #147 about the upgrade path.

Correct. 151 was just checking the work done, as explained.

Attached includes a test for datetime_update_8001(). I'm afraid I couldn't reproduce the issue mentioned in #147 either in the test or manually.

The patch also changes the check to see if it should store a timezone to use the storage setting specically for that (rather than if there is a fixed timezone option).

It also adds an additional test for the REST API for datetime with a timezone. This highlighted the need for additional validators. Also added.

Version: 8.6.x-dev » 8.7.x-dev

Drupal 8.6.0-alpha1 will be released the week of July 16, 2018, which means new developments and disruptive changes should now be targeted against the 8.7.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Dane Powell’s picture

#154 does not apply cleanly to 8.7.x, which is the new target branch.

Additionally, for some reason Composer refuses to apply it to 8.6.x. I'll attach a version that seems to work with 8.6.x.

SKAUGHT’s picture

Status: Needs work » Needs review
ekes’s picture

Re-roll for 8.7.x to take into account changes from #2846963: Clean up DateRangeWidgetBase::massageFormValues(). Change is in the logic of the ::massageFormValues of both widgets. One of the new tests now fails (For scenario per-date 'not allowed' with default 'user': the time zone stored is expected to be '', actually is 'storage'.). I'll try and work out if that's the new logic or the test itself.

Interdiff is a diff of the two diffs (as the original doesn't apply).

SKAUGHT’s picture

Status: Needs work » Needs review
nlisgo’s picture

Issue tags: +Needs reroll
ekes’s picture

Status: Needs work » Needs review
FileSize
95.15 KB

It's this horrid fixture sql stuff that's not applying, maybe mangled already, but quickly what I have here at the moment --binary on.

ekes’s picture

Rolling the patch with git diff --binary solves the issue with the fixture sql applying.

This adds back some code from #54 which got lost somewhere and was causing test fails. The timezone correction to storage should only be done once after validation.

It also changes the same test so that when timezone storage is enabled (on the field settings) but the widget does not allow the user to change it per-field, the test checks for the default timezone being stored (which is the case).

arsoman’s picture

Category: Feature request » Bug report
FileSize
123.2 KB
107.74 KB
112.8 KB
154.19 KB

Hi, team! The patch is ok, but needs some UI change, in order to be perfect. In this file: core/lib/Drupal/Core/Datetime/Element/Datetime.php we are building select field, with list of all countries, but using machine names of the countries both as a keys, and as a values for that list. This is wrong, because leading to the effect, that the end user will see in the dropdown machine names of the city/timezone, with underscores, instead of spaces! Screenshot: problem ui
The problem comes from here, where the values of the select element is not OK for UI use: problem code.
So i suggest the following fix:

// Expose a timezone selector.
    if (!empty($element['#expose_timezone']) && $element['#expose_timezone']) {
      <strong>$timezone_names = \DateTimeZone::listIdentifiers();
      array_walk($timezone_names, function (&$e, $i) {
        $e = str_replace('_', ' ', $e);
      });</strong>
      $element['timezone'] = array(
        '#type' => 'select',
        '#options' => array_combine(\DateTimeZone::listIdentifiers(), <strong>$timezone_names</strong>),
        // Default to user's timezone.
        '#default_value' => $element['#date_timezone'],
        '#required' => $element['#required'],
      );
    }

.
Screenshot of the code base: problem code solution
That way we will have the correct UI names of the timezones: problem ui solution

mpdonadio’s picture

Category: Bug report » Feature request

Thanks for the review. This is still a Feature Request, though.

lfilipov’s picture

We had an issue with the timezone names because they were *odd* according to the client so the solution that @arsoman has offered worked for us. #171

givanov’s picture

I have used the patch proposed by arsoman and it worked as described for me. Thanks.

SKAUGHT’s picture

Status: Needs work » Needs review

changing status to try to trigger test correctly.

jofitz’s picture

The patch in #171 is an interdiff to be applied after the patch in #167. Here is the combination of the two.

jofitz’s picture

Status: Needs work » Needs review
FileSize
95.33 KB

I forgot --binary.

mpdonadio’s picture

+++ b/core/lib/Drupal/Core/Datetime/Element/Datetime.php
@@ -332,6 +345,23 @@ public static function processAjaxForm(&$element, FormStateInterface $form_state
+    // Expose a timezone selector.
+    if (!empty($element['#expose_timezone']) && $element['#expose_timezone']) {
+      $timezone_names = \DateTimeZone::listIdentifiers();
+
+      // Replace underscores with spaces within name of the timezone.
+      array_walk($timezone_names, function (&$e, $i) {
+        $e = str_replace('_', ' ', $e);
+      });
+      $element['timezone'] = array(
+        '#type' => 'select',
+        '#options' => array_combine(\DateTimeZone::listIdentifiers(), $timezone_names),
+        // Default to user's timezone.
+        '#default_value' => $element['#date_timezone'],
+        '#required' => $element['#required'],
+      );
+    }

This should use system_time_zones() instead of getting them directly from `\DateTimeZone::listIdentifiers()`. That has the improved UX and already does the nicer name display. See #2847651: Improve timezones selector with optgroups.

jofitz’s picture

jofitz’s picture

Fix coding standards errors and one of the test failures.

John Cook’s picture

I've tested this with a datetime field in a paragraph type. The DB update fails on this line:

+++ b/core/modules/datetime/datetime.install
@@ -0,0 +1,100 @@
+        $schema->addField($revision_table, $timezone_field_name, $field_spec);

At this point, the $field_changes variable is as follows:

$field_changes = array (
  'field_storage_definitions' => 
  array (
    'field_date' => 'paragraph__field_date',
  ),
  'revision_table' => 'paragraphs_item_revision',
)

The actual revision table should be paragraph_revision__field_date, so the update fails with

Cannot add field paragraphs_item_revision__field_date.field_date_timezone: table doesn't exist.
John Cook’s picture

I've changed the the way the revision table is determined. This patch uses DefaultTableMapping::getDedicatedRevisionTableName() to get the correct table name.

John Cook’s picture

Status: Needs work » Needs review
John Cook’s picture

Status: Needs work » Needs review
FileSize
95.29 KB

Re-uploaded patch #184 using --binary.

wengerk’s picture

Fix Drupal coding standards from #187. Only on new added code.

wengerk’s picture

Reupload #188 with --binary .... sorry ...

Also, about the 6 fails, I have 2 questions to help on these:

- Fail on Drupal\Tests\layout_builder\FunctionalJavascript\FieldBlockTest: Is it a side effect or an issue on layout_builder ?
- Fail on Drupal\Tests\node\Kernel\Migrate\d7\MigrateNodeTest: same questions, as upper - side effect of reflecting changes ? Should we only update the expected results ?

Waiting answers, I'm working on the Datetime Range missing upgrade path & Tests.

mpdonadio’s picture

Did a quick look at the fails, but did not look at the patch yet.

I think the FieldBlockTest one is unrelated.

I think the fail in MigrateNodeTest is legit and a side effect of the patch. We can't just update the value in the test, we need to figure out what is causing it.

wengerk’s picture

I struggle with the datetime_range.post_update.php which keeps failing on:

Failed: Drupal\Core\Entity\EntityStorageException: Exception thrown while performing a schema update. SQLSTATE[HY000]: General error: 1 no such column: field_range_timezone: SELECT 1 AS expression FROM {node_revision__field_range} t WHERE (field_range_value IS NOT NULL) OR (field_range_timezone IS NOT NULL) OR (field_range_end_value IS NOT NULL) LIMIT 1 OFFSET 0;

Even after adding a new datetime_range.install::datetime_range_update_8001 derived from datetime.install::datetime_update_8001. which add the timezone field to the daterange field type.

I think I miss a point, maybe from the datetime_range.schema.yml which should be updated with the new timezone storage.
Someone with a global & better vision on datetime & daterange modules could found the solution.

To me, it seems the datetime_range.post_update.php don't see the updated schema from the previous update. But I'm maybe wrong.

jonathanshaw’s picture

@mpdonadio if you're ever ready to think about this more, #127 - #130 is the discussion that I think is still unresolved where I stalled waiting for direction.

jonathanshaw’s picture

Given we've made little progress for 18 months, I suggest splitting the issue per #107 is the way forward.

mpdonadio’s picture

I talked to @wengerk offline quickly and am going to review this patch again this weekend.

Re the test split, I will be making a meta about this. The existing tests are way too wieldy right now. There are a few in-progress patches are doing this.

jonathanshaw’s picture

It's also the formatter refactoring that could be split out of this issue.

rgristroph’s picture

This is an extension of the patch at #157, targeted at 8.5.6.

The fix I did was that when you have a datetime_range which is not using the default timezone, the timezone was being applied only to the start time in the validation function. That meant that if your site had a site timezone of Chicago, and you created an event that lasted 2 hours in the Los Angeles timezone, on saving the node you would get the "end time cannot be before start time" error.

As I am testing it, there is still at least one issue -- after saving the node, the time displays in UTC but the timezone displayed is the local one. I will try to do more testing of that over the weekend.

rgristroph’s picture

This patch is a further re-working of #198 and #157, targeting 8.5.7.

The main change was in core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimeFormatterBase.php in the buildDateWithIsoAttribute() method - it was presuming that the date could just be formatted and 'Z' appended to it, but if we are keeping track of a timezone that isn't the case. Also, I added a 'timezone' => $timezone element to the render array, because if you want to display the timezone on the theming end you will need that.

I think this patch needs more attention both on reviewing the code and tests. I'll try to re-work #191 and get something that passes tests and can be reviewed.

SKAUGHT’s picture

Status: Needs work » Needs review
John Cook’s picture

This issue seems to have been forked.

There's two versions of the patch; one in Comment #191 and one in Comment #199.

From the comments, I believe that #191 is actually ahead of #199. So the changes from #198 and #199 should be brought into #191 as a new patch.

recrit’s picture

- Added an update function for the datetime_range module.
- Added some checks for table exists, field exists, index exists.

recrit’s picture

FileSize
100.88 KB
7.62 KB

Fixed the table names in the update functions. The original was getting the field table name and then creating the revision table as:

$revision_table = $field_changes['revision_table'] . '__' . $field_name;

This does not work for all entity types.
Example: "paragraph" entity type, "field_example"
*Original patch <= 203* would create the table name of "paragraphs_item_revision_field_example", which is wrong and causes errors during the update. The field revision table name is "paragraph_revision__field_example".

*New patch 204" uses the correct "paragraph_revision__field_example".

akalata’s picture

Status: Needs work » Needs review
recrit’s picture

Fix some bugs with original patch storing "" instead of NULL.

sjerdo’s picture

Since #2799987: Datetime and Datelist element's timezone handling is fragile, buggy and wrongly documented is committed to 8.7.x-dev branch, this patch needs an update.

akalata’s picture

Here is a reroll of #191 for 8.7.x.

I am ignoring patches #198, #199, #203, #204, and #207, since they are explicitly against 8.5.x.

I am not yet able to get the change to the field actually working, though, field data ends up set as UTC regardless of what I enter. I think this is because the data is getting looped through massageValues() multiple times, and the second loop ends up using the storage timezone as the selected timezone.

akalata’s picture

Updated reroll.

akalata’s picture

Status: Needs work » Needs review
FileSize
101.5 KB
5 KB

This patch fixes the timezone handling for DatetimeRange fields.

Two additional issues I'm seeing:

Date range field validation: Since since the end_date is always communicated in UTC, it's possible for the start_date in a specific timezone to be after the end_date value in UTC.

This fails validation:

Start time entered: 2018-12-20 at 12:00:00 (12pm) America/Chicago (UTC-500)
End time entered: 2018-12-20 at 13:00:00 (1pm)

While this passes validation:

Start time entered: 2018-12-20 at 12:00:00 (12pm) Pacific/Wallis (UTC+1200)
End time entered: 2018-12-20 at 13:00:00 (1pm)

Timezone name validity: Timezone name values with spaces (ex America/New York) are not valid (should be America/New_York). The error message here is misleading/incomplete "The Start date date is invalid. Please enter a date in the format 2018-12-13 18:54:34. ".

akalata’s picture

Status: Needs work » Needs review
FileSize
101.26 KB
933 bytes

This adds a fix for the timezone validity.

I'm guessing one of the changes in #211 made the date range field validation even wonkier than I had described, so I wanted to post this progress before continuing to dig into that.

akalata’s picture

Status: Needs work » Needs review
FileSize
102.09 KB
1.09 KB

This update adds a check to the datetime_range's validateStartEnd to ensure that the explict start_date/value timezone and the implicit end_date/end_value timezone are the same before comparison.

jhedstrom’s picture

Here's a quick review, by no means thorough, but just some nits and changes for the next version of the patch. It's looking really good, and once the tests are passing, I think it's very close! I'm also removing the upgrade path tests tag, as this now has those too :)

  1. +++ b/core/modules/datetime/src/Plugin/Field/FieldWidget/DateTimeWidgetBase.php
    @@ -2,17 +2,101 @@
    +  use ConfigurableTimezoneTrait;
    
    +++ b/core/modules/datetime/src/Plugin/Validation/Constraint/DateTimeFormatConstraint.php
    @@ -35,4 +35,18 @@ class DateTimeFormatConstraint extends Constraint {
    +  public $badTimezoneValue = "The timezone value '@timezone' did not parse properly.";
    

    The word 'parse' seems overly specific. How about something a bit more user-facing?

    PHP natively uses this message:

    Unknown or bad timezone (@timezone).
    
  2. +++ b/core/modules/datetime/tests/src/Functional/DateTimeFieldTest.php
    @@ -368,6 +374,329 @@ public function testDatetimeField() {
    +
    

    nit: extra spacing.

  3. +++ b/core/modules/datetime/tests/src/Functional/DateTimeFieldTest.php
    @@ -368,6 +374,329 @@ public function testDatetimeField() {
    +    $user = User::load($this->loggedInUser->id());
    +    $user->set('timezone', $timezone)->save();
    +    $this->setCurrentUser($user);
    

    Just to be safe, this should probably use an assertInstanceOf here to to ensure there is a current user. Otherwise future uses of this will throw a non-useful fatal error if somebody calls this when there is no logged in user. Something like this perhaps:

    $this->assertInstanceOf(UserInterface::class, $this->loggedInUser, 'Tried to set timezone without a user.');
    
akalata’s picture

Thanks @jhedstrom!

I've addressed #1 and #3. I couldn't see the issue mentioned in #2.

I've also reverted the class name change in EntityTestDatetimeTest, and added the hook_update_N for datetime_range (daterange) fields.

dpi’s picture

ConfigurableTimezoneTrait has a hidden dependency to the configuration factory (as ->config).

Im thinking the trait should implement a method returning the config factory from the \Drupal::service() helper.

Implementing classes such as DateTimeDefaultFormatter can implement their own method that uses DI, overriding the traits new configfactory method.

The patch also changes the constructor for DateTimeDefaultFormatter, it probably shouldnt do that. See Safely extending Drupal 8 plugin classes without fear of constructor changes for a rationale and workaround.

akalata’s picture

Status: Needs work » Needs review

Thanks @dpi,

Based on the blog post you've linked and the related comments/discussions linked from there, it appears that constructors for plugins are considered @internal and are allowed to change per https://www.drupal.org/core/d8-bc-policy#constructors.

While the "workaround" technique looks sound, I don't think we can tell core to override itself?

Some good discussion about breaking changes caused by core updates begins at https://www.drupal.org/project/drupal/issues/2856625#comment-11967635 on an issue that removed an unused service from a constructor. I am not currently aware of any similar discussion around adding a service to a constructor -- but we should definitely have a recommendation to avoid contrib breaks as much as possible.

koppie’s picture

I've rerolled the patch from #199 against Drupal 8.6.4.

koppie’s picture

Newb alert, sorry! I read the thread more carefully and I'm grabbing the patch from #218, and backporting against 8.6.x-dev.

SocialNicheGuru’s picture

225 is not working
git apply drupal-2632040-225.patch
fatal: invalid path '.'

nlisgo’s picture

Issue tags: +Needs reroll
savkaviktor16@gmail.com’s picture

Assigned: Unassigned » savkaviktor16@gmail.com

I'm going to reroll it

savkaviktor16@gmail.com’s picture

Assigned: savkaviktor16@gmail.com » Unassigned
Issue tags: -Needs reroll

actually, the patch #218 doesn't need to be rerolled. it applies cleanly

kkasson’s picture

I just got this error running database updates after testing the patch from #218:

Failed: Drupal\Core\Database\SchemaObjectDoesNotExistException: Cannot add field user_revision__field_MY_DATETIME_FIELD.field_MY_DATETIME_FIELD_timezone: table doesn't exist. in Drupal\Core\Database\Driver\mysql\Schema->addField()

The datetime field is on the user and it correctly updated the user__ table. It looks like it shouldn't be a problem because that table doesn't need to exist anyway. It looks like the update code is supposed to be checking if revisions exist or not, so I'm not sure exactly why it's trying to update it. I'm running the patch on 8.6.x - not sure if that makes a difference.

pcambra’s picture

captaindav’s picture

I tried the above patches for 8.6.x, they failed to apply. As a work-around I installed the https://www.drupal.org/project/datetime_range_timezone module which made it possible to create new fields with a new field type (based on the core date range field) that supports timezones. Unfortunately the only easy way we could find to switch to the new field type was to simply delete the existing fields with the core date/range type, and then make new fields with the same name. This of course breaks any views that referenced the original field, which is unfortunate and of course takes some time to fix, but it was the easiest way we could find to implement the new field type. I think from this respect the patch is preferable, in that the patch would alter the existing fields so they had timezone support, without requiring deletion of the existing date range fields first.

Version: 8.7.x-dev » 8.8.x-dev

Drupal 8.7.0-alpha1 will be released the week of March 11, 2019, which means new developments and disruptive changes should now be targeted against the 8.8.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

WidgetsBurritos’s picture

This is mostly just a reroll of #218 against 8.8.x with a couple exceptions.

  1. It appears that changes made in #2926508: Add DateTimeNormalizer+TimestampNormalizer, deprecate TimestampItemNormalizer: @DataType-level normalizers are reusable by JSON:API on February 15 are preventing the patch in #218 from applying cleanly against both 8.7.x and 8.8.x.

    The following change was made in that issue to the EntityTestDatetimeTest class:

    +++ b/core/modules/datetime/tests/src/Functional/EntityResource/EntityTest/EntityTestDatetimeTest.php
    @@ -90,7 +90,7 @@ protected function getExpectedNormalizedEntity() {
         return parent::getExpectedNormalizedEntity() + [
           static::$fieldName => [
             [
    -          'value' => $this->entity->get(static::$fieldName)->value,
    +          'value' => '2017-03-02T07:02:00+11:00',
             ],
           ],
         ];
    

    The old value would have been '2017-03-01T20:02:00' where it's now '2017-03-02T07:02:00+11:00', which includes expected timezone. From what I understand from that issue it's supposed to be based on system default timezone. I dunno if that should affect strategy on this ticket in any way.

    That said after running the test locally, looks like we got a new issue with this:

    1)
    Drupal\Tests\datetime\Functional\EntityResource\EntityTest\EntityTestDatetimeTest::testPatch
    Exception: Notice: Undefined index: timezone
    Drupal\datetime\Plugin\Validation\Constraint\DateTimeFormatConstraintValidator->validate()()
    (Line: 55)

    I didn't see that same error in the test error log for #218, so this will require further investigation.

  2. Also it seems like starting in #209 we dropped these lines from one of the tests in that same file:
    +++ b/core/modules/datetime/tests/src/Functional/EntityResource/EntityTest/EntityTestDatetimeTimezoneTest.php
    @@ -118,35 +125,16 @@ protected function assertNormalizationEdgeCases($method, Url $url, array $reques
    -      // DX: 422 when date type is incorrect.
    -      $normalization = $this->getNormalizedPostEntity();
    -      $normalization[static::$fieldName][0]['value'] = [
    -        '2017', '03', '01', '21', '53', '00',
    -      ];
    -
    -      $request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format);
    -      $response = $this->request($method, $url, $request_options);
    -      $message = "Unprocessable Entity: validation failed.\n{$fieldName}.0: The datetime value must be a string.\n{$fieldName}.0.value: This value should be of the correct primitive type.\n";
    -      $this->assertResourceErrorResponse(422, $message, $response);
    -
    -      // DX: 422 when date format is incorrect.
    -      $normalization = $this->getNormalizedPostEntity();
    -      $value = '2017-03-01';
    -      $normalization[static::$fieldName][0]['value'] = $value;
    -
    -      $request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format);
    -      $response = $this->request($method, $url, $request_options);
    -      $message = "Unprocessable Entity: validation failed.\n{$fieldName}.0: The datetime value '{$value}' is invalid for the format 'Y-m-d\\TH:i:s'\n";
    -      $this->assertResourceErrorResponse(422, $message, $response);
    -
    -      // DX: 422 when date format is incorrect.
    

    I'm unsure as to the reasoning there. This didn't seem to be the case in #191, which that patch is based on and I didn't see any comments in between those two issues indicating the reason for it, so I don't really know why it was left out. As this portion of the file was in conflict as well, I put that bit of code back in that file because it seems like we don't want to lose important test context there.

WidgetsBurritos’s picture

Status: Needs work » Needs review
FileSize
99.02 KB
5.91 KB

This patch attempts to rationalize some of the test behavior from #2926508: Add DateTimeNormalizer+TimestampNormalizer, deprecate TimestampItemNormalizer: @DataType-level normalizers are reusable by JSON:API with the new test behavior from this issue. I don't expect all tests to pass here, but we placing into review anyway to trigger the test runner.

Part of my logic in this patch was, instead of replacing the existing assertNormalizationEdgeCases test behavior like it seemed like we were doing in #218, is rather to just add additional test coverage for the timezone validation (although all other tests did need some sort of definition for timezone, even if that value was NULL).

@@ -50,7 +55,7 @@ public function setUp() {
       'field_name' => static::$fieldName,
       'type' => 'datetime',
       'entity_type' => static::$entityTypeId,
-      'settings' => ['datetime_type' => DateTimeItem::DATETIME_TYPE_DATETIME],
+      'settings' => ['datetime_type' => DateTimeItem::DATETIME_TYPE_DATETIME, 'timezone_storage' => TRUE],
     ])
       ->save();

^ Based on this though we're still only ever testing if timezone storage is true. We probably need a second test for when this value is false (which I imagine should behave almost exactly as it does now).

WidgetsBurritos’s picture

FileSize
110.63 KB
5.91 KB

Oddly the binary file got left out on that last patch. Attempting again. Interdiff from previous post is still correct, but posting again just to avoid confusion on numeric indexes.

WidgetsBurritos’s picture

As I've been digging around, I discovered that EntityTestDateRangeTest won't be able to perform the same assertNormalizationEdgeCases() tests that EntityTestDateonlyTest does as DateTimeItem currently utilizes the DateTimeFormat constraint, but DateRangeItem does not, but there is open issue for that: #2847041: Add a format and start/end validation constraints to DateRangeItem to provide helpful REST/JSON:API error message. So I'm making note of it here, in the event that issue gets resolved before this one does, so we don't forget to update that test.

WidgetsBurritos’s picture

Issue summary: View changes
WidgetsBurritos’s picture

FileSize
111.9 KB
6.51 KB

This patch should resolve a few more test failures, but still not all of them.

Specifically, in regard to comments in #191 and #192, I've identified what's causing the the Drupal\Tests\node\Kernel\Migrate\d7\MigrateNodeTest failures, but haven't been able to identify the fix quite yet:

+++ b/core/modules/datetime/src/Plugin/Field/FieldType/DateTimeItem.php
@@ -64,18 +68,26 @@ public static function propertyDefinitions(FieldStorageDefinitionInterface $fiel
   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'],
       ],
     ];

The addition of the timezone value in field schema seems to be what's causing the failures. I'd presume that means we need to modify the corresponding fixture in core/modules/migrate_drupal/tests/fixtures/drupal7.php, but that's definitely not the easiest file to work with as it's over 52000 lines long. My initial attempts were fruitless so I did not include them in this patch, but I'll try again later if noone else beats me to it.

**Edit**
I forgot to include in my comment here, that it seems like the the change time is always getting set to the current system time. So whatever is happening there, is causing it to update the node again.

WidgetsBurritos’s picture

So looking at the remaining test failures a couple things stand out to me:

  1. Drupal\Tests\datetime\Functional\Update\DatetimeUpdateTest is only failing because it's referencing deprecated code and should probably add @group legacy to that new test. As such we'd need a follow up ticket to clean that up, but I presume we'd wait until this was actually merged in to see if it's necessary. If this doesn't make it in until D9 it may not be necessary. The included patch makes that change and I'm updating the IS to annotate that.
  2. Drupal\Tests\jsonapi\Functional\JsonApiRegressionTest test is failing because it appears a data structure is changing:

    Drupal\Tests\jsonapi\Functional\JsonApiRegressionTest::testPatchingDateTimeNormalizedWrongTimeZoneIssue3021194
    Failed asserting that Array &0 (
    'value' => '2018-09-16T22:00:00+10:00'
    'timezone' => null
    ) is identical to '2018-09-16T22:00:00+10:00'.

    This seems like this could be a BC break here as it's affecting the way things are outputted in the jsonapi. I think this warrants further discussion. I think it's a change that probably needs to happen, but could potentially break a bunch of decoupled sites/apps.

WidgetsBurritos’s picture

Issue summary: View changes

Making slight correction to my previous IS update

WidgetsBurritos’s picture

This patch addresses my previous comment in in #236:

we're still only ever testing if timezone storage is true. We probably need a second test for when this value is false (which I imagine should behave almost exactly as it does now).

So what I've done is actually rolled back a few of the changes in the EntityTestDatetimeTest and created a new property called $timezoneStorage which indicates if timezone storage should be used. I've defaulted this to FALSE. The old test operates in much the same way it does now.

Then I created a new test called EntityTestDatetimeTimezoneStorageTest that extends EntityTestDatetimeTest. This class sets $timezoneStorage to TRUE instead, and runs all the parent tests, but then also runs its own additional checks on invalid timezones.

The only real issue I could see here is if we ever need to run specific tests only when timezoneStorage is FALSE, in which case the existing EntityTestDatetimeTest may need to get converted to an abstract EntityTestDatetimeTestBase class instead, and then two separate tests extend that instead. I didn't necessarily want to go that far without external feedback as to whether that is the best approach here or not.

WidgetsBurritos’s picture

FileSize
112.34 KB
1.33 KB
  1. This patch should resolve the test failure described in #241.

    It took a bit of digging to figure out, a slight modification needed to be made to the DateField migration plugin to also account for timezone. I ended up not modifying the drupal7.php fixture (although I did learn that core/scripts/db-tools.php was a thing in the process of researching it).

    That said, if we want test coverage around the actual migration of timezone data, it might be worth modifying that fixture to include a new "Date with timezone" field, or something like that. I have not done that in this patch, but wanted to annotate the potential need for that in this issue.

  2. I can't seem to replicate the Drupal\Tests\layout_builder\FunctionalJavascript\FieldBlockTest failures locally:

    Drupal\Tests\layout_builder\FunctionalJavascript\FieldBlockT 1 passes

    I'm assuming it's some environment difference there, but may need another set of eyes on that one.

WidgetsBurritos’s picture

+++ b/core/modules/datetime/src/Plugin/migrate/field/DateField.php
@@ -68,6 +68,11 @@ public function defineValueProcessPipeline(MigrationInterface $migration, $field
+      'timezone' => [
+        'plugin' => 'default_value',
+        'default_value' => 'America/Chicago',
+        'source' => 'timezone',
+      ],

Welp, just realized that's the wrong default value, which illustrates the need for a test here. I'm off for the night but that will need to be fixed in a subsequent patch.

heddn’s picture

+++ b/core/modules/datetime/src/Plugin/migrate/field/DateField.php
@@ -68,6 +68,11 @@ public function defineValueProcessPipeline(MigrationInterface $migration, $field
+      'timezone' => [
+        'plugin' => 'default_value',
+        'default_value' => 'America/Chicago',
+        'source' => 'timezone',
+      ],

This should probably block things. Can we get a migrate issue opened up specifically for this aspect? It will need tests, etc. Do we need the rest of this patch though to do that?

WidgetsBurritos’s picture

@heddn,

Thanks for the feedback. This patch creates the necessary database columns to migrate into, so I think at least some portions of this patch would need to be required. Although maybe we could potentially split up this issue into multiple issues. One for all the database aspects and one for actually using the timezone field and one for the UI work. But that does create the potential for migrating in data that isn't actually being used anywhere, and causing some confusion (especially once the rest of this does get completed and starts changing timezone data on people unsuspectingly).

tim.plunkett’s picture

There's some timeout happening when opening the date field formatter form within the Layout Builder UI.
Couldn't track it down much further than there.


This is a HUGE patch. A lot of it seems necessary, but there are many things that seems extraneous or like they were just refactored while making the needed changes.
Even after removing all of the out of scope changes, it will still likely be a very large patch. Anything that can be done to reduce scope, I'd suggest you do it

  1. +++ b/core/modules/datetime_range/src/Plugin/Field/FieldWidget/DateRangeDefaultWidget.php
    @@ -33,8 +34,8 @@ class DateRangeDefaultWidget extends DateRangeWidgetBase implements ContainerFac
    -  public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, EntityStorageInterface $date_storage) {
    -    parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings);
    +  public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, ConfigFactoryInterface $config_factory, EntityStorageInterface $date_storage) {
    +    parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings, $config_factory);
    
    @@ -49,6 +50,7 @@ public static function create(ContainerInterface $container, array $configuratio
    +      $container->get('config.factory'),
           $container->get('entity_type.manager')->getStorage('date_format')
    

    This should add ConfigFactoryInterface to the end, allow it to be NULL, and add a @trigger_error when it's NULL and pull it from Drupal::service

  2. +++ b/core/modules/datetime/config/schema/datetime.schema.yml
    @@ -26,9 +29,15 @@ field.value.datetime:
    -      label: 'Time zone override'
    +      label: 'Timezone override'
    
    +++ b/core/modules/datetime_range/src/Plugin/Field/FieldWidget/DateRangeWidgetBase.php
    --- a/core/modules/datetime_range/tests/fixtures/update/datetime_range-filter-values.php
    +++ b/core/modules/datetime_range/tests/fixtures/update/datetime_range-filter-values.php
    
    +++ b/core/modules/datetime_range/tests/fixtures/update/datetime_range-filter-values.php
    @@ -329,4 +333,3 @@
       ),
       'mysql_character_set' => 'utf8mb4',
     ));
    -
    
    +++ b/core/modules/field/tests/src/Kernel/Migrate/d6/MigrateFieldFormatterSettingsTest.php
    @@ -93,10 +94,10 @@ public function testEntityDisplaySettings() {
         $expected['settings'] = [
    -       'scale' => 2,
    -       'decimal_separator' => '.',
    -       'thousand_separator' => ',',
    -       'prefix_suffix' => TRUE,
    +      'scale' => 2,
    +      'decimal_separator' => '.',
    +      'thousand_separator' => ',',
    +      'prefix_suffix' => TRUE,
         ];
    
    +++ b/core/modules/field_ui/src/Form/FieldStorageAddForm.php
    @@ -441,7 +441,7 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
    -   * Configures the field for the default form mode.
    +   * Configures the newly created field for the default view and form modes.
    
    @@ -467,7 +467,7 @@ protected function configureEntityFormDisplay($field_name, $widget_id = NULL, ar
    -   * Configures the field for the default view mode.
    +   * Configures the newly created field for the default view and form modes.
    

    Out of scope

  3. +++ b/core/modules/datetime/src/Plugin/Field/ConfigurableTimezoneTrait.php
    @@ -0,0 +1,133 @@
    +    $datetime_type = $this->fieldDefinition->getFieldStorageDefinition()
    +      ->getSetting('datetime_type');
    

    One line. Going to stop highlighting these, but there are many more to fix

  4. +++ b/core/modules/datetime/src/Plugin/Field/ConfigurableTimezoneTrait.php
    @@ -0,0 +1,133 @@
    +      if ($timezone_storage === TRUE) {
    

    === TRUE is one I haven't seen in a while

  5. +++ b/core/modules/datetime/src/Plugin/Field/ConfigurableTimezoneTrait.php
    @@ -0,0 +1,133 @@
    +        $summary[] = $this->t($per_date_summary['use']);
    

    AFAIK t() doesn't support this

  6. +++ b/core/modules/datetime/src/Plugin/Field/ConfigurableTimezoneTrait.php
    @@ -0,0 +1,133 @@
    +  public function getDefaultTimezone($itemTimezone = NULL) {
    

    Misuse of camelCase

  7. +++ b/core/modules/datetime/src/Plugin/Field/ConfigurableTimezoneTrait.php
    --- a/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimeCustomFormatter.php
    +++ b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimeCustomFormatter.php
    
    +++ b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimeCustomFormatter.php
    @@ -16,7 +16,7 @@
    - *)
    + * )
      */
    

    Out of scope

  8. +++ b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimeCustomFormatter.php
    @@ -83,7 +82,7 @@ public function settingsSummary() {
    -    $summary[] = $date->format($this->getSetting('date_format'), $this->getFormatSettings());
    +    $summary[] = $date->format($this->getSetting('date_format'));
    

    This change looks suspicious. Why is that second param no longer needed?

  9. +++ b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimeFormatterBase.php
    @@ -54,12 +64,15 @@
    -  public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, $label, $view_mode, array $third_party_settings, DateFormatterInterface $date_formatter, EntityStorageInterface $date_format_storage) {
    +  public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, $label, $view_mode, array $third_party_settings, DateFormatterInterface $date_formatter, EntityStorageInterface $date_format_storage, ConfigFactoryInterface $config_factory) {
    ...
    +    $this->config = $config_factory;
    

    Same as before with the @trigger_error

  10. +++ b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimeFormatterBase.php
    @@ -92,7 +108,9 @@ public static function defaultSettings() {
    +    $form = $this->timezoneSettingsForm($form, $form_state) +
    +    parent::settingsForm($form, $form_state);
    

    This should go on one line. Also, + might be problematic here if there are clashing numeric keys. Consider \Drupal\Component\Utility\NestedArray::mergeDeep()

  11. +++ b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimeFormatterBase.php
    @@ -92,7 +108,9 @@ public static function defaultSettings() {
    +    $form['timezone_per_date']['#description'] = "Where a time zone has been specified for a particular date value, use that instead of the default selected above.";
    

    "time zone" vs "timezone", here and elsewhere

  12. +++ b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimeFormatterBase.php
    @@ -109,12 +127,12 @@ public function settingsForm(array $form, FormStateInterface $form_state) {
    +    $summary = $this->timezoneSettingsSummary($per_date_summary) +
    +      parent::settingsSummary();
    

    One line, and same concern as above

  13. +++ b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimeFormatterBase.php
    --- a/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimePlainFormatter.php
    +++ b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimePlainFormatter.php
    
    +++ b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimePlainFormatter.php
    @@ -15,7 +15,7 @@
    - *)
    + * )
    

    Out of scope

  14. +++ b/core/modules/datetime/src/Plugin/Field/FieldType/DateTimeItem.php
    @@ -64,18 +68,26 @@ public static function propertyDefinitions(FieldStorageDefinitionInterface $fiel
    -    return [
    +    $schema = [
    ...
    +    return $schema;
    

    Unneeded?

  15. +++ b/core/modules/datetime/src/Plugin/Field/FieldWidget/DateTimeDefaultWidget.php
    @@ -33,8 +34,8 @@ class DateTimeDefaultWidget extends DateTimeWidgetBase implements ContainerFacto
    -  public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, EntityStorageInterface $date_storage) {
    -    parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings);
    +  public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, ConfigFactoryInterface $config_factory, EntityStorageInterface $date_storage) {
    +    parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings, $config_factory);
    

    And again

  16. +++ b/core/modules/datetime/src/Plugin/Field/FieldWidget/DateTimeWidgetBase.php
    @@ -22,20 +106,20 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen
    +      // @todo Remove after #2799987, as then the element will handle this.
    

    Link to the full URL please

  17. +++ b/core/modules/datetime/src/Plugin/Field/FieldWidget/DateTimeWidgetBase.php
    @@ -66,13 +151,41 @@ public function massageFormValues(array $values, array $form, FormStateInterface
    +  protected function shouldStoreTimezone(DrupalDateTime $date, array $form, FormStateInterface $form_state) {
    

    None of these parameters are used

  18. +++ b/core/modules/datetime/src/Tests/DateTestBase.php
    @@ -63,7 +70,7 @@
    -    // UTC-11, no DST
    +    // UTC-11, no DST.
    
    @@ -71,7 +78,7 @@
    -    // UTC+12, no DST
    +    // UTC+12, no DST.
    

    Out of scope

WidgetsBurritos’s picture

FileSize
26.22 KB
110.55 KB

@tim.plunkett,

Thank you for your feedback. This patch is an initial stab at cleaning the items you addressed in #249. It is quite possible I missed an item or two though so I'll recheck in the morning with fresh eyes.

Also, I haven't had a chance to investigate the test failure yet, but based on our slack chat earlier my next steps will probably be to do some digging into system_time_zones() usage. I still need to figure out how to get my system failing.

**Edit**
Oh I didn't change this todo line to the URL format, because it appears #2799987: Datetime and Datelist element's timezone handling is fragile, buggy and wrongly documented has been fixed already, so we should revisit this functionality anyway.

+++ b/core/modules/datetime/src/Plugin/Field/FieldWidget/DateTimeWidgetBase.php
@@ -22,20 +116,20 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen
     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.
+      // @todo Remove after #2799987, as then the element will handle this.
       $date->setTimezone(new \DateTimeZone($element['value']['#date_timezone']));
       $element['value']['#default_value'] = $this->createDefaultValue($date, $element['value']['#date_timezone']);
WidgetsBurritos’s picture

FileSize
108.25 KB
25.77 KB

This is just another cleanup patch. It attempts to provide a bit more consistent "time zone" usage in comments/strings. I've opted to use "time zone" as it seems to the more universal option, if this stackexchange answer is to be believed. That said, there appears to be zero consistency within the rest of core of using "timezone" vs "time zone", but at least we can strive to be consistent here.

We did manage to chop of about 4KB off of this patch since #245.

** Edit **

Just saw this comment in #99 that is also advocating for the two word "time zone" here, so yeah, that's the way to go.

The user facing term and in comments needs to be "time zone", two words. That is the preferred language from the docs team.

WidgetsBurritos’s picture

FileSize
108.22 KB
1.04 KB

Regarding #249.8, I reverted that one change in a previous patch, but didn't realize that another part of the patch was getting rid of the getFormatSettings() method altogether, which means I broke the custom datetime formatter. Digging in a little further, I did identify the purpose of that code change. It was to address the issue brought up in #74.1:

The settings summary for the display formatter is not always accurate. If you select a timezone override, it will always display information relating to that override, even if you edit the settings and select a different display type.

So instead of removing the functionality altogether, what I've done, is restored the getFormatSettings() back to its previous glory, with one exception. Instead of:

if ($this->getSetting('timezone_override') != '') {
     $settings['timezone'] = $this->getSetting('timezone_override');
}

It is:

if (!$this->getSetting('timezone_per_date') && $this->getSetting('timezone_override') != '') {
     $settings['timezone'] = $this->getSetting('timezone_override');
}

This way, it's only overriding the timezone if it's not configurable per field instance. I think that is the desired behavior there, but maybe not. Let me know if anything thinks I misinterpreted that.

WidgetsBurritos’s picture

FileSize
5.04 KB
108.53 KB

This patch should hopefully resolve the layout builder mentioned in #245.2. That was a rather tricky issue to track down, but the gist of it is it was performing infinite recursion due to circular references in the form-building process.

WidgetsBurritos’s picture

FileSize
732 bytes
108.55 KB

This patch resolves a minor issue with the changes #253 that was causing a few test failures.

WidgetsBurritos’s picture

Issue summary: View changes
Status: Needs work » Needs review
Issue tags: +BC break, +Needs tests, +Needs framework manager review

So with only one failing class remaining, I think we're at a good spot to summarize where I think this issue is at.

  1. Per #242.2, I think the JsonApiRegressionTest failures indicate a BC break as the data structure is changing and I think we need framework manager review here. I've tagged this issue accordingly.
  2. This needs a lot more test coverage. Time zone handling is very tricky, and we need to make sure all the bases are covered. Several of these items are already called out in the IS, but I'm adding the need for migration-specific test coverage per #245.1. It was requested to block this issue in #247 and to create a separate issue for tackling the migration and testing, but I don't think we can decouple that effort from this issue, as this issue is responsible for creating the necessary columns in the database.
  3. Per feedback in #249 from @tim.plunkett on finding opportunities to reduce scope in this patch, one thing I was thinking that could potentially help, would be to only handle the datetime field type here, and move the datetime_range field type support into a separate issue. That could help reduce a lot of the noise in this patch, although could temporarily create a fractured experience between those two field types, depending on how long the second issue took to resolve. I'll hold off on creating any new issues until I get some feedback on whether or not that's a good strategy there. That would remove 12 or so files from this patch which may allow for an easier time reviewing and moving this issue forward (especially since we still need to add a lot of test coverage to this issue).
  4. +++ b/core/modules/datetime/src/Plugin/Field/FieldWidget/DateTimeWidgetBase.php
    @@ -22,20 +115,20 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen
    +      /** @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.
    +      // @todo Remove after #2799987, as then the element will handle this.
           $date->setTimezone(new \DateTimeZone($element['value']['#date_timezone']));
           $element['value']['#default_value'] = $this->createDefaultValue($date, $element['value']['#date_timezone']);
    

    This @todo needs to be addressed as #2799987 is fixed now.

  5. +++ b/core/modules/datetime/src/Plugin/Field/FieldWidget/DateTimeWidgetBase.php
    @@ -93,6 +207,7 @@ protected function createDefaultValue($date, $timezone) {
         if ($this->getFieldSetting('datetime_type') === DateTimeItem::DATETIME_TYPE_DATE) {
           $date->setDefaultDateTime();
         }
    +    // @todo Remove after #2799987, as then the element will handle this.
         $date->setTimezone(new \DateTimeZone($timezone));
         return $date;
       }
    

    same here

Despite the remaining test failure, and the action items above, I'm gonna go ahead and move this issue into Needs review as I think it's a good time to get additional feedback here.

jonathanshaw’s picture

I thi k splitting up is vital. See #107 for some ideas.

dww’s picture

Excited to see this continuing to move forward, thanks everyone!

I think #107 looks like a very sensible way to split this into a meta with reviewable sub-issues, and a healthy order in which to do each step.

#255.2 makes a lot of sense as part of #107.1.

I don't think separating datetime and datetime_range from #255.3 is the way forward. That world is already a painfully fractured mess, and adding timezones for one but not the other, even "only for a couple of weeks" would be a fairly large-scale disaster. :( I'd much rather split this up and get it in along the lines of #107. If we do that, the datetime vs. datetime_range aspect of each piece won't be so bad, especially between #107.1 (shared tests) and #107.3 (shared trait) for most of this.

We're about to surpass 300 comments and this will be more clumsy to use, so either we need to quickly and efficiently make this a meta, or open a new "plan" issue to be the meta and postpone this issue on that. Given how often folks like to edit meta issues (and that each edit generates a new comment), I propose we create a new meta plan with new sub-issues for each of the 4-5 steps, and postpone this issue on all that. Folks who currently want to help get it done can follow / help the new issue(s). When it's all done we can mark this issue fixed to notify everyone following here. As a datetime_extras co-maintainer (and therefore quasi-datetime-in-core-provisional-co-maintainer), that's what I'd recommend and prefer as a strategy to get this done. Thoughts?

Thanks again!
-Derek

WidgetsBurritos’s picture

Ahh yeah, I remember seeing #107 when I first read through the issue, but it fell of my radar as I was working through some of the other issues. That definitely seems like a way better strategy than what I suggested with splitting up the datetime and datetime_range, so yes, let's definitely go that route. And I definitely agree that this issue has grown into a hard-to-follow behemoth so a separate meta issue makes a ton of sense here.

I'm gonna be a bit tied up the next couple days getting ready for DrupalCon and travelling, but if anybody is gonna be in Seattle and wants to find some time at the con to flesh out some of these issues, or wants to just go ahead and start creating the issues themselves, that would be greatly appreciated. Otherwise, I probably can find some time to start working through some of that next week.

baisong’s picture

@jonathanshaw @dww @WidgetsBurritos I'm trying to phrase the proposed new issues titles & descriptions.

Does this look right? Once we make these new issues, do we mark this current issue closed?

Create TimezoneFieldTest class

Create `TimezoneFieldTest` class, to contain test methods that can be applied to any widget/formatter datetime/date_range in order to test timezone handling. This way if we add some new timezone logic, we only have to touch a couple of places in the test and it magically gets tested on all widgets/formatters.

This is a meta issue tracking the following four issues:

Refactor datetime formatters

Refactor to reduce code duplication and confusing code in the timezone handling for datetime formatters. We have a method on DateTimeFormatterBase called "setTimeZone", but timezone overrides are not handled in this method, they are instead handled in each of the formatters formatDate method. Those methods should not need to deal with timezones at all.

Move formatter timezone settings into a trait

Move the formatters timezone settings into a trait, with the ['timezone_default'] options: user, site and fixed; fixed allows you to choose (what we call for backward-compatibility reasons) the ['timezone_override'].

Apply timezone trait to datetime widgets

All the same options make sense for them. This addresses the problem when creating session entities, the times of the sessions as input by the editor are interpreted by Drupal in terms of the timezone of the editor who is creating or updating it, not the venue timezone. In this case their site builder needs to be able to control the timezone used by the widget to interpret the times the editors input.

Add per-instance datetime timezone storage

This solves the problem when creating meeting entities to describe meetings held at various locations across the world, the intended time zone needs to specified for each meeting by the editor, and that preference needs to be stored with the date/time.

A few more reasons for per-date settings.

a) We have to handle fields with existing values that will have a NULL stored time zone. And having this setting, which will then be ignored most of the time, is slightly misleading for site builders. It would make it more bluntly obvious how things worked if you have the additional checkbox for "If a preferred time zone is stored, use it instead of the default". This drives home the idea there are two different things at work here, a default and something that can override it per-date.

b) This would allow site builders to render a time in its canonical (venue) time zone and sometimes in the time zone of the user.

c) This would allow site builders to hide the time zone selector on a "basic" form but have it exposed on an "advanced" form for superusers.

jonathanshaw’s picture

Looks good @baisong!

Yes, I think it makes sense to create those 5 issues. We can then discuss the merits and scope of each more fully individually.

This is a meta issue tracking the following four issues:

I'm not sure what you mean by "this" here. Do you mean converting the present issue into a meta? I share @dww's concern that at 300 comments it's become unwieldy and a new plan issue is probably better.

@mpdonadio the maintainer said in #196:

Re the test split, I will be making a meta about this. The existing tests are way too wieldy right now. There are a few in-progress patches are doing this.

But I can't find it such a meta issue. I thought I did some factoring of existing tests in an earlier patch in this issue but can't see that code now when I look.

It's possible that this relates to the new tests we need, but maybe this feature will progress better if we simply create new tests rather than refactor the existing convoluted tangle.

Anonymous’s picture

Trying to catch up on this issue, as I'm building a media site that is referencing data from all over with varied time zones. Pretty necessary for what I'm trying to do to be able to specify at timezone alongside a date / time field.

Is the patch at #254 working? I see it has failing tests, but does it add the ability to specify a unique timezone per entry?

Is this something that will make it into 8.8.x?

ltrain’s picture

Re-rolled 2632040-254.patch to work with 8.7.x.

ltrain’s picture

Kasey_MK’s picture

#262 fails to apply to 8.7.7 (I'm using cweagans composer-patches). I don't get any errors applying to patch, but none of my core files are changed.

Kasey_MK’s picture

FWIW, this is what I'm doing in the meantime (Drupal 8.7.7). We use our site's time zone for all dates, and don't let users set their own time zones. We have events which we want to show in their own time zone; e.g., an event taking place in California should show its times in Pacific. (I spent some time trying to fix/figure out the latest patch on this branch but I could not get things working.)

Add a time zone field to the content type (https://www.drupal.org/project/tzfield) - field_time_zone.

In our case, field_event_date is our date field. Since we want to store accurate metadata, we'll convert the datetimes when they're entered, and then convert them back when they're displayed. That way what the user sees makes sense to them, and what's stored in the database is correct.

function MYMODULE_node_presave(EntityInterface $node) {
  // Drupal datetime fields don't come with a time zone (pending
  // https://www.drupal.org/project/drupal/issues/2632040); dates are
  // stored in UTC time but displayed in the site's time zone. Since the
  // user can select a time zone in another field, convert the date to
  // the site's time zone to store it accurately.
  //
  // Get the node's time zone.
  $eventTZ = new \DateTimeZone($node->get('field_time_zone')->getString());
  // Get the start and end values of the node's date field.
  $startDate = new \DateTime($node->get('field_event_date')->value);
  $endDate = new \DateTime($node->get('field_event_date')->end_value);
  // Convert the dates to the node's time zone.
  $startDate->setTimezone($eventTZ);
  $endDate->setTimezone($eventTZ);
  // Reset the field values with the new ISO 8601 date format (without the
  // offset, which Drupal doesn't store; it's storing in the site TZ).
  $node->set('field_event_date', [
    'value' => substr($startDate->format('c'), 0, -6),
    'end_value' => substr($endDate->format('c'), 0, -6),
  ]);
}
function MYMODULE_form_node_form_alter(&$form, FormStateInterface $form_state, $form_id) {
  // Drupal datetime fields don't come with a time zone (pending
  // https://www.drupal.org/project/drupal/issues/2632040); dates are
  // stored in UTC time but displayed in the site's time zone. Since the user
  // can select a time zone in another field, uua_functions_node_presave
  // converted the date to that zone to store it accurately. Since we want
  // the user who edits the node to see the time as if it were in the selected
  // time zone, we need to convert it back.
  if ($form['field_event_date']['widget'][0]['value']['#default_value'] !== NULL) {
    // Get the node's time zone.
    $eventTZ = new \DateTimeZone($form['field_time_zone']['widget'][0]['value']['#default_value']);
    // Get the event start and end times (which are already in the site's TZ).
    $startDate = new \DateTime($form['field_event_date']['widget'][0]['value']['#default_value']);
    $endDate = new \DateTime($form['field_event_date']['widget'][0]['end_value']['#default_value']);
    // Get the site's time zone offset.
    $siteOffset = $startDate->format('Z');
    // Convert the event dates to the node's time zone.
    $startDate->setTimezone($eventTZ);
    $endDate->setTimezone($eventTZ);
    // Get the event's time zone offset.
    $eventOffset = $startDate->format('Z');
    // Calculate the offset needed to put the stored date back into dates &
    // times that match the event time zone.
    $offset = (($siteOffset - $eventOffset) * 2);
    // Adjust the dates accordingly.
    $startDate->add(\DateInterval::createFromDateString($offset . ' seconds'));
    $endDate->add(\DateInterval::createFromDateString($offset . ' seconds'));
    // Change the form date values to reflect our re-calculated dates.
    $form['field_event_date']['widget'][0]['value']['#default_value']->setDate($startDate->format('Y'), $startDate->format('m'), $startDate->format('d'))->setTime($startDate->format('H'), $startDate->format('i'), $startDate->format('s'), $startDate->format('u'));
    $form['field_event_date']['widget'][0]['end_value']['#default_value']->setDate($endDate->format('Y'), $endDate->format('m'), $endDate->format('d'))->setTime($endDate->format('H'), $endDate->format('i'), $endDate->format('s'), $endDate->format('u'));
  }
}
MYMODULE_function preprocess_field__field_event_date(array &$variables) {
  // Drupal datetime fields don't come with a time zone (pending
  // https://www.drupal.org/project/drupal/issues/2632040); dates are
  // stored in UTC time but displayed in the site's time zone. Since the user
  // can select a time zone in another field, uua_functions_node_presave
  // converted the date to that zone to store it accurately. Since we want
  // the user who views the node to see the time as if it were in the selected
  // time zone, we need to convert it back.
  //
  // Get the node so we can load the time zone which is in another field.
  $node =& $variables['element']['#object'];
  // Get the site's default time zone.
  $siteTZ = new \DateTimeZone(\Drupal::config('system.date')->get('timezone')['default']);
  // Get the node's time zone (if none, default to the site's TZ).
  $eventTZ = new \DateTimeZone($node->get('field_time_zone')->getString() ? $node->get('field_time_zone')->getString() : \Drupal::config('system.date')->get('timezone')['default']);
  // Get the event start and end times.
  $startDate = new \DateTime($variables['items'][0]['content']['start_date']['#attributes']['datetime']);
  $endDate = new \DateTime($variables['items'][0]['content']['end_date']['#attributes']['datetime']);
  // Convert the start date from UTC to the site's time zone.
  $startDate->setTimezone($siteTZ);
  // Get the site's time zone offset.
  $siteOffset = $startDate->format('Z');
  // Convert the event dates to the node's time zone.
  $startDate->setTimezone($eventTZ);
  $endDate->setTimezone($eventTZ);
  // Get the event's time zone offset.
  $eventOffset = $startDate->format('Z');
  // Calculate the offset needed to put the stored date back into dates &
  // times that match the event time zone.
  $offset = (($siteOffset - $eventOffset) * 2);
  // Adjust the dates accordingly.
  $startDate->add(\DateInterval::createFromDateString($offset . ' seconds'));
  $endDate->add(\DateInterval::createFromDateString($offset . ' seconds'));
  // Make some date outputs that minimize duplicated info and set "All day"
  // values (this overrides any format chosen in the field display).
  $endDateText = '';
  if (($startDate->format('g:i a') == '12:00 am') && ($endDate->format('g:i a') == '11:59 pm')) {
    $startDateText = $startDate->format('l, F j, Y');
    if ($startDate->format('F j, Y') !== $endDate->format('F j, Y')) {
      $endDateText .= $endDate->format('l, F j, Y') . ' - All day';
    }
    else {
      $endDateText .= 'All day';
    }
  }
  else {
    $startDateText = $startDate->format('l, F j, Y, g:i a');
    if ($startDate->format('F j, Y') !== $endDate->format('F j, Y')) {
      $endDateText .= $endDate->format('l, F j, Y, ');
    }
    $endDateText .= $endDate->format('g:i a T');
  }
  // Output our new values.
  $variables['items'][0]['content']['start_date']['#text'] = $startDateText;
  $variables['items'][0]['content']['end_date']['#text'] = $endDateText;
}

Views outputs require conversions too.

function MYMODULE_preprocess_views_view_field(&$variables) {
  // Drupal datetime fields don't come with a time zone (pending
  // https://www.drupal.org/project/drupal/issues/2632040); dates are
  // stored in UTC time but displayed in the site's time zone. Since the user
  // can select a time zone in another field, uua_functions_node_presave
  // converted the date to that zone to store it accurately. Since we want
  // the user who views the date to see it as if it were in the selected
  // time zone, we need to convert it back.
  $view = $variables['view'];
  // field_event_date
  if (strpos($variables['field']->field, 'field_event_date') !== FALSE) {
    // Get the site's default time zone.
    $siteTZ = new \DateTimeZone(\Drupal::config('system.date')->get('timezone')['default']);
    // Get the node's time zone (if none, default to the site's TZ).
    $eventTZ = new \DateTimeZone($variables['row']->_entity->get('field_time_zone')->value ? $variables['row']->_entity->get('field_time_zone')->value : \Drupal::config('system.date')->get('timezone')['default']);
    // Get the event start and end times.
    $startDate = new \DateTime($variables['row']->_entity->get('field_event_date')->value);
    $endDate = new \DateTime($variables['row']->_entity->get('field_event_date')->end_value);
    // Convert the start date from UTC to the site's time zone.
    $startDate->setTimezone($siteTZ);
    // Get the site's time zone offset.
    $siteOffset = $startDate->format('Z');
    // Convert the event dates to the node's time zone.
    $startDate->setTimezone($eventTZ);
    $endDate->setTimezone($eventTZ);
    // Get the event's time zone offset.
    $eventOffset = $startDate->format('Z');
    // Calculate the offset needed to put the stored date back into dates &
    // times that match the event time zone (different on admin views)
    if (strpos($view->id(), 'admin') !== FALSE) {
      if ($siteTZ->getName() !== $eventTZ->getName()) {
        $offset = ($eventOffset * -2) - 43200;
      }
      else {
        $offset = $eventOffset;
      }
    }
    else {
      $offset = (($siteOffset - $eventOffset) * 2);
    }
    // Adjust the dates accordingly.
    $startDate->add(\DateInterval::createFromDateString($offset . ' seconds'));
    $endDate->add(\DateInterval::createFromDateString($offset . ' seconds'));
    if (strpos($view->id(), 'admin') !== FALSE) {
      // On admin views, make some date outputs that minimize duplicated info
      // and set "All day" values (this overrides any format chosen in the field display).
      $endDateText = '';
      if (($startDate->format('g:i a') == '12:00 am') && ($endDate->format('g:i a') == '11:59 pm')) {
        $startDateText = $startDate->format('n/j/Y');
        if ($startDate->format('n/j/Y') !== $endDate->format('n/j/Y')) {
          $endDateText .= $endDate->format('n/j/Y') . ' - All day';
        }
        else {
          $endDateText .= 'All day';
        }
      }
      else {
        $startDateText = $startDate->format('n/j/Y, g:i a');
        if ($startDate->format('n/j/Y') !== $endDate->format('n/j/Y')) {
          $endDateText .= $endDate->format('n/j/Y, ');
        }
        $endDateText .= $endDate->format('g:i a T');
      }
      // Change the field output.
      $variables['output'] = \Drupal\Core\Render\Markup::create('
        <time datetime="' . substr($startDate->format('c'), 0, -6) . '" class="datetime">' . $startDateText . '</time> -
        <time datetime="' . substr($endDate->format('c'), 0, -6) . '" class="datetime">' . $endDateText . '</time>');
    }
    else {
      // Change the field values.
      $variables['row']->_entity->set('field_event_date', [
        'value' => substr($startDate->format('c'), 0, -6),
        'end_value' => substr($endDate->format('c'), 0, -6),
      ]);
    }
  }
}
jhedstrom’s picture

The patch in #262 is malformed and literally just creates a file named 632040-254.patch. Reviews and testing should still happen against #254.

Version: 8.8.x-dev » 8.9.x-dev

Drupal 8.8.0-alpha1 will be released the week of October 14th, 2019, which means new developments and disruptive changes should now be targeted against the 8.9.x-dev branch. (Any changes to 8.9.x will also be committed to 9.0.x in preparation for Drupal 9’s release, but some changes like significant feature additions will be deferred to 9.1.x.). For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

jhedstrom’s picture

Status: Needs review » Needs work

Moving to NW to address the failing test and feedback since the patch in #254.

recrit’s picture

Rolling patch in #254 for the latest 8.7.x.
Use patch in #273.

Kasey_MK’s picture

Bless you, recrit. Thank you so much.

Martijn de Wit’s picture

Status: Needs work » Needs review
recrit’s picture

Rolling patch in #254 for the latest 8.7.x with a full index diff but without the new file "core/modules/datetime/tests/fixtures/update/datetime-date_8001-values.php".
If the new test file is not needed for your build, then use this patch 272.
Else, use the patch in #273 that needs to be manually applied with "git apply".

recrit’s picture

Rolling patch in #254 for the latest 8.7.x with a full index diff that includes the new binary files.

NOTE:
If the new test file is not needed for your build, then use path #272.
The patch #254 adds a new file for testing - "core/modules/datetime/tests/fixtures/update/datetime-date_8001-values.php".
When using composer + cweagans/composer-patches, this patch will fail when using a stable version of core since there will not be .git directory within the core directory. In this scenario, cweagans/composer-patches will use the local "patch" command instead of "git apply" which fails when using GNU Patch due to "Binary diffs are not supported yet". See https://savannah.gnu.org/forum/forum.php?forum_id=7361.

Test results:
- Patch is applied successfully.
- Fails for the same "Jsonapi.Drupal\Tests\jsonapi\Functional\JsonApiRegressionTest" as #254

Update needed:
Fix failing "Migrate.Drupal\Tests\datetime\Unit\Plugin\migrate\field\DateFieldTest"

Status: Needs review » Needs work

The last submitted patch, 273: 2632040-254--8.7.x--full-index-with-binaries.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

Kasey_MK’s picture

I think this is basically a copy of 2632040-254--8.7.x--full-index.patch in #272 but for 8.8.x.

awolfey’s picture

Here's an update that applies to 8.8.2. A minor edit to Kasey_MK's patch in #275.

japerry’s picture

While the patch itself is coming along well, the update script has a bit of work needed. Specifically:

  • Better checks to see if fields exist and have already been updated
  • Check only fields, basefielddefinitions will need another approach (change record?)
  • Revision checks for what the table 'could' be not that the table actually exists.

I've added the checks to the following patch and the update script works for me. Not setting to needs review because I think covering how we deal with basefields still needs some work.

Version: 8.9.x-dev » 9.1.x-dev

Drupal 8.9.0-beta1 was released on March 20, 2020. 8.9.x is the final, long-term support (LTS) minor release of Drupal 8, which means new developments and disruptive changes should now be targeted against the 9.1.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

iamdroid’s picture

Re-roll of #277 for 9.0.x

drewfranz’s picture

Re-roll of #279 to remove the duplicate datetime schema key.

steinmb’s picture

Status: Needs work » Needs review

Status: Needs review » Needs work

The last submitted patch, 280: 2632040-280-9.0.x.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

hussainweb’s picture

hussainweb’s picture

Fixing the errors found in #280.

The last submitted patch, 283: 2632040-283.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

Status: Needs review » Needs work

The last submitted patch, 284: 2632040-284.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

amjad1233’s picture

Is there anything we can do this ticket to move forward ?

jonathanshaw’s picture

Split it up per #259 / #260 and earlier comments.

Version: 9.1.x-dev » 9.2.x-dev

Drupal 9.1.0-alpha1 will be released the week of October 19, 2020, which means new developments and disruptive changes should now be targeted for the 9.2.x-dev branch. For more information see the Drupal 9 minor version schedule and the Allowed changes during the Drupal 9 release cycle.

amjad1233’s picture

dww’s picture

Title: Add ability to select a timezone for datetime field » [PP-1] Add ability to select a timezone for datetime field
Issue summary: View changes
Status: Needs work » Postponed
Issue tags: -Needs tests, -Needs issue summary update, -Needs issue rescope

Thanks for opening those child issues! 🙏

I opened #3185747: [META] Add timezone support to core date fields as a working meta plan issue to actually coordinate this effort. Trying to do that in this issue is going to be a mess.

We should stop posting "composer-friendly" giant patches in here, too. It's important we have a place for folks to put the patches they're using on real sites while the dust settles on doing this properly, but we don't want those patches either in this issue, nor in the new meta. So I also opened #3185750: Composer friendly patches adding timezones to core date fields for that.

Updating the summary here to clarify what's happening where. Removing tags for issue rescope and summary update.

Ideally, no one else should comment here until #3185747 is marked fixed and we can close this. 🤞

  • General commentary on the new plan should happen at #3185747
  • Comments about specific pieces of the plan should go in the appropriate child issue.
  • Comments about the monster patches, re-rolls, conflicts, etc, should happen in #3185750

Thanks for all the effort here so far, and for your cooperation in keeping the scope manageable and the signal to noise ratio high. 😉

Cheers,
-Derek

Version: 9.2.x-dev » 9.3.x-dev

Drupal 9.2.0-alpha1 will be released the week of May 3, 2021, which means new developments and disruptive changes should now be targeted for the 9.3.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

recrit’s picture

Rolled the patch agains 9.2.x since the patch 284 does not apply cleanly to 9.2.x mainly due to the test "assert*" methods have changed.

Berdir’s picture

I'm not a date system maintainer, but I feel quite strongly that extending the existing field type is adding an immense amount of complexity to this issue and patch that is IMHO not worth it.

I understand that people have some use cases for this, but this also has severe downsides.. sorting on such a field for example is impossible if you care about correct sort order by the hour, it can be mixed up by up to +/- 12h.

Wouldn't it be a lot easier to add a new field type that extends from the existing one and only that has a support for this new feature? The need for an update path would simply vanish then. There is an edge case of people that already have date fields that want them to be timezone specific, but is supporting that really worth the effort? There could be a contrib module or even just a script that people can run that would basically do what the update function does.

A new field type is something that could be implemented in a contrib module, I'm not sure if the need for this is strong enough that it has to be in core. The core date system is also barely maintained and and many issues have been stuck for years, I honestly think the chances of a patch with this level of complexity getting committed in the near future as very narrow. There might be small parts that are much easier to do in core, like the form element stuff, that could be split off and likely has a much bigger chance of getting in.

I also equally strongly advice against using this patch on production sites. Relying on patches with update functions is a time bomb. If another patch with a 8001 update function is committed then you have have a huge mess. Your installation thinks that 8001 will not need to run anymore, will skip that other update, this will be rerolled with 8002 and then it will instead try to run this update _again_.

dpi’s picture

As maintainer of Recurring Date Field I want to throw in some brief (late night) high level thoughts.

  • I think Drupal deserves a new Datetime-Local field, something that represents a date and time associated with a location. Which is basically this issue: a date and time and time zone. A date-only storage may not be needed.
  • The datetime storage should use a database native representation, not a string or int, which likely means the raw value may differ depending on database engine.

With both these points, we'd need to start over, no migration, no extending existing code, brand new views integration. Widgets and elements could possibly be adapted for use with both.

I think its a bit late to tack on time zone functionality to the existing datetime field.

Check out the Recurring Date Field field type, which is effectively the existing Datetime field + time zone + RRULE data. It does its best to extend as much from core as possible, making the switch from core's field easy. But I dont think the foundation is rock solid, and would certainly reconsider if starting the project over.

Its interesting that neither Smart Date nor Recurring Date Field has been brought up in this issue yet, both good examples of well utilized contrib implementations featuring time zone features. As mentioned above, Recurring Date Field suffers from legacy storage, and non-native database type. Smart Date is tied to using Timestamp/epoch, which has its own problems such as limited range (<1970) and database-level date math could be easier. Certainly some code could be borrowed and learnings informing this implementation could have been made.

recrit’s picture

The attached 2632040-D9.2.x-296--full.patch fixes a bug in the "core/modules/datetime_range/src/Plugin/Field/FieldWidget/DateRangeDefaultWidget.php" which was missing "use Drupal\Core\Config\ConfigFactoryInterface;". See interdiff-2632040-296-full-293.txt.

@dpi,
Thanks for the contrib module suggestions. The Smart Date is perfect for this use case. Unfortunately, that module was created in April 2019 and this patch started way back in Dec 2015.

@Berdir,
I agree that this patch is not ideal, however it was a viable option for a site that needed to implement date entry with a timezone other than the site's time zone or user's timezone. Example: An editor creates an Event and sets the timezone of the date to that of the event's location, which is not the site's timezone or the editor's Drupal user timezone.
Sorting being off - This would be an issue for Database sorting. For a Solr search, then this is not an issue when indexing the field as a Date which indexes the date as GMT.

Suggestions

For new sites

Use Smart Date.

For existing sites that are using this patch

The datetime and datetime_range update has ran already to add the timezone field to the database table.
To support any future updates provide by core while continuing to use this patch:

  • Use the patch 2632040-D9.2.x-296--no-updates.patch - this is the same as the full patch but without the updates for datetime and datetime_range
  • Implement a site update similar to the following:
    function MY_MODULE_update_N() {
      drupal_set_installed_schema_version('datetime', 8000); 
      drupal_set_installed_schema_version('datetime_range', 8000);
    }
    
dpi’s picture

@recrit again and just to be clear, both projects solve the need for time zone. Not only Smart Date.

Unfortunately, that module was created in April 2019 and this patch started way back in Dec 2015.

The point I'm making is that at any point we can stop, analyse, and evaluate, the solutions out there. A patch can be steered in a new direction or start from scratch. I'd encourage anyone keen on continuing this patch to look at the mentioned projects and write up something comparing the patch, outlining differences and pros/cons.

recrit’s picture

@dpi I steered away from Recurring Dates Field since its main goal is recurring dates with the majority of its code centered around handling the recurring dates. For this patch, we're only trying to add a timezone. A custom field widget could be created to exclude the recurring rule functionality, but it would have to be tested that the module work work without a recurring rule.

There is a module that only adds the timezone based on this patch - Datetime Range Timezone. However, the last commit was 3 years ago so it does not have any of the updates to the patch since then.

I agree that this should move to a contrib module since it's unlikely that it would make it into core. Then we would only need some upgrade code to convert over to the new field type.

Version: 9.3.x-dev » 9.4.x-dev

Drupal 9.3.0-rc1 was released on November 26, 2021, which means new developments and disruptive changes should now be targeted for the 9.4.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Rajab Natshah made their first commit to this issue’s fork.

Version: 9.4.x-dev » 9.5.x-dev

Drupal 9.4.0-alpha1 was released on May 6, 2022, which means new developments and disruptive changes should now be targeted for the 9.5.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.5.x-dev » 10.1.x-dev

Drupal 9.5.0-beta2 and Drupal 10.0.0-beta2 were released on September 29, 2022, which means new developments and disruptive changes should now be targeted for the 10.1.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

c_archer’s picture

Patch from #296 does not apply agains Drupal 9.5.7

Version: 10.1.x-dev » 11.x-dev

Drupal core is moving towards using a “main” branch. As an interim step, a new 11.x branch has been opened, as Drupal.org infrastructure cannot currently fully support a branch named main. New developments and disruptive changes should now be targeted for the 11.x branch, which currently accepts only minor-version allowed changes. For more information, see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

joseph.olstad’s picture

FileSize
679 bytes

I suggest simplifying this to allow contrib to handle this.

In a Drupal classic approach we'd go ahead and add a new hook to alter the list of time zones.

This works quite well with the latest Drupal 10 and has been in use for a while without telling anyone.

Here in Canada for intranet sites we restrict the timezone selection down to 5 Canadian timezones.

Using the attached hook invokeAll patch to core and this example and an implements the said hook for example:

/**
 *  Implements hook_timezone_list_alter($zones).
 */
function mymodule_timezone_list_alter($zones) {
  // Don't modify in Views UI
  $route_match = \Drupal::routeMatch();
  if ($route_match->getRouteName() == 'views_ui.form_handler') {
    return $zones;
  }

  $keep = [
    'Rocky Mountain House' => t('Mountain time'),
    'Cape Breton' => t('Atlantic time') ,
    'St Johns' => t('Newfoundland time'),
    'Mississauga' => t('Eastern time'),
    'Salmon arm' => t('Pacific time'),
    'Brandon' => t('Central time'),
  ];
  foreach ($zones as $optgroup => &$group) {
    if ($optgroup != 'America') {
      unset($zones[$optgroup]);
    }
    else {
      foreach ($zones['America'] as $index => $option) {
        if (!in_array($option, array_keys($keep))) {
          unset($zones['America'][$index]);
        }
        else {
          $zones['America'][$index] .= ' (' . $keep[$option] . ')';
        }
      }
    }
  }
  return $zones;
}

I imagine someone is going to suggest extending the TimeZoneFormHelper class and overriding the getOptionsListByRegion method instead?

Is there some Drupal 10 documentation for this sort of thing with examples if for say we wanted to adopt this approach in a contrib module?

I think this should be a contrib solution.

Are there some sort of informal or formal "white paper" with a performance analysis / benefits/risk to a list of various approaches to solve this?

joseph.olstad’s picture

This has been open 9 years, we should probably just leave this to contrib. With that said, given my example provided in comment #305, what is the highest performance /most practical way to handle this case?

joseph.olstad’s picture