Problem/Motivation
In #2786599: No way to post a datetime value with timezone and #2768651: Let TimestampItem (de)normalize to/from RFC3339 timestamps, not UNIX timestamps, for better DX it was pointed out that it was not obvious if the client should provide datetime fields in UTC or a timezone.
If in rest post you provide 2016-11-01T18:46:41-0400 instead of 2016-11-01T18:46:41 you will get the response back
{
"message": "A fatal error occurred: Internal Server Error"
}This is not helpful at all. If gives you no idea the problem is the format with the datetime or if you could figure that out what the correct date format is.
If you checked the Drupal logs you would an error like
Message Drupal\Core\Database\DatabaseExceptionWrapper: SQLSTATE[22001]: String data, right truncated: 1406 Data too long for column 'field_date_value' at row 1: INSERT INTO {node__field_date} (entity_id, revision_id, bundle, delta, langcode, field_date_value)......
Proposed resolution
If there was a constraint on the datetime field type for the date format then it would be more obvious what was wrong.
Remaining tasks
Create constraint plugin and validator
User interface changes
None
API changes
None
Data model changes
None
| Comment | File | Size | Author |
|---|---|---|---|
| #61 | interdiff-52-61.txt | 5.64 KB | mpdonadio |
| #61 | 2824717-61.patch | 22.76 KB | mpdonadio |
| #52 | 2824717-52.patch | 18.29 KB | mpdonadio |
Comments
Comment #2
tedbowComment #3
tedbowComment #4
tedbowComment #6
mpdonadioThanks for this. This got lost in the shuffle.
Hmmm. Rather not use the date function. Can we do something like
instead? Probably also need to add logic to check for date+time vs date-only.
Nit, short array syntax.
Also need explicit test coverage in DateTimeItemTest, and do something similar for daterange?
Comment #7
mpdonadioGoing to work on this for a bit tonight.
Comment #8
mpdonadioTagging out for the night.
The changes to DateTimeItemTest are a bit radical, but needed and in-scope if we are adding this constraint. One, some of the changes are needed to ensure the constraint gets called. Two, both date+time and date-only need to be tested. C, if you look closely at a few of these changes, you will see that a lot of DateTimeItemTest was invalid and the problems only surfaced when the constraint was added.
DateTimeItemTest passes locally, but didn't look at other fails from #3. We'll see what happens with this.
Still need to add the daterange constraint.
As a side note, I am tempted to call this a bug since we aren't currently catching some invalid uses, and this can result in PHP fatals. However, this may be disruptive for 8.2.x (esp outside of core).
Comment #10
wim leersComment #11
mpdonadioNeed to use `setExpectedException()`, https://www.drupal.org/node/2808063#comment-11753147
Comment #12
wim leersShould be typehinted to the interface,
\Drupal\Core\TypedData\Type\DateTimeInterface.s/date+time/date/
(because date only?)
Where can we find documentation that explains _why_ these are unsupported?
Especially these, they seem very common?
"proper", which to me reads as "valid", but this is the data provider listing all invalid values. So… I'm confused.
Same questions here.
Comment #13
mpdonadioThanks Wim.
Re 12.3 and 12.4, I am not sure we really document this anywhere. When setting the value, you need to use the storage format (DateTimeItem doesn't implement its own setValue, and the computed value uses DateTimeComputed which does a `DrupalDateTime::createFromFormat($storage_format, $value, DATETIME_STORAGE_TIMEZONE);`. So, I added some common uses where people would use well-known formats to make sure they fail appropriately for the scope of this issue (just add a constraint to enforce how DateTimeItem works). We should have a followup to change this behavior to normalize values to what storage accepts.
Re 12.5 and 12.6, that is just me not figuring out a good way to comment that. Those cases are covering trying to store a date-only value in a date+time field, and vice versa. By "proper" I meant they are in the correct storage format, but for the wrong type. DateTimeItemTest in HEAD is invalid because of this.
Comment #14
mpdonadio#12.1
Don't get this? $value is a DateTimeItem, which extends FieldItemBase and doesn't implement that (the item's value does)?
Working on this over the weekend. I see what the additional fails are and where they are coming from, but I don't understand why they are happening.
Comment #15
mpdonadioWork in progress for a full test run. This it will be green.
I think my confusion is when the constraint gets used, and the fact that we have two widgets for the item, one uses a string and the other that uses the array of parts. I suspect using validation on the value DataDefinition would be better, but we need to get the format which is in the item. And then datetime and date both use datetime_iso8601 for the DataDefinition, but format doesn't match our storage (see #2716891: DateTimeIso8601::setDateTime() vs. DateTimeItem::schema(): (at least) one of them is broken).
Very befuddled right now.
Comment #16
wim leersJust discovered that Symfony 3 includes a
DateTimeNormalizer: http://api.symfony.com/3.1/Symfony/Component/Serializer/Normalizer/DateT....Still need to review #15.
Comment #17
mpdonadioUnassigning myself. Also removing Needs Tests, since it looks like they got added.
Comment #18
wim leers@mpdonadio: Does this mean you will not have time in the near future to push this further? Or does it mean you don't ever intend to continue this?
Comment #19
mpdonadioWim, just unassigned myself since I am not actively working on it right now.
I think #15 needs a review in general, and also some thoughts around the questions in that comment.
I plan on wrapping this up once I have some better direction, as it looks like this may be important for some some recent bugs that were uncovered.
Comment #20
mpdonadioThere is some test overlap changes with this and #2832264: DateTimeItemTest is not valid. Probably want that in first, then we can fold in the specific changes for this patch.
Comment #21
alexpottThis patch should also remove some of \Drupal\Core\Datetime\Element\Datetime::validateDatetime() since it should no longer be necessary to check for
$form_state->setError($element, t('The %field date is invalid. Please enter a date in the format %format.', array('%field' => $title, '%format' => static::formatExample($format))));This means we need to make this constraint work (or something like it) work for DateRangeItem as well.
Comment #23
mpdonadioNeeds reroll b/c #2832264: DateTimeItemTest is not valid. Working on that now, and will also start on some of the other work.
Comment #24
mpdonadioThink this is a good re-roll, and passes locally. Starting on some of the work.
Comment #25
mpdonadioThat was fun. The problem was DateTimeWidgetBase::massageFormValues() not actually doing anything to the item when the form element didn't convert the user input into a DrupalDateTime object. In the case of the DateTimeDatelistWidget, the $value was the array input form element.
Re #21, developers can use that form element on their own, so I am not sure how we can get rid of any of that validation?
If we also add to daterange, we probably want to add to timestamp/created/changed. In this issue, or two followups?
Comment #26
wim leersSo #25 addressed #21.
These messages are identical. Do we want that?
I don't see where we are testing that the appropriate constraint violation error message is being sent. Not in REST, not outside REST. Am I overlooking that?
I'd say follow-ups. Or do you have good reasons for those not to be follow-ups? For timestamp/created/changed, we already have #2768651: Let TimestampItem (de)normalize to/from RFC3339 timestamps, not UNIX timestamps, for better DX. So I'm not entirely sure what you mean?
Comment #27
wim leersFor the first point in #26.
Comment #28
mpdonadioThought I had posted this. Guess not.
Do we want to split out DateTimeItemTest to test the separate exception messages? I started down that road, but
results in
There was 1 failure: 1) Drupal\Tests\datetime\Kernel\DateTimeItemTest::testDatetimeValidationType Failed asserting that exception message 'Object(Drupal\Core\Entity\Plugin\DataType\EntityAdapter).field_datetime: field_datetime: this field cannot hold more than 1 values. (code 756b1212-697c-468d-a9ad-50dd783bb169) Object(Drupal\Core\Entity\Plugin\DataType\EntityAdapter).field_datetime.0: The datetime value '2014' is invalid for the format 'Y-m-d\TH:i:s' Object(Drupal\Core\Entity\Plugin\DataType\EntityAdapter).field_datetime.1: The datetime value '01' is invalid for the format 'Y-m-d\TH:i:s' Object(Drupal\Core\Entity\Plugin\DataType\EntityAdapter).field_datetime.1.value: This value should be of the correct primitive type. Object(Drupal\Core\Entity\Plugin\DataType\EntityAdapter).field_datetime.2: The datetime value '01' is invalid for the format 'Y-m-d\TH:i:s' Object(Drupal\Core\Entity\Plugin\DataType\EntityAdapter).field_datetime.2.value: This value should be of the correct primitive type. Object(Drupal\Core\Entity\Plugin\DataType\EntityAdapter).field_datetime.3: The datetime value '00' is invalid for the format 'Y-m-d\TH:i:s' Object(Drupal\Core\Entity\Plugin\DataType\EntityAdapter).field_datetime.3.value: This value should be of the correct primitive type. Object(Drupal\Core\Entity\Plugin\DataType\EntityAdapter).field_datetime.4: The datetime value '00' is invalid for the format 'Y-m-d\TH:i:s' Object(Drupal\Core\Entity\Plugin\DataType\EntityAdapter).field_datetime.4.value: This value should be of the correct primitive type. Object(Drupal\Core\Entity\Plugin\DataType\EntityAdapter).field_datetime.5: The datetime value '00' is invalid for the format 'Y-m-d\TH:i:s' Object(Drupal\Core\Entity\Plugin\DataType\EntityAdapter).field_datetime.5.value: This value should be of the correct primitive type. ' contains 'The datetime value should be a string.'.Not 100% sure how to handle this scenario in a kernel test; in the UI the validation is happening in a different place.
Comment #29
wim leerss/should/must/ ?
And yes, ideally we want explicit test coverage for both. However:
I don't think that's necessary. I think we can simply set the expected message by passing the second parameter to
$this->setExpectedException(). I tried that, and ended up with exactly the same output as you. This is happening because of the order that constraints are evaluated in. That will require #2820364-20: Entity + Field + Property validation constraints are processed in the incorrect order to be fixed, which will likely take months at best, years is not unlikely…This solves the problem and proves via tests with not 100% accuracy but definitely 90% accuracy that this is working as expected. Let's go with this for now.
Thanks!
Comment #30
wim leersWell actually, we still are lacking functional tests that verify that when you interact with DateTimeItem fields via REST you get sensible error messages.
See
\Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBaseand specifically\Drupal\Tests\rest\Functional\EntityResource\EntityTest\EntityTestResourceTestBase. Thedatetimemodule could subclass that test class, add adatetimefield. It'd then be automatically testing GET/PATCH/POST/DELETE requests. You'd just have to override existing methods.Note that you can override
assertNormalizationEdgeCases()to test various kinds of wrong data to be sent, which will allow you to test the error response!Comment #31
mpdonadioThis is going to fail.
Wuh? I don't understand this? OK, It's a few minutes later and I already rolled and uploaded this (and I am tired). I think I need to do the FieldStorageConfig / FieldConfig in the setUp method...
-{"message":"The datetime value \u00272017-03-01\u0027 is invalid for the format \u0027Y-m-d\\TH:i:s\u0027"} +{"message":"Unprocessable Entity: validation failed.\nfield_datetime: field_datetime: this field cannot hold more than 1 values.\nfield_datetime.1: The datetime value \u00272017-03-01\u0027 is invalid for the format \u0027Y-m-d\\TH:i:s\u0027\n"}I don't get the first part about more than one value?
Comment #32
mpdonadioCouldn't sleep, clown would eat me.
Should be green now. @WimLeers, if you like this test I will clone it to the date-only flavor.
Comment #35
wim leersEh…
01:12:57.618 Fatal error: Cannot redeclare class Drupal\Tests\datetime\Functional\EntityResource\EntityTest\EntityTestDatetimeTest in /var/www/html/core/modules/datetime/tests/src/Functional/EntityResourceEntityTest/EntityTestDatetimeTest.php on line 18That's so weird, because there's nothing else that's declaring this! I can't reproduce the failing test. So, retesting.
Remove
$fieldStorageand$field, just call->save()immediately.I don't think this is necessary with the
createEntity()override?You could simplify this: you could do:
Same here.
YAY!
YAY!
That'd be splendid! We'll have super solid test coverage then :) We won't have to worry about this ever again!
Comment #37
mpdonadioThanks.
#35-2, I'll play with this some more tonight. The base classes have some assumptions about how the IDs start out and progress through the test, and the parent class creates the entity before the child class can add the new fields. I ran into a situation where either the get tests would fail b/c the new field wasn't populated properly (I think because it uses the entity from the setUp) or the post/patch/delete tests would fail because the IDs got out of sync.
Comment #38
wim leersAlright, things that can't be simplified can stay this way. But I think the
get*Normalized*Entitymethods can be simplified, and those are the ones with the biggest gains.(Also: if you see how you can remove that assumption from the base class without massive changes, removing those assumptions would be fine too.)
Comment #39
mpdonadio#35 should take care of except for (2). I can't see how to work around how EntityTestResourceTestBase::setUp() handles creating $this->entity and get the field in EntityTestDatetimeTest.
Also did some additional tidying up to make this less fragile.
This passes locally.
If this passes on the bot, do we want the date-only version?
Comment #41
mpdonadioBe gone unused `use`!
Still don't see the bot doesn't like this.
Comment #42
mpdonadioThink I figured this out.
Comment #44
wim leers#42: HAH! Good find :)
#39: Yay! Yes, I do think we want a date-only field to also have explicit test coverage. NW just for that. This is very close now! :)
Comment #45
mpdonadioOK, this should be all green.
Added a test class for the date-only flavor of datetime fields, and also fixed a bunch of things phpcs was complaining about (esp since these are mostly new files).
Comment #46
wim leersAwesome!
Wonderful work here, @mpdonadio :) Thanks!
Comment #47
alexpottShouldn't we do exactly what is done in \Drupal\datetime\DateTimeComputed::getValue() and use DATETIME_STORAGE_TIMEZONE too. Plus shouldn't we check the result of
hasErrors()method on the return?\Drupal\Component\Datetime\DateTimePlus::createFromFormat() can also throw exceptions - do they need to be handled here too?
Also in the issue summary it says:
The answer is that the time should be UTC - but we're still not really telling people that the service that posts the dates to Drupal that they are responsible for sending them in UTC. Not sure how to do that though.
Also there is a BC break here? Are there values that we used to accept but we're not? I don't think so but I just want to confirm that.
Comment #48
mpdonadio#47 raises some actionable issues. I think reworking this to use DateTimePlus::createFromFormat() is the better plan here. May end up with one more constraint message from the ambiguous date check.
Comment #49
mpdonadioI found an obscure bug in DateTimePlus, #2858295: DateTimePlus doesn't track errors properly.
Comment #50
wim leersComment #51
mpdonadioI'll finish this up tonight. Should be quick, and I have an idea to make the tests a little more robust / futureproof.
Comment #52
mpdonadioOk, I think #47 is take care of.
Changed tests are green, let's try a full run.
Comment #53
wim leerss/now/not/
#47:
We automatically convert to UTC, i.e. to
Confirming that this is not the case.
@mpdonadio Please confirm :)
Comment #54
mpdonadio[Saving my crosspost, will look at #53 later and post an update; letting #52 run to see if anything broke.]
This is just an aside for @WimLeers and the other REST maintainers to ponder. This is a little messy, and I essentially had to look at the error to get the full message instead of knowing it from the constraint (IOW, the constraint just added one of the errors in the full response, but I have to look for all of them). What could be a nice enhancement to the REST test framework would be a `assertResourceErrorResponseContains` that takes the $response, explodes it on newlines, and then does an in_array() for the $message that you pass in. Not in-scope for this, but could be a nice followup that could reduce the fragility of hardcoding the full error message here.
Comment #55
mpdonadioSo, do we want to add the UTC language to all of these? UTC is only needed for date+time; for date-only, the time portion is ignore so the date is always the local time (see #2739290: UTC+12 is broken on date only fields). Just want to be sure before I split these out into six messages instead of three.
Comment #56
mpdonadioRe, BC, we have this in the patch
My new local version uses DrupalDateTime instead of DateTimePlus so we full match other places, such as
DateTimeComputed::getValue() has
so that validates precisely to the two defined storage formats.
DateTimeWidgetBase::massageFormValues() has
So, both the computed value and the field widget use the defined storage formats.
#2832264: DateTimeItemTest is not valid cleaned up DateTimeItem test, and this patch adds coverage of invalid values in that. The DateTimeFieldTest tested invalid values.
I think we are fully backwards compatible with the advertised API for this field (the two storage constants in datetime.module). I think it may be possible that you could use the FieldAPI to set a value that doesn't follow the format and get it saved into the database, but with the way DateTimeComputed::getValue() is written, I don't see how anything could really work since it validates to the defined formats.
Comment #57
mpdonadioAnd
and
@alexpott has a point here, and I missed addressing that in #52 when I picked this back up after the side issue.
When you use the field via the UI, the date you enter gets converted to UTC (see DateTimeWidgetBase::massageFormValues). If you use the Field API or REST, UTC is implied; the date formats for storage do not have a timezone explicitly specified, and the items do not implement FieldItemInterface::preSave() to do any conversion, so the user has to make sure the value is already UTC. If we reject based on the format, we should also mention the UTC thing for date+time or local for date-only.
If we could go back in time, storage for date+time should have the offset included, always adjusted to UTC, and there should probably be a preSave() to enforce this, and possible some other item-specific things from FieldItemInterface. But, this is where we are :/ See also #2716891: DateTimeIso8601::setDateTime() vs. DateTimeItem::schema(): (at least) one of them is broken.
The constraint and the normalizer issue will help matters, though.
Comment #58
wim leers#54: We could do that. But I think this is fine. The fact that it's a bit ugly & painful is good: it shows that we should improve our error responses! In fact, this is really only a problem for entity validation error responses. Obviously, they're hard to parse by REST clients too. We have #2856110: [PP-1] Expose entity validation errors in a machine readable REST API: for that.
#55: so what I wrote in #53 is incorrect? About the automatic conversion to UTC? EDIT: #57 says that what I said was indeed wrong. #2768651: Let TimestampItem (de)normalize to/from RFC3339 timestamps, not UNIX timestamps, for better DX automatically converts to UTC. This should too. Let's also add explicit test coverage for that.
Comment #59
mpdonadioNot sure what we can do to test UTC conversion here, but I'll ponder it. Essentially, user has to give '2017-03-21T15:00:00' and make sure that is already UTC. I think the best we can do is add the message to the errors. And for '2017-03-21' it is supposed to be the local date for the user setting the value (timezones get ignore for date-only).
Comment #60
wim leersComment #61
mpdonadioOK, here is another explanation the time zone thing a different so we can figure out how to proceed.
In storage, date+time field is stored with the format 'Y-m-d\TH:i:s'. An example of this would be '2017-04-05T:02:17:00'. Note that this only has date and time. There is no offset or time zone in that string. This is what we mean that storage is implicitly UTC. Anything that updates storage is responsible for converting to UTC, and anything consuming the field is responsible for converting to the proper zone.
Another way to say this is that DateTimeItem doesn't do anything with timezones. The 'value' is the string; 'date' is a DrupalDateTime object with the TZ set to UTC (see DateTimeComputed::getValue).
On an entity form (simplifying this a bit)
- The time is entered in the user's timezone. The widget then converts to UTC (see DateTimeWidgetBase::massageFormValues).
- The entity storage system then takes this value and shoves it in the database,
In a field formatter
- The value is read out of the database, and set to UTC (lazy via DateTimeComputed::getValue())
- The formatter converts to the proper time zone (see DateTimeDefaultFormatter::formatDate())
When using the Field API directly, you need to use DATETIME_DATETIME_STORAGE_FORMAT, and do the time zone conversion yourself (added some coverage here). REST needs to send in UTC, and convert anything that it consumes to what it needs. Adding conversion to/from in this issue is out of scope (short term we can add normalizers for REST, long term I think we need to do #2716891: DateTimeIso8601::setDateTime() vs. DateTimeItem::schema(): (at least) one of them is broken and also convert in a new DateTimeItem::setValue()).
Based on this, how do we want to proceed? Add something to the error messages?
Comment #62
wim leersThis is clear. What I was saying in #53 is that I thought this was doing an automatic conversion from any timezone to UTC. I think I must have been context switching too much, because I now think I've been confusing this with #2768651: Let TimestampItem (de)normalize to/from RFC3339 timestamps, not UNIX timestamps, for better DX!
You see, that issue is adding timezone conversion. In fact, you saying all these things made me doubt that it was working there. But AFAICT, it is working there — see #2768651-107: Let TimestampItem (de)normalize to/from RFC3339 timestamps, not UNIX timestamps, for better DX, in which I added explicit test coverage for this.
So… what I think we want is:
Because then:
DateTimeItemNormalizer. It's not a hard requirement, but it'd help improve the DX for sure. I'm happy to create that follow-up issue for you. And I think it should be blocked on #2768651: Let TimestampItem (de)normalize to/from RFC3339 timestamps, not UNIX timestamps, for better DX, so that #2768651 does all the hard work, and then this follow-up issue would only have to follow the example set by #2768651.Thoughts?
Comment #63
mpdonadio#62, I think that is a good plan. We had talked about a normalizer for DateTimeItem and DateRangeItem in IRC, but never really documented it anywhere. It would also let us think about how a normalizer and a solution for #2716891: DateTimeIso8601::setDateTime() vs. DateTimeItem::schema(): (at least) one of them is broken could be related.
Comment #64
wim leersIn #30 I asked for functional REST tests. Those were added in #32 and later. I forgot to remove in #46, when I RTBC'd this.
Then @alexpott un-RTBC'd this in #47. I gave a partial answer to #47 in #53, and it was wrong. I confused two issues. See #62 for the clear-up.
Created the follow-up I mentioned — I also vaguely remember discussing this in IRC. Now we have an issue to continue in: #2867206: API-First DX of "date" and "date range" fields: add 'datetime_iso8601' @DataType (de)normalizer using RFC3339 timestamps with timezones :)
That means this issue is now fully RTBC-worthy again! Thanks for your patience :)
Comment #65
wim leersComment #67
mpdonadioMore JSTB fails:
https://www.drupal.org/pift-ci-job/650067
https://www.drupal.org/pift-ci-job/650091
Going to wait a day or two to set this back to RTBC to see if this is an reliable random failure again (actual quote heard at a sprint) that has an issue / fix.
Comment #68
mpdonadioLet's see if this stays RTBC; there are issues about the randoms this encountered logged on #2829040: [meta] Known intermittent, random, and environment-specific test failures.
Comment #70
wim leersComment #71
alexpottCommitted a132d45 and pushed to 8.4.x. Thanks!
Comment #73
wim leersWOOOT!
Comment #74
wim leersI didn't realize this wasn't committed to 8.3.x. I think that's a problem, because it means there's another entire release without validation for data stored for
DateTimeItemfields.I queued a test of #61 for 8.3.x.
Comment #75
wim leersThis has been waiting to be committed for >1 month now. It seems core committers are not looking at issues.
Comment #77
mpdonadioWith 8.4.x-alpha1 looming, I am moving this back to Fixed to help clear up the queue.
Comment #78
wim leers+1