Fixed #25304 -- Allowed management commands to check if migrations are applied.

This commit is contained in:
Mounir Messelmeni 2016-02-12 11:02:36 +01:00 committed by Tim Graham
parent 004ba0f99e
commit 50931dfa53
8 changed files with 76 additions and 39 deletions

View File

@ -12,7 +12,7 @@ from django.utils.encoding import force_str
class Command(BaseCommand): class Command(BaseCommand):
help = "Change a user's password for django.contrib.auth." help = "Change a user's password for django.contrib.auth."
requires_migrations_checks = True
requires_system_checks = False requires_system_checks = False
def _get_pass(self, prompt="Password: "): def _get_pass(self, prompt="Password: "):

View File

@ -23,6 +23,7 @@ class NotRunningInTTYException(Exception):
class Command(BaseCommand): class Command(BaseCommand):
help = 'Used to create a superuser.' help = 'Used to create a superuser.'
requires_migrations_checks = True
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(Command, self).__init__(*args, **kwargs) super(Command, self).__init__(*args, **kwargs)

View File

@ -11,8 +11,10 @@ from argparse import ArgumentParser
import django import django
from django.core import checks from django.core import checks
from django.core.exceptions import ImproperlyConfigured
from django.core.management.color import color_style, no_style from django.core.management.color import color_style, no_style
from django.db import connections from django.db import DEFAULT_DB_ALIAS, connections
from django.db.migrations.exceptions import MigrationSchemaMissing
from django.utils.encoding import force_str from django.utils.encoding import force_str
@ -165,6 +167,10 @@ class BaseCommand(object):
wrapped with ``BEGIN;`` and ``COMMIT;``. Default value is wrapped with ``BEGIN;`` and ``COMMIT;``. Default value is
``False``. ``False``.
``requires_migrations_checks``
A boolean; if ``True``, the command prints a warning if the set of
migrations on disk don't match the migrations in the database.
``requires_system_checks`` ``requires_system_checks``
A boolean; if ``True``, entire Django project will be checked for errors A boolean; if ``True``, entire Django project will be checked for errors
prior to executing the command. Default value is ``True``. prior to executing the command. Default value is ``True``.
@ -199,6 +205,7 @@ class BaseCommand(object):
can_import_settings = True can_import_settings = True
output_transaction = False # Whether to wrap the output in a "BEGIN; COMMIT;" output_transaction = False # Whether to wrap the output in a "BEGIN; COMMIT;"
leave_locale_alone = False leave_locale_alone = False
requires_migrations_checks = False
requires_system_checks = True requires_system_checks = True
def __init__(self, stdout=None, stderr=None, no_color=False): def __init__(self, stdout=None, stderr=None, no_color=False):
@ -336,6 +343,8 @@ class BaseCommand(object):
try: try:
if self.requires_system_checks and not options.get('skip_checks'): if self.requires_system_checks and not options.get('skip_checks'):
self.check() self.check()
if self.requires_migrations_checks:
self.check_migrations()
output = self.handle(*args, **options) output = self.handle(*args, **options)
if output: if output:
if self.output_transaction: if self.output_transaction:
@ -419,6 +428,38 @@ class BaseCommand(object):
else: else:
self.stdout.write(msg) self.stdout.write(msg)
def check_migrations(self):
"""
Print a warning if the set of migrations on disk don't match the
migrations in the database.
"""
from django.db.migrations.executor import MigrationExecutor
try:
executor = MigrationExecutor(connections[DEFAULT_DB_ALIAS])
except ImproperlyConfigured:
# No databases are configured (or the dummy one)
return
except MigrationSchemaMissing:
self.stdout.write(self.style.NOTICE(
"\nNot checking migrations as it is not possible to access/create the django_migrations table."
))
return
plan = executor.migration_plan(executor.loader.graph.leaf_nodes())
if plan:
apps_waiting_migration = sorted(set(migration.app_label for migration, backwards in plan))
self.stdout.write(
self.style.NOTICE(
"\nYou have %(unpplied_migration_count)s unapplied migration(s). "
"Your project may not work properly until you apply the "
"migrations for app(s): %(apps_waiting_migration)s." % {
"unpplied_migration_count": len(plan),
"apps_waiting_migration": ", ".join(apps_waiting_migration),
}
)
)
self.stdout.write(self.style.NOTICE("Run 'python manage.py migrate' to apply them.\n"))
def handle(self, *args, **options): def handle(self, *args, **options):
""" """
The actual logic of the command. Subclasses must implement The actual logic of the command. Subclasses must implement

View File

@ -8,12 +8,8 @@ import sys
from datetime import datetime from datetime import datetime
from django.conf import settings from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand, CommandError
from django.core.servers.basehttp import get_internal_wsgi_application, run from django.core.servers.basehttp import get_internal_wsgi_application, run
from django.db import DEFAULT_DB_ALIAS, connections
from django.db.migrations.exceptions import MigrationSchemaMissing
from django.db.migrations.executor import MigrationExecutor
from django.utils import autoreload, six from django.utils import autoreload, six
from django.utils.encoding import force_text, get_system_encoding from django.utils.encoding import force_text, get_system_encoding
@ -114,6 +110,8 @@ class Command(BaseCommand):
self.stdout.write("Performing system checks...\n\n") self.stdout.write("Performing system checks...\n\n")
self.check(display_num_errors=True) self.check(display_num_errors=True)
# Need to check migrations here, so can't use the
# requires_migrations_check attribute.
self.check_migrations() self.check_migrations()
now = datetime.now().strftime('%B %d, %Y - %X') now = datetime.now().strftime('%B %d, %Y - %X')
if six.PY2: if six.PY2:
@ -154,36 +152,5 @@ class Command(BaseCommand):
self.stdout.write(shutdown_message) self.stdout.write(shutdown_message)
sys.exit(0) sys.exit(0)
def check_migrations(self):
"""
Checks to see if the set of migrations on disk matches the
migrations in the database. Prints a warning if they don't match.
"""
try:
executor = MigrationExecutor(connections[DEFAULT_DB_ALIAS])
except ImproperlyConfigured:
# No databases are configured (or the dummy one)
return
except MigrationSchemaMissing:
self.stdout.write(self.style.NOTICE(
"\nNot checking migrations as it is not possible to access/create the django_migrations table."
))
return
plan = executor.migration_plan(executor.loader.graph.leaf_nodes())
if plan:
apps_waiting_migration = sorted(set(migration.app_label for migration, backwards in plan))
self.stdout.write(
self.style.NOTICE(
"\nYou have %(unpplied_migration_count)s unapplied migration(s). "
"Your project may not work properly until you apply the "
"migrations for app(s): %(apps_waiting_migration)s." % {
"unpplied_migration_count": len(plan),
"apps_waiting_migration": ", ".join(apps_waiting_migration),
}
)
)
self.stdout.write(self.style.NOTICE("Run 'python manage.py migrate' to apply them.\n"))
# Kept for backward compatibility # Kept for backward compatibility
BaseRunserverCommand = Command BaseRunserverCommand = Command

View File

@ -231,6 +231,14 @@ All attributes can be set in your derived class and can be used in
``True``, the output will automatically be wrapped with ``BEGIN;`` and ``True``, the output will automatically be wrapped with ``BEGIN;`` and
``COMMIT;``. Default value is ``False``. ``COMMIT;``. Default value is ``False``.
.. attribute:: BaseCommand.requires_migrations_checks
.. versionadded:: 1.10
A boolean; if ``True``, the command prints a warning if the set of
migrations on disk don't match the migrations in the database. A warning
doesn't prevent the command from executing. Default value is ``False``.
.. attribute:: BaseCommand.requires_system_checks .. attribute:: BaseCommand.requires_system_checks
A boolean; if ``True``, the entire Django project will be checked for A boolean; if ``True``, the entire Django project will be checked for

View File

@ -248,6 +248,12 @@ Management Commands
* Added a warning to :djadmin:`dumpdata` if a proxy model is specified (which * Added a warning to :djadmin:`dumpdata` if a proxy model is specified (which
results in no output) without its concrete parent. results in no output) without its concrete parent.
* The new :attr:`BaseCommand.requires_migrations_checks
<django.core.management.BaseCommand.requires_migrations_checks>` attribute
may be set to ``True`` if you want your command to print a warning, like
:djadmin:`runserver` does, if the set of migrations on disk don't match the
migrations in the database.
Migrations Migrations
~~~~~~~~~~ ~~~~~~~~~~

View File

@ -1367,7 +1367,7 @@ class ManageRunserver(AdminScriptTestCase):
Ensure runserver.check_migrations doesn't choke on empty DATABASES. Ensure runserver.check_migrations doesn't choke on empty DATABASES.
""" """
tested_connections = ConnectionHandler({}) tested_connections = ConnectionHandler({})
with mock.patch('django.core.management.commands.runserver.connections', new=tested_connections): with mock.patch('django.core.management.base.connections', new=tested_connections):
self.cmd.check_migrations() self.cmd.check_migrations()
def test_readonly_database(self): def test_readonly_database(self):

View File

@ -7,12 +7,14 @@ from django.core import management
from django.core.management import BaseCommand, CommandError, find_commands from django.core.management import BaseCommand, CommandError, find_commands
from django.core.management.utils import find_command, popen_wrapper from django.core.management.utils import find_command, popen_wrapper
from django.db import connection from django.db import connection
from django.test import SimpleTestCase, override_settings from django.test import SimpleTestCase, mock, override_settings
from django.test.utils import captured_stderr, extend_sys_path from django.test.utils import captured_stderr, extend_sys_path
from django.utils import translation from django.utils import translation
from django.utils._os import upath from django.utils._os import upath
from django.utils.six import StringIO from django.utils.six import StringIO
from .management.commands import dance
# A minimal set of apps to avoid system checks running on all apps. # A minimal set of apps to avoid system checks running on all apps.
@override_settings( @override_settings(
@ -161,6 +163,18 @@ class CommandTests(SimpleTestCase):
finally: finally:
BaseCommand.check = saved_check BaseCommand.check = saved_check
def test_check_migrations(self):
requires_migrations_checks = dance.Command.requires_migrations_checks
try:
with mock.patch.object(BaseCommand, 'check_migrations') as check_migrations:
management.call_command('dance', verbosity=0)
self.assertFalse(check_migrations.called)
dance.Command.requires_migrations_checks = True
management.call_command('dance', verbosity=0)
self.assertTrue(check_migrations.called)
finally:
dance.Command.requires_migrations_checks = requires_migrations_checks
class CommandRunTests(AdminScriptTestCase): class CommandRunTests(AdminScriptTestCase):
""" """