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.forms.utils import to_current_timezone
from django.templatetags.static import static 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.datastructures import OrderedSet
from django.utils.dates import MONTHS from django.utils.dates import MONTHS
from django.utils.formats import get_format from django.utils.formats import get_format
@ -1070,13 +1070,13 @@ class SelectDateWidget(Widget):
return None return None
if y is not None and m is not None and d is not 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 = get_format('DATE_INPUT_FORMATS')[0]
input_format = formats.sanitize_strftime_format(input_format)
try: try:
date_value = datetime.date(int(y), int(m), int(d)) date_value = datetime.date(int(y), int(m), int(d))
except ValueError: except ValueError:
# Return pseudo-ISO dates with zeros for any unselected values, # Return pseudo-ISO dates with zeros for any unselected values,
# e.g. '2017-0-23'. # e.g. '2017-0-23'.
return '%s-%s-%s' % (y or 0, m or 0, d or 0) 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 date_value.strftime(input_format)
return data.get(name) return data.get(name)

View File

@ -1,10 +1,12 @@
import datetime import datetime
import decimal import decimal
import functools
import re
import unicodedata import unicodedata
from importlib import import_module from importlib import import_module
from django.conf import settings 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.functional import lazy
from django.utils.translation import ( from django.utils.translation import (
check_for_language, get_language, to_locale, check_for_language, get_language, to_locale,
@ -221,12 +223,12 @@ def localize_input(value, default=None):
elif isinstance(value, (decimal.Decimal, float, int)): elif isinstance(value, (decimal.Decimal, float, int)):
return number_format(value) return number_format(value)
elif isinstance(value, datetime.datetime): elif isinstance(value, datetime.datetime):
value = datetime_safe.new_datetime(value)
format = default or get_format('DATETIME_INPUT_FORMATS')[0] format = default or get_format('DATETIME_INPUT_FORMATS')[0]
format = sanitize_strftime_format(format)
return value.strftime(format) return value.strftime(format)
elif isinstance(value, datetime.date): elif isinstance(value, datetime.date):
value = datetime_safe.new_date(value)
format = default or get_format('DATE_INPUT_FORMATS')[0] format = default or get_format('DATE_INPUT_FORMATS')[0]
format = sanitize_strftime_format(format)
return value.strftime(format) return value.strftime(format)
elif isinstance(value, datetime.time): elif isinstance(value, datetime.time):
format = default or get_format('TIME_INPUT_FORMATS')[0] format = default or get_format('TIME_INPUT_FORMATS')[0]
@ -234,6 +236,39 @@ def localize_input(value, default=None):
return value 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): def sanitize_separators(value):
""" """
Sanitize a value according to the current decimal and 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'), w.value_from_datadict({'date_year': '1899', 'date_month': '8', 'date_day': '13'}, {}, 'date'),
'13-08-1899', '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',)) w = SelectDateWidget(years=('0001',))
self.assertEqual( self.assertEqual(
w.value_from_datadict({'date_year': '0001', 'date_month': '8', 'date_day': '13'}, {}, 'date'), 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 import translation
from django.utils.formats import ( from django.utils.formats import (
date_format, get_format, get_format_modules, iter_format_modules, localize, 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.numberformat import format as nformat
from django.utils.safestring import SafeString, mark_safe from django.utils.safestring import SafeString, mark_safe
@ -1074,6 +1075,51 @@ class FormattingTests(SimpleTestCase):
with self.subTest(value=value): with self.subTest(value=value):
self.assertEqual(localize_input(value), expected) 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): def test_sanitize_separators(self):
""" """
Tests django.utils.formats.sanitize_separators. Tests django.utils.formats.sanitize_separators.