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): """