Fixed #32900 -- Improved migrations questioner prompts.

This commit is contained in:
Mateo Radman 2021-07-17 10:54:18 +03:00 committed by Mariusz Felisiak
parent 61c5eae516
commit 02bc7161ec
2 changed files with 64 additions and 50 deletions

View File

@ -71,7 +71,7 @@ class MigrationQuestioner:
return self.defaults.get("ask_rename_model", False) return self.defaults.get("ask_rename_model", False)
def ask_merge(self, app_label): def ask_merge(self, app_label):
"""Do you really want to merge these migrations?""" """Should these migrations really be merged?"""
return self.defaults.get("ask_merge", False) return self.defaults.get("ask_merge", False)
def ask_auto_now_add_addition(self, field_name, model_name): def ask_auto_now_add_addition(self, field_name, model_name):
@ -113,13 +113,16 @@ class InteractiveMigrationQuestioner(MigrationQuestioner):
string) which will be shown to the user and used as the return value string) which will be shown to the user and used as the return value
if the user doesn't provide any other input. if the user doesn't provide any other input.
""" """
print("Please enter the default value now, as valid Python") print('Please enter the default value as valid Python.')
if default: if default:
print( print(
"You can accept the default '{}' by pressing 'Enter' or you " f"Accept the default '{default}' by pressing 'Enter' or "
"can provide another value.".format(default) f"provide another value."
)
print(
'The datetime and django.utils.timezone modules are available, so '
'it is possible to provide e.g. timezone.now as a value.'
) )
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") print("Type 'exit' to exit this prompt")
while True: while True:
if default: if default:
@ -130,7 +133,7 @@ class InteractiveMigrationQuestioner(MigrationQuestioner):
if not code and default: if not code and default:
code = default code = default
if not code: if not code:
print("Please enter some code, or 'exit' (with no quotes) to exit.") print("Please enter some code, or 'exit' (without quotes) to exit.")
elif code == "exit": elif code == "exit":
sys.exit(1) sys.exit(1)
else: else:
@ -143,13 +146,15 @@ class InteractiveMigrationQuestioner(MigrationQuestioner):
"""Adding a NOT NULL field to a model.""" """Adding a NOT NULL field to a model."""
if not self.dry_run: if not self.dry_run:
choice = self._choice_input( choice = self._choice_input(
"You are trying to add a non-nullable field '%s' to %s without a default; " f"It is impossible to add a non-nullable field '{field_name}' "
"we can't do that (the database needs something to populate existing rows).\n" f"to {model_name} without specifying a default. This is "
"Please select a fix:" % (field_name, model_name), f"because the database needs something to populate existing "
f"rows.\n"
f"Please select a fix:",
[ [
("Provide a one-off default now (will be set on all existing " ("Provide a one-off default now (will be set on all existing "
"rows with a null value for this column)"), "rows with a null value for this column)"),
"Quit, and let me add a default in models.py", 'Quit and manually define a default value in models.py.',
] ]
) )
if choice == 2: if choice == 2:
@ -162,17 +167,18 @@ class InteractiveMigrationQuestioner(MigrationQuestioner):
"""Changing a NULL field to NOT NULL.""" """Changing a NULL field to NOT NULL."""
if not self.dry_run: if not self.dry_run:
choice = self._choice_input( choice = self._choice_input(
"You are trying to change the nullable field '%s' on %s to non-nullable " f"It is impossible to change a nullable field '{field_name}' "
"without a default; we can't do that (the database needs something to " f"on {model_name} to non-nullable without providing a "
"populate existing rows).\n" f"default. This is because the database needs something to "
"Please select a fix:" % (field_name, model_name), f"populate existing rows.\n"
f"Please select a fix:",
[ [
("Provide a one-off default now (will be set on all existing " ("Provide a one-off default now (will be set on all existing "
"rows with a null value for this column)"), "rows with a null value for this column)"),
("Ignore for now, and let me handle existing rows with NULL myself " 'Ignore for now. Existing rows that contain NULL values '
"(e.g. because you added a RunPython or RunSQL operation to handle " 'will have to be handled manually, for example with a '
"NULL values in a previous data migration)"), 'RunPython or RunSQL operation.',
"Quit, and let me add a default in models.py", 'Quit and manually define a default value in models.py.',
] ]
) )
if choice == 2: if choice == 2:
@ -185,13 +191,13 @@ class InteractiveMigrationQuestioner(MigrationQuestioner):
def ask_rename(self, model_name, old_name, new_name, field_instance): def ask_rename(self, model_name, old_name, new_name, field_instance):
"""Was this field really renamed?""" """Was this field really renamed?"""
msg = "Did you rename %s.%s to %s.%s (a %s)? [y/N]" msg = 'Was %s.%s renamed to %s.%s (a %s)? [y/N]'
return self._boolean_input(msg % (model_name, old_name, model_name, new_name, return self._boolean_input(msg % (model_name, old_name, model_name, new_name,
field_instance.__class__.__name__), False) field_instance.__class__.__name__), False)
def ask_rename_model(self, old_model_state, new_model_state): def ask_rename_model(self, old_model_state, new_model_state):
"""Was this model really renamed?""" """Was this model really renamed?"""
msg = "Did you rename the %s.%s model to %s? [y/N]" msg = 'Was the model %s.%s renamed to %s? [y/N]'
return self._boolean_input(msg % (old_model_state.app_label, old_model_state.name, return self._boolean_input(msg % (old_model_state.app_label, old_model_state.name,
new_model_state.name), False) new_model_state.name), False)
@ -199,7 +205,7 @@ class InteractiveMigrationQuestioner(MigrationQuestioner):
return self._boolean_input( return self._boolean_input(
"\nMerging will only work if the operations printed above do not conflict\n" + "\nMerging will only work if the operations printed above do not conflict\n" +
"with each other (working on different fields or models)\n" + "with each other (working on different fields or models)\n" +
"Do you want to merge these migration branches? [y/N]", 'Should these migration branches be merged?',
False, False,
) )
@ -207,13 +213,14 @@ class InteractiveMigrationQuestioner(MigrationQuestioner):
"""Adding an auto_now_add field to a model.""" """Adding an auto_now_add field to a model."""
if not self.dry_run: if not self.dry_run:
choice = self._choice_input( choice = self._choice_input(
"You are trying to add the field '{}' with 'auto_now_add=True' " f"It is impossible to add the field '{field_name}' with "
"to {} without a default; the database needs something to " f"'auto_now_add=True' to {model_name} without providing a "
"populate existing rows.\n".format(field_name, model_name), f"default. This is because the database needs something to "
f"populate existing rows.\n",
[ [
"Provide a one-off default now (will be set on all " 'Provide a one-off default now which will be set on all '
"existing rows)", 'existing rows',
"Quit, and let me add a default in models.py", 'Quit and manually define a default value in models.py.',
] ]
) )
if choice == 2: if choice == 2:

View File

@ -1360,13 +1360,13 @@ class MakeMigrationsTests(MigrationTestBase):
app_label = 'migrations' app_label = 'migrations'
input_msg = ( input_msg = (
"You are trying to add a non-nullable field 'silly_field' to " "It is impossible to add a non-nullable field 'silly_field' to "
"author without a default; we can't do that (the database needs " "author without specifying a default. This is because the "
"something to populate existing rows).\n" "database needs something to populate existing rows.\n"
"Please select a fix:\n" "Please select a fix:\n"
" 1) Provide a one-off default now (will be set on all existing " " 1) Provide a one-off default now (will be set on all existing "
"rows with a null value for this column)\n" "rows with a null value for this column)\n"
" 2) Quit, and let me add a default in models.py" " 2) Quit and manually define a default value in models.py."
) )
with self.temporary_migration_module(module='migrations.test_migrations'): with self.temporary_migration_module(module='migrations.test_migrations'):
# 2 - quit. # 2 - quit.
@ -1380,10 +1380,11 @@ class MakeMigrationsTests(MigrationTestBase):
call_command('makemigrations', 'migrations', interactive=True) call_command('makemigrations', 'migrations', interactive=True)
output = out.getvalue() output = out.getvalue()
self.assertIn(input_msg, output) self.assertIn(input_msg, output)
self.assertIn('Please enter the default value now, as valid Python', output) self.assertIn('Please enter the default value as valid Python.', output)
self.assertIn( self.assertIn(
'The datetime and django.utils.timezone modules are ' 'The datetime and django.utils.timezone modules are '
'available, so you can do e.g. timezone.now', 'available, so it is possible to provide e.g. timezone.now as '
'a value',
output, output,
) )
self.assertIn("Type 'exit' to exit this prompt", output) self.assertIn("Type 'exit' to exit this prompt", output)
@ -1418,16 +1419,16 @@ class MakeMigrationsTests(MigrationTestBase):
app_label = 'migrations' app_label = 'migrations'
input_msg = ( input_msg = (
"You are trying to change the nullable field 'slug' on author to " "It is impossible to change a nullable field 'slug' on author to "
"non-nullable without a default; we can't do that (the database " "non-nullable without providing a default. This is because the "
"needs something to populate existing rows).\n" "database needs something to populate existing rows.\n"
"Please select a fix:\n" "Please select a fix:\n"
" 1) Provide a one-off default now (will be set on all existing " " 1) Provide a one-off default now (will be set on all existing "
"rows with a null value for this column)\n" "rows with a null value for this column)\n"
" 2) Ignore for now, and let me handle existing rows with NULL " " 2) Ignore for now. Existing rows that contain NULL values will "
"myself (e.g. because you added a RunPython or RunSQL operation " "have to be handled manually, for example with a RunPython or "
"to handle NULL values in a previous data migration)\n" "RunSQL operation.\n"
" 3) Quit, and let me add a default in models.py" " 3) Quit and manually define a default value in models.py."
) )
with self.temporary_migration_module(module='migrations.test_migrations'): with self.temporary_migration_module(module='migrations.test_migrations'):
# 3 - quit. # 3 - quit.
@ -1441,10 +1442,11 @@ class MakeMigrationsTests(MigrationTestBase):
call_command('makemigrations', 'migrations', interactive=True) call_command('makemigrations', 'migrations', interactive=True)
output = out.getvalue() output = out.getvalue()
self.assertIn(input_msg, output) self.assertIn(input_msg, output)
self.assertIn('Please enter the default value now, as valid Python', output) self.assertIn('Please enter the default value as valid Python.', output)
self.assertIn( self.assertIn(
'The datetime and django.utils.timezone modules are ' 'The datetime and django.utils.timezone modules are '
'available, so you can do e.g. timezone.now', 'available, so it is possible to provide e.g. timezone.now as '
'a value',
output, output,
) )
self.assertIn("Type 'exit' to exit this prompt", output) self.assertIn("Type 'exit' to exit this prompt", output)
@ -1814,12 +1816,13 @@ class MakeMigrationsTests(MigrationTestBase):
app_label = 'migrations' app_label = 'migrations'
input_msg = ( input_msg = (
"You are trying to add the field 'creation_date' with " "It is impossible to add the field 'creation_date' with "
"'auto_now_add=True' to entry without a default; the database " "'auto_now_add=True' to entry without providing a default. This "
"needs something to populate existing rows.\n\n" "is because the database needs something to populate existing "
" 1) Provide a one-off default now (will be set on all existing " "rows.\n\n"
"rows)\n" " 1) Provide a one-off default now which will be set on all "
" 2) Quit, and let me add a default in models.py" "existing rows\n"
" 2) Quit and manually define a default value in models.py."
) )
# Monkeypatch interactive questioner to auto accept # Monkeypatch interactive questioner to auto accept
with mock.patch('django.db.migrations.questioner.sys.stdout', new_callable=io.StringIO) as prompt_stdout: with mock.patch('django.db.migrations.questioner.sys.stdout', new_callable=io.StringIO) as prompt_stdout:
@ -1830,10 +1833,14 @@ class MakeMigrationsTests(MigrationTestBase):
prompt_output = prompt_stdout.getvalue() prompt_output = prompt_stdout.getvalue()
self.assertIn(input_msg, prompt_output) self.assertIn(input_msg, prompt_output)
self.assertIn( self.assertIn(
'Please enter the default value now, as valid Python', 'Please enter the default value as valid Python.',
prompt_output,
)
self.assertIn(
"Accept the default 'timezone.now' by pressing 'Enter' or "
"provide another value.",
prompt_output, prompt_output,
) )
self.assertIn("You can accept the default 'timezone.now' by pressing 'Enter'", prompt_output)
self.assertIn("Type 'exit' to exit this prompt", prompt_output) self.assertIn("Type 'exit' to exit this prompt", prompt_output)
self.assertIn("Add field creation_date to entry", output) self.assertIn("Add field creation_date to entry", output)