Commit dd3a8838 authored by Warren Smith's avatar Warren Smith
Browse files

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.
parent fa572666
Loading
Loading
Loading
Loading
+82 −51
Original line number Diff line number Diff line
@@ -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)
+7 −0
Original line number Diff line number Diff line
@@ -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'),
+7 −4
Original line number Diff line number Diff line
@@ -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')