summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSimon Charette <charette.s@gmail.com>2016-05-13 11:58:54 -0400
committerSimon Charette <charette.s@gmail.com>2016-05-15 19:51:15 -0400
commitf937c9ec975ebd719f0c22e5d1d5f5fb87ff1edd (patch)
treec3af8fe0e962e6ec07ba01cac9cd76019230c41e
parente475e849703d937e158e75e7a6d9cb99090857f6 (diff)
Fixed #24100 -- Made the migration signals dispatch its plan and apps.
Thanks Markus for your contribution and Tim for your review.
-rw-r--r--django/core/management/commands/migrate.py30
-rw-r--r--django/core/management/sql.py12
-rw-r--r--django/db/migrations/executor.py52
-rw-r--r--django/db/models/signals.py4
-rw-r--r--docs/ref/signals.txt34
-rw-r--r--docs/releases/1.10.txt4
-rw-r--r--tests/migrate_signals/custom_migrations/0001_initial.py16
-rw-r--r--tests/migrate_signals/tests.py14
8 files changed, 148 insertions, 18 deletions
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 <django.apps>` containing the state of the
+ project before the migration run. It should be used instead of the global
+ :attr:`apps <django.apps.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 <django.apps.apps>` containing the state of the
+ project after the migration run. It should be used instead of the global
+ :attr:`apps <django.apps.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']
+ )