Commit a407b846 authored by Rudy Mutter's avatar Rudy Mutter Committed by Tim Graham
Browse files

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

parent 12809e16
Loading
Loading
Loading
Loading
+4 −3
Original line number Diff line number Diff line
@@ -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
Original line number Diff line number Diff line
@@ -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
Original line number Diff line number Diff line
@@ -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
Original line number Diff line number Diff line
@@ -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
Original line number Diff line number Diff line
@@ -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)")