1
0
mirror of https://github.com/django/django.git synced 2025-10-24 06:06:09 +00:00

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

Thanks Markus for your contribution and Tim for your review.
This commit is contained in:
Simon Charette
2016-05-13 11:58:54 -04:00
parent e475e84970
commit f937c9ec97
8 changed files with 148 additions and 18 deletions

View File

@@ -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:

View File

@@ -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
)

View File

@@ -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):
"""

View File

@@ -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"])