summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAymeric Augustin <aymeric.augustin@m4x.org>2011-11-18 13:01:06 +0000
committerAymeric Augustin <aymeric.augustin@m4x.org>2011-11-18 13:01:06 +0000
commit9b1cb755a28f020e27d4268c214b25315d4de42e (patch)
tree2ff0827176f0eb49defa4ce7ce10164f2fc26e86
parent01f70349c9ef23d6751437dcd57d2efc193b2661 (diff)
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
-rw-r--r--django/conf/global_settings.py9
-rw-r--r--django/conf/project_template/project_name/settings.py5
-rw-r--r--django/contrib/admin/util.py3
-rw-r--r--django/contrib/humanize/templatetags/humanize.py18
-rw-r--r--django/contrib/humanize/tests.py72
-rw-r--r--django/contrib/syndication/views.py3
-rw-r--r--django/core/context_processors.py5
-rw-r--r--django/core/serializers/json.py25
-rw-r--r--django/db/backends/__init__.py3
-rw-r--r--django/db/backends/mysql/base.py27
-rw-r--r--django/db/backends/oracle/base.py45
-rw-r--r--django/db/backends/postgresql_psycopg2/base.py17
-rw-r--r--django/db/backends/sqlite3/base.py56
-rw-r--r--django/db/backends/util.py6
-rw-r--r--django/db/models/fields/__init__.py168
-rw-r--r--django/db/utils.py2
-rw-r--r--django/forms/fields.py18
-rw-r--r--django/forms/util.py31
-rw-r--r--django/forms/widgets.py3
-rw-r--r--django/template/base.py7
-rw-r--r--django/template/context.py12
-rw-r--r--django/template/debug.py2
-rw-r--r--django/template/defaultfilters.py4
-rw-r--r--django/templatetags/tz.py191
-rw-r--r--django/utils/cache.py5
-rw-r--r--django/utils/dateformat.py14
-rw-r--r--django/utils/dateparse.py93
-rw-r--r--django/utils/feedgenerator.py5
-rw-r--r--django/utils/timesince.py16
-rw-r--r--django/utils/timezone.py266
-rw-r--r--django/utils/tzinfo.py19
-rw-r--r--docs/howto/custom-template-tags.txt25
-rw-r--r--docs/ref/models/querysets.txt13
-rw-r--r--docs/ref/settings.txt54
-rw-r--r--docs/ref/templates/builtins.txt63
-rw-r--r--docs/ref/utils.txt125
-rw-r--r--docs/releases/1.4.txt53
-rw-r--r--docs/topics/cache.txt4
-rw-r--r--docs/topics/i18n/index.txt10
-rw-r--r--docs/topics/i18n/timezones.txt429
-rw-r--r--tests/modeltests/fixtures/tests.py22
-rw-r--r--tests/modeltests/serializers/tests.py6
-rw-r--r--tests/modeltests/timezones/__init__.py0
-rw-r--r--tests/modeltests/timezones/admin.py15
-rw-r--r--tests/modeltests/timezones/fixtures/users.xml17
-rw-r--r--tests/modeltests/timezones/forms.py13
-rw-r--r--tests/modeltests/timezones/models.py8
-rw-r--r--tests/modeltests/timezones/tests.py871
-rw-r--r--tests/modeltests/timezones/urls.py10
-rw-r--r--tests/modeltests/validation/test_error_messages.py43
-rw-r--r--tests/regressiontests/cache/tests.py18
-rw-r--r--tests/regressiontests/datatypes/tests.py4
-rw-r--r--tests/regressiontests/defaultfilters/tests.py2
-rw-r--r--tests/regressiontests/utils/dateformat.py2
-rw-r--r--tests/regressiontests/utils/tests.py1
-rw-r--r--tests/regressiontests/utils/timesince.py9
-rw-r--r--tests/regressiontests/utils/timezone.py18
-rw-r--r--tests/regressiontests/utils/tzinfo.py17
58 files changed, 2719 insertions, 283 deletions
diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py
index 6b09be23b2..2ef2f63618 100644
--- a/django/conf/global_settings.py
+++ b/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',
)
diff --git a/django/conf/project_template/project_name/settings.py b/django/conf/project_template/project_name/settings.py
index aaaeb37688..f28c61e15d 100644
--- a/django/conf/project_template/project_name/settings.py
+++ b/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 = ''
diff --git a/django/contrib/admin/util.py b/django/contrib/admin/util.py
index 7204a12fc4..af8fad382e 100644
--- a/django/contrib/admin/util.py
+++ b/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):
diff --git a/django/contrib/humanize/templatetags/humanize.py b/django/contrib/humanize/templatetags/humanize.py
index 4ed07125c0..b075ff05c7 100644
--- a/django/contrib/humanize/templatetags/humanize.py
+++ b/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:
diff --git a/django/contrib/humanize/tests.py b/django/contrib/humanize/tests.py
index c59885a0dc..0217136b11 100644
--- a/django/contrib/humanize/tests.py
+++ b/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
diff --git a/django/contrib/syndication/views.py b/django/contrib/syndication/views.py
index 8e7c1d125c..a6e18e3cd8 100644
--- a/django/contrib/syndication/views.py
+++ b/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)
diff --git a/django/core/context_processors.py b/django/core/context_processors.py
index 5597f68a55..80f1e76d35 100644
--- a/django/core/context_processors.py
+++ b/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.
diff --git a/django/core/serializers/json.py b/django/core/serializers/json.py
index b8119f54d4..2b4b2a382b 100644
--- a/django/core/serializers/json.py
+++ b/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:
diff --git a/django/db/backends/__init__.py b/django/db/backends/__init__.py
index d6bd380886..dd248787e2 100644
--- a/django/db/backends/__init__.py
+++ b/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):
diff --git a/django/db/backends/mysql/base.py b/django/db/backends/mysql/base.py
index e9f5b8b6a9..93aaf9971a 100644
--- a/django/db/backends/mysql/base.py
+++ b/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))
diff --git a/django/db/backends/oracle/base.py b/django/db/backends/oracle/base.py
index c9830f5a7c..c1974225f6 100644
--- a/django/db/backends/oracle/base.py
+++ b/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
diff --git a/django/db/backends/postgresql_psycopg2/base.py b/django/db/backends/postgresql_psycopg2/base.py
index 74342e9ac6..87df6bd1bc 100644
--- a/django/db/backends/postgresql_psycopg2/base.py
+++ b/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)
diff --git a/django/db/backends/sqlite3/base.py b/django/db/backends/sqlite3/base.py
index c437f1517e..a610606635 100644
--- a/django/db/backends/sqlite3/base.py
+++ b/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
diff --git a/django/db/backends/util.py b/django/db/backends/util.py
index 9d2d379132..b0463b79af 100644
--- a/django/db/backends/util.py
+++ b/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 == '':
diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py
index 55b993af02..5e7cf804e8 100644
--- a/django/db/models/fields/__init__.py
+++ b/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}
diff --git a/django/db/utils.py b/django/db/utils.py
index 1d7994b336..f0c13e3aac 100644
--- a/django/db/utils.py
+++ b/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']:
diff --git a/django/forms/fields.py b/django/forms/fields.py
index 7f8bac5052..0ba7467191 100644
--- a/django/forms/fields.py
+++ b/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
diff --git a/django/forms/util.py b/django/forms/util.py
index 85baffa005..886f08e581 100644
--- a/django/forms/util.py
+++ b/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
diff --git a/django/forms/widgets.py b/django/forms/widgets.py
index 8c1e20b179..f756957960 100644
--- a/django/forms/widgets.py
+++ b/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]
diff --git a/django/template/base.py b/django/template/base.py
index 238e2db9ef..572ec6138f 100644
--- a/django/template/base.py
+++ b/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
diff --git a/django/template/context.py b/django/template/context.py
index 8f70d704e5..bbd38ad468 100644
--- a/django/template/context.py
+++ b/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:
diff --git a/django/template/debug.py b/django/template/debug.py
index d11f1ee265..d4b973b5e0 100644
--- a/django/template/debug.py
+++ b/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:
diff --git a/django/template/defaultfilters.py b/django/template/defaultfilters.py
index 7484c7bf28..6b0e2f0b06 100644
--- a/django/template/defaultfilters.py
+++ b/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''):
diff --git a/django/templatetags/tz.py b/django/templatetags/tz.py
new file mode 100644
index 0000000000..14b613e24f
--- /dev/null
+++ b/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])
diff --git a/django/utils/cache.py b/django/utils/cache.py
index be4fa58645..1015c2f277 100644
--- a/django/utils/cache.py
+++ b/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):
diff --git a/django/utils/dateformat.py b/django/utils/dateformat.py
index 0afda1850d..d87fb13105 100644
--- a/django/utils/dateformat.py
+++ b/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()))
diff --git a/django/utils/dateparse.py b/django/utils/dateparse.py
new file mode 100644
index 0000000000..3ce475f5ea
--- /dev/null
+++ b/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)
diff --git a/django/utils/feedgenerator.py b/django/utils/feedgenerator.py
index 7d7c7af65c..df3cf41652 100644
--- a/django/utils/feedgenerator.py
+++ b/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)
diff --git a/django/utils/timesince.py b/django/utils/timesince.py
index 369a3e2c01..511acb518a 100644
--- a/django/utils/timesince.py
+++ b/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)
diff --git a/django/utils/timezone.py b/django/utils/timezone.py
new file mode 100644
index 0000000000..22860eb8cc
--- /dev/null
+++ b/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)
diff --git a/django/utils/tzinfo.py b/django/utils/tzinfo.py
index daffffb496..a07b635a99 100644
--- a/django/utils/tzinfo.py
+++ b/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)
diff --git a/docs/howto/custom-template-tags.txt b/docs/howto/custom-template-tags.txt
index 6b2355e8d0..0b962f8863 100644
--- a/docs/howto/custom-template-tags.txt
+++ b/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
----------------------------
diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt
index 68da9c7343..9736d94838 100644
--- a/docs/ref/models/querysets.txt
+++ b/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
diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt
index 20366e353d..0d91f16821 100644
--- a/docs/ref/settings.txt
+++ b/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
diff --git a/docs/ref/templates/builtins.txt b/docs/ref/templates/builtins.txt
index 7b305746ab..695a21296b 100644
--- a/docs/ref/templates/builtins.txt
+++ b/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
^^^^^^
diff --git a/docs/ref/utils.txt b/docs/ref/utils.txt
index 0d6e9c6b61..9a9adba0d8 100644
--- a/docs/ref/utils.txt
+++ b/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``
=======================
diff --git a/docs/releases/1.4.txt b/docs/releases/1.4.txt
index 49284fb90b..46527da581 100644
--- a/docs/releases/1.4.txt
+++ b/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
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/docs/topics/cache.txt b/docs/topics/cache.txt
index a3aac74b76..99d764b60d 100644
--- a/docs/topics/cache.txt
+++ b/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`_
diff --git a/docs/topics/i18n/index.txt b/docs/topics/i18n/index.txt
index 4e2c9c345c..25ec8392de 100644
--- a/docs/topics/i18n/index.txt
+++ b/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
===========
diff --git a/docs/topics/i18n/timezones.txt b/docs/topics/i18n/timezones.txt
new file mode 100644
index 0000000000..41e8380acc
--- /dev/null
+++ b/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
diff --git a/tests/modeltests/fixtures/tests.py b/tests/modeltests/fixtures/tests.py
index 2dca7ced9c..e7daf2d861 100644
--- a/tests/modeltests/fixtures/tests.py
+++ b/tests/modeltests/fixtures/tests.py
@@ -56,25 +56,25 @@ class FixtureLoadingTests(TestCase):
])
# Dump the current contents of the database as a JSON fixture
- self._dumpdata_assert(['fixtures'], '[{"pk": 1, "model": "fixtures.category", "fields": {"description": "Latest news stories", "title": "News Stories"}}, {"pk": 3, "model": "fixtures.article", "fields": {"headline": "Time to reform copyright", "pub_date": "2006-06-16 13:00:00"}}, {"pk": 2, "model": "fixtures.article", "fields": {"headline": "Poker has no place on ESPN", "pub_date": "2006-06-16 12:00:00"}}, {"pk": 1, "model": "fixtures.article", "fields": {"headline": "Python program becomes self aware", "pub_date": "2006-06-16 11:00:00"}}]')
+ self._dumpdata_assert(['fixtures'], '[{"pk": 1, "model": "fixtures.category", "fields": {"description": "Latest news stories", "title": "News Stories"}}, {"pk": 3, "model": "fixtures.article", "fields": {"headline": "Time to reform copyright", "pub_date": "2006-06-16T13:00:00"}}, {"pk": 2, "model": "fixtures.article", "fields": {"headline": "Poker has no place on ESPN", "pub_date": "2006-06-16T12:00:00"}}, {"pk": 1, "model": "fixtures.article", "fields": {"headline": "Python program becomes self aware", "pub_date": "2006-06-16T11:00:00"}}]')
# Try just dumping the contents of fixtures.Category
self._dumpdata_assert(['fixtures.Category'], '[{"pk": 1, "model": "fixtures.category", "fields": {"description": "Latest news stories", "title": "News Stories"}}]')
# ...and just fixtures.Article
- self._dumpdata_assert(['fixtures.Article'], '[{"pk": 3, "model": "fixtures.article", "fields": {"headline": "Time to reform copyright", "pub_date": "2006-06-16 13:00:00"}}, {"pk": 2, "model": "fixtures.article", "fields": {"headline": "Poker has no place on ESPN", "pub_date": "2006-06-16 12:00:00"}}, {"pk": 1, "model": "fixtures.article", "fields": {"headline": "Python program becomes self aware", "pub_date": "2006-06-16 11:00:00"}}]')
+ self._dumpdata_assert(['fixtures.Article'], '[{"pk": 3, "model": "fixtures.article", "fields": {"headline": "Time to reform copyright", "pub_date": "2006-06-16T13:00:00"}}, {"pk": 2, "model": "fixtures.article", "fields": {"headline": "Poker has no place on ESPN", "pub_date": "2006-06-16T12:00:00"}}, {"pk": 1, "model": "fixtures.article", "fields": {"headline": "Python program becomes self aware", "pub_date": "2006-06-16T11:00:00"}}]')
# ...and both
- self._dumpdata_assert(['fixtures.Category', 'fixtures.Article'], '[{"pk": 1, "model": "fixtures.category", "fields": {"description": "Latest news stories", "title": "News Stories"}}, {"pk": 3, "model": "fixtures.article", "fields": {"headline": "Time to reform copyright", "pub_date": "2006-06-16 13:00:00"}}, {"pk": 2, "model": "fixtures.article", "fields": {"headline": "Poker has no place on ESPN", "pub_date": "2006-06-16 12:00:00"}}, {"pk": 1, "model": "fixtures.article", "fields": {"headline": "Python program becomes self aware", "pub_date": "2006-06-16 11:00:00"}}]')
+ self._dumpdata_assert(['fixtures.Category', 'fixtures.Article'], '[{"pk": 1, "model": "fixtures.category", "fields": {"description": "Latest news stories", "title": "News Stories"}}, {"pk": 3, "model": "fixtures.article", "fields": {"headline": "Time to reform copyright", "pub_date": "2006-06-16T13:00:00"}}, {"pk": 2, "model": "fixtures.article", "fields": {"headline": "Poker has no place on ESPN", "pub_date": "2006-06-16T12:00:00"}}, {"pk": 1, "model": "fixtures.article", "fields": {"headline": "Python program becomes self aware", "pub_date": "2006-06-16T11:00:00"}}]')
# Specify a specific model twice
- self._dumpdata_assert(['fixtures.Article', 'fixtures.Article'], '[{"pk": 3, "model": "fixtures.article", "fields": {"headline": "Time to reform copyright", "pub_date": "2006-06-16 13:00:00"}}, {"pk": 2, "model": "fixtures.article", "fields": {"headline": "Poker has no place on ESPN", "pub_date": "2006-06-16 12:00:00"}}, {"pk": 1, "model": "fixtures.article", "fields": {"headline": "Python program becomes self aware", "pub_date": "2006-06-16 11:00:00"}}]')
+ self._dumpdata_assert(['fixtures.Article', 'fixtures.Article'], '[{"pk": 3, "model": "fixtures.article", "fields": {"headline": "Time to reform copyright", "pub_date": "2006-06-16T13:00:00"}}, {"pk": 2, "model": "fixtures.article", "fields": {"headline": "Poker has no place on ESPN", "pub_date": "2006-06-16T12:00:00"}}, {"pk": 1, "model": "fixtures.article", "fields": {"headline": "Python program becomes self aware", "pub_date": "2006-06-16T11:00:00"}}]')
# Specify a dump that specifies Article both explicitly and implicitly
- self._dumpdata_assert(['fixtures.Article', 'fixtures'], '[{"pk": 1, "model": "fixtures.category", "fields": {"description": "Latest news stories", "title": "News Stories"}}, {"pk": 3, "model": "fixtures.article", "fields": {"headline": "Time to reform copyright", "pub_date": "2006-06-16 13:00:00"}}, {"pk": 2, "model": "fixtures.article", "fields": {"headline": "Poker has no place on ESPN", "pub_date": "2006-06-16 12:00:00"}}, {"pk": 1, "model": "fixtures.article", "fields": {"headline": "Python program becomes self aware", "pub_date": "2006-06-16 11:00:00"}}]')
+ self._dumpdata_assert(['fixtures.Article', 'fixtures'], '[{"pk": 1, "model": "fixtures.category", "fields": {"description": "Latest news stories", "title": "News Stories"}}, {"pk": 3, "model": "fixtures.article", "fields": {"headline": "Time to reform copyright", "pub_date": "2006-06-16T13:00:00"}}, {"pk": 2, "model": "fixtures.article", "fields": {"headline": "Poker has no place on ESPN", "pub_date": "2006-06-16T12:00:00"}}, {"pk": 1, "model": "fixtures.article", "fields": {"headline": "Python program becomes self aware", "pub_date": "2006-06-16T11:00:00"}}]')
# Same again, but specify in the reverse order
- self._dumpdata_assert(['fixtures'], '[{"pk": 1, "model": "fixtures.category", "fields": {"description": "Latest news stories", "title": "News Stories"}}, {"pk": 3, "model": "fixtures.article", "fields": {"headline": "Time to reform copyright", "pub_date": "2006-06-16 13:00:00"}}, {"pk": 2, "model": "fixtures.article", "fields": {"headline": "Poker has no place on ESPN", "pub_date": "2006-06-16 12:00:00"}}, {"pk": 1, "model": "fixtures.article", "fields": {"headline": "Python program becomes self aware", "pub_date": "2006-06-16 11:00:00"}}]')
+ self._dumpdata_assert(['fixtures'], '[{"pk": 1, "model": "fixtures.category", "fields": {"description": "Latest news stories", "title": "News Stories"}}, {"pk": 3, "model": "fixtures.article", "fields": {"headline": "Time to reform copyright", "pub_date": "2006-06-16T13:00:00"}}, {"pk": 2, "model": "fixtures.article", "fields": {"headline": "Poker has no place on ESPN", "pub_date": "2006-06-16T12:00:00"}}, {"pk": 1, "model": "fixtures.article", "fields": {"headline": "Python program becomes self aware", "pub_date": "2006-06-16T11:00:00"}}]')
# Specify one model from one application, and an entire other application.
self._dumpdata_assert(['fixtures.Category', 'sites'], '[{"pk": 1, "model": "fixtures.category", "fields": {"description": "Latest news stories", "title": "News Stories"}}, {"pk": 1, "model": "sites.site", "fields": {"domain": "example.com", "name": "example.com"}}]')
@@ -153,11 +153,11 @@ class FixtureLoadingTests(TestCase):
self._dumpdata_assert(['fixtures.book'], '[{"pk": 1, "model": "fixtures.book", "fields": {"name": "Music for all ages", "authors": [["Artist formerly known as \\"Prince\\""], ["Django Reinhardt"]]}}]', natural_keys=True)
# Dump the current contents of the database as a JSON fixture
- self._dumpdata_assert(['fixtures'], '[{"pk": 1, "model": "fixtures.category", "fields": {"description": "Latest news stories", "title": "News Stories"}}, {"pk": 5, "model": "fixtures.article", "fields": {"headline": "XML identified as leading cause of cancer", "pub_date": "2006-06-16 16:00:00"}}, {"pk": 4, "model": "fixtures.article", "fields": {"headline": "Django conquers world!", "pub_date": "2006-06-16 15:00:00"}}, {"pk": 3, "model": "fixtures.article", "fields": {"headline": "Copyright is fine the way it is", "pub_date": "2006-06-16 14:00:00"}}, {"pk": 2, "model": "fixtures.article", "fields": {"headline": "Poker on TV is great!", "pub_date": "2006-06-16 11:00:00"}}, {"pk": 1, "model": "fixtures.article", "fields": {"headline": "Python program becomes self aware", "pub_date": "2006-06-16 11:00:00"}}, {"pk": 1, "model": "fixtures.tag", "fields": {"tagged_type": ["fixtures", "article"], "name": "copyright", "tagged_id": 3}}, {"pk": 2, "model": "fixtures.tag", "fields": {"tagged_type": ["fixtures", "article"], "name": "legal", "tagged_id": 3}}, {"pk": 3, "model": "fixtures.tag", "fields": {"tagged_type": ["fixtures", "article"], "name": "django", "tagged_id": 4}}, {"pk": 4, "model": "fixtures.tag", "fields": {"tagged_type": ["fixtures", "article"], "name": "world domination", "tagged_id": 4}}, {"pk": 3, "model": "fixtures.person", "fields": {"name": "Artist formerly known as \\"Prince\\""}}, {"pk": 1, "model": "fixtures.person", "fields": {"name": "Django Reinhardt"}}, {"pk": 2, "model": "fixtures.person", "fields": {"name": "Stephane Grappelli"}}, {"pk": 1, "model": "fixtures.visa", "fields": {"person": ["Django Reinhardt"], "permissions": [["add_user", "auth", "user"], ["change_user", "auth", "user"], ["delete_user", "auth", "user"]]}}, {"pk": 2, "model": "fixtures.visa", "fields": {"person": ["Stephane Grappelli"], "permissions": [["add_user", "auth", "user"], ["delete_user", "auth", "user"]]}}, {"pk": 3, "model": "fixtures.visa", "fields": {"person": ["Artist formerly known as \\"Prince\\""], "permissions": [["change_user", "auth", "user"]]}}, {"pk": 1, "model": "fixtures.book", "fields": {"name": "Music for all ages", "authors": [["Artist formerly known as \\"Prince\\""], ["Django Reinhardt"]]}}]', natural_keys=True)
+ self._dumpdata_assert(['fixtures'], '[{"pk": 1, "model": "fixtures.category", "fields": {"description": "Latest news stories", "title": "News Stories"}}, {"pk": 5, "model": "fixtures.article", "fields": {"headline": "XML identified as leading cause of cancer", "pub_date": "2006-06-16T16:00:00"}}, {"pk": 4, "model": "fixtures.article", "fields": {"headline": "Django conquers world!", "pub_date": "2006-06-16T15:00:00"}}, {"pk": 3, "model": "fixtures.article", "fields": {"headline": "Copyright is fine the way it is", "pub_date": "2006-06-16T14:00:00"}}, {"pk": 2, "model": "fixtures.article", "fields": {"headline": "Poker on TV is great!", "pub_date": "2006-06-16T11:00:00"}}, {"pk": 1, "model": "fixtures.article", "fields": {"headline": "Python program becomes self aware", "pub_date": "2006-06-16T11:00:00"}}, {"pk": 1, "model": "fixtures.tag", "fields": {"tagged_type": ["fixtures", "article"], "name": "copyright", "tagged_id": 3}}, {"pk": 2, "model": "fixtures.tag", "fields": {"tagged_type": ["fixtures", "article"], "name": "legal", "tagged_id": 3}}, {"pk": 3, "model": "fixtures.tag", "fields": {"tagged_type": ["fixtures", "article"], "name": "django", "tagged_id": 4}}, {"pk": 4, "model": "fixtures.tag", "fields": {"tagged_type": ["fixtures", "article"], "name": "world domination", "tagged_id": 4}}, {"pk": 3, "model": "fixtures.person", "fields": {"name": "Artist formerly known as \\"Prince\\""}}, {"pk": 1, "model": "fixtures.person", "fields": {"name": "Django Reinhardt"}}, {"pk": 2, "model": "fixtures.person", "fields": {"name": "Stephane Grappelli"}}, {"pk": 1, "model": "fixtures.visa", "fields": {"person": ["Django Reinhardt"], "permissions": [["add_user", "auth", "user"], ["change_user", "auth", "user"], ["delete_user", "auth", "user"]]}}, {"pk": 2, "model": "fixtures.visa", "fields": {"person": ["Stephane Grappelli"], "permissions": [["add_user", "auth", "user"], ["delete_user", "auth", "user"]]}}, {"pk": 3, "model": "fixtures.visa", "fields": {"person": ["Artist formerly known as \\"Prince\\""], "permissions": [["change_user", "auth", "user"]]}}, {"pk": 1, "model": "fixtures.book", "fields": {"name": "Music for all ages", "authors": [["Artist formerly known as \\"Prince\\""], ["Django Reinhardt"]]}}]', natural_keys=True)
# Dump the current contents of the database as an XML fixture
self._dumpdata_assert(['fixtures'], """<?xml version="1.0" encoding="utf-8"?>
-<django-objects version="1.0"><object pk="1" model="fixtures.category"><field type="CharField" name="title">News Stories</field><field type="TextField" name="description">Latest news stories</field></object><object pk="5" model="fixtures.article"><field type="CharField" name="headline">XML identified as leading cause of cancer</field><field type="DateTimeField" name="pub_date">2006-06-16 16:00:00</field></object><object pk="4" model="fixtures.article"><field type="CharField" name="headline">Django conquers world!</field><field type="DateTimeField" name="pub_date">2006-06-16 15:00:00</field></object><object pk="3" model="fixtures.article"><field type="CharField" name="headline">Copyright is fine the way it is</field><field type="DateTimeField" name="pub_date">2006-06-16 14:00:00</field></object><object pk="2" model="fixtures.article"><field type="CharField" name="headline">Poker on TV is great!</field><field type="DateTimeField" name="pub_date">2006-06-16 11:00:00</field></object><object pk="1" model="fixtures.article"><field type="CharField" name="headline">Python program becomes self aware</field><field type="DateTimeField" name="pub_date">2006-06-16 11:00:00</field></object><object pk="1" model="fixtures.tag"><field type="CharField" name="name">copyright</field><field to="contenttypes.contenttype" name="tagged_type" rel="ManyToOneRel"><natural>fixtures</natural><natural>article</natural></field><field type="PositiveIntegerField" name="tagged_id">3</field></object><object pk="2" model="fixtures.tag"><field type="CharField" name="name">legal</field><field to="contenttypes.contenttype" name="tagged_type" rel="ManyToOneRel"><natural>fixtures</natural><natural>article</natural></field><field type="PositiveIntegerField" name="tagged_id">3</field></object><object pk="3" model="fixtures.tag"><field type="CharField" name="name">django</field><field to="contenttypes.contenttype" name="tagged_type" rel="ManyToOneRel"><natural>fixtures</natural><natural>article</natural></field><field type="PositiveIntegerField" name="tagged_id">4</field></object><object pk="4" model="fixtures.tag"><field type="CharField" name="name">world domination</field><field to="contenttypes.contenttype" name="tagged_type" rel="ManyToOneRel"><natural>fixtures</natural><natural>article</natural></field><field type="PositiveIntegerField" name="tagged_id">4</field></object><object pk="3" model="fixtures.person"><field type="CharField" name="name">Artist formerly known as "Prince"</field></object><object pk="1" model="fixtures.person"><field type="CharField" name="name">Django Reinhardt</field></object><object pk="2" model="fixtures.person"><field type="CharField" name="name">Stephane Grappelli</field></object><object pk="1" model="fixtures.visa"><field to="fixtures.person" name="person" rel="ManyToOneRel"><natural>Django Reinhardt</natural></field><field to="auth.permission" name="permissions" rel="ManyToManyRel"><object><natural>add_user</natural><natural>auth</natural><natural>user</natural></object><object><natural>change_user</natural><natural>auth</natural><natural>user</natural></object><object><natural>delete_user</natural><natural>auth</natural><natural>user</natural></object></field></object><object pk="2" model="fixtures.visa"><field to="fixtures.person" name="person" rel="ManyToOneRel"><natural>Stephane Grappelli</natural></field><field to="auth.permission" name="permissions" rel="ManyToManyRel"><object><natural>add_user</natural><natural>auth</natural><natural>user</natural></object><object><natural>delete_user</natural><natural>auth</natural><natural>user</natural></object></field></object><object pk="3" model="fixtures.visa"><field to="fixtures.person" name="person" rel="ManyToOneRel"><natural>Artist formerly known as "Prince"</natural></field><field to="auth.permission" name="permissions" rel="ManyToManyRel"><object><natural>change_user</natural><natural>auth</natural><natural>user</natural></object></field></object><object pk="1" model="fixtures.book"><field type="CharField" name="name">Music for all ages</field><field to="fixtures.person" name="authors" rel="ManyToManyRel"><object><natural>Artist formerly known as "Prince"</natural></object><object><natural>Django Reinhardt</natural></object></field></object></django-objects>""", format='xml', natural_keys=True)
+<django-objects version="1.0"><object pk="1" model="fixtures.category"><field type="CharField" name="title">News Stories</field><field type="TextField" name="description">Latest news stories</field></object><object pk="5" model="fixtures.article"><field type="CharField" name="headline">XML identified as leading cause of cancer</field><field type="DateTimeField" name="pub_date">2006-06-16T16:00:00</field></object><object pk="4" model="fixtures.article"><field type="CharField" name="headline">Django conquers world!</field><field type="DateTimeField" name="pub_date">2006-06-16T15:00:00</field></object><object pk="3" model="fixtures.article"><field type="CharField" name="headline">Copyright is fine the way it is</field><field type="DateTimeField" name="pub_date">2006-06-16T14:00:00</field></object><object pk="2" model="fixtures.article"><field type="CharField" name="headline">Poker on TV is great!</field><field type="DateTimeField" name="pub_date">2006-06-16T11:00:00</field></object><object pk="1" model="fixtures.article"><field type="CharField" name="headline">Python program becomes self aware</field><field type="DateTimeField" name="pub_date">2006-06-16T11:00:00</field></object><object pk="1" model="fixtures.tag"><field type="CharField" name="name">copyright</field><field to="contenttypes.contenttype" name="tagged_type" rel="ManyToOneRel"><natural>fixtures</natural><natural>article</natural></field><field type="PositiveIntegerField" name="tagged_id">3</field></object><object pk="2" model="fixtures.tag"><field type="CharField" name="name">legal</field><field to="contenttypes.contenttype" name="tagged_type" rel="ManyToOneRel"><natural>fixtures</natural><natural>article</natural></field><field type="PositiveIntegerField" name="tagged_id">3</field></object><object pk="3" model="fixtures.tag"><field type="CharField" name="name">django</field><field to="contenttypes.contenttype" name="tagged_type" rel="ManyToOneRel"><natural>fixtures</natural><natural>article</natural></field><field type="PositiveIntegerField" name="tagged_id">4</field></object><object pk="4" model="fixtures.tag"><field type="CharField" name="name">world domination</field><field to="contenttypes.contenttype" name="tagged_type" rel="ManyToOneRel"><natural>fixtures</natural><natural>article</natural></field><field type="PositiveIntegerField" name="tagged_id">4</field></object><object pk="3" model="fixtures.person"><field type="CharField" name="name">Artist formerly known as "Prince"</field></object><object pk="1" model="fixtures.person"><field type="CharField" name="name">Django Reinhardt</field></object><object pk="2" model="fixtures.person"><field type="CharField" name="name">Stephane Grappelli</field></object><object pk="1" model="fixtures.visa"><field to="fixtures.person" name="person" rel="ManyToOneRel"><natural>Django Reinhardt</natural></field><field to="auth.permission" name="permissions" rel="ManyToManyRel"><object><natural>add_user</natural><natural>auth</natural><natural>user</natural></object><object><natural>change_user</natural><natural>auth</natural><natural>user</natural></object><object><natural>delete_user</natural><natural>auth</natural><natural>user</natural></object></field></object><object pk="2" model="fixtures.visa"><field to="fixtures.person" name="person" rel="ManyToOneRel"><natural>Stephane Grappelli</natural></field><field to="auth.permission" name="permissions" rel="ManyToManyRel"><object><natural>add_user</natural><natural>auth</natural><natural>user</natural></object><object><natural>delete_user</natural><natural>auth</natural><natural>user</natural></object></field></object><object pk="3" model="fixtures.visa"><field to="fixtures.person" name="person" rel="ManyToOneRel"><natural>Artist formerly known as "Prince"</natural></field><field to="auth.permission" name="permissions" rel="ManyToManyRel"><object><natural>change_user</natural><natural>auth</natural><natural>user</natural></object></field></object><object pk="1" model="fixtures.book"><field type="CharField" name="name">Music for all ages</field><field to="fixtures.person" name="authors" rel="ManyToManyRel"><object><natural>Artist formerly known as "Prince"</natural></object><object><natural>Django Reinhardt</natural></object></field></object></django-objects>""", format='xml', natural_keys=True)
def test_dumpdata_with_excludes(self):
# Load fixture1 which has a site, two articles, and a category
@@ -305,11 +305,11 @@ class FixtureLoadingTests(TestCase):
])
# Dump the current contents of the database as a JSON fixture
- self._dumpdata_assert(['fixtures'], '[{"pk": 1, "model": "fixtures.category", "fields": {"description": "Latest news stories", "title": "News Stories"}}, {"pk": 3, "model": "fixtures.article", "fields": {"headline": "Time to reform copyright", "pub_date": "2006-06-16 13:00:00"}}, {"pk": 2, "model": "fixtures.article", "fields": {"headline": "Poker has no place on ESPN", "pub_date": "2006-06-16 12:00:00"}}, {"pk": 1, "model": "fixtures.article", "fields": {"headline": "Python program becomes self aware", "pub_date": "2006-06-16 11:00:00"}}, {"pk": 1, "model": "fixtures.tag", "fields": {"tagged_type": ["fixtures", "article"], "name": "copyright", "tagged_id": 3}}, {"pk": 2, "model": "fixtures.tag", "fields": {"tagged_type": ["fixtures", "article"], "name": "law", "tagged_id": 3}}, {"pk": 1, "model": "fixtures.person", "fields": {"name": "Django Reinhardt"}}, {"pk": 3, "model": "fixtures.person", "fields": {"name": "Prince"}}, {"pk": 2, "model": "fixtures.person", "fields": {"name": "Stephane Grappelli"}}]', natural_keys=True)
+ self._dumpdata_assert(['fixtures'], '[{"pk": 1, "model": "fixtures.category", "fields": {"description": "Latest news stories", "title": "News Stories"}}, {"pk": 3, "model": "fixtures.article", "fields": {"headline": "Time to reform copyright", "pub_date": "2006-06-16T13:00:00"}}, {"pk": 2, "model": "fixtures.article", "fields": {"headline": "Poker has no place on ESPN", "pub_date": "2006-06-16T12:00:00"}}, {"pk": 1, "model": "fixtures.article", "fields": {"headline": "Python program becomes self aware", "pub_date": "2006-06-16T11:00:00"}}, {"pk": 1, "model": "fixtures.tag", "fields": {"tagged_type": ["fixtures", "article"], "name": "copyright", "tagged_id": 3}}, {"pk": 2, "model": "fixtures.tag", "fields": {"tagged_type": ["fixtures", "article"], "name": "law", "tagged_id": 3}}, {"pk": 1, "model": "fixtures.person", "fields": {"name": "Django Reinhardt"}}, {"pk": 3, "model": "fixtures.person", "fields": {"name": "Prince"}}, {"pk": 2, "model": "fixtures.person", "fields": {"name": "Stephane Grappelli"}}]', natural_keys=True)
# Dump the current contents of the database as an XML fixture
self._dumpdata_assert(['fixtures'], """<?xml version="1.0" encoding="utf-8"?>
-<django-objects version="1.0"><object pk="1" model="fixtures.category"><field type="CharField" name="title">News Stories</field><field type="TextField" name="description">Latest news stories</field></object><object pk="3" model="fixtures.article"><field type="CharField" name="headline">Time to reform copyright</field><field type="DateTimeField" name="pub_date">2006-06-16 13:00:00</field></object><object pk="2" model="fixtures.article"><field type="CharField" name="headline">Poker has no place on ESPN</field><field type="DateTimeField" name="pub_date">2006-06-16 12:00:00</field></object><object pk="1" model="fixtures.article"><field type="CharField" name="headline">Python program becomes self aware</field><field type="DateTimeField" name="pub_date">2006-06-16 11:00:00</field></object><object pk="1" model="fixtures.tag"><field type="CharField" name="name">copyright</field><field to="contenttypes.contenttype" name="tagged_type" rel="ManyToOneRel"><natural>fixtures</natural><natural>article</natural></field><field type="PositiveIntegerField" name="tagged_id">3</field></object><object pk="2" model="fixtures.tag"><field type="CharField" name="name">law</field><field to="contenttypes.contenttype" name="tagged_type" rel="ManyToOneRel"><natural>fixtures</natural><natural>article</natural></field><field type="PositiveIntegerField" name="tagged_id">3</field></object><object pk="1" model="fixtures.person"><field type="CharField" name="name">Django Reinhardt</field></object><object pk="3" model="fixtures.person"><field type="CharField" name="name">Prince</field></object><object pk="2" model="fixtures.person"><field type="CharField" name="name">Stephane Grappelli</field></object></django-objects>""", format='xml', natural_keys=True)
+<django-objects version="1.0"><object pk="1" model="fixtures.category"><field type="CharField" name="title">News Stories</field><field type="TextField" name="description">Latest news stories</field></object><object pk="3" model="fixtures.article"><field type="CharField" name="headline">Time to reform copyright</field><field type="DateTimeField" name="pub_date">2006-06-16T13:00:00</field></object><object pk="2" model="fixtures.article"><field type="CharField" name="headline">Poker has no place on ESPN</field><field type="DateTimeField" name="pub_date">2006-06-16T12:00:00</field></object><object pk="1" model="fixtures.article"><field type="CharField" name="headline">Python program becomes self aware</field><field type="DateTimeField" name="pub_date">2006-06-16T11:00:00</field></object><object pk="1" model="fixtures.tag"><field type="CharField" name="name">copyright</field><field to="contenttypes.contenttype" name="tagged_type" rel="ManyToOneRel"><natural>fixtures</natural><natural>article</natural></field><field type="PositiveIntegerField" name="tagged_id">3</field></object><object pk="2" model="fixtures.tag"><field type="CharField" name="name">law</field><field to="contenttypes.contenttype" name="tagged_type" rel="ManyToOneRel"><natural>fixtures</natural><natural>article</natural></field><field type="PositiveIntegerField" name="tagged_id">3</field></object><object pk="1" model="fixtures.person"><field type="CharField" name="name">Django Reinhardt</field></object><object pk="3" model="fixtures.person"><field type="CharField" name="name">Prince</field></object><object pk="2" model="fixtures.person"><field type="CharField" name="name">Stephane Grappelli</field></object></django-objects>""", format='xml', natural_keys=True)
class FixtureTransactionTests(TransactionTestCase):
def _dumpdata_assert(self, args, output, format='json'):
@@ -344,7 +344,7 @@ class FixtureTransactionTests(TransactionTestCase):
])
# Dump the current contents of the database as a JSON fixture
- self._dumpdata_assert(['fixtures'], '[{"pk": 1, "model": "fixtures.category", "fields": {"description": "Latest news stories", "title": "News Stories"}}, {"pk": 3, "model": "fixtures.article", "fields": {"headline": "Time to reform copyright", "pub_date": "2006-06-16 13:00:00"}}, {"pk": 2, "model": "fixtures.article", "fields": {"headline": "Poker has no place on ESPN", "pub_date": "2006-06-16 12:00:00"}}, {"pk": 1, "model": "fixtures.article", "fields": {"headline": "Python program becomes self aware", "pub_date": "2006-06-16 11:00:00"}}]')
+ self._dumpdata_assert(['fixtures'], '[{"pk": 1, "model": "fixtures.category", "fields": {"description": "Latest news stories", "title": "News Stories"}}, {"pk": 3, "model": "fixtures.article", "fields": {"headline": "Time to reform copyright", "pub_date": "2006-06-16T13:00:00"}}, {"pk": 2, "model": "fixtures.article", "fields": {"headline": "Poker has no place on ESPN", "pub_date": "2006-06-16T12:00:00"}}, {"pk": 1, "model": "fixtures.article", "fields": {"headline": "Python program becomes self aware", "pub_date": "2006-06-16T11:00:00"}}]')
# Load fixture 4 (compressed), using format discovery
management.call_command('loaddata', 'fixture4', verbosity=0, commit=False)
diff --git a/tests/modeltests/serializers/tests.py b/tests/modeltests/serializers/tests.py
index 99c065aeaf..721ca09282 100644
--- a/tests/modeltests/serializers/tests.py
+++ b/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
}
diff --git a/tests/modeltests/timezones/__init__.py b/tests/modeltests/timezones/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/tests/modeltests/timezones/__init__.py
diff --git a/tests/modeltests/timezones/admin.py b/tests/modeltests/timezones/admin.py
new file mode 100644
index 0000000000..4c199813e2
--- /dev/null
+++ b/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)
diff --git a/tests/modeltests/timezones/fixtures/users.xml b/tests/modeltests/timezones/fixtures/users.xml
new file mode 100644
index 0000000000..6cf441f01e
--- /dev/null
+++ b/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> \ No newline at end of file
diff --git a/tests/modeltests/timezones/forms.py b/tests/modeltests/timezones/forms.py
new file mode 100644
index 0000000000..3c9c31167e
--- /dev/null
+++ b/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
diff --git a/tests/modeltests/timezones/models.py b/tests/modeltests/timezones/models.py
new file mode 100644
index 0000000000..bd488d1132
--- /dev/null
+++ b/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)
diff --git a/tests/modeltests/timezones/tests.py b/tests/modeltests/timezones/tests.py
new file mode 100644
index 0000000000..e33d4c61f8
--- /dev/null
+++ b/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)
diff --git a/tests/modeltests/timezones/urls.py b/tests/modeltests/timezones/urls.py
new file mode 100644
index 0000000000..e4f42972db
--- /dev/null
+++ b/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)),
+)
diff --git a/tests/modeltests/validation/test_error_messages.py b/tests/modeltests/validation/test_error_messages.py
index b042c2ee7c..4a2ad1fffa 100644
--- a/tests/modeltests/validation/test_error_messages.py
+++ b/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."]) \ No newline at end of file
+ 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."])
diff --git a/tests/regressiontests/cache/tests.py b/tests/regressiontests/cache/tests.py
index e6c03837d4..f1590d1121 100644
--- a/tests/regressiontests/cache/tests.py
+++ b/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",
diff --git a/tests/regressiontests/datatypes/tests.py b/tests/regressiontests/datatypes/tests.py
index 3cf11024fd..fb94e831ba 100644
--- a/tests/regressiontests/datatypes/tests.py
+++ b/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.
diff --git a/tests/regressiontests/defaultfilters/tests.py b/tests/regressiontests/defaultfilters/tests.py
index b18334d011..6da4f75a67 100644
--- a/tests/regressiontests/defaultfilters/tests.py
+++ b/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(
diff --git a/tests/regressiontests/utils/dateformat.py b/tests/regressiontests/utils/dateformat.py
index f1ef720b20..313a76ba4b 100644
--- a/tests/regressiontests/utils/dateformat.py
+++ b/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')
diff --git a/tests/regressiontests/utils/tests.py b/tests/regressiontests/utils/tests.py
index 4db894d6ad..b72a88b33d 100644
--- a/tests/regressiontests/utils/tests.py
+++ b/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
diff --git a/tests/regressiontests/utils/timesince.py b/tests/regressiontests/utils/timesince.py
index c0fe590028..b0f94d182b 100644
--- a/tests/regressiontests/utils/timesince.py
+++ b/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')
diff --git a/tests/regressiontests/utils/timezone.py b/tests/regressiontests/utils/timezone.py
new file mode 100644
index 0000000000..870997db65
--- /dev/null
+++ b/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)
diff --git a/tests/regressiontests/utils/tzinfo.py b/tests/regressiontests/utils/tzinfo.py
index a60a4574de..cec92652cd 100644
--- a/tests/regressiontests/utils/tzinfo.py
+++ b/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)