diff --git a/django/db/migrations/state.py b/django/db/migrations/state.py index 5754bd3a97..ad2adc57e8 100644 --- a/django/db/migrations/state.py +++ b/django/db/migrations/state.py @@ -101,12 +101,25 @@ class ProjectState(object): # Get all outgoing references from the model to be rendered model_state = self.models[(app_label, model_name)] + # Directly related models are the models pointed to by ForeignKeys, + # OneToOneFields, and ManyToManyFields. + direct_related_models = set() for name, field in model_state.fields: if field.is_relation: if field.rel.to == RECURSIVE_RELATIONSHIP_CONSTANT: continue rel_app_label, rel_model_name = _get_app_label_and_model_name(field.rel.to, app_label) - related_models.add((rel_app_label, rel_model_name.lower())) + direct_related_models.add((rel_app_label, rel_model_name.lower())) + + # For all direct related models recursively get all related models. + related_models.update(direct_related_models) + for rel_app_label, rel_model_name in direct_related_models: + try: + rel_model = self.apps.get_model(rel_app_label, rel_model_name) + except LookupError: + pass + else: + related_models.update(get_related_models_recursive(rel_model)) # Include the model itself related_models.add((app_label, model_name)) diff --git a/docs/releases/1.8.1.txt b/docs/releases/1.8.1.txt index b226b0928c..54382d5c33 100644 --- a/docs/releases/1.8.1.txt +++ b/docs/releases/1.8.1.txt @@ -63,6 +63,9 @@ Bugfixes * Fixed JavaScript path of ``contrib.admin``’s related field widget when using alternate static file storages (:ticket:`24655`). +* Fixed a migration crash when adding new relations to models + (:ticket:`24573`). + Optimizations ============= diff --git a/tests/migrations/test_state.py b/tests/migrations/test_state.py index 663815d6f9..33016fdfce 100644 --- a/tests/migrations/test_state.py +++ b/tests/migrations/test_state.py @@ -1,7 +1,7 @@ from django.apps.registry import Apps from django.db import models from django.db.migrations.operations import ( - AlterField, DeleteModel, RemoveField, + AddField, AlterField, DeleteModel, RemoveField, ) from django.db.migrations.state import ( InvalidBasesError, ModelState, ProjectState, get_related_models_recursive, @@ -368,6 +368,57 @@ class StateTests(TestCase): project_state.add_model(ModelState.from_model(B)) self.assertEqual(len(project_state.apps.get_models()), 2) + def test_add_relations(self): + """ + #24573 - Adding relations to existing models should reload the + referenced models too. + """ + class A(models.Model): + class Meta: + app_label = 'something' + + class B(A): + class Meta: + app_label = 'something' + + class C(models.Model): + class Meta: + app_label = 'something' + + project_state = ProjectState() + project_state.add_model(ModelState.from_model(A)) + project_state.add_model(ModelState.from_model(B)) + project_state.add_model(ModelState.from_model(C)) + + project_state.apps # We need to work with rendered models + + old_state = project_state.clone() + model_a_old = old_state.apps.get_model('something', 'A') + model_b_old = old_state.apps.get_model('something', 'B') + model_c_old = old_state.apps.get_model('something', 'C') + # Check that the relations between the old models are correct + self.assertIs(model_a_old._meta.get_field('b').related_model, model_b_old) + self.assertIs(model_b_old._meta.get_field('a_ptr').related_model, model_a_old) + + operation = AddField('c', 'to_a', models.OneToOneField('something.A', related_name='from_c')) + operation.state_forwards('something', project_state) + model_a_new = project_state.apps.get_model('something', 'A') + model_b_new = project_state.apps.get_model('something', 'B') + model_c_new = project_state.apps.get_model('something', 'C') + + # Check that all models have changed + self.assertIsNot(model_a_old, model_a_new) + self.assertIsNot(model_b_old, model_b_new) + self.assertIsNot(model_c_old, model_c_new) + # Check that the relations between the old models still hold + self.assertIs(model_a_old._meta.get_field('b').related_model, model_b_old) + self.assertIs(model_b_old._meta.get_field('a_ptr').related_model, model_a_old) + # Check that the relations between the new models correct + self.assertIs(model_a_new._meta.get_field('b').related_model, model_b_new) + self.assertIs(model_b_new._meta.get_field('a_ptr').related_model, model_a_new) + self.assertIs(model_a_new._meta.get_field('from_c').related_model, model_c_new) + self.assertIs(model_c_new._meta.get_field('to_a').related_model, model_a_new) + def test_remove_relations(self): """ #24225 - Tests that relations between models are updated while