Skip to content

Conversation

mgaligniana
Copy link
Contributor

@mgaligniana mgaligniana commented Apr 4, 2024

Description

Hi!

This PR delete everything after the first 3 digits of the miliseconds part of a DateTimeField input at HTMLFormRenderer to avoid break in browsers:

image

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 tests

Fix #5363

@mgaligniana
Copy link
Contributor Author

Hi @auvipy! Following your comment on #5363 (comment) I ask if you could do the review! Thank you!

@auvipy auvipy self-requested a review May 27, 2024 16:08
@auvipy auvipy requested a review from a team June 10, 2024 07:08
Copy link

stale bot commented Apr 27, 2025

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.

@stale stale bot added the stale label Apr 27, 2025
Copy link
Collaborator

@auvipy auvipy left a 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

@stale stale bot removed the stale label Apr 28, 2025
@mgaligniana
Copy link
Contributor Author

Thanks @auvipy! I'll do it!

@mgaligniana
Copy link
Contributor Author

Just an update here. I'm still active! I'll do as soon as possible

@mgaligniana
Copy link
Contributor Author

I finally did it, sorry for the long delay!

@mgaligniana mgaligniana requested a review from auvipy July 30, 2025 14:58
@auvipy auvipy requested a review from Copilot August 16, 2025 09:33
Copy link

@Copilot Copilot AI left a 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.

auvipy
auvipy previously approved these changes Aug 18, 2025
Copy link
Collaborator

@peterthomassen peterthomassen left a 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?

@mgaligniana
Copy link
Contributor Author

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

@peterthomassen
Copy link
Collaborator

do you mean use the datetime value instead of the input.value (string)?

Yes, that's what I meant. Sorry for being unclear!

@browniebroke
Copy link
Collaborator

I tried to use this branch on a Django project with TIME_ZONE="Europe/Paris" and I'm getting a similar error as the original report, bacause the timezone is not Z but +02:00 at the end. While it feels a bit out of scope of what this fix is trying to do, this last suggestion could address it as well?

@mgaligniana
Copy link
Contributor Author

Correct! I'll go for that approach and add one more test for that case Bruno! Thanks!

@auvipy auvipy dismissed their stale review August 20, 2025 14:06

stale

@auvipy auvipy self-requested a review August 20, 2025 14:07
@mgaligniana mgaligniana force-pushed the issue-5363 branch 2 times, most recently from 6d5cff3 to 0c2b20c Compare August 21, 2025 04:12
assert rendered == ''


class TestDateTimeFieldHTMLFormRender(TestCase):
Copy link
Collaborator

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

Copy link
Contributor Author

@mgaligniana mgaligniana Sep 7, 2025

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?

Copy link
Collaborator

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.

@mgaligniana
Copy link
Contributor Author

@browniebroke and @peterthomassen thank you so much for your quick reviews and guidance! 🙏

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

Sure, I'll add a new test with timezone!

I'm not sure. Is it guaranteed that there is always a parent?

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!

@mgaligniana
Copy link
Contributor Author

mgaligniana commented Sep 18, 2025

Hi team! Just to remember that I've added a new test and a question about .parent. Thank you!

@auvipy auvipy requested a review from Copilot September 21, 2025 05:10
Copy link

@Copilot Copilot AI left a 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.

@mgaligniana mgaligniana force-pushed the issue-5363 branch 3 times, most recently from bce2aa2 to fee4af2 Compare October 15, 2025 02:31
@mgaligniana mgaligniana requested a review from Copilot October 15, 2025 02:35
Copy link

@Copilot Copilot AI left a 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.

@mgaligniana
Copy link
Contributor Author

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!

Copy link
Collaborator

@peterthomassen peterthomassen left a 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).

Comment on lines 352 to 357
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)
Copy link
Collaborator

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?)

Copy link
Collaborator

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?

Copy link
Contributor Author

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

Copy link
Collaborator

@peterthomassen peterthomassen Oct 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DATETIME_FORMAT can't be None 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?

Copy link
Contributor Author

@mgaligniana mgaligniana Oct 17, 2025

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 as None and no format 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.

Copy link
Collaborator

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 as None and no format 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")

@mgaligniana
Copy link
Contributor Author

Also, the tests have quite some code duplication (I'm not sure if that is a problem).

Yes!! Of course, I can improve that!

@mgaligniana
Copy link
Contributor Author

mgaligniana commented Oct 16, 2025

Also, the tests have quite some code duplication (I'm not sure if that is a problem).

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" '
Copy link
Collaborator

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)
Copy link
Collaborator

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?

Comment on lines +521 to +524
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"
)
Copy link
Collaborator

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

HTML5 datetime-local valid format HTMLFormRenderer

5 participants