Author: aaugustin
Date: 2012-02-27 13:15:25 -0800 (Mon, 27 Feb 2012)
New Revision: 17596

Modified:
   django/trunk/django/db/backends/mysql/base.py
   django/trunk/django/db/backends/oracle/base.py
   django/trunk/django/db/backends/sqlite3/base.py
   django/trunk/tests/modeltests/timezones/tests.py
Log:
Fixed #17755 -- Ensured datetime objects that bypass the model layer (for 
instance, in raw SQL queries) are converted to UTC before sending them to the 
database when time zone support is enabled. Thanks Anssi for the report.


Modified: django/trunk/django/db/backends/mysql/base.py
===================================================================
--- django/trunk/django/db/backends/mysql/base.py       2012-02-27 19:04:33 UTC 
(rev 17595)
+++ django/trunk/django/db/backends/mysql/base.py       2012-02-27 21:15:25 UTC 
(rev 17596)
@@ -4,6 +4,7 @@
 Requires MySQLdb: http://sourceforge.net/projects/mysql-python
 """
 
+import datetime
 import re
 import sys
 
@@ -24,6 +25,7 @@
 
 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.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 @@
 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 @@
     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 @@
             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 @@
             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

Modified: django/trunk/django/db/backends/oracle/base.py
===================================================================
--- django/trunk/django/db/backends/oracle/base.py      2012-02-27 19:04:33 UTC 
(rev 17595)
+++ django/trunk/django/db/backends/oracle/base.py      2012-02-27 21:15:25 UTC 
(rev 17596)
@@ -52,7 +52,7 @@
 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 @@
             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 @@
             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 @@
     """
 
     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 @@
         # 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)

Modified: django/trunk/django/db/backends/sqlite3/base.py
===================================================================
--- django/trunk/django/db/backends/sqlite3/base.py     2012-02-27 19:04:33 UTC 
(rev 17595)
+++ django/trunk/django/db/backends/sqlite3/base.py     2012-02-27 21:15:25 UTC 
(rev 17596)
@@ -19,7 +19,7 @@
 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 @@
 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("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 @@
             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 @@
             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)

Modified: django/trunk/tests/modeltests/timezones/tests.py
===================================================================
--- django/trunk/tests/modeltests/timezones/tests.py    2012-02-27 19:04:33 UTC 
(rev 17595)
+++ django/trunk/tests/modeltests/timezones/tests.py    2012-02-27 21:15:25 UTC 
(rev 17596)
@@ -263,6 +263,15 @@
         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 @@
                  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()

-- 
You received this message because you are subscribed to the Google Groups 
"Django updates" group.
To post to this group, send email to django-updates@googlegroups.com.
To unsubscribe from this group, send email to 
django-updates+unsubscr...@googlegroups.com.
For more options, visit this group at 
http://groups.google.com/group/django-updates?hl=en.

Reply via email to