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

This commit is contained in:
Rudy Mutter 2014-09-06 13:42:36 -07:00 committed by Tim Graham
parent 12809e1609
commit a407b846b4
5 changed files with 56 additions and 11 deletions

View File

@ -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

View File

@ -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)

View File

@ -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
^^^^^^

View File

@ -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)

View File

@ -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)")