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 import sys
from django.apps import apps 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 django.utils.six.moves import input
from .loader import MIGRATIONS_MODULE_NAME from .loader import MIGRATIONS_MODULE_NAME
@ -108,7 +108,8 @@ class InteractiveMigrationQuestioner(MigrationQuestioner):
sys.exit(3) sys.exit(3)
else: else:
print("Please enter the default value now, as valid Python") 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: while True:
if six.PY3: if six.PY3:
# Six does not correctly abstract over the fact that # Six does not correctly abstract over the fact that
@ -123,7 +124,7 @@ class InteractiveMigrationQuestioner(MigrationQuestioner):
sys.exit(1) sys.exit(1)
else: else:
try: try:
return eval(code, {}, {"datetime": datetime_safe}) return eval(code, {}, {"datetime": datetime_safe, "timezone": timezone})
except (SyntaxError, NameError) as e: except (SyntaxError, NameError) as e:
print("Invalid input: %s" % e) print("Invalid input: %s" % e)
return None 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 import datetime_safe, six
from django.utils.encoding import force_text from django.utils.encoding import force_text
from django.utils.functional import Promise from django.utils.functional import Promise
from django.utils.timezone import utc
COMPILED_REGEX_TYPE = type(re.compile('')) COMPILED_REGEX_TYPE = type(re.compile(''))
@ -164,6 +165,20 @@ class MigrationWriter(object):
return (MIGRATION_TEMPLATE % items).encode("utf8") 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 @property
def filename(self): def filename(self):
return "%s.py" % self.migration.name 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 return "{%s}" % (", ".join("%s: %s" % (k, v) for k, v in strings)), imports
# Datetimes # Datetimes
elif isinstance(value, datetime.datetime): elif isinstance(value, datetime.datetime):
value_repr = cls.serialize_datetime(value)
imports = ["import datetime"]
if value.tzinfo is not None: 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.") imports.append("from django.utils.timezone import utc")
value_repr = repr(value) return value_repr, set(imports)
if isinstance(value, datetime_safe.datetime):
value_repr = "datetime.%s" % value_repr
return value_repr, {"import datetime"}
# Dates # Dates
elif isinstance(value, datetime.date): elif isinstance(value, datetime.date):
value_repr = repr(value) value_repr = repr(value)

View File

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

View File

@ -543,12 +543,17 @@ Django can serialize the following:
- ``int``, ``long``, ``float``, ``bool``, ``str``, ``unicode``, ``bytes``, ``None`` - ``int``, ``long``, ``float``, ``bool``, ``str``, ``unicode``, ``bytes``, ``None``
- ``list``, ``set``, ``tuple``, ``dict`` - ``list``, ``set``, ``tuple``, ``dict``
- ``datetime.date``, ``datetime.time``, and ``datetime.datetime`` instances - ``datetime.date``, ``datetime.time``, and ``datetime.datetime`` instances
(include those that are timezone-aware)
- ``decimal.Decimal`` instances - ``decimal.Decimal`` instances
- Any Django field - Any Django field
- Any function or method reference (e.g. ``datetime.datetime.today``) (must be in module's top-level scope) - 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) - Any class reference (must be in module's top-level scope)
- Anything with a custom ``deconstruct()`` method (:ref:`see below <custom-deconstruct-method>`) - 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: Django can serialize the following on Python 3 only:
- Unbound methods used from within the class body (see below) - 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 import datetime_safe, six
from django.utils.deconstruct import deconstructible from django.utils.deconstruct import deconstructible
from django.utils.translation import ugettext_lazy as _ 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.operations
import custom_migration_operations.more_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.date.today) self.assertSerializedEqual(datetime.date.today)
self.assertSerializedEqual(datetime.datetime.now().time()) self.assertSerializedEqual(datetime.datetime.now().time())
with self.assertRaises(ValueError): self.assertSerializedEqual(datetime.datetime(2014, 1, 1, 1, 1, tzinfo=get_default_timezone()))
self.assertSerializedEqual(datetime.datetime(2012, 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) safe_date = datetime_safe.date(2014, 3, 31)
string, imports = MigrationWriter.serialize(safe_date) string, imports = MigrationWriter.serialize(safe_date)
self.assertEqual(string, repr(datetime.date(2014, 3, 31))) self.assertEqual(string, repr(datetime.date(2014, 3, 31)))
@ -111,6 +111,10 @@ class WriterTests(TestCase):
string, imports = MigrationWriter.serialize(safe_datetime) string, imports = MigrationWriter.serialize(safe_datetime)
self.assertEqual(string, repr(datetime.datetime(2014, 3, 31, 16, 4, 31))) self.assertEqual(string, repr(datetime.datetime(2014, 3, 31, 16, 4, 31)))
self.assertEqual(imports, {'import datetime'}) 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 # Django fields
self.assertSerializedFieldEqual(models.CharField(max_length=255)) self.assertSerializedFieldEqual(models.CharField(max_length=255))
self.assertSerializedFieldEqual(models.TextField(null=True, blank=True)) 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'].operations.TestOperation,
result['custom_migration_operations'].more_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)")