Fixed #27432 -- Made app_label arguments limit showmigrations --plan output.

This commit is contained in:
Sebastian Spiegel 2016-11-06 13:52:06 +01:00 committed by Tim Graham
parent d976760260
commit 8b734d2f99
5 changed files with 121 additions and 12 deletions

View File

@ -685,6 +685,7 @@ answer newbie questions, and generally made Django that much better:
scott@staplefish.com scott@staplefish.com
Sean Brant Sean Brant
Sebastian Hillig <sebastian.hillig@gmail.com> Sebastian Hillig <sebastian.hillig@gmail.com>
Sebastian Spiegel <http://www.tivix.com/>
Selwin Ong <selwin@ui.co.id> Selwin Ong <selwin@ui.co.id>
Sengtha Chay <sengtha@e-khmer.com> Sengtha Chay <sengtha@e-khmer.com>
Senko Rašić <senko.rasic@dobarkod.hr> Senko Rašić <senko.rasic@dobarkod.hr>

View File

@ -43,10 +43,18 @@ class Command(BaseCommand):
connection = connections[db] connection = connections[db]
if options['format'] == "plan": if options['format'] == "plan":
return self.show_plan(connection) return self.show_plan(connection, options['app_label'])
else: else:
return self.show_list(connection, options['app_label']) return self.show_list(connection, options['app_label'])
def _validate_app_names(self, loader, app_names):
invalid_apps = []
for app_name in app_names:
if app_name not in loader.migrated_apps:
invalid_apps.append(app_name)
if invalid_apps:
raise CommandError('No migrations present for: %s' % (', '.join(sorted(invalid_apps))))
def show_list(self, connection, app_names=None): def show_list(self, connection, app_names=None):
""" """
Shows a list of all migrations on the system, or only those of Shows a list of all migrations on the system, or only those of
@ -57,12 +65,7 @@ class Command(BaseCommand):
graph = loader.graph graph = loader.graph
# If we were passed a list of apps, validate it # If we were passed a list of apps, validate it
if app_names: if app_names:
invalid_apps = [] self._validate_app_names(loader, app_names)
for app_name in app_names:
if app_name not in loader.migrated_apps:
invalid_apps.append(app_name)
if invalid_apps:
raise CommandError("No migrations present for: %s" % (", ".join(invalid_apps)))
# Otherwise, show all apps in alphabetic order # Otherwise, show all apps in alphabetic order
else: else:
app_names = sorted(loader.migrated_apps) app_names = sorted(loader.migrated_apps)
@ -88,13 +91,18 @@ class Command(BaseCommand):
if not shown: if not shown:
self.stdout.write(" (no migrations)", self.style.ERROR) self.stdout.write(" (no migrations)", self.style.ERROR)
def show_plan(self, connection): def show_plan(self, connection, app_names=None):
""" """
Shows all known migrations in the order they will be applied Shows all known migrations (or only those of the specified app_names)
in the order they will be applied.
""" """
# Load migrations from disk/DB # Load migrations from disk/DB
loader = MigrationLoader(connection) loader = MigrationLoader(connection)
graph = loader.graph graph = loader.graph
if app_names:
self._validate_app_names(loader, app_names)
targets = [key for key in graph.leaf_nodes() if key[0] in app_names]
else:
targets = graph.leaf_nodes() targets = graph.leaf_nodes()
plan = [] plan = []
seen = set() seen = set()

View File

@ -1015,11 +1015,17 @@ This is the default output format.
.. django-admin-option:: --plan, -p .. django-admin-option:: --plan, -p
Shows the migration plan Django will follow to apply migrations. Any supplied Shows the migration plan Django will follow to apply migrations. Like
app labels are ignored because the plan might go beyond those apps. Like
``--list``, applied migrations are marked by an ``[X]``. For a ``--verbosity`` ``--list``, applied migrations are marked by an ``[X]``. For a ``--verbosity``
of 2 and above, all dependencies of a migration will also be shown. of 2 and above, all dependencies of a migration will also be shown.
``app_label``\s arguments limit the output, however, dependencies of provided
apps may also be included.
.. versionchanged:: 1.11
In older versions, ``showmigrations --plan`` ignores app labels.
.. django-admin-option:: --database DATABASE .. django-admin-option:: --database DATABASE
Specifies the database to examine. Defaults to ``default``. Specifies the database to examine. Defaults to ``default``.

View File

@ -325,6 +325,9 @@ Management Commands
* The new :option:`diffsettings --default` option allows specifying a settings * The new :option:`diffsettings --default` option allows specifying a settings
module other than Django's default settings to compare against. module other than Django's default settings to compare against.
* ``app_label``\s arguments now limit the :option:`showmigrations --plan`
output.
Migrations Migrations
~~~~~~~~~~ ~~~~~~~~~~

View File

@ -339,6 +339,97 @@ class MigrateTests(MigrationTestBase):
out.getvalue().lower() out.getvalue().lower()
) )
@override_settings(INSTALLED_APPS=[
'migrations.migrations_test_apps.mutate_state_b',
'migrations.migrations_test_apps.alter_fk.author_app',
'migrations.migrations_test_apps.alter_fk.book_app',
])
def test_showmigrations_plan_single_app_label(self):
"""
`showmigrations --plan app_label` output with a single app_label.
"""
# Single app with no dependencies on other apps.
out = six.StringIO()
call_command('showmigrations', 'mutate_state_b', format='plan', stdout=out)
self.assertEqual(
'[ ] mutate_state_b.0001_initial\n'
'[ ] mutate_state_b.0002_add_field\n',
out.getvalue()
)
# Single app with dependencies.
out = six.StringIO()
call_command('showmigrations', 'author_app', format='plan', stdout=out)
self.assertEqual(
'[ ] author_app.0001_initial\n'
'[ ] book_app.0001_initial\n'
'[ ] author_app.0002_alter_id\n',
out.getvalue()
)
# Some migrations already applied.
call_command('migrate', 'author_app', '0001', verbosity=0)
out = six.StringIO()
call_command('showmigrations', 'author_app', format='plan', stdout=out)
self.assertEqual(
'[X] author_app.0001_initial\n'
'[ ] book_app.0001_initial\n'
'[ ] author_app.0002_alter_id\n',
out.getvalue()
)
# Cleanup by unmigrating author_app.
call_command('migrate', 'author_app', 'zero', verbosity=0)
@override_settings(INSTALLED_APPS=[
'migrations.migrations_test_apps.mutate_state_b',
'migrations.migrations_test_apps.alter_fk.author_app',
'migrations.migrations_test_apps.alter_fk.book_app',
])
def test_showmigrations_plan_multiple_app_labels(self):
"""
`showmigrations --plan app_label` output with multiple app_labels.
"""
# Multiple apps: author_app depends on book_app; mutate_state_b doesn't
# depend on other apps.
out = six.StringIO()
call_command('showmigrations', 'mutate_state_b', 'author_app', format='plan', stdout=out)
self.assertEqual(
'[ ] author_app.0001_initial\n'
'[ ] book_app.0001_initial\n'
'[ ] author_app.0002_alter_id\n'
'[ ] mutate_state_b.0001_initial\n'
'[ ] mutate_state_b.0002_add_field\n',
out.getvalue()
)
# Multiple apps: args order shouldn't matter (the same result is
# expected as above).
out = six.StringIO()
call_command('showmigrations', 'author_app', 'mutate_state_b', format='plan', stdout=out)
self.assertEqual(
'[ ] author_app.0001_initial\n'
'[ ] book_app.0001_initial\n'
'[ ] author_app.0002_alter_id\n'
'[ ] mutate_state_b.0001_initial\n'
'[ ] mutate_state_b.0002_add_field\n',
out.getvalue()
)
@override_settings(INSTALLED_APPS=['migrations.migrations_test_apps.unmigrated_app'])
def test_showmigrations_plan_app_label_error(self):
"""
`showmigrations --plan app_label` raises an error when no app or
no migrations are present in provided app labels.
"""
# App with no migrations.
with self.assertRaisesMessage(CommandError, 'No migrations present for: unmigrated_app'):
call_command('showmigrations', 'unmigrated_app', format='plan')
# Nonexistent app (wrong app label).
with self.assertRaisesMessage(CommandError, 'No migrations present for: nonexistent_app'):
call_command('showmigrations', 'nonexistent_app', format='plan')
# Multiple nonexistent apps; input order shouldn't matter.
with self.assertRaisesMessage(CommandError, 'No migrations present for: nonexistent_app1, nonexistent_app2'):
call_command('showmigrations', 'nonexistent_app1', 'nonexistent_app2', format='plan')
with self.assertRaisesMessage(CommandError, 'No migrations present for: nonexistent_app1, nonexistent_app2'):
call_command('showmigrations', 'nonexistent_app2', 'nonexistent_app1', format='plan')
@override_settings(MIGRATION_MODULES={"migrations": "migrations.test_migrations"}) @override_settings(MIGRATION_MODULES={"migrations": "migrations.test_migrations"})
def test_sqlmigrate_forwards(self): def test_sqlmigrate_forwards(self):
""" """