From 3111d7f60b67d5aefa4d891d1d9a86ba56d7607c Mon Sep 17 00:00:00 2001 From: Malcolm Tredinnick Date: Tue, 26 Aug 2008 08:08:55 +0000 Subject: [PATCH] Fixed #7201 -- Fixed the timeuntil filter to work correctly with timezone-aware times. Patch from Jeremy Carbaugh. This is backwards incompatible in the sense that previously, if you tried to compare timezone-aware and timezone-naive values, you got an incorrect result. Now you get an empty string. So your previously incorrect code returns a different incorrect result. git-svn-id: http://code.djangoproject.com/svn/django/trunk@8579 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- AUTHORS | 1 + django/template/defaultfilters.py | 18 +++++++++++------- django/utils/timesince.py | 22 +++++++++++----------- docs/ref/templates/builtins.txt | 4 ++++ tests/regressiontests/templates/filters.py | 14 +++++++++++++- tests/regressiontests/utils/timesince.py | 14 +++++++++++++- 6 files changed, 53 insertions(+), 20 deletions(-) diff --git a/AUTHORS b/AUTHORS index 9300d3ce11..cd3190b2d7 100644 --- a/AUTHORS +++ b/AUTHORS @@ -88,6 +88,7 @@ answer newbie questions, and generally made Django that much better: Juan Manuel Caicedo Trevor Caira Ricardo Javier Cárdenes Medina + Jeremy Carbaugh Graham Carlyle Antonio Cavedoni C8E diff --git a/django/template/defaultfilters.py b/django/template/defaultfilters.py index 56bf405018..bc78542184 100644 --- a/django/template/defaultfilters.py +++ b/django/template/defaultfilters.py @@ -646,20 +646,24 @@ def timesince(value, arg=None): from django.utils.timesince import timesince if not value: return u'' - if arg: - return timesince(value, arg) - return timesince(value) + try: + if arg: + return timesince(value, arg) + return timesince(value) + except (ValueError, TypeError): + return u'' timesince.is_safe = False def timeuntil(value, arg=None): """Formats a date as the time until that date (i.e. "4 days, 6 hours").""" - from django.utils.timesince import timesince + from django.utils.timesince import timeuntil from datetime import datetime if not value: return u'' - if arg: - return timesince(arg, value) - return timesince(datetime.now(), value) + try: + return timeuntil(value, arg) + except (ValueError, TypeError): + return u'' timeuntil.is_safe = False ################### diff --git a/django/utils/timesince.py b/django/utils/timesince.py index e1132c3ab3..1ce3550580 100644 --- a/django/utils/timesince.py +++ b/django/utils/timesince.py @@ -28,15 +28,12 @@ def timesince(d, now=None): # Convert datetime.date to datetime.datetime for comparison if d.__class__ is not datetime.datetime: d = datetime.datetime(d.year, d.month, d.day) - if now: - t = now.timetuple() - else: - t = time.localtime() - if d.tzinfo: - tz = LocalTimezone(d) - else: - tz = None - now = datetime.datetime(t[0], t[1], t[2], t[3], t[4], t[5], tzinfo=tz) + + if not now: + if d.tzinfo: + now = datetime.datetime.now(LocalTimezone(d)) + else: + now = datetime.datetime.now() # ignore microsecond part of 'd' since we removed it from 'now' delta = now - (d - datetime.timedelta(0, 0, d.microsecond)) @@ -62,6 +59,9 @@ def timeuntil(d, now=None): Like timesince, but returns a string measuring the time until the given time. """ - if now == None: - now = datetime.datetime.now() + if not now: + if d.tzinfo: + now = datetime.datetime.now(LocalTimezone(d)) + else: + now = datetime.datetime.now() return timesince(now, d) diff --git a/docs/ref/templates/builtins.txt b/docs/ref/templates/builtins.txt index 20c5ee513e..6fc0d5cf1e 100644 --- a/docs/ref/templates/builtins.txt +++ b/docs/ref/templates/builtins.txt @@ -1332,6 +1332,8 @@ For example, if ``blog_date`` is a date instance representing midnight on 1 June 2006, and ``comment_date`` is a date instance for 08:00 on 1 June 2006, then ``{{ blog_date|timesince:comment_date }}`` would return "8 hours". +Comparing offset-naive and offset-aware datetimes will return an empty string. + Minutes is the smallest unit used, and "0 minutes" will be returned for any date that is in the future relative to the comparison point. @@ -1349,6 +1351,8 @@ Takes an optional argument that is a variable containing the date to use as the comparison point (instead of *now*). If ``from_date`` contains 22 June 2006, then ``{{ conference_date|timeuntil:from_date }}`` will return "1 week". +Comparing offset-naive and offset-aware datetimes will return an empty string. + Minutes is the smallest unit used, and "0 minutes" will be returned for any date that is in the past relative to the comparison point. diff --git a/tests/regressiontests/templates/filters.py b/tests/regressiontests/templates/filters.py index bb92336dcc..bee568698c 100644 --- a/tests/regressiontests/templates/filters.py +++ b/tests/regressiontests/templates/filters.py @@ -9,7 +9,7 @@ consistent. from datetime import datetime, timedelta -from django.utils.tzinfo import LocalTimezone +from django.utils.tzinfo import LocalTimezone, FixedOffset from django.utils.safestring import mark_safe # These two classes are used to test auto-escaping of __unicode__ output. @@ -27,6 +27,7 @@ class SafeClass: def get_filter_tests(): now = datetime.now() now_tz = datetime.now(LocalTimezone(now)) + now_tz_i = datetime.now(FixedOffset((3 * 60) + 15)) # imaginary time zone return { # Default compare with datetime.now() 'filter-timesince01' : ('{{ a|timesince }}', {'a': datetime.now() + timedelta(minutes=-1, seconds = -10)}, '1 minute'), @@ -46,6 +47,14 @@ def get_filter_tests(): 'filter-timesince09': ('{{ later|timesince }}', { 'later': now + timedelta(days=7) }, '0 minutes'), 'filter-timesince10': ('{{ later|timesince:now }}', { 'now': now, 'later': now + timedelta(days=7) }, '0 minutes'), + # Ensures that differing timezones are calculated correctly + 'filter-timesince11' : ('{{ a|timesince }}', {'a': now}, '0 minutes'), + 'filter-timesince12' : ('{{ a|timesince }}', {'a': now_tz}, '0 minutes'), + 'filter-timesince13' : ('{{ a|timesince }}', {'a': now_tz_i}, '0 minutes'), + 'filter-timesince14' : ('{{ a|timesince:b }}', {'a': now_tz, 'b': now_tz_i}, '0 minutes'), + 'filter-timesince15' : ('{{ a|timesince:b }}', {'a': now, 'b': now_tz_i}, ''), + 'filter-timesince16' : ('{{ a|timesince:b }}', {'a': now_tz_i, 'b': now}, ''), + # Default compare with datetime.now() 'filter-timeuntil01' : ('{{ a|timeuntil }}', {'a':datetime.now() + timedelta(minutes=2, seconds = 10)}, '2 minutes'), 'filter-timeuntil02' : ('{{ a|timeuntil }}', {'a':(datetime.now() + timedelta(days=1, seconds = 10))}, '1 day'), @@ -61,6 +70,9 @@ def get_filter_tests(): 'filter-timeuntil08': ('{{ later|timeuntil }}', { 'later': now + timedelta(days=7, hours=1) }, '1 week'), 'filter-timeuntil09': ('{{ later|timeuntil:now }}', { 'now': now, 'later': now + timedelta(days=7) }, '1 week'), + # Ensures that differing timezones are calculated correctly + 'filter-timeuntil10' : ('{{ a|timeuntil }}', {'a': now_tz_i}, '0 minutes'), + 'filter-timeuntil11' : ('{{ a|timeuntil:b }}', {'a': now_tz_i, 'b': now_tz}, '0 minutes'), 'filter-addslash01': ("{% autoescape off %}{{ a|addslashes }} {{ b|addslashes }}{% endautoescape %}", {"a": "'", "b": mark_safe("'")}, ur"\' \'"), 'filter-addslash02': ("{{ a|addslashes }} {{ b|addslashes }}", {"a": "'", "b": mark_safe("'")}, ur"<a>\' \'"), diff --git a/tests/regressiontests/utils/timesince.py b/tests/regressiontests/utils/timesince.py index 4f304ec7f7..d4c671f2e2 100644 --- a/tests/regressiontests/utils/timesince.py +++ b/tests/regressiontests/utils/timesince.py @@ -1,6 +1,7 @@ """ >>> from datetime import datetime, timedelta ->>> from django.utils.timesince import timesince +>>> from django.utils.timesince import timesince, timeuntil +>>> from django.utils.tzinfo import LocalTimezone, FixedOffset >>> t = datetime(2007, 8, 14, 13, 46, 0) @@ -74,4 +75,15 @@ u'0 minutes' u'0 minutes' >>> timesince(t, t-4*oneday-5*oneminute) u'0 minutes' + +# When using two different timezones. +>>> now = datetime.now() +>>> now_tz = datetime.now(LocalTimezone(now)) +>>> now_tz_i = datetime.now(FixedOffset((3 * 60) + 15)) +>>> timesince(now) +u'0 minutes' +>>> timesince(now_tz) +u'0 minutes' +>>> timeuntil(now_tz, now_tz_i) +u'0 minutes' """