From dd3a883894a219bc6c69e556c694734ab82b33e9 Mon Sep 17 00:00:00 2001 From: Warren Smith Date: Tue, 2 Jul 2013 17:19:56 -0500 Subject: [PATCH] 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. --- django/utils/dateformat.py | 133 +++++++++++++++++---------- tests/template_tests/filters.py | 7 ++ tests/utils_tests/test_dateformat.py | 11 ++- 3 files changed, 96 insertions(+), 55 deletions(-) diff --git a/django/utils/dateformat.py b/django/utils/dateformat.py index 6d0a7b63f7..fbf299631b 100644 --- a/django/utils/dateformat.py +++ b/django/utils/dateformat.py @@ -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) diff --git a/tests/template_tests/filters.py b/tests/template_tests/filters.py index 68ef15d827..142f56f073 100644 --- a/tests/template_tests/filters.py +++ b/tests/template_tests/filters.py @@ -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'), diff --git a/tests/utils_tests/test_dateformat.py b/tests/utils_tests/test_dateformat.py index 15262121a0..2f682b6ce0 100644 --- a/tests/utils_tests/test_dateformat.py +++ b/tests/utils_tests/test_dateformat.py @@ -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')