summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAymeric Augustin <aymeric.augustin@m4x.org>2015-04-12 14:46:24 +0200
committerAymeric Augustin <aymeric.augustin@m4x.org>2015-05-17 09:36:23 +0200
commitec186572e6cfde4cd4bc1491ff552c5d32211d9f (patch)
tree84bfb6b786f7dfecc535347a14284032bf0a5370
parenteda12ceef16aa1a98567e25be619b05a5b3c5757 (diff)
Removed global timezone-aware datetime converters.
Refs #23820.
-rw-r--r--django/core/cache/backends/db.py4
-rw-r--r--django/db/backends/mysql/base.py14
-rw-r--r--django/db/backends/mysql/operations.py8
-rw-r--r--django/db/backends/oracle/base.py6
-rw-r--r--django/db/backends/oracle/operations.py9
-rw-r--r--django/db/backends/sqlite3/base.py11
-rw-r--r--django/db/backends/sqlite3/operations.py21
-rw-r--r--django/db/backends/sqlite3/utils.py11
-rw-r--r--docs/releases/1.9.txt21
-rw-r--r--tests/timezones/tests.py25
10 files changed, 85 insertions, 45 deletions
diff --git a/django/core/cache/backends/db.py b/django/core/cache/backends/db.py
index a41b611d5b..6b9716b430 100644
--- a/django/core/cache/backends/db.py
+++ b/django/core/cache/backends/db.py
@@ -74,6 +74,8 @@ class DatabaseCache(BaseDatabaseCache):
# All core backends work without typecasting, so be careful about
# changes here - test suite will NOT pick regressions here.
expires = typecast_timestamp(str(expires))
+ if settings.USE_TZ:
+ expires = expires.replace(tzinfo=timezone.utc)
if expires < now:
db = router.db_for_write(self.cache_model_class)
with connections[db].cursor() as cursor:
@@ -132,6 +134,8 @@ class DatabaseCache(BaseDatabaseCache):
if (connections[db].features.needs_datetime_string_cast and not
isinstance(current_expires, datetime)):
current_expires = typecast_timestamp(str(current_expires))
+ if settings.USE_TZ:
+ current_expires = current_expires.replace(tzinfo=timezone.utc)
exp = connections[db].ops.value_to_db_datetime(exp)
if result and (mode == 'set' or (mode == 'add' and current_expires < now)):
cursor.execute("UPDATE %s SET value = %%s, expires = %%s "
diff --git a/django/db/backends/mysql/base.py b/django/db/backends/mysql/base.py
index f31697dadc..4c8809fdb7 100644
--- a/django/db/backends/mysql/base.py
+++ b/django/db/backends/mysql/base.py
@@ -51,17 +51,6 @@ if (version < (1, 2, 1) or (version[:3] == (1, 2, 1) and
DatabaseError = Database.DatabaseError
IntegrityError = Database.IntegrityError
-# It's impossible to import datetime_or_None directly from MySQLdb.times
-parse_datetime = conversions[FIELD_TYPE.DATETIME]
-
-
-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 timezone.is_naive(dt):
- dt = dt.replace(tzinfo=timezone.utc)
- return dt
-
def adapt_datetime_with_timezone_support(value, conv):
# Equivalent to DateTimeField.get_db_prep_value. Used only by raw SQL.
@@ -80,14 +69,11 @@ def adapt_datetime_with_timezone_support(value, conv):
# and Django expects time, so we still need to override that. We also need to
# add special handling for SafeText and SafeBytes 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: backend_utils.typecast_time,
FIELD_TYPE.DECIMAL: backend_utils.typecast_decimal,
FIELD_TYPE.NEWDECIMAL: backend_utils.typecast_decimal,
- FIELD_TYPE.DATETIME: parse_datetime_with_timezone_support,
datetime.datetime: adapt_datetime_with_timezone_support,
})
diff --git a/django/db/backends/mysql/operations.py b/django/db/backends/mysql/operations.py
index bc53f59ec5..3de35c7891 100644
--- a/django/db/backends/mysql/operations.py
+++ b/django/db/backends/mysql/operations.py
@@ -184,6 +184,8 @@ class DatabaseOperations(BaseDatabaseOperations):
internal_type = expression.output_field.get_internal_type()
if internal_type in ['BooleanField', 'NullBooleanField']:
converters.append(self.convert_booleanfield_value)
+ if internal_type == 'DateTimeField':
+ converters.append(self.convert_datetimefield_value)
if internal_type == 'UUIDField':
converters.append(self.convert_uuidfield_value)
if internal_type == 'TextField':
@@ -195,6 +197,12 @@ class DatabaseOperations(BaseDatabaseOperations):
value = bool(value)
return value
+ def convert_datetimefield_value(self, value, expression, connection, context):
+ if value is not None:
+ if settings.USE_TZ:
+ value = value.replace(tzinfo=timezone.utc)
+ return value
+
def convert_uuidfield_value(self, value, expression, connection, context):
if value is not None:
value = uuid.UUID(value)
diff --git a/django/db/backends/oracle/base.py b/django/db/backends/oracle/base.py
index d79b582e82..b84b28b97b 100644
--- a/django/db/backends/oracle/base.py
+++ b/django/db/backends/oracle/base.py
@@ -588,12 +588,6 @@ def _rowfactory(row, cursor):
value = decimal.Decimal(value)
else:
value = int(value)
- # datetimes are returned as TIMESTAMP, except the results
- # of "dates" queries, which are returned as DATETIME.
- elif desc[1] in (Database.TIMESTAMP, Database.DATETIME):
- # Confirm that dt is naive before overwriting its tzinfo.
- if settings.USE_TZ and value is not None and timezone.is_naive(value):
- value = value.replace(tzinfo=timezone.utc)
elif desc[1] in (Database.STRING, Database.FIXED_CHAR,
Database.LONG_STRING):
value = to_unicode(value)
diff --git a/django/db/backends/oracle/operations.py b/django/db/backends/oracle/operations.py
index 32dd627216..2ef8caf4e5 100644
--- a/django/db/backends/oracle/operations.py
+++ b/django/db/backends/oracle/operations.py
@@ -163,6 +163,8 @@ WHEN (new.%(col_name)s IS NULL)
converters.append(self.convert_binaryfield_value)
elif internal_type in ['BooleanField', 'NullBooleanField']:
converters.append(self.convert_booleanfield_value)
+ elif internal_type == 'DateTimeField':
+ converters.append(self.convert_datetimefield_value)
elif internal_type == 'DateField':
converters.append(self.convert_datefield_value)
elif internal_type == 'TimeField':
@@ -202,6 +204,13 @@ WHEN (new.%(col_name)s IS NULL)
# cx_Oracle always returns datetime.datetime objects for
# DATE and TIMESTAMP columns, but Django wants to see a
# python datetime.date, .time, or .datetime.
+
+ def convert_datetimefield_value(self, value, expression, connection, context):
+ if value is not None:
+ if settings.USE_TZ:
+ value = value.replace(tzinfo=timezone.utc)
+ return value
+
def convert_datefield_value(self, value, expression, connection, context):
if isinstance(value, Database.Timestamp):
return value.date()
diff --git a/django/db/backends/sqlite3/base.py b/django/db/backends/sqlite3/base.py
index 6eb39ad2c3..4cc6266893 100644
--- a/django/db/backends/sqlite3/base.py
+++ b/django/db/backends/sqlite3/base.py
@@ -17,7 +17,9 @@ from django.db.backends import utils as backend_utils
from django.db.backends.base.base import BaseDatabaseWrapper
from django.db.backends.base.validation import BaseDatabaseValidation
from django.utils import six, timezone
-from django.utils.dateparse import parse_date, parse_duration, parse_time
+from django.utils.dateparse import (
+ parse_date, parse_datetime, parse_duration, parse_time,
+)
from django.utils.encoding import force_text
from django.utils.safestring import SafeBytes
@@ -42,7 +44,6 @@ from .features import DatabaseFeatures # isort:skip
from .introspection import DatabaseIntrospection # isort:skip
from .operations import DatabaseOperations # isort:skip
from .schema import DatabaseSchemaEditor # isort:skip
-from .utils import parse_datetime_with_timezone_support # isort:skip
DatabaseError = Database.DatabaseError
IntegrityError = Database.IntegrityError
@@ -71,9 +72,9 @@ def decoder(conv_func):
Database.register_converter(str("bool"), decoder(lambda s: s == '1'))
Database.register_converter(str("time"), decoder(parse_time))
Database.register_converter(str("date"), decoder(parse_date))
-Database.register_converter(str("datetime"), decoder(parse_datetime_with_timezone_support))
-Database.register_converter(str("timestamp"), decoder(parse_datetime_with_timezone_support))
-Database.register_converter(str("TIMESTAMP"), decoder(parse_datetime_with_timezone_support))
+Database.register_converter(str("datetime"), decoder(parse_datetime))
+Database.register_converter(str("timestamp"), decoder(parse_datetime))
+Database.register_converter(str("TIMESTAMP"), decoder(parse_datetime))
Database.register_converter(str("decimal"), decoder(backend_utils.typecast_decimal))
Database.register_adapter(datetime.datetime, adapt_datetime_with_timezone_support)
diff --git a/django/db/backends/sqlite3/operations.py b/django/db/backends/sqlite3/operations.py
index f7e2c64da1..3505ebda03 100644
--- a/django/db/backends/sqlite3/operations.py
+++ b/django/db/backends/sqlite3/operations.py
@@ -10,11 +10,9 @@ from django.db.backends import utils as backend_utils
from django.db.backends.base.operations import BaseDatabaseOperations
from django.db.models import aggregates, fields
from django.utils import six, timezone
-from django.utils.dateparse import parse_date, parse_time
+from django.utils.dateparse import parse_date, parse_datetime, parse_time
from django.utils.duration import duration_string
-from .utils import parse_datetime_with_timezone_support
-
try:
import pytz
except ImportError:
@@ -157,18 +155,23 @@ class DatabaseOperations(BaseDatabaseOperations):
return backend_utils.typecast_decimal(expression.output_field.format_number(value))
def convert_datefield_value(self, value, expression, connection, context):
- if value is not None and not isinstance(value, datetime.date):
- value = parse_date(value)
+ if value is not None:
+ if not isinstance(value, datetime.date):
+ value = parse_date(value)
return value
def convert_datetimefield_value(self, value, expression, connection, context):
- if value is not None and not isinstance(value, datetime.datetime):
- value = parse_datetime_with_timezone_support(value)
+ if value is not None:
+ if not isinstance(value, datetime.datetime):
+ value = parse_datetime(value)
+ if settings.USE_TZ:
+ value = value.replace(tzinfo=timezone.utc)
return value
def convert_timefield_value(self, value, expression, connection, context):
- if value is not None and not isinstance(value, datetime.time):
- value = parse_time(value)
+ if value is not None:
+ if not isinstance(value, datetime.time):
+ value = parse_time(value)
return value
def convert_uuidfield_value(self, value, expression, connection, context):
diff --git a/django/db/backends/sqlite3/utils.py b/django/db/backends/sqlite3/utils.py
deleted file mode 100644
index 6a8b3b8943..0000000000
--- a/django/db/backends/sqlite3/utils.py
+++ /dev/null
@@ -1,11 +0,0 @@
-from django.conf import settings
-from django.utils import timezone
-from django.utils.dateparse import parse_datetime
-
-
-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 timezone.is_naive(dt):
- dt = dt.replace(tzinfo=timezone.utc)
- return dt
diff --git a/docs/releases/1.9.txt b/docs/releases/1.9.txt
index c2ceb5f5db..28c953b212 100644
--- a/docs/releases/1.9.txt
+++ b/docs/releases/1.9.txt
@@ -313,6 +313,12 @@ Database backend API
doesn't implement this. You may want to review the implementation on the
backends that Django includes for reference (:ticket:`24245`).
+* The recommended way to add time zone information to datetimes fetched from
+ databases that don't support time zones is to register a converter for
+ ``DateTimeField``. Do this in ``DatabaseOperations.get_db_converters()``.
+ Registering a global converter at the level of the DB-API module is
+ discouraged because it can conflict with other libraries.
+
Default settings that were tuples are now lists
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -412,6 +418,21 @@ console, for example.
If you are overriding Django's default logging, you should check to see how
your configuration merges with the new defaults.
+Removal of time zone aware global converters for datetimes
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Django no longer registers global converters for returning time zone aware
+datetimes in database query results when :setting:`USE_TZ` is ``True``.
+Instead the ORM adds suitable time zone information.
+
+As a consequence, SQL queries executed outside of the ORM, for instance with
+``cursor.execute(query, params)``, now return naive datetimes instead of aware
+datetimes on databases that do not support time zones: SQLite, MySQL, and
+Oracle. Since these datetimes are in UTC, you can make them aware as follows::
+
+ from django.utils import timezone
+ value = value.replace(tzinfo=timezone.utc)
+
Miscellaneous
~~~~~~~~~~~~~
diff --git a/tests/timezones/tests.py b/tests/timezones/tests.py
index 8c1d533165..39158badc1 100644
--- a/tests/timezones/tests.py
+++ b/tests/timezones/tests.py
@@ -10,6 +10,7 @@ from xml.dom.minidom import parseString
from django.contrib.auth.models import User
from django.core import serializers
from django.core.urlresolvers import reverse
+from django.db import connection
from django.db.models import Max, Min
from django.http import HttpRequest
from django.template import (
@@ -265,6 +266,13 @@ class LegacyDatabaseTests(TestCase):
[event],
transform=lambda d: d)
+ def test_cursor_execute_returns_naive_datetime(self):
+ dt = datetime.datetime(2011, 9, 1, 13, 20, 30)
+ Event.objects.create(dt=dt)
+ with connection.cursor() as cursor:
+ cursor.execute('SELECT dt FROM timezones_event WHERE dt = %s', [dt])
+ self.assertEqual(cursor.fetchall()[0][0], dt)
+
def test_filter_date_field_with_aware_datetime(self):
# Regression test for #17742
day = datetime.date(2011, 9, 1)
@@ -556,6 +564,23 @@ class NewDatabaseTests(TestCase):
[event],
transform=lambda d: d)
+ @skipUnlessDBFeature('supports_timezones')
+ def test_cursor_execute_returns_aware_datetime(self):
+ dt = datetime.datetime(2011, 9, 1, 13, 20, 30, tzinfo=EAT)
+ Event.objects.create(dt=dt)
+ with connection.cursor() as cursor:
+ cursor.execute('SELECT dt FROM timezones_event WHERE dt = %s', [dt])
+ self.assertEqual(cursor.fetchall()[0][0], dt)
+
+ @skipIfDBFeature('supports_timezones')
+ def test_cursor_execute_returns_naive_datetime(self):
+ dt = datetime.datetime(2011, 9, 1, 13, 20, 30, tzinfo=EAT)
+ utc_naive_dt = timezone.make_naive(dt, timezone.utc)
+ Event.objects.create(dt=dt)
+ with connection.cursor() as cursor:
+ cursor.execute('SELECT dt FROM timezones_event WHERE dt = %s', [dt])
+ self.assertEqual(cursor.fetchall()[0][0], utc_naive_dt)
+
@requires_tz_support
def test_filter_date_field_with_aware_datetime(self):
# Regression test for #17742