mirror of https://github.com/django/django.git
Fixed #25304 -- Allowed management commands to check if migrations are applied.
This commit is contained in:
parent
004ba0f99e
commit
50931dfa53
|
@ -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: "):
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
~~~~~~~~~~
|
~~~~~~~~~~
|
||||||
|
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
|
Loading…
Reference in New Issue