Browse Source

Fixed #23365 -- Added support for timezone-aware datetimes to migrations.

Rudy Mutter 10 years ago
parent
commit
a407b846b4

+ 4 - 3
django/db/migrations/questioner.py

@@ -5,7 +5,7 @@ import os
 import sys
 
 from django.apps import apps
-from django.utils import datetime_safe, six
+from django.utils import datetime_safe, six, timezone
 from django.utils.six.moves import input
 
 from .loader import MIGRATIONS_MODULE_NAME
@@ -108,7 +108,8 @@ class InteractiveMigrationQuestioner(MigrationQuestioner):
                 sys.exit(3)
             else:
                 print("Please enter the default value now, as valid Python")
-                print("The datetime module is available, so you can do e.g. datetime.date.today()")
+                print("The datetime and django.utils.timezone modules are "
+                      "available, so you can do e.g. timezone.now()")
                 while True:
                     if six.PY3:
                         # Six does not correctly abstract over the fact that
@@ -123,7 +124,7 @@ class InteractiveMigrationQuestioner(MigrationQuestioner):
                         sys.exit(1)
                     else:
                         try:
-                            return eval(code, {}, {"datetime": datetime_safe})
+                            return eval(code, {}, {"datetime": datetime_safe, "timezone": timezone})
                         except (SyntaxError, NameError) as e:
                             print("Invalid input: %s" % e)
         return None

+ 19 - 5
django/db/migrations/writer.py

@@ -16,6 +16,7 @@ from django.db.migrations.loader import MigrationLoader
 from django.utils import datetime_safe, six
 from django.utils.encoding import force_text
 from django.utils.functional import Promise
+from django.utils.timezone import utc
 
 
 COMPILED_REGEX_TYPE = type(re.compile(''))
@@ -164,6 +165,20 @@ class MigrationWriter(object):
 
         return (MIGRATION_TEMPLATE % items).encode("utf8")
 
+    @staticmethod
+    def serialize_datetime(value):
+        """
+        Returns a serialized version of a datetime object that is valid,
+        executable python code. It converts timezone-aware values to utc with
+        an 'executable' utc representation of tzinfo.
+        """
+        if value.tzinfo is not None and value.tzinfo != utc:
+            value = value.astimezone(utc)
+        value_repr = repr(value).replace("<UTC>", "utc")
+        if isinstance(value, datetime_safe.datetime):
+            value_repr = "datetime.%s" % value_repr
+        return value_repr
+
     @property
     def filename(self):
         return "%s.py" % self.migration.name
@@ -268,12 +283,11 @@ class MigrationWriter(object):
             return "{%s}" % (", ".join("%s: %s" % (k, v) for k, v in strings)), imports
         # Datetimes
         elif isinstance(value, datetime.datetime):
+            value_repr = cls.serialize_datetime(value)
+            imports = ["import datetime"]
             if value.tzinfo is not None:
-                raise ValueError("Cannot serialize datetime values with timezones. Either use a callable value for default or remove the timezone.")
-            value_repr = repr(value)
-            if isinstance(value, datetime_safe.datetime):
-                value_repr = "datetime.%s" % value_repr
-            return value_repr, {"import datetime"}
+                imports.append("from django.utils.timezone import utc")
+            return value_repr, set(imports)
         # Dates
         elif isinstance(value, datetime.date):
             value_repr = repr(value)

+ 2 - 0
docs/releases/1.8.txt

@@ -260,6 +260,8 @@ Management Commands
 * The :djadminopt:`--name` option for :djadmin:`makemigrations` allows you to
   to give the migration(s) a custom name instead of a generated one.
 
+* :djadmin:`makemigrations` can now serialize timezone-aware values.
+
 Models
 ^^^^^^
 

+ 5 - 0
docs/topics/migrations.txt

@@ -543,12 +543,17 @@ Django can serialize the following:
 - ``int``, ``long``, ``float``, ``bool``, ``str``, ``unicode``, ``bytes``, ``None``
 - ``list``, ``set``, ``tuple``, ``dict``
 - ``datetime.date``, ``datetime.time``, and ``datetime.datetime`` instances
+  (include those that are timezone-aware)
 - ``decimal.Decimal`` instances
 - Any Django field
 - Any function or method reference (e.g. ``datetime.datetime.today``) (must be in module's top-level scope)
 - Any class reference (must be in module's top-level scope)
 - Anything with a custom ``deconstruct()`` method (:ref:`see below <custom-deconstruct-method>`)
 
+.. versionchanged:: 1.8
+
+    Support for serializing timezone-aware datetimes was added.
+
 Django can serialize the following on Python 3 only:
 
 - Unbound methods used from within the class body (see below)

+ 26 - 3
tests/migrations/test_writer.py

@@ -16,7 +16,7 @@ from django.conf import settings
 from django.utils import datetime_safe, six
 from django.utils.deconstruct import deconstructible
 from django.utils.translation import ugettext_lazy as _
-from django.utils.timezone import get_default_timezone
+from django.utils.timezone import get_default_timezone, utc, FixedOffset
 
 import custom_migration_operations.operations
 import custom_migration_operations.more_operations
@@ -101,8 +101,8 @@ class WriterTests(TestCase):
         self.assertSerializedEqual(datetime.date.today())
         self.assertSerializedEqual(datetime.date.today)
         self.assertSerializedEqual(datetime.datetime.now().time())
-        with self.assertRaises(ValueError):
-            self.assertSerializedEqual(datetime.datetime(2012, 1, 1, 1, 1, tzinfo=get_default_timezone()))
+        self.assertSerializedEqual(datetime.datetime(2014, 1, 1, 1, 1, tzinfo=get_default_timezone()))
+        self.assertSerializedEqual(datetime.datetime(2014, 1, 1, 1, 1, tzinfo=FixedOffset(180)))
         safe_date = datetime_safe.date(2014, 3, 31)
         string, imports = MigrationWriter.serialize(safe_date)
         self.assertEqual(string, repr(datetime.date(2014, 3, 31)))
@@ -111,6 +111,10 @@ class WriterTests(TestCase):
         string, imports = MigrationWriter.serialize(safe_datetime)
         self.assertEqual(string, repr(datetime.datetime(2014, 3, 31, 16, 4, 31)))
         self.assertEqual(imports, {'import datetime'})
+        timezone_aware_datetime = datetime.datetime(2012, 1, 1, 1, 1, tzinfo=utc)
+        string, imports = MigrationWriter.serialize(timezone_aware_datetime)
+        self.assertEqual(string, "datetime.datetime(2012, 1, 1, 1, 1, tzinfo=utc)")
+        self.assertEqual(imports, {'import datetime', 'from django.utils.timezone import utc'})
         # Django fields
         self.assertSerializedFieldEqual(models.CharField(max_length=255))
         self.assertSerializedFieldEqual(models.TextField(null=True, blank=True))
@@ -312,3 +316,22 @@ class WriterTests(TestCase):
             result['custom_migration_operations'].operations.TestOperation,
             result['custom_migration_operations'].more_operations.TestOperation
         )
+
+    def test_serialize_datetime(self):
+        """
+        #23365 -- Timezone-aware datetimes should be allowed.
+        """
+        # naive datetime
+        naive_datetime = datetime.datetime(2014, 1, 1, 1, 1)
+        self.assertEqual(MigrationWriter.serialize_datetime(naive_datetime),
+                         "datetime.datetime(2014, 1, 1, 1, 1)")
+
+        # datetime with utc timezone
+        utc_datetime = datetime.datetime(2014, 1, 1, 1, 1, tzinfo=utc)
+        self.assertEqual(MigrationWriter.serialize_datetime(utc_datetime),
+                         "datetime.datetime(2014, 1, 1, 1, 1, tzinfo=utc)")
+
+        # datetime with FixedOffset tzinfo
+        fixed_offset_datetime = datetime.datetime(2014, 1, 1, 1, 1, tzinfo=FixedOffset(180))
+        self.assertEqual(MigrationWriter.serialize_datetime(fixed_offset_datetime),
+                         "datetime.datetime(2013, 12, 31, 22, 1, tzinfo=utc)")