Permalink
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...
1 parent 01f7034 commit 9b1cb755a28f020e27d4268c214b25315d4de42e @aaugustin aaugustin committed Nov 18, 2011
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
@@ -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',
)
@@ -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 = ''
@@ -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):
@@ -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:
@@ -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
@@ -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)
@@ -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.
@@ -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:
@@ -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):
@@ -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))
Oops, something went wrong. Retry.

0 comments on commit 9b1cb75

Please sign in to comment.