Commit f937c9ec authored by Simon Charette's avatar Simon Charette
Browse files

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

Thanks Markus for your contribution and Tim for your review.
parent e475e849
Loading
Loading
Loading
Loading
+26 −4
Original line number Diff line number Diff line
@@ -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
Original line number Diff line number Diff line
@@ -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
Original line number Diff line number Diff line
@@ -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
Original line number Diff line number Diff line
@@ -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
Original line number Diff line number Diff line
@@ -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::

Loading