diff --git a/django/core/management/commands/migrate.py b/django/core/management/commands/migrate.py index a8e9831cda..a1eb190eb1 100644 --- a/django/core/management/commands/migrate.py +++ b/django/core/management/commands/migrate.py @@ -15,7 +15,7 @@ from django.db import DEFAULT_DB_ALIAS, connections, router, transaction from django.db.migrations.autodetector import MigrationAutodetector from django.db.migrations.executor import MigrationExecutor from django.db.migrations.loader import AmbiguityError -from django.db.migrations.state import ProjectState +from django.db.migrations.state import ModelState, ProjectState from django.utils.module_loading import module_has_submodule @@ -160,7 +160,10 @@ class Command(BaseCommand): % (targets[0][1], targets[0][0]) ) - emit_pre_migrate_signal(self.verbosity, self.interactive, connection.alias) + pre_migrate_apps = executor._create_project_state().apps + emit_pre_migrate_signal( + self.verbosity, self.interactive, connection.alias, apps=pre_migrate_apps, plan=plan, + ) # Run the syncdb phase. if run_syncdb: @@ -191,14 +194,33 @@ class Command(BaseCommand): "migrations, and then re-run 'manage.py migrate' to " "apply them." )) + post_migrate_apps = pre_migrate_apps else: fake = options['fake'] fake_initial = options['fake_initial'] - executor.migrate(targets, plan, fake=fake, fake_initial=fake_initial) + post_migrate_project_state = executor.migrate( + targets, plan, fake=fake, fake_initial=fake_initial + ) + post_migrate_apps = post_migrate_project_state.apps + + # Re-render models of real apps to include relationships now that + # we've got a final state. This wouldn't be necessary if real apps + # models were rendered with relationships in the first place. + with post_migrate_apps.bulk_update(): + model_keys = [] + for model_state in post_migrate_apps.real_models: + model_key = model_state.app_label, model_state.name_lower + model_keys.append(model_key) + post_migrate_apps.unregister_model(*model_key) + post_migrate_apps.render_multiple([ + ModelState.from_model(apps.get_model(*model_key)) for model_key in model_keys + ]) # Send the post_migrate signal, so individual apps can do whatever they need # to do at this point. - emit_post_migrate_signal(self.verbosity, self.interactive, connection.alias) + emit_post_migrate_signal( + self.verbosity, self.interactive, connection.alias, apps=post_migrate_apps, plan=plan, + ) def migration_progress_callback(self, action, migration=None, fake=False): if self.verbosity >= 1: diff --git a/django/core/management/sql.py b/django/core/management/sql.py index 0c86a944ac..c72519f30a 100644 --- a/django/core/management/sql.py +++ b/django/core/management/sql.py @@ -20,7 +20,7 @@ def sql_flush(style, connection, only_django=False, reset_sequences=True, allow_ return statements -def emit_pre_migrate_signal(verbosity, interactive, db): +def emit_pre_migrate_signal(verbosity, interactive, db, **kwargs): # Emit the pre_migrate signal for every application. for app_config in apps.get_app_configs(): if app_config.models_module is None: @@ -32,10 +32,12 @@ def emit_pre_migrate_signal(verbosity, interactive, db): app_config=app_config, verbosity=verbosity, interactive=interactive, - using=db) + using=db, + **kwargs + ) -def emit_post_migrate_signal(verbosity, interactive, db): +def emit_post_migrate_signal(verbosity, interactive, db, **kwargs): # Emit the post_migrate signal for every application. for app_config in apps.get_app_configs(): if app_config.models_module is None: @@ -47,4 +49,6 @@ def emit_post_migrate_signal(verbosity, interactive, db): app_config=app_config, verbosity=verbosity, interactive=interactive, - using=db) + using=db, + **kwargs + ) diff --git a/django/db/migrations/executor.py b/django/db/migrations/executor.py index fb1b833cfa..4487543f16 100644 --- a/django/db/migrations/executor.py +++ b/django/db/migrations/executor.py @@ -63,6 +63,9 @@ class MigrationExecutor(object): applied.add(migration) return plan + def _create_project_state(self): + return ProjectState(real_apps=list(self.loader.unmigrated_apps)) + def migrate(self, targets, plan=None, fake=False, fake_initial=False): """ Migrates the database up to the given targets. @@ -79,7 +82,9 @@ class MigrationExecutor(object): all_backwards = all(backwards for mig, backwards in plan) if not plan: - pass # Nothing to do for an empty plan + # Nothing to do for an empty plan, except for building the post + # migrate project state + state = self._create_project_state() elif all_forwards == all_backwards: # This should only happen if there's a mixed plan raise InvalidMigrationPlan( @@ -89,21 +94,27 @@ class MigrationExecutor(object): plan ) elif all_forwards: - self._migrate_all_forwards(plan, full_plan, fake=fake, fake_initial=fake_initial) + state = self._migrate_all_forwards(plan, full_plan, fake=fake, fake_initial=fake_initial) else: # No need to check for `elif all_backwards` here, as that condition # would always evaluate to true. - self._migrate_all_backwards(plan, full_plan, fake=fake) + state = self._migrate_all_backwards(plan, full_plan, fake=fake) self.check_replacements() + return state + def _migrate_all_forwards(self, plan, full_plan, fake, fake_initial): """ Take a list of 2-tuples of the form (migration instance, False) and apply them in the order they occur in the full_plan. """ migrations_to_run = {m[0] for m in plan} - state = ProjectState(real_apps=list(self.loader.unmigrated_apps)) + state = self._create_project_state() + applied_migrations = { + self.loader.graph.nodes[key] for key in self.loader.applied_migrations + if key in self.loader.graph.nodes + } for migration, _ in full_plan: if not migrations_to_run: # We remove every migration that we applied from this set so @@ -120,9 +131,14 @@ class MigrationExecutor(object): self.progress_callback("render_success") state = self.apply_migration(state, migration, fake=fake, fake_initial=fake_initial) migrations_to_run.remove(migration) - else: + elif migration in applied_migrations: + # Only mutate the state if the migration is actually applied + # to make sure the resulting state doesn't include changes + # from unrelated migrations. migration.mutate_state(state, preserve=False) + return state + def _migrate_all_backwards(self, plan, full_plan, fake): """ Take a list of 2-tuples of the form (migration instance, True) and @@ -136,7 +152,11 @@ class MigrationExecutor(object): migrations_to_run = {m[0] for m in plan} # Holds all migration states prior to the migrations being unapplied states = {} - state = ProjectState(real_apps=list(self.loader.unmigrated_apps)) + state = self._create_project_state() + applied_migrations = { + self.loader.graph.nodes[key] for key in self.loader.applied_migrations + if key in self.loader.graph.nodes + } if self.progress_callback: self.progress_callback("render_start") for migration, _ in full_plan: @@ -154,13 +174,31 @@ class MigrationExecutor(object): # The old state keeps as-is, we continue with the new state state = migration.mutate_state(state, preserve=True) migrations_to_run.remove(migration) - else: + elif migration in applied_migrations: + # Only mutate the state if the migration is actually applied + # to make sure the resulting state doesn't include changes + # from unrelated migrations. migration.mutate_state(state, preserve=False) if self.progress_callback: self.progress_callback("render_success") for migration, _ in plan: self.unapply_migration(states[migration], migration, fake=fake) + applied_migrations.remove(migration) + + # Generate the post migration state by starting from the state before + # the last migration is unapplied and mutating it to include all the + # remaining applied migrations. + last_unapplied_migration = plan[-1][0] + state = states[last_unapplied_migration] + for index, (migration, _) in enumerate(full_plan): + if migration == last_unapplied_migration: + for migration, _ in full_plan[index:]: + if migration in applied_migrations: + migration.mutate_state(state, preserve=False) + break + + return state def collect_sql(self, plan): """ diff --git a/django/db/models/signals.py b/django/db/models/signals.py index a87d473f94..b69a2812a6 100644 --- a/django/db/models/signals.py +++ b/django/db/models/signals.py @@ -65,5 +65,5 @@ m2m_changed = ModelSignal( use_caching=True, ) -pre_migrate = Signal(providing_args=["app_config", "verbosity", "interactive", "using"]) -post_migrate = Signal(providing_args=["app_config", "verbosity", "interactive", "using"]) +pre_migrate = Signal(providing_args=["app_config", "verbosity", "interactive", "using", "apps", "plan"]) +post_migrate = Signal(providing_args=["app_config", "verbosity", "interactive", "using", "apps", "plan"]) diff --git a/docs/ref/signals.txt b/docs/ref/signals.txt index b7371a1ed0..a3fad2d6ea 100644 --- a/docs/ref/signals.txt +++ b/docs/ref/signals.txt @@ -406,6 +406,23 @@ Arguments sent with this signal: ``using`` The alias of database on which a command will operate. +``plan`` + .. versionadded:: 1.10 + + The migration plan that is going to be used for the migration run. While + the plan is not public API, this allows for the rare cases when it is + necessary to know the plan. A plan is a list of two-tuples with the first + item being the instance of a migration class and the second item showing + if the migration was rolled back (``True``) or applied (``False``). + +``apps`` + .. versionadded:: 1.10 + + An instance of :data:`Apps ` containing the state of the + project before the migration run. It should be used instead of the global + :attr:`apps ` registry to retrieve the models you + want to perform operations on. + ``post_migrate`` ---------------- @@ -448,6 +465,23 @@ Arguments sent with this signal: The database alias used for synchronization. Defaults to the ``default`` database. +``plan`` + .. versionadded:: 1.10 + + The migration plan that was used for the migration run. While the plan is + not public API, this allows for the rare cases when it is necessary to + know the plan. A plan is a list of two-tuples with the first item being + the instance of a migration class and the second item showing if the + migration was rolled back (``True``) or applied (``False``). + +``apps`` + .. versionadded:: 1.10 + + An instance of :data:`Apps ` containing the state of the + project after the migration run. It should be used instead of the global + :attr:`apps ` registry to retrieve the models you + want to perform operations on. + For example, you could register a callback in an :class:`~django.apps.AppConfig` like this:: diff --git a/docs/releases/1.10.txt b/docs/releases/1.10.txt index 0463a284b3..9e24225173 100644 --- a/docs/releases/1.10.txt +++ b/docs/releases/1.10.txt @@ -368,6 +368,10 @@ Migrations migration history. If they find some unapplied dependencies of an applied migration, ``InconsistentMigrationHistory`` is raised. +* The :func:`~django.db.models.signals.pre_migrate` and + :func:`~django.db.models.signals.post_migrate` signals now dispatch their + migration ``plan`` and ``apps``. + Models ~~~~~~ diff --git a/tests/migrate_signals/custom_migrations/0001_initial.py b/tests/migrate_signals/custom_migrations/0001_initial.py new file mode 100644 index 0000000000..6e969d29ed --- /dev/null +++ b/tests/migrate_signals/custom_migrations/0001_initial.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + operations = [ + migrations.CreateModel( + "Signal", + [ + ("id", models.AutoField(primary_key=True)), + ], + ), + ] diff --git a/tests/migrate_signals/tests.py b/tests/migrate_signals/tests.py index ad828fa008..ca9721e71c 100644 --- a/tests/migrate_signals/tests.py +++ b/tests/migrate_signals/tests.py @@ -1,11 +1,12 @@ from django.apps import apps from django.core import management +from django.db import migrations from django.db.models import signals from django.test import TestCase, override_settings from django.utils import six APP_CONFIG = apps.get_app_config('migrate_signals') -SIGNAL_ARGS = ['app_config', 'verbosity', 'interactive', 'using'] +SIGNAL_ARGS = ['app_config', 'verbosity', 'interactive', 'using', 'plan', 'apps'] MIGRATE_DATABASE = 'default' MIGRATE_VERBOSITY = 1 MIGRATE_INTERACTIVE = False @@ -80,6 +81,8 @@ class MigrateSignalTests(TestCase): self.assertEqual(args['verbosity'], MIGRATE_VERBOSITY) self.assertEqual(args['interactive'], MIGRATE_INTERACTIVE) self.assertEqual(args['using'], 'default') + self.assertEqual(args['plan'], []) + self.assertIsInstance(args['apps'], migrations.state.StateApps) @override_settings(MIGRATION_MODULES={'migrate_signals': 'migrate_signals.custom_migrations'}) def test_migrations_only(self): @@ -101,3 +104,12 @@ class MigrateSignalTests(TestCase): self.assertEqual(args['verbosity'], MIGRATE_VERBOSITY) self.assertEqual(args['interactive'], MIGRATE_INTERACTIVE) self.assertEqual(args['using'], 'default') + self.assertIsInstance(args['plan'][0][0], migrations.Migration) + # The migration isn't applied backward. + self.assertFalse(args['plan'][0][1]) + self.assertIsInstance(args['apps'], migrations.state.StateApps) + self.assertEqual(pre_migrate_receiver.call_args['apps'].get_models(), []) + self.assertEqual( + [model._meta.label for model in post_migrate_receiver.call_args['apps'].get_models()], + ['migrate_signals.Signal'] + )