mirror of
https://github.com/django/django.git
synced 2024-11-29 06:03:25 +01:00
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 logging
|
||||
from datetime import datetime
|
||||
|
||||
from django.db.backends.utils import truncate_name
|
||||
from django.db.transaction import atomic
|
||||
from django.utils import six
|
||||
from django.utils import six, timezone
|
||||
from django.utils.encoding import force_bytes
|
||||
|
||||
logger = logging.getLogger('django.db.backends.schema')
|
||||
@ -201,6 +202,15 @@ class BaseDatabaseSchemaEditor(object):
|
||||
default = six.binary_type()
|
||||
else:
|
||||
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:
|
||||
default = None
|
||||
# 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
|
||||
# which don't allow empty strings as default.
|
||||
preserve_default = True
|
||||
if (not field.null and not field.has_default() and not field.many_to_many and
|
||||
not (field.blank and field.empty_strings_allowed)):
|
||||
time_fields = (models.DateField, models.DateTimeField, models.TimeField)
|
||||
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.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
|
||||
self.add_operation(
|
||||
app_label,
|
||||
|
@ -76,6 +76,11 @@ class MigrationQuestioner(object):
|
||||
"Do you really want to merge these migrations?"
|
||||
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):
|
||||
|
||||
@ -101,17 +106,36 @@ class InteractiveMigrationQuestioner(MigrationQuestioner):
|
||||
pass
|
||||
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")
|
||||
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("Type 'exit' to exit this prompt")
|
||||
while True:
|
||||
if default:
|
||||
prompt = "[default: {}] >>> ".format(default)
|
||||
else:
|
||||
prompt = ">>> "
|
||||
if six.PY3:
|
||||
# Six does not correctly abstract over the fact that
|
||||
# py3 input returns a unicode string, while py2 raw_input
|
||||
# returns a bytestring.
|
||||
code = input(">>> ")
|
||||
code = input(prompt)
|
||||
else:
|
||||
code = input(">>> ").decode(sys.stdin.encoding)
|
||||
code = input(prompt).decode(sys.stdin.encoding)
|
||||
if not code and default:
|
||||
code = default
|
||||
if not code:
|
||||
print("Please enter some code, or 'exit' (with no quotes) to exit.")
|
||||
elif code == "exit":
|
||||
@ -186,6 +210,25 @@ class InteractiveMigrationQuestioner(MigrationQuestioner):
|
||||
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):
|
||||
|
||||
@ -196,3 +239,7 @@ class NonInteractiveMigrationQuestioner(MigrationQuestioner):
|
||||
def ask_not_null_alteration(self, field_name, model_name):
|
||||
# We can't ask the user, so set as 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)
|
||||
|
19
tests/migrations/test_auto_now_add/0001_initial.py
Normal file
19
tests/migrations/test_auto_now_add/0001_initial.py
Normal file
@ -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)),
|
||||
],
|
||||
),
|
||||
]
|
0
tests/migrations/test_auto_now_add/__init__.py
Normal file
0
tests/migrations/test_auto_now_add/__init__.py
Normal file
@ -61,6 +61,18 @@ class AutodetectorTests(TestCase):
|
||||
("id", models.AutoField(primary_key=True)),
|
||||
("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", [
|
||||
("id", models.AutoField(primary_key=True)),
|
||||
("name", models.CharField(max_length=200, default=DeconstructibleObject())),
|
||||
@ -634,6 +646,51 @@ class AutodetectorTests(TestCase):
|
||||
self.assertOperationTypes(changes, 'testapp', 0, ["AddField"])
|
||||
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):
|
||||
"""Tests autodetection of removed fields."""
|
||||
# Make state
|
||||
|
@ -4,6 +4,7 @@ from __future__ import unicode_literals
|
||||
import codecs
|
||||
import importlib
|
||||
import os
|
||||
import sys
|
||||
|
||||
from django.apps import apps
|
||||
from django.core.management import CommandError, call_command
|
||||
@ -1078,6 +1079,30 @@ class MakeMigrationsTests(MigrationTestBase):
|
||||
with self.assertRaisesMessage(InconsistentMigrationHistory, msg):
|
||||
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):
|
||||
"""
|
||||
|
@ -18,8 +18,9 @@ from django.db.models.fields.related import (
|
||||
)
|
||||
from django.db.transaction import atomic
|
||||
from django.test import (
|
||||
TransactionTestCase, skipIfDBFeature, skipUnlessDBFeature,
|
||||
TransactionTestCase, mock, skipIfDBFeature, skipUnlessDBFeature,
|
||||
)
|
||||
from django.utils.timezone import UTC
|
||||
|
||||
from .fields import (
|
||||
CustomManyToManyField, InheritedManyToManyField, MediumBlobField,
|
||||
@ -124,8 +125,17 @@ class SchemaTests(TransactionTestCase):
|
||||
constraints_for_column.append(name)
|
||||
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):
|
||||
"""
|
||||
Tries creating a model's table, and then deleting it.
|
||||
@ -1833,3 +1843,63 @@ class SchemaTests(TransactionTestCase):
|
||||
new_field.set_attributes_from_name('id')
|
||||
with connection.schema_editor() as editor:
|
||||
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
Block a user