From 29e41f1a4add8390b7e57e83591345f15f60f913 Mon Sep 17 00:00:00 2001 From: Marcelo Galigniana Date: Sat, 26 Jul 2025 10:19:06 -0300 Subject: [PATCH 1/5] Fixed #5363 -- HTML5 datetime-local valid format HTMLFormRenderer --- rest_framework/renderers.py | 6 +++++- tests/test_renderers.py | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index b81f9ab46..c471e1f84 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -343,7 +343,11 @@ class HTMLFormRenderer(BaseRenderer): field = field.as_form_field() if style.get('input_type') == 'datetime-local' and isinstance(field.value, str): - field.value = field.value.rstrip('Z') + # 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. + datetime_parts = field.value.split(".") + field.value = f"{datetime_parts[0]}.{datetime_parts[1][:3]}" if 'template' in style: template_name = style['template'] diff --git a/tests/test_renderers.py b/tests/test_renderers.py index 1b396575d..2bc01c369 100644 --- a/tests/test_renderers.py +++ b/tests/test_renderers.py @@ -1,5 +1,6 @@ import re from collections.abc import MutableMapping +from datetime import datetime import pytest from django.core.cache import cache @@ -488,6 +489,23 @@ class TestHiddenFieldHTMLFormRenderer(TestCase): assert rendered == '' +class TestDateTimeFieldHTMLFormRender(TestCase): + def test_datetime_field_rendering(self): + class TestSerializer(serializers.Serializer): + appointment = serializers.DateTimeField() + + appointment = datetime(2024, 12, 24, 00, 55, 30, 345678) + serializer = TestSerializer(data={"appointment": appointment}) + serializer.is_valid() + renderer = HTMLFormRenderer() + field = serializer['appointment'] + rendered = renderer.render_field(field, {}) + self.assertInHTML( + '', + rendered + ) + + class TestHTMLFormRenderer(TestCase): def setUp(self): class TestSerializer(serializers.Serializer): From 85ca28225b92c554494a6e75d300a8b85b0ce9b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Asif=20Saif=20Uddin=20=7B=22Auvi=22=3A=22=E0=A6=85?= =?UTF-8?q?=E0=A6=AD=E0=A6=BF=22=7D?= Date: Sat, 16 Aug 2025 15:37:55 +0600 Subject: [PATCH 2/5] Update tests/test_renderers.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tests/test_renderers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_renderers.py b/tests/test_renderers.py index 2bc01c369..e76908e7e 100644 --- a/tests/test_renderers.py +++ b/tests/test_renderers.py @@ -494,7 +494,7 @@ class TestDateTimeFieldHTMLFormRender(TestCase): class TestSerializer(serializers.Serializer): appointment = serializers.DateTimeField() - appointment = datetime(2024, 12, 24, 00, 55, 30, 345678) + appointment = datetime(2024, 12, 24, 0, 55, 30, 345678) serializer = TestSerializer(data={"appointment": appointment}) serializer.is_valid() renderer = HTMLFormRenderer() From b39982be733f17830d14d1a093360ee8474f912e Mon Sep 17 00:00:00 2001 From: Marcelo Galigniana Date: Sun, 17 Aug 2025 17:54:02 -0300 Subject: [PATCH 3/5] Apply changes requested by peterthomassen --- rest_framework/renderers.py | 5 ++++- tests/test_renderers.py | 33 ++++++++++++++++++++++++++++++++- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index c471e1f84..4f3fce0ab 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -347,7 +347,10 @@ class HTMLFormRenderer(BaseRenderer): # followed by optional ":ss" or ":ss.SSS", so keep only the first three # digits of milliseconds to avoid browser console error. datetime_parts = field.value.split(".") - field.value = f"{datetime_parts[0]}.{datetime_parts[1][:3]}" + if len(datetime_parts) > 1: + field.value = f"{datetime_parts[0]}.{datetime_parts[1][:3]}" + else: + field.value = field.value.rstrip('Z') if 'template' in style: template_name = style['template'] diff --git a/tests/test_renderers.py b/tests/test_renderers.py index e76908e7e..d723f90c2 100644 --- a/tests/test_renderers.py +++ b/tests/test_renderers.py @@ -489,8 +489,9 @@ class TestHiddenFieldHTMLFormRenderer(TestCase): assert rendered == '' +@override_settings(TIME_ZONE='UTC', USE_TZ=True) class TestDateTimeFieldHTMLFormRender(TestCase): - def test_datetime_field_rendering(self): + def test_datetime_field_rendering_milliseconds(self): class TestSerializer(serializers.Serializer): appointment = serializers.DateTimeField() @@ -505,6 +506,36 @@ class TestDateTimeFieldHTMLFormRender(TestCase): rendered ) + def test_datetime_field_rendering_no_milliseconds(self): + class TestSerializer(serializers.Serializer): + appointment = serializers.DateTimeField() + + appointment = datetime(2024, 12, 24, 0, 55, 30, 0) + serializer = TestSerializer(data={"appointment": appointment}) + serializer.is_valid() + renderer = HTMLFormRenderer() + field = serializer['appointment'] + rendered = renderer.render_field(field, {}) + self.assertInHTML( + '', + rendered + ) + + def test_datetime_field_rendering_no_seconds_and_milliseconds(self): + class TestSerializer(serializers.Serializer): + appointment = serializers.DateTimeField() + + appointment = datetime(2024, 12, 24, 0, 55, 0, 0) + serializer = TestSerializer(data={"appointment": appointment}) + serializer.is_valid() + renderer = HTMLFormRenderer() + field = serializer['appointment'] + rendered = renderer.render_field(field, {}) + self.assertInHTML( + '', + rendered + ) + class TestHTMLFormRenderer(TestCase): def setUp(self): From 0c2b20cebc07ebe8e1a828da89bbeec176c562a8 Mon Sep 17 00:00:00 2001 From: Marcelo Galigniana Date: Thu, 21 Aug 2025 01:00:25 -0300 Subject: [PATCH 4/5] Use datetime value to render datetime-local input_type --- rest_framework/renderers.py | 9 +++------ tests/test_renderers.py | 22 ++++++++++++++++++---- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 4f3fce0ab..ef321b16e 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -342,15 +342,12 @@ class HTMLFormRenderer(BaseRenderer): # Get a clone of the field with text-only value representation. field = field.as_form_field() - if style.get('input_type') == 'datetime-local' and isinstance(field.value, str): + if style.get('input_type') == 'datetime-local': # 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. - datetime_parts = field.value.split(".") - if len(datetime_parts) > 1: - field.value = f"{datetime_parts[0]}.{datetime_parts[1][:3]}" - else: - field.value = field.value.rstrip('Z') + datetime_value = field._field.parent.validated_data.get(field.field_name) + field.value = datetime_value.replace(tzinfo=None).isoformat(timespec="milliseconds").rstrip('Z') if 'template' in style: template_name = style['template'] diff --git a/tests/test_renderers.py b/tests/test_renderers.py index d723f90c2..522924cf6 100644 --- a/tests/test_renderers.py +++ b/tests/test_renderers.py @@ -489,7 +489,6 @@ class TestHiddenFieldHTMLFormRenderer(TestCase): assert rendered == '' -@override_settings(TIME_ZONE='UTC', USE_TZ=True) class TestDateTimeFieldHTMLFormRender(TestCase): def test_datetime_field_rendering_milliseconds(self): class TestSerializer(serializers.Serializer): @@ -517,11 +516,11 @@ class TestDateTimeFieldHTMLFormRender(TestCase): field = serializer['appointment'] rendered = renderer.render_field(field, {}) self.assertInHTML( - '', + '', rendered ) - def test_datetime_field_rendering_no_seconds_and_milliseconds(self): + def test_datetime_field_rendering_no_seconds_and_no_milliseconds(self): class TestSerializer(serializers.Serializer): appointment = serializers.DateTimeField() @@ -532,7 +531,22 @@ class TestDateTimeFieldHTMLFormRender(TestCase): field = serializer['appointment'] rendered = renderer.render_field(field, {}) self.assertInHTML( - '', + '', + rendered + ) + + def test_datetime_field_rendering_with_format(self): + class TestSerializer(serializers.Serializer): + appointment = serializers.DateTimeField(format='%a %d %b %Y, %I:%M%p') + + appointment = datetime(2024, 12, 24, 0, 55, 30, 345678) + serializer = TestSerializer(data={"appointment": appointment}) + serializer.is_valid() + renderer = HTMLFormRenderer() + field = serializer['appointment'] + rendered = renderer.render_field(field, {}) + self.assertInHTML( + '', rendered ) From 34db0cbcd72894b6a1412ba0039b0967be9782f5 Mon Sep 17 00:00:00 2001 From: Marcelo Galigniana Date: Sun, 7 Sep 2025 12:07:16 -0500 Subject: [PATCH 5/5] Add test for `HTMLFormRenderer` when is `UTC` --- tests/test_renderers.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/test_renderers.py b/tests/test_renderers.py index 522924cf6..1742f598b 100644 --- a/tests/test_renderers.py +++ b/tests/test_renderers.py @@ -490,6 +490,11 @@ class TestHiddenFieldHTMLFormRenderer(TestCase): class TestDateTimeFieldHTMLFormRender(TestCase): + """ + Default USE_TZ is True. + Default TIME_ZONE is 'America/Chicago'. + """ + def test_datetime_field_rendering_milliseconds(self): class TestSerializer(serializers.Serializer): appointment = serializers.DateTimeField() @@ -550,6 +555,22 @@ class TestDateTimeFieldHTMLFormRender(TestCase): rendered ) + @override_settings(TIME_ZONE='UTC', USE_TZ=True) + def test_datetime_field_utc(self): + class TestSerializer(serializers.Serializer): + appointment = serializers.DateTimeField() + + appointment = datetime(2024, 12, 24, 0, 55, 30, 345678) + serializer = TestSerializer(data={"appointment": appointment}) + serializer.is_valid() + renderer = HTMLFormRenderer() + field = serializer['appointment'] + rendered = renderer.render_field(field, {}) + self.assertInHTML( + '', + rendered + ) + class TestHTMLFormRenderer(TestCase): def setUp(self):