diff --git a/django/core/management/commands/makemigrations.py b/django/core/management/commands/makemigrations.py index cb78107d335..d90f5b9428f 100644 --- a/django/core/management/commands/makemigrations.py +++ b/django/core/management/commands/makemigrations.py @@ -9,6 +9,7 @@ from django.db.migrations.autodetector import MigrationAutodetector from django.db.migrations.loader import MigrationLoader from django.db.migrations.questioner import ( InteractiveMigrationQuestioner, MigrationQuestioner, + NonInteractiveMigrationQuestioner, ) from django.db.migrations.state import ProjectState from django.db.migrations.writer import MigrationWriter @@ -93,11 +94,15 @@ class Command(BaseCommand): if self.merge and conflicts: return self.handle_merge(loader, conflicts) + if self.interactive: + questioner = InteractiveMigrationQuestioner(specified_apps=app_labels, dry_run=self.dry_run) + else: + questioner = NonInteractiveMigrationQuestioner(specified_apps=app_labels, dry_run=self.dry_run) # Set up autodetector autodetector = MigrationAutodetector( loader.project_state(), ProjectState.from_apps(apps), - InteractiveMigrationQuestioner(specified_apps=app_labels, dry_run=self.dry_run), + questioner, ) # If they want to make an empty migration, make one for each app diff --git a/django/db/migrations/questioner.py b/django/db/migrations/questioner.py index 5928e3dfab9..21ca6f2c751 100644 --- a/django/db/migrations/questioner.py +++ b/django/db/migrations/questioner.py @@ -180,3 +180,14 @@ class InteractiveMigrationQuestioner(MigrationQuestioner): "Do you want to merge these migration branches? [y/N]", False, ) + + +class NonInteractiveMigrationQuestioner(MigrationQuestioner): + + def ask_not_null_addition(self, field_name, model_name): + # We can't ask the user, so act like the user aborted. + sys.exit(3) + + def ask_not_null_alteration(self, field_name, model_name): + # We can't ask the user, so set as not provided. + return NOT_PROVIDED diff --git a/docs/ref/django-admin.txt b/docs/ref/django-admin.txt index 4d969889d5f..a354bdb0450 100644 --- a/docs/ref/django-admin.txt +++ b/docs/ref/django-admin.txt @@ -649,6 +649,11 @@ Providing one or more app names as arguments will limit the migrations created to the app(s) specified and any dependencies needed (the table at the other end of a ``ForeignKey``, for example). +.. versionchanged:: 1.9 + +The ``--noinput`` option may be provided to suppress all user prompts. If a suppressed +prompt cannot be resolved automatically, the command will exit with error code 3. + .. django-admin-option:: --empty The ``--empty`` option will cause ``makemigrations`` to output an empty @@ -666,9 +671,7 @@ written. .. django-admin-option:: --merge -The ``--merge`` option enables fixing of migration conflicts. The -:djadminopt:`--noinput` option may be provided to suppress user prompts during -a merge. +The ``--merge`` option enables fixing of migration conflicts. .. django-admin-option:: --name, -n diff --git a/tests/migrations/test_commands.py b/tests/migrations/test_commands.py index 86eb769a32e..ca7214be2bb 100644 --- a/tests/migrations/test_commands.py +++ b/tests/migrations/test_commands.py @@ -531,6 +531,80 @@ class MakeMigrationsTests(MigrationTestBase): questioner.input = old_input self.assertIn("Created new merge migration", force_text(out.getvalue())) + def test_makemigrations_non_interactive_not_null_addition(self): + """ + Tests that non-interactive makemigrations fails when a default is missing on a new not-null field. + """ + class SillyModel(models.Model): + silly_field = models.BooleanField(default=False) + silly_int = models.IntegerField() + + class Meta: + app_label = "migrations" + + out = six.StringIO() + with self.assertRaises(SystemExit): + with self.temporary_migration_module(module="migrations.test_migrations_no_default"): + call_command("makemigrations", "migrations", interactive=False, stdout=out) + + def test_makemigrations_non_interactive_not_null_alteration(self): + """ + Tests that non-interactive makemigrations fails when a default is missing on a field changed to not-null. + """ + class Author(models.Model): + name = models.CharField(max_length=255) + slug = models.SlugField() + age = models.IntegerField(default=0) + + class Meta: + app_label = "migrations" + + out = six.StringIO() + try: + with self.temporary_migration_module(module="migrations.test_migrations"): + call_command("makemigrations", "migrations", interactive=False, stdout=out) + except CommandError: + self.fail("Makemigrations failed while running non-interactive questioner.") + self.assertIn("Alter field slug on author", force_text(out.getvalue())) + + def test_makemigrations_non_interactive_no_model_rename(self): + """ + Makes sure that makemigrations adds and removes a possible model rename in non-interactive mode. + """ + class RenamedModel(models.Model): + silly_field = models.BooleanField(default=False) + + class Meta: + app_label = "migrations" + + out = six.StringIO() + try: + with self.temporary_migration_module(module="migrations.test_migrations_no_default"): + call_command("makemigrations", "migrations", interactive=False, stdout=out) + except CommandError: + self.fail("Makemigrations failed while running non-interactive questioner") + self.assertIn("Delete model SillyModel", force_text(out.getvalue())) + self.assertIn("Create model RenamedModel", force_text(out.getvalue())) + + def test_makemigrations_non_interactive_no_field_rename(self): + """ + Makes sure that makemigrations adds and removes a possible field rename in non-interactive mode. + """ + class SillyModel(models.Model): + silly_rename = models.BooleanField(default=False) + + class Meta: + app_label = "migrations" + + out = six.StringIO() + try: + with self.temporary_migration_module(module="migrations.test_migrations_no_default"): + call_command("makemigrations", "migrations", interactive=False, stdout=out) + except CommandError: + self.fail("Makemigrations failed while running non-interactive questioner") + self.assertIn("Remove field silly_field from sillymodel", force_text(out.getvalue())) + self.assertIn("Add field silly_rename to sillymodel", force_text(out.getvalue())) + def test_makemigrations_handle_merge(self): """ Makes sure that makemigrations properly merges the conflicting migrations with --noinput.