-
-
Notifications
You must be signed in to change notification settings - Fork 7k
Fixed #5363 -- HTML5 datetime-local valid format HTMLFormRenderer #9365
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Hi @auvipy! Following your comment on #5363 (comment) I ask if you could do the review! Thank you! |
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please address the review comment
Thanks @auvipy! I'll do it! |
Just an update here. I'm still active! I'll do as soon as possible |
I finally did it, sorry for the long delay! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR fixes HTML5 datetime-local input formatting by truncating milliseconds to 3 digits to ensure browser compatibility. The HTML5 datetime-local input type specification only supports up to 3 digits of milliseconds precision, but DRF was potentially outputting more digits which caused browser validation errors.
- Modifies the datetime-local field value formatting in HTMLFormRenderer to limit milliseconds to 3 digits
- Adds a test case to verify the correct datetime-local input formatting
Reviewed Changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
File | Description |
---|---|
rest_framework/renderers.py | Updates datetime-local field formatting to truncate milliseconds to 3 digits |
tests/test_renderers.py | Adds test case to verify datetime-local input formatting with milliseconds |
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks. I have two more remarks:
- Timezones currently get dropped, but I assume that's fine because this is about
datetime-local
which "represent[s] a local date and time, with no time-zone offset information". - The transformation assumes that the string format is iso-8601. It doesn't work if I specify, for example,
appointment = serializers.DateTimeField(format='%a %d %b %Y, %I:%M%p')
. Shouldn't the internal value be used as an input?
Thank you Peter! Not sure if I'm following: do you mean use the datetime value instead of the input.value (string)? So let me know if you want to fix this in this pull request |
Yes, that's what I meant. Sorry for being unclear! |
I tried to use this branch on a Django project with |
Correct! I'll go for that approach and add one more test for that case Bruno! Thanks! |
6d5cff3
to
0c2b20c
Compare
assert rendered == '' | ||
|
||
|
||
class TestDateTimeFieldHTMLFormRender(TestCase): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be nice to add a couple of test cases with some non-naive datetimes, with a timezone specified. Could try with UTC and another timezone where the offset is non-zero
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you!
So I've added a docstring to let know that config variables are set in
USE_TZ=True
TIME_ZONE='America/Chicago'
And added a fourth test where @override_settings(TIME_ZONE='UTC', USE_TZ=True)
Is that correct?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess so. You can confirm by setting format=None
and printing field.value
.
Let's also add a test for a timezone with non-zero offset.
@browniebroke and @peterthomassen thank you so much for your quick reviews and guidance! 🙏
Sure, I'll add a new test with timezone!
Good question. I didn't fully understand what is parent so I'll read more code to be sure and add new cases if needed! |
617f876
to
34db0cb
Compare
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
bce2aa2
to
fee4af2
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
fee4af2
to
d894aab
Compare
d894aab
to
a15dfb4
Compare
Hello team! I went for a new approach: get datetime string and transform to datetime object to better manipulation. I've added 7 tests in total! I think I covered all possible cases! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the new approach, but I don't understand the typing considerations. You probably have thought about that -- any insights on my comments?
Also, the tests have quite some code duplication (I'm not sure if that is a problem).
rest_framework/renderers.py
Outdated
datetime_field_value = datetime.datetime.strptime(field.value, field._field.format) | ||
else: | ||
if api_settings.DATETIME_FORMAT == ISO_8601: | ||
datetime_field_value = datetime.datetime.fromisoformat(field.value.rstrip('Z')) | ||
else: | ||
datetime_field_value = datetime.datetime.strptime(field.value, api_settings.DATETIME_FORMAT) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These assume that field.value
is a string. Earlier, this was checked in line 345, but it no longer is. This is only safe if field.value
always is a string if either the format
argument is not set or is not None
. Is that guaranteed? (How?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I now understand that when format=None
, then field.value
is a datetime object. Is it guaranteed that otherwise it is a string?
The docs you linked in the other conversation say:
Setting to a format string indicates that
to_representation
return values should be coerced to string output.
... so maybe the coercion should happen first before calling rstrip
or passing to strptime
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think so!
Following https://github.com/encode/django-rest-framework/blob/main/rest_framework/fields.py#L1198
Mmm, unless ... DATETIME_FORMAT
can't be None
right? And if it can you should set a format
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
DATETIME_FORMAT
can't beNone
right?
It can be. So, in the last branch when format
is not set and DATETIME_FORMAT
is not equal to ISO_8601
, DATETIME_FORMAT
may be None
in which case field.value
will be a datetime object. I think this can be handled the same way as when format=None
.
This would allow simplifying the whole thing as follows:
try:
format = field._field.format
except AttributeError:
format = api_settings.DATETIME_FORMAT
if format is None:
# field.value is a datetime
# https://www.django-rest-framework.org/api-guide/fields/#datetimefield
datetime_field_value = field.value
else:
# field.value is expected to be a string
datetime_field_value = datetime.datetime.strptime(field.value, format)
Also, the comment in line 342 is misleading (not your fault): .as_form_field()
only transforms None
and False
into textual representations, but not datetime objects. Perhaps you can fix that comment to just say "Get a clone of the field that properly deals with None value" or something?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you peter, thanks for your patience! 🙏
- I've added a new test with
DATETIME_FORMAT
asNone
and noformat
defined. - I updated the
.as_form_field()
but trying to keep it as it is, just adding the example you mentioned. - I've used your code suggestion + adding the corresponding formatting in case of ISO_8601 string.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you peter, thanks for your patience! 🙏
Thank you for your patience! Always easy to make comments from the side lines, as I'm doing :p
- I've added a new test with
DATETIME_FORMAT
asNone
and noformat
defined.
Great.
- I've used your code suggestion + adding the corresponding formatting in case of ISO_8601 string.
Ah of course, had overlooked that.
Now I realize this can be further simplified by getting rid of datetime_field_value
:
try:
format = field._field.format
except AttributeError:
format = api_settings.DATETIME_FORMAT
if format is not None:
# field.value is expected to be a string, see DateTimeField docs
field.value = (
datetime.datetime.fromisoformat(field.value.rstrip('Z')) if format == ISO_8601
else datetime.datetime.strptime(field.value, format)
)
# The format of an input type="datetime-local" is "yyyy-MM-ddThh:mm"
# followed by optional ":ss" or ":ss.SSS", so keep only the first three
# digits of milliseconds to avoid browser console error.
field.value = field.value.replace(tzinfo=None).isoformat(timespec="milliseconds")
Yes!! Of course, I can improve that! |
Tests are prettier now! (I hope so) |
field = serializer['appointment'] | ||
rendered = renderer.render_field(field, {}) | ||
expected_html = ( | ||
f'<input name="appointment" class="form-control" ' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
f
prefix unnecessary
f'type="datetime-local" value="{expected}">' | ||
) | ||
|
||
self.assertInHTML(expected_html, rendered) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we really mean assertInHTML
, or would the somewhat stricter assertHTMLEqual
work as well?
def test_datetime_field_rendering_no_milliseconds(self): | ||
self._assert_datetime_rendering( | ||
datetime(2024, 12, 24, 0, 55, 30, 0), "2024-12-24T00:55:30.000" | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this one is redundant as test_datetime_field_rendering_no_seconds_and_no_milliseconds
covers it as well.
Description
Hi!
This PR delete everything after the first 3 digits of the miliseconds part of a
DateTimeField
input atHTMLFormRenderer
to avoid break in browsers:https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input/datetime-local#try_it
Please execute
./runtests.py TestDateTimeFieldHTMLFormRender
to run the new testsFix #5363