Fixed #32900 -- Improved migrations questioner prompts.
This commit is contained in:
parent
61c5eae516
commit
02bc7161ec
|
@ -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:
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue