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