Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

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
  • Loading branch information...
commit 9b1cb755a28f020e27d4268c214b25315d4de42e 1 parent 01f7034
@aaugustin aaugustin authored
Showing with 2,719 additions and 283 deletions.
  1. +7 −2 django/conf/global_settings.py
  2. +4 −1 django/conf/project_template/project_name/settings.py
  3. +3 −0  django/contrib/admin/util.py
  4. +5 −13 django/contrib/humanize/templatetags/humanize.py
  5. +42 −30 django/contrib/humanize/tests.py
  6. +2 −1  django/contrib/syndication/views.py
  7. +5 −0 django/core/context_processors.py
  8. +15 −10 django/core/serializers/json.py
  9. +3 −0  django/db/backends/__init__.py
  10. +22 −5 django/db/backends/mysql/base.py
  11. +36 −9 django/db/backends/oracle/base.py
  12. +11 −6 django/db/backends/postgresql_psycopg2/base.py
  13. +45 −11 django/db/backends/sqlite3/base.py
  14. +5 −1 django/db/backends/util.py
  15. +77 −91 django/db/models/fields/__init__.py
  16. +1 −1  django/db/utils.py
  17. +13 −5 django/forms/fields.py
  18. +31 −0 django/forms/util.py
  19. +2 −1  django/forms/widgets.py
  20. +6 −1 django/template/base.py
  21. +8 −4 django/template/context.py
  22. +2 −0  django/template/debug.py
  23. +2 −2 django/template/defaultfilters.py
  24. +191 −0 django/templatetags/tz.py
  25. +4 −1 django/utils/cache.py
  26. +10 −4 django/utils/dateformat.py
  27. +93 −0 django/utils/dateparse.py
  28. +3 −2 django/utils/feedgenerator.py
  29. +5 −11 django/utils/timesince.py
  30. +266 −0 django/utils/timezone.py
  31. +19 −0 django/utils/tzinfo.py
  32. +25 −0 docs/howto/custom-template-tags.txt
  33. +13 −0 docs/ref/models/querysets.txt
  34. +42 −12 docs/ref/settings.txt
  35. +39 −24 docs/ref/templates/builtins.txt
  36. +125 −0 docs/ref/utils.txt
  37. +52 −1 docs/releases/1.4.txt
  38. +3 −1 docs/topics/cache.txt
  39. +6 −4 docs/topics/i18n/index.txt
  40. +429 −0 docs/topics/i18n/timezones.txt
  41. +11 −11 tests/modeltests/fixtures/tests.py
  42. +3 −3 tests/modeltests/serializers/tests.py
  43. 0  tests/modeltests/timezones/__init__.py
  44. +15 −0 tests/modeltests/timezones/admin.py
  45. +17 −0 tests/modeltests/timezones/fixtures/users.xml
  46. +13 −0 tests/modeltests/timezones/forms.py
  47. +8 −0 tests/modeltests/timezones/models.py
  48. +871 −0 tests/modeltests/timezones/tests.py
  49. +10 −0 tests/modeltests/timezones/urls.py
  50. +35 −8 tests/modeltests/validation/test_error_messages.py
  51. +15 −3 tests/regressiontests/cache/tests.py
  52. +2 −2 tests/regressiontests/datatypes/tests.py
  53. +1 −1  tests/regressiontests/defaultfilters/tests.py
  54. +1 −1  tests/regressiontests/utils/dateformat.py
  55. +1 −0  tests/regressiontests/utils/tests.py
  56. +9 −0 tests/regressiontests/utils/timesince.py
  57. +18 −0 tests/regressiontests/utils/timezone.py
  58. +17 −0 tests/regressiontests/utils/tzinfo.py
View
9 django/conf/global_settings.py
@@ -31,9 +31,13 @@
# 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 @@
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 @@
'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',
)
View
5 django/conf/project_template/project_name/settings.py
@@ -40,9 +40,12 @@
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 = ''
View
3  django/contrib/admin/util.py
@@ -7,6 +7,7 @@
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):
View
18 django/contrib/humanize/templatetags/humanize.py
@@ -7,7 +7,7 @@
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:
View
72 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 @@ def test_apnumber(self):
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 @@ def test_naturalday(self):
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 @@ def test_naturaltime(self):
'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
View
3  django/contrib/syndication/views.py
@@ -6,6 +6,7 @@
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 @@ def get_feed(self, obj, request):
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)
View
5 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.
View
25 django/core/serializers/json.py
@@ -8,8 +8,8 @@
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:
View
3  django/db/backends/__init__.py
@@ -10,6 +10,7 @@
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 @@ def value_to_db_time(self, value):
"""
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):
View
27 django/db/backends/mysql/base.py
@@ -33,6 +33,7 @@
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 @@
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 @@ def value_to_db_datetime(self, value):
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 @@ def value_to_db_time(self, value):
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))
View
45 django/db/backends/oracle/base.py
@@ -44,6 +44,7 @@ def _setup_environment(environ):
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 @@ def _setup_environment(environ):
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 @@ def tablespace_sql(self, tablespace, inline=False):
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 @@ def value_to_db_time(self, value):
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 @@ def _cursor(self):
# 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
View
17 django/db/backends/postgresql_psycopg2/base.py
@@ -13,8 +13,9 @@
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 @@
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 @@ def _get_pg_version(self):
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 @@ def _cursor(self):
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)
View
56 django/db/backends/sqlite3/base.py
@@ -10,13 +10,16 @@
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 @@
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 @@ def sql_flush(self, style, tables, sequences):
# 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 @@ def convert_values(self, value, field):
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
View
6 django/db/backends/util.py
@@ -3,7 +3,9 @@
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 == '':
View
168 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 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 @@ def validate(self, value, model_instance):
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 @@ def formfield(self, **kwargs):
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 @@ def to_python(self, value):
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 @@ def get_db_prep_value(self, value, connection, prepared=False):
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 @@ def formfield(self, **kwargs):
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 @@ def to_python(self, value):
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 @@ def formfield(self, **kwargs):
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 @@ def to_python(self, value):
# 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 @@ def get_db_prep_value(self, value, connection, prepared=False):
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}
View
2  django/db/utils.py
@@ -66,7 +66,7 @@ def ensure_defaults(self, alias):
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']:
View
18 django/forms/fields.py
@@ -17,7 +17,7 @@
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 @@ def to_python(self, value):
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 @@ def to_python(self, value):
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 @@ def compress(self, data_list):
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
View
31 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 @@ def as_text(self):
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
View
3  django/forms/widgets.py
@@ -10,7 +10,7 @@
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 @@ def __init__(self, attrs=None, date_format=None, time_format=None):
def decompress(self, value):
if value:
+ value = to_current_timezone(value)
return [value.date(), value.time().replace(microsecond=0)]
return [None, None]
View
7 django/template/base.py
@@ -18,6 +18,7 @@
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 @@ def resolve(self, context, ignore_failures=False):
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 @@ def dec(func):
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 @@ def render(self, context):
'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
View
12 django/template/context.py
@@ -83,10 +83,12 @@ def new(self, values=None):
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:
View
2  django/template/debug.py
@@ -3,6 +3,7 @@
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:
View
4 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''):
View
191 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])
View
5 django/utils/cache.py
@@ -25,6 +25,7 @@
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):
View
14 django/utils/dateformat.py
@@ -14,10 +14,13 @@
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 @@ def T(self):
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()))
View
93 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)
View
5 django/utils/feedgenerator.py
@@ -28,6 +28,7 @@
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)
View
16 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)
View
266 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)
View
19 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 @@ def __init__(self, offset):
def __repr__(self):
return self.__name
+ def __getinitargs__(self):
+ return self.__offset,
+
def utcoffset(self, dt):
return self.__offset
@@ -28,15 +37,25 @@ def tzname(self, dt):
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)
View
25 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: