Browse Source

Added support for time zones. Thanks Luke Plant for the review. Fixed #2626.

For more information on this project, see this thread:
http://groups.google.com/group/django-developers/browse_thread/thread/cf0423bbb85b1bbf



git-svn-id: http://code.djangoproject.com/svn/django/trunk@17106 bcc190cf-cafb-0310-a4f2-bffc1f526a37
Aymeric Augustin 13 years ago
parent
commit
9b1cb755a2
58 changed files with 2714 additions and 278 deletions
  1. 7 2
      django/conf/global_settings.py
  2. 4 1
      django/conf/project_template/project_name/settings.py
  3. 3 0
      django/contrib/admin/util.py
  4. 5 13
      django/contrib/humanize/templatetags/humanize.py
  5. 42 30
      django/contrib/humanize/tests.py
  6. 2 1
      django/contrib/syndication/views.py
  7. 5 0
      django/core/context_processors.py
  8. 15 10
      django/core/serializers/json.py
  9. 3 0
      django/db/backends/__init__.py
  10. 22 5
      django/db/backends/mysql/base.py
  11. 36 9
      django/db/backends/oracle/base.py
  12. 11 6
      django/db/backends/postgresql_psycopg2/base.py
  13. 45 11
      django/db/backends/sqlite3/base.py
  14. 5 1
      django/db/backends/util.py
  15. 77 91
      django/db/models/fields/__init__.py
  16. 1 1
      django/db/utils.py
  17. 13 5
      django/forms/fields.py
  18. 31 0
      django/forms/util.py
  19. 2 1
      django/forms/widgets.py
  20. 6 1
      django/template/base.py
  21. 8 4
      django/template/context.py
  22. 2 0
      django/template/debug.py
  23. 2 2
      django/template/defaultfilters.py
  24. 191 0
      django/templatetags/tz.py
  25. 4 1
      django/utils/cache.py
  26. 10 4
      django/utils/dateformat.py
  27. 93 0
      django/utils/dateparse.py
  28. 3 2
      django/utils/feedgenerator.py
  29. 5 11
      django/utils/timesince.py
  30. 266 0
      django/utils/timezone.py
  31. 19 0
      django/utils/tzinfo.py
  32. 25 0
      docs/howto/custom-template-tags.txt
  33. 13 0
      docs/ref/models/querysets.txt
  34. 42 12
      docs/ref/settings.txt
  35. 39 24
      docs/ref/templates/builtins.txt
  36. 125 0
      docs/ref/utils.txt
  37. 52 1
      docs/releases/1.4.txt
  38. 3 1
      docs/topics/cache.txt
  39. 6 4
      docs/topics/i18n/index.txt
  40. 429 0
      docs/topics/i18n/timezones.txt
  41. 6 6
      tests/modeltests/fixtures/tests.py
  42. 3 3
      tests/modeltests/serializers/tests.py
  43. 0 0
      tests/modeltests/timezones/__init__.py
  44. 15 0
      tests/modeltests/timezones/admin.py
  45. 17 0
      tests/modeltests/timezones/fixtures/users.xml
  46. 13 0
      tests/modeltests/timezones/forms.py
  47. 8 0
      tests/modeltests/timezones/models.py
  48. 871 0
      tests/modeltests/timezones/tests.py
  49. 10 0
      tests/modeltests/timezones/urls.py
  50. 35 8
      tests/modeltests/validation/test_error_messages.py
  51. 15 3
      tests/regressiontests/cache/tests.py
  52. 2 2
      tests/regressiontests/datatypes/tests.py
  53. 1 1
      tests/regressiontests/defaultfilters/tests.py
  54. 1 1
      tests/regressiontests/utils/dateformat.py
  55. 1 0
      tests/regressiontests/utils/tests.py
  56. 9 0
      tests/regressiontests/utils/timesince.py
  57. 18 0
      tests/regressiontests/utils/timezone.py
  58. 17 0
      tests/regressiontests/utils/tzinfo.py

+ 7 - 2
django/conf/global_settings.py

@@ -31,9 +31,13 @@ INTERNAL_IPS = ()
 
 # Local time zone for this installation. All choices can be found here:
 # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name (although not all
-# systems may support all possibilities).
+# systems may support all possibilities). When USE_TZ is True, this is
+# interpreted as the default user time zone.
 TIME_ZONE = 'America/Chicago'
 
+# If you set this to True, Django will use timezone-aware datetimes.
+USE_TZ = False
+
 # Language code for this installation. All choices can be found here:
 # http://www.i18nguy.com/unicode/language-identifiers.html
 LANGUAGE_CODE = 'en-us'
@@ -119,7 +123,7 @@ LOCALE_PATHS = ()
 LANGUAGE_COOKIE_NAME = 'django_language'
 
 # If you set this to True, Django will format dates, numbers and calendars
-# according to user current locale
+# according to user current locale.
 USE_L10N = False
 
 # Not-necessarily-technical managers of the site. They get broken link
@@ -192,6 +196,7 @@ TEMPLATE_CONTEXT_PROCESSORS = (
     'django.core.context_processors.i18n',
     'django.core.context_processors.media',
     'django.core.context_processors.static',
+    'django.core.context_processors.tz',
 #    'django.core.context_processors.request',
     'django.contrib.messages.context_processors.messages',
 )

+ 4 - 1
django/conf/project_template/project_name/settings.py

@@ -40,9 +40,12 @@ SITE_ID = 1
 USE_I18N = True
 
 # If you set this to False, Django will not format dates, numbers and
-# calendars according to the current locale
+# calendars according to the current locale.
 USE_L10N = True
 
+# If you set this to False, Django will not use timezone-aware datetimes.
+USE_TZ = True
+
 # Absolute filesystem path to the directory that will hold user-uploaded files.
 # Example: "/home/media/media.lawrence.com/media/"
 MEDIA_ROOT = ''

+ 3 - 0
django/contrib/admin/util.py

@@ -7,6 +7,7 @@ from django.utils import formats
 from django.utils.html import escape
 from django.utils.safestring import mark_safe
 from django.utils.text import capfirst
+from django.utils import timezone
 from django.utils.encoding import force_unicode, smart_unicode, smart_str
 from django.utils.translation import ungettext
 from django.core.urlresolvers import reverse
@@ -293,6 +294,8 @@ def display_for_field(value, field):
         return _boolean_icon(value)
     elif value is None:
         return EMPTY_CHANGELIST_VALUE
+    elif isinstance(field, models.DateTimeField):
+        return formats.localize(timezone.aslocaltime(value))
     elif isinstance(field, models.DateField) or isinstance(field, models.TimeField):
         return formats.localize(value)
     elif isinstance(field, models.DecimalField):

+ 5 - 13
django/contrib/humanize/templatetags/humanize.py

@@ -7,7 +7,7 @@ from django.template import defaultfilters
 from django.utils.encoding import force_unicode
 from django.utils.formats import number_format
 from django.utils.translation import pgettext, ungettext, ugettext as _
-from django.utils.tzinfo import LocalTimezone
+from django.utils.timezone import is_aware, utc
 
 register = template.Library()
 
@@ -158,8 +158,8 @@ def naturalday(value, arg=None):
     except ValueError:
         # Date arguments out of range
         return value
-    today = datetime.now(tzinfo).replace(microsecond=0, second=0, minute=0, hour=0)
-    delta = value - today.date()
+    today = datetime.now(tzinfo).date()
+    delta = value - today
     if delta.days == 0:
         return _(u'today')
     elif delta.days == 1:
@@ -174,18 +174,10 @@ def naturaltime(value):
     For date and time values shows how many seconds, minutes or hours ago
     compared to current timestamp returns representing string.
     """
-    try:
-        value = datetime(value.year, value.month, value.day, value.hour, value.minute, value.second)
-    except AttributeError:
-        return value
-    except ValueError:
+    if not isinstance(value, date): # datetime is a subclass of date
         return value
 
-    if getattr(value, 'tzinfo', None):
-        now = datetime.now(LocalTimezone(value))
-    else:
-        now = datetime.now()
-    now = now - timedelta(0, 0, now.microsecond)
+    now = datetime.now(utc if is_aware(value) else None)
     if value < now:
         delta = now - value
         if delta.days != 0:

+ 42 - 30
django/contrib/humanize/tests.py

@@ -1,11 +1,12 @@
 from __future__ import with_statement
-from datetime import timedelta, date, datetime
+import datetime
 
 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.utils.html import escape
+from django.utils.timezone import utc
 
 
 class HumanizeTests(TestCase):
@@ -88,10 +89,10 @@ class HumanizeTests(TestCase):
         self.humanize_tester(test_list, result_list, 'apnumber')
 
     def test_naturalday(self):
-        today = date.today()
-        yesterday = today - timedelta(days=1)
-        tomorrow = today + timedelta(days=1)
-        someday = today - timedelta(days=10)
+        today = datetime.date.today()
+        yesterday = today - datetime.timedelta(days=1)
+        tomorrow = today + datetime.timedelta(days=1)
+        someday = today - datetime.timedelta(days=10)
         notdate = u"I'm not a date value"
 
         test_list = (today, yesterday, tomorrow, someday, notdate, None)
@@ -103,41 +104,46 @@ class HumanizeTests(TestCase):
     def test_naturalday_tz(self):
         from django.contrib.humanize.templatetags.humanize import naturalday
 
-        today = date.today()
-        tz_one = tzinfo.FixedOffset(timedelta(hours=-12))
-        tz_two = tzinfo.FixedOffset(timedelta(hours=12))
+        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(today.year, today.month, today.day, tzinfo=tz_one)
+        date_one = datetime.datetime(today.year, today.month, today.day, tzinfo=tz_one)
         naturalday_one = naturalday(date_one)
         # Can be today or tomorrow
-        date_two = datetime(today.year, today.month, today.day, tzinfo=tz_two)
+        date_two = datetime.datetime(today.year, today.month, today.day, tzinfo=tz_two)
         naturalday_two = naturalday(date_two)
 
         # As 24h of difference they will never be the same
         self.assertNotEqual(naturalday_one, naturalday_two)
 
     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(2011, 8, 15)
+        now = datetime.datetime(2011, 8, 15)
         test_list = [
             now,
-            now - timedelta(seconds=1),
-            now - timedelta(seconds=30),
-            now - timedelta(minutes=1, seconds=30),
-            now - timedelta(minutes=2),
-            now - timedelta(hours=1, minutes=30, seconds=30),
-            now - timedelta(hours=23, minutes=50, seconds=50),
-            now - timedelta(days=1),
-            now - timedelta(days=500),
-            now + timedelta(seconds=1),
-            now + timedelta(seconds=30),
-            now + timedelta(minutes=1, seconds=30),
-            now + timedelta(minutes=2),
-            now + timedelta(hours=1, minutes=30, seconds=30),
-            now + timedelta(hours=23, minutes=50, seconds=50),
-            now + timedelta(days=1),
-            now + timedelta(days=500),
+            now - datetime.timedelta(seconds=1),
+            now - datetime.timedelta(seconds=30),
+            now - datetime.timedelta(minutes=1, seconds=30),
+            now - datetime.timedelta(minutes=2),
+            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=500),
+            now + datetime.timedelta(seconds=1),
+            now + datetime.timedelta(seconds=30),
+            now + datetime.timedelta(minutes=1, seconds=30),
+            now + datetime.timedelta(minutes=2),
+            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=500),
+            now.replace(tzinfo=naive()),
+            now.replace(tzinfo=utc),
         ]
         result_list = [
             'now',
@@ -157,14 +163,20 @@ class HumanizeTests(TestCase):
             '23 hours from now',
             '1 day from now',
             '1 year, 4 months from now',
+            'now',
+            'now',
         ]
 
         # mock out datetime so these tests don't fail occasionally when the
         # test runs too slow
-        class MockDateTime(datetime):
+        class MockDateTime(datetime.datetime):
             @classmethod
-            def now(self):
-                return now
+            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)
 
         # naturaltime also calls timesince/timeuntil
         from django.contrib.humanize.templatetags import humanize

+ 2 - 1
django/contrib/syndication/views.py

@@ -6,6 +6,7 @@ from django.template import loader, TemplateDoesNotExist, RequestContext
 from django.utils import feedgenerator, tzinfo
 from django.utils.encoding import force_unicode, iri_to_uri, smart_unicode
 from django.utils.html import escape
+from django.utils.timezone import is_naive
 
 def add_domain(domain, url, secure=False):
     if not (url.startswith('http://')
@@ -164,7 +165,7 @@ class Feed(object):
                 author_email = author_link = None
 
             pubdate = self.__get_dynamic_attr('item_pubdate', item)
-            if pubdate and not pubdate.tzinfo:
+            if pubdate and is_naive(pubdate):
                 ltz = tzinfo.LocalTimezone(pubdate)
                 pubdate = pubdate.replace(tzinfo=ltz)
 

+ 5 - 0
django/core/context_processors.py

@@ -48,6 +48,11 @@ def i18n(request):
 
     return context_extras
 
+def tz(request):
+    from django.utils import timezone
+
+    return {'TIME_ZONE': timezone.get_current_timezone_name()}
+
 def static(request):
     """
     Adds static-related context variables to the context.

+ 15 - 10
django/core/serializers/json.py

@@ -8,8 +8,8 @@ from StringIO import StringIO
 
 from django.core.serializers.python import Serializer as PythonSerializer
 from django.core.serializers.python import Deserializer as PythonDeserializer
-from django.utils import datetime_safe
 from django.utils import simplejson
+from django.utils.timezone import is_aware
 
 class Serializer(PythonSerializer):
     """
@@ -39,19 +39,24 @@ class DjangoJSONEncoder(simplejson.JSONEncoder):
     """
     JSONEncoder subclass that knows how to encode date/time and decimal types.
     """
-
-    DATE_FORMAT = "%Y-%m-%d"
-    TIME_FORMAT = "%H:%M:%S"
-
     def default(self, o):
+        # See "Date Time String Format" in the ECMA-262 specification.
         if isinstance(o, datetime.datetime):
-            d = datetime_safe.new_datetime(o)
-            return d.strftime("%s %s" % (self.DATE_FORMAT, self.TIME_FORMAT))
+            r = o.isoformat()
+            if o.microsecond:
+                r = r[:23] + r[26:]
+            if r.endswith('+00:00'):
+                r = r[:-6] + 'Z'
+            return r
         elif isinstance(o, datetime.date):
-            d = datetime_safe.new_date(o)
-            return d.strftime(self.DATE_FORMAT)
+            return o.isoformat()
         elif isinstance(o, datetime.time):
-            return o.strftime(self.TIME_FORMAT)
+            if is_aware(o):
+                raise ValueError("JSON can't represent timezone-aware times.")
+            r = o.isoformat()
+            if o.microsecond:
+                r = r[:12]
+            return r
         elif isinstance(o, decimal.Decimal):
             return str(o)
         else:

+ 3 - 0
django/db/backends/__init__.py

@@ -10,6 +10,7 @@ from django.db import DEFAULT_DB_ALIAS
 from django.db.backends import util
 from django.db.transaction import TransactionManagementError
 from django.utils.importlib import import_module
+from django.utils.timezone import is_aware
 
 
 class BaseDatabaseWrapper(local):
@@ -743,6 +744,8 @@ class BaseDatabaseOperations(object):
         """
         if value is None:
             return None
+        if is_aware(value):
+            raise ValueError("Django does not support timezone-aware times.")
         return unicode(value)
 
     def value_to_db_decimal(self, value, max_digits, decimal_places):

+ 22 - 5
django/db/backends/mysql/base.py

@@ -33,6 +33,7 @@ from django.db.backends.mysql.creation import DatabaseCreation
 from django.db.backends.mysql.introspection import DatabaseIntrospection
 from django.db.backends.mysql.validation import DatabaseValidation
 from django.utils.safestring import SafeString, SafeUnicode
+from django.utils.timezone import is_aware, is_naive, utc
 
 # Raise exceptions for database warnings if DEBUG is on
 from django.conf import settings
@@ -43,16 +44,29 @@ if settings.DEBUG:
 DatabaseError = Database.DatabaseError
 IntegrityError = Database.IntegrityError
 
+# It's impossible to import datetime_or_None directly from MySQLdb.times
+datetime_or_None = conversions[FIELD_TYPE.DATETIME]
+
+def datetime_or_None_with_timezone_support(value):
+    dt = datetime_or_None(value)
+    # Confirm that dt is naive before overwriting its tzinfo.
+    if dt is not None and settings.USE_TZ and is_naive(dt):
+        dt = dt.replace(tzinfo=utc)
+    return dt
+
 # MySQLdb-1.2.1 returns TIME columns as timedelta -- they are more like
 # timedelta in terms of actual behavior as they are signed and include days --
 # and Django expects time, so we still need to override that. We also need to
 # add special handling for SafeUnicode and SafeString as MySQLdb's type
 # checking is too tight to catch those (see Django ticket #6052).
+# Finally, MySQLdb always returns naive datetime objects. However, when
+# timezone support is active, Django expects timezone-aware datetime objects.
 django_conversions = conversions.copy()
 django_conversions.update({
     FIELD_TYPE.TIME: util.typecast_time,
     FIELD_TYPE.DECIMAL: util.typecast_decimal,
     FIELD_TYPE.NEWDECIMAL: util.typecast_decimal,
+    FIELD_TYPE.DATETIME: datetime_or_None_with_timezone_support,
 })
 
 # This should match the numerical portion of the version numbers (we can treat
@@ -238,8 +252,11 @@ class DatabaseOperations(BaseDatabaseOperations):
             return None
 
         # MySQL doesn't support tz-aware datetimes
-        if value.tzinfo is not None:
-            raise ValueError("MySQL backend does not support timezone-aware datetimes.")
+        if is_aware(value):
+            if settings.USE_TZ:
+                value = value.astimezone(utc).replace(tzinfo=None)
+            else:
+                raise ValueError("MySQL backend does not support timezone-aware datetimes when USE_TZ is False.")
 
         # MySQL doesn't support microseconds
         return unicode(value.replace(microsecond=0))
@@ -248,9 +265,9 @@ class DatabaseOperations(BaseDatabaseOperations):
         if value is None:
             return None
 
-        # MySQL doesn't support tz-aware datetimes
-        if value.tzinfo is not None:
-            raise ValueError("MySQL backend does not support timezone-aware datetimes.")
+        # MySQL doesn't support tz-aware times
+        if is_aware(value):
+            raise ValueError("MySQL backend does not support timezone-aware times.")
 
         # MySQL doesn't support microseconds
         return unicode(value.replace(microsecond=0))

+ 36 - 9
django/db/backends/oracle/base.py

@@ -44,6 +44,7 @@ except ImportError, e:
     from django.core.exceptions import ImproperlyConfigured
     raise ImproperlyConfigured("Error loading cx_Oracle module: %s" % e)
 
+from django.conf import settings
 from django.db import utils
 from django.db.backends import *
 from django.db.backends.signals import connection_created
@@ -51,6 +52,7 @@ from django.db.backends.oracle.client import DatabaseClient
 from django.db.backends.oracle.creation import DatabaseCreation
 from django.db.backends.oracle.introspection import DatabaseIntrospection
 from django.utils.encoding import smart_str, force_unicode
+from django.utils.timezone import is_aware, is_naive, utc
 
 DatabaseError = Database.DatabaseError
 IntegrityError = Database.IntegrityError
@@ -333,11 +335,17 @@ WHEN (new.%(col_name)s IS NULL)
             return "TABLESPACE %s" % self.quote_name(tablespace)
 
     def value_to_db_datetime(self, value):
+        if value is None:
+            return None
+
         # Oracle doesn't support tz-aware datetimes
-        if getattr(value, 'tzinfo', None) is not None:
-            raise ValueError("Oracle backend does not support timezone-aware datetimes.")
+        if is_aware(value):
+            if settings.USE_TZ:
+                value = value.astimezone(utc).replace(tzinfo=None)
+            else:
+                raise ValueError("Oracle backend does not support timezone-aware datetimes when USE_TZ is False.")
 
-        return super(DatabaseOperations, self).value_to_db_datetime(value)
+        return unicode(value)
 
     def value_to_db_time(self, value):
         if value is None:
@@ -346,9 +354,9 @@ WHEN (new.%(col_name)s IS NULL)
         if isinstance(value, basestring):
             return datetime.datetime.strptime(value, '%H:%M:%S')
 
-        # Oracle doesn't support tz-aware datetimes
-        if value.tzinfo is not None:
-            raise ValueError("Oracle backend does not support timezone-aware datetimes.")
+        # Oracle doesn't support tz-aware times
+        if is_aware(value):
+            raise ValueError("Oracle backend does not support timezone-aware times.")
 
         return datetime.datetime(1900, 1, 1, value.hour, value.minute,
                                  value.second, value.microsecond)
@@ -472,9 +480,28 @@ class DatabaseWrapper(BaseDatabaseWrapper):
             # Set oracle date to ansi date format.  This only needs to execute
             # once when we create a new connection. We also set the Territory
             # to 'AMERICA' which forces Sunday to evaluate to a '1' in TO_CHAR().
-            cursor.execute("ALTER SESSION SET NLS_DATE_FORMAT = 'YYYY-MM-DD HH24:MI:SS' "
-                           "NLS_TIMESTAMP_FORMAT = 'YYYY-MM-DD HH24:MI:SS.FF' "
-                           "NLS_TERRITORY = 'AMERICA'")
+            cursor.execute("ALTER SESSION SET NLS_DATE_FORMAT = 'YYYY-MM-DD HH24:MI:SS'"
+                           " NLS_TIMESTAMP_FORMAT = 'YYYY-MM-DD HH24:MI:SS.FF'"
+                           " NLS_TERRITORY = 'AMERICA'"
+                           + (" TIME_ZONE = 'UTC'" if settings.USE_TZ else ''))
+
+            def datetime_converter(dt):
+                # Confirm that dt is naive before overwriting its tzinfo.
+                if dt is not None and is_naive(dt):
+                    dt = dt.replace(tzinfo=utc)
+                return dt
+
+            def output_type_handler(cursor, name, default_type,
+                                    size, precision, scale):
+                # datetimes are returned as TIMESTAMP, except the results
+                # of "dates" queries, which are returned as DATETIME.
+                if settings.USE_TZ and default_type in (Database.TIMESTAMP,
+                                                        Database.DATETIME):
+                    return cursor.var(default_type,
+                                      arraysize=cursor.arraysize,
+                                      outconverter=datetime_converter)
+
+            self.connection.outputtypehandler = output_type_handler
 
             if 'operators' not in self.__dict__:
                 # Ticket #14149: Check whether our LIKE implementation will

+ 11 - 6
django/db/backends/postgresql_psycopg2/base.py

@@ -13,8 +13,9 @@ from django.db.backends.postgresql_psycopg2.client import DatabaseClient
 from django.db.backends.postgresql_psycopg2.creation import DatabaseCreation
 from django.db.backends.postgresql_psycopg2.version import get_version
 from django.db.backends.postgresql_psycopg2.introspection import DatabaseIntrospection
-from django.utils.safestring import SafeUnicode, SafeString
 from django.utils.log import getLogger
+from django.utils.safestring import SafeUnicode, SafeString
+from django.utils.timezone import utc
 
 try:
     import psycopg2 as Database
@@ -32,6 +33,11 @@ psycopg2.extensions.register_adapter(SafeUnicode, psycopg2.extensions.QuotedStri
 
 logger = getLogger('django.db.backends')
 
+def utc_tzinfo_factory(offset):
+    if offset != 0:
+        raise AssertionError("database connection isn't set to UTC")
+    return utc
+
 class CursorWrapper(object):
     """
     A thin wrapper around psycopg2's normal cursor class so that we can catch
@@ -144,11 +150,9 @@ class DatabaseWrapper(BaseDatabaseWrapper):
 
     def _cursor(self):
         new_connection = False
-        set_tz = False
         settings_dict = self.settings_dict
         if self.connection is None:
             new_connection = True
-            set_tz = settings_dict.get('TIME_ZONE')
             if settings_dict['NAME'] == '':
                 from django.core.exceptions import ImproperlyConfigured
                 raise ImproperlyConfigured("You need to specify NAME in your Django settings file.")
@@ -171,10 +175,11 @@ class DatabaseWrapper(BaseDatabaseWrapper):
             self.connection.set_isolation_level(self.isolation_level)
             connection_created.send(sender=self.__class__, connection=self)
         cursor = self.connection.cursor()
-        cursor.tzinfo_factory = None
+        cursor.tzinfo_factory = utc_tzinfo_factory if settings.USE_TZ else None
         if new_connection:
-            if set_tz:
-                cursor.execute("SET TIME ZONE %s", [settings_dict['TIME_ZONE']])
+            tz = 'UTC' if settings.USE_TZ else settings_dict.get('TIME_ZONE')
+            if tz:
+                cursor.execute("SET TIME ZONE %s", [tz])
             self._get_pg_version()
         return CursorWrapper(cursor)
 

+ 45 - 11
django/db/backends/sqlite3/base.py

@@ -10,13 +10,16 @@ import decimal
 import re
 import sys
 
+from django.conf import settings
 from django.db import utils
 from django.db.backends import *
 from django.db.backends.signals import connection_created
 from django.db.backends.sqlite3.client import DatabaseClient
 from django.db.backends.sqlite3.creation import DatabaseCreation
 from django.db.backends.sqlite3.introspection import DatabaseIntrospection
+from django.utils.dateparse import parse_date, parse_datetime, parse_time
 from django.utils.safestring import SafeString
+from django.utils.timezone import is_aware, is_naive, utc
 
 try:
     try:
@@ -31,22 +34,29 @@ except ImportError, exc:
 DatabaseError = Database.DatabaseError
 IntegrityError = Database.IntegrityError
 
+def parse_datetime_with_timezone_support(value):
+    dt = parse_datetime(value)
+    # Confirm that dt is naive before overwriting its tzinfo.
+    if dt is not None and settings.USE_TZ and is_naive(dt):
+        dt = dt.replace(tzinfo=utc)
+    return dt
+
 Database.register_converter("bool", lambda s: str(s) == '1')
-Database.register_converter("time", util.typecast_time)
-Database.register_converter("date", util.typecast_date)
-Database.register_converter("datetime", util.typecast_timestamp)
-Database.register_converter("timestamp", util.typecast_timestamp)
-Database.register_converter("TIMESTAMP", util.typecast_timestamp)
+Database.register_converter("time", parse_time)
+Database.register_converter("date", parse_date)
+Database.register_converter("datetime", parse_datetime_with_timezone_support)
+Database.register_converter("timestamp", parse_datetime_with_timezone_support)
+Database.register_converter("TIMESTAMP", parse_datetime_with_timezone_support)
 Database.register_converter("decimal", util.typecast_decimal)
 Database.register_adapter(decimal.Decimal, util.rev_typecast_decimal)
-if Database.version_info >= (2,4,1):
+if Database.version_info >= (2, 4, 1):
     # Starting in 2.4.1, the str type is not accepted anymore, therefore,
     # we convert all str objects to Unicode
     # As registering a adapter for a primitive type causes a small
     # slow-down, this adapter is only registered for sqlite3 versions
     # needing it.
-    Database.register_adapter(str, lambda s:s.decode('utf-8'))
-    Database.register_adapter(SafeString, lambda s:s.decode('utf-8'))
+    Database.register_adapter(str, lambda s: s.decode('utf-8'))
+    Database.register_adapter(SafeString, lambda s: s.decode('utf-8'))
 
 class DatabaseFeatures(BaseDatabaseFeatures):
     # SQLite cannot handle us only partially reading from a cursor's result set
@@ -56,6 +66,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
     can_use_chunked_reads = False
     test_db_allows_multiple_connections = False
     supports_unspecified_pk = True
+    supports_timezones = False
     supports_1000_query_parameters = False
     supports_mixed_date_datetime_comparisons = False
     has_bulk_insert = True
@@ -131,6 +142,29 @@ class DatabaseOperations(BaseDatabaseOperations):
         # sql_flush() implementations). Just return SQL at this point
         return sql
 
+    def value_to_db_datetime(self, value):
+        if value is None:
+            return None
+
+        # SQLite doesn't support tz-aware datetimes
+        if is_aware(value):
+            if settings.USE_TZ:
+                value = value.astimezone(utc).replace(tzinfo=None)
+            else:
+                raise ValueError("SQLite backend does not support timezone-aware datetimes when USE_TZ is False.")
+
+        return unicode(value)
+
+    def value_to_db_time(self, value):
+        if value is None:
+            return None
+
+        # SQLite doesn't support tz-aware datetimes
+        if is_aware(value):
+            raise ValueError("SQLite backend does not support timezone-aware times.")
+
+        return unicode(value)
+
     def year_lookup_bounds(self, value):
         first = '%s-01-01'
         second = '%s-12-31 23:59:59.999999'
@@ -147,11 +181,11 @@ class DatabaseOperations(BaseDatabaseOperations):
         elif internal_type and internal_type.endswith('IntegerField') or internal_type == 'AutoField':
             return int(value)
         elif internal_type == 'DateField':
-            return util.typecast_date(value)
+            return parse_date(value)
         elif internal_type == 'DateTimeField':
-            return util.typecast_timestamp(value)
+            return parse_datetime_with_timezone_support(value)
         elif internal_type == 'TimeField':
-            return util.typecast_time(value)
+            return parse_time(value)
 
         # No field, or the field isn't known to be a decimal or integer
         return value

+ 5 - 1
django/db/backends/util.py

@@ -3,7 +3,9 @@ import decimal
 import hashlib
 from time import time
 
+from django.conf import settings
 from django.utils.log import getLogger
+from django.utils.timezone import utc
 
 
 logger = getLogger('django.db.backends')
@@ -99,8 +101,10 @@ def typecast_timestamp(s): # does NOT store time zone information
         seconds, microseconds = seconds.split('.')
     else:
         microseconds = '0'
+    tzinfo = utc if settings.USE_TZ else None
     return datetime.datetime(int(dates[0]), int(dates[1]), int(dates[2]),
-        int(times[0]), int(times[1]), int(seconds), int((microseconds + '000000')[:6]))
+        int(times[0]), int(times[1]), int(seconds),
+        int((microseconds + '000000')[:6]), tzinfo)
 
 def typecast_decimal(s):
     if s is None or s == '':

+ 77 - 91
django/db/models/fields/__init__.py

@@ -1,8 +1,6 @@
 import copy
 import datetime
 import decimal
-import re
-import time
 import math
 from itertools import tee
 
@@ -12,8 +10,10 @@ from django.conf import settings
 from django import forms
 from django.core import exceptions, validators
 from django.utils.datastructures import DictWrapper
+from django.utils.dateparse import parse_date, parse_datetime, parse_time
 from django.utils.functional import curry
 from django.utils.text import capfirst
+from django.utils import timezone
 from django.utils.translation import ugettext_lazy as _
 from django.utils.encoding import smart_unicode, force_unicode, smart_str
 from django.utils.ipv6 import clean_ipv6_address
@@ -180,8 +180,8 @@ class Field(object):
                             return
                 elif value == option_key:
                     return
-            raise exceptions.ValidationError(
-                self.error_messages['invalid_choice'] % value)
+            msg = self.error_messages['invalid_choice'] % value
+            raise exceptions.ValidationError(msg)
 
         if value is None and not self.null:
             raise exceptions.ValidationError(self.error_messages['null'])
@@ -638,11 +638,7 @@ class CommaSeparatedIntegerField(CharField):
         defaults.update(kwargs)
         return super(CommaSeparatedIntegerField, self).formfield(**defaults)
 
-ansi_date_re = re.compile(r'^\d{4}-\d{1,2}-\d{1,2}$')
-
 class DateField(Field):
-    description = _("Date (without time)")
-
     empty_strings_allowed = False
     default_error_messages = {
         'invalid': _(u"'%s' value has an invalid date format. It must be "
@@ -650,11 +646,11 @@ class DateField(Field):
         'invalid_date': _(u"'%s' value has the correct format (YYYY-MM-DD) "
                           u"but it is an invalid date."),
     }
+    description = _("Date (without time)")
+
     def __init__(self, verbose_name=None, name=None, auto_now=False,
                  auto_now_add=False, **kwargs):
         self.auto_now, self.auto_now_add = auto_now, auto_now_add
-        # HACKs : auto_now_add/auto_now should be done as a default or a
-        # pre_save.
         if auto_now or auto_now_add:
             kwargs['editable'] = False
             kwargs['blank'] = True
@@ -671,20 +667,19 @@ class DateField(Field):
         if isinstance(value, datetime.date):
             return value
 
-        if not ansi_date_re.search(value):
-            msg = self.error_messages['invalid'] % str(value)
-            raise exceptions.ValidationError(msg)
-        # Now that we have the date string in YYYY-MM-DD format, check to make
-        # sure it's a valid date.
-        # We could use time.strptime here and catch errors, but datetime.date
-        # produces much friendlier error messages.
-        year, month, day = map(int, value.split('-'))
+        value = smart_str(value)
+
         try:
-            return datetime.date(year, month, day)
-        except ValueError, e:
-            msg = self.error_messages['invalid_date'] % str(value)
+            parsed = parse_date(value)
+            if parsed is not None:
+                return parsed
+        except ValueError:
+            msg = self.error_messages['invalid_date'] % value
             raise exceptions.ValidationError(msg)
 
+        msg = self.error_messages['invalid'] % value
+        raise exceptions.ValidationError(msg)
+
     def pre_save(self, model_instance, add):
         if self.auto_now or (self.auto_now_add and add):
             value = datetime.date.today()
@@ -721,11 +716,7 @@ class DateField(Field):
 
     def value_to_string(self, obj):
         val = self._get_val_from_obj(obj)
-        if val is None:
-            data = ''
-        else:
-            data = str(val)
-        return data
+        return '' if val is None else val.isoformat()
 
     def formfield(self, **kwargs):
         defaults = {'form_class': forms.DateField}
@@ -733,13 +724,20 @@ class DateField(Field):
         return super(DateField, self).formfield(**defaults)
 
 class DateTimeField(DateField):
+    empty_strings_allowed = False
     default_error_messages = {
-        'invalid': _(u"'%s' value either has an invalid valid format (The "
-                     u"format must be YYYY-MM-DD HH:MM[:ss[.uuuuuu]]) or is "
-                     u"an invalid date/time."),
+        'invalid': _(u"'%s' value has an invalid format. It must be in "
+                     u"YYYY-MM-DD HH:MM[:ss[.uuuuuu]][TZ] format."),
+        'invalid_date': _(u"'%s' value has the correct format "
+                          u"(YYYY-MM-DD) but it is an invalid date."),
+        'invalid_datetime': _(u"'%s' value has the correct format "
+                              u"(YYYY-MM-DD HH:MM[:ss[.uuuuuu]][TZ]) "
+                              u"but it is an invalid date/time."),
     }
     description = _("Date (with time)")
 
+    # __init__ is inherited from DateField
+
     def get_internal_type(self):
         return "DateTimeField"
 
@@ -751,59 +749,59 @@ class DateTimeField(DateField):
         if isinstance(value, datetime.date):
             return datetime.datetime(value.year, value.month, value.day)
 
-        # Attempt to parse a datetime:
         value = smart_str(value)
-        # split usecs, because they are not recognized by strptime.
-        if '.' in value:
-            try:
-                value, usecs = value.split('.')
-                usecs = int(usecs)
-            except ValueError:
-                raise exceptions.ValidationError(
-                    self.error_messages['invalid'] % str(value))
-        else:
-            usecs = 0
-        kwargs = {'microsecond': usecs}
-        try: # Seconds are optional, so try converting seconds first.
-            return datetime.datetime(
-                *time.strptime(value, '%Y-%m-%d %H:%M:%S')[:6], **kwargs)
 
+        try:
+            parsed = parse_datetime(value)
+            if parsed is not None:
+                return parsed
+        except ValueError:
+            msg = self.error_messages['invalid_datetime'] % value
+            raise exceptions.ValidationError(msg)
+
+        try:
+            parsed = parse_date(value)
+            if parsed is not None:
+                return datetime.datetime(parsed.year, parsed.month, parsed.day)
         except ValueError:
-            try: # Try without seconds.
-                return datetime.datetime(
-                    *time.strptime(value, '%Y-%m-%d %H:%M')[:5], **kwargs)
-            except ValueError: # Try without hour/minutes/seconds.
-                try:
-                    return datetime.datetime(
-                        *time.strptime(value, '%Y-%m-%d')[:3], **kwargs)
-                except ValueError:
-                    raise exceptions.ValidationError(
-                        self.error_messages['invalid'] % str(value))
+            msg = self.error_messages['invalid_date'] % value
+            raise exceptions.ValidationError(msg)
+
+        msg = self.error_messages['invalid'] % value
+        raise exceptions.ValidationError(msg)
 
     def pre_save(self, model_instance, add):
         if self.auto_now or (self.auto_now_add and add):
-            value = datetime.datetime.now()
+            value = timezone.now()
             setattr(model_instance, self.attname, value)
             return value
         else:
             return super(DateTimeField, self).pre_save(model_instance, add)
 
+    # contribute_to_class is inherited from DateField, it registers
+    # get_next_by_FOO and get_prev_by_FOO
+
+    # get_prep_lookup is inherited from DateField
+
     def get_prep_value(self, value):
-        return self.to_python(value)
+        value = self.to_python(value)
+        if settings.USE_TZ and timezone.is_naive(value):
+            # For backwards compatibility, interpret naive datetimes in local
+            # time. This won't work during DST change, but we can't do much
+            # about it, so we let the exceptions percolate up the call stack.
+            default_timezone = timezone.get_default_timezone()
+            value = timezone.make_aware(value, default_timezone)
+        return value
 
     def get_db_prep_value(self, value, connection, prepared=False):
-        # Casts dates into the format expected by the backend
+        # Casts datetimes into the format expected by the backend
         if not prepared:
             value = self.get_prep_value(value)
         return connection.ops.value_to_db_datetime(value)
 
     def value_to_string(self, obj):
         val = self._get_val_from_obj(obj)
-        if val is None:
-            data = ''
-        else:
-            data = str(val.replace(microsecond=0, tzinfo=None))
-        return data
+        return '' if val is None else val.isoformat()
 
     def formfield(self, **kwargs):
         defaults = {'form_class': forms.DateTimeField}
@@ -1158,17 +1156,21 @@ class TextField(Field):
         return super(TextField, self).formfield(**defaults)
 
 class TimeField(Field):
-    description = _("Time")
-
     empty_strings_allowed = False
     default_error_messages = {
-        'invalid': _('Enter a valid time in HH:MM[:ss[.uuuuuu]] format.'),
+        'invalid': _(u"'%s' value has an invalid format. It must be in "
+                     u"HH:MM[:ss[.uuuuuu]] format."),
+        'invalid_time': _(u"'%s' value has the correct format "
+                          u"(HH:MM[:ss[.uuuuuu]]) but it is an invalid time."),
     }
+    description = _("Time")
+
     def __init__(self, verbose_name=None, name=None, auto_now=False,
                  auto_now_add=False, **kwargs):
         self.auto_now, self.auto_now_add = auto_now, auto_now_add
         if auto_now or auto_now_add:
             kwargs['editable'] = False
+            kwargs['blank'] = True
         Field.__init__(self, verbose_name, name, **kwargs)
 
     def get_internal_type(self):
@@ -1185,30 +1187,18 @@ class TimeField(Field):
             # database backend (e.g. Oracle), so we'll be accommodating.
             return value.time()
 
-        # Attempt to parse a datetime:
         value = smart_str(value)
-        # split usecs, because they are not recognized by strptime.
-        if '.' in value:
-            try:
-                value, usecs = value.split('.')
-                usecs = int(usecs)
-            except ValueError:
-                raise exceptions.ValidationError(
-                    self.error_messages['invalid'])
-        else:
-            usecs = 0
-        kwargs = {'microsecond': usecs}
 
-        try: # Seconds are optional, so try converting seconds first.
-            return datetime.time(*time.strptime(value, '%H:%M:%S')[3:6],
-                                 **kwargs)
+        try:
+            parsed = parse_time(value)
+            if parsed is not None:
+                return parsed
         except ValueError:
-            try: # Try without seconds.
-                return datetime.time(*time.strptime(value, '%H:%M')[3:5],
-                                         **kwargs)
-            except ValueError:
-                raise exceptions.ValidationError(
-                    self.error_messages['invalid'])
+            msg = self.error_messages['invalid_time'] % value
+            raise exceptions.ValidationError(msg)
+
+        msg = self.error_messages['invalid'] % value
+        raise exceptions.ValidationError(msg)
 
     def pre_save(self, model_instance, add):
         if self.auto_now or (self.auto_now_add and add):
@@ -1229,11 +1219,7 @@ class TimeField(Field):
 
     def value_to_string(self, obj):
         val = self._get_val_from_obj(obj)
-        if val is None:
-            data = ''
-        else:
-            data = str(val.replace(microsecond=0))
-        return data
+        return '' if val is None else val.isoformat()
 
     def formfield(self, **kwargs):
         defaults = {'form_class': forms.TimeField}

+ 1 - 1
django/db/utils.py

@@ -66,7 +66,7 @@ class ConnectionHandler(object):
         if conn['ENGINE'] == 'django.db.backends.' or not conn['ENGINE']:
             conn['ENGINE'] = 'django.db.backends.dummy'
         conn.setdefault('OPTIONS', {})
-        conn.setdefault('TIME_ZONE', settings.TIME_ZONE)
+        conn.setdefault('TIME_ZONE', 'UTC' if settings.USE_TZ else settings.TIME_ZONE)
         for setting in ['NAME', 'USER', 'PASSWORD', 'HOST', 'PORT']:
             conn.setdefault(setting, '')
         for setting in ['TEST_CHARSET', 'TEST_COLLATION', 'TEST_NAME', 'TEST_MIRROR']:

+ 13 - 5
django/forms/fields.py

@@ -17,7 +17,7 @@ except ImportError:
 
 from django.core import validators
 from django.core.exceptions import ValidationError
-from django.forms.util import ErrorList
+from django.forms.util import ErrorList, from_current_timezone, to_current_timezone
 from django.forms.widgets import (TextInput, PasswordInput, HiddenInput,
     MultipleHiddenInput, ClearableFileInput, CheckboxInput, Select,
     NullBooleanSelect, SelectMultiple, DateInput, DateTimeInput, TimeInput,
@@ -409,6 +409,11 @@ class DateTimeField(BaseTemporalField):
         'invalid': _(u'Enter a valid date/time.'),
     }
 
+    def prepare_value(self, value):
+        if isinstance(value, datetime.datetime):
+            value = to_current_timezone(value)
+        return value
+
     def to_python(self, value):
         """
         Validates that the input can be converted to a datetime. Returns a
@@ -417,9 +422,10 @@ class DateTimeField(BaseTemporalField):
         if value in validators.EMPTY_VALUES:
             return None
         if isinstance(value, datetime.datetime):
-            return value
+            return from_current_timezone(value)
         if isinstance(value, datetime.date):
-            return datetime.datetime(value.year, value.month, value.day)
+            result = datetime.datetime(value.year, value.month, value.day)
+            return from_current_timezone(result)
         if isinstance(value, list):
             # Input comes from a SplitDateTimeWidget, for example. So, it's two
             # components: date and time.
@@ -428,7 +434,8 @@ class DateTimeField(BaseTemporalField):
             if value[0] in validators.EMPTY_VALUES and value[1] in validators.EMPTY_VALUES:
                 return None
             value = '%s %s' % tuple(value)
-        return super(DateTimeField, self).to_python(value)
+        result = super(DateTimeField, self).to_python(value)
+        return from_current_timezone(result)
 
     def strptime(self, value, format):
         return datetime.datetime.strptime(value, format)
@@ -979,7 +986,8 @@ class SplitDateTimeField(MultiValueField):
                 raise ValidationError(self.error_messages['invalid_date'])
             if data_list[1] in validators.EMPTY_VALUES:
                 raise ValidationError(self.error_messages['invalid_time'])
-            return datetime.datetime.combine(*data_list)
+            result = datetime.datetime.combine(*data_list)
+            return from_current_timezone(result)
         return None
 
 

+ 31 - 0
django/forms/util.py

@@ -1,6 +1,9 @@
+from django.conf import settings
 from django.utils.html import conditional_escape
 from django.utils.encoding import StrAndUnicode, force_unicode
 from django.utils.safestring import mark_safe
+from django.utils import timezone
+from django.utils.translation import ugettext_lazy as _
 
 # Import ValidationError so that it can be imported from this
 # module to maintain backwards compatibility.
@@ -52,3 +55,31 @@ class ErrorList(list, StrAndUnicode):
     def __repr__(self):
         return repr([force_unicode(e) for e in self])
 
+# Utilities for time zone support in DateTimeField et al.
+
+def from_current_timezone(value):
+    """
+    When time zone support is enabled, convert naive datetimes
+    entered in the current time zone to aware datetimes.
+    """
+    if settings.USE_TZ and value is not None and timezone.is_naive(value):
+        current_timezone = timezone.get_current_timezone()
+        try:
+            return timezone.make_aware(value, current_timezone)
+        except Exception, e:
+            raise ValidationError(_('%(datetime)s couldn\'t be interpreted '
+                                    'in time zone %(current_timezone)s; it '
+                                    'may be ambiguous or it may not exist.')
+                                  % {'datetime': value,
+                                     'current_timezone': current_timezone})
+    return value
+
+def to_current_timezone(value):
+    """
+    When time zone support is enabled, convert aware datetimes
+    to naive dateimes in the current time zone for display.
+    """
+    if settings.USE_TZ and value is not None and timezone.is_aware(value):
+        current_timezone = timezone.get_current_timezone()
+        return timezone.make_naive(value, current_timezone)
+    return value

+ 2 - 1
django/forms/widgets.py

@@ -10,7 +10,7 @@ from itertools import chain
 from urlparse import urljoin
 
 from django.conf import settings
-from django.forms.util import flatatt
+from django.forms.util import flatatt, to_current_timezone
 from django.utils.datastructures import MultiValueDict, MergeDict
 from django.utils.html import escape, conditional_escape
 from django.utils.translation import ugettext, ugettext_lazy
@@ -847,6 +847,7 @@ class SplitDateTimeWidget(MultiWidget):
 
     def decompress(self, value):
         if value:
+            value = to_current_timezone(value)
             return [value.date(), value.time().replace(microsecond=0)]
         return [None, None]
 

+ 6 - 1
django/template/base.py

@@ -18,6 +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 aslocaltime
 
 
 TOKEN_TEXT = 0
@@ -593,6 +594,8 @@ class FilterExpression(object):
                     arg_vals.append(mark_safe(arg))
                 else:
                     arg_vals.append(arg.resolve(context))
+            if getattr(func, 'expects_localtime', False):
+                obj = aslocaltime(obj, context.use_tz)
             if getattr(func, 'needs_autoescape', False):
                 new_obj = func(obj, autoescape=context.autoescape, *arg_vals)
             else:
@@ -853,6 +856,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 = aslocaltime(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
@@ -1077,7 +1081,7 @@ class Library(object):
         elif name is not None and filter_func is not None:
             # register.filter('somename', somefunc)
             self.filters[name] = filter_func
-            for attr in ('is_safe', 'needs_autoescape'):
+            for attr in ('expects_localtime', 'is_safe', 'needs_autoescape'):
                 if attr in flags:
                     value = flags[attr]
                     # set the flag on the filter for FilterExpression.resolve
@@ -1189,6 +1193,7 @@ class Library(object):
                         'autoescape': context.autoescape,
                         'current_app': context.current_app,
                         'use_l10n': context.use_l10n,
+                        'use_tz': context.use_tz,
                     })
                     # Copy across the CSRF token, if present, because
                     # inclusion tags are often used for forms, and we need

+ 8 - 4
django/template/context.py

@@ -83,10 +83,12 @@ class BaseContext(object):
 
 class Context(BaseContext):
     "A stack container for variable context"
-    def __init__(self, dict_=None, autoescape=True, current_app=None, use_l10n=None):
+    def __init__(self, dict_=None, autoescape=True, current_app=None,
+            use_l10n=None, use_tz=None):
         self.autoescape = autoescape
-        self.use_l10n = use_l10n
         self.current_app = current_app
+        self.use_l10n = use_l10n
+        self.use_tz = use_tz
         self.render_context = RenderContext()
         super(Context, self).__init__(dict_)
 
@@ -162,8 +164,10 @@ class RequestContext(Context):
     Additional processors can be specified as a list of callables
     using the "processors" keyword argument.
     """
-    def __init__(self, request, dict=None, processors=None, current_app=None, use_l10n=None):
-        Context.__init__(self, dict, current_app=current_app, use_l10n=use_l10n)
+    def __init__(self, request, dict_=None, processors=None, current_app=None,
+            use_l10n=None, use_tz=None):
+        Context.__init__(self, dict_, current_app=current_app,
+                use_l10n=use_l10n, use_tz=use_tz)
         if processors is None:
             processors = ()
         else:

+ 2 - 0
django/template/debug.py

@@ -3,6 +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 aslocaltime
 
 
 class DebugLexer(Lexer):
@@ -81,6 +82,7 @@ class DebugVariableNode(VariableNode):
     def render(self, context):
         try:
             output = self.filter_expression.resolve(context)
+            output = aslocaltime(output, use_tz=context.use_tz)
             output = localize(output, use_l10n=context.use_l10n)
             output = force_unicode(output)
         except UnicodeDecodeError:

+ 2 - 2
django/template/defaultfilters.py

@@ -692,7 +692,7 @@ def get_digit(value, arg):
 # DATES           #
 ###################
 
-@register.filter(is_safe=False)
+@register.filter(expects_localtime=True, is_safe=False)
 def date(value, arg=None):
     """Formats a date according to the given format."""
     if not value:
@@ -707,7 +707,7 @@ def date(value, arg=None):
         except AttributeError:
             return ''
 
-@register.filter(is_safe=False)
+@register.filter(expects_localtime=True, is_safe=False)
 def time(value, arg=None):
     """Formats a time according to the given format."""
     if value in (None, u''):

+ 191 - 0
django/templatetags/tz.py

@@ -0,0 +1,191 @@
+from __future__ import with_statement
+
+from datetime import datetime, tzinfo
+
+try:
+    import pytz
+except ImportError:
+    pytz = None
+
+from django.template import Node
+from django.template import TemplateSyntaxError, Library
+from django.utils import timezone
+
+register = Library()
+
+# HACK: datetime is an old-style class, create a new-style equivalent
+# so we can define additional attributes.
+class datetimeobject(datetime, object):
+    pass
+
+
+# Template filters
+
+@register.filter
+def aslocaltime(value):
+    """
+    Converts a datetime to local time in the active time zone.
+
+    This only makes sense within a {% localtime off %} block.
+    """
+    return astimezone(value, timezone.get_current_timezone())
+
+@register.filter
+def asutc(value):
+    """
+    Converts a datetime to UTC.
+    """
+    return astimezone(value, timezone.utc)
+
+@register.filter
+def astimezone(value, arg):
+    """
+    Converts a datetime to local time in a given time zone.
+
+    The argument must be an instance of a tzinfo subclass or a time zone name.
+    If it is a time zone name, pytz is required.
+
+    Naive datetimes are assumed to be in local time in the default time zone.
+    """
+    if not isinstance(value, datetime):
+        return ''
+
+    # Obtain a timezone-aware datetime
+    try:
+        if timezone.is_naive(value):
+            default_timezone = timezone.get_default_timezone()
+            value = timezone.make_aware(value, default_timezone)
+    # Filters must never raise exceptions, and pytz' exceptions inherit
+    # Exception directly, not a specific subclass. So catch everything.
+    except Exception:
+        return ''
+
+    # Obtain a tzinfo instance
+    if isinstance(arg, tzinfo):
+        tz = arg
+    elif isinstance(arg, basestring) and pytz is not None:
+        try:
+            tz = pytz.timezone(arg)
+        except pytz.UnknownTimeZoneError:
+            return ''
+    else:
+        return ''
+
+    # Convert and prevent further conversion
+    result = value.astimezone(tz)
+    if hasattr(tz, 'normalize'):
+        # available for pytz time zones
+        result = tz.normalize(result)
+
+    # HACK: the convert_to_local_time flag will prevent
+    #       automatic conversion of the value to local time.
+    result = datetimeobject(result.year, result.month, result.day,
+                            result.hour, result.minute, result.second,
+                            result.microsecond, result.tzinfo)
+    result.convert_to_local_time = False
+    return result
+
+
+# Template tags
+
+class LocalTimeNode(Node):
+    """
+    Template node class used by ``localtime_tag``.
+    """
+    def __init__(self, nodelist, use_tz):
+        self.nodelist = nodelist
+        self.use_tz = use_tz
+
+    def render(self, context):
+        old_setting = context.use_tz
+        context.use_tz = self.use_tz
+        output = self.nodelist.render(context)
+        context.use_tz = old_setting
+        return output
+
+class TimezoneNode(Node):
+    """
+    Template node class used by ``timezone_tag``.
+    """
+    def __init__(self, nodelist, tz):
+        self.nodelist = nodelist
+        self.tz = tz
+
+    def render(self, context):
+        with timezone.override(self.tz.resolve(context)):
+            output = self.nodelist.render(context)
+        return output
+
+class GetCurrentTimezoneNode(Node):
+    """
+    Template node class used by ``get_current_timezone_tag``.
+    """
+    def __init__(self, variable):
+        self.variable = variable
+
+    def render(self, context):
+        context[self.variable] = timezone.get_current_timezone_name()
+        return ''
+
+@register.tag('localtime')
+def localtime_tag(parser, token):
+    """
+    Forces or prevents conversion of datetime objects to local time,
+    regardless of the value of ``settings.USE_TZ``.
+
+    Sample usage::
+
+        {% localtime off %}{{ value_in_utc }}{% endlocaltime %}
+
+    """
+    bits = token.split_contents()
+    if len(bits) == 1:
+        use_tz = True
+    elif len(bits) > 2 or bits[1] not in ('on', 'off'):
+        raise TemplateSyntaxError("%r argument should be 'on' or 'off'" % bits[0])
+    else:
+        use_tz = bits[1] == 'on'
+    nodelist = parser.parse(('endlocaltime',))
+    parser.delete_first_token()
+    return LocalTimeNode(nodelist, use_tz)
+
+@register.tag('timezone')
+def timezone_tag(parser, token):
+    """
+    Enables a given time zone just for this block.
+
+    The ``timezone`` argument must be an instance of a ``tzinfo`` subclass, a
+    time zone name, or ``None``. If is it a time zone name, pytz is required.
+    If it is ``None``, the default time zone is used within the block.
+
+    Sample usage::
+
+        {% timezone "Europe/Paris" %}
+            It is {{ now }} in Paris.
+        {% endtimezone %}
+
+    """
+    bits = token.split_contents()
+    if len(bits) != 2:
+        raise TemplateSyntaxError("'%s' takes one argument (timezone)" % bits[0])
+    tz = parser.compile_filter(bits[1])
+    nodelist = parser.parse(('endtimezone',))
+    parser.delete_first_token()
+    return TimezoneNode(nodelist, tz)
+
+@register.tag("get_current_timezone")
+def get_current_timezone_tag(parser, token):
+    """
+    Stores the name of the current time zone in the context.
+
+    Usage::
+
+        {% get_current_timezone as TIME_ZONE %}
+
+    This will fetch the currently active time zone and put its name
+    into the ``TIME_ZONE`` context variable.
+    """
+    args = token.contents.split()
+    if len(args) != 3 or args[1] != 'as':
+        raise TemplateSyntaxError("'get_current_timezone' requires 'as variable' (got %r)" % args)
+    return GetCurrentTimezoneNode(args[2])

+ 4 - 1
django/utils/cache.py

@@ -25,6 +25,7 @@ from django.conf import settings
 from django.core.cache import get_cache
 from django.utils.encoding import smart_str, iri_to_uri
 from django.utils.http import http_date
+from django.utils.timezone import get_current_timezone_name
 from django.utils.translation import get_language
 
 cc_delim_re = re.compile(r'\s*,\s*')
@@ -157,12 +158,14 @@ def has_vary_header(response, header_query):
     return header_query.lower() in existing_headers
 
 def _i18n_cache_key_suffix(request, cache_key):
-    """If enabled, returns the cache key ending with a locale."""
+    """If necessary, adds the current locale or time zone to the cache key."""
     if settings.USE_I18N or settings.USE_L10N:
         # first check if LocaleMiddleware or another middleware added
         # LANGUAGE_CODE to request, then fall back to the active language
         # which in turn can also fall back to settings.LANGUAGE_CODE
         cache_key += '.%s' % getattr(request, 'LANGUAGE_CODE', get_language())
+    if settings.USE_TZ:
+        cache_key += '.%s' % get_current_timezone_name()
     return cache_key
 
 def _generate_cache_key(request, method, headerlist, key_prefix):

+ 10 - 4
django/utils/dateformat.py

@@ -14,10 +14,13 @@ Usage:
 import re
 import time
 import calendar
+import datetime
+
 from django.utils.dates import MONTHS, MONTHS_3, MONTHS_ALT, MONTHS_AP, WEEKDAYS, WEEKDAYS_ABBR
 from django.utils.tzinfo import LocalTimezone
 from django.utils.translation import ugettext as _
 from django.utils.encoding import force_unicode
+from django.utils.timezone import is_aware, is_naive
 
 re_formatchars = re.compile(r'(?<!\\)([aAbBcdDEfFgGhHiIjlLmMnNOPrsStTUuwWyYzZ])')
 re_escaped = re.compile(r'\\(.)')
@@ -115,9 +118,12 @@ class DateFormat(TimeFormat):
     def __init__(self, dt):
         # Accepts either a datetime or date object.
         self.data = dt
-        self.timezone = getattr(dt, 'tzinfo', None)
-        if hasattr(self.data, 'hour') and not self.timezone:
-            self.timezone = LocalTimezone(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'"
@@ -218,7 +224,7 @@ class DateFormat(TimeFormat):
 
     def U(self):
         "Seconds since the Unix epoch (January 1 1970 00:00:00 GMT)"
-        if getattr(self.data, 'tzinfo', None):
+        if isinstance(self.data, datetime.datetime) and is_aware(self.data):
             return int(calendar.timegm(self.data.utctimetuple()))
         else:
             return int(time.mktime(self.data.timetuple()))

+ 93 - 0
django/utils/dateparse.py

@@ -0,0 +1,93 @@
+"""Functions to parse datetime objects."""
+
+# We're using regular expressions rather than time.strptime because:
+# - they provide both validation and parsing,
+# - they're more flexible for datetimes,
+# - the date/datetime/time constructors produce friendlier error messages.
+
+
+import datetime
+import re
+
+from django.utils.timezone import utc
+from django.utils.tzinfo import FixedOffset
+
+
+date_re = re.compile(
+        r'(?P<year>\d{4})-(?P<month>\d{1,2})-(?P<day>\d{1,2})$'
+)
+
+
+datetime_re = re.compile(
+        r'(?P<year>\d{4})-(?P<month>\d{1,2})-(?P<day>\d{1,2})'
+        r'[T ](?P<hour>\d{1,2}):(?P<minute>\d{1,2})'
+        r'(?::(?P<second>\d{1,2})(?:\.(?P<microsecond>\d{1,6})\d{0,6})?)?'
+        r'(?P<tzinfo>Z|[+-]\d{1,2}:\d{1,2})?$'
+)
+
+
+time_re = re.compile(
+        r'(?P<hour>\d{1,2}):(?P<minute>\d{1,2})'
+        r'(?::(?P<second>\d{1,2})(?:\.(?P<microsecond>\d{1,6})\d{0,6})?)?'
+)
+
+
+def parse_date(value):
+    """Parse a string and return a datetime.date.
+
+    Raise ValueError if the input is well formatted but not a valid date.
+    Return None if the input isn't well formatted.
+    """
+    match = date_re.match(value)
+    if match:
+        kw = dict((k, int(v)) for k, v in match.groupdict().iteritems())
+        return datetime.date(**kw)
+
+
+def parse_time(value):
+    """Parse a string and return a datetime.time.
+
+    This function doesn't support time zone offsets.
+
+    Sub-microsecond precision is accepted, but ignored.
+
+    Raise ValueError if the input is well formatted but not a valid time.
+    Return None if the input isn't well formatted, in particular if it
+    contains an offset.
+    """
+    match = time_re.match(value)
+    if match:
+        kw = match.groupdict()
+        if kw['microsecond']:
+            kw['microsecond'] = kw['microsecond'].ljust(6, '0')
+        kw = dict((k, int(v)) for k, v in kw.iteritems() if v is not None)
+        return datetime.time(**kw)
+
+
+def parse_datetime(value):
+    """Parse a string and return a datetime.datetime.
+
+    This function supports time zone offsets. When the input contains one,
+    the output uses an instance of FixedOffset as tzinfo.
+
+    Sub-microsecond precision is accepted, but ignored.
+
+    Raise ValueError if the input is well formatted but not a valid datetime.
+    Return None if the input isn't well formatted.
+    """
+    match = datetime_re.match(value)
+    if match:
+        kw = match.groupdict()
+        if kw['microsecond']:
+            kw['microsecond'] = kw['microsecond'].ljust(6, '0')
+        tzinfo = kw.pop('tzinfo')
+        if tzinfo == 'Z':
+            tzinfo = utc
+        elif tzinfo is not None:
+            offset = 60 * int(tzinfo[1:3]) + int(tzinfo[4:6])
+            if tzinfo[0] == '-':
+                offset = -offset
+            tzinfo = FixedOffset(offset)
+        kw = dict((k, int(v)) for k, v in kw.iteritems() if v is not None)
+        kw['tzinfo'] = tzinfo
+        return datetime.datetime(**kw)

+ 3 - 2
django/utils/feedgenerator.py

@@ -28,6 +28,7 @@ import urlparse
 from django.utils.xmlutils import SimplerXMLGenerator
 from django.utils.encoding import force_unicode, iri_to_uri
 from django.utils import datetime_safe
+from django.utils.timezone import is_aware
 
 def rfc2822_date(date):
     # We can't use strftime() because it produces locale-dependant results, so
@@ -40,7 +41,7 @@ def rfc2822_date(date):
     dow = days[date.weekday()]
     month = months[date.month - 1]
     time_str = date.strftime('%s, %%d %s %%Y %%H:%%M:%%S ' % (dow, month))
-    if date.tzinfo:
+    if is_aware(date):
         offset = date.tzinfo.utcoffset(date)
         timezone = (offset.days * 24 * 60) + (offset.seconds // 60)
         hour, minute = divmod(timezone, 60)
@@ -51,7 +52,7 @@ def rfc2822_date(date):
 def rfc3339_date(date):
     # Support datetime objects older than 1900
     date = datetime_safe.new_datetime(date)
-    if date.tzinfo:
+    if is_aware(date):
         time_str = date.strftime('%Y-%m-%dT%H:%M:%S')
         offset = date.tzinfo.utcoffset(date)
         timezone = (offset.days * 24 * 60) + (offset.seconds // 60)

+ 5 - 11
django/utils/timesince.py

@@ -1,6 +1,6 @@
 import datetime
 
-from django.utils.tzinfo import LocalTimezone
+from django.utils.timezone import is_aware, utc
 from django.utils.translation import ungettext, ugettext
 
 def timesince(d, now=None):
@@ -31,13 +31,10 @@ def timesince(d, now=None):
         now = datetime.datetime(now.year, now.month, now.day)
 
     if not now:
-        if d.tzinfo:
-            now = datetime.datetime.now(LocalTimezone(d))
-        else:
-            now = datetime.datetime.now()
+        now = datetime.datetime.now(utc if is_aware(d) else None)
 
-    # ignore microsecond part of 'd' since we removed it from 'now'
-    delta = now - (d - datetime.timedelta(0, 0, d.microsecond))
+    delta = now - d
+    # ignore microseconds
     since = delta.days * 24 * 60 * 60 + delta.seconds
     if since <= 0:
         # d is in the future compared to now, stop processing.
@@ -61,8 +58,5 @@ def timeuntil(d, now=None):
     the given time.
     """
     if not now:
-        if getattr(d, 'tzinfo', None):
-            now = datetime.datetime.now(LocalTimezone(d))
-        else:
-            now = datetime.datetime.now()
+        now = datetime.datetime.now(utc if is_aware(d) else None)
     return timesince(now, d)

+ 266 - 0
django/utils/timezone.py

@@ -0,0 +1,266 @@
+"""Timezone helper functions.
+
+This module uses pytz when it's available and fallbacks when it isn't.
+"""
+
+from datetime import datetime, timedelta, tzinfo
+from threading import local
+import time as _time
+
+try:
+    import pytz
+except ImportError:
+    pytz = None
+
+from django.conf import settings
+
+__all__ = [
+    'utc', 'get_default_timezone', 'get_current_timezone',
+    'activate', 'deactivate', 'override',
+    'aslocaltime', 'isnaive',
+]
+
+
+# UTC and local time zones
+
+ZERO = timedelta(0)
+
+class UTC(tzinfo):
+    """
+    UTC implementation taken from Python's docs.
+
+    Used only when pytz isn't available.
+    """
+
+    def utcoffset(self, dt):
+        return ZERO
+
+    def tzname(self, dt):
+        return "UTC"
+
+    def dst(self, dt):
+        return ZERO
+
+class LocalTimezone(tzinfo):
+    """
+    Local time implementation taken from Python's docs.
+
+    Used only when pytz isn't available, and most likely inaccurate. If you're
+    having trouble with this class, don't waste your time, just install pytz.
+    """
+
+    def __init__(self):
+        # This code is moved in __init__ to execute it as late as possible
+        # See get_default_timezone().
+        self.STDOFFSET = timedelta(seconds=-_time.timezone)
+        if _time.daylight:
+            self.DSTOFFSET = timedelta(seconds=-_time.altzone)
+        else:
+            self.DSTOFFSET = self.STDOFFSET
+        self.DSTDIFF = self.DSTOFFSET - self.STDOFFSET
+        tzinfo.__init__(self)
+
+    def utcoffset(self, dt):
+        if self._isdst(dt):
+            return self.DSTOFFSET
+        else:
+            return self.STDOFFSET
+
+    def dst(self, dt):
+        if self._isdst(dt):
+            return self.DSTDIFF
+        else:
+            return ZERO
+
+    def tzname(self, dt):
+        return _time.tzname[self._isdst(dt)]
+
+    def _isdst(self, dt):
+        tt = (dt.year, dt.month, dt.day,
+              dt.hour, dt.minute, dt.second,
+              dt.weekday(), 0, 0)
+        stamp = _time.mktime(tt)
+        tt = _time.localtime(stamp)
+        return tt.tm_isdst > 0
+
+
+utc = pytz.utc if pytz else UTC()
+"""UTC time zone as a tzinfo instance."""
+
+# In order to avoid accessing the settings at compile time,
+# wrap the expression in a function and cache the result.
+# If you change settings.TIME_ZONE in tests, reset _localtime to None.
+_localtime = None
+
+def get_default_timezone():
+    """
+    Returns the default time zone as a tzinfo instance.
+
+    This is the time zone defined by settings.TIME_ZONE.
+
+    See also :func:`get_current_timezone`.
+    """
+    global _localtime
+    if _localtime is None:
+        tz = settings.TIME_ZONE
+        _localtime = pytz.timezone(tz) if pytz else LocalTimezone()
+    return _localtime
+
+# This function exists for consistency with get_current_timezone_name
+def get_default_timezone_name():
+    """
+    Returns the name of the default time zone.
+    """
+    return _get_timezone_name(get_default_timezone())
+
+_active = local()
+
+def get_current_timezone():
+    """
+    Returns the currently active time zone as a tzinfo instance.
+    """
+    return getattr(_active, "value", get_default_timezone())
+
+def get_current_timezone_name():
+    """
+    Returns the name of the currently active time zone.
+    """
+    return _get_timezone_name(get_current_timezone())
+
+def _get_timezone_name(timezone):
+    """
+    Returns the name of ``timezone``.
+    """
+    try:
+        # for pytz timezones
+        return timezone.zone
+    except AttributeError:
+        # for regular tzinfo objects
+        local_now = datetime.now(timezone)
+        return timezone.tzname(local_now)
+
+# Timezone selection functions.
+
+# These functions don't change os.environ['TZ'] and call time.tzset()
+# because it isn't thread safe.
+
+def activate(timezone):
+    """
+    Sets the time zone for the current thread.
+
+    The ``timezone`` argument must be an instance of a tzinfo subclass or a
+    time zone name. If it is a time zone name, pytz is required.
+    """
+    if isinstance(timezone, tzinfo):
+        _active.value = timezone
+    elif isinstance(timezone, basestring) and pytz is not None:
+        _active.value = pytz.timezone(timezone)
+    else:
+        raise ValueError("Invalid timezone: %r" % timezone)
+
+def deactivate():
+    """
+    Unsets the time zone for the current thread.
+
+    Django will then use the time zone defined by settings.TIME_ZONE.
+    """
+    if hasattr(_active, "value"):
+        del _active.value
+
+class override(object):
+    """
+    Temporarily set the time zone for the current thread.
+
+    This is a context manager that uses ``~django.utils.timezone.activate()``
+    to set the timezone on entry, and restores the previously active timezone
+    on exit.
+
+    The ``timezone`` argument must be an instance of a ``tzinfo`` subclass, a
+    time zone name, or ``None``. If is it a time zone name, pytz is required.
+    If it is ``None``, Django enables the default time zone.
+    """
+    def __init__(self, timezone):
+        self.timezone = timezone
+        self.old_timezone = getattr(_active, 'value', None)
+
+    def __enter__(self):
+        if self.timezone is None:
+            deactivate()
+        else:
+            activate(self.timezone)
+
+    def __exit__(self, exc_type, exc_value, traceback):
+        if self.old_timezone is not None:
+            _active.value = self.old_timezone
+        else:
+            del _active.value
+
+
+# Utilities
+
+def aslocaltime(value, use_tz=None):
+    """
+    Checks if value is a datetime and converts it to local time if necessary.
+
+    If use_tz is provided and is not None, that will force the value to
+    be converted (or not), overriding the value of settings.USE_TZ.
+    """
+    if (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
+
+def now():
+    """
+    Returns an aware or naive datetime.datetime, depending on settings.USE_TZ.
+    """
+    if settings.USE_TZ:
+        # timeit shows that datetime.now(tz=utc) is 24% slower
+        return datetime.utcnow().replace(tzinfo=utc)
+    else:
+        return datetime.now()
+
+def is_aware(value):
+    """
+    Determines if a given datetime.datetime is aware.
+
+    The logic is described in Python's docs:
+    http://docs.python.org/library/datetime.html#datetime.tzinfo
+    """
+    return value.tzinfo is not None and value.tzinfo.utcoffset(value) is not None
+
+def is_naive(value):
+    """
+    Determines if a given datetime.datetime is naive.
+
+    The logic is described in Python's docs:
+    http://docs.python.org/library/datetime.html#datetime.tzinfo
+    """
+    return value.tzinfo is None or value.tzinfo.utcoffset(value) is None
+
+def make_aware(value, timezone):
+    """
+    Makes a naive datetime.datetime in a given time zone aware.
+    """
+    if hasattr(timezone, 'localize'):
+        # available for pytz time zones
+        return timezone.localize(value, is_dst=None)
+    else:
+        # may be wrong around DST changes
+        return value.replace(tzinfo=timezone)
+
+def make_naive(value, timezone):
+    """
+    Makes an aware datetime.datetime naive in a given time zone.
+    """
+    value = value.astimezone(timezone)
+    if hasattr(timezone, 'normalize'):
+        # available for pytz time zones
+        return timezone.normalize(value)
+    return value.replace(tzinfo=None)

+ 19 - 0
django/utils/tzinfo.py

@@ -2,8 +2,14 @@
 
 import time
 from datetime import timedelta, tzinfo
+
 from django.utils.encoding import smart_unicode, smart_str, DEFAULT_LOCALE_ENCODING
 
+# Python's doc say: "A tzinfo subclass must have an __init__() method that can
+# be called with no arguments". FixedOffset and LocalTimezone don't honor this
+# requirement. Defining __getinitargs__ is sufficient to fix copy/deepcopy as
+# well as pickling/unpickling.
+
 class FixedOffset(tzinfo):
     "Fixed offset in minutes east from UTC."
     def __init__(self, offset):
@@ -19,6 +25,9 @@ class FixedOffset(tzinfo):
     def __repr__(self):
         return self.__name
 
+    def __getinitargs__(self):
+        return self.__offset,
+
     def utcoffset(self, dt):
         return self.__offset
 
@@ -28,15 +37,25 @@ class FixedOffset(tzinfo):
     def dst(self, dt):
         return timedelta(0)
 
+# This implementation is used for display purposes. It uses an approximation
+# for DST computations on dates >= 2038.
+
+# A similar implementation exists in django.utils.timezone. It's used for
+# timezone support (when USE_TZ = True) and focuses on correctness.
+
 class LocalTimezone(tzinfo):
     "Proxy timezone information from time module."
     def __init__(self, dt):
         tzinfo.__init__(self)
+        self.__dt = dt
         self._tzname = self.tzname(dt)
 
     def __repr__(self):
         return smart_str(self._tzname)
 
+    def __getinitargs__(self):
+        return self.__dt,
+
     def utcoffset(self, dt):
         if self._isdst(dt):
             return timedelta(seconds=-time.altzone)

+ 25 - 0
docs/howto/custom-template-tags.txt

@@ -347,6 +347,31 @@ function; this syntax is deprecated.
         return mark_safe(result)
     initial_letter_filter.needs_autoescape = True
 
+.. _filters-timezones:
+
+Filters and time zones
+~~~~~~~~~~~~~~~~~~~~~~
+
+.. versionadded:: 1.4
+
+If you write a custom filter that operates on :class:`~datetime.datetime`
+objects, you'll usually register it with the ``expects_localtime`` flag set to
+``True``:
+
+.. code-block:: python
+
+    @register.filter(expects_localtime=True)
+    def businesshours(value):
+        try:
+            return 9 <= value.hour < 17
+        except AttributeError:
+            return ''
+
+When this flag is set, if the first argument to your filter is a time zone
+aware datetime, Django will convert it to the current time zone before passing
+to your filter when appropriate, according to :ref:`rules for time zones
+conversions in templates <time-zones-in-templates>`.
+
 Writing custom template tags
 ----------------------------
 

+ 13 - 0
docs/ref/models/querysets.txt

@@ -546,6 +546,12 @@ Examples::
     >>> Entry.objects.filter(headline__contains='Lennon').dates('pub_date', 'day')
     [datetime.datetime(2005, 3, 20)]
 
+.. warning::
+
+    When :doc:`time zone support </topics/i18n/timezones>` is enabled, Django
+    uses UTC in the database connection, which means the aggregation is
+    performed in UTC. This is a known limitation of the current implementation.
+
 none
 ~~~~
 
@@ -1953,6 +1959,13 @@ Note this will match any record with a ``pub_date`` that falls on a Monday (day
 2 of the week), regardless of the month or year in which it occurs. Week days
 are indexed with day 1 being Sunday and day 7 being Saturday.
 
+.. warning::
+
+    When :doc:`time zone support </topics/i18n/timezones>` is enabled, Django
+    uses UTC in the database connection, which means the ``year``, ``month``,
+    ``day`` and ``week_day`` lookups are performed in UTC. This is a known
+    limitation of the current implementation.
+
 .. fieldlookup:: isnull
 
 isnull

+ 42 - 12
docs/ref/settings.txt

@@ -1810,6 +1810,7 @@ Default::
     "django.core.context_processors.i18n",
     "django.core.context_processors.media",
     "django.core.context_processors.static",
+    "django.core.context_processors.tz",
     "django.contrib.messages.context_processors.messages")
 
 A tuple of callables that are used to populate the context in ``RequestContext``.
@@ -1830,6 +1831,10 @@ of items to be merged into the context.
     The ``django.core.context_processors.static`` context processor
     was added in this release.
 
+.. versionadded:: 1.4
+    The ``django.core.context_processors.tz`` context processor
+    was added in this release.
+
 .. setting:: TEMPLATE_DEBUG
 
 TEMPLATE_DEBUG
@@ -1971,6 +1976,9 @@ Default: ``'America/Chicago'``
 .. versionchanged:: 1.2
    ``None`` was added as an allowed value.
 
+.. versionchanged:: 1.4
+   The meaning of this setting now depends on the value of :setting:`USE_TZ`.
+
 A string representing the time zone for this installation, or
 ``None``. `See available choices`_. (Note that list of available
 choices lists more than one on the same line; you'll want to use just
@@ -1978,16 +1986,19 @@ one of the choices for a given time zone. For instance, one line says
 ``'Europe/London GB GB-Eire'``, but you should use the first bit of
 that -- ``'Europe/London'`` -- as your :setting:`TIME_ZONE` setting.)
 
-Note that this is the time zone to which Django will convert all
-dates/times -- not necessarily the timezone of the server. For
-example, one server may serve multiple Django-powered sites, each with
-a separate time-zone setting.
+Note that this isn't necessarily the timezone of the server. For example, one
+server may serve multiple Django-powered sites, each with a separate time zone
+setting.
+
+When :setting:`USE_TZ` is ``False``, this is the time zone in which Django will
+store all datetimes. When :setting:`USE_TZ` is ``True``, this is the default
+time zone that Django will use to display datetimes in templates and to
+interpret datetimes entered in forms.
 
-Normally, Django sets the ``os.environ['TZ']`` variable to the time
-zone you specify in the :setting:`TIME_ZONE` setting. Thus, all your views
-and models will automatically operate in the correct time zone.
-However, Django won't set the ``TZ`` environment variable under the
-following conditions:
+Django sets the ``os.environ['TZ']`` variable to the time zone you specify in
+the :setting:`TIME_ZONE` setting. Thus, all your views and models will
+automatically operate in this time zone. However, Django won't set the ``TZ``
+environment variable under the following conditions:
 
 * If you're using the manual configuration option as described in
   :ref:`manually configuring settings
@@ -2004,7 +2015,6 @@ to ensure your processes are running in the correct environment.
     environment. If you're running Django on Windows, this variable
     must be set to match the system timezone.
 
-
 .. _See available choices: http://www.postgresql.org/docs/8.1/static/datetime-keywords.html#DATETIME-TIMEZONE-SET-TABLE
 
 .. setting:: URL_VALIDATOR_USER_AGENT
@@ -2043,7 +2053,7 @@ This provides an easy way to turn it off, for performance. If this is set to
 ``False``, Django will make some optimizations so as not to load the
 translation machinery.
 
-See also :setting:`USE_L10N`
+See also :setting:`LANGUAGE_CODE`, :setting:`USE_L10N` and :setting:`USE_TZ`.
 
 .. setting:: USE_L10N
 
@@ -2058,7 +2068,7 @@ A boolean that specifies if localized formatting of data will be enabled by
 default or not. If this is set to ``True``, e.g. Django will display numbers and
 dates using the format of the current locale.
 
-See also :setting:`USE_I18N` and :setting:`LANGUAGE_CODE`
+See also :setting:`LANGUAGE_CODE`, :setting:`USE_I18N` and :setting:`USE_TZ`.
 
 .. note::
 
@@ -2082,6 +2092,26 @@ When :setting:`USE_L10N` is set to ``True`` and if this is also set to
 See also :setting:`DECIMAL_SEPARATOR`, :setting:`NUMBER_GROUPING` and
 :setting:`THOUSAND_SEPARATOR`.
 
+.. setting:: USE_TZ
+
+USE_TZ
+------
+
+.. versionadded:: 1.4
+
+Default: ``False``
+
+A boolean that specifies if datetimes will be timezone-aware by default or not.
+If this is set to ``True``, Django will use timezone-aware datetimes internally.
+Otherwise, Django will use naive datetimes in local time.
+
+See also :setting:`TIME_ZONE`, :setting:`USE_I18N` and :setting:`USE_L10N`.
+
+.. note::
+    The default :file:`settings.py` file created by
+    :djadmin:`django-admin.py startproject <startproject>` includes
+    ``USE_TZ = True`` for convenience.
+
 .. setting:: USE_X_FORWARDED_HOST
 
 USE_X_FORWARDED_HOST

+ 39 - 24
docs/ref/templates/builtins.txt

@@ -2318,8 +2318,45 @@ Value       Argument                Outputs
                                     if no mapping for None is given)
 ==========  ======================  ==================================
 
-Other tags and filter libraries
--------------------------------
+Internationalization tags and filters
+-------------------------------------
+
+Django provides template tags and filters to control each aspect of
+`internationalization </topics/i18n/index>`_ in templates. They allow for
+granular control of translations, formatting, and time zone conversions.
+
+i18n
+^^^^
+
+This library allows specifying translatable text in templates.
+To enable it, set :setting:`USE_I18N` to ``True``, then load it with
+``{% load i18n %}``.
+
+See :ref:`specifying-translation-strings-in-template-code`.
+
+l10n
+^^^^
+
+This library provides control over the localization of values in templates.
+You only need to load the library using ``{% load l10n %}``, but you'll often
+set :setting:`USE_L10N` to ``True`` so that localization is active by default.
+
+See :ref:`topic-l10n-templates`.
+
+tz
+^^
+
+.. versionadded:: 1.4
+
+This library provides control over time zone conversions in templates.
+Like ``l10n``, you only need to load the library using ``{% load tz %}``,
+but you'll usually also set :setting:`USE_TZ` to ``True`` so that conversion
+to local time happens by default.
+
+See :ref:`time-zones-in-templates`.
+
+Other tags and filters libraries
+--------------------------------
 
 Django comes with a couple of other template-tag libraries that you have to
 enable explicitly in your :setting:`INSTALLED_APPS` setting and enable in your
@@ -2348,28 +2385,6 @@ django.contrib.webdesign
 A collection of template tags that can be useful while designing a Web site,
 such as a generator of Lorem Ipsum text. See :doc:`/ref/contrib/webdesign`.
 
-i18n
-^^^^
-
-Provides a couple of templatetags that allow specifying translatable text in
-Django templates. It is slightly different from the libraries described
-above because you don't need to add any application to the
-:setting:`INSTALLED_APPS` setting but rather set :setting:`USE_I18N` to True,
-then loading it with ``{% load i18n %}``.
-
-See :ref:`specifying-translation-strings-in-template-code`.
-
-l10n
-^^^^
-
-Provides a couple of templatetags that allow control over the localization of
-values in Django templates. It is slightly different from the libraries
-described above because you don't need to add any application to the
-:setting:`INSTALLED_APPS`; you only need to load the library using
-``{% load l10n %}``.
-
-See :ref:`topic-l10n-templates`.
-
 static
 ^^^^^^
 

+ 125 - 0
docs/ref/utils.txt

@@ -131,6 +131,41 @@ results. Instead do::
 
     SortedDict([('b', 1), ('a', 2), ('c', 3)])
 
+``django.utils.dateparse``
+==========================
+
+.. versionadded:: 1.4
+
+.. module:: django.utils.dateparse
+   :synopsis: Functions to parse datetime objects.
+
+The functions defined in this module share the following properties:
+
+- They raise :exc:`ValueError` if their input is well formatted but isn't a
+  valid date or time.
+- They return ``None`` if it isn't well formatted at all.
+- They accept up to picosecond resolution in input, but they truncate it to
+  microseconds, since that's what Python supports.
+
+.. function:: parse_date(value)
+
+    Parses a string and returns a :class:`datetime.date`.
+
+.. function:: parse_time(value)
+
+    Parses a string and returns a :class:`datetime.time`.
+
+    UTC offsets aren't supported; if ``value`` describes one, the result is
+    ``None``.
+
+.. function:: parse_datetime(value)
+
+    Parses a string and returns a :class:`datetime.datetime`.
+
+    UTC offsets are supported; if ``value`` describes one, the result's
+    ``tzinfo`` attribute is a :class:`~django.utils.tzinfo.FixedOffset`
+    instance.
+
 ``django.utils.encoding``
 =========================
 
@@ -573,6 +608,96 @@ For a complete discussion on the usage of the following see the
     so by translating the Django translation tags into standard gettext function
     invocations.
 
+.. _time-zone-selection-functions:
+
+``django.utils.timezone``
+=========================
+
+.. versionadded:: 1.4
+
+.. module:: django.utils.timezone
+    :synopsis: Timezone support.
+
+.. data:: utc
+
+    :class:`~datetime.tzinfo` instance that represents UTC.
+
+.. function:: get_default_timezone()
+
+    Returns a :class:`~datetime.tzinfo` instance that represents the
+    :ref:`default time zone <default-current-time-zone>`.
+
+.. function:: get_default_timezone_name()
+
+    Returns the name of the :ref:`default time zone
+    <default-current-time-zone>`.
+
+.. function:: get_current_timezone()
+
+    Returns a :class:`~datetime.tzinfo` instance that represents the
+    :ref:`current time zone <default-current-time-zone>`.
+
+.. function:: get_current_timezone_name()
+
+    Returns the name of the :ref:`current time zone
+    <default-current-time-zone>`.
+
+.. function:: activate(timezone)
+
+    Sets the :ref:`current time zone <default-current-time-zone>`. The
+    ``timezone`` argument must be an instance of a :class:`~datetime.tzinfo`
+    subclass or, if pytz_ is available, a time zone name.
+
+.. function:: deactivate()
+
+    Unsets the :ref:`current time zone <default-current-time-zone>`.
+
+.. function:: override(timezone)
+
+    This is a Python context manager that sets the :ref:`current time zone
+    <default-current-time-zone>` on entry with :func:`activate()`, and restores
+    the previously active time zone on exit. If the ``timezone`` argument is
+    ``None``, the :ref:`current time zone <default-current-time-zone>` is unset
+    on entry with :func:`deactivate()` instead.
+
+.. function:: aslocaltime(value, use_tz=None)
+
+    This function is used by the template engine to convert datetimes to local
+    time where appropriate.
+
+.. function:: now()
+
+    Returns an aware or naive :class:`~datetime.datetime` that represents the
+    current point in time when :setting:`USE_TZ` is ``True`` or ``False``
+    respectively.
+
+.. function:: is_aware(value)
+
+    Returns ``True`` if ``value`` is aware, ``False`` if it is naive. This
+    function assumes that ``value`` is a :class:`~datetime.datetime`.
+
+.. function:: is_naive(value)
+
+    Returns ``True`` if ``value`` is naive, ``False`` if it is aware. This
+    function assumes that ``value`` is a :class:`~datetime.datetime`.
+
+.. function:: make_aware(value, timezone)
+
+    Returns an aware :class:`~datetime.datetime` that represents the same
+    point in time as ``value`` in ``timezone``, ``value`` being a naive
+    :class:`~datetime.datetime`.
+
+    This function can raise an exception if ``value`` doesn't exist or is
+    ambiguous because of DST transitions.
+
+.. function:: make_naive(value, timezone)
+
+    Returns an naive :class:`~datetime.datetime` that represents in
+    ``timezone``  the same point in time as ``value``, ``value`` being an
+    aware :class:`~datetime.datetime`
+
+.. _pytz: http://pytz.sourceforge.net/
+
 ``django.utils.tzinfo``
 =======================
 

+ 52 - 1
docs/releases/1.4.txt

@@ -409,7 +409,6 @@ If the same code is imported inconsistently (some places with the project
 prefix, some places without it), the imports will need to be cleaned up when
 switching to the new ``manage.py``.
 
-
 Improved WSGI support
 ~~~~~~~~~~~~~~~~~~~~~
 
@@ -427,6 +426,25 @@ callable :djadmin:`runserver` uses.
 (The :djadmin:`runfcgi` management command also internally wraps the WSGI
 callable configured via :setting:`WSGI_APPLICATION`.)
 
+Support for time zones
+~~~~~~~~~~~~~~~~~~~~~~
+
+Django 1.4 adds :ref:`support for time zones <time-zones>`. When it's enabled,
+Django stores date and time information in UTC in the database, uses time
+zone-aware datetime objects internally, and translates them to the end user's
+time zone in templates and forms.
+
+Reasons for using this feature include:
+
+- Customizing date and time display for users around the world.
+- Storing datetimes in UTC for database portability and interoperability.
+  (This argument doesn't apply to PostgreSQL, because it already stores
+  timestamps with time zone information in Django 1.3.)
+- Avoiding data corruption problems around DST transitions.
+
+Time zone support in enabled by default in new projects created with
+:djadmin:`startproject`. If you want to use this feature in an existing
+project, there is a :ref:`migration guide <time-zones-migration-guide>`.
 
 Minor features
 ~~~~~~~~~~~~~~
@@ -616,6 +634,39 @@ immediately raise a 404. Additionally redirects returned by flatpages are now
 permanent (301 status code) to match the behavior of the
 :class:`~django.middleware.common.CommonMiddleware`.
 
+Serialization of :class:`~datetime.datetime` and :class:`~datetime.time`
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+As a consequence of time zone support, and according to the ECMA-262
+specification, some changes were made to the JSON serializer:
+
+- It includes the time zone for aware datetime objects. It raises an exception
+  for aware time objects.
+- It includes milliseconds for datetime and time objects. There is still
+  some precision loss, because Python stores microseconds (6 digits) and JSON
+  only supports milliseconds (3 digits). However, it's better than discarding
+  microseconds entirely.
+
+The XML serializer was also changed to use ISO8601 for datetimes. The letter
+``T`` is used to separate the date part from the time part, instead of a
+space. Time zone information is included in the ``[+-]HH:MM`` format.
+
+The serializers will dump datetimes in fixtures with these new formats. They
+can still load fixtures that use the old format.
+
+``supports_timezone`` changed to ``False`` for SQLite
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The database feature ``supports_timezone`` used to be ``True`` for SQLite.
+Indeed, if you saved an aware datetime object, SQLite stored a string that
+included an UTC offset. However, this offset was ignored when loading the value
+back from the database, which could corrupt the data.
+
+In the context of time zone support, this flag was changed to ``False``, and
+datetimes are now stored without time zone information in SQLite. When
+:setting:`USE_TZ` is ``False``, if you attempt to save an aware datetime
+object, Django raises an exception.
+
 `COMMENTS_BANNED_USERS_GROUP` setting
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 

+ 3 - 1
docs/topics/cache.txt

@@ -502,7 +502,9 @@ cache multilingual sites without having to create the cache key yourself.
 
 .. versionchanged:: 1.4
 
-This also happens when :setting:`USE_L10N` is set to ``True``.
+Cache keys also include the active :term:`language <language code>` when
+:setting:`USE_L10N` is set to ``True`` and the :ref:`current time zone
+<default-current-time-zone>` when :setting:`USE_TZ` is set to ``True``.
 
 __ `Controlling cache: Using other headers`_
 

+ 6 - 4
docs/topics/i18n/index.txt

@@ -8,6 +8,7 @@ Internationalization and localization
 
    translation
    formatting
+   timezones
 
 Overview
 ========
@@ -17,8 +18,8 @@ application to offer its content in languages and formats tailored to the
 audience.
 
 Django has full support for :doc:`translation of text
-</topics/i18n/translation>` and :doc:`formatting of dates, times and numbers
-</topics/i18n/formatting>`.
+</topics/i18n/translation>`, :doc:`formatting of dates, times and numbers
+</topics/i18n/formatting>`, and :doc:`time zones </topics/i18n/timezones>`.
 
 Essentially, Django does two things:
 
@@ -27,8 +28,9 @@ Essentially, Django does two things:
 * It uses these hooks to localize Web apps for particular users according to
   their preferences.
 
-Obviously, translation depends on the target language. Formatting usually
-depends on the target country.
+Obviously, translation depends on the target language, and formatting usually
+depends on the target country. These informations are provided by browsers in
+the ``Accept-Language`` header. However, the time zone isn't readily available.
 
 Definitions
 ===========

+ 429 - 0
docs/topics/i18n/timezones.txt

@@ -0,0 +1,429 @@
+.. _time-zones:
+
+==========
+Time zones
+==========
+
+.. versionadded:: 1.4
+
+Overview
+========
+
+When support for time zones is enabled, Django stores date and time
+information in UTC in the database, uses time zone-aware datetime objects
+internally, and translates them to the end user's time zone in templates and
+forms.
+
+This is handy if your users live in more than one time zone and you want to
+display date and time information according to each user's wall clock. Even if
+your website is available in only one time zone, it's still a good practice to
+store data in UTC in your database. Here is why.
+
+Many countries have a system of daylight saving time (DST), where clocks are
+moved forwards in spring and backwards in autumn. If you're working in local
+time, you're likely to encounter errors twice a year, when the transitions
+happen. pytz' docs discuss `these issues`_ in greater detail. It probably
+doesn't matter for your blog, but it's more annoying if you over-bill or
+under-bill your customers by one hour, twice a year, every year. The solution
+to this problem is to use UTC in the code and local time only when
+interacting with end users.
+
+Time zone support is disabled by default. To enable it, set :setting:`USE_TZ =
+True <USE_TZ>` in your settings file. Installing pytz_ is highly recommended,
+but not mandatory.
+
+.. note::
+
+    The default :file:`settings.py` file created by :djadmin:`django-admin.py
+    startproject <startproject>` includes :setting:`USE_TZ = True <USE_TZ>`
+    for convenience.
+
+.. note::
+
+    There is also an independent but related :setting:`USE_L10N` setting that
+    controls if Django should activate format localization. See
+    :doc:`/topics/i18n/formatting` for more details.
+
+Concepts
+========
+
+Naive and aware datetime objects
+--------------------------------
+
+Python's :class:`datetime.datetime` objects have a ``tzinfo`` attribute that
+can be used to store time zone information, represented as an instance of a
+subclass of :class:`datetime.tzinfo`. When this attribute is set and describes
+an offset, a datetime object is **aware**; otherwise, it's **naive**.
+
+You can use :func:`~django.utils.timezone.is_aware` and
+:func:`~django.utils.timezone.is_naive` to determine if datetimes are aware or
+naive.
+
+When time zone support is disabled, Django uses naive datetime objects in local
+time. This is simple and sufficient for many use cases. In this mode, to obtain
+the current time, you would write::
+
+    import datetime
+
+    now = datetime.datetime.now()
+
+When time zone support is enabled, Django uses time zone aware datetime
+objects. If your code creates datetime objects, they should be aware too. In
+this mode, the example above becomes::
+
+    import datetime
+    from django.utils.timezone import utc
+
+    now = datetime.datetime.utcnow().replace(tzinfo=utc)
+
+.. note::
+
+    :mod:`django.utils.timezone` provides a
+    :func:`~django.utils.timezone.now()` function that returns a naive or
+    aware datetime object according to the value of :setting:`USE_TZ`.
+
+.. warning::
+
+    Dealing with aware datetime objects isn't always intuitive. For instance,
+    the ``tzinfo`` argument of the standard datetime constructor doesn't work
+    reliably for time zones with DST. Using UTC is generally safe; if you're
+    using other time zones, you should review `pytz' documentation <pytz>`_
+    carefully.
+
+.. note::
+
+    Python's :class:`datetime.time` objects also feature a ``tzinfo``
+    attribute, and PostgreSQL has a matching ``time with time zone`` type.
+    However, as PostgreSQL's docs put it, this type "exhibits properties which
+    lead to questionable usefulness".
+
+    Django only supports naive time objects and will raise an exception if you
+    attempt to save an aware time object.
+
+.. _naive-datetime-objects:
+
+Interpretation of naive datetime objects
+----------------------------------------
+
+When :setting:`USE_TZ` is ``True``, Django still accepts naive datetime
+objects, in order to preserve backwards-compatibility. It attempts to make them
+aware by interpreting them in the :ref:`default time zone
+<default-current-time-zone>`.
+
+Unfortunately, during DST transitions, some datetimes don't exist or are
+ambiguous. In such situations, pytz_ raises an exception. Other
+:class:`~datetime.tzinfo` implementations, such as the local time zone used as
+a fallback when pytz_ isn't installed, may raise an exception or return
+inaccurate results. That's why you should always create aware datetime objects
+when time zone support is enabled.
+
+In practice, this is rarely an issue. Django gives you aware datetime objects
+in the models and forms, and most often, new datetime objects are created from
+existing ones through :class:`~datetime.timedelta` arithmetic. The only
+datetime that's often created in application code is the current time, and
+:func:`timezone.now() <django.utils.timezone.now>` automatically does the
+right thing.
+
+.. _default-current-time-zone:
+
+Default time zone and current time zone
+---------------------------------------
+
+The **default time zone** is the time zone defined by the :setting:`TIME_ZONE`
+setting.
+
+When pytz_ is available, Django loads the definition of the default time zone
+from the `tz database`_. This is the most accurate solution. Otherwise, it
+relies on the difference between local time and UTC, as reported by the
+operating system, to compute conversions. This is less reliable, especially
+around DST transitions.
+
+The **current time zone** is the time zone that's used for rendering.
+
+You should set it to the end user's actual time zone with
+:func:`~django.utils.timezone.activate`. Otherwise, the default time zone is
+used.
+
+.. note::
+
+    As explained in the documentation of :setting:`TIME_ZONE`, Django sets
+    environment variables so that its process runs in the default time zone.
+    This happens regardless of the value of :setting:`USE_TZ` and of the
+    current time zone.
+
+    When :setting:`USE_TZ` is ``True``, this is useful to preserve
+    backwards-compatibility with applications that still rely on local time.
+    However, :ref:`as explained above <naive-datetime-objects>`, this isn't
+    entirely reliable, and you should always work with aware datetimes in UTC
+    in your own code. For instance, use
+    :meth:`~datetime.datetime.utcfromtimestamp` instead of
+    :meth:`~datetime.datetime.fromtimestamp` -- and don't forget to set
+    ``tzinfo`` to :data:`~django.utils.timezone.utc`.
+
+Selecting the current time zone
+-------------------------------
+
+The current time zone is the equivalent of the current :term:`locale <locale
+name>` for translations. However, there's no equivalent of the
+``Accept-Language`` HTTP header that Django could use to determine the user's
+time zone automatically. Instead, Django provides :ref:`time zone selection
+functions <time-zone-selection-functions>`. Use them to build the time zone
+selection logic that makes sense for you.
+
+Most websites who care about time zones just ask users in which time zone they
+live and store this information in the user's profile. For anonymous users,
+they use the time zone of their primary audience or UTC. pytz_ provides
+helpers, like a list of time zones per country, that you can use to pre-select
+the most likely choices.
+
+Here's an example that stores the current timezone in the session. (It skips
+error handling entirely for the sake of simplicity.)
+
+Add the following middleware to :setting:`MIDDLEWARE_CLASSES`::
+
+    from django.utils import timezone
+
+    class TimezoneMiddleware(object):
+        def process_request(self, request):
+            tz = request.session.get('django_timezone')
+            if tz:
+                timezone.activate(tz)
+
+Create a view that can set the current timezone::
+
+    import pytz
+    from django.shortcuts import redirect, render
+
+    def set_timezone(request):
+        if request.method == 'POST':
+            request.session[session_key] = pytz.timezone(request.POST['timezone'])
+            return redirect('/')
+        else:
+            return render(request, 'template.html', {'timezones': pytz.common_timezones})
+
+Include in :file:`template.html` a form that will ``POST`` to this view:
+
+.. code-block:: html+django
+
+    {% load tz %}{% load url from future %}
+    <form action="{% url 'set_timezone' %}" method="POST">
+        {% csrf_token %}
+        <label for="timezone">Time zone:</label>
+        <select name="timezone">
+            {% for tz in timezones %}
+            <option value="{{ tz }}"{% if tz == TIME_ZONE %} selected="selected"{% endif %}>{{ tz }}</option>
+            {% endfor %}
+        </select>
+        <input type="submit" value="Set" />
+    </form>
+
+Time zone aware input in forms
+==============================
+
+When you enable time zone support, Django interprets datetimes entered in
+forms in the :ref:`current time zone <default-current-time-zone>` and returns
+aware datetime objects in ``cleaned_data``.
+
+If the current time zone raises an exception for datetimes that don't exist or
+are ambiguous because they fall in a DST transition (the timezones provided by
+pytz_ do this), such datetimes will be reported as invalid values.
+
+.. _time-zones-in-templates:
+
+Time zone aware output in templates
+===================================
+
+When you enable time zone support, Django converts aware datetime objects to
+the :ref:`current time zone <default-current-time-zone>` when they're rendered
+in templates. This behaves very much like :doc:`format localization
+</topics/i18n/formatting>`.
+
+.. warning::
+
+    Django doesn't convert naive datetime objects, because they could be
+    ambiguous, and because your code should never produce naive datetimes when
+    time zone support is enabled. However, you can force conversion with the
+    template filters described below.
+
+Conversion to local time isn't always appropriate -- you may be generating
+output for computers rather than for humans. The following filters and tags,
+provided the ``tz`` template library, allow you to control the time zone
+conversions.
+
+Template tags
+-------------
+
+.. templatetag:: localtime
+
+localtime
+~~~~~~~~~
+
+Enables or disables conversion of aware datetime objects to the current time
+zone in the contained block.
+
+This tag has exactly the same effects as the :setting:`USE_TZ` setting as far
+as the template engine is concerned. It allows a more fine grained control of
+conversion.
+
+To activate or deactivate conversion for a template block, use::
+
+    {% load tz %}
+
+    {% localtime on %}
+        {{ value }}
+    {% endlocaltime %}
+
+    {% localtime off %}
+        {{ value }}
+    {% endlocaltime %}
+
+.. note::
+
+    The value of :setting:`USE_TZ` isn't respected inside of a
+    ``{% localtime %}`` block.
+
+.. templatetag:: timezone
+
+timezone
+~~~~~~~~
+
+Sets or unsets the current time zone in the contained block. When the current
+time zone is unset, the default time zone applies.
+
+::
+
+    {% load tz %}
+
+    {% timezone "Europe/Paris" %}
+        Paris time: {{ value }}
+    {% endtimezone %}
+
+    {% timezone None %}
+        Server time: {{ value }}
+    {% endtimezone %}
+
+.. note::
+
+    In the second block, ``None`` resolves to the Python object ``None``
+    because isn't defined in the template context, not because it's the string
+    ``None``.
+
+.. templatetag:: get_current_timezone
+
+get_current_timezone
+~~~~~~~~~~~~~~~~~~~~
+
+When the :func:`django.core.context_processors.tz` context processor is
+enabled -- by default, it is -- each :class:`~django.template.RequestContext`
+contains a ``TIME_ZONE`` variable that provides the name of the current time
+zone.
+
+If you don't use a :class:`~django.template.RequestContext`, you can obtain
+this value with the ``get_current_timezone`` tag::
+
+    {% get_current_timezone as TIME_ZONE %}
+
+Template filters
+----------------
+
+These filters accept both aware and naive datetimes. For conversion purposes,
+they assume that naive datetimes are in the default time zone. They always
+return aware datetimes.
+
+.. templatefilter:: aslocaltime
+
+aslocaltime
+~~~~~~~~~~~
+
+Forces conversion of a single value to the current time zone.
+
+For example::
+
+    {% load tz %}
+
+    {{ value|aslocaltime }}
+
+.. templatefilter:: asutc
+
+asutc
+~~~~~
+
+Forces conversion of a single value to UTC.
+
+For example::
+
+    {% load tz %}
+
+    {{ value|asutc }}
+
+astimezone
+~~~~~~~~~~
+
+Forces conversion of a single value to an arbitrary timezone.
+
+The argument must be an instance of a :class:`~datetime.tzinfo` subclass or a
+time zone name. If it is a time zone name, pytz_ is required.
+
+For example::
+
+    {% load tz %}
+
+    {{ value|astimezone:"Europe/Paris" }}
+
+.. _time-zones-migration-guide:
+
+Migration guide
+===============
+
+Here's how to migrate a project that was started before Django supported time
+zones.
+
+Data
+----
+
+PostgreSQL
+~~~~~~~~~~
+
+The PostgreSQL backend stores datetimes as ``timestamp with time zone``. In
+practice, this means it converts datetimes from the connection's time zone to
+UTC on storage, and from UTC to the connection's time zone on retrieval.
+
+As a consequence, if you're using PostgreSQL, you can switch between ``USE_TZ
+= False`` and ``USE_TZ = True`` freely. The database connection's time zone
+will be set to :setting:`TIME_ZONE` or ``UTC`` respectively, so that Django
+obtains correct datetimes in all cases. You don't need to perform any data
+conversions.
+
+Other databases
+~~~~~~~~~~~~~~~
+
+Other backends store datetimes without time zone information. If you switch
+from ``USE_TZ = False`` to ``USE_TZ = True``, you must convert your data from
+local time to UTC -- which isn't deterministic if your local time has DST.
+
+Code
+----
+
+The first step is to add :setting:`USE_TZ = True <USE_TZ>` to your settings
+file and install pytz_ (if possible). At this point, things should mostly
+work. If you create naive datetime objects in your code, Django makes them
+aware when necessary.
+
+However, these conversions may fail around DST transitions, which means you
+aren't getting the full benefits of time zone support yet. Also, you're likely
+to run into a few problems because it's impossible to compare a naive datetime
+with an aware datetime. Since Django now gives you aware datetimes, you'll get
+exceptions wherever you compare a datetime that comes from a model or a form
+with a naive datetime that you've created in your code.
+
+So the second step is to refactor your code wherever you instanciate datetime
+objects to make them aware. This can be done incrementally.
+:mod:`django.utils.timezone` defines some handy helpers for compatibility
+code: :func:`~django.utils.timezone.is_aware`,
+:func:`~django.utils.timezone.is_naive`,
+:func:`~django.utils.timezone.make_aware`, and
+:func:`~django.utils.timezone.make_naive`.
+
+.. _pytz: http://pytz.sourceforge.net/
+.. _these issues: http://pytz.sourceforge.net/#problems-with-localtime
+.. _tz database: http://en.wikipedia.org/wiki/Tz_database

File diff suppressed because it is too large
+ 6 - 6
tests/modeltests/fixtures/tests.py


+ 3 - 3
tests/modeltests/serializers/tests.py

@@ -230,7 +230,7 @@ class SerializersTestBase(object):
 
         serial_str = serializers.serialize(self.serializer_name, [a])
         date_values = self._get_field_values(serial_str, "pub_date")
-        self.assertEqual(date_values[0], "0001-02-03 04:05:06")
+        self.assertEqual(date_values[0].replace('T', ' '), "0001-02-03 04:05:06")
 
     def test_pkless_serialized_strings(self):
         """
@@ -323,7 +323,7 @@ class XmlSerializerTransactionTestCase(SerializersTransactionTestBase, Transacti
     <object pk="1" model="serializers.article">
         <field to="serializers.author" name="author" rel="ManyToOneRel">1</field>
         <field type="CharField" name="headline">Forward references pose no problem</field>
-        <field type="DateTimeField" name="pub_date">2006-06-16 15:00:00</field>
+        <field type="DateTimeField" name="pub_date">2006-06-16T15:00:00</field>
         <field to="serializers.category" name="categories" rel="ManyToManyRel">
             <object pk="1"></object>
         </field>
@@ -374,7 +374,7 @@ class JsonSerializerTransactionTestCase(SerializersTransactionTestBase, Transact
         "model": "serializers.article",
         "fields": {
             "headline": "Forward references pose no problem",
-            "pub_date": "2006-06-16 15:00:00",
+            "pub_date": "2006-06-16T15:00:00",
             "categories": [1],
             "author": 1
         }

+ 0 - 0
tests/modeltests/timezones/__init__.py


+ 15 - 0
tests/modeltests/timezones/admin.py

@@ -0,0 +1,15 @@
+from __future__ import absolute_import
+
+from django.contrib import admin
+
+from .models import Event, Timestamp
+
+class EventAdmin(admin.ModelAdmin):
+    list_display = ('dt',)
+
+admin.site.register(Event, EventAdmin)
+
+class TimestampAdmin(admin.ModelAdmin):
+    readonly_fields = ('created', 'updated')
+
+admin.site.register(Timestamp, TimestampAdmin)

+ 17 - 0
tests/modeltests/timezones/fixtures/users.xml

@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<django-objects version="1.0">
+    <object pk="100" model="auth.user">
+        <field type="CharField" name="username">super</field>
+        <field type="CharField" name="first_name">Super</field>
+        <field type="CharField" name="last_name">User</field>
+        <field type="CharField" name="email">super@example.com</field>
+        <field type="CharField" name="password">sha1$995a3$6011485ea3834267d719b4c801409b8b1ddd0158</field>
+        <field type="BooleanField" name="is_staff">True</field>
+        <field type="BooleanField" name="is_active">True</field>
+        <field type="BooleanField" name="is_superuser">True</field>
+        <field type="DateTimeField" name="last_login">2007-05-30 13:20:10</field>
+        <field type="DateTimeField" name="date_joined">2007-05-30 13:20:10</field>
+        <field to="auth.group" name="groups" rel="ManyToManyRel"></field>
+        <field to="auth.permission" name="user_permissions" rel="ManyToManyRel"></field>
+    </object>
+</django-objects>

+ 13 - 0
tests/modeltests/timezones/forms.py

@@ -0,0 +1,13 @@
+from django import forms
+
+from .models import Event
+
+class EventForm(forms.Form):
+    dt = forms.DateTimeField()
+
+class EventSplitForm(forms.Form):
+    dt = forms.SplitDateTimeField()
+
+class EventModelForm(forms.ModelForm):
+    class Meta:
+        model = Event

+ 8 - 0
tests/modeltests/timezones/models.py

@@ -0,0 +1,8 @@
+from django.db import models
+
+class Event(models.Model):
+    dt = models.DateTimeField()
+
+class Timestamp(models.Model):
+    created = models.DateTimeField(auto_now_add=True)
+    updated = models.DateTimeField(auto_now=True)

+ 871 - 0
tests/modeltests/timezones/tests.py

@@ -0,0 +1,871 @@
+from __future__ import with_statement
+
+import datetime
+import os
+import time
+
+try:
+    import pytz
+except ImportError:
+    pytz = None
+
+from django.conf import settings
+from django.core import serializers
+from django.core.urlresolvers import reverse
+from django.db import connection
+from django.db.models import Min, Max
+from django.http import HttpRequest
+from django.template import Context, RequestContext, Template, TemplateSyntaxError
+from django.test import TestCase, skipIfDBFeature, skipUnlessDBFeature
+from django.test.utils import override_settings
+from django.utils import timezone
+from django.utils.tzinfo import FixedOffset
+from django.utils.unittest import skipIf
+
+from .forms import EventForm, EventSplitForm, EventModelForm
+from .models import Event, Timestamp
+
+
+# These tests use the EAT (Eastern Africa Time) and ICT (Indochina Time)
+# who don't have Daylight Saving Time, so we can represent them easily
+# with FixedOffset, and use them directly as tzinfo in the constructors.
+
+# settings.TIME_ZONE is forced to EAT. Most tests use a variant of
+# datetime.datetime(2011, 9, 1, 13, 20, 30), which translates to
+# 10:20:30 in UTC and 17:20:30 in ICT.
+
+UTC = timezone.utc
+EAT = FixedOffset(180)      # Africa/Nairobi
+ICT = FixedOffset(420)      # Asia/Bangkok
+
+TZ_SUPPORT = hasattr(time, 'tzset')
+
+
+class BaseDateTimeTests(TestCase):
+
+    @classmethod
+    def setUpClass(self):
+        self._old_time_zone = settings.TIME_ZONE
+        settings.TIME_ZONE = connection.settings_dict['TIME_ZONE'] = 'Africa/Nairobi'
+        timezone._localtime = None
+        if TZ_SUPPORT:
+            self._old_tz = os.environ.get('TZ')
+            os.environ['TZ'] = 'Africa/Nairobi'
+            time.tzset()
+        # Create a new cursor, for test cases that change the value of USE_TZ.
+        connection.close()
+
+    @classmethod
+    def tearDownClass(self):
+        settings.TIME_ZONE = connection.settings_dict['TIME_ZONE'] = self._old_time_zone
+        timezone._localtime = None
+        if TZ_SUPPORT:
+            if self._old_tz is None:
+                del os.environ['TZ']
+            else:
+                os.environ['TZ'] = self._old_tz
+            time.tzset()
+
+
+#@override_settings(USE_TZ=False)
+class LegacyDatabaseTests(BaseDateTimeTests):
+
+    def test_naive_datetime(self):
+        dt = datetime.datetime(2011, 9, 1, 13, 20, 30)
+        Event.objects.create(dt=dt)
+        event = Event.objects.get()
+        self.assertEqual(event.dt, dt)
+
+    @skipUnlessDBFeature('supports_microsecond_precision')
+    def test_naive_datetime_with_microsecond(self):
+        dt = datetime.datetime(2011, 9, 1, 13, 20, 30, 405060)
+        Event.objects.create(dt=dt)
+        event = Event.objects.get()
+        self.assertEqual(event.dt, dt)
+
+    @skipIfDBFeature('supports_microsecond_precision')
+    def test_naive_datetime_with_microsecond_unsupported(self):
+        dt = datetime.datetime(2011, 9, 1, 13, 20, 30, 405060)
+        Event.objects.create(dt=dt)
+        event = Event.objects.get()
+        # microseconds are lost during a round-trip in the database
+        self.assertEqual(event.dt, dt.replace(microsecond=0))
+
+    @skipUnlessDBFeature('supports_timezones')
+    def test_aware_datetime_in_local_timezone(self):
+        dt = datetime.datetime(2011, 9, 1, 13, 20, 30, tzinfo=EAT)
+        Event.objects.create(dt=dt)
+        event = Event.objects.get()
+        self.assertIsNone(event.dt.tzinfo)
+        # interpret the naive datetime in local time to get the correct value
+        self.assertEqual(event.dt.replace(tzinfo=EAT), dt)
+
+    @skipUnlessDBFeature('supports_timezones')
+    @skipUnlessDBFeature('supports_microsecond_precision')
+    def test_aware_datetime_in_local_timezone_with_microsecond(self):
+        dt = datetime.datetime(2011, 9, 1, 13, 20, 30, 405060, tzinfo=EAT)
+        Event.objects.create(dt=dt)
+        event = Event.objects.get()
+        self.assertIsNone(event.dt.tzinfo)
+        # interpret the naive datetime in local time to get the correct value
+        self.assertEqual(event.dt.replace(tzinfo=EAT), dt)
+
+    # This combination actually never happens.
+    @skipUnlessDBFeature('supports_timezones')
+    @skipIfDBFeature('supports_microsecond_precision')
+    def test_aware_datetime_in_local_timezone_with_microsecond_unsupported(self):
+        dt = datetime.datetime(2011, 9, 1, 13, 20, 30, 405060, tzinfo=EAT)
+        Event.objects.create(dt=dt)
+        event = Event.objects.get()
+        self.assertIsNone(event.dt.tzinfo)
+        # interpret the naive datetime in local time to get the correct value
+        # microseconds are lost during a round-trip in the database
+        self.assertEqual(event.dt.replace(tzinfo=EAT), dt.replace(microsecond=0))
+
+    @skipUnlessDBFeature('supports_timezones')
+    @skipIfDBFeature('needs_datetime_string_cast')
+    def test_aware_datetime_in_utc(self):
+        dt = datetime.datetime(2011, 9, 1, 10, 20, 30, tzinfo=UTC)
+        Event.objects.create(dt=dt)
+        event = Event.objects.get()
+        self.assertIsNone(event.dt.tzinfo)
+        # interpret the naive datetime in local time to get the correct value
+        self.assertEqual(event.dt.replace(tzinfo=EAT), dt)
+
+    # This combination is no longer possible since timezone support
+    # was removed from the SQLite backend -- it didn't work.
+    @skipUnlessDBFeature('supports_timezones')
+    @skipUnlessDBFeature('needs_datetime_string_cast')
+    def test_aware_datetime_in_utc_unsupported(self):
+        dt = datetime.datetime(2011, 9, 1, 10, 20, 30, tzinfo=UTC)
+        Event.objects.create(dt=dt)
+        event = Event.objects.get()
+        self.assertIsNone(event.dt.tzinfo)
+        # django.db.backend.utils.typecast_dt will just drop the
+        # timezone, so a round-trip in the database alters the data (!)
+        # interpret the naive datetime in local time and you get a wrong value
+        self.assertNotEqual(event.dt.replace(tzinfo=EAT), dt)
+        # interpret the naive datetime in original time to get the correct value
+        self.assertEqual(event.dt.replace(tzinfo=UTC), dt)
+
+    @skipUnlessDBFeature('supports_timezones')
+    @skipIfDBFeature('needs_datetime_string_cast')
+    def test_aware_datetime_in_other_timezone(self):
+        dt = datetime.datetime(2011, 9, 1, 17, 20, 30, tzinfo=ICT)
+        Event.objects.create(dt=dt)
+        event = Event.objects.get()
+        self.assertIsNone(event.dt.tzinfo)
+        # interpret the naive datetime in local time to get the correct value
+        self.assertEqual(event.dt.replace(tzinfo=EAT), dt)
+
+    # This combination is no longer possible since timezone support
+    # was removed from the SQLite backend -- it didn't work.
+    @skipUnlessDBFeature('supports_timezones')
+    @skipUnlessDBFeature('needs_datetime_string_cast')
+    def test_aware_datetime_in_other_timezone_unsupported(self):
+        dt = datetime.datetime(2011, 9, 1, 17, 20, 30, tzinfo=ICT)
+        Event.objects.create(dt=dt)
+        event = Event.objects.get()
+        self.assertIsNone(event.dt.tzinfo)
+        # django.db.backend.utils.typecast_dt will just drop the
+        # timezone, so a round-trip in the database alters the data (!)
+        # interpret the naive datetime in local time and you get a wrong value
+        self.assertNotEqual(event.dt.replace(tzinfo=EAT), dt)
+        # interpret the naive datetime in original time to get the correct value
+        self.assertEqual(event.dt.replace(tzinfo=ICT), dt)
+
+    @skipIfDBFeature('supports_timezones')
+    def test_aware_datetime_unspported(self):
+        dt = datetime.datetime(2011, 9, 1, 13, 20, 30, tzinfo=EAT)
+        with self.assertRaises(ValueError):
+            Event.objects.create(dt=dt)
+
+    def test_auto_now_and_auto_now_add(self):
+        now = datetime.datetime.now()
+        past = now - datetime.timedelta(seconds=2)
+        future = now + datetime.timedelta(seconds=2)
+        Timestamp.objects.create()
+        ts = Timestamp.objects.get()
+        self.assertLess(past, ts.created)
+        self.assertLess(past, ts.updated)
+        self.assertGreater(future, ts.updated)
+        self.assertGreater(future, ts.updated)
+
+    def test_query_filter(self):
+        dt1 = datetime.datetime(2011, 9, 1, 12, 20, 30)
+        dt2 = datetime.datetime(2011, 9, 1, 14, 20, 30)
+        Event.objects.create(dt=dt1)
+        Event.objects.create(dt=dt2)
+        self.assertEqual(Event.objects.filter(dt__gte=dt1).count(), 2)
+        self.assertEqual(Event.objects.filter(dt__gt=dt1).count(), 1)
+        self.assertEqual(Event.objects.filter(dt__gte=dt2).count(), 1)
+        self.assertEqual(Event.objects.filter(dt__gt=dt2).count(), 0)
+
+    def test_query_date_related_filters(self):
+        Event.objects.create(dt=datetime.datetime(2011, 1, 1, 1, 30, 0))
+        Event.objects.create(dt=datetime.datetime(2011, 1, 1, 4, 30, 0))
+        self.assertEqual(Event.objects.filter(dt__year=2011).count(), 2)
+        self.assertEqual(Event.objects.filter(dt__month=1).count(), 2)
+        self.assertEqual(Event.objects.filter(dt__day=1).count(), 2)
+        self.assertEqual(Event.objects.filter(dt__week_day=7).count(), 2)
+
+    def test_query_aggregation(self):
+        # Only min and max make sense for datetimes.
+        Event.objects.create(dt=datetime.datetime(2011, 9, 1, 23, 20, 20))
+        Event.objects.create(dt=datetime.datetime(2011, 9, 1, 13, 20, 30))
+        Event.objects.create(dt=datetime.datetime(2011, 9, 1, 3, 20, 40))
+        result = Event.objects.all().aggregate(Min('dt'), Max('dt'))
+        self.assertEqual(result, {
+            'dt__min': datetime.datetime(2011, 9, 1, 3, 20, 40),
+            'dt__max': datetime.datetime(2011, 9, 1, 23, 20, 20),
+        })
+
+    def test_query_dates(self):
+        Event.objects.create(dt=datetime.datetime(2011, 1, 1, 1, 30, 0))
+        Event.objects.create(dt=datetime.datetime(2011, 1, 1, 4, 30, 0))
+        self.assertQuerysetEqual(Event.objects.dates('dt', 'year'),
+                [datetime.datetime(2011, 1, 1)], transform=lambda d: d)
+        self.assertQuerysetEqual(Event.objects.dates('dt', 'month'),
+                [datetime.datetime(2011, 1, 1)], transform=lambda d: d)
+        self.assertQuerysetEqual(Event.objects.dates('dt', 'day'),
+                [datetime.datetime(2011, 1, 1)], transform=lambda d: d)
+
+LegacyDatabaseTests = override_settings(USE_TZ=False)(LegacyDatabaseTests)
+
+
+#@override_settings(USE_TZ=True)
+class NewDatabaseTests(BaseDateTimeTests):
+
+    def test_naive_datetime(self):
+        dt = datetime.datetime(2011, 9, 1, 13, 20, 30)
+        Event.objects.create(dt=dt)
+        event = Event.objects.get()
+        # naive datetimes are interpreted in local time
+        self.assertEqual(event.dt, dt.replace(tzinfo=EAT))
+
+    @skipUnlessDBFeature('supports_microsecond_precision')
+    def test_naive_datetime_with_microsecond(self):
+        dt = datetime.datetime(2011, 9, 1, 13, 20, 30, 405060)
+        Event.objects.create(dt=dt)
+        event = Event.objects.get()
+        # naive datetimes are interpreted in local time
+        self.assertEqual(event.dt, dt.replace(tzinfo=EAT))
+
+    @skipIfDBFeature('supports_microsecond_precision')
+    def test_naive_datetime_with_microsecond_unsupported(self):
+        dt = datetime.datetime(2011, 9, 1, 13, 20, 30, 405060)
+        Event.objects.create(dt=dt)
+        event = Event.objects.get()
+        # microseconds are lost during a round-trip in the database
+        # naive datetimes are interpreted in local time
+        self.assertEqual(event.dt, dt.replace(microsecond=0, tzinfo=EAT))
+
+    def test_aware_datetime_in_local_timezone(self):
+        dt = datetime.datetime(2011, 9, 1, 13, 20, 30, tzinfo=EAT)
+        Event.objects.create(dt=dt)
+        event = Event.objects.get()
+        self.assertEqual(event.dt, dt)
+
+    @skipUnlessDBFeature('supports_microsecond_precision')
+    def test_aware_datetime_in_local_timezone_with_microsecond(self):
+        dt = datetime.datetime(2011, 9, 1, 13, 20, 30, 405060, tzinfo=EAT)
+        Event.objects.create(dt=dt)
+        event = Event.objects.get()
+        self.assertEqual(event.dt, dt)
+
+    @skipIfDBFeature('supports_microsecond_precision')
+    def test_aware_datetime_in_local_timezone_with_microsecond_unsupported(self):
+        dt = datetime.datetime(2011, 9, 1, 13, 20, 30, 405060, tzinfo=EAT)
+        Event.objects.create(dt=dt)
+        event = Event.objects.get()
+        # microseconds are lost during a round-trip in the database
+        self.assertEqual(event.dt, dt.replace(microsecond=0))
+
+    def test_aware_datetime_in_utc(self):
+        dt = datetime.datetime(2011, 9, 1, 10, 20, 30, tzinfo=UTC)
+        Event.objects.create(dt=dt)
+        event = Event.objects.get()
+        self.assertEqual(event.dt, dt)
+
+    def test_aware_datetime_in_other_timezone(self):
+        dt = datetime.datetime(2011, 9, 1, 17, 20, 30, tzinfo=ICT)
+        Event.objects.create(dt=dt)
+        event = Event.objects.get()
+        self.assertEqual(event.dt, dt)
+
+    def test_auto_now_and_auto_now_add(self):
+        now = datetime.datetime.utcnow().replace(tzinfo=UTC)
+        past = now - datetime.timedelta(seconds=2)
+        future = now + datetime.timedelta(seconds=2)
+        Timestamp.objects.create()
+        ts = Timestamp.objects.get()
+        self.assertLess(past, ts.created)
+        self.assertLess(past, ts.updated)
+        self.assertGreater(future, ts.updated)
+        self.assertGreater(future, ts.updated)
+
+    def test_query_filter(self):
+        dt1 = datetime.datetime(2011, 9, 1, 12, 20, 30, tzinfo=EAT)
+        dt2 = datetime.datetime(2011, 9, 1, 14, 20, 30, tzinfo=EAT)
+        Event.objects.create(dt=dt1)
+        Event.objects.create(dt=dt2)
+        self.assertEqual(Event.objects.filter(dt__gte=dt1).count(), 2)
+        self.assertEqual(Event.objects.filter(dt__gt=dt1).count(), 1)
+        self.assertEqual(Event.objects.filter(dt__gte=dt2).count(), 1)
+        self.assertEqual(Event.objects.filter(dt__gt=dt2).count(), 0)
+
+    @skipIf(pytz is None, "this test requires pytz")
+    def test_query_filter_with_pytz_timezones(self):
+        tz = pytz.timezone('Europe/Paris')
+        dt = datetime.datetime(2011, 9, 1, 12, 20, 30, tzinfo=tz)
+        Event.objects.create(dt=dt)
+        next = dt + datetime.timedelta(seconds=3)
+        prev = dt - datetime.timedelta(seconds=3)
+        self.assertEqual(Event.objects.filter(dt__exact=dt).count(), 1)
+        self.assertEqual(Event.objects.filter(dt__exact=next).count(), 0)
+        self.assertEqual(Event.objects.filter(dt__in=(prev, next)).count(), 0)
+        self.assertEqual(Event.objects.filter(dt__in=(prev, dt, next)).count(), 1)
+        self.assertEqual(Event.objects.filter(dt__range=(prev, next)).count(), 1)
+
+    def test_query_date_related_filters(self):
+        # These two dates fall in the same day in EAT, but in different days,
+        # years and months in UTC, and aggregation is performed in UTC when
+        # time zone support is enabled. This test could be changed if the
+        # implementation is changed to perform the aggregation is local time.
+        Event.objects.create(dt=datetime.datetime(2011, 1, 1, 1, 30, 0, tzinfo=EAT))
+        Event.objects.create(dt=datetime.datetime(2011, 1, 1, 4, 30, 0, tzinfo=EAT))
+        self.assertEqual(Event.objects.filter(dt__year=2011).count(), 1)
+        self.assertEqual(Event.objects.filter(dt__month=1).count(), 1)
+        self.assertEqual(Event.objects.filter(dt__day=1).count(), 1)
+        self.assertEqual(Event.objects.filter(dt__week_day=7).count(), 1)
+
+    def test_query_aggregation(self):
+        # Only min and max make sense for datetimes.
+        Event.objects.create(dt=datetime.datetime(2011, 9, 1, 23, 20, 20, tzinfo=EAT))
+        Event.objects.create(dt=datetime.datetime(2011, 9, 1, 13, 20, 30, tzinfo=EAT))
+        Event.objects.create(dt=datetime.datetime(2011, 9, 1, 3, 20, 40, tzinfo=EAT))
+        result = Event.objects.all().aggregate(Min('dt'), Max('dt'))
+        self.assertEqual(result, {
+            'dt__min': datetime.datetime(2011, 9, 1, 3, 20, 40, tzinfo=EAT),
+            'dt__max': datetime.datetime(2011, 9, 1, 23, 20, 20, tzinfo=EAT),
+        })
+
+    def test_query_dates(self):
+        # Same comment as in test_query_date_related_filters.
+        Event.objects.create(dt=datetime.datetime(2011, 1, 1, 1, 30, 0, tzinfo=EAT))
+        Event.objects.create(dt=datetime.datetime(2011, 1, 1, 4, 30, 0, tzinfo=EAT))
+        self.assertQuerysetEqual(Event.objects.dates('dt', 'year'),
+                [datetime.datetime(2010, 1, 1, tzinfo=UTC),
+                 datetime.datetime(2011, 1, 1, tzinfo=UTC)],
+                transform=lambda d: d)
+        self.assertQuerysetEqual(Event.objects.dates('dt', 'month'),
+                [datetime.datetime(2010, 12, 1, tzinfo=UTC),
+                 datetime.datetime(2011, 1, 1, tzinfo=UTC)],
+                transform=lambda d: d)
+        self.assertQuerysetEqual(Event.objects.dates('dt', 'day'),
+                [datetime.datetime(2010, 12, 31, tzinfo=UTC),
+                 datetime.datetime(2011, 1, 1, tzinfo=UTC)],
+                transform=lambda d: d)
+
+NewDatabaseTests = override_settings(USE_TZ=True)(NewDatabaseTests)
+
+
+class SerializationTests(BaseDateTimeTests):
+
+    # Backend-specific notes:
+    # - JSON supports only milliseconds, microseconds will be truncated.
+    # - PyYAML dumps the UTC offset correctly for timezone-aware datetimes,
+    #   but when it loads this representation, it substracts the offset and
+    #   returns a naive datetime object in UTC (http://pyyaml.org/ticket/202).
+    # Tests are adapted to take these quirks into account.
+
+    def test_naive_datetime(self):
+        dt = datetime.datetime(2011, 9, 1, 13, 20, 30)
+
+        data = serializers.serialize('python', [Event(dt=dt)])
+        self.assertEqual(data[0]['fields']['dt'], dt)
+        obj = serializers.deserialize('python', data).next().object
+        self.assertEqual(obj.dt, dt)
+
+        data = serializers.serialize('json', [Event(dt=dt)])
+        self.assertIn('"fields": {"dt": "2011-09-01T13:20:30"}', data)
+        obj = serializers.deserialize('json', data).next().object
+        self.assertEqual(obj.dt, dt)
+
+        data = serializers.serialize('xml', [Event(dt=dt)])
+        self.assertIn('<field type="DateTimeField" name="dt">2011-09-01T13:20:30</field>', data)
+        obj = serializers.deserialize('xml', data).next().object
+        self.assertEqual(obj.dt, dt)
+
+        if 'yaml' in serializers.get_serializer_formats():
+            data = serializers.serialize('yaml', [Event(dt=dt)])
+            self.assertIn("- fields: {dt: !!timestamp '2011-09-01 13:20:30'}", data)
+            obj = serializers.deserialize('yaml', data).next().object
+            self.assertEqual(obj.dt, dt)
+
+    def test_naive_datetime_with_microsecond(self):
+        dt = datetime.datetime(2011, 9, 1, 13, 20, 30, 405060)
+
+        data = serializers.serialize('python', [Event(dt=dt)])
+        self.assertEqual(data[0]['fields']['dt'], dt)
+        obj = serializers.deserialize('python', data).next().object
+        self.assertEqual(obj.dt, dt)
+
+        data = serializers.serialize('json', [Event(dt=dt)])
+        self.assertIn('"fields": {"dt": "2011-09-01T13:20:30.405"}', data)
+        obj = serializers.deserialize('json', data).next().object
+        self.assertEqual(obj.dt, dt.replace(microsecond=405000))
+
+        data = serializers.serialize('xml', [Event(dt=dt)])
+        self.assertIn('<field type="DateTimeField" name="dt">2011-09-01T13:20:30.405060</field>', data)
+        obj = serializers.deserialize('xml', data).next().object
+        self.assertEqual(obj.dt, dt)
+
+        if 'yaml' in serializers.get_serializer_formats():
+            data = serializers.serialize('yaml', [Event(dt=dt)])
+            self.assertIn("- fields: {dt: !!timestamp '2011-09-01 13:20:30.405060'}", data)
+            obj = serializers.deserialize('yaml', data).next().object
+            self.assertEqual(obj.dt, dt)
+
+    def test_aware_datetime_with_microsecond(self):
+        dt = datetime.datetime(2011, 9, 1, 17, 20, 30, 405060, tzinfo=ICT)
+
+        data = serializers.serialize('python', [Event(dt=dt)])
+        self.assertEqual(data[0]['fields']['dt'], dt)
+        obj = serializers.deserialize('python', data).next().object
+        self.assertEqual(obj.dt, dt)
+
+        data = serializers.serialize('json', [Event(dt=dt)])
+        self.assertIn('"fields": {"dt": "2011-09-01T17:20:30.405+07:00"}', data)
+        obj = serializers.deserialize('json', data).next().object
+        self.assertEqual(obj.dt, dt.replace(microsecond=405000))
+
+        data = serializers.serialize('xml', [Event(dt=dt)])
+        self.assertIn('<field type="DateTimeField" name="dt">2011-09-01T17:20:30.405060+07:00</field>', data)
+        obj = serializers.deserialize('xml', data).next().object
+        self.assertEqual(obj.dt, dt)
+
+        if 'yaml' in serializers.get_serializer_formats():
+            data = serializers.serialize('yaml', [Event(dt=dt)])
+            self.assertIn("- fields: {dt: !!timestamp '2011-09-01 17:20:30.405060+07:00'}", data)
+            obj = serializers.deserialize('yaml', data).next().object
+            self.assertEqual(obj.dt.replace(tzinfo=UTC), dt)
+
+    def test_aware_datetime_in_utc(self):
+        dt = datetime.datetime(2011, 9, 1, 10, 20, 30, tzinfo=UTC)
+
+        data = serializers.serialize('python', [Event(dt=dt)])
+        self.assertEqual(data[0]['fields']['dt'], dt)
+        obj = serializers.deserialize('python', data).next().object
+        self.assertEqual(obj.dt, dt)
+
+        data = serializers.serialize('json', [Event(dt=dt)])
+        self.assertIn('"fields": {"dt": "2011-09-01T10:20:30Z"}', data)
+        obj = serializers.deserialize('json', data).next().object
+        self.assertEqual(obj.dt, dt)
+
+        data = serializers.serialize('xml', [Event(dt=dt)])
+        self.assertIn('<field type="DateTimeField" name="dt">2011-09-01T10:20:30+00:00</field>', data)
+        obj = serializers.deserialize('xml', data).next().object
+        self.assertEqual(obj.dt, dt)
+
+        if 'yaml' in serializers.get_serializer_formats():
+            data = serializers.serialize('yaml', [Event(dt=dt)])
+            self.assertIn("- fields: {dt: !!timestamp '2011-09-01 10:20:30+00:00'}", data)
+            obj = serializers.deserialize('yaml', data).next().object
+            self.assertEqual(obj.dt.replace(tzinfo=UTC), dt)
+
+    def test_aware_datetime_in_local_timezone(self):
+        dt = datetime.datetime(2011, 9, 1, 13, 20, 30, tzinfo=EAT)
+
+        data = serializers.serialize('python', [Event(dt=dt)])
+        self.assertEqual(data[0]['fields']['dt'], dt)
+        obj = serializers.deserialize('python', data).next().object
+        self.assertEqual(obj.dt, dt)
+
+        data = serializers.serialize('json', [Event(dt=dt)])
+        self.assertIn('"fields": {"dt": "2011-09-01T13:20:30+03:00"}', data)
+        obj = serializers.deserialize('json', data).next().object
+        self.assertEqual(obj.dt, dt)
+
+        data = serializers.serialize('xml', [Event(dt=dt)])
+        self.assertIn('<field type="DateTimeField" name="dt">2011-09-01T13:20:30+03:00</field>', data)
+        obj = serializers.deserialize('xml', data).next().object
+        self.assertEqual(obj.dt, dt)
+
+        if 'yaml' in serializers.get_serializer_formats():
+            data = serializers.serialize('yaml', [Event(dt=dt)])
+            self.assertIn("- fields: {dt: !!timestamp '2011-09-01 13:20:30+03:00'}", data)
+            obj = serializers.deserialize('yaml', data).next().object
+            self.assertEqual(obj.dt.replace(tzinfo=UTC), dt)
+
+    def test_aware_datetime_in_other_timezone(self):
+        dt = datetime.datetime(2011, 9, 1, 17, 20, 30, tzinfo=ICT)
+
+        data = serializers.serialize('python', [Event(dt=dt)])
+        self.assertEqual(data[0]['fields']['dt'], dt)
+        obj = serializers.deserialize('python', data).next().object
+        self.assertEqual(obj.dt, dt)
+
+        data = serializers.serialize('json', [Event(dt=dt)])
+        self.assertIn('"fields": {"dt": "2011-09-01T17:20:30+07:00"}', data)
+        obj = serializers.deserialize('json', data).next().object
+        self.assertEqual(obj.dt, dt)
+
+        data = serializers.serialize('xml', [Event(dt=dt)])
+        self.assertIn('<field type="DateTimeField" name="dt">2011-09-01T17:20:30+07:00</field>', data)
+        obj = serializers.deserialize('xml', data).next().object
+        self.assertEqual(obj.dt, dt)
+
+        if 'yaml' in serializers.get_serializer_formats():
+            data = serializers.serialize('yaml', [Event(dt=dt)])
+            self.assertIn("- fields: {dt: !!timestamp '2011-09-01 17:20:30+07:00'}", data)
+            obj = serializers.deserialize('yaml', data).next().object
+            self.assertEqual(obj.dt.replace(tzinfo=UTC), dt)
+
+#@override_settings(DATETIME_FORMAT='c', USE_L10N=False, USE_TZ=True)
+class TemplateTests(BaseDateTimeTests):
+
+    def test_localtime_templatetag_and_filters(self):
+        """
+        Test the {% localtime %} templatetag and related filters.
+        """
+        datetimes = {
+            'utc': datetime.datetime(2011, 9, 1, 10, 20, 30, tzinfo=UTC),
+            'eat': datetime.datetime(2011, 9, 1, 13, 20, 30, tzinfo=EAT),
+            'ict': datetime.datetime(2011, 9, 1, 17, 20, 30, tzinfo=ICT),
+            'naive': datetime.datetime(2011, 9, 1, 13, 20, 30),
+        }
+        templates = {
+            'notag': Template("{% load tz %}{{ dt }}|{{ dt|aslocaltime }}|{{ dt|asutc }}|{{ dt|astimezone:ICT }}"),
+            'noarg': Template("{% load tz %}{% localtime %}{{ dt }}|{{ dt|aslocaltime }}|{{ dt|asutc }}|{{ dt|astimezone:ICT }}{% endlocaltime %}"),
+            'on':    Template("{% load tz %}{% localtime on %}{{ dt }}|{{ dt|aslocaltime }}|{{ dt|asutc }}|{{ dt|astimezone:ICT }}{% endlocaltime %}"),
+            'off':   Template("{% load tz %}{% localtime off %}{{ dt }}|{{ dt|aslocaltime }}|{{ dt|asutc }}|{{ dt|astimezone:ICT }}{% endlocaltime %}"),
+        }
+
+        # Transform a list of keys in 'datetimes' to the expected template
+        # output. This makes the definition of 'results' more readable.
+        def t(*result):
+            return '|'.join(datetimes[key].isoformat() for key in result)
+
+        # Results for USE_TZ = True
+
+        results = {
+            'utc': {
+                'notag': t('eat', 'eat', 'utc', 'ict'),
+                'noarg': t('eat', 'eat', 'utc', 'ict'),
+                'on':    t('eat', 'eat', 'utc', 'ict'),
+                'off':   t('utc', 'eat', 'utc', 'ict'),
+            },
+            'eat': {
+                'notag': t('eat', 'eat', 'utc', 'ict'),
+                'noarg': t('eat', 'eat', 'utc', 'ict'),
+                'on':    t('eat', 'eat', 'utc', 'ict'),
+                'off':   t('eat', 'eat', 'utc', 'ict'),
+            },
+            'ict': {
+                'notag': t('eat', 'eat', 'utc', 'ict'),
+                'noarg': t('eat', 'eat', 'utc', 'ict'),
+                'on':    t('eat', 'eat', 'utc', 'ict'),
+                'off':   t('ict', 'eat', 'utc', 'ict'),
+            },
+            'naive': {
+                'notag': t('naive', 'eat', 'utc', 'ict'),
+                'noarg': t('naive', 'eat', 'utc', 'ict'),
+                'on':    t('naive', 'eat', 'utc', 'ict'),
+                'off':   t('naive', 'eat', 'utc', 'ict'),
+            }
+        }
+
+        for k1, dt in datetimes.iteritems():
+            for k2, tpl in templates.iteritems():
+                ctx = Context({'dt': dt, 'ICT': ICT})
+                actual = tpl.render(ctx)
+                expected = results[k1][k2]
+                self.assertEqual(actual, expected, '%s / %s: %r != %r' % (k1, k2, actual, expected))
+
+        # Changes for USE_TZ = False
+
+        results['utc']['notag'] = t('utc', 'eat', 'utc', 'ict')
+        results['ict']['notag'] = t('ict', 'eat', 'utc', 'ict')
+
+        with self.settings(USE_TZ=False):
+            for k1, dt in datetimes.iteritems():
+                for k2, tpl in templates.iteritems():
+                    ctx = Context({'dt': dt, 'ICT': ICT})
+                    actual = tpl.render(ctx)
+                    expected = results[k1][k2]
+                    self.assertEqual(actual, expected, '%s / %s: %r != %r' % (k1, k2, actual, expected))
+
+    @skipIf(pytz is None, "this test requires pytz")
+    def test_localtime_filters_with_pytz(self):
+        """
+        Test the |aslocaltime, |asutc, and |astimezone filters with pytz.
+        """
+        # Use a pytz timezone as local time
+        tpl = Template("{% load tz %}{{ dt|aslocaltime }}|{{ dt|asutc }}")
+        ctx = Context({'dt': datetime.datetime(2011, 9, 1, 12, 20, 30)})
+
+        timezone._localtime = None
+        with self.settings(TIME_ZONE='Europe/Paris'):
+            self.assertEqual(tpl.render(ctx), "2011-09-01T12:20:30+02:00|2011-09-01T10:20:30+00:00")
+        timezone._localtime = None
+
+        # Use a pytz timezone as argument
+        tpl = Template("{% load tz %}{{ dt|astimezone:tz }}")
+        ctx = Context({'dt': datetime.datetime(2011, 9, 1, 13, 20, 30),
+                       'tz': pytz.timezone('Europe/Paris')})
+        self.assertEqual(tpl.render(ctx), "2011-09-01T12:20:30+02:00")
+
+        # Use a pytz timezone name as argument
+        tpl = Template("{% load tz %}{{ dt|astimezone:'Europe/Paris' }}")
+        ctx = Context({'dt': datetime.datetime(2011, 9, 1, 13, 20, 30),
+                       'tz': pytz.timezone('Europe/Paris')})
+        self.assertEqual(tpl.render(ctx), "2011-09-01T12:20:30+02:00")
+
+    def test_localtime_templatetag_invalid_argument(self):
+        with self.assertRaises(TemplateSyntaxError):
+            Template("{% load tz %}{% localtime foo %}{% endlocaltime %}").render()
+
+    def test_localtime_filters_do_not_raise_exceptions(self):
+        """
+        Test the |aslocaltime, |asutc, and |astimezone filters on bad inputs.
+        """
+        tpl = Template("{% load tz %}{{ dt }}|{{ dt|aslocaltime }}|{{ dt|asutc }}|{{ dt|astimezone:tz }}")
+        with self.settings(USE_TZ=True):
+            # bad datetime value
+            ctx = Context({'dt': None, 'tz': ICT})
+            self.assertEqual(tpl.render(ctx), "None|||")
+            ctx = Context({'dt': 'not a date', 'tz': ICT})
+            self.assertEqual(tpl.render(ctx), "not a date|||")
+            # bad timezone value
+            tpl = Template("{% load tz %}{{ dt|astimezone:tz }}")
+            ctx = Context({'dt': datetime.datetime(2011, 9, 1, 13, 20, 30), 'tz': None})
+            self.assertEqual(tpl.render(ctx), "")
+            ctx = Context({'dt': datetime.datetime(2011, 9, 1, 13, 20, 30), 'tz': 'not a tz'})
+            self.assertEqual(tpl.render(ctx), "")
+
+    def test_timezone_templatetag(self):
+        """
+        Test the {% timezone %} templatetag.
+        """
+        tpl = Template("{% load tz %}"
+                "{{ dt }}|"
+                "{% timezone tz1 %}"
+                    "{{ dt }}|"
+                    "{% timezone tz2 %}"
+                        "{{ dt }}"
+                    "{% endtimezone %}"
+                "{% endtimezone %}")
+        ctx = Context({'dt': datetime.datetime(2011, 9, 1, 10, 20, 30, tzinfo=UTC),
+                       'tz1': ICT, 'tz2': None})
+        self.assertEqual(tpl.render(ctx), "2011-09-01T13:20:30+03:00|2011-09-01T17:20:30+07:00|2011-09-01T13:20:30+03:00")
+
+    @skipIf(pytz is None, "this test requires pytz")
+    def test_timezone_templatetag_with_pytz(self):
+        """
+        Test the {% timezone %} templatetag with pytz.
+        """
+        tpl = Template("{% load tz %}{% timezone tz %}{{ dt }}{% endtimezone %}")
+
+        # Use a pytz timezone as argument
+        ctx = Context({'dt': datetime.datetime(2011, 9, 1, 13, 20, 30, tzinfo=EAT),
+                       'tz': pytz.timezone('Europe/Paris')})
+        self.assertEqual(tpl.render(ctx), "2011-09-01T12:20:30+02:00")
+
+        # Use a pytz timezone name as argument
+        ctx = Context({'dt': datetime.datetime(2011, 9, 1, 13, 20, 30, tzinfo=EAT),
+                       'tz': 'Europe/Paris'})
+        self.assertEqual(tpl.render(ctx), "2011-09-01T12:20:30+02:00")
+
+    def test_timezone_templatetag_invalid_argument(self):
+        with self.assertRaises(TemplateSyntaxError):
+            Template("{% load tz %}{% timezone %}{% endtimezone %}").render()
+        with self.assertRaises(ValueError if pytz is None else pytz.UnknownTimeZoneError):
+            Template("{% load tz %}{% timezone tz %}{% endtimezone %}").render(Context({'tz': 'foobar'}))
+
+    def test_get_current_timezone_templatetag(self):
+        """
+        Test the {% get_current_timezone %} templatetag.
+        """
+        tpl = Template("{% load tz %}{% get_current_timezone as time_zone %}{{ time_zone }}")
+
+        self.assertEqual(tpl.render(Context()), "Africa/Nairobi" if pytz else "EAT")
+        with timezone.override(UTC):
+            self.assertEqual(tpl.render(Context()), "UTC")
+
+        tpl = Template("{% load tz %}{% timezone tz %}{% get_current_timezone as time_zone %}{% endtimezone %}{{ time_zone }}")
+
+        self.assertEqual(tpl.render(Context({'tz': ICT})), "+0700")
+        with timezone.override(UTC):
+            self.assertEqual(tpl.render(Context({'tz': ICT})), "+0700")
+
+    @skipIf(pytz is None, "this test requires pytz")
+    def test_get_current_timezone_templatetag_with_pytz(self):
+        """
+        Test the {% get_current_timezone %} templatetag with pytz.
+        """
+        tpl = Template("{% load tz %}{% get_current_timezone as time_zone %}{{ time_zone }}")
+        with timezone.override(pytz.timezone('Europe/Paris')):
+            self.assertEqual(tpl.render(Context()), "Europe/Paris")
+
+        tpl = Template("{% load tz %}{% timezone 'Europe/Paris' %}{% get_current_timezone as time_zone %}{% endtimezone %}{{ time_zone }}")
+        self.assertEqual(tpl.render(Context()), "Europe/Paris")
+
+    def test_get_current_timezone_templatetag_invalid_argument(self):
+        with self.assertRaises(TemplateSyntaxError):
+            Template("{% load tz %}{% get_current_timezone %}").render()
+
+    def test_tz_template_context_processor(self):
+        """
+        Test the django.core.context_processors.tz template context processor.
+        """
+        tpl = Template("{{ TIME_ZONE }}")
+        self.assertEqual(tpl.render(Context()), "")
+        self.assertEqual(tpl.render(RequestContext(HttpRequest())), "Africa/Nairobi" if pytz else "EAT")
+
+    def test_date_and_time_template_filters(self):
+        tpl = Template("{{ dt|date:'Y-m-d' }} at {{ dt|time:'H:i:s' }}")
+        ctx = Context({'dt': datetime.datetime(2011, 9, 1, 20, 20, 20, tzinfo=UTC)})
+        self.assertEqual(tpl.render(ctx), "2011-09-01 at 23:20:20")
+        with timezone.override(ICT):
+            self.assertEqual(tpl.render(ctx), "2011-09-02 at 03:20:20")
+
+    def test_date_and_time_template_filters_honor_localtime(self):
+        tpl = Template("{% load tz %}{% localtime off %}{{ dt|date:'Y-m-d' }} at {{ dt|time:'H:i:s' }}{% endlocaltime %}")
+        ctx = Context({'dt': datetime.datetime(2011, 9, 1, 20, 20, 20, tzinfo=UTC)})
+        self.assertEqual(tpl.render(ctx), "2011-09-01 at 20:20:20")
+        with timezone.override(ICT):
+            self.assertEqual(tpl.render(ctx), "2011-09-01 at 20:20:20")
+
+TemplateTests = override_settings(DATETIME_FORMAT='c', USE_L10N=False, USE_TZ=True)(TemplateTests)
+
+#@override_settings(DATETIME_FORMAT='c', USE_L10N=False, USE_TZ=False)
+class LegacyFormsTests(BaseDateTimeTests):
+
+    def test_form(self):
+        form = EventForm({'dt': u'2011-09-01 13:20:30'})
+        self.assertTrue(form.is_valid())
+        self.assertEqual(form.cleaned_data['dt'], datetime.datetime(2011, 9, 1, 13, 20, 30))
+
+    @skipIf(pytz is None, "this test requires pytz")
+    def test_form_with_non_existent_time(self):
+        form = EventForm({'dt': u'2011-03-27 02:30:00'})
+        with timezone.override(pytz.timezone('Europe/Paris')):
+            # this is obviously a bug
+            self.assertTrue(form.is_valid())
+            self.assertEqual(form.cleaned_data['dt'], datetime.datetime(2011, 3, 27, 2, 30, 0))
+
+    @skipIf(pytz is None, "this test requires pytz")
+    def test_form_with_ambiguous_time(self):
+        form = EventForm({'dt': u'2011-10-30 02:30:00'})
+        with timezone.override(pytz.timezone('Europe/Paris')):
+            # this is obviously a bug
+            self.assertTrue(form.is_valid())
+            self.assertEqual(form.cleaned_data['dt'], datetime.datetime(2011, 10, 30, 2, 30, 0))
+
+    def test_split_form(self):
+        form = EventSplitForm({'dt_0': u'2011-09-01', 'dt_1': u'13:20:30'})
+        self.assertTrue(form.is_valid())
+        self.assertEqual(form.cleaned_data['dt'], datetime.datetime(2011, 9, 1, 13, 20, 30))
+
+    def test_model_form(self):
+        EventModelForm({'dt': u'2011-09-01 13:20:30'}).save()
+        e = Event.objects.get()
+        self.assertEqual(e.dt, datetime.datetime(2011, 9, 1, 13, 20, 30))
+
+LegacyFormsTests = override_settings(DATETIME_FORMAT='c', USE_L10N=False, USE_TZ=False)(LegacyFormsTests)
+
+#@override_settings(DATETIME_FORMAT='c', USE_L10N=False, USE_TZ=True)
+class NewFormsTests(BaseDateTimeTests):
+
+    def test_form(self):
+        form = EventForm({'dt': u'2011-09-01 13:20:30'})
+        self.assertTrue(form.is_valid())
+        self.assertEqual(form.cleaned_data['dt'], datetime.datetime(2011, 9, 1, 10, 20, 30, tzinfo=UTC))
+
+    def test_form_with_other_timezone(self):
+        form = EventForm({'dt': u'2011-09-01 17:20:30'})
+        with timezone.override(ICT):
+            self.assertTrue(form.is_valid())
+            self.assertEqual(form.cleaned_data['dt'], datetime.datetime(2011, 9, 1, 10, 20, 30, tzinfo=UTC))
+
+    @skipIf(pytz is None, "this test requires pytz")
+    def test_form_with_non_existent_time(self):
+        with timezone.override(pytz.timezone('Europe/Paris')):
+            form = EventForm({'dt': u'2011-03-27 02:30:00'})
+            self.assertFalse(form.is_valid())
+            self.assertEqual(form.errors['dt'],
+                [u"2011-03-27 02:30:00 couldn't be interpreted in time zone "
+                 u"Europe/Paris; it may be ambiguous or it may not exist."])
+
+    @skipIf(pytz is None, "this test requires pytz")
+    def test_form_with_ambiguous_time(self):
+        with timezone.override(pytz.timezone('Europe/Paris')):
+            form = EventForm({'dt': u'2011-10-30 02:30:00'})
+            self.assertFalse(form.is_valid())
+            self.assertEqual(form.errors['dt'],
+                [u"2011-10-30 02:30:00 couldn't be interpreted in time zone "
+                 u"Europe/Paris; it may be ambiguous or it may not exist."])
+
+    def test_split_form(self):
+        form = EventSplitForm({'dt_0': u'2011-09-01', 'dt_1': u'13:20:30'})
+        self.assertTrue(form.is_valid())
+        self.assertEqual(form.cleaned_data['dt'], datetime.datetime(2011, 9, 1, 10, 20, 30, tzinfo=UTC))
+
+    def test_model_form(self):
+        EventModelForm({'dt': u'2011-09-01 13:20:30'}).save()
+        e = Event.objects.get()
+        self.assertEqual(e.dt, datetime.datetime(2011, 9, 1, 10, 20, 30, tzinfo=UTC))
+
+NewFormsTests = override_settings(DATETIME_FORMAT='c', USE_L10N=False, USE_TZ=True)(NewFormsTests)
+
+#@override_settings(DATETIME_FORMAT='c', USE_L10N=False, USE_TZ=True)
+class AdminTests(BaseDateTimeTests):
+
+    urls = 'modeltests.timezones.urls'
+    fixtures = ['users.xml']
+
+    def setUp(self):
+        self.client.login(username='super', password='secret')
+
+    def test_changelist(self):
+        e = Event.objects.create(dt=datetime.datetime(2011, 9, 1, 10, 20, 30, tzinfo=UTC))
+        response = self.client.get(reverse('admin:timezones_event_changelist'))
+        self.assertContains(response, e.dt.astimezone(EAT).isoformat())
+
+    def test_changelist_in_other_timezone(self):
+        e = Event.objects.create(dt=datetime.datetime(2011, 9, 1, 10, 20, 30, tzinfo=UTC))
+        with timezone.override(ICT):
+            response = self.client.get(reverse('admin:timezones_event_changelist'))
+        self.assertContains(response, e.dt.astimezone(ICT).isoformat())
+
+    def test_change_editable(self):
+        e = Event.objects.create(dt=datetime.datetime(2011, 9, 1, 10, 20, 30, tzinfo=UTC))
+        response = self.client.get(reverse('admin:timezones_event_change', args=(e.pk,)))
+        self.assertContains(response, e.dt.astimezone(EAT).date().isoformat())
+        self.assertContains(response, e.dt.astimezone(EAT).time().isoformat())
+
+    def test_change_editable_in_other_timezone(self):
+        e = Event.objects.create(dt=datetime.datetime(2011, 9, 1, 10, 20, 30, tzinfo=UTC))
+        with timezone.override(ICT):
+            response = self.client.get(reverse('admin:timezones_event_change', args=(e.pk,)))
+        self.assertContains(response, e.dt.astimezone(ICT).date().isoformat())
+        self.assertContains(response, e.dt.astimezone(ICT).time().isoformat())
+
+    def test_change_readonly(self):
+        Timestamp.objects.create()
+        # re-fetch the object for backends that lose microseconds (MySQL)
+        t = Timestamp.objects.get()
+        response = self.client.get(reverse('admin:timezones_timestamp_change', args=(t.pk,)))
+        self.assertContains(response, t.created.astimezone(EAT).isoformat())
+
+    def test_change_readonly_in_other_timezone(self):
+        Timestamp.objects.create()
+        # re-fetch the object for backends that lose microseconds (MySQL)
+        t = Timestamp.objects.get()
+        with timezone.override(ICT):
+            response = self.client.get(reverse('admin:timezones_timestamp_change', args=(t.pk,)))
+        self.assertContains(response, t.created.astimezone(ICT).isoformat())
+
+AdminTests = override_settings(DATETIME_FORMAT='c', USE_L10N=False, USE_TZ=True)(AdminTests)

+ 10 - 0
tests/modeltests/timezones/urls.py

@@ -0,0 +1,10 @@
+from __future__ import absolute_import
+
+from django.conf.urls import patterns, include
+from django.contrib import admin
+
+from . import admin as tz_admin
+
+urlpatterns = patterns('',
+    (r'^admin/', include(admin.site.urls)),
+)

+ 35 - 8
tests/modeltests/validation/test_error_messages.py

@@ -104,16 +104,43 @@ class ValidationMessagesTest(TestCase):
             f.clean('foo', None)
         except ValidationError, e:
             self.assertEqual(e.messages, [
-                u"'foo' value either has an invalid valid format "
-                u"(The format must be YYYY-MM-DD HH:MM[:ss[.uuuuuu]]) "
-                u"or is an invalid date/time."])
-        self.assertRaises(ValidationError, f.clean,
-                          '2011-10-32 10:10', None)
+                u"'foo' value has an invalid format. It must be "
+                u"in YYYY-MM-DD HH:MM[:ss[.uuuuuu]][TZ] format."])
+
+        # Correct format but invalid date
+        self.assertRaises(ValidationError, f.clean, '2011-10-32', None)
+        try:
+            f.clean('2011-10-32', None)
+        except ValidationError, e:
+            self.assertEqual(e.messages, [
+                u"'2011-10-32' value has the correct format "
+                u"(YYYY-MM-DD) but it is an invalid date."])
+
         # Correct format but invalid date/time
+        self.assertRaises(ValidationError, f.clean, '2011-10-32 10:10', None)
         try:
             f.clean('2011-10-32 10:10', None)
         except ValidationError, e:
             self.assertEqual(e.messages, [
-                u"'2011-10-32 10:10' value either has an invalid valid format "
-                u"(The format must be YYYY-MM-DD HH:MM[:ss[.uuuuuu]]) "
-                u"or is an invalid date/time."])
+                u"'2011-10-32 10:10' value has the correct format "
+                u"(YYYY-MM-DD HH:MM[:ss[.uuuuuu]][TZ]) "
+                u"but it is an invalid date/time."])
+
+    def test_time_field_raises_error_message(self):
+        f = models.TimeField()
+        # Wrong format
+        self.assertRaises(ValidationError, f.clean, 'foo', None)
+        try:
+            f.clean('foo', None)
+        except ValidationError, e:
+            self.assertEqual(e.messages, [
+                u"'foo' value has an invalid format. It must be in "
+                u"HH:MM[:ss[.uuuuuu]] format."])
+        # Correct format but invalid time
+        self.assertRaises(ValidationError, f.clean, '25:50', None)
+        try:
+            f.clean('25:50', None)
+        except ValidationError, e:
+            self.assertEqual(e.messages, [
+                u"'25:50' value has the correct format "
+                u"(HH:MM[:ss[.uuuuuu]]) but it is an invalid time."])

+ 15 - 3
tests/regressiontests/cache/tests.py

@@ -24,7 +24,7 @@ from django.template.response import TemplateResponse
 from django.test import TestCase, RequestFactory
 from django.test.utils import (get_warnings_state, restore_warnings_state,
     override_settings)
-from django.utils import translation, unittest
+from django.utils import timezone, translation, unittest
 from django.utils.cache import (patch_vary_headers, get_cache_key,
     learn_cache_key, patch_cache_control, patch_response_headers)
 from django.views.decorators.cache import cache_page
@@ -1154,7 +1154,7 @@ class CacheI18nTest(TestCase):
         request.session = {}
         return request
 
-    @override_settings(USE_I18N=True, USE_L10N=False)
+    @override_settings(USE_I18N=True, USE_L10N=False, USE_TZ=False)
     def test_cache_key_i18n_translation(self):
         request = self._get_request()
         lang = translation.get_language()
@@ -1164,7 +1164,7 @@ class CacheI18nTest(TestCase):
         key2 = get_cache_key(request)
         self.assertEqual(key, key2)
 
-    @override_settings(USE_I18N=False, USE_L10N=True)
+    @override_settings(USE_I18N=False, USE_L10N=True, USE_TZ=False)
     def test_cache_key_i18n_formatting(self):
         request = self._get_request()
         lang = translation.get_language()
@@ -1174,13 +1174,25 @@ class CacheI18nTest(TestCase):
         key2 = get_cache_key(request)
         self.assertEqual(key, key2)
 
+    @override_settings(USE_I18N=False, USE_L10N=False, USE_TZ=True)
+    def test_cache_key_i18n_timezone(self):
+        request = self._get_request()
+        tz = timezone.get_current_timezone_name()
+        response = HttpResponse()
+        key = learn_cache_key(request, response)
+        self.assertIn(tz, key, "Cache keys should include the time zone name when time zones are active")
+        key2 = get_cache_key(request)
+        self.assertEqual(key, key2)
+
     @override_settings(USE_I18N=False, USE_L10N=False)
     def test_cache_key_no_i18n (self):
         request = self._get_request()
         lang = translation.get_language()
+        tz = timezone.get_current_timezone_name()
         response = HttpResponse()
         key = learn_cache_key(request, response)
         self.assertNotIn(lang, key, "Cache keys shouldn't include the language name when i18n isn't active")
+        self.assertNotIn(tz, key, "Cache keys shouldn't include the time zone name when i18n isn't active")
 
     @override_settings(
             CACHE_MIDDLEWARE_KEY_PREFIX="test",

+ 2 - 2
tests/regressiontests/datatypes/tests.py

@@ -3,7 +3,7 @@ from __future__ import absolute_import
 import datetime
 
 from django.test import TestCase, skipIfDBFeature
-from django.utils import tzinfo
+from django.utils.timezone import utc
 
 from .models import Donut, RumBaba
 
@@ -79,7 +79,7 @@ class DataTypesTestCase(TestCase):
     def test_error_on_timezone(self):
         """Regression test for #8354: the MySQL and Oracle backends should raise
         an error if given a timezone-aware datetime object."""
-        dt = datetime.datetime(2008, 8, 31, 16, 20, tzinfo=tzinfo.FixedOffset(0))
+        dt = datetime.datetime(2008, 8, 31, 16, 20, tzinfo=utc)
         d = Donut(name='Bear claw', consumed_at=dt)
         self.assertRaises(ValueError, d.save)
         # ValueError: MySQL backend does not support timezone-aware datetimes.

+ 1 - 1
tests/regressiontests/defaultfilters/tests.py

@@ -421,7 +421,7 @@ class DefaultFiltersTests(TestCase):
 
     def test_timeuntil(self):
         self.assertEqual(
-            timeuntil_filter(datetime.datetime.now() + datetime.timedelta(1)),
+            timeuntil_filter(datetime.datetime.now() + datetime.timedelta(1, 1)),
             u'1 day')
 
         self.assertEqual(

+ 1 - 1
tests/regressiontests/utils/dateformat.py

@@ -4,6 +4,7 @@ import time
 
 from django.utils.dateformat import format
 from django.utils import dateformat, translation, unittest
+from django.utils.timezone import utc
 from django.utils.tzinfo import FixedOffset, LocalTimezone
 
 
@@ -56,7 +57,6 @@ class DateFormatTests(unittest.TestCase):
         self.assertEqual(datetime.fromtimestamp(int(format(dt, 'U')), ltz).utctimetuple(), dt.utctimetuple())
 
     def test_epoch(self):
-        utc = FixedOffset(0)
         udt = datetime(1970, 1, 1, tzinfo=utc)
         self.assertEqual(format(udt, 'U'), u'0')
 

+ 1 - 0
tests/regressiontests/utils/tests.py

@@ -23,3 +23,4 @@ from .datetime_safe import DatetimeTests
 from .baseconv import TestBaseConv
 from .jslex import JsTokensTest, JsToCForGettextTest
 from .ipv6 import TestUtilsIPv6
+from .timezone import TimezoneTests

+ 9 - 0
tests/regressiontests/utils/timesince.py

@@ -105,3 +105,12 @@ class TimesinceTests(unittest.TestCase):
         self.assertEqual(timeuntil(today+self.oneday, today), u'1 day')
         self.assertEqual(timeuntil(today-self.oneday, today), u'0 minutes')
         self.assertEqual(timeuntil(today+self.oneweek, today), u'1 week')
+
+    def test_naive_datetime_with_tzinfo_attribute(self):
+        class naive(datetime.tzinfo):
+            def utcoffset(self, dt):
+                return None
+        future = datetime.datetime(2080, 1, 1, tzinfo=naive())
+        self.assertEqual(timesince(future), u'0 minutes')
+        past = datetime.datetime(1980, 1, 1, tzinfo=naive())
+        self.assertEqual(timeuntil(past), u'0 minutes')

+ 18 - 0
tests/regressiontests/utils/timezone.py

@@ -0,0 +1,18 @@
+import copy
+import pickle
+from django.utils.timezone import UTC, LocalTimezone
+from django.utils import unittest
+
+class TimezoneTests(unittest.TestCase):
+
+    def test_copy(self):
+        self.assertIsInstance(copy.copy(UTC()), UTC)
+        self.assertIsInstance(copy.copy(LocalTimezone()), LocalTimezone)
+
+    def test_deepcopy(self):
+        self.assertIsInstance(copy.deepcopy(UTC()), UTC)
+        self.assertIsInstance(copy.deepcopy(LocalTimezone()), LocalTimezone)
+
+    def test_pickling_unpickling(self):
+        self.assertIsInstance(pickle.loads(pickle.dumps(UTC())), UTC)
+        self.assertIsInstance(pickle.loads(pickle.dumps(LocalTimezone())), LocalTimezone)

+ 17 - 0
tests/regressiontests/utils/tzinfo.py

@@ -1,5 +1,7 @@
+import copy
 import datetime
 import os
+import pickle
 import time
 from django.utils.tzinfo import FixedOffset, LocalTimezone
 from django.utils import unittest
@@ -60,3 +62,18 @@ class TzinfoTests(unittest.TestCase):
         self.assertEqual(
                 repr(datetime.datetime.fromtimestamp(ts + 3600, tz)),
                 'datetime.datetime(2010, 11, 7, 1, 0, tzinfo=EST)')
+
+    def test_copy(self):
+        now = datetime.datetime.now()
+        self.assertIsInstance(copy.copy(FixedOffset(90)), FixedOffset)
+        self.assertIsInstance(copy.copy(LocalTimezone(now)), LocalTimezone)
+
+    def test_deepcopy(self):
+        now = datetime.datetime.now()
+        self.assertIsInstance(copy.deepcopy(FixedOffset(90)), FixedOffset)
+        self.assertIsInstance(copy.deepcopy(LocalTimezone(now)), LocalTimezone)
+
+    def test_pickling_unpickling(self):
+        now = datetime.datetime.now()
+        self.assertIsInstance(pickle.loads(pickle.dumps(FixedOffset(90))), FixedOffset)
+        self.assertIsInstance(pickle.loads(pickle.dumps(LocalTimezone(now))), LocalTimezone)

Some files were not shown because too many files changed in this diff