summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDavid Wobrock <david.wobrock@gmail.com>2020-04-25 17:20:26 +0200
committerMariusz Felisiak <felisiak.mariusz@gmail.com>2021-04-16 11:18:10 +0200
commitaa4acc164d1247c0de515c959f7b09648b57dc42 (patch)
tree08f10963024e6b239467dc0de6461da665d76abc
parenta67849499a7aac6a18f8ce0e15f17914765a319c (diff)
Fixed #29899 -- Made autodetector use model states instead of model classes.
Thanks Simon Charette and Markus Holtermann for reviews.
-rw-r--r--django/db/migrations/autodetector.py220
-rw-r--r--django/db/migrations/state.py79
-rw-r--r--tests/migrations/test_autodetector.py98
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."""