Refs #32738 -- Added sanitize_strftime_format() to replace datetime_safe.

This commit is contained in:
Nick Pope 2021-05-10 14:39:50 +01:00 committed by Carlton Gibson
parent 44accb066a
commit 46346f8ea0
4 changed files with 89 additions and 7 deletions

View File

@ -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)

View File

@ -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

View File

@ -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'),

View File

@ -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.