From e272904ff7cc4d138e316ba2e7811666053860a4 Mon Sep 17 00:00:00 2001
From: Marten Kenbeek <marten.knbk@gmail.com>
Date: Sat, 7 Mar 2015 13:53:13 +0100
Subject: [PATCH] Fixed #23407 -- Extended coverage of makemigrations --noinput
 option.

Changed --noinput option in makemigrations to suppress all user prompts,
not just when combined with --merge.
---
 .../management/commands/makemigrations.py     |  7 +-
 django/db/migrations/questioner.py            | 11 +++
 docs/ref/django-admin.txt                     |  9 ++-
 tests/migrations/test_commands.py             | 74 +++++++++++++++++++
 4 files changed, 97 insertions(+), 4 deletions(-)

diff --git a/django/core/management/commands/makemigrations.py b/django/core/management/commands/makemigrations.py
index cb78107d33..d90f5b9428 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 5928e3dfab..21ca6f2c75 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 4d969889d5..a354bdb045 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 86eb769a32..ca7214be2b 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.