From 3e8b40f479a02e0f8c40ef3c7dae740082478b89 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Sun, 29 Apr 2012 15:37:23 +0200 Subject: [PATCH] Fixed #17992 -- Added a public API for localtime. Thanks Bradley Ayers for the report. --- django/contrib/admin/filters.py | 10 +++------ django/contrib/admin/util.py | 4 ++-- django/template/base.py | 6 ++--- django/template/debug.py | 4 ++-- django/templatetags/tz.py | 6 +---- django/utils/timezone.py | 28 ++++++++++++++++-------- docs/ref/utils.txt | 10 +++++++++ docs/releases/1.5.txt | 3 +++ tests/regressiontests/utils/timezone.py | 29 +++++++++++++++++++------ 9 files changed, 65 insertions(+), 35 deletions(-) diff --git a/django/contrib/admin/filters.py b/django/contrib/admin/filters.py index 76b8d30c0d..538bf54df9 100644 --- a/django/contrib/admin/filters.py +++ b/django/contrib/admin/filters.py @@ -290,19 +290,15 @@ class DateFieldListFilter(FieldListFilter): now = timezone.now() # When time zone support is enabled, convert "now" to the user's time # zone so Django's definition of "Today" matches what the user expects. - if now.tzinfo is not None: - current_tz = timezone.get_current_timezone() - now = now.astimezone(current_tz) - if hasattr(current_tz, 'normalize'): - # available for pytz time zones - now = current_tz.normalize(now) + if timezone.is_aware(now): + now = timezone.localtime(now) if isinstance(field, models.DateTimeField): today = now.replace(hour=0, minute=0, second=0, microsecond=0) else: # field is a models.DateField today = now.date() tomorrow = today + datetime.timedelta(days=1) - + self.lookup_kwarg_since = '%s__gte' % field_path self.lookup_kwarg_until = '%s__lt' % field_path self.links = ( diff --git a/django/contrib/admin/util.py b/django/contrib/admin/util.py index b8456d859f..0eb6791485 100644 --- a/django/contrib/admin/util.py +++ b/django/contrib/admin/util.py @@ -325,7 +325,7 @@ def display_for_field(value, field): elif value is None: return EMPTY_CHANGELIST_VALUE elif isinstance(field, models.DateTimeField): - return formats.localize(timezone.localtime(value)) + return formats.localize(timezone.template_localtime(value)) elif isinstance(field, (models.DateField, models.TimeField)): return formats.localize(value) elif isinstance(field, models.DecimalField): @@ -345,7 +345,7 @@ def display_for_value(value, boolean=False): elif value is None: return EMPTY_CHANGELIST_VALUE elif isinstance(value, datetime.datetime): - return formats.localize(timezone.localtime(value)) + return formats.localize(timezone.template_localtime(value)) elif isinstance(value, (datetime.date, datetime.time)): return formats.localize(value) elif isinstance(value, (decimal.Decimal, float, int, long)): diff --git a/django/template/base.py b/django/template/base.py index e2fc66be01..f6f0027f69 100644 --- a/django/template/base.py +++ b/django/template/base.py @@ -18,7 +18,7 @@ from django.utils.safestring import (SafeData, EscapeData, mark_safe, from django.utils.formats import localize from django.utils.html import escape from django.utils.module_loading import module_has_submodule -from django.utils.timezone import localtime +from django.utils.timezone import template_localtime TOKEN_TEXT = 0 @@ -592,7 +592,7 @@ class FilterExpression(object): else: arg_vals.append(arg.resolve(context)) if getattr(func, 'expects_localtime', False): - obj = localtime(obj, context.use_tz) + obj = template_localtime(obj, context.use_tz) if getattr(func, 'needs_autoescape', False): new_obj = func(obj, autoescape=context.autoescape, *arg_vals) else: @@ -853,7 +853,7 @@ def _render_value_in_context(value, context): means escaping, if required, and conversion to a unicode object. If value is a string, it is expected to have already been translated. """ - value = localtime(value, use_tz=context.use_tz) + value = template_localtime(value, use_tz=context.use_tz) value = localize(value, use_l10n=context.use_l10n) value = force_unicode(value) if ((context.autoescape and not isinstance(value, SafeData)) or diff --git a/django/template/debug.py b/django/template/debug.py index 74aa82bcec..ba4f23dc94 100644 --- a/django/template/debug.py +++ b/django/template/debug.py @@ -3,7 +3,7 @@ from django.utils.encoding import force_unicode from django.utils.html import escape from django.utils.safestring import SafeData, EscapeData from django.utils.formats import localize -from django.utils.timezone import localtime +from django.utils.timezone import template_localtime class DebugLexer(Lexer): @@ -82,7 +82,7 @@ class DebugVariableNode(VariableNode): def render(self, context): try: output = self.filter_expression.resolve(context) - output = localtime(output, use_tz=context.use_tz) + output = template_localtime(output, use_tz=context.use_tz) output = localize(output, use_l10n=context.use_l10n) output = force_unicode(output) except UnicodeDecodeError: diff --git a/django/templatetags/tz.py b/django/templatetags/tz.py index ea35b75e77..ca72ca5ec8 100644 --- a/django/templatetags/tz.py +++ b/django/templatetags/tz.py @@ -72,11 +72,7 @@ def do_timezone(value, arg): else: return '' - # Convert and prevent further conversion - result = value.astimezone(tz) - if hasattr(tz, 'normalize'): - # available for pytz time zones - result = tz.normalize(result) + result = timezone.localtime(value, tz) # HACK: the convert_to_local_time flag will prevent # automatic conversion of the value to local time. diff --git a/django/utils/timezone.py b/django/utils/timezone.py index 9d706c96a9..0bba97e736 100644 --- a/django/utils/timezone.py +++ b/django/utils/timezone.py @@ -206,7 +206,7 @@ class override(object): # Templates -def localtime(value, use_tz=None): +def template_localtime(value, use_tz=None): """ Checks if value is a datetime and converts it to local time if necessary. @@ -215,20 +215,30 @@ def localtime(value, use_tz=None): This function is designed for use by the template engine. """ - if (isinstance(value, datetime) + should_convert = (isinstance(value, datetime) and (settings.USE_TZ if use_tz is None else use_tz) and not is_naive(value) - and getattr(value, 'convert_to_local_time', True)): - timezone = get_current_timezone() - value = value.astimezone(timezone) - if hasattr(timezone, 'normalize'): - # available for pytz time zones - value = timezone.normalize(value) - return value + and getattr(value, 'convert_to_local_time', True)) + return localtime(value) if should_convert else value # Utilities +def localtime(value, timezone=None): + """ + Converts an aware datetime.datetime to local time. + + Local time is defined by the current time zone, unless another time zone + is specified. + """ + if timezone is None: + timezone = get_current_timezone() + value = value.astimezone(timezone) + if hasattr(timezone, 'normalize'): + # available for pytz time zones + value = timezone.normalize(value) + return value + def now(): """ Returns an aware or naive datetime.datetime, depending on settings.USE_TZ. diff --git a/docs/ref/utils.txt b/docs/ref/utils.txt index a4fdf7dd48..2525690869 100644 --- a/docs/ref/utils.txt +++ b/docs/ref/utils.txt @@ -666,6 +666,16 @@ For a complete discussion on the usage of the following see the ``None``, the :ref:`current time zone ` is unset on entry with :func:`deactivate()` instead. +.. versionadded:: 1.5 + +.. function:: localtime(value, timezone=None) + + Converts an aware :class:`~datetime.datetime` to a different time zone, + by default the :ref:`current time zone `. + + This function doesn't work on naive datetimes; use :func:`make_aware` + instead. + .. function:: now() Returns an aware or naive :class:`~datetime.datetime` that represents the diff --git a/docs/releases/1.5.txt b/docs/releases/1.5.txt index 4d9572dce9..dcf050f2ae 100644 --- a/docs/releases/1.5.txt +++ b/docs/releases/1.5.txt @@ -41,6 +41,9 @@ Django 1.5 also includes several smaller improvements worth noting: * The template engine now interprets ``True``, ``False`` and ``None`` as the corresponding Python objects. +* :mod:`django.utils.timezone` provides a helper for converting aware + datetimes between time zones, see :func:`~django.utils.timezone.localtime`. + Backwards incompatible changes in 1.5 ===================================== diff --git a/tests/regressiontests/utils/timezone.py b/tests/regressiontests/utils/timezone.py index 870997db65..991600c8b9 100644 --- a/tests/regressiontests/utils/timezone.py +++ b/tests/regressiontests/utils/timezone.py @@ -1,18 +1,33 @@ import copy +import datetime import pickle -from django.utils.timezone import UTC, LocalTimezone +from django.test.utils import override_settings +from django.utils import timezone from django.utils import unittest + class TimezoneTests(unittest.TestCase): + def test_localtime(self): + now = datetime.datetime.utcnow().replace(tzinfo=timezone.utc) + local_tz = timezone.LocalTimezone() + local_now = timezone.localtime(now, local_tz) + self.assertEqual(local_now.tzinfo, local_tz) + + def test_now(self): + with override_settings(USE_TZ=True): + self.assertTrue(timezone.is_aware(timezone.now())) + with override_settings(USE_TZ=False): + self.assertTrue(timezone.is_naive(timezone.now())) + def test_copy(self): - self.assertIsInstance(copy.copy(UTC()), UTC) - self.assertIsInstance(copy.copy(LocalTimezone()), LocalTimezone) + self.assertIsInstance(copy.copy(timezone.UTC()), timezone.UTC) + self.assertIsInstance(copy.copy(timezone.LocalTimezone()), timezone.LocalTimezone) def test_deepcopy(self): - self.assertIsInstance(copy.deepcopy(UTC()), UTC) - self.assertIsInstance(copy.deepcopy(LocalTimezone()), LocalTimezone) + self.assertIsInstance(copy.deepcopy(timezone.UTC()), timezone.UTC) + self.assertIsInstance(copy.deepcopy(timezone.LocalTimezone()), timezone.LocalTimezone) def test_pickling_unpickling(self): - self.assertIsInstance(pickle.loads(pickle.dumps(UTC())), UTC) - self.assertIsInstance(pickle.loads(pickle.dumps(LocalTimezone())), LocalTimezone) + self.assertIsInstance(pickle.loads(pickle.dumps(timezone.UTC())), timezone.UTC) + self.assertIsInstance(pickle.loads(pickle.dumps(timezone.LocalTimezone())), timezone.LocalTimezone)