summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNick Pope <nick@nickpope.me.uk>2021-05-10 14:39:50 +0100
committerCarlton Gibson <carlton.gibson@noumenal.es>2021-05-12 14:42:17 +0200
commit46346f8ea08020d503b25472a26b949a5ce980a6 (patch)
tree2ea5991cc8dc896fd2858ad1df51ae34655053ff
parent44accb066a51d15f27e38f203c73830eddea16df (diff)
Refs #32738 -- Added sanitize_strftime_format() to replace datetime_safe.
-rw-r--r--django/forms/widgets.py4
-rw-r--r--django/utils/formats.py41
-rw-r--r--tests/forms_tests/widget_tests/test_selectdatewidget.py3
-rw-r--r--tests/i18n/tests.py48
4 files changed, 89 insertions, 7 deletions
diff --git a/django/forms/widgets.py b/django/forms/widgets.py
index 1e67857c31..0a24e924a5 100644
--- a/django/forms/widgets.py
+++ b/django/forms/widgets.py
@@ -10,7 +10,7 @@ from itertools import chain
from django.forms.utils import to_current_timezone
from django.templatetags.static import static
-from django.utils import datetime_safe, formats
+from django.utils import formats
from django.utils.datastructures import OrderedSet
from django.utils.dates import MONTHS
from django.utils.formats import get_format
@@ -1070,13 +1070,13 @@ class SelectDateWidget(Widget):
return None
if y is not None and m is not None and d is not None:
input_format = get_format('DATE_INPUT_FORMATS')[0]
+ input_format = formats.sanitize_strftime_format(input_format)
try:
date_value = datetime.date(int(y), int(m), int(d))
except ValueError:
# Return pseudo-ISO dates with zeros for any unselected values,
# e.g. '2017-0-23'.
return '%s-%s-%s' % (y or 0, m or 0, d or 0)
- date_value = datetime_safe.new_date(date_value)
return date_value.strftime(input_format)
return data.get(name)
diff --git a/django/utils/formats.py b/django/utils/formats.py
index 028e11415a..3bad64150a 100644
--- a/django/utils/formats.py
+++ b/django/utils/formats.py
@@ -1,10 +1,12 @@
import datetime
import decimal
+import functools
+import re
import unicodedata
from importlib import import_module
from django.conf import settings
-from django.utils import dateformat, datetime_safe, numberformat
+from django.utils import dateformat, numberformat
from django.utils.functional import lazy
from django.utils.translation import (
check_for_language, get_language, to_locale,
@@ -221,12 +223,12 @@ def localize_input(value, default=None):
elif isinstance(value, (decimal.Decimal, float, int)):
return number_format(value)
elif isinstance(value, datetime.datetime):
- value = datetime_safe.new_datetime(value)
format = default or get_format('DATETIME_INPUT_FORMATS')[0]
+ format = sanitize_strftime_format(format)
return value.strftime(format)
elif isinstance(value, datetime.date):
- value = datetime_safe.new_date(value)
format = default or get_format('DATE_INPUT_FORMATS')[0]
+ format = sanitize_strftime_format(format)
return value.strftime(format)
elif isinstance(value, datetime.time):
format = default or get_format('TIME_INPUT_FORMATS')[0]
@@ -234,6 +236,39 @@ def localize_input(value, default=None):
return value
+@functools.lru_cache()
+def sanitize_strftime_format(fmt):
+ """
+ Ensure that certain specifiers are correctly padded with leading zeros.
+
+ For years < 1000 specifiers %C, %F, %G, and %Y don't work as expected for
+ strftime provided by glibc on Linux as they don't pad the year or century
+ with leading zeros. Support for specifying the padding explicitly is
+ available, however, which can be used to fix this issue.
+
+ FreeBSD, macOS, and Windows do not support explicitly specifying the
+ padding, but return four digit years (with leading zeros) as expected.
+
+ This function checks whether the %Y produces a correctly padded string and,
+ if not, makes the following substitutions:
+
+ - %C → %02C
+ - %F → %010F
+ - %G → %04G
+ - %Y → %04Y
+
+ See https://bugs.python.org/issue13305 for more details.
+ """
+ if datetime.date(1, 1, 1).strftime('%Y') == '0001':
+ return fmt
+ mapping = {'C': 2, 'F': 10, 'G': 4, 'Y': 4}
+ return re.sub(
+ r'((?:^|[^%])(?:%%)*)%([CFGY])',
+ lambda m: r'%s%%0%s%s' % (m[1], mapping[m[2]], m[2]),
+ fmt,
+ )
+
+
def sanitize_separators(value):
"""
Sanitize a value according to the current decimal and
diff --git a/tests/forms_tests/widget_tests/test_selectdatewidget.py b/tests/forms_tests/widget_tests/test_selectdatewidget.py
index 00cfbae758..661268a08d 100644
--- a/tests/forms_tests/widget_tests/test_selectdatewidget.py
+++ b/tests/forms_tests/widget_tests/test_selectdatewidget.py
@@ -477,7 +477,8 @@ class SelectDateWidgetTest(WidgetTest):
w.value_from_datadict({'date_year': '1899', 'date_month': '8', 'date_day': '13'}, {}, 'date'),
'13-08-1899',
)
- # And years before 1000 (demonstrating the need for datetime_safe).
+ # And years before 1000 (demonstrating the need for
+ # sanitize_strftime_format).
w = SelectDateWidget(years=('0001',))
self.assertEqual(
w.value_from_datadict({'date_year': '0001', 'date_month': '8', 'date_day': '13'}, {}, 'date'),
diff --git a/tests/i18n/tests.py b/tests/i18n/tests.py
index 7d81f2a07d..7705df738c 100644
--- a/tests/i18n/tests.py
+++ b/tests/i18n/tests.py
@@ -24,7 +24,8 @@ from django.test import (
from django.utils import translation
from django.utils.formats import (
date_format, get_format, get_format_modules, iter_format_modules, localize,
- localize_input, reset_format_cache, sanitize_separators, time_format,
+ localize_input, reset_format_cache, sanitize_separators,
+ sanitize_strftime_format, time_format,
)
from django.utils.numberformat import format as nformat
from django.utils.safestring import SafeString, mark_safe
@@ -1074,6 +1075,51 @@ class FormattingTests(SimpleTestCase):
with self.subTest(value=value):
self.assertEqual(localize_input(value), expected)
+ def test_sanitize_strftime_format(self):
+ for year in (1, 99, 999, 1000):
+ dt = datetime.date(year, 1, 1)
+ for fmt, expected in [
+ ('%C', '%02d' % (year // 100)),
+ ('%F', '%04d-01-01' % year),
+ ('%G', '%04d' % year),
+ ('%Y', '%04d' % year),
+ ]:
+ with self.subTest(year=year, fmt=fmt):
+ fmt = sanitize_strftime_format(fmt)
+ self.assertEqual(dt.strftime(fmt), expected)
+
+ def test_sanitize_strftime_format_with_escaped_percent(self):
+ dt = datetime.date(1, 1, 1)
+ for fmt, expected in [
+ ('%%C', '%C'),
+ ('%%F', '%F'),
+ ('%%G', '%G'),
+ ('%%Y', '%Y'),
+ ('%%%%C', '%%C'),
+ ('%%%%F', '%%F'),
+ ('%%%%G', '%%G'),
+ ('%%%%Y', '%%Y'),
+ ]:
+ with self.subTest(fmt=fmt):
+ fmt = sanitize_strftime_format(fmt)
+ self.assertEqual(dt.strftime(fmt), expected)
+
+ for year in (1, 99, 999, 1000):
+ dt = datetime.date(year, 1, 1)
+ for fmt, expected in [
+ ('%%%C', '%%%02d' % (year // 100)),
+ ('%%%F', '%%%04d-01-01' % year),
+ ('%%%G', '%%%04d' % year),
+ ('%%%Y', '%%%04d' % year),
+ ('%%%%%C', '%%%%%02d' % (year // 100)),
+ ('%%%%%F', '%%%%%04d-01-01' % year),
+ ('%%%%%G', '%%%%%04d' % year),
+ ('%%%%%Y', '%%%%%04d' % year),
+ ]:
+ with self.subTest(year=year, fmt=fmt):
+ fmt = sanitize_strftime_format(fmt)
+ self.assertEqual(dt.strftime(fmt), expected)
+
def test_sanitize_separators(self):
"""
Tests django.utils.formats.sanitize_separators.