From aa4acc164d1247c0de515c959f7b09648b57dc42 Mon Sep 17 00:00:00 2001 From: David Wobrock Date: Sat, 25 Apr 2020 17:20:26 +0200 Subject: [PATCH] Fixed #29899 -- Made autodetector use model states instead of model classes. Thanks Simon Charette and Markus Holtermann for reviews. --- django/db/migrations/autodetector.py | 220 ++++++++++++++------------ django/db/migrations/state.py | 79 ++++++++- tests/migrations/test_autodetector.py | 98 +++++++----- 3 files changed, 249 insertions(+), 148 deletions(-) diff --git a/django/db/migrations/autodetector.py b/django/db/migrations/autodetector.py index bf53a22ede..77cfc01ca9 100644 --- a/django/db/migrations/autodetector.py +++ b/django/db/migrations/autodetector.py @@ -9,7 +9,9 @@ from django.db.migrations.migration import Migration from django.db.migrations.operations.models import AlterModelOptions from django.db.migrations.optimizer import MigrationOptimizer from django.db.migrations.questioner import MigrationQuestioner -from django.db.migrations.utils import COMPILED_REGEX_TYPE, RegexObject +from django.db.migrations.utils import ( + COMPILED_REGEX_TYPE, RegexObject, resolve_relation, +) from django.utils.topological_sort import stable_topological_sort @@ -123,37 +125,36 @@ class MigrationAutodetector: # Prepare some old/new state and model lists, separating # proxy models and ignoring unmigrated apps. - self.old_apps = self.from_state.concrete_apps - self.new_apps = self.to_state.apps self.old_model_keys = set() self.old_proxy_keys = set() self.old_unmanaged_keys = set() self.new_model_keys = set() self.new_proxy_keys = set() self.new_unmanaged_keys = set() - for app_label, model_name in self.from_state.models: - model = self.old_apps.get_model(app_label, model_name) - if not model._meta.managed: + for (app_label, model_name), model_state in self.from_state.models.items(): + if not model_state.options.get('managed', True): self.old_unmanaged_keys.add((app_label, model_name)) elif app_label not in self.from_state.real_apps: - if model._meta.proxy: + if model_state.options.get('proxy'): self.old_proxy_keys.add((app_label, model_name)) else: self.old_model_keys.add((app_label, model_name)) - for app_label, model_name in self.to_state.models: - model = self.new_apps.get_model(app_label, model_name) - if not model._meta.managed: + for (app_label, model_name), model_state in self.to_state.models.items(): + if not model_state.options.get('managed', True): self.new_unmanaged_keys.add((app_label, model_name)) elif ( app_label not in self.from_state.real_apps or (convert_apps and app_label in convert_apps) ): - if model._meta.proxy: + if model_state.options.get('proxy'): self.new_proxy_keys.add((app_label, model_name)) else: self.new_model_keys.add((app_label, model_name)) + self.from_state.resolve_fields_and_relations() + self.to_state.resolve_fields_and_relations() + # Renames have to come first self.generate_renamed_models() @@ -224,14 +225,9 @@ class MigrationAutodetector: for app_label, model_name in sorted(self.old_model_keys): old_model_name = self.renamed_models.get((app_label, model_name), model_name) old_model_state = self.from_state.models[app_label, old_model_name] - for field_name in old_model_state.fields: - old_field = self.old_apps.get_model(app_label, old_model_name)._meta.get_field(field_name) - if (hasattr(old_field, "remote_field") and getattr(old_field.remote_field, "through", None) and - not old_field.remote_field.through._meta.auto_created): - through_key = ( - old_field.remote_field.through._meta.app_label, - old_field.remote_field.through._meta.model_name, - ) + for field_name, field in old_model_state.fields.items(): + if hasattr(field, 'remote_field') and getattr(field.remote_field, 'through', None): + through_key = resolve_relation(field.remote_field.through, app_label, model_name) self.through_users[through_key] = (app_label, old_model_name, field_name) @staticmethod @@ -446,11 +442,14 @@ class MigrationAutodetector: real way to solve #22783). """ try: - model = self.new_apps.get_model(item[0], item[1]) - base_names = [base.__name__ for base in model.__bases__] + model_state = self.to_state.models[item] + base_names = { + base if isinstance(base, str) else base.__name__ + for base in model_state.bases + } string_version = "%s.%s" % (item[0], item[1]) if ( - model._meta.swappable or + model_state.options.get('swappable') or "AbstractUser" in base_names or "AbstractBaseUser" in base_names or settings.AUTH_USER_MODEL.lower() == string_version.lower() @@ -480,11 +479,19 @@ class MigrationAutodetector: rem_model_fields_def = self.only_relation_agnostic_fields(rem_model_state.fields) if model_fields_def == rem_model_fields_def: if self.questioner.ask_rename_model(rem_model_state, model_state): - model_opts = self.new_apps.get_model(app_label, model_name)._meta dependencies = [] - for field in model_opts.get_fields(): + fields = list(model_state.fields.values()) + [ + field.remote_field + for relations in self.to_state.relations[app_label, model_name].values() + for _, field in relations + ] + for field in fields: if field.is_relation: - dependencies.extend(self._get_dependencies_for_foreign_key(field)) + dependencies.extend( + self._get_dependencies_for_foreign_key( + app_label, model_name, field, self.to_state, + ) + ) self.add_operation( app_label, operations.RenameModel( @@ -525,27 +532,19 @@ class MigrationAutodetector: ) for app_label, model_name in all_added_models: model_state = self.to_state.models[app_label, model_name] - model_opts = self.new_apps.get_model(app_label, model_name)._meta # Gather related fields related_fields = {} primary_key_rel = None - for field in model_opts.local_fields: + for field_name, field in model_state.fields.items(): if field.remote_field: if field.remote_field.model: if field.primary_key: primary_key_rel = field.remote_field.model elif not field.remote_field.parent_link: - related_fields[field.name] = field - # through will be none on M2Ms on swapped-out models; - # we can treat lack of through as auto_created=True, though. - if (getattr(field.remote_field, "through", None) and - not field.remote_field.through._meta.auto_created): - related_fields[field.name] = field - for field in model_opts.local_many_to_many: - if field.remote_field.model: - related_fields[field.name] = field - if getattr(field.remote_field, "through", None) and not field.remote_field.through._meta.auto_created: - related_fields[field.name] = field + related_fields[field_name] = field + if getattr(field.remote_field, 'through', None): + related_fields[field_name] = field + # Are there indexes/unique|index_together to defer? indexes = model_state.options.pop('indexes') constraints = model_state.options.pop('constraints') @@ -573,12 +572,11 @@ class MigrationAutodetector: dependencies.append((base_app_label, base_name, removed_base_field, False)) # Depend on the other end of the primary key if it's a relation if primary_key_rel: - dependencies.append(( - primary_key_rel._meta.app_label, - primary_key_rel._meta.object_name, - None, - True - )) + dependencies.append( + resolve_relation( + primary_key_rel, app_label, model_name, + ) + (None, True) + ) # Generate creation operation self.add_operation( app_label, @@ -594,12 +592,14 @@ class MigrationAutodetector: ) # Don't add operations which modify the database for unmanaged models - if not model_opts.managed: + if not model_state.options.get('managed', True): continue # Generate operations for each related field for name, field in sorted(related_fields.items()): - dependencies = self._get_dependencies_for_foreign_key(field) + dependencies = self._get_dependencies_for_foreign_key( + app_label, model_name, field, self.to_state, + ) # Depend on our own model being created dependencies.append((app_label, model_name, None, True)) # Make operation @@ -668,17 +668,20 @@ class MigrationAutodetector: ) # Fix relationships if the model changed from a proxy model to a # concrete model. + relations = self.to_state.relations if (app_label, model_name) in self.old_proxy_keys: - for related_object in model_opts.related_objects: - self.add_operation( - related_object.related_model._meta.app_label, - operations.AlterField( - model_name=related_object.related_model._meta.object_name, - name=related_object.field.name, - field=related_object.field, - ), - dependencies=[(app_label, model_name, None, True)], - ) + for related_model_key, related_fields in relations[app_label, model_name].items(): + related_model_state = self.to_state.models[related_model_key] + for related_field_name, related_field in related_fields: + self.add_operation( + related_model_state.app_label, + operations.AlterField( + model_name=related_model_state.name, + name=related_field_name, + field=related_field, + ), + dependencies=[(app_label, model_name, None, True)], + ) def generate_created_proxies(self): """ @@ -729,23 +732,14 @@ class MigrationAutodetector: all_deleted_models = chain(sorted(deleted_models), sorted(deleted_unmanaged_models)) for app_label, model_name in all_deleted_models: model_state = self.from_state.models[app_label, model_name] - model = self.old_apps.get_model(app_label, model_name) # Gather related fields related_fields = {} - for field in model._meta.local_fields: + for field_name, field in model_state.fields.items(): if field.remote_field: if field.remote_field.model: - related_fields[field.name] = field - # through will be none on M2Ms on swapped-out models; - # we can treat lack of through as auto_created=True, though. - if (getattr(field.remote_field, "through", None) and - not field.remote_field.through._meta.auto_created): - related_fields[field.name] = field - for field in model._meta.local_many_to_many: - if field.remote_field.model: - related_fields[field.name] = field - if getattr(field.remote_field, "through", None) and not field.remote_field.through._meta.auto_created: - related_fields[field.name] = field + related_fields[field_name] = field + if getattr(field.remote_field, 'through', None): + related_fields[field_name] = field # Generate option removal first unique_together = model_state.options.pop('unique_together', None) index_together = model_state.options.pop('index_together', None) @@ -779,13 +773,18 @@ class MigrationAutodetector: # and the removal of all its own related fields, and if it's # a through model the field that references it. dependencies = [] - for related_object in model._meta.related_objects: - related_object_app_label = related_object.related_model._meta.app_label - object_name = related_object.related_model._meta.object_name - field_name = related_object.field.name - dependencies.append((related_object_app_label, object_name, field_name, False)) - if not related_object.many_to_many: - dependencies.append((related_object_app_label, object_name, field_name, "alter")) + relations = self.from_state.relations + for (related_object_app_label, object_name), relation_related_fields in ( + relations[app_label, model_name].items() + ): + for field_name, field in relation_related_fields: + dependencies.append( + (related_object_app_label, object_name, field_name, False), + ) + if not field.many_to_many: + dependencies.append( + (related_object_app_label, object_name, field_name, 'alter'), + ) for name in sorted(related_fields): dependencies.append((app_label, model_name, name, False)) @@ -821,12 +820,13 @@ class MigrationAutodetector: for app_label, model_name, field_name in sorted(self.new_field_keys - self.old_field_keys): old_model_name = self.renamed_models.get((app_label, model_name), model_name) old_model_state = self.from_state.models[app_label, old_model_name] - field = self.new_apps.get_model(app_label, model_name)._meta.get_field(field_name) + new_model_state = self.to_state.models[app_label, old_model_name] + field = new_model_state.get_field(field_name) # Scan to see if this is actually a rename! field_dec = self.deep_deconstruct(field) for rem_app_label, rem_model_name, rem_field_name in sorted(self.old_field_keys - self.new_field_keys): if rem_app_label == app_label and rem_model_name == model_name: - old_field = old_model_state.fields[rem_field_name] + old_field = old_model_state.get_field(rem_field_name) old_field_dec = self.deep_deconstruct(old_field) if field.remote_field and field.remote_field.model and 'to' in old_field_dec[2]: old_rel_to = old_field_dec[2]['to'] @@ -859,11 +859,13 @@ class MigrationAutodetector: self._generate_added_field(app_label, model_name, field_name) def _generate_added_field(self, app_label, model_name, field_name): - field = self.new_apps.get_model(app_label, model_name)._meta.get_field(field_name) + field = self.to_state.models[app_label, model_name].get_field(field_name) # Fields that are foreignkeys/m2ms depend on stuff dependencies = [] if field.remote_field and field.remote_field.model: - dependencies.extend(self._get_dependencies_for_foreign_key(field)) + dependencies.extend(self._get_dependencies_for_foreign_key( + app_label, model_name, field, self.to_state, + )) # You can't just add NOT NULL fields with no default or fields # which don't allow empty strings as default. time_fields = (models.DateField, models.DateTimeField, models.TimeField) @@ -919,16 +921,13 @@ class MigrationAutodetector: # Did the field change? old_model_name = self.renamed_models.get((app_label, model_name), model_name) old_field_name = self.renamed_fields.get((app_label, model_name, field_name), field_name) - old_field = self.old_apps.get_model(app_label, old_model_name)._meta.get_field(old_field_name) - new_field = self.new_apps.get_model(app_label, model_name)._meta.get_field(field_name) + old_field = self.from_state.models[app_label, old_model_name].get_field(old_field_name) + new_field = self.to_state.models[app_label, model_name].get_field(field_name) dependencies = [] # Implement any model renames on relations; these are handled by RenameModel # so we need to exclude them from the comparison if hasattr(new_field, "remote_field") and getattr(new_field.remote_field, "model", None): - rename_key = ( - new_field.remote_field.model._meta.app_label, - new_field.remote_field.model._meta.model_name, - ) + rename_key = resolve_relation(new_field.remote_field.model, app_label, model_name) if rename_key in self.renamed_models: new_field.remote_field.model = old_field.remote_field.model # Handle ForeignKey which can only have a single to_field. @@ -953,12 +952,14 @@ class MigrationAutodetector: self.renamed_fields.get(rename_key + (to_field,), to_field) for to_field in new_field.to_fields ]) - dependencies.extend(self._get_dependencies_for_foreign_key(new_field)) - if hasattr(new_field, "remote_field") and getattr(new_field.remote_field, "through", None): - rename_key = ( - new_field.remote_field.through._meta.app_label, - new_field.remote_field.through._meta.model_name, - ) + dependencies.extend(self._get_dependencies_for_foreign_key( + app_label, model_name, new_field, self.to_state, + )) + if ( + hasattr(new_field, 'remote_field') and + getattr(new_field.remote_field, 'through', None) + ): + rename_key = resolve_relation(new_field.remote_field.through, app_label, model_name) if rename_key in self.renamed_models: new_field.remote_field.through = old_field.remote_field.through old_field_dec = self.deep_deconstruct(old_field) @@ -1073,23 +1074,32 @@ class MigrationAutodetector: ) ) - def _get_dependencies_for_foreign_key(self, field): + @staticmethod + def _get_dependencies_for_foreign_key(app_label, model_name, field, project_state): + remote_field_model = None + if hasattr(field.remote_field, 'model'): + remote_field_model = field.remote_field.model + else: + relations = project_state.relations[app_label, model_name] + for (remote_app_label, remote_model_name), fields in relations.items(): + if any(field == related_field.remote_field for _, related_field in fields): + remote_field_model = f'{remote_app_label}.{remote_model_name}' + break # Account for FKs to swappable models swappable_setting = getattr(field, 'swappable_setting', None) if swappable_setting is not None: dep_app_label = "__setting__" dep_object_name = swappable_setting else: - dep_app_label = field.remote_field.model._meta.app_label - dep_object_name = field.remote_field.model._meta.object_name + dep_app_label, dep_object_name = resolve_relation( + remote_field_model, app_label, model_name, + ) dependencies = [(dep_app_label, dep_object_name, None, True)] - if getattr(field.remote_field, "through", None) and not field.remote_field.through._meta.auto_created: - dependencies.append(( - field.remote_field.through._meta.app_label, - field.remote_field.through._meta.object_name, - None, - True, - )) + if getattr(field.remote_field, 'through', None): + through_app_label, through_object_name = resolve_relation( + remote_field_model, app_label, model_name, + ) + dependencies.append((through_app_label, through_object_name, None, True)) return dependencies def _generate_altered_foo_together(self, operation): @@ -1116,9 +1126,11 @@ class MigrationAutodetector: dependencies = [] for foo_togethers in new_value: for field_name in foo_togethers: - field = self.new_apps.get_model(app_label, model_name)._meta.get_field(field_name) + field = new_model_state.get_field(field_name) if field.remote_field and field.remote_field.model: - dependencies.extend(self._get_dependencies_for_foreign_key(field)) + dependencies.extend(self._get_dependencies_for_foreign_key( + app_label, model_name, field, self.to_state, + )) self.add_operation( app_label, diff --git a/django/db/migrations/state.py b/django/db/migrations/state.py index 796687853d..38ec72dd72 100644 --- a/django/db/migrations/state.py +++ b/django/db/migrations/state.py @@ -1,5 +1,7 @@ import copy +from collections import defaultdict from contextlib import contextmanager +from functools import partial from django.apps import AppConfig from django.apps.registry import Apps, apps as global_apps @@ -13,6 +15,7 @@ from django.utils.module_loading import import_string from django.utils.version import get_docs_version from .exceptions import InvalidBasesError +from .utils import resolve_relation def _get_app_label_and_model_name(model, app_label=''): @@ -87,6 +90,8 @@ class ProjectState: # Apps to include from main registry, usually unmigrated ones self.real_apps = real_apps or [] self.is_delayed = False + # {remote_model_key: {model_key: [(field_name, field)]}} + self.relations = None def add_model(self, model_state): app_label, model_name = model_state.app_label, model_state.name_lower @@ -188,6 +193,67 @@ class ProjectState: # Render all models self.apps.render_multiple(states_to_be_rendered) + def resolve_fields_and_relations(self): + # Resolve fields. + for model_state in self.models.values(): + for field_name, field in model_state.fields.items(): + field.name = field_name + # Resolve relations. + # {remote_model_key: {model_key: [(field_name, field)]}} + self.relations = defaultdict(partial(defaultdict, list)) + concretes, proxies = self._get_concrete_models_mapping_and_proxy_models() + + real_apps = set(self.real_apps) + for model_key in concretes: + model_state = self.models[model_key] + for field_name, field in model_state.fields.items(): + remote_field = field.remote_field + if not remote_field: + continue + remote_model_key = resolve_relation(remote_field.model, *model_key) + if remote_model_key[0] not in real_apps and remote_model_key in concretes: + remote_model_key = concretes[remote_model_key] + self.relations[remote_model_key][model_key].append((field_name, field)) + + through = getattr(remote_field, 'through', None) + if not through: + continue + through_model_key = resolve_relation(through, *model_key) + if through_model_key[0] not in real_apps and through_model_key in concretes: + through_model_key = concretes[through_model_key] + self.relations[through_model_key][model_key].append((field_name, field)) + for model_key in proxies: + self.relations[model_key] = self.relations[concretes[model_key]] + + def get_concrete_model_key(self, model): + concrete_models_mapping, _ = self._get_concrete_models_mapping_and_proxy_models() + model_key = make_model_tuple(model) + return concrete_models_mapping[model_key] + + def _get_concrete_models_mapping_and_proxy_models(self): + concrete_models_mapping = {} + proxy_models = {} + # Split models to proxy and concrete models. + for model_key, model_state in self.models.items(): + if model_state.options.get('proxy'): + proxy_models[model_key] = model_state + # Find a concrete model for the proxy. + concrete_models_mapping[model_key] = self._find_concrete_model_from_proxy( + proxy_models, model_state, + ) + else: + concrete_models_mapping[model_key] = model_key + return concrete_models_mapping, proxy_models + + def _find_concrete_model_from_proxy(self, proxy_models, model_state): + for base in model_state.bases: + base_key = make_model_tuple(base) + base_state = proxy_models.get(base_key) + if not base_state: + # Concrete model found, stop looking at bases. + return base_key + return self._find_concrete_model_from_proxy(proxy_models, base_state) + def clone(self): """Return an exact copy of this ProjectState.""" new_state = ProjectState( @@ -207,11 +273,6 @@ class ProjectState: def apps(self): return StateApps(self.real_apps, self.models) - @property - def concrete_apps(self): - self.apps = StateApps(self.real_apps, self.models, ignore_swappable=True) - return self.apps - @classmethod def from_apps(cls, apps): """Take an Apps and return a ProjectState matching it.""" @@ -392,6 +453,14 @@ class ModelState: def name_lower(self): return self.name.lower() + def get_field(self, field_name): + field_name = ( + self.options['order_with_respect_to'] + if field_name == '_order' + else field_name + ) + return self.fields[field_name] + @classmethod def from_model(cls, model, exclude_rels=False): """Given a model, return a ModelState representing it.""" diff --git a/tests/migrations/test_autodetector.py b/tests/migrations/test_autodetector.py index 707b84dab3..5b12d3da58 100644 --- a/tests/migrations/test_autodetector.py +++ b/tests/migrations/test_autodetector.py @@ -584,9 +584,13 @@ class AutodetectorTests(TestCase): return project_state def get_changes(self, before_states, after_states, questioner=None): + if not isinstance(before_states, ProjectState): + before_states = self.make_project_state(before_states) + if not isinstance(after_states, ProjectState): + after_states = self.make_project_state(after_states) return MigrationAutodetector( - self.make_project_state(before_states), - self.make_project_state(after_states), + before_states, + after_states, questioner, )._detect_changes() @@ -1646,30 +1650,38 @@ class AutodetectorTests(TestCase): """ # First, we test the default pk field name changes = self.get_changes([], [self.author_empty, self.author_proxy_third, self.book_proxy_fk]) - # The field name the FK on the book model points to - self.assertEqual(changes['otherapp'][0].operations[0].fields[2][1].remote_field.field_name, 'id') + # The model the FK is pointing from and to. + self.assertEqual( + changes['otherapp'][0].operations[0].fields[2][1].remote_field.model, + 'thirdapp.AuthorProxy', + ) # Now, we test the custom pk field name changes = self.get_changes([], [self.author_custom_pk, self.author_proxy_third, self.book_proxy_fk]) - # The field name the FK on the book model points to - self.assertEqual(changes['otherapp'][0].operations[0].fields[2][1].remote_field.field_name, 'pk_field') + # The model the FK is pointing from and to. + self.assertEqual( + changes['otherapp'][0].operations[0].fields[2][1].remote_field.model, + 'thirdapp.AuthorProxy', + ) def test_proxy_to_mti_with_fk_to_proxy(self): # First, test the pk table and field name. - changes = self.get_changes( - [], + to_state = self.make_project_state( [self.author_empty, self.author_proxy_third, self.book_proxy_fk], ) + changes = self.get_changes([], to_state) + fk_field = changes['otherapp'][0].operations[0].fields[2][1] self.assertEqual( - changes['otherapp'][0].operations[0].fields[2][1].remote_field.model._meta.db_table, - 'testapp_author', + to_state.get_concrete_model_key(fk_field.remote_field.model), + ('testapp', 'author'), ) - self.assertEqual(changes['otherapp'][0].operations[0].fields[2][1].remote_field.field_name, 'id') + self.assertEqual(fk_field.remote_field.model, 'thirdapp.AuthorProxy') # Change AuthorProxy to use MTI. - changes = self.get_changes( - [self.author_empty, self.author_proxy_third, self.book_proxy_fk], + from_state = to_state.clone() + to_state = self.make_project_state( [self.author_empty, self.author_proxy_third_notproxy, self.book_proxy_fk], ) + changes = self.get_changes(from_state, to_state) # Right number/type of migrations for the AuthorProxy model? self.assertNumberMigrations(changes, 'thirdapp', 1) self.assertOperationTypes(changes, 'thirdapp', 0, ['DeleteModel', 'CreateModel']) @@ -1680,30 +1692,39 @@ class AutodetectorTests(TestCase): # otherapp should depend on thirdapp. self.assertMigrationDependencies(changes, 'otherapp', 0, [('thirdapp', 'auto_1')]) # Now, test the pk table and field name. + fk_field = changes['otherapp'][0].operations[0].field self.assertEqual( - changes['otherapp'][0].operations[0].field.remote_field.model._meta.db_table, - 'thirdapp_authorproxy', + to_state.get_concrete_model_key(fk_field.remote_field.model), + ('thirdapp', 'authorproxy'), ) - self.assertEqual(changes['otherapp'][0].operations[0].field.remote_field.field_name, 'author_ptr') + self.assertEqual(fk_field.remote_field.model, 'thirdapp.AuthorProxy') def test_proxy_to_mti_with_fk_to_proxy_proxy(self): # First, test the pk table and field name. - changes = self.get_changes( - [], - [self.author_empty, self.author_proxy, self.author_proxy_proxy, self.book_proxy_proxy_fk], - ) + to_state = self.make_project_state([ + self.author_empty, + self.author_proxy, + self.author_proxy_proxy, + self.book_proxy_proxy_fk, + ]) + changes = self.get_changes([], to_state) + fk_field = changes['otherapp'][0].operations[0].fields[1][1] self.assertEqual( - changes['otherapp'][0].operations[0].fields[1][1].remote_field.model._meta.db_table, - 'testapp_author', + to_state.get_concrete_model_key(fk_field.remote_field.model), + ('testapp', 'author'), ) - self.assertEqual(changes['otherapp'][0].operations[0].fields[1][1].remote_field.field_name, 'id') + self.assertEqual(fk_field.remote_field.model, 'testapp.AAuthorProxyProxy') # Change AuthorProxy to use MTI. FK still points to AAuthorProxyProxy, # a proxy of AuthorProxy. - changes = self.get_changes( - [self.author_empty, self.author_proxy, self.author_proxy_proxy, self.book_proxy_proxy_fk], - [self.author_empty, self.author_proxy_notproxy, self.author_proxy_proxy, self.book_proxy_proxy_fk], - ) + from_state = to_state.clone() + to_state = self.make_project_state([ + self.author_empty, + self.author_proxy_notproxy, + self.author_proxy_proxy, + self.book_proxy_proxy_fk, + ]) + changes = self.get_changes(from_state, to_state) # Right number/type of migrations for the AuthorProxy model? self.assertNumberMigrations(changes, 'testapp', 1) self.assertOperationTypes(changes, 'testapp', 0, ['DeleteModel', 'CreateModel']) @@ -1714,11 +1735,12 @@ class AutodetectorTests(TestCase): # otherapp should depend on testapp. self.assertMigrationDependencies(changes, 'otherapp', 0, [('testapp', 'auto_1')]) # Now, test the pk table and field name. + fk_field = changes['otherapp'][0].operations[0].field self.assertEqual( - changes['otherapp'][0].operations[0].field.remote_field.model._meta.db_table, - 'testapp_authorproxy', + to_state.get_concrete_model_key(fk_field.remote_field.model), + ('testapp', 'authorproxy'), ) - self.assertEqual(changes['otherapp'][0].operations[0].field.remote_field.field_name, 'author_ptr') + self.assertEqual(fk_field.remote_field.model, 'testapp.AAuthorProxyProxy') def test_unmanaged_create(self): """The autodetector correctly deals with managed models.""" @@ -1761,12 +1783,14 @@ class AutodetectorTests(TestCase): """ # First, we test the default pk field name changes = self.get_changes([], [self.author_unmanaged_default_pk, self.book]) - # The field name the FK on the book model points to - self.assertEqual(changes['otherapp'][0].operations[0].fields[2][1].remote_field.field_name, 'id') + # The model the FK on the book model points to. + fk_field = changes['otherapp'][0].operations[0].fields[2][1] + self.assertEqual(fk_field.remote_field.model, 'testapp.Author') # Now, we test the custom pk field name changes = self.get_changes([], [self.author_unmanaged_custom_pk, self.book]) - # The field name the FK on the book model points to - self.assertEqual(changes['otherapp'][0].operations[0].fields[2][1].remote_field.field_name, 'pk_field') + # The model the FK on the book model points to. + fk_field = changes['otherapp'][0].operations[0].fields[2][1] + self.assertEqual(fk_field.remote_field.model, 'testapp.Author') @override_settings(AUTH_USER_MODEL="thirdapp.CustomUser") def test_swappable(self): @@ -1790,11 +1814,7 @@ class AutodetectorTests(TestCase): self.assertOperationTypes(changes, 'testapp', 0, ["AlterField"]) self.assertOperationAttributes(changes, 'testapp', 0, 0, model_name="author", name='user') fk_field = changes['testapp'][0].operations[0].field - to_model = '%s.%s' % ( - fk_field.remote_field.model._meta.app_label, - fk_field.remote_field.model._meta.object_name, - ) - self.assertEqual(to_model, 'thirdapp.CustomUser') + self.assertEqual(fk_field.remote_field.model, 'thirdapp.CustomUser') def test_add_field_with_default(self): """#22030 - Adding a field with a default should work."""