summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJacob Walls <jacobtylerwalls@gmail.com>2021-10-03 08:48:59 -0400
committerMariusz Felisiak <felisiak.mariusz@gmail.com>2021-10-06 08:05:33 +0200
commit47f791f1328bb434bae312aa7ff190d49884395b (patch)
tree774b0ec0dadc907295c9b5d885f5291f4bf87e7e
parent0f3e1a54bfa19f034f88bf3c25c67402d19e906c (diff)
Fixed #23408 -- Added migrations questioner prompt for adding unique fields with a callable default.
-rw-r--r--django/db/migrations/autodetector.py6
-rw-r--r--django/db/migrations/questioner.py27
-rw-r--r--tests/migrations/test_commands.py53
3 files changed, 86 insertions, 0 deletions
diff --git a/django/db/migrations/autodetector.py b/django/db/migrations/autodetector.py
index 594658ce99..3276320a1e 100644
--- a/django/db/migrations/autodetector.py
+++ b/django/db/migrations/autodetector.py
@@ -880,6 +880,12 @@ class MigrationAutodetector:
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)
+ if (
+ field.unique and
+ field.default is not models.NOT_PROVIDED and
+ callable(field.default)
+ ):
+ self.questioner.ask_unique_callable_default_addition(field_name, model_name)
self.add_operation(
app_label,
operations.AddField(
diff --git a/django/db/migrations/questioner.py b/django/db/migrations/questioner.py
index 3891616ce0..349e35130e 100644
--- a/django/db/migrations/questioner.py
+++ b/django/db/migrations/questioner.py
@@ -6,6 +6,7 @@ import sys
from django.apps import apps
from django.db.models import NOT_PROVIDED
from django.utils import timezone
+from django.utils.version import get_docs_version
from .loader import MigrationLoader
@@ -79,6 +80,11 @@ class MigrationQuestioner:
# None means quit
return None
+ def ask_unique_callable_default_addition(self, field_name, model_name):
+ """Adding a unique field with a callable default."""
+ # None means continue.
+ return None
+
class InteractiveMigrationQuestioner(MigrationQuestioner):
@@ -229,6 +235,27 @@ class InteractiveMigrationQuestioner(MigrationQuestioner):
return self._ask_default(default='timezone.now')
return None
+ def ask_unique_callable_default_addition(self, field_name, model_name):
+ """Adding a unique field with a callable default."""
+ if not self.dry_run:
+ version = get_docs_version()
+ choice = self._choice_input(
+ f'Callable default on unique field {model_name}.{field_name} '
+ f'will not generate unique values upon migrating.\n'
+ f'Please choose how to proceed:\n',
+ [
+ f'Continue making this migration as the first step in '
+ f'writing a manual migration to generate unique values '
+ f'described here: '
+ f'https://docs.djangoproject.com/en/{version}/howto/'
+ f'writing-migrations/#migrations-that-add-unique-fields.',
+ 'Quit and edit field options in models.py.',
+ ],
+ )
+ if choice == 2:
+ sys.exit(3)
+ return None
+
class NonInteractiveMigrationQuestioner(MigrationQuestioner):
diff --git a/tests/migrations/test_commands.py b/tests/migrations/test_commands.py
index d545f125dd..19bd0ef043 100644
--- a/tests/migrations/test_commands.py
+++ b/tests/migrations/test_commands.py
@@ -17,6 +17,8 @@ from django.db.migrations.exceptions import InconsistentMigrationHistory
from django.db.migrations.recorder import MigrationRecorder
from django.test import TestCase, override_settings, skipUnlessDBFeature
from django.test.utils import captured_stdout
+from django.utils import timezone
+from django.utils.version import get_docs_version
from .models import UnicodeModel, UnserializableModel
from .routers import TestRouter
@@ -1870,6 +1872,57 @@ class MakeMigrationsTests(MigrationTestBase):
with self.assertRaises(SystemExit):
call_command('makemigrations', 'migrations', interactive=True)
+ def test_makemigrations_interactive_unique_callable_default_addition(self):
+ """
+ makemigrations prompts the user when adding a unique field with
+ a callable default.
+ """
+ class Book(models.Model):
+ created = models.DateTimeField(unique=True, default=timezone.now)
+
+ class Meta:
+ app_label = 'migrations'
+
+ version = get_docs_version()
+ input_msg = (
+ f'Callable default on unique field book.created will not generate '
+ f'unique values upon migrating.\n'
+ f'Please choose how to proceed:\n\n'
+ f' 1) Continue making this migration as the first step in writing '
+ f'a manual migration to generate unique values described here: '
+ f'https://docs.djangoproject.com/en/{version}/howto/'
+ f'writing-migrations/#migrations-that-add-unique-fields.\n'
+ f' 2) Quit and edit field options in models.py.\n'
+ )
+ with self.temporary_migration_module(module='migrations.test_migrations'):
+ # 2 - quit.
+ with mock.patch('builtins.input', return_value='2'):
+ with captured_stdout() as out, self.assertRaises(SystemExit):
+ call_command('makemigrations', 'migrations', interactive=True)
+ out_value = out.getvalue()
+ self.assertIn(input_msg, out_value)
+ self.assertNotIn('Add field created to book', out_value)
+ # 1 - continue.
+ with mock.patch('builtins.input', return_value='1'):
+ with captured_stdout() as out:
+ call_command('makemigrations', 'migrations', interactive=True)
+ out_value = out.getvalue()
+ self.assertIn(input_msg, out_value)
+ self.assertIn('Add field created to book', out_value)
+
+ def test_makemigrations_non_interactive_unique_callable_default_addition(self):
+ class Book(models.Model):
+ created = models.DateTimeField(unique=True, default=timezone.now)
+
+ class Meta:
+ app_label = 'migrations'
+
+ with self.temporary_migration_module(module='migrations.test_migrations'):
+ with captured_stdout() as out:
+ call_command('makemigrations', 'migrations', interactive=False)
+ out_value = out.getvalue()
+ self.assertIn('Add field created to book', out_value)
+
class SquashMigrationsTests(MigrationTestBase):
"""