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:
Warren Smith 2013-07-02 17:19:56 -05:00
parent fa57266699
commit dd3a883894
3 changed files with 96 additions and 55 deletions

View File

@ -38,8 +38,19 @@ class Formatter(object):
return ''.join(pieces) return ''.join(pieces)
class TimeFormat(Formatter): 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): def a(self):
"'a.m.' or 'p.m.'" "'a.m.' or 'p.m.'"
@ -57,6 +68,25 @@ class TimeFormat(Formatter):
"Swatch Internet time" "Swatch Internet time"
raise NotImplementedError 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): def f(self):
""" """
Time, in 12-hour hours and minutes, with minutes left off if they're 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'" "Minutes; i.e. '00' to '59'"
return '%02d' % self.data.minute 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): def P(self):
""" """
Time, in 12-hour hours, minutes and 'a.m.'/'p.m.', with minutes left off 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'" "Seconds; i.e. '00' to '59'"
return '%02d' % self.data.second 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): def u(self):
"Microseconds; i.e. '000000' to '999999'" "Microseconds; i.e. '000000' to '999999'"
return '%06d' %self.data.microsecond 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): class DateFormat(TimeFormat):
year_days = [None, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334] 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): def b(self):
"Month, textual, 3 letters, lowercase; e.g. 'jan'" "Month, textual, 3 letters, lowercase; e.g. 'jan'"
return MONTHS_3[self.data.month] return MONTHS_3[self.data.month]
@ -146,17 +215,6 @@ class DateFormat(TimeFormat):
"Day of the week, textual, 3 letters; e.g. 'Fri'" "Day of the week, textual, 3 letters; e.g. 'Fri'"
return WEEKDAYS_ABBR[self.data.weekday()] 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): def E(self):
"Alternative month names as required by some locales. Proprietary extension." "Alternative month names as required by some locales. Proprietary extension."
return MONTHS_ALT[self.data.month] return MONTHS_ALT[self.data.month]
@ -204,13 +262,6 @@ class DateFormat(TimeFormat):
"ISO 8601 year number matching the ISO week number (W)" "ISO 8601 year number matching the ISO week number (W)"
return self.data.isocalendar()[0] 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): def r(self):
"RFC 2822 formatted date; e.g. 'Thu, 21 Dec 2000 16:01:07 +0200'" "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') 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'" "Number of days in the given month; i.e. '28' to '31'"
return '%02d' % calendar.monthrange(self.data.year, self.data.month)[1] 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): def U(self):
"Seconds since the Unix epoch (January 1 1970 00:00:00 GMT)" "Seconds since the Unix epoch (January 1 1970 00:00:00 GMT)"
if isinstance(self.data, datetime.datetime) and is_aware(self.data): if isinstance(self.data, datetime.datetime) and is_aware(self.data):
@ -291,26 +335,13 @@ class DateFormat(TimeFormat):
doy += 1 doy += 1
return doy 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): def format(value, format_string):
"Convenience function" "Convenience function"
df = DateFormat(value) df = DateFormat(value)
return df.format(format_string) return df.format(format_string)
def time_format(value, format_string): def time_format(value, format_string):
"Convenience function" "Convenience function"
tf = TimeFormat(value) tf = TimeFormat(value)

View File

@ -360,6 +360,13 @@ def get_filter_tests():
# Ticket 19370: Make sure |date doesn't blow up on a midnight time object # 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'), 'date08': (r'{{ t|date:"H:i" }}', {'t': time(0, 1)}, '00:01'),
'date09': (r'{{ t|date:"H:i" }}', {'t': time(0, 0)}, '00:00'), '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 # Tests for #11687 and #16676
'add01': (r'{{ i|add:"5" }}', {'i': 2000}, '2005'), 'add01': (r'{{ i|add:"5" }}', {'i': 2000}, '2005'),

View File

@ -127,10 +127,16 @@ class DateFormatTests(unittest.TestCase):
wintertime = datetime(2005, 10, 30, 4, 00) wintertime = datetime(2005, 10, 30, 4, 00)
timestamp = datetime(2008, 5, 19, 11, 45, 23, 123456) 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: if self.tz_tests:
self.assertEqual(dateformat.format(my_birthday, 'O'), '+0100') 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, 'r'), 'Sun, 8 Jul 1979 22:00:00 +0100')
self.assertEqual(dateformat.format(my_birthday, 'T'), 'CET') 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(my_birthday, 'U'), '300315600')
self.assertEqual(dateformat.format(timestamp, 'u'), '123456') self.assertEqual(dateformat.format(timestamp, 'u'), '123456')
self.assertEqual(dateformat.format(my_birthday, 'Z'), '3600') self.assertEqual(dateformat.format(my_birthday, 'Z'), '3600')
@ -140,7 +146,4 @@ class DateFormatTests(unittest.TestCase):
self.assertEqual(dateformat.format(wintertime, 'O'), '+0100') self.assertEqual(dateformat.format(wintertime, 'O'), '+0100')
# Ticket #16924 -- We don't need timezone support to test this # Ticket #16924 -- We don't need timezone support to test this
# 3h30m to the west of UTC self.assertEqual(dateformat.format(aware_dt, 'O'), '-0330')
tz = FixedOffset(-3*60 - 30)
dt = datetime(2009, 5, 16, 5, 30, 30, tzinfo=tz)
self.assertEqual(dateformat.format(dt, 'O'), '-0330')