Fixed #5363 -- HTML5 datetime-local valid format HTMLFormRenderer (#9365)

* Fixed #5363 -- HTML5 datetime-local valid format HTMLFormRenderer

Co-authored-by: Peter Thomassen

* Add condition to make code cleanable by pyupgrade

---------

Co-authored-by: Bruno Alla <alla.brunoo@gmail.com>
This commit is contained in:
Marcelo Galigniana 2025-10-30 05:23:54 -05:00 committed by GitHub
parent ade172e1d5
commit 01659c075a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 107 additions and 4 deletions

View File

@ -10,6 +10,7 @@ REST framework also provides an HTML renderer that renders the browsable API.
import base64 import base64
import contextlib import contextlib
import datetime import datetime
import sys
from urllib import parse from urllib import parse
from django import forms from django import forms
@ -22,7 +23,7 @@ from django.utils.html import mark_safe
from django.utils.http import parse_header_parameters from django.utils.http import parse_header_parameters
from django.utils.safestring import SafeString 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 ( from rest_framework.compat import (
INDENT_SEPARATORS, LONG_SEPARATORS, SHORT_SEPARATORS, coreapi, coreschema, INDENT_SEPARATORS, LONG_SEPARATORS, SHORT_SEPARATORS, coreapi, coreschema,
pygments_css, yaml pygments_css, yaml
@ -339,11 +340,32 @@ class HTMLFormRenderer(BaseRenderer):
style['template_pack'] = parent_style.get('template_pack', self.template_pack) style['template_pack'] = parent_style.get('template_pack', self.template_pack)
style['renderer'] = self 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() field = field.as_form_field()
if style.get('input_type') == 'datetime-local' and isinstance(field.value, str): if style.get('input_type') == 'datetime-local':
field.value = field.value.rstrip('Z') 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 = field.value
if format_ == ISO_8601 and sys.version_info < (3, 11):
# We can drop this branch once we drop support for Python < 3.11
# https://docs.python.org/3/whatsnew/3.11.html#datetime
field_value = field_value.rstrip('Z')
field.value = (
datetime.datetime.fromisoformat(field_value) 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: if 'template' in style:
template_name = style['template'] template_name = style['template']

View File

@ -1,5 +1,7 @@
import re import re
from collections.abc import MutableMapping from collections.abc import MutableMapping
from datetime import datetime
from zoneinfo import ZoneInfo
import pytest import pytest
from django.core.cache import cache from django.core.cache import cache
@ -488,6 +490,85 @@ class TestHiddenFieldHTMLFormRenderer(TestCase):
assert rendered == '' 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 = (
'<input name="appointment" class="form-control" '
f'type="datetime-local" value="{expected}">'
)
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"}
)
# New project templates default to 'UTC'.
@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(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"
)
# Enforce it in True because in Django versions under 4.2 was False by default.
@override_settings(USE_TZ=True)
def test_datetime_field_rendering_timezone_aware_datetime(self):
self._assert_datetime_rendering(
datetime(2024, 12, 24, 0, 55, 30, 345678, tzinfo=ZoneInfo('Asia/Tokyo')), # +09:00
"2024-12-23T09:55:30.345" # Rendered in -06:00
)
class TestHTMLFormRenderer(TestCase): class TestHTMLFormRenderer(TestCase):
def setUp(self): def setUp(self):
class TestSerializer(serializers.Serializer): class TestSerializer(serializers.Serializer):