Fixed #25005 -- Made date and time fields with auto_now/auto_now_add use effective default.
Thanks to Andriy Sokolovskiy for initial patch.
This commit is contained in:
parent
f5ff5010cd
commit
49c57f8565
|
@ -1,9 +1,10 @@
|
||||||
import hashlib
|
import hashlib
|
||||||
import logging
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from django.db.backends.utils import truncate_name
|
from django.db.backends.utils import truncate_name
|
||||||
from django.db.transaction import atomic
|
from django.db.transaction import atomic
|
||||||
from django.utils import six
|
from django.utils import six, timezone
|
||||||
from django.utils.encoding import force_bytes
|
from django.utils.encoding import force_bytes
|
||||||
|
|
||||||
logger = logging.getLogger('django.db.backends.schema')
|
logger = logging.getLogger('django.db.backends.schema')
|
||||||
|
@ -201,6 +202,15 @@ class BaseDatabaseSchemaEditor(object):
|
||||||
default = six.binary_type()
|
default = six.binary_type()
|
||||||
else:
|
else:
|
||||||
default = six.text_type()
|
default = six.text_type()
|
||||||
|
elif getattr(field, 'auto_now', False) or getattr(field, 'auto_now_add', False):
|
||||||
|
default = datetime.now()
|
||||||
|
internal_type = field.get_internal_type()
|
||||||
|
if internal_type == 'DateField':
|
||||||
|
default = default.date
|
||||||
|
elif internal_type == 'TimeField':
|
||||||
|
default = default.time
|
||||||
|
elif internal_type == 'DateTimeField':
|
||||||
|
default = timezone.now
|
||||||
else:
|
else:
|
||||||
default = None
|
default = None
|
||||||
# If it's a callable, call it
|
# If it's a callable, call it
|
||||||
|
|
|
@ -802,10 +802,16 @@ class MigrationAutodetector(object):
|
||||||
# You can't just add NOT NULL fields with no default or fields
|
# You can't just add NOT NULL fields with no default or fields
|
||||||
# which don't allow empty strings as default.
|
# which don't allow empty strings as default.
|
||||||
preserve_default = True
|
preserve_default = True
|
||||||
if (not field.null and not field.has_default() and not field.many_to_many and
|
time_fields = (models.DateField, models.DateTimeField, models.TimeField)
|
||||||
not (field.blank and field.empty_strings_allowed)):
|
if (not field.null and not field.has_default() and
|
||||||
|
not field.many_to_many and
|
||||||
|
not (field.blank and field.empty_strings_allowed) and
|
||||||
|
not (isinstance(field, time_fields) and field.auto_now)):
|
||||||
field = field.clone()
|
field = field.clone()
|
||||||
field.default = self.questioner.ask_not_null_addition(field_name, model_name)
|
if isinstance(field, time_fields) and field.auto_now_add:
|
||||||
|
field.default = self.questioner.ask_auto_now_add_addition(field_name, model_name)
|
||||||
|
else:
|
||||||
|
field.default = self.questioner.ask_not_null_addition(field_name, model_name)
|
||||||
preserve_default = False
|
preserve_default = False
|
||||||
self.add_operation(
|
self.add_operation(
|
||||||
app_label,
|
app_label,
|
||||||
|
|
|
@ -76,6 +76,11 @@ class MigrationQuestioner(object):
|
||||||
"Do you really want to merge these migrations?"
|
"Do you really want to merge these migrations?"
|
||||||
return self.defaults.get("ask_merge", False)
|
return self.defaults.get("ask_merge", False)
|
||||||
|
|
||||||
|
def ask_auto_now_add_addition(self, field_name, model_name):
|
||||||
|
"Adding an auto_now_add field to a model"
|
||||||
|
# None means quit
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
class InteractiveMigrationQuestioner(MigrationQuestioner):
|
class InteractiveMigrationQuestioner(MigrationQuestioner):
|
||||||
|
|
||||||
|
@ -101,17 +106,36 @@ class InteractiveMigrationQuestioner(MigrationQuestioner):
|
||||||
pass
|
pass
|
||||||
result = input("Please select a valid option: ")
|
result = input("Please select a valid option: ")
|
||||||
|
|
||||||
def _ask_default(self):
|
def _ask_default(self, default=''):
|
||||||
|
"""
|
||||||
|
Prompt for a default value.
|
||||||
|
|
||||||
|
The ``default`` argument allows providing a custom default value (as a
|
||||||
|
string) which will be shown to the user and used as the return value
|
||||||
|
if the user doesn't provide any other input.
|
||||||
|
"""
|
||||||
print("Please enter the default value now, as valid Python")
|
print("Please enter the default value now, as valid Python")
|
||||||
|
if default:
|
||||||
|
print(
|
||||||
|
"You can accept the default '{}' by pressing 'Enter' or you "
|
||||||
|
"can provide another value.".format(default)
|
||||||
|
)
|
||||||
print("The datetime and django.utils.timezone modules are available, so you can do e.g. timezone.now")
|
print("The datetime and django.utils.timezone modules are available, so you can do e.g. timezone.now")
|
||||||
|
print("Type 'exit' to exit this prompt")
|
||||||
while True:
|
while True:
|
||||||
|
if default:
|
||||||
|
prompt = "[default: {}] >>> ".format(default)
|
||||||
|
else:
|
||||||
|
prompt = ">>> "
|
||||||
if six.PY3:
|
if six.PY3:
|
||||||
# Six does not correctly abstract over the fact that
|
# Six does not correctly abstract over the fact that
|
||||||
# py3 input returns a unicode string, while py2 raw_input
|
# py3 input returns a unicode string, while py2 raw_input
|
||||||
# returns a bytestring.
|
# returns a bytestring.
|
||||||
code = input(">>> ")
|
code = input(prompt)
|
||||||
else:
|
else:
|
||||||
code = input(">>> ").decode(sys.stdin.encoding)
|
code = input(prompt).decode(sys.stdin.encoding)
|
||||||
|
if not code and default:
|
||||||
|
code = default
|
||||||
if not code:
|
if not code:
|
||||||
print("Please enter some code, or 'exit' (with no quotes) to exit.")
|
print("Please enter some code, or 'exit' (with no quotes) to exit.")
|
||||||
elif code == "exit":
|
elif code == "exit":
|
||||||
|
@ -186,6 +210,25 @@ class InteractiveMigrationQuestioner(MigrationQuestioner):
|
||||||
False,
|
False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def ask_auto_now_add_addition(self, field_name, model_name):
|
||||||
|
"Adding an auto_now_add field to a model"
|
||||||
|
if not self.dry_run:
|
||||||
|
choice = self._choice_input(
|
||||||
|
"You are trying to add the field '{}' with 'auto_now_add=True' "
|
||||||
|
"to {} without a default; the database needs something to "
|
||||||
|
"populate existing rows.\n".format(field_name, model_name),
|
||||||
|
[
|
||||||
|
"Provide a one-off default now (will be set on all "
|
||||||
|
"existing rows)",
|
||||||
|
"Quit, and let me add a default in models.py",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
if choice == 2:
|
||||||
|
sys.exit(3)
|
||||||
|
else:
|
||||||
|
return self._ask_default(default='timezone.now')
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
class NonInteractiveMigrationQuestioner(MigrationQuestioner):
|
class NonInteractiveMigrationQuestioner(MigrationQuestioner):
|
||||||
|
|
||||||
|
@ -196,3 +239,7 @@ class NonInteractiveMigrationQuestioner(MigrationQuestioner):
|
||||||
def ask_not_null_alteration(self, field_name, model_name):
|
def ask_not_null_alteration(self, field_name, model_name):
|
||||||
# We can't ask the user, so set as not provided.
|
# We can't ask the user, so set as not provided.
|
||||||
return NOT_PROVIDED
|
return NOT_PROVIDED
|
||||||
|
|
||||||
|
def ask_auto_now_add_addition(self, field_name, model_name):
|
||||||
|
# We can't ask the user, so act like the user aborted.
|
||||||
|
sys.exit(3)
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Entry',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('title', models.CharField(max_length=255)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
|
@ -61,6 +61,18 @@ class AutodetectorTests(TestCase):
|
||||||
("id", models.AutoField(primary_key=True)),
|
("id", models.AutoField(primary_key=True)),
|
||||||
("name", models.CharField(max_length=200, default='Ada Lovelace')),
|
("name", models.CharField(max_length=200, default='Ada Lovelace')),
|
||||||
])
|
])
|
||||||
|
author_dates_of_birth_auto_now = ModelState("testapp", "Author", [
|
||||||
|
("id", models.AutoField(primary_key=True)),
|
||||||
|
("date_of_birth", models.DateField(auto_now=True)),
|
||||||
|
("date_time_of_birth", models.DateTimeField(auto_now=True)),
|
||||||
|
("time_of_birth", models.TimeField(auto_now=True)),
|
||||||
|
])
|
||||||
|
author_dates_of_birth_auto_now_add = ModelState("testapp", "Author", [
|
||||||
|
("id", models.AutoField(primary_key=True)),
|
||||||
|
("date_of_birth", models.DateField(auto_now_add=True)),
|
||||||
|
("date_time_of_birth", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("time_of_birth", models.TimeField(auto_now_add=True)),
|
||||||
|
])
|
||||||
author_name_deconstructible_1 = ModelState("testapp", "Author", [
|
author_name_deconstructible_1 = ModelState("testapp", "Author", [
|
||||||
("id", models.AutoField(primary_key=True)),
|
("id", models.AutoField(primary_key=True)),
|
||||||
("name", models.CharField(max_length=200, default=DeconstructibleObject())),
|
("name", models.CharField(max_length=200, default=DeconstructibleObject())),
|
||||||
|
@ -634,6 +646,51 @@ class AutodetectorTests(TestCase):
|
||||||
self.assertOperationTypes(changes, 'testapp', 0, ["AddField"])
|
self.assertOperationTypes(changes, 'testapp', 0, ["AddField"])
|
||||||
self.assertOperationAttributes(changes, "testapp", 0, 0, name="name")
|
self.assertOperationAttributes(changes, "testapp", 0, 0, name="name")
|
||||||
|
|
||||||
|
@mock.patch('django.db.migrations.questioner.MigrationQuestioner.ask_not_null_addition',
|
||||||
|
side_effect=AssertionError("Should not have prompted for not null addition"))
|
||||||
|
def test_add_date_fields_with_auto_now_not_asking_for_default(self, mocked_ask_method):
|
||||||
|
# Make state
|
||||||
|
before = self.make_project_state([self.author_empty])
|
||||||
|
after = self.make_project_state([self.author_dates_of_birth_auto_now])
|
||||||
|
autodetector = MigrationAutodetector(before, after)
|
||||||
|
changes = autodetector._detect_changes()
|
||||||
|
# Right number/type of migrations?
|
||||||
|
self.assertNumberMigrations(changes, 'testapp', 1)
|
||||||
|
self.assertOperationTypes(changes, 'testapp', 0, ["AddField", "AddField", "AddField"])
|
||||||
|
self.assertOperationFieldAttributes(changes, "testapp", 0, 0, auto_now=True)
|
||||||
|
self.assertOperationFieldAttributes(changes, "testapp", 0, 1, auto_now=True)
|
||||||
|
self.assertOperationFieldAttributes(changes, "testapp", 0, 2, auto_now=True)
|
||||||
|
|
||||||
|
@mock.patch('django.db.migrations.questioner.MigrationQuestioner.ask_not_null_addition',
|
||||||
|
side_effect=AssertionError("Should not have prompted for not null addition"))
|
||||||
|
def test_add_date_fields_with_auto_now_add_not_asking_for_null_addition(self, mocked_ask_method):
|
||||||
|
# Make state
|
||||||
|
before = self.make_project_state([self.author_empty])
|
||||||
|
after = self.make_project_state([self.author_dates_of_birth_auto_now_add])
|
||||||
|
autodetector = MigrationAutodetector(before, after)
|
||||||
|
changes = autodetector._detect_changes()
|
||||||
|
# Right number/type of migrations?
|
||||||
|
self.assertNumberMigrations(changes, 'testapp', 1)
|
||||||
|
self.assertOperationTypes(changes, 'testapp', 0, ["AddField", "AddField", "AddField"])
|
||||||
|
self.assertOperationFieldAttributes(changes, "testapp", 0, 0, auto_now_add=True)
|
||||||
|
self.assertOperationFieldAttributes(changes, "testapp", 0, 1, auto_now_add=True)
|
||||||
|
self.assertOperationFieldAttributes(changes, "testapp", 0, 2, auto_now_add=True)
|
||||||
|
|
||||||
|
@mock.patch('django.db.migrations.questioner.MigrationQuestioner.ask_auto_now_add_addition')
|
||||||
|
def test_add_date_fields_with_auto_now_add_asking_for_default(self, mocked_ask_method):
|
||||||
|
# Make state
|
||||||
|
before = self.make_project_state([self.author_empty])
|
||||||
|
after = self.make_project_state([self.author_dates_of_birth_auto_now_add])
|
||||||
|
autodetector = MigrationAutodetector(before, after)
|
||||||
|
changes = autodetector._detect_changes()
|
||||||
|
# Right number/type of migrations?
|
||||||
|
self.assertNumberMigrations(changes, 'testapp', 1)
|
||||||
|
self.assertOperationTypes(changes, 'testapp', 0, ["AddField", "AddField", "AddField"])
|
||||||
|
self.assertOperationFieldAttributes(changes, "testapp", 0, 0, auto_now_add=True)
|
||||||
|
self.assertOperationFieldAttributes(changes, "testapp", 0, 1, auto_now_add=True)
|
||||||
|
self.assertOperationFieldAttributes(changes, "testapp", 0, 2, auto_now_add=True)
|
||||||
|
self.assertEqual(mocked_ask_method.call_count, 3)
|
||||||
|
|
||||||
def test_remove_field(self):
|
def test_remove_field(self):
|
||||||
"""Tests autodetection of removed fields."""
|
"""Tests autodetection of removed fields."""
|
||||||
# Make state
|
# Make state
|
||||||
|
|
|
@ -4,6 +4,7 @@ from __future__ import unicode_literals
|
||||||
import codecs
|
import codecs
|
||||||
import importlib
|
import importlib
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.core.management import CommandError, call_command
|
from django.core.management import CommandError, call_command
|
||||||
|
@ -1078,6 +1079,30 @@ class MakeMigrationsTests(MigrationTestBase):
|
||||||
with self.assertRaisesMessage(InconsistentMigrationHistory, msg):
|
with self.assertRaisesMessage(InconsistentMigrationHistory, msg):
|
||||||
call_command("makemigrations")
|
call_command("makemigrations")
|
||||||
|
|
||||||
|
@mock.patch('django.db.migrations.questioner.input', return_value='1')
|
||||||
|
@mock.patch('django.db.migrations.questioner.sys.stdin', mock.MagicMock(encoding=sys.getdefaultencoding()))
|
||||||
|
def test_makemigrations_auto_now_add_interactive(self, *args):
|
||||||
|
"""
|
||||||
|
makemigrations prompts the user when adding auto_now_add to an existing
|
||||||
|
model.
|
||||||
|
"""
|
||||||
|
class Entry(models.Model):
|
||||||
|
title = models.CharField(max_length=255)
|
||||||
|
creation_date = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
app_label = 'migrations'
|
||||||
|
|
||||||
|
# Monkeypatch interactive questioner to auto accept
|
||||||
|
with mock.patch('django.db.migrations.questioner.sys.stdout', new_callable=six.StringIO) as prompt_stdout:
|
||||||
|
out = six.StringIO()
|
||||||
|
with self.temporary_migration_module(module='migrations.test_auto_now_add'):
|
||||||
|
call_command('makemigrations', 'migrations', interactive=True, stdout=out)
|
||||||
|
output = force_text(out.getvalue())
|
||||||
|
prompt_output = force_text(prompt_stdout.getvalue())
|
||||||
|
self.assertIn("You can accept the default 'timezone.now' by pressing 'Enter'", prompt_output)
|
||||||
|
self.assertIn("Add field creation_date to entry", output)
|
||||||
|
|
||||||
|
|
||||||
class SquashMigrationsTests(MigrationTestBase):
|
class SquashMigrationsTests(MigrationTestBase):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -18,8 +18,9 @@ from django.db.models.fields.related import (
|
||||||
)
|
)
|
||||||
from django.db.transaction import atomic
|
from django.db.transaction import atomic
|
||||||
from django.test import (
|
from django.test import (
|
||||||
TransactionTestCase, skipIfDBFeature, skipUnlessDBFeature,
|
TransactionTestCase, mock, skipIfDBFeature, skipUnlessDBFeature,
|
||||||
)
|
)
|
||||||
|
from django.utils.timezone import UTC
|
||||||
|
|
||||||
from .fields import (
|
from .fields import (
|
||||||
CustomManyToManyField, InheritedManyToManyField, MediumBlobField,
|
CustomManyToManyField, InheritedManyToManyField, MediumBlobField,
|
||||||
|
@ -124,8 +125,17 @@ class SchemaTests(TransactionTestCase):
|
||||||
constraints_for_column.append(name)
|
constraints_for_column.append(name)
|
||||||
return sorted(constraints_for_column)
|
return sorted(constraints_for_column)
|
||||||
|
|
||||||
# Tests
|
def check_added_field_default(self, schema_editor, model, field, field_name, expected_default,
|
||||||
|
cast_function=None):
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
schema_editor.add_field(model, field)
|
||||||
|
cursor.execute("SELECT {} FROM {};".format(field_name, model._meta.db_table))
|
||||||
|
database_default = cursor.fetchall()[0][0]
|
||||||
|
if cast_function and not type(database_default) == type(expected_default):
|
||||||
|
database_default = cast_function(database_default)
|
||||||
|
self.assertEqual(database_default, expected_default)
|
||||||
|
|
||||||
|
# Tests
|
||||||
def test_creation_deletion(self):
|
def test_creation_deletion(self):
|
||||||
"""
|
"""
|
||||||
Tries creating a model's table, and then deleting it.
|
Tries creating a model's table, and then deleting it.
|
||||||
|
@ -1833,3 +1843,63 @@ class SchemaTests(TransactionTestCase):
|
||||||
new_field.set_attributes_from_name('id')
|
new_field.set_attributes_from_name('id')
|
||||||
with connection.schema_editor() as editor:
|
with connection.schema_editor() as editor:
|
||||||
editor.alter_field(Node, old_field, new_field)
|
editor.alter_field(Node, old_field, new_field)
|
||||||
|
|
||||||
|
@mock.patch('django.db.backends.base.schema.datetime')
|
||||||
|
@mock.patch('django.db.backends.base.schema.timezone')
|
||||||
|
def test_add_datefield_and_datetimefield_use_effective_default(self, mocked_datetime, mocked_tz):
|
||||||
|
"""
|
||||||
|
effective_default() should be used for DateField, DateTimeField, and
|
||||||
|
TimeField if auto_now or auto_add_now is set (#25005).
|
||||||
|
"""
|
||||||
|
now = datetime.datetime(month=1, day=1, year=2000, hour=1, minute=1)
|
||||||
|
now_tz = datetime.datetime(month=1, day=1, year=2000, hour=1, minute=1, tzinfo=UTC())
|
||||||
|
mocked_datetime.now = mock.MagicMock(return_value=now)
|
||||||
|
mocked_tz.now = mock.MagicMock(return_value=now_tz)
|
||||||
|
# Create the table
|
||||||
|
with connection.schema_editor() as editor:
|
||||||
|
editor.create_model(Author)
|
||||||
|
# Check auto_now/auto_now_add attributes are not defined
|
||||||
|
columns = self.column_classes(Author)
|
||||||
|
self.assertNotIn("dob_auto_now", columns)
|
||||||
|
self.assertNotIn("dob_auto_now_add", columns)
|
||||||
|
self.assertNotIn("dtob_auto_now", columns)
|
||||||
|
self.assertNotIn("dtob_auto_now_add", columns)
|
||||||
|
self.assertNotIn("tob_auto_now", columns)
|
||||||
|
self.assertNotIn("tob_auto_now_add", columns)
|
||||||
|
# Create a row
|
||||||
|
Author.objects.create(name='Anonymous1')
|
||||||
|
# Ensure fields were added with the correct defaults
|
||||||
|
dob_auto_now = DateField(auto_now=True)
|
||||||
|
dob_auto_now.set_attributes_from_name('dob_auto_now')
|
||||||
|
self.check_added_field_default(
|
||||||
|
editor, Author, dob_auto_now, 'dob_auto_now', now.date(),
|
||||||
|
cast_function=lambda x: x.date(),
|
||||||
|
)
|
||||||
|
dob_auto_now_add = DateField(auto_now_add=True)
|
||||||
|
dob_auto_now_add.set_attributes_from_name('dob_auto_now_add')
|
||||||
|
self.check_added_field_default(
|
||||||
|
editor, Author, dob_auto_now_add, 'dob_auto_now_add', now.date(),
|
||||||
|
cast_function=lambda x: x.date(),
|
||||||
|
)
|
||||||
|
dtob_auto_now = DateTimeField(auto_now=True)
|
||||||
|
dtob_auto_now.set_attributes_from_name('dtob_auto_now')
|
||||||
|
self.check_added_field_default(
|
||||||
|
editor, Author, dtob_auto_now, 'dtob_auto_now', now,
|
||||||
|
)
|
||||||
|
dt_tm_of_birth_auto_now_add = DateTimeField(auto_now_add=True)
|
||||||
|
dt_tm_of_birth_auto_now_add.set_attributes_from_name('dtob_auto_now_add')
|
||||||
|
self.check_added_field_default(
|
||||||
|
editor, Author, dt_tm_of_birth_auto_now_add, 'dtob_auto_now_add', now,
|
||||||
|
)
|
||||||
|
tob_auto_now = TimeField(auto_now=True)
|
||||||
|
tob_auto_now.set_attributes_from_name('tob_auto_now')
|
||||||
|
self.check_added_field_default(
|
||||||
|
editor, Author, tob_auto_now, 'tob_auto_now', now.time(),
|
||||||
|
cast_function=lambda x: x.time(),
|
||||||
|
)
|
||||||
|
tob_auto_now_add = TimeField(auto_now_add=True)
|
||||||
|
tob_auto_now_add.set_attributes_from_name('tob_auto_now_add')
|
||||||
|
self.check_added_field_default(
|
||||||
|
editor, Author, tob_auto_now_add, 'tob_auto_now_add', now.time(),
|
||||||
|
cast_function=lambda x: x.time(),
|
||||||
|
)
|
||||||
|
|
Loading…
Reference in New Issue