diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py index 6b09be23b25..2ef2f63618f 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 aaaeb376884..f28c61e15d5 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 7204a12fc46..af8fad382e2 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 4ed07125c0e..b075ff05c7c 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 c59885a0dcb..0217136b11c 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 8e7c1d125c4..a6e18e3cd8d 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 5597f68a55a..80f1e76d35b 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 b8119f54d43..2b4b2a382bc 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 d6bd3808860..dd248787e2e 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 e9f5b8b6a97..93aaf9971a2 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 c9830f5a7cd..c1974225f67 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): - # 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 value is None: + return None - return super(DatabaseOperations, self).value_to_db_datetime(value) + # Oracle doesn't support tz-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 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 74342e9ac64..87df6bd1bc8 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 c437f1517e3..a6106066355 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 9d2d3791322..b0463b79af1 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 55b993af028..5e7cf804e89 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: - 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_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: + 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 1d7994b3368..f0c13e3aac1 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 7f8bac5052e..0ba7467191e 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 85baffa0051..886f08e581b 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 8c1e20b1797..f7569579603 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 238e2db9efa..572ec6138f1 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 8f70d704e59..bbd38ad4682 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 d11f1ee2651..d4b973b5e04 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 7484c7bf285..6b0e2f0b065 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 00000000000..14b613e24f0 --- /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 be4fa586458..1015c2f277c 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 0afda1850d4..d87fb131058 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'(?\d{4})-(?P\d{1,2})-(?P\d{1,2})$' +) + + +datetime_re = re.compile( + r'(?P\d{4})-(?P\d{1,2})-(?P\d{1,2})' + r'[T ](?P\d{1,2}):(?P\d{1,2})' + r'(?::(?P\d{1,2})(?:\.(?P\d{1,6})\d{0,6})?)?' + r'(?PZ|[+-]\d{1,2}:\d{1,2})?$' +) + + +time_re = re.compile( + r'(?P\d{1,2}):(?P\d{1,2})' + r'(?::(?P\d{1,2})(?:\.(?P\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 7d7c7af65cb..df3cf416523 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 369a3e2c011..511acb518a0 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 00000000000..22860eb8cc9 --- /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 daffffb4962..a07b635a995 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 6b2355e8d0e..0b962f88634 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 `. + Writing custom template tags ---------------------------- diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt index 68da9c73437..9736d94838f 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 ` 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 ` 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 20366e353d0..0d91f168210 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. -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: +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. + +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 ` 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 7b305746abb..695a21296b5 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 `_ 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 0d6e9c6b611..9a9adba0d85 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 `. + +.. function:: get_default_timezone_name() + + Returns the name of the :ref:`default time zone + `. + +.. function:: get_current_timezone() + + Returns a :class:`~datetime.tzinfo` instance that represents the + :ref:`current time zone `. + +.. function:: get_current_timezone_name() + + Returns the name of the :ref:`current time zone + `. + +.. function:: activate(timezone) + + Sets the :ref:`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 `. + +.. function:: override(timezone) + + This is a Python context manager that sets the :ref:`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 ` 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 49284fb90bf..46527da581b 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 `. 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 `. 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 a3aac74b765..99d764b60d3 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 ` when +:setting:`USE_L10N` is set to ``True`` and the :ref:`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 4e2c9c345c4..25ec8392de5 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 -` and :doc:`formatting of dates, times and numbers -`. +`, :doc:`formatting of dates, times and numbers +`, and :doc:`time zones `. 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 00000000000..41e8380acc9 --- /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 ` 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 ` includes :setting:`USE_TZ = True ` + 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 `_ + 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 +`. + +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() ` 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 `, 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 ` 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 `. 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 %} +
+ {% csrf_token %} + + + +
+ +Time zone aware input in forms +============================== + +When you enable time zone support, Django interprets datetimes entered in +forms in the :ref:`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 ` when they're rendered +in templates. This behaves very much like :doc:`format localization +`. + +.. 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 ` 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 2dca7ced9c5..e7daf2d861d 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'], """ -News StoriesLatest news storiesXML identified as leading cause of cancer2006-06-16 16:00:00Django conquers world!2006-06-16 15:00:00Copyright is fine the way it is2006-06-16 14:00:00Poker on TV is great!2006-06-16 11:00:00Python program becomes self aware2006-06-16 11:00:00copyrightfixturesarticle3legalfixturesarticle3djangofixturesarticle4world dominationfixturesarticle4Artist formerly known as "Prince"Django ReinhardtStephane GrappelliDjango Reinhardtadd_userauthuserchange_userauthuserdelete_userauthuserStephane Grappelliadd_userauthuserdelete_userauthuserArtist formerly known as "Prince"change_userauthuserMusic for all agesArtist formerly known as "Prince"Django Reinhardt""", format='xml', natural_keys=True) +News StoriesLatest news storiesXML identified as leading cause of cancer2006-06-16T16:00:00Django conquers world!2006-06-16T15:00:00Copyright is fine the way it is2006-06-16T14:00:00Poker on TV is great!2006-06-16T11:00:00Python program becomes self aware2006-06-16T11:00:00copyrightfixturesarticle3legalfixturesarticle3djangofixturesarticle4world dominationfixturesarticle4Artist formerly known as "Prince"Django ReinhardtStephane GrappelliDjango Reinhardtadd_userauthuserchange_userauthuserdelete_userauthuserStephane Grappelliadd_userauthuserdelete_userauthuserArtist formerly known as "Prince"change_userauthuserMusic for all agesArtist formerly known as "Prince"Django Reinhardt""", 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'], """ -News StoriesLatest news storiesTime to reform copyright2006-06-16 13:00:00Poker has no place on ESPN2006-06-16 12:00:00Python program becomes self aware2006-06-16 11:00:00copyrightfixturesarticle3lawfixturesarticle3Django ReinhardtPrinceStephane Grappelli""", format='xml', natural_keys=True) +News StoriesLatest news storiesTime to reform copyright2006-06-16T13:00:00Poker has no place on ESPN2006-06-16T12:00:00Python program becomes self aware2006-06-16T11:00:00copyrightfixturesarticle3lawfixturesarticle3Django ReinhardtPrinceStephane Grappelli""", 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 99c065aeafa..721ca09282e 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 1 Forward references pose no problem - 2006-06-16 15:00:00 + 2006-06-16T15:00:00 @@ -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 00000000000..e69de29bb2d diff --git a/tests/modeltests/timezones/admin.py b/tests/modeltests/timezones/admin.py new file mode 100644 index 00000000000..4c199813e25 --- /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 00000000000..6cf441f01ef --- /dev/null +++ b/tests/modeltests/timezones/fixtures/users.xml @@ -0,0 +1,17 @@ + + + + super + Super + User + super@example.com + sha1$995a3$6011485ea3834267d719b4c801409b8b1ddd0158 + True + True + True + 2007-05-30 13:20:10 + 2007-05-30 13:20:10 + + + + \ 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 00000000000..3c9c31167e6 --- /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 00000000000..bd488d1132a --- /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 00000000000..e33d4c61f84 --- /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('2011-09-01T13:20:30', 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('2011-09-01T13:20:30.405060', 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('2011-09-01T17:20:30.405060+07:00', 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('2011-09-01T10:20:30+00:00', 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('2011-09-01T13:20:30+03:00', 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('2011-09-01T17:20:30+07:00', 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 00000000000..e4f42972dbc --- /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 b042c2ee7c9..4a2ad1fffad 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 e6c03837d44..f1590d1121a 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 3cf11024fdb..fb94e831ba0 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 b18334d0118..6da4f75a67d 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 f1ef720b205..313a76ba4b3 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 4db894d6ad1..b72a88b33d5 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 c0fe5900288..b0f94d182b8 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 00000000000..870997db65a --- /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 a60a4574dea..cec92652cdc 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)