diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index b81f9ab46..1297e4a3b 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -22,7 +22,7 @@ from django.utils.html import mark_safe from django.utils.http import parse_header_parameters from django.utils.safestring import SafeString -from rest_framework import VERSION, exceptions, serializers, status +from rest_framework import ISO_8601, VERSION, exceptions, serializers, status from rest_framework.compat import ( INDENT_SEPARATORS, LONG_SEPARATORS, SHORT_SEPARATORS, coreapi, coreschema, pygments_css, yaml @@ -339,11 +339,27 @@ class HTMLFormRenderer(BaseRenderer): style['template_pack'] = parent_style.get('template_pack', self.template_pack) style['renderer'] = self - # Get a clone of the field with text-only value representation. + # Get a clone of the field with text-only value representation ('' if None or False). field = field.as_form_field() - if style.get('input_type') == 'datetime-local' and isinstance(field.value, str): - field.value = field.value.rstrip('Z') + if style.get('input_type') == 'datetime-local': + 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 + # https://www.django-rest-framework.org/api-guide/fields/#datetimefield + 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") if 'template' in style: template_name = style['template'] diff --git a/tests/test_renderers.py b/tests/test_renderers.py index 1b396575d..66c6fc194 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,83 @@ class TestHiddenFieldHTMLFormRenderer(TestCase): assert rendered == '' +class TestDateTimeFieldHTMLFormRender(TestCase): + """ + Default USE_TZ is True. + Default TIME_ZONE is 'America/Chicago'. + """ + + def _assert_datetime_rendering(self, appointment, expected, datetimefield_kwargs=None): + datetimefield_kwargs = datetimefield_kwargs or {} + + class TestSerializer(serializers.Serializer): + appointment = serializers.DateTimeField(**datetimefield_kwargs) + + serializer = TestSerializer(data={"appointment": appointment}) + serializer.is_valid() + renderer = HTMLFormRenderer() + field = serializer['appointment'] + rendered = renderer.render_field(field, {}) + expected_html = ( + '' + ) + + self.assertInHTML(expected_html, rendered) + + def test_datetime_field_rendering_milliseconds(self): + self._assert_datetime_rendering( + datetime(2024, 12, 24, 0, 55, 30, 345678), "2024-12-24T00:55:30.345" + ) + + def test_datetime_field_rendering_no_seconds_and_no_milliseconds(self): + self._assert_datetime_rendering( + datetime(2024, 12, 24, 0, 55, 0, 0), "2024-12-24T00:55:00.000" + ) + + def test_datetime_field_rendering_with_format_as_none(self): + self._assert_datetime_rendering( + datetime(2024, 12, 24, 0, 55, 30, 345678), + "2024-12-24T00:55:30.345", + {"format": None} + ) + + def test_datetime_field_rendering_with_format(self): + self._assert_datetime_rendering( + datetime(2024, 12, 24, 0, 55, 30, 345678), + "2024-12-24T00:55:00.000", + {"format": "%a %d %b %Y, %I:%M%p"} + ) + + @override_settings(TIME_ZONE='UTC') + def test_datetime_field_rendering_utc(self): + self._assert_datetime_rendering( + datetime(2024, 12, 24, 0, 55, 30, 345678), + "2024-12-24T00:55:30.345" + ) + + @override_settings(TIME_ZONE='Asia/Tokyo') # +09:00 + def test_datetime_field_rendering_non_zero_timezone(self): + self._assert_datetime_rendering( + datetime(2024, 12, 24, 0, 55, 30, 345678), + "2024-12-24T00:55:30.345" + ) + + @override_settings(REST_FRAMEWORK={'DATETIME_FORMAT': '%a %d %b %Y, %I:%M%p'}) + def test_datetime_field_rendering_with_custom_datetime_format(self): + self._assert_datetime_rendering( + datetime(2024, 12, 24, 0, 55, 30, 345678), + "2024-12-24T00:55:00.000" + ) + + @override_settings(REST_FRAMEWORK={'DATETIME_FORMAT': None}) + def test_datetime_field_rendering_datetime_format_is_none(self): + self._assert_datetime_rendering( + datetime(2024, 12, 24, 0, 55, 30, 345678), + "2024-12-24T00:55:30.345" + ) + + class TestHTMLFormRenderer(TestCase): def setUp(self): class TestSerializer(serializers.Serializer):