From 29e41f1a4add8390b7e57e83591345f15f60f913 Mon Sep 17 00:00:00 2001 From: Marcelo Galigniana Date: Sat, 26 Jul 2025 10:19:06 -0300 Subject: [PATCH 1/4] 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 b81f9ab46c..c471e1f844 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -343,7 +343,11 @@ def render_field(self, field, parent_style): 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 1b396575d4..2bc01c3699 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 TestSerializer(serializers.Serializer): 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/4] 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 2bc01c3699..e76908e7eb 100644 --- a/tests/test_renderers.py +++ b/tests/test_renderers.py @@ -494,7 +494,7 @@ def test_datetime_field_rendering(self): 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/4] 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 c471e1f844..4f3fce0ab3 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -347,7 +347,10 @@ def render_field(self, field, parent_style): # 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 e76908e7eb..d723f90c21 100644 --- a/tests/test_renderers.py +++ b/tests/test_renderers.py @@ -489,8 +489,9 @@ class TestSerializer(serializers.Serializer): 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 TestSerializer(serializers.Serializer): 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/4] 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 4f3fce0ab3..ef321b16e2 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -342,15 +342,12 @@ def render_field(self, field, parent_style): # 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 d723f90c21..522924cf61 100644 --- a/tests/test_renderers.py +++ b/tests/test_renderers.py @@ -489,7 +489,6 @@ class TestSerializer(serializers.Serializer): 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 TestSerializer(serializers.Serializer): 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 TestSerializer(serializers.Serializer): 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 )