diff --git a/django/contrib/humanize/templatetags/humanize.py b/django/contrib/humanize/templatetags/humanize.py index 1398b43676..8f6c2602c9 100644 --- a/django/contrib/humanize/templatetags/humanize.py +++ b/django/contrib/humanize/templatetags/humanize.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals import re -from datetime import date, datetime, timedelta +from datetime import date, datetime from django import template from django.conf import settings @@ -143,7 +143,9 @@ def apnumber(value): return value return (_('one'), _('two'), _('three'), _('four'), _('five'), _('six'), _('seven'), _('eight'), _('nine'))[value-1] -@register.filter +# Perform the comparison in the default time zone when USE_TZ = True +# (unless a specific time zone has been applied with the |timezone filter). +@register.filter(expects_localtime=True) def naturalday(value, arg=None): """ For date values that are tomorrow, today or yesterday compared to @@ -169,6 +171,8 @@ def naturalday(value, arg=None): return _('yesterday') return defaultfilters.date(value, arg) +# This filter doesn't require expects_localtime=True because it deals properly +# with both naive and aware datetimes. Therefore avoid the cost of conversion. @register.filter def naturaltime(value): """ diff --git a/django/contrib/humanize/tests.py b/django/contrib/humanize/tests.py index 62df2be143..a0f13d3ee9 100644 --- a/django/contrib/humanize/tests.py +++ b/django/contrib/humanize/tests.py @@ -1,13 +1,31 @@ from __future__ import unicode_literals import datetime -import new +from django.contrib.humanize.templatetags import humanize from django.template import Template, Context, defaultfilters from django.test import TestCase -from django.utils import translation, tzinfo -from django.utils.translation import ugettext as _ +from django.test.utils import override_settings from django.utils.html import escape from django.utils.timezone import utc +from django.utils import translation +from django.utils.translation import ugettext as _ +from django.utils import tzinfo + + +# Mock out datetime in some tests so they don't fail occasionally when they +# run too slow. Use a fixed datetime for datetime.now(). DST change in +# America/Chicago (the default time zone) happened on March 11th in 2012. + +now = datetime.datetime(2012, 3, 9, 22, 30) + +class MockDateTime(datetime.datetime): + @classmethod + def now(self, tz=None): + if tz is None or tz.utcoffset(now) is None: + return now + else: + # equals now.replace(tzinfo=utc) + return now.replace(tzinfo=tz) + tz.utcoffset(now) class HumanizeTests(TestCase): @@ -109,28 +127,36 @@ class HumanizeTests(TestCase): self.humanize_tester(test_list, result_list, 'naturalday') def test_naturalday_tz(self): - from django.contrib.humanize.templatetags.humanize import naturalday - today = datetime.date.today() tz_one = tzinfo.FixedOffset(datetime.timedelta(hours=-12)) tz_two = tzinfo.FixedOffset(datetime.timedelta(hours=12)) # Can be today or yesterday date_one = datetime.datetime(today.year, today.month, today.day, tzinfo=tz_one) - naturalday_one = naturalday(date_one) + naturalday_one = humanize.naturalday(date_one) # Can be today or tomorrow date_two = datetime.datetime(today.year, today.month, today.day, tzinfo=tz_two) - naturalday_two = naturalday(date_two) + naturalday_two = humanize.naturalday(date_two) # As 24h of difference they will never be the same self.assertNotEqual(naturalday_one, naturalday_two) + def test_naturalday_uses_localtime(self): + # Regression for #18504 + # This is 2012-03-08HT19:30:00-06:00 in Ameria/Chicago + dt = datetime.datetime(2012, 3, 9, 1, 30, tzinfo=utc) + + orig_humanize_datetime, humanize.datetime = humanize.datetime, MockDateTime + try: + with override_settings(USE_TZ=True): + self.humanize_tester([dt], ['yesterday'], 'naturalday') + finally: + humanize.datetime = orig_humanize_datetime + def test_naturaltime(self): class naive(datetime.tzinfo): def utcoffset(self, dt): return None - # we're going to mock datetime.datetime, so use a fixed datetime - now = datetime.datetime(2011, 8, 15, 1, 23) test_list = [ now, now - datetime.timedelta(seconds=1), @@ -148,6 +174,7 @@ class HumanizeTests(TestCase): now + datetime.timedelta(hours=1, minutes=30, seconds=30), now + datetime.timedelta(hours=23, minutes=50, seconds=50), now + datetime.timedelta(days=1), + now + datetime.timedelta(days=2, hours=6), now + datetime.timedelta(days=500), now.replace(tzinfo=naive()), now.replace(tzinfo=utc), @@ -169,27 +196,22 @@ class HumanizeTests(TestCase): 'an hour from now', '23 hours from now', '1 day from now', + '2 days, 6 hours from now', '1 year, 4 months from now', 'now', 'now', ] + # Because of the DST change, 2 days and 6 hours after the chosen + # date in naive arithmetic is only 2 days and 5 hours after in + # aware arithmetic. + result_list_with_tz_support = result_list[:] + assert result_list_with_tz_support[-4] == '2 days, 6 hours from now' + result_list_with_tz_support[-4] == '2 days, 5 hours from now' - # mock out datetime so these tests don't fail occasionally when the - # test runs too slow - class MockDateTime(datetime.datetime): - @classmethod - def now(self, tz=None): - if tz is None or tz.utcoffset(now) is None: - return now - else: - # equals now.replace(tzinfo=utc) - return now.replace(tzinfo=tz) + tz.utcoffset(now) - - from django.contrib.humanize.templatetags import humanize - orig_humanize_datetime = humanize.datetime - humanize.datetime = MockDateTime - + orig_humanize_datetime, humanize.datetime = humanize.datetime, MockDateTime try: self.humanize_tester(test_list, result_list, 'naturaltime') + with override_settings(USE_TZ=True): + self.humanize_tester(test_list, result_list_with_tz_support, 'naturaltime') finally: humanize.datetime = orig_humanize_datetime