Ver Fonte

Fixed #24100 -- Made the migration signals dispatch its plan and apps.

Thanks Markus for your contribution and Tim for your review.
Simon Charette há 8 anos atrás
pai
commit
f937c9ec97

+ 26 - 4
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:

+ 8 - 4
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
+        )

+ 45 - 7
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):
         """

+ 2 - 2
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"])

+ 34 - 0
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::
 

+ 4 - 0
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
 ~~~~~~
 

+ 16 - 0
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)),
+            ],
+        ),
+    ]

+ 13 - 1
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']
+        )