Fixed #20693 -- Add timezone support to built-in time filter.
Modified django.utils.dateformat module, moving __init__() method and timezone-related format methods from DateFormat class to TimeFormat base class. Modified timezone-related format methods to return an empty string when timezone is inappropriate for input value.
This commit is contained in:
parent
fa57266699
commit
dd3a883894
|
@ -38,8 +38,19 @@ class Formatter(object):
|
|||
return ''.join(pieces)
|
||||
|
||||
class TimeFormat(Formatter):
|
||||
def __init__(self, t):
|
||||
self.data = t
|
||||
|
||||
def __init__(self, obj):
|
||||
self.data = obj
|
||||
self.timezone = None
|
||||
|
||||
# We only support timezone when formatting datetime objects,
|
||||
# not date objects (timezone information not appropriate),
|
||||
# or time objects (against established django policy).
|
||||
if isinstance(obj, datetime.datetime):
|
||||
if is_naive(obj):
|
||||
self.timezone = LocalTimezone(obj)
|
||||
else:
|
||||
self.timezone = obj.tzinfo
|
||||
|
||||
def a(self):
|
||||
"'a.m.' or 'p.m.'"
|
||||
|
@ -57,6 +68,25 @@ class TimeFormat(Formatter):
|
|||
"Swatch Internet time"
|
||||
raise NotImplementedError
|
||||
|
||||
def e(self):
|
||||
"""
|
||||
Timezone name.
|
||||
|
||||
If timezone information is not available, this method returns
|
||||
an empty string.
|
||||
"""
|
||||
if not self.timezone:
|
||||
return ""
|
||||
|
||||
try:
|
||||
if hasattr(self.data, 'tzinfo') and self.data.tzinfo:
|
||||
# Have to use tzinfo.tzname and not datetime.tzname
|
||||
# because datatime.tzname does not expect Unicode
|
||||
return self.data.tzinfo.tzname(self.data) or ""
|
||||
except NotImplementedError:
|
||||
pass
|
||||
return ""
|
||||
|
||||
def f(self):
|
||||
"""
|
||||
Time, in 12-hour hours and minutes, with minutes left off if they're
|
||||
|
@ -92,6 +122,21 @@ class TimeFormat(Formatter):
|
|||
"Minutes; i.e. '00' to '59'"
|
||||
return '%02d' % self.data.minute
|
||||
|
||||
def O(self):
|
||||
"""
|
||||
Difference to Greenwich time in hours; e.g. '+0200', '-0430'.
|
||||
|
||||
If timezone information is not available, this method returns
|
||||
an empty string.
|
||||
"""
|
||||
if not self.timezone:
|
||||
return ""
|
||||
|
||||
seconds = self.Z()
|
||||
sign = '-' if seconds < 0 else '+'
|
||||
seconds = abs(seconds)
|
||||
return "%s%02d%02d" % (sign, seconds // 3600, (seconds // 60) % 60)
|
||||
|
||||
def P(self):
|
||||
"""
|
||||
Time, in 12-hour hours, minutes and 'a.m.'/'p.m.', with minutes left off
|
||||
|
@ -109,24 +154,48 @@ class TimeFormat(Formatter):
|
|||
"Seconds; i.e. '00' to '59'"
|
||||
return '%02d' % self.data.second
|
||||
|
||||
def T(self):
|
||||
"""
|
||||
Time zone of this machine; e.g. 'EST' or 'MDT'.
|
||||
|
||||
If timezone information is not available, this method returns
|
||||
an empty string.
|
||||
"""
|
||||
if not self.timezone:
|
||||
return ""
|
||||
|
||||
name = self.timezone.tzname(self.data) if self.timezone else None
|
||||
if name is None:
|
||||
name = self.format('O')
|
||||
return six.text_type(name)
|
||||
|
||||
def u(self):
|
||||
"Microseconds; i.e. '000000' to '999999'"
|
||||
return '%06d' %self.data.microsecond
|
||||
|
||||
def Z(self):
|
||||
"""
|
||||
Time zone offset in seconds (i.e. '-43200' to '43200'). The offset for
|
||||
timezones west of UTC is always negative, and for those east of UTC is
|
||||
always positive.
|
||||
|
||||
If timezone information is not available, this method returns
|
||||
an empty string.
|
||||
"""
|
||||
if not self.timezone:
|
||||
return ""
|
||||
|
||||
offset = self.timezone.utcoffset(self.data)
|
||||
# `offset` is a datetime.timedelta. For negative values (to the west of
|
||||
# UTC) only days can be negative (days=-1) and seconds are always
|
||||
# positive. e.g. UTC-1 -> timedelta(days=-1, seconds=82800, microseconds=0)
|
||||
# Positive offsets have days=0
|
||||
return offset.days * 86400 + offset.seconds
|
||||
|
||||
|
||||
class DateFormat(TimeFormat):
|
||||
year_days = [None, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334]
|
||||
|
||||
def __init__(self, dt):
|
||||
# Accepts either a datetime or date object.
|
||||
self.data = dt
|
||||
self.timezone = None
|
||||
if isinstance(dt, datetime.datetime):
|
||||
if is_naive(dt):
|
||||
self.timezone = LocalTimezone(dt)
|
||||
else:
|
||||
self.timezone = dt.tzinfo
|
||||
|
||||
def b(self):
|
||||
"Month, textual, 3 letters, lowercase; e.g. 'jan'"
|
||||
return MONTHS_3[self.data.month]
|
||||
|
@ -146,17 +215,6 @@ class DateFormat(TimeFormat):
|
|||
"Day of the week, textual, 3 letters; e.g. 'Fri'"
|
||||
return WEEKDAYS_ABBR[self.data.weekday()]
|
||||
|
||||
def e(self):
|
||||
"Timezone name if available"
|
||||
try:
|
||||
if hasattr(self.data, 'tzinfo') and self.data.tzinfo:
|
||||
# Have to use tzinfo.tzname and not datetime.tzname
|
||||
# because datatime.tzname does not expect Unicode
|
||||
return self.data.tzinfo.tzname(self.data) or ""
|
||||
except NotImplementedError:
|
||||
pass
|
||||
return ""
|
||||
|
||||
def E(self):
|
||||
"Alternative month names as required by some locales. Proprietary extension."
|
||||
return MONTHS_ALT[self.data.month]
|
||||
|
@ -204,13 +262,6 @@ class DateFormat(TimeFormat):
|
|||
"ISO 8601 year number matching the ISO week number (W)"
|
||||
return self.data.isocalendar()[0]
|
||||
|
||||
def O(self):
|
||||
"Difference to Greenwich time in hours; e.g. '+0200', '-0430'"
|
||||
seconds = self.Z()
|
||||
sign = '-' if seconds < 0 else '+'
|
||||
seconds = abs(seconds)
|
||||
return "%s%02d%02d" % (sign, seconds // 3600, (seconds // 60) % 60)
|
||||
|
||||
def r(self):
|
||||
"RFC 2822 formatted date; e.g. 'Thu, 21 Dec 2000 16:01:07 +0200'"
|
||||
return self.format('D, j M Y H:i:s O')
|
||||
|
@ -232,13 +283,6 @@ class DateFormat(TimeFormat):
|
|||
"Number of days in the given month; i.e. '28' to '31'"
|
||||
return '%02d' % calendar.monthrange(self.data.year, self.data.month)[1]
|
||||
|
||||
def T(self):
|
||||
"Time zone of this machine; e.g. 'EST' or 'MDT'"
|
||||
name = self.timezone.tzname(self.data) if self.timezone else None
|
||||
if name is None:
|
||||
name = self.format('O')
|
||||
return six.text_type(name)
|
||||
|
||||
def U(self):
|
||||
"Seconds since the Unix epoch (January 1 1970 00:00:00 GMT)"
|
||||
if isinstance(self.data, datetime.datetime) and is_aware(self.data):
|
||||
|
@ -291,26 +335,13 @@ class DateFormat(TimeFormat):
|
|||
doy += 1
|
||||
return doy
|
||||
|
||||
def Z(self):
|
||||
"""
|
||||
Time zone offset in seconds (i.e. '-43200' to '43200'). The offset for
|
||||
timezones west of UTC is always negative, and for those east of UTC is
|
||||
always positive.
|
||||
"""
|
||||
if not self.timezone:
|
||||
return 0
|
||||
offset = self.timezone.utcoffset(self.data)
|
||||
# `offset` is a datetime.timedelta. For negative values (to the west of
|
||||
# UTC) only days can be negative (days=-1) and seconds are always
|
||||
# positive. e.g. UTC-1 -> timedelta(days=-1, seconds=82800, microseconds=0)
|
||||
# Positive offsets have days=0
|
||||
return offset.days * 86400 + offset.seconds
|
||||
|
||||
def format(value, format_string):
|
||||
"Convenience function"
|
||||
df = DateFormat(value)
|
||||
return df.format(format_string)
|
||||
|
||||
|
||||
def time_format(value, format_string):
|
||||
"Convenience function"
|
||||
tf = TimeFormat(value)
|
||||
|
|
|
@ -360,6 +360,13 @@ def get_filter_tests():
|
|||
# Ticket 19370: Make sure |date doesn't blow up on a midnight time object
|
||||
'date08': (r'{{ t|date:"H:i" }}', {'t': time(0, 1)}, '00:01'),
|
||||
'date09': (r'{{ t|date:"H:i" }}', {'t': time(0, 0)}, '00:00'),
|
||||
# Ticket 20693: Add timezone support to built-in time template filter
|
||||
'time01': (r'{{ dt|time:"e:O:T:Z" }}', {'dt': now_tz_i}, '+0315:+0315:+0315:11700'),
|
||||
'time02': (r'{{ dt|time:"e:T" }}', {'dt': now}, ':' + now_tz.tzinfo.tzname(now_tz)),
|
||||
'time03': (r'{{ t|time:"P:e:O:T:Z" }}', {'t': time(4, 0, tzinfo=FixedOffset(30))}, '4 a.m.::::'),
|
||||
'time04': (r'{{ t|time:"P:e:O:T:Z" }}', {'t': time(4, 0)}, '4 a.m.::::'),
|
||||
'time05': (r'{{ d|time:"P:e:O:T:Z" }}', {'d': today}, ''),
|
||||
'time06': (r'{{ obj|time:"P:e:O:T:Z" }}', {'obj': 'non-datetime-value'}, ''),
|
||||
|
||||
# Tests for #11687 and #16676
|
||||
'add01': (r'{{ i|add:"5" }}', {'i': 2000}, '2005'),
|
||||
|
|
|
@ -127,10 +127,16 @@ class DateFormatTests(unittest.TestCase):
|
|||
wintertime = datetime(2005, 10, 30, 4, 00)
|
||||
timestamp = datetime(2008, 5, 19, 11, 45, 23, 123456)
|
||||
|
||||
# 3h30m to the west of UTC
|
||||
tz = FixedOffset(-3*60 - 30)
|
||||
aware_dt = datetime(2009, 5, 16, 5, 30, 30, tzinfo=tz)
|
||||
|
||||
if self.tz_tests:
|
||||
self.assertEqual(dateformat.format(my_birthday, 'O'), '+0100')
|
||||
self.assertEqual(dateformat.format(my_birthday, 'r'), 'Sun, 8 Jul 1979 22:00:00 +0100')
|
||||
self.assertEqual(dateformat.format(my_birthday, 'T'), 'CET')
|
||||
self.assertEqual(dateformat.format(my_birthday, 'e'), '')
|
||||
self.assertEqual(dateformat.format(aware_dt, 'e'), '-0330')
|
||||
self.assertEqual(dateformat.format(my_birthday, 'U'), '300315600')
|
||||
self.assertEqual(dateformat.format(timestamp, 'u'), '123456')
|
||||
self.assertEqual(dateformat.format(my_birthday, 'Z'), '3600')
|
||||
|
@ -140,7 +146,4 @@ class DateFormatTests(unittest.TestCase):
|
|||
self.assertEqual(dateformat.format(wintertime, 'O'), '+0100')
|
||||
|
||||
# Ticket #16924 -- We don't need timezone support to test this
|
||||
# 3h30m to the west of UTC
|
||||
tz = FixedOffset(-3*60 - 30)
|
||||
dt = datetime(2009, 5, 16, 5, 30, 30, tzinfo=tz)
|
||||
self.assertEqual(dateformat.format(dt, 'O'), '-0330')
|
||||
self.assertEqual(dateformat.format(aware_dt, 'O'), '-0330')
|
||||
|
|
Loading…
Reference in New Issue