From a407b846b4c706a54887afaf26b052914601b8b5 Mon Sep 17 00:00:00 2001 From: Rudy Mutter Date: Sat, 6 Sep 2014 13:42:36 -0700 Subject: [PATCH] Fixed #23365 -- Added support for timezone-aware datetimes to migrations. --- django/db/migrations/questioner.py | 7 ++++--- django/db/migrations/writer.py | 24 +++++++++++++++++++----- docs/releases/1.8.txt | 2 ++ docs/topics/migrations.txt | 5 +++++ tests/migrations/test_writer.py | 29 ++++++++++++++++++++++++++--- 5 files changed, 56 insertions(+), 11 deletions(-) diff --git a/django/db/migrations/questioner.py b/django/db/migrations/questioner.py index 0476ba749f..3b31796cbc 100644 --- a/django/db/migrations/questioner.py +++ b/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 diff --git a/django/db/migrations/writer.py b/django/db/migrations/writer.py index 0c3be77e6b..17975409d1 100644 --- a/django/db/migrations/writer.py +++ b/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") + 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) diff --git a/docs/releases/1.8.txt b/docs/releases/1.8.txt index 331e386b41..beca378ac7 100644 --- a/docs/releases/1.8.txt +++ b/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 ^^^^^^ diff --git a/docs/topics/migrations.txt b/docs/topics/migrations.txt index 65ae6ac0da..dfee3df7ca 100644 --- a/docs/topics/migrations.txt +++ b/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 `) +.. 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) diff --git a/tests/migrations/test_writer.py b/tests/migrations/test_writer.py index 8aac038c1e..f98b2493aa 100644 --- a/tests/migrations/test_writer.py +++ b/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)")