diff --git a/django/db/backends/mysql/base.py b/django/db/backends/mysql/base.py index 8073e55cf4..cefd86f330 100644 --- a/django/db/backends/mysql/base.py +++ b/django/db/backends/mysql/base.py @@ -4,6 +4,7 @@ MySQL database backend for Django. Requires MySQLdb: http://sourceforge.net/projects/mysql-python """ +import datetime import re import sys @@ -24,6 +25,7 @@ if (version < (1,2,1) or (version[:3] == (1, 2, 1) and from MySQLdb.converters import conversions from MySQLdb.constants import FIELD_TYPE, CLIENT +from _mysql import string_literal from django.db import utils from django.db.backends import * @@ -33,7 +35,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 +from django.utils import timezone # Raise exceptions for database warnings if DEBUG is on from django.conf import settings @@ -45,15 +47,27 @@ 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] +parse_datetime = conversions[FIELD_TYPE.DATETIME] -def datetime_or_None_with_timezone_support(value): - dt = datetime_or_None(value) +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) + 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. + if settings.USE_TZ: + if timezone.is_naive(value): + warnings.warn(u"SQLite received a naive datetime (%s)" + u" while time zone support is active." % value, + RuntimeWarning) + default_timezone = timezone.get_default_timezone() + value = timezone.make_aware(value, default_timezone) + value = value.astimezone(timezone.utc).replace(tzinfo=None) + return string_literal(value.strftime("%Y-%m-%d %H:%M:%S"), conv) + # 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 @@ -66,7 +80,8 @@ 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, + FIELD_TYPE.DATETIME: parse_datetime_with_timezone_support, + datetime.datetime: adapt_datetime_with_timezone_support, }) # This should match the numerical portion of the version numbers (we can treat @@ -268,9 +283,9 @@ class DatabaseOperations(BaseDatabaseOperations): return None # MySQL doesn't support tz-aware datetimes - if is_aware(value): + if timezone.is_aware(value): if settings.USE_TZ: - value = value.astimezone(utc).replace(tzinfo=None) + value = value.astimezone(timezone.utc).replace(tzinfo=None) else: raise ValueError("MySQL backend does not support timezone-aware datetimes when USE_TZ is False.") @@ -282,7 +297,7 @@ class DatabaseOperations(BaseDatabaseOperations): return None # MySQL doesn't support tz-aware times - if is_aware(value): + if timezone.is_aware(value): raise ValueError("MySQL backend does not support timezone-aware times.") # MySQL doesn't support microseconds diff --git a/django/db/backends/oracle/base.py b/django/db/backends/oracle/base.py index f1ee987ba8..6bc6e1dd9d 100644 --- a/django/db/backends/oracle/base.py +++ b/django/db/backends/oracle/base.py @@ -52,7 +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 +from django.utils import timezone DatabaseError = Database.DatabaseError IntegrityError = Database.IntegrityError @@ -339,9 +339,9 @@ WHEN (new.%(col_name)s IS NULL) return None # Oracle doesn't support tz-aware datetimes - if is_aware(value): + if timezone.is_aware(value): if settings.USE_TZ: - value = value.astimezone(utc).replace(tzinfo=None) + value = value.astimezone(timezone.utc).replace(tzinfo=None) else: raise ValueError("Oracle backend does not support timezone-aware datetimes when USE_TZ is False.") @@ -355,7 +355,7 @@ WHEN (new.%(col_name)s IS NULL) return datetime.datetime.strptime(value, '%H:%M:%S') # Oracle doesn't support tz-aware times - if is_aware(value): + if timezone.is_aware(value): raise ValueError("Oracle backend does not support timezone-aware times.") return datetime.datetime(1900, 1, 1, value.hour, value.minute, @@ -561,6 +561,17 @@ class OracleParam(object): """ def __init__(self, param, cursor, strings_only=False): + # With raw SQL queries, datetimes can reach this function + # without being converted by DateTimeField.get_db_prep_value. + if settings.USE_TZ and isinstance(param, datetime.datetime): + if timezone.is_naive(param): + warnings.warn(u"Oracle received a naive datetime (%s)" + u" while time zone support is active." % param, + RuntimeWarning) + default_timezone = timezone.get_default_timezone() + param = timezone.make_aware(param, default_timezone) + param = param.astimezone(timezone.utc).replace(tzinfo=None) + if hasattr(param, 'bind_parameter'): self.smart_str = param.bind_parameter(cursor) else: @@ -783,8 +794,8 @@ def _rowfactory(row, cursor): # 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 is_naive(value): - value = value.replace(tzinfo=utc) + 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/sqlite3/base.py b/django/db/backends/sqlite3/base.py index f5f0c644e9..0b19442e78 100644 --- a/django/db/backends/sqlite3/base.py +++ b/django/db/backends/sqlite3/base.py @@ -19,7 +19,7 @@ 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 +from django.utils import timezone try: try: @@ -37,10 +37,22 @@ 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) + 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): + # Equivalent to DateTimeField.get_db_prep_value. Used only by raw SQL. + if settings.USE_TZ: + if timezone.is_naive(value): + warnings.warn(u"SQLite received a naive datetime (%s)" + u" while time zone support is active." % value, + RuntimeWarning) + default_timezone = timezone.get_default_timezone() + value = timezone.make_aware(value, default_timezone) + value = value.astimezone(timezone.utc).replace(tzinfo=None) + return value.isoformat(" ") + Database.register_converter("bool", lambda s: str(s) == '1') Database.register_converter("time", parse_time) Database.register_converter("date", parse_date) @@ -48,13 +60,14 @@ 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(datetime.datetime, adapt_datetime_with_timezone_support) Database.register_adapter(decimal.Decimal, util.rev_typecast_decimal) 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. + # needing it (Python 2.6 and up). Database.register_adapter(str, lambda s: s.decode('utf-8')) Database.register_adapter(SafeString, lambda s: s.decode('utf-8')) @@ -147,9 +160,9 @@ class DatabaseOperations(BaseDatabaseOperations): return None # SQLite doesn't support tz-aware datetimes - if is_aware(value): + if timezone.is_aware(value): if settings.USE_TZ: - value = value.astimezone(utc).replace(tzinfo=None) + value = value.astimezone(timezone.utc).replace(tzinfo=None) else: raise ValueError("SQLite backend does not support timezone-aware datetimes when USE_TZ is False.") @@ -160,7 +173,7 @@ class DatabaseOperations(BaseDatabaseOperations): return None # SQLite doesn't support tz-aware datetimes - if is_aware(value): + if timezone.is_aware(value): raise ValueError("SQLite backend does not support timezone-aware times.") return unicode(value) diff --git a/tests/modeltests/timezones/tests.py b/tests/modeltests/timezones/tests.py index 818405971c..a8d2c0c332 100644 --- a/tests/modeltests/timezones/tests.py +++ b/tests/modeltests/timezones/tests.py @@ -263,6 +263,15 @@ class LegacyDatabaseTests(BaseDateTimeTests): self.assertQuerysetEqual(Event.objects.dates('dt', 'day'), [datetime.datetime(2011, 1, 1)], transform=lambda d: d) + def test_raw_sql(self): + # Regression test for #17755 + dt = datetime.datetime(2011, 9, 1, 13, 20, 30) + event = Event.objects.create(dt=dt) + self.assertQuerysetEqual( + Event.objects.raw('SELECT * FROM timezones_event WHERE dt = %s', [dt]), + [event], + transform=lambda d: d) + LegacyDatabaseTests = override_settings(USE_TZ=False)(LegacyDatabaseTests) @@ -473,6 +482,15 @@ class NewDatabaseTests(BaseDateTimeTests): datetime.datetime(2011, 1, 1, tzinfo=UTC)], transform=lambda d: d) + def test_raw_sql(self): + # Regression test for #17755 + dt = datetime.datetime(2011, 9, 1, 13, 20, 30, tzinfo=EAT) + event = Event.objects.create(dt=dt) + self.assertQuerysetEqual( + Event.objects.raw('SELECT * FROM timezones_event WHERE dt = %s', [dt]), + [event], + transform=lambda d: d) + def test_null_datetime(self): # Regression for #17294 e = MaybeEvent.objects.create()