mirror of
				https://github.com/encode/django-rest-framework.git
				synced 2025-10-31 16:07:38 +03:00 
			
		
		
		
	* 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:
		
							parent
							
								
									ade172e1d5
								
							
						
					
					
						commit
						01659c075a
					
				|  | @ -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'] | ||||||
|  |  | ||||||
|  | @ -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): | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue
	
	Block a user