mirror of
https://github.com/django/django.git
synced 2025-03-06 15:32:33 +00:00
Once the deprecation period ends CheckConstraint.check() can become the documented method that performs system checks for BaseConstraint subclasses.
2224 lines
78 KiB
Python
2224 lines
78 KiB
Python
from django.apps.registry import Apps
|
|
from django.contrib.contenttypes.fields import GenericForeignKey
|
|
from django.db import models
|
|
from django.db.migrations.exceptions import InvalidBasesError
|
|
from django.db.migrations.operations import (
|
|
AddField,
|
|
AlterField,
|
|
DeleteModel,
|
|
RemoveField,
|
|
)
|
|
from django.db.migrations.state import (
|
|
ModelState,
|
|
ProjectState,
|
|
get_related_models_recursive,
|
|
)
|
|
from django.test import SimpleTestCase, override_settings
|
|
from django.test.utils import isolate_apps
|
|
|
|
from .models import (
|
|
FoodManager,
|
|
FoodQuerySet,
|
|
ModelWithCustomBase,
|
|
NoMigrationFoodManager,
|
|
UnicodeModel,
|
|
)
|
|
|
|
|
|
class StateTests(SimpleTestCase):
|
|
"""
|
|
Tests state construction, rendering and modification by operations.
|
|
"""
|
|
|
|
def test_create(self):
|
|
"""
|
|
Tests making a ProjectState from an Apps
|
|
"""
|
|
|
|
new_apps = Apps(["migrations"])
|
|
|
|
class Author(models.Model):
|
|
name = models.CharField(max_length=255)
|
|
bio = models.TextField()
|
|
age = models.IntegerField(blank=True, null=True)
|
|
|
|
class Meta:
|
|
app_label = "migrations"
|
|
apps = new_apps
|
|
unique_together = ["name", "bio"]
|
|
|
|
class AuthorProxy(Author):
|
|
class Meta:
|
|
app_label = "migrations"
|
|
apps = new_apps
|
|
proxy = True
|
|
ordering = ["name"]
|
|
|
|
class SubAuthor(Author):
|
|
width = models.FloatField(null=True)
|
|
|
|
class Meta:
|
|
app_label = "migrations"
|
|
apps = new_apps
|
|
|
|
class Book(models.Model):
|
|
title = models.CharField(max_length=1000)
|
|
author = models.ForeignKey(Author, models.CASCADE)
|
|
contributors = models.ManyToManyField(Author)
|
|
|
|
class Meta:
|
|
app_label = "migrations"
|
|
apps = new_apps
|
|
verbose_name = "tome"
|
|
db_table = "test_tome"
|
|
indexes = [models.Index(fields=["title"])]
|
|
|
|
class Food(models.Model):
|
|
food_mgr = FoodManager("a", "b")
|
|
food_qs = FoodQuerySet.as_manager()
|
|
food_no_mgr = NoMigrationFoodManager("x", "y")
|
|
|
|
class Meta:
|
|
app_label = "migrations"
|
|
apps = new_apps
|
|
|
|
class FoodNoManagers(models.Model):
|
|
class Meta:
|
|
app_label = "migrations"
|
|
apps = new_apps
|
|
|
|
class FoodNoDefaultManager(models.Model):
|
|
food_no_mgr = NoMigrationFoodManager("x", "y")
|
|
food_mgr = FoodManager("a", "b")
|
|
food_qs = FoodQuerySet.as_manager()
|
|
|
|
class Meta:
|
|
app_label = "migrations"
|
|
apps = new_apps
|
|
|
|
mgr1 = FoodManager("a", "b")
|
|
mgr2 = FoodManager("x", "y", c=3, d=4)
|
|
|
|
class FoodOrderedManagers(models.Model):
|
|
# The managers on this model should be ordered by their creation
|
|
# counter and not by the order in model body
|
|
|
|
food_no_mgr = NoMigrationFoodManager("x", "y")
|
|
food_mgr2 = mgr2
|
|
food_mgr1 = mgr1
|
|
|
|
class Meta:
|
|
app_label = "migrations"
|
|
apps = new_apps
|
|
|
|
project_state = ProjectState.from_apps(new_apps)
|
|
author_state = project_state.models["migrations", "author"]
|
|
author_proxy_state = project_state.models["migrations", "authorproxy"]
|
|
sub_author_state = project_state.models["migrations", "subauthor"]
|
|
book_state = project_state.models["migrations", "book"]
|
|
food_state = project_state.models["migrations", "food"]
|
|
food_no_managers_state = project_state.models["migrations", "foodnomanagers"]
|
|
food_no_default_manager_state = project_state.models[
|
|
"migrations", "foodnodefaultmanager"
|
|
]
|
|
food_order_manager_state = project_state.models[
|
|
"migrations", "foodorderedmanagers"
|
|
]
|
|
book_index = models.Index(fields=["title"])
|
|
book_index.set_name_with_model(Book)
|
|
|
|
self.assertEqual(author_state.app_label, "migrations")
|
|
self.assertEqual(author_state.name, "Author")
|
|
self.assertEqual(list(author_state.fields), ["id", "name", "bio", "age"])
|
|
self.assertEqual(author_state.fields["name"].max_length, 255)
|
|
self.assertIs(author_state.fields["bio"].null, False)
|
|
self.assertIs(author_state.fields["age"].null, True)
|
|
self.assertEqual(
|
|
author_state.options,
|
|
{
|
|
"unique_together": {("name", "bio")},
|
|
"indexes": [],
|
|
"constraints": [],
|
|
},
|
|
)
|
|
self.assertEqual(author_state.bases, (models.Model,))
|
|
|
|
self.assertEqual(book_state.app_label, "migrations")
|
|
self.assertEqual(book_state.name, "Book")
|
|
self.assertEqual(
|
|
list(book_state.fields), ["id", "title", "author", "contributors"]
|
|
)
|
|
self.assertEqual(book_state.fields["title"].max_length, 1000)
|
|
self.assertIs(book_state.fields["author"].null, False)
|
|
self.assertEqual(
|
|
book_state.fields["contributors"].__class__.__name__, "ManyToManyField"
|
|
)
|
|
self.assertEqual(
|
|
book_state.options,
|
|
{
|
|
"verbose_name": "tome",
|
|
"db_table": "test_tome",
|
|
"indexes": [book_index],
|
|
"constraints": [],
|
|
},
|
|
)
|
|
self.assertEqual(book_state.bases, (models.Model,))
|
|
|
|
self.assertEqual(author_proxy_state.app_label, "migrations")
|
|
self.assertEqual(author_proxy_state.name, "AuthorProxy")
|
|
self.assertEqual(author_proxy_state.fields, {})
|
|
self.assertEqual(
|
|
author_proxy_state.options,
|
|
{"proxy": True, "ordering": ["name"], "indexes": [], "constraints": []},
|
|
)
|
|
self.assertEqual(author_proxy_state.bases, ("migrations.author",))
|
|
|
|
self.assertEqual(sub_author_state.app_label, "migrations")
|
|
self.assertEqual(sub_author_state.name, "SubAuthor")
|
|
self.assertEqual(len(sub_author_state.fields), 2)
|
|
self.assertEqual(sub_author_state.bases, ("migrations.author",))
|
|
|
|
# The default manager is used in migrations
|
|
self.assertEqual([name for name, mgr in food_state.managers], ["food_mgr"])
|
|
self.assertTrue(all(isinstance(name, str) for name, mgr in food_state.managers))
|
|
self.assertEqual(food_state.managers[0][1].args, ("a", "b", 1, 2))
|
|
|
|
# No explicit managers defined. Migrations will fall back to the default
|
|
self.assertEqual(food_no_managers_state.managers, [])
|
|
|
|
# food_mgr is used in migration but isn't the default mgr, hence add the
|
|
# default
|
|
self.assertEqual(
|
|
[name for name, mgr in food_no_default_manager_state.managers],
|
|
["food_no_mgr", "food_mgr"],
|
|
)
|
|
self.assertTrue(
|
|
all(
|
|
isinstance(name, str)
|
|
for name, mgr in food_no_default_manager_state.managers
|
|
)
|
|
)
|
|
self.assertEqual(
|
|
food_no_default_manager_state.managers[0][1].__class__, models.Manager
|
|
)
|
|
self.assertIsInstance(food_no_default_manager_state.managers[1][1], FoodManager)
|
|
|
|
self.assertEqual(
|
|
[name for name, mgr in food_order_manager_state.managers],
|
|
["food_mgr1", "food_mgr2"],
|
|
)
|
|
self.assertTrue(
|
|
all(
|
|
isinstance(name, str) for name, mgr in food_order_manager_state.managers
|
|
)
|
|
)
|
|
self.assertEqual(
|
|
[mgr.args for name, mgr in food_order_manager_state.managers],
|
|
[("a", "b", 1, 2), ("x", "y", 3, 4)],
|
|
)
|
|
|
|
def test_custom_default_manager_added_to_the_model_state(self):
|
|
"""
|
|
When the default manager of the model is a custom manager,
|
|
it needs to be added to the model state.
|
|
"""
|
|
new_apps = Apps(["migrations"])
|
|
custom_manager = models.Manager()
|
|
|
|
class Author(models.Model):
|
|
objects = models.TextField()
|
|
authors = custom_manager
|
|
|
|
class Meta:
|
|
app_label = "migrations"
|
|
apps = new_apps
|
|
|
|
project_state = ProjectState.from_apps(new_apps)
|
|
author_state = project_state.models["migrations", "author"]
|
|
self.assertEqual(author_state.managers, [("authors", custom_manager)])
|
|
|
|
def test_custom_default_manager_named_objects_with_false_migration_flag(self):
|
|
"""
|
|
When a manager is added with a name of 'objects' but it does not
|
|
have `use_in_migrations = True`, no migration should be added to the
|
|
model state (#26643).
|
|
"""
|
|
new_apps = Apps(["migrations"])
|
|
|
|
class Author(models.Model):
|
|
objects = models.Manager()
|
|
|
|
class Meta:
|
|
app_label = "migrations"
|
|
apps = new_apps
|
|
|
|
project_state = ProjectState.from_apps(new_apps)
|
|
author_state = project_state.models["migrations", "author"]
|
|
self.assertEqual(author_state.managers, [])
|
|
|
|
def test_no_duplicate_managers(self):
|
|
"""
|
|
When a manager is added with `use_in_migrations = True` and a parent
|
|
model had a manager with the same name and `use_in_migrations = True`,
|
|
the parent's manager shouldn't appear in the model state (#26881).
|
|
"""
|
|
new_apps = Apps(["migrations"])
|
|
|
|
class PersonManager(models.Manager):
|
|
use_in_migrations = True
|
|
|
|
class Person(models.Model):
|
|
objects = PersonManager()
|
|
|
|
class Meta:
|
|
abstract = True
|
|
|
|
class BossManager(PersonManager):
|
|
use_in_migrations = True
|
|
|
|
class Boss(Person):
|
|
objects = BossManager()
|
|
|
|
class Meta:
|
|
app_label = "migrations"
|
|
apps = new_apps
|
|
|
|
project_state = ProjectState.from_apps(new_apps)
|
|
boss_state = project_state.models["migrations", "boss"]
|
|
self.assertEqual(boss_state.managers, [("objects", Boss.objects)])
|
|
|
|
def test_custom_default_manager(self):
|
|
new_apps = Apps(["migrations"])
|
|
|
|
class Author(models.Model):
|
|
manager1 = models.Manager()
|
|
manager2 = models.Manager()
|
|
|
|
class Meta:
|
|
app_label = "migrations"
|
|
apps = new_apps
|
|
default_manager_name = "manager2"
|
|
|
|
project_state = ProjectState.from_apps(new_apps)
|
|
author_state = project_state.models["migrations", "author"]
|
|
self.assertEqual(author_state.options["default_manager_name"], "manager2")
|
|
self.assertEqual(author_state.managers, [("manager2", Author.manager1)])
|
|
|
|
def test_custom_base_manager(self):
|
|
new_apps = Apps(["migrations"])
|
|
|
|
class Author(models.Model):
|
|
manager1 = models.Manager()
|
|
manager2 = models.Manager()
|
|
|
|
class Meta:
|
|
app_label = "migrations"
|
|
apps = new_apps
|
|
base_manager_name = "manager2"
|
|
|
|
class Author2(models.Model):
|
|
manager1 = models.Manager()
|
|
manager2 = models.Manager()
|
|
|
|
class Meta:
|
|
app_label = "migrations"
|
|
apps = new_apps
|
|
base_manager_name = "manager1"
|
|
|
|
project_state = ProjectState.from_apps(new_apps)
|
|
|
|
author_state = project_state.models["migrations", "author"]
|
|
self.assertEqual(author_state.options["base_manager_name"], "manager2")
|
|
self.assertEqual(
|
|
author_state.managers,
|
|
[
|
|
("manager1", Author.manager1),
|
|
("manager2", Author.manager2),
|
|
],
|
|
)
|
|
|
|
author2_state = project_state.models["migrations", "author2"]
|
|
self.assertEqual(author2_state.options["base_manager_name"], "manager1")
|
|
self.assertEqual(
|
|
author2_state.managers,
|
|
[
|
|
("manager1", Author2.manager1),
|
|
],
|
|
)
|
|
|
|
def test_apps_bulk_update(self):
|
|
"""
|
|
StateApps.bulk_update() should update apps.ready to False and reset
|
|
the value afterward.
|
|
"""
|
|
project_state = ProjectState()
|
|
apps = project_state.apps
|
|
with apps.bulk_update():
|
|
self.assertFalse(apps.ready)
|
|
self.assertTrue(apps.ready)
|
|
with self.assertRaises(ValueError):
|
|
with apps.bulk_update():
|
|
self.assertFalse(apps.ready)
|
|
raise ValueError()
|
|
self.assertTrue(apps.ready)
|
|
|
|
def test_render(self):
|
|
"""
|
|
Tests rendering a ProjectState into an Apps.
|
|
"""
|
|
project_state = ProjectState()
|
|
project_state.add_model(
|
|
ModelState(
|
|
app_label="migrations",
|
|
name="Tag",
|
|
fields=[
|
|
("id", models.AutoField(primary_key=True)),
|
|
("name", models.CharField(max_length=100)),
|
|
("hidden", models.BooleanField()),
|
|
],
|
|
)
|
|
)
|
|
project_state.add_model(
|
|
ModelState(
|
|
app_label="migrations",
|
|
name="SubTag",
|
|
fields=[
|
|
(
|
|
"tag_ptr",
|
|
models.OneToOneField(
|
|
"migrations.Tag",
|
|
models.CASCADE,
|
|
auto_created=True,
|
|
parent_link=True,
|
|
primary_key=True,
|
|
to_field="id",
|
|
serialize=False,
|
|
),
|
|
),
|
|
("awesome", models.BooleanField()),
|
|
],
|
|
bases=("migrations.Tag",),
|
|
)
|
|
)
|
|
|
|
base_mgr = models.Manager()
|
|
mgr1 = FoodManager("a", "b")
|
|
mgr2 = FoodManager("x", "y", c=3, d=4)
|
|
project_state.add_model(
|
|
ModelState(
|
|
app_label="migrations",
|
|
name="Food",
|
|
fields=[
|
|
("id", models.AutoField(primary_key=True)),
|
|
],
|
|
managers=[
|
|
# The ordering we really want is objects, mgr1, mgr2
|
|
("default", base_mgr),
|
|
("food_mgr2", mgr2),
|
|
("food_mgr1", mgr1),
|
|
],
|
|
)
|
|
)
|
|
|
|
new_apps = project_state.apps
|
|
self.assertEqual(
|
|
new_apps.get_model("migrations", "Tag")._meta.get_field("name").max_length,
|
|
100,
|
|
)
|
|
self.assertIs(
|
|
new_apps.get_model("migrations", "Tag")._meta.get_field("hidden").null,
|
|
False,
|
|
)
|
|
|
|
self.assertEqual(
|
|
len(new_apps.get_model("migrations", "SubTag")._meta.local_fields), 2
|
|
)
|
|
|
|
Food = new_apps.get_model("migrations", "Food")
|
|
self.assertEqual(
|
|
[mgr.name for mgr in Food._meta.managers],
|
|
["default", "food_mgr1", "food_mgr2"],
|
|
)
|
|
self.assertTrue(all(isinstance(mgr.name, str) for mgr in Food._meta.managers))
|
|
self.assertEqual(
|
|
[mgr.__class__ for mgr in Food._meta.managers],
|
|
[models.Manager, FoodManager, FoodManager],
|
|
)
|
|
|
|
def test_render_model_inheritance(self):
|
|
class Book(models.Model):
|
|
title = models.CharField(max_length=1000)
|
|
|
|
class Meta:
|
|
app_label = "migrations"
|
|
apps = Apps()
|
|
|
|
class Novel(Book):
|
|
class Meta:
|
|
app_label = "migrations"
|
|
apps = Apps()
|
|
|
|
# First, test rendering individually
|
|
apps = Apps(["migrations"])
|
|
|
|
# We shouldn't be able to render yet
|
|
ms = ModelState.from_model(Novel)
|
|
with self.assertRaises(InvalidBasesError):
|
|
ms.render(apps)
|
|
|
|
# Once the parent model is in the app registry, it should be fine
|
|
ModelState.from_model(Book).render(apps)
|
|
ModelState.from_model(Novel).render(apps)
|
|
|
|
def test_render_model_with_multiple_inheritance(self):
|
|
class Foo(models.Model):
|
|
class Meta:
|
|
app_label = "migrations"
|
|
apps = Apps()
|
|
|
|
class Bar(models.Model):
|
|
class Meta:
|
|
app_label = "migrations"
|
|
apps = Apps()
|
|
|
|
class FooBar(Foo, Bar):
|
|
class Meta:
|
|
app_label = "migrations"
|
|
apps = Apps()
|
|
|
|
class AbstractSubFooBar(FooBar):
|
|
class Meta:
|
|
abstract = True
|
|
apps = Apps()
|
|
|
|
class SubFooBar(AbstractSubFooBar):
|
|
class Meta:
|
|
app_label = "migrations"
|
|
apps = Apps()
|
|
|
|
apps = Apps(["migrations"])
|
|
|
|
# We shouldn't be able to render yet
|
|
ms = ModelState.from_model(FooBar)
|
|
with self.assertRaises(InvalidBasesError):
|
|
ms.render(apps)
|
|
|
|
# Once the parent models are in the app registry, it should be fine
|
|
ModelState.from_model(Foo).render(apps)
|
|
self.assertSequenceEqual(ModelState.from_model(Foo).bases, [models.Model])
|
|
ModelState.from_model(Bar).render(apps)
|
|
self.assertSequenceEqual(ModelState.from_model(Bar).bases, [models.Model])
|
|
ModelState.from_model(FooBar).render(apps)
|
|
self.assertSequenceEqual(
|
|
ModelState.from_model(FooBar).bases, ["migrations.foo", "migrations.bar"]
|
|
)
|
|
ModelState.from_model(SubFooBar).render(apps)
|
|
self.assertSequenceEqual(
|
|
ModelState.from_model(SubFooBar).bases, ["migrations.foobar"]
|
|
)
|
|
|
|
def test_render_project_dependencies(self):
|
|
"""
|
|
The ProjectState render method correctly renders models
|
|
to account for inter-model base dependencies.
|
|
"""
|
|
new_apps = Apps()
|
|
|
|
class A(models.Model):
|
|
class Meta:
|
|
app_label = "migrations"
|
|
apps = new_apps
|
|
|
|
class B(A):
|
|
class Meta:
|
|
app_label = "migrations"
|
|
apps = new_apps
|
|
|
|
class C(B):
|
|
class Meta:
|
|
app_label = "migrations"
|
|
apps = new_apps
|
|
|
|
class D(A):
|
|
class Meta:
|
|
app_label = "migrations"
|
|
apps = new_apps
|
|
|
|
class E(B):
|
|
class Meta:
|
|
app_label = "migrations"
|
|
apps = new_apps
|
|
proxy = True
|
|
|
|
class F(D):
|
|
class Meta:
|
|
app_label = "migrations"
|
|
apps = new_apps
|
|
proxy = True
|
|
|
|
# Make a ProjectState and render it
|
|
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.add_model(ModelState.from_model(D))
|
|
project_state.add_model(ModelState.from_model(E))
|
|
project_state.add_model(ModelState.from_model(F))
|
|
final_apps = project_state.apps
|
|
self.assertEqual(len(final_apps.get_models()), 6)
|
|
|
|
# Now make an invalid ProjectState and make sure it fails
|
|
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.add_model(ModelState.from_model(F))
|
|
with self.assertRaises(InvalidBasesError):
|
|
project_state.apps
|
|
|
|
def test_render_unique_app_labels(self):
|
|
"""
|
|
The ProjectState render method doesn't raise an
|
|
ImproperlyConfigured exception about unique labels if two dotted app
|
|
names have the same last part.
|
|
"""
|
|
|
|
class A(models.Model):
|
|
class Meta:
|
|
app_label = "django.contrib.auth"
|
|
|
|
class B(models.Model):
|
|
class Meta:
|
|
app_label = "vendor.auth"
|
|
|
|
# Make a ProjectState and render it
|
|
project_state = ProjectState()
|
|
project_state.add_model(ModelState.from_model(A))
|
|
project_state.add_model(ModelState.from_model(B))
|
|
self.assertEqual(len(project_state.apps.get_models()), 2)
|
|
|
|
def test_reload_related_model_on_non_relational_fields(self):
|
|
"""
|
|
The model is reloaded even on changes that are not involved in
|
|
relations. Other models pointing to or from it are also reloaded.
|
|
"""
|
|
project_state = ProjectState()
|
|
project_state.apps # Render project state.
|
|
project_state.add_model(ModelState("migrations", "A", []))
|
|
project_state.add_model(
|
|
ModelState(
|
|
"migrations",
|
|
"B",
|
|
[
|
|
("a", models.ForeignKey("A", models.CASCADE)),
|
|
],
|
|
)
|
|
)
|
|
project_state.add_model(
|
|
ModelState(
|
|
"migrations",
|
|
"C",
|
|
[
|
|
("b", models.ForeignKey("B", models.CASCADE)),
|
|
("name", models.TextField()),
|
|
],
|
|
)
|
|
)
|
|
project_state.add_model(
|
|
ModelState(
|
|
"migrations",
|
|
"D",
|
|
[
|
|
("a", models.ForeignKey("A", models.CASCADE)),
|
|
],
|
|
)
|
|
)
|
|
operation = AlterField(
|
|
model_name="C",
|
|
name="name",
|
|
field=models.TextField(blank=True),
|
|
)
|
|
operation.state_forwards("migrations", project_state)
|
|
project_state.reload_model("migrations", "a", delay=True)
|
|
A = project_state.apps.get_model("migrations.A")
|
|
B = project_state.apps.get_model("migrations.B")
|
|
D = project_state.apps.get_model("migrations.D")
|
|
self.assertIs(B._meta.get_field("a").related_model, A)
|
|
self.assertIs(D._meta.get_field("a").related_model, A)
|
|
|
|
def test_reload_model_relationship_consistency(self):
|
|
project_state = ProjectState()
|
|
project_state.add_model(ModelState("migrations", "A", []))
|
|
project_state.add_model(
|
|
ModelState(
|
|
"migrations",
|
|
"B",
|
|
[
|
|
("a", models.ForeignKey("A", models.CASCADE)),
|
|
],
|
|
)
|
|
)
|
|
project_state.add_model(
|
|
ModelState(
|
|
"migrations",
|
|
"C",
|
|
[
|
|
("b", models.ForeignKey("B", models.CASCADE)),
|
|
],
|
|
)
|
|
)
|
|
A = project_state.apps.get_model("migrations.A")
|
|
B = project_state.apps.get_model("migrations.B")
|
|
C = project_state.apps.get_model("migrations.C")
|
|
self.assertEqual([r.related_model for r in A._meta.related_objects], [B])
|
|
self.assertEqual([r.related_model for r in B._meta.related_objects], [C])
|
|
self.assertEqual([r.related_model for r in C._meta.related_objects], [])
|
|
|
|
project_state.reload_model("migrations", "a", delay=True)
|
|
A = project_state.apps.get_model("migrations.A")
|
|
B = project_state.apps.get_model("migrations.B")
|
|
C = project_state.apps.get_model("migrations.C")
|
|
self.assertEqual([r.related_model for r in A._meta.related_objects], [B])
|
|
self.assertEqual([r.related_model for r in B._meta.related_objects], [C])
|
|
self.assertEqual([r.related_model for r in C._meta.related_objects], [])
|
|
|
|
def test_add_relations(self):
|
|
"""
|
|
#24573 - Adding relations to existing models should reload the
|
|
referenced models too.
|
|
"""
|
|
new_apps = Apps()
|
|
|
|
class A(models.Model):
|
|
class Meta:
|
|
app_label = "something"
|
|
apps = new_apps
|
|
|
|
class B(A):
|
|
class Meta:
|
|
app_label = "something"
|
|
apps = new_apps
|
|
|
|
class C(models.Model):
|
|
class Meta:
|
|
app_label = "something"
|
|
apps = new_apps
|
|
|
|
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")
|
|
# 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",
|
|
models.CASCADE,
|
|
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")
|
|
|
|
# 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)
|
|
# 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)
|
|
# 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 - Relations between models are updated while
|
|
remaining the relations and references for models of an old state.
|
|
"""
|
|
new_apps = Apps()
|
|
|
|
class A(models.Model):
|
|
class Meta:
|
|
app_label = "something"
|
|
apps = new_apps
|
|
|
|
class B(models.Model):
|
|
to_a = models.ForeignKey(A, models.CASCADE)
|
|
|
|
class Meta:
|
|
app_label = "something"
|
|
apps = new_apps
|
|
|
|
def get_model_a(state):
|
|
return [
|
|
mod for mod in state.apps.get_models() if mod._meta.model_name == "a"
|
|
][0]
|
|
|
|
project_state = ProjectState()
|
|
project_state.add_model(ModelState.from_model(A))
|
|
project_state.add_model(ModelState.from_model(B))
|
|
self.assertEqual(len(get_model_a(project_state)._meta.related_objects), 1)
|
|
old_state = project_state.clone()
|
|
|
|
operation = RemoveField("b", "to_a")
|
|
operation.state_forwards("something", project_state)
|
|
# Model from old_state still has the relation
|
|
model_a_old = get_model_a(old_state)
|
|
model_a_new = get_model_a(project_state)
|
|
self.assertIsNot(model_a_old, model_a_new)
|
|
self.assertEqual(len(model_a_old._meta.related_objects), 1)
|
|
self.assertEqual(len(model_a_new._meta.related_objects), 0)
|
|
|
|
# Same test for deleted model
|
|
project_state = ProjectState()
|
|
project_state.add_model(ModelState.from_model(A))
|
|
project_state.add_model(ModelState.from_model(B))
|
|
old_state = project_state.clone()
|
|
|
|
operation = DeleteModel("b")
|
|
operation.state_forwards("something", project_state)
|
|
model_a_old = get_model_a(old_state)
|
|
model_a_new = get_model_a(project_state)
|
|
self.assertIsNot(model_a_old, model_a_new)
|
|
self.assertEqual(len(model_a_old._meta.related_objects), 1)
|
|
self.assertEqual(len(model_a_new._meta.related_objects), 0)
|
|
|
|
def test_self_relation(self):
|
|
"""
|
|
#24513 - Modifying an object pointing to itself would cause it to be
|
|
rendered twice and thus breaking its related M2M through objects.
|
|
"""
|
|
|
|
class A(models.Model):
|
|
to_a = models.ManyToManyField("something.A", symmetrical=False)
|
|
|
|
class Meta:
|
|
app_label = "something"
|
|
|
|
def get_model_a(state):
|
|
return [
|
|
mod for mod in state.apps.get_models() if mod._meta.model_name == "a"
|
|
][0]
|
|
|
|
project_state = ProjectState()
|
|
project_state.add_model(ModelState.from_model(A))
|
|
self.assertEqual(len(get_model_a(project_state)._meta.related_objects), 1)
|
|
old_state = project_state.clone()
|
|
|
|
operation = AlterField(
|
|
model_name="a",
|
|
name="to_a",
|
|
field=models.ManyToManyField("something.A", symmetrical=False, blank=True),
|
|
)
|
|
# At this point the model would be rendered twice causing its related
|
|
# M2M through objects to point to an old copy and thus breaking their
|
|
# attribute lookup.
|
|
operation.state_forwards("something", project_state)
|
|
|
|
model_a_old = get_model_a(old_state)
|
|
model_a_new = get_model_a(project_state)
|
|
self.assertIsNot(model_a_old, model_a_new)
|
|
|
|
# The old model's _meta is still consistent
|
|
field_to_a_old = model_a_old._meta.get_field("to_a")
|
|
self.assertEqual(field_to_a_old.m2m_field_name(), "from_a")
|
|
self.assertEqual(field_to_a_old.m2m_reverse_field_name(), "to_a")
|
|
self.assertIs(field_to_a_old.related_model, model_a_old)
|
|
self.assertIs(
|
|
field_to_a_old.remote_field.through._meta.get_field("to_a").related_model,
|
|
model_a_old,
|
|
)
|
|
self.assertIs(
|
|
field_to_a_old.remote_field.through._meta.get_field("from_a").related_model,
|
|
model_a_old,
|
|
)
|
|
|
|
# The new model's _meta is still consistent
|
|
field_to_a_new = model_a_new._meta.get_field("to_a")
|
|
self.assertEqual(field_to_a_new.m2m_field_name(), "from_a")
|
|
self.assertEqual(field_to_a_new.m2m_reverse_field_name(), "to_a")
|
|
self.assertIs(field_to_a_new.related_model, model_a_new)
|
|
self.assertIs(
|
|
field_to_a_new.remote_field.through._meta.get_field("to_a").related_model,
|
|
model_a_new,
|
|
)
|
|
self.assertIs(
|
|
field_to_a_new.remote_field.through._meta.get_field("from_a").related_model,
|
|
model_a_new,
|
|
)
|
|
|
|
def test_equality(self):
|
|
"""
|
|
== and != are implemented correctly.
|
|
"""
|
|
# Test two things that should be equal
|
|
project_state = ProjectState()
|
|
project_state.add_model(
|
|
ModelState(
|
|
"migrations",
|
|
"Tag",
|
|
[
|
|
("id", models.AutoField(primary_key=True)),
|
|
("name", models.CharField(max_length=100)),
|
|
("hidden", models.BooleanField()),
|
|
],
|
|
{},
|
|
None,
|
|
)
|
|
)
|
|
project_state.apps # Fill the apps cached property
|
|
other_state = project_state.clone()
|
|
self.assertEqual(project_state, project_state)
|
|
self.assertEqual(project_state, other_state)
|
|
self.assertIs(project_state != project_state, False)
|
|
self.assertIs(project_state != other_state, False)
|
|
self.assertNotEqual(project_state.apps, other_state.apps)
|
|
|
|
# Make a very small change (max_len 99) and see if that affects it
|
|
project_state = ProjectState()
|
|
project_state.add_model(
|
|
ModelState(
|
|
"migrations",
|
|
"Tag",
|
|
[
|
|
("id", models.AutoField(primary_key=True)),
|
|
("name", models.CharField(max_length=99)),
|
|
("hidden", models.BooleanField()),
|
|
],
|
|
{},
|
|
None,
|
|
)
|
|
)
|
|
self.assertNotEqual(project_state, other_state)
|
|
self.assertIs(project_state == other_state, False)
|
|
|
|
def test_dangling_references_throw_error(self):
|
|
new_apps = Apps()
|
|
|
|
class Author(models.Model):
|
|
name = models.TextField()
|
|
|
|
class Meta:
|
|
app_label = "migrations"
|
|
apps = new_apps
|
|
|
|
class Publisher(models.Model):
|
|
name = models.TextField()
|
|
|
|
class Meta:
|
|
app_label = "migrations"
|
|
apps = new_apps
|
|
|
|
class Book(models.Model):
|
|
author = models.ForeignKey(Author, models.CASCADE)
|
|
publisher = models.ForeignKey(Publisher, models.CASCADE)
|
|
|
|
class Meta:
|
|
app_label = "migrations"
|
|
apps = new_apps
|
|
|
|
class Magazine(models.Model):
|
|
authors = models.ManyToManyField(Author)
|
|
|
|
class Meta:
|
|
app_label = "migrations"
|
|
apps = new_apps
|
|
|
|
# Make a valid ProjectState and render it
|
|
project_state = ProjectState()
|
|
project_state.add_model(ModelState.from_model(Author))
|
|
project_state.add_model(ModelState.from_model(Publisher))
|
|
project_state.add_model(ModelState.from_model(Book))
|
|
project_state.add_model(ModelState.from_model(Magazine))
|
|
self.assertEqual(len(project_state.apps.get_models()), 4)
|
|
|
|
# now make an invalid one with a ForeignKey
|
|
project_state = ProjectState()
|
|
project_state.add_model(ModelState.from_model(Book))
|
|
msg = (
|
|
"The field migrations.Book.author was declared with a lazy reference "
|
|
"to 'migrations.author', but app 'migrations' doesn't provide model "
|
|
"'author'.\n"
|
|
"The field migrations.Book.publisher was declared with a lazy reference "
|
|
"to 'migrations.publisher', but app 'migrations' doesn't provide model "
|
|
"'publisher'."
|
|
)
|
|
with self.assertRaisesMessage(ValueError, msg):
|
|
project_state.apps
|
|
|
|
# And another with ManyToManyField.
|
|
project_state = ProjectState()
|
|
project_state.add_model(ModelState.from_model(Magazine))
|
|
msg = (
|
|
"The field migrations.Magazine.authors was declared with a lazy reference "
|
|
"to 'migrations.author', but app 'migrations' doesn't provide model "
|
|
"'author'.\n"
|
|
"The field migrations.Magazine_authors.author was declared with a lazy "
|
|
"reference to 'migrations.author', but app 'migrations' doesn't provide "
|
|
"model 'author'."
|
|
)
|
|
with self.assertRaisesMessage(ValueError, msg):
|
|
project_state.apps
|
|
|
|
# And now with multiple models and multiple fields.
|
|
project_state.add_model(ModelState.from_model(Book))
|
|
msg = (
|
|
"The field migrations.Book.author was declared with a lazy reference "
|
|
"to 'migrations.author', but app 'migrations' doesn't provide model "
|
|
"'author'.\n"
|
|
"The field migrations.Book.publisher was declared with a lazy reference "
|
|
"to 'migrations.publisher', but app 'migrations' doesn't provide model "
|
|
"'publisher'.\n"
|
|
"The field migrations.Magazine.authors was declared with a lazy reference "
|
|
"to 'migrations.author', but app 'migrations' doesn't provide model "
|
|
"'author'.\n"
|
|
"The field migrations.Magazine_authors.author was declared with a lazy "
|
|
"reference to 'migrations.author', but app 'migrations' doesn't provide "
|
|
"model 'author'."
|
|
)
|
|
with self.assertRaisesMessage(ValueError, msg):
|
|
project_state.apps
|
|
|
|
def test_reference_mixed_case_app_label(self):
|
|
new_apps = Apps()
|
|
|
|
class Author(models.Model):
|
|
class Meta:
|
|
app_label = "MiXedCase_migrations"
|
|
apps = new_apps
|
|
|
|
class Book(models.Model):
|
|
author = models.ForeignKey(Author, models.CASCADE)
|
|
|
|
class Meta:
|
|
app_label = "MiXedCase_migrations"
|
|
apps = new_apps
|
|
|
|
class Magazine(models.Model):
|
|
authors = models.ManyToManyField(Author)
|
|
|
|
class Meta:
|
|
app_label = "MiXedCase_migrations"
|
|
apps = new_apps
|
|
|
|
project_state = ProjectState()
|
|
project_state.add_model(ModelState.from_model(Author))
|
|
project_state.add_model(ModelState.from_model(Book))
|
|
project_state.add_model(ModelState.from_model(Magazine))
|
|
self.assertEqual(len(project_state.apps.get_models()), 3)
|
|
|
|
def test_real_apps(self):
|
|
"""
|
|
Including real apps can resolve dangling FK errors.
|
|
This test relies on the fact that contenttypes is always loaded.
|
|
"""
|
|
new_apps = Apps()
|
|
|
|
class TestModel(models.Model):
|
|
ct = models.ForeignKey("contenttypes.ContentType", models.CASCADE)
|
|
|
|
class Meta:
|
|
app_label = "migrations"
|
|
apps = new_apps
|
|
|
|
# If we just stick it into an empty state it should fail
|
|
project_state = ProjectState()
|
|
project_state.add_model(ModelState.from_model(TestModel))
|
|
with self.assertRaises(ValueError):
|
|
project_state.apps
|
|
|
|
# If we include the real app it should succeed
|
|
project_state = ProjectState(real_apps={"contenttypes"})
|
|
project_state.add_model(ModelState.from_model(TestModel))
|
|
rendered_state = project_state.apps
|
|
self.assertEqual(
|
|
len(
|
|
[
|
|
x
|
|
for x in rendered_state.get_models()
|
|
if x._meta.app_label == "migrations"
|
|
]
|
|
),
|
|
1,
|
|
)
|
|
|
|
def test_real_apps_non_set(self):
|
|
with self.assertRaises(AssertionError):
|
|
ProjectState(real_apps=["contenttypes"])
|
|
|
|
def test_ignore_order_wrt(self):
|
|
"""
|
|
Makes sure ProjectState doesn't include OrderWrt fields when
|
|
making from existing models.
|
|
"""
|
|
new_apps = Apps()
|
|
|
|
class Author(models.Model):
|
|
name = models.TextField()
|
|
|
|
class Meta:
|
|
app_label = "migrations"
|
|
apps = new_apps
|
|
|
|
class Book(models.Model):
|
|
author = models.ForeignKey(Author, models.CASCADE)
|
|
|
|
class Meta:
|
|
app_label = "migrations"
|
|
apps = new_apps
|
|
order_with_respect_to = "author"
|
|
|
|
# Make a valid ProjectState and render it
|
|
project_state = ProjectState()
|
|
project_state.add_model(ModelState.from_model(Author))
|
|
project_state.add_model(ModelState.from_model(Book))
|
|
self.assertEqual(
|
|
list(project_state.models["migrations", "book"].fields),
|
|
["id", "author"],
|
|
)
|
|
|
|
def test_modelstate_get_field_order_wrt(self):
|
|
new_apps = Apps()
|
|
|
|
class Author(models.Model):
|
|
name = models.TextField()
|
|
|
|
class Meta:
|
|
app_label = "migrations"
|
|
apps = new_apps
|
|
|
|
class Book(models.Model):
|
|
author = models.ForeignKey(Author, models.CASCADE)
|
|
|
|
class Meta:
|
|
app_label = "migrations"
|
|
apps = new_apps
|
|
order_with_respect_to = "author"
|
|
|
|
model_state = ModelState.from_model(Book)
|
|
order_wrt_field = model_state.get_field("_order")
|
|
self.assertIsInstance(order_wrt_field, models.ForeignKey)
|
|
self.assertEqual(order_wrt_field.related_model, "migrations.author")
|
|
|
|
def test_modelstate_get_field_no_order_wrt_order_field(self):
|
|
new_apps = Apps()
|
|
|
|
class HistoricalRecord(models.Model):
|
|
_order = models.PositiveSmallIntegerField()
|
|
|
|
class Meta:
|
|
app_label = "migrations"
|
|
apps = new_apps
|
|
|
|
model_state = ModelState.from_model(HistoricalRecord)
|
|
order_field = model_state.get_field("_order")
|
|
self.assertIsNone(order_field.related_model)
|
|
self.assertIsInstance(order_field, models.PositiveSmallIntegerField)
|
|
|
|
def test_manager_refer_correct_model_version(self):
|
|
"""
|
|
#24147 - Managers refer to the correct version of a
|
|
historical model
|
|
"""
|
|
project_state = ProjectState()
|
|
project_state.add_model(
|
|
ModelState(
|
|
app_label="migrations",
|
|
name="Tag",
|
|
fields=[
|
|
("id", models.AutoField(primary_key=True)),
|
|
("hidden", models.BooleanField()),
|
|
],
|
|
managers=[
|
|
("food_mgr", FoodManager("a", "b")),
|
|
("food_qs", FoodQuerySet.as_manager()),
|
|
],
|
|
)
|
|
)
|
|
|
|
old_model = project_state.apps.get_model("migrations", "tag")
|
|
|
|
new_state = project_state.clone()
|
|
operation = RemoveField("tag", "hidden")
|
|
operation.state_forwards("migrations", new_state)
|
|
|
|
new_model = new_state.apps.get_model("migrations", "tag")
|
|
|
|
self.assertIsNot(old_model, new_model)
|
|
self.assertIs(old_model, old_model.food_mgr.model)
|
|
self.assertIs(old_model, old_model.food_qs.model)
|
|
self.assertIs(new_model, new_model.food_mgr.model)
|
|
self.assertIs(new_model, new_model.food_qs.model)
|
|
self.assertIsNot(old_model.food_mgr, new_model.food_mgr)
|
|
self.assertIsNot(old_model.food_qs, new_model.food_qs)
|
|
self.assertIsNot(old_model.food_mgr.model, new_model.food_mgr.model)
|
|
self.assertIsNot(old_model.food_qs.model, new_model.food_qs.model)
|
|
|
|
def test_choices_iterator(self):
|
|
"""
|
|
#24483 - ProjectState.from_apps should not destructively consume
|
|
Field.choices iterators.
|
|
"""
|
|
new_apps = Apps(["migrations"])
|
|
choices = [("a", "A"), ("b", "B")]
|
|
|
|
class Author(models.Model):
|
|
name = models.CharField(max_length=255)
|
|
choice = models.CharField(max_length=255, choices=iter(choices))
|
|
|
|
class Meta:
|
|
app_label = "migrations"
|
|
apps = new_apps
|
|
|
|
ProjectState.from_apps(new_apps)
|
|
choices_field = Author._meta.get_field("choice")
|
|
self.assertEqual(list(choices_field.choices), choices)
|
|
|
|
|
|
class StateRelationsTests(SimpleTestCase):
|
|
def get_base_project_state(self):
|
|
new_apps = Apps()
|
|
|
|
class User(models.Model):
|
|
class Meta:
|
|
app_label = "tests"
|
|
apps = new_apps
|
|
|
|
class Comment(models.Model):
|
|
text = models.TextField()
|
|
user = models.ForeignKey(User, models.CASCADE)
|
|
comments = models.ManyToManyField("self")
|
|
|
|
class Meta:
|
|
app_label = "tests"
|
|
apps = new_apps
|
|
|
|
class Post(models.Model):
|
|
text = models.TextField()
|
|
authors = models.ManyToManyField(User)
|
|
|
|
class Meta:
|
|
app_label = "tests"
|
|
apps = new_apps
|
|
|
|
project_state = ProjectState()
|
|
project_state.add_model(ModelState.from_model(User))
|
|
project_state.add_model(ModelState.from_model(Comment))
|
|
project_state.add_model(ModelState.from_model(Post))
|
|
return project_state
|
|
|
|
def test_relations_population(self):
|
|
tests = [
|
|
(
|
|
"add_model",
|
|
[
|
|
ModelState(
|
|
app_label="migrations",
|
|
name="Tag",
|
|
fields=[("id", models.AutoField(primary_key=True))],
|
|
),
|
|
],
|
|
),
|
|
("remove_model", ["tests", "comment"]),
|
|
("rename_model", ["tests", "comment", "opinion"]),
|
|
(
|
|
"add_field",
|
|
[
|
|
"tests",
|
|
"post",
|
|
"next_post",
|
|
models.ForeignKey("self", models.CASCADE),
|
|
True,
|
|
],
|
|
),
|
|
("remove_field", ["tests", "post", "text"]),
|
|
("rename_field", ["tests", "comment", "user", "author"]),
|
|
(
|
|
"alter_field",
|
|
[
|
|
"tests",
|
|
"comment",
|
|
"user",
|
|
models.IntegerField(),
|
|
True,
|
|
],
|
|
),
|
|
]
|
|
for method, args in tests:
|
|
with self.subTest(method=method):
|
|
project_state = self.get_base_project_state()
|
|
getattr(project_state, method)(*args)
|
|
# ProjectState's `_relations` are populated on `relations` access.
|
|
self.assertIsNone(project_state._relations)
|
|
self.assertEqual(project_state.relations, project_state._relations)
|
|
self.assertIsNotNone(project_state._relations)
|
|
|
|
def test_add_model(self):
|
|
project_state = self.get_base_project_state()
|
|
self.assertEqual(
|
|
list(project_state.relations["tests", "user"]),
|
|
[("tests", "comment"), ("tests", "post")],
|
|
)
|
|
self.assertEqual(
|
|
list(project_state.relations["tests", "comment"]),
|
|
[("tests", "comment")],
|
|
)
|
|
self.assertNotIn(("tests", "post"), project_state.relations)
|
|
|
|
def test_add_model_no_relations(self):
|
|
project_state = ProjectState()
|
|
project_state.add_model(
|
|
ModelState(
|
|
app_label="migrations",
|
|
name="Tag",
|
|
fields=[("id", models.AutoField(primary_key=True))],
|
|
)
|
|
)
|
|
self.assertEqual(project_state.relations, {})
|
|
|
|
def test_add_model_other_app(self):
|
|
project_state = self.get_base_project_state()
|
|
self.assertEqual(
|
|
list(project_state.relations["tests", "user"]),
|
|
[("tests", "comment"), ("tests", "post")],
|
|
)
|
|
project_state.add_model(
|
|
ModelState(
|
|
app_label="tests_other",
|
|
name="comment",
|
|
fields=[
|
|
("id", models.AutoField(primary_key=True)),
|
|
("user", models.ForeignKey("tests.user", models.CASCADE)),
|
|
],
|
|
)
|
|
)
|
|
self.assertEqual(
|
|
list(project_state.relations["tests", "user"]),
|
|
[("tests", "comment"), ("tests", "post"), ("tests_other", "comment")],
|
|
)
|
|
|
|
def test_remove_model(self):
|
|
project_state = self.get_base_project_state()
|
|
self.assertEqual(
|
|
list(project_state.relations["tests", "user"]),
|
|
[("tests", "comment"), ("tests", "post")],
|
|
)
|
|
self.assertEqual(
|
|
list(project_state.relations["tests", "comment"]),
|
|
[("tests", "comment")],
|
|
)
|
|
|
|
project_state.remove_model("tests", "comment")
|
|
self.assertEqual(
|
|
list(project_state.relations["tests", "user"]),
|
|
[("tests", "post")],
|
|
)
|
|
self.assertNotIn(("tests", "comment"), project_state.relations)
|
|
project_state.remove_model("tests", "post")
|
|
self.assertEqual(project_state.relations, {})
|
|
project_state.remove_model("tests", "user")
|
|
self.assertEqual(project_state.relations, {})
|
|
|
|
def test_rename_model(self):
|
|
project_state = self.get_base_project_state()
|
|
self.assertEqual(
|
|
list(project_state.relations["tests", "user"]),
|
|
[("tests", "comment"), ("tests", "post")],
|
|
)
|
|
self.assertEqual(
|
|
list(project_state.relations["tests", "comment"]),
|
|
[("tests", "comment")],
|
|
)
|
|
|
|
related_field = project_state.relations["tests", "user"]["tests", "comment"]
|
|
project_state.rename_model("tests", "comment", "opinion")
|
|
self.assertEqual(
|
|
list(project_state.relations["tests", "user"]),
|
|
[("tests", "post"), ("tests", "opinion")],
|
|
)
|
|
self.assertEqual(
|
|
list(project_state.relations["tests", "opinion"]),
|
|
[("tests", "opinion")],
|
|
)
|
|
self.assertNotIn(("tests", "comment"), project_state.relations)
|
|
self.assertEqual(
|
|
project_state.relations["tests", "user"]["tests", "opinion"],
|
|
related_field,
|
|
)
|
|
|
|
project_state.rename_model("tests", "user", "author")
|
|
self.assertEqual(
|
|
list(project_state.relations["tests", "author"]),
|
|
[("tests", "post"), ("tests", "opinion")],
|
|
)
|
|
self.assertNotIn(("tests", "user"), project_state.relations)
|
|
|
|
def test_rename_model_no_relations(self):
|
|
project_state = self.get_base_project_state()
|
|
self.assertEqual(
|
|
list(project_state.relations["tests", "user"]),
|
|
[("tests", "comment"), ("tests", "post")],
|
|
)
|
|
related_field = project_state.relations["tests", "user"]["tests", "post"]
|
|
self.assertNotIn(("tests", "post"), project_state.relations)
|
|
# Rename a model without relations.
|
|
project_state.rename_model("tests", "post", "blog")
|
|
self.assertEqual(
|
|
list(project_state.relations["tests", "user"]),
|
|
[("tests", "comment"), ("tests", "blog")],
|
|
)
|
|
self.assertNotIn(("tests", "blog"), project_state.relations)
|
|
self.assertEqual(
|
|
related_field,
|
|
project_state.relations["tests", "user"]["tests", "blog"],
|
|
)
|
|
|
|
def test_add_field(self):
|
|
project_state = self.get_base_project_state()
|
|
self.assertNotIn(("tests", "post"), project_state.relations)
|
|
# Add a self-referential foreign key.
|
|
new_field = models.ForeignKey("self", models.CASCADE)
|
|
project_state.add_field(
|
|
"tests",
|
|
"post",
|
|
"next_post",
|
|
new_field,
|
|
preserve_default=True,
|
|
)
|
|
self.assertEqual(
|
|
list(project_state.relations["tests", "post"]),
|
|
[("tests", "post")],
|
|
)
|
|
self.assertEqual(
|
|
project_state.relations["tests", "post"]["tests", "post"],
|
|
{"next_post": new_field},
|
|
)
|
|
# Add a foreign key.
|
|
new_field = models.ForeignKey("tests.post", models.CASCADE)
|
|
project_state.add_field(
|
|
"tests",
|
|
"comment",
|
|
"post",
|
|
new_field,
|
|
preserve_default=True,
|
|
)
|
|
self.assertEqual(
|
|
list(project_state.relations["tests", "post"]),
|
|
[("tests", "post"), ("tests", "comment")],
|
|
)
|
|
self.assertEqual(
|
|
project_state.relations["tests", "post"]["tests", "comment"],
|
|
{"post": new_field},
|
|
)
|
|
|
|
def test_add_field_m2m_with_through(self):
|
|
project_state = self.get_base_project_state()
|
|
project_state.add_model(
|
|
ModelState(
|
|
app_label="tests",
|
|
name="Tag",
|
|
fields=[("id", models.AutoField(primary_key=True))],
|
|
)
|
|
)
|
|
project_state.add_model(
|
|
ModelState(
|
|
app_label="tests",
|
|
name="PostTag",
|
|
fields=[
|
|
("id", models.AutoField(primary_key=True)),
|
|
("post", models.ForeignKey("tests.post", models.CASCADE)),
|
|
("tag", models.ForeignKey("tests.tag", models.CASCADE)),
|
|
],
|
|
)
|
|
)
|
|
self.assertEqual(
|
|
list(project_state.relations["tests", "post"]),
|
|
[("tests", "posttag")],
|
|
)
|
|
self.assertEqual(
|
|
list(project_state.relations["tests", "tag"]),
|
|
[("tests", "posttag")],
|
|
)
|
|
# Add a many-to-many field with the through model.
|
|
new_field = models.ManyToManyField("tests.tag", through="tests.posttag")
|
|
project_state.add_field(
|
|
"tests",
|
|
"post",
|
|
"tags",
|
|
new_field,
|
|
preserve_default=True,
|
|
)
|
|
self.assertEqual(
|
|
list(project_state.relations["tests", "post"]),
|
|
[("tests", "posttag")],
|
|
)
|
|
self.assertEqual(
|
|
list(project_state.relations["tests", "tag"]),
|
|
[("tests", "posttag"), ("tests", "post")],
|
|
)
|
|
self.assertEqual(
|
|
project_state.relations["tests", "tag"]["tests", "post"],
|
|
{"tags": new_field},
|
|
)
|
|
|
|
def test_remove_field(self):
|
|
project_state = self.get_base_project_state()
|
|
self.assertEqual(
|
|
list(project_state.relations["tests", "user"]),
|
|
[("tests", "comment"), ("tests", "post")],
|
|
)
|
|
# Remove a many-to-many field.
|
|
project_state.remove_field("tests", "post", "authors")
|
|
self.assertEqual(
|
|
list(project_state.relations["tests", "user"]),
|
|
[("tests", "comment")],
|
|
)
|
|
# Remove a foreign key.
|
|
project_state.remove_field("tests", "comment", "user")
|
|
self.assertEqual(project_state.relations["tests", "user"], {})
|
|
|
|
def test_remove_field_no_relations(self):
|
|
project_state = self.get_base_project_state()
|
|
self.assertEqual(
|
|
list(project_state.relations["tests", "user"]),
|
|
[("tests", "comment"), ("tests", "post")],
|
|
)
|
|
# Remove a non-relation field.
|
|
project_state.remove_field("tests", "post", "text")
|
|
self.assertEqual(
|
|
list(project_state.relations["tests", "user"]),
|
|
[("tests", "comment"), ("tests", "post")],
|
|
)
|
|
|
|
def test_rename_field(self):
|
|
project_state = self.get_base_project_state()
|
|
field = project_state.models["tests", "comment"].fields["user"]
|
|
self.assertEqual(
|
|
project_state.relations["tests", "user"]["tests", "comment"],
|
|
{"user": field},
|
|
)
|
|
|
|
project_state.rename_field("tests", "comment", "user", "author")
|
|
renamed_field = project_state.models["tests", "comment"].fields["author"]
|
|
self.assertEqual(
|
|
project_state.relations["tests", "user"]["tests", "comment"],
|
|
{"author": renamed_field},
|
|
)
|
|
self.assertEqual(field, renamed_field)
|
|
|
|
def test_rename_field_no_relations(self):
|
|
project_state = self.get_base_project_state()
|
|
self.assertEqual(
|
|
list(project_state.relations["tests", "user"]),
|
|
[("tests", "comment"), ("tests", "post")],
|
|
)
|
|
# Rename a non-relation field.
|
|
project_state.rename_field("tests", "post", "text", "description")
|
|
self.assertEqual(
|
|
list(project_state.relations["tests", "user"]),
|
|
[("tests", "comment"), ("tests", "post")],
|
|
)
|
|
|
|
def test_alter_field(self):
|
|
project_state = self.get_base_project_state()
|
|
self.assertEqual(
|
|
list(project_state.relations["tests", "user"]),
|
|
[("tests", "comment"), ("tests", "post")],
|
|
)
|
|
# Alter a foreign key to a non-relation field.
|
|
project_state.alter_field(
|
|
"tests",
|
|
"comment",
|
|
"user",
|
|
models.IntegerField(),
|
|
preserve_default=True,
|
|
)
|
|
self.assertEqual(
|
|
list(project_state.relations["tests", "user"]),
|
|
[("tests", "post")],
|
|
)
|
|
# Alter a non-relation field to a many-to-many field.
|
|
m2m_field = models.ManyToManyField("tests.user")
|
|
project_state.alter_field(
|
|
"tests",
|
|
"comment",
|
|
"user",
|
|
m2m_field,
|
|
preserve_default=True,
|
|
)
|
|
self.assertEqual(
|
|
list(project_state.relations["tests", "user"]),
|
|
[("tests", "post"), ("tests", "comment")],
|
|
)
|
|
self.assertEqual(
|
|
project_state.relations["tests", "user"]["tests", "comment"],
|
|
{"user": m2m_field},
|
|
)
|
|
|
|
def test_alter_field_m2m_to_fk(self):
|
|
project_state = self.get_base_project_state()
|
|
project_state.add_model(
|
|
ModelState(
|
|
app_label="tests_other",
|
|
name="user_other",
|
|
fields=[("id", models.AutoField(primary_key=True))],
|
|
)
|
|
)
|
|
self.assertEqual(
|
|
list(project_state.relations["tests", "user"]),
|
|
[("tests", "comment"), ("tests", "post")],
|
|
)
|
|
self.assertNotIn(("tests_other", "user_other"), project_state.relations)
|
|
# Alter a many-to-many field to a foreign key.
|
|
foreign_key = models.ForeignKey("tests_other.user_other", models.CASCADE)
|
|
project_state.alter_field(
|
|
"tests",
|
|
"post",
|
|
"authors",
|
|
foreign_key,
|
|
preserve_default=True,
|
|
)
|
|
self.assertEqual(
|
|
list(project_state.relations["tests", "user"]),
|
|
[("tests", "comment")],
|
|
)
|
|
self.assertEqual(
|
|
list(project_state.relations["tests_other", "user_other"]),
|
|
[("tests", "post")],
|
|
)
|
|
self.assertEqual(
|
|
project_state.relations["tests_other", "user_other"]["tests", "post"],
|
|
{"authors": foreign_key},
|
|
)
|
|
|
|
def test_many_relations_to_same_model(self):
|
|
project_state = self.get_base_project_state()
|
|
new_field = models.ForeignKey("tests.user", models.CASCADE)
|
|
project_state.add_field(
|
|
"tests",
|
|
"comment",
|
|
"reviewer",
|
|
new_field,
|
|
preserve_default=True,
|
|
)
|
|
self.assertEqual(
|
|
list(project_state.relations["tests", "user"]),
|
|
[("tests", "comment"), ("tests", "post")],
|
|
)
|
|
comment_rels = project_state.relations["tests", "user"]["tests", "comment"]
|
|
# Two foreign keys to the same model.
|
|
self.assertEqual(len(comment_rels), 2)
|
|
self.assertEqual(comment_rels["reviewer"], new_field)
|
|
# Rename the second foreign key.
|
|
project_state.rename_field("tests", "comment", "reviewer", "supervisor")
|
|
self.assertEqual(len(comment_rels), 2)
|
|
self.assertEqual(comment_rels["supervisor"], new_field)
|
|
# Remove the first foreign key.
|
|
project_state.remove_field("tests", "comment", "user")
|
|
self.assertEqual(comment_rels, {"supervisor": new_field})
|
|
|
|
|
|
class ModelStateTests(SimpleTestCase):
|
|
def test_custom_model_base(self):
|
|
state = ModelState.from_model(ModelWithCustomBase)
|
|
self.assertEqual(state.bases, (models.Model,))
|
|
|
|
def test_bound_field_sanity_check(self):
|
|
field = models.CharField(max_length=1)
|
|
field.model = models.Model
|
|
with self.assertRaisesMessage(
|
|
ValueError, 'ModelState.fields cannot be bound to a model - "field" is.'
|
|
):
|
|
ModelState("app", "Model", [("field", field)])
|
|
|
|
def test_sanity_check_to(self):
|
|
field = models.ForeignKey(UnicodeModel, models.CASCADE)
|
|
with self.assertRaisesMessage(
|
|
ValueError,
|
|
'Model fields in "ModelState.fields" cannot refer to a model class - '
|
|
'"app.Model.field.to" does. Use a string reference instead.',
|
|
):
|
|
ModelState("app", "Model", [("field", field)])
|
|
|
|
def test_sanity_check_through(self):
|
|
field = models.ManyToManyField("UnicodeModel")
|
|
field.remote_field.through = UnicodeModel
|
|
with self.assertRaisesMessage(
|
|
ValueError,
|
|
'Model fields in "ModelState.fields" cannot refer to a model class - '
|
|
'"app.Model.field.through" does. Use a string reference instead.',
|
|
):
|
|
ModelState("app", "Model", [("field", field)])
|
|
|
|
def test_sanity_index_name(self):
|
|
field = models.IntegerField()
|
|
options = {"indexes": [models.Index(fields=["field"])]}
|
|
msg = (
|
|
"Indexes passed to ModelState require a name attribute. <Index: "
|
|
"fields=['field']> doesn't have one."
|
|
)
|
|
with self.assertRaisesMessage(ValueError, msg):
|
|
ModelState("app", "Model", [("field", field)], options=options)
|
|
|
|
def test_fields_immutability(self):
|
|
"""
|
|
Rendering a model state doesn't alter its internal fields.
|
|
"""
|
|
apps = Apps()
|
|
field = models.CharField(max_length=1)
|
|
state = ModelState("app", "Model", [("name", field)])
|
|
Model = state.render(apps)
|
|
self.assertNotEqual(Model._meta.get_field("name"), field)
|
|
|
|
def test_repr(self):
|
|
field = models.CharField(max_length=1)
|
|
state = ModelState(
|
|
"app", "Model", [("name", field)], bases=["app.A", "app.B", "app.C"]
|
|
)
|
|
self.assertEqual(repr(state), "<ModelState: 'app.Model'>")
|
|
|
|
project_state = ProjectState()
|
|
project_state.add_model(state)
|
|
with self.assertRaisesMessage(
|
|
InvalidBasesError, "Cannot resolve bases for [<ModelState: 'app.Model'>]"
|
|
):
|
|
project_state.apps
|
|
|
|
def test_fields_ordering_equality(self):
|
|
state = ModelState(
|
|
"migrations",
|
|
"Tag",
|
|
[
|
|
("id", models.AutoField(primary_key=True)),
|
|
("name", models.CharField(max_length=100)),
|
|
("hidden", models.BooleanField()),
|
|
],
|
|
)
|
|
reordered_state = ModelState(
|
|
"migrations",
|
|
"Tag",
|
|
[
|
|
("id", models.AutoField(primary_key=True)),
|
|
# Purposely re-ordered.
|
|
("hidden", models.BooleanField()),
|
|
("name", models.CharField(max_length=100)),
|
|
],
|
|
)
|
|
self.assertEqual(state, reordered_state)
|
|
|
|
@override_settings(TEST_SWAPPABLE_MODEL="migrations.SomeFakeModel")
|
|
def test_create_swappable(self):
|
|
"""
|
|
Tests making a ProjectState from an Apps with a swappable model
|
|
"""
|
|
new_apps = Apps(["migrations"])
|
|
|
|
class Author(models.Model):
|
|
name = models.CharField(max_length=255)
|
|
bio = models.TextField()
|
|
age = models.IntegerField(blank=True, null=True)
|
|
|
|
class Meta:
|
|
app_label = "migrations"
|
|
apps = new_apps
|
|
swappable = "TEST_SWAPPABLE_MODEL"
|
|
|
|
author_state = ModelState.from_model(Author)
|
|
self.assertEqual(author_state.app_label, "migrations")
|
|
self.assertEqual(author_state.name, "Author")
|
|
self.assertEqual(list(author_state.fields), ["id", "name", "bio", "age"])
|
|
self.assertEqual(author_state.fields["name"].max_length, 255)
|
|
self.assertIs(author_state.fields["bio"].null, False)
|
|
self.assertIs(author_state.fields["age"].null, True)
|
|
self.assertEqual(
|
|
author_state.options,
|
|
{"swappable": "TEST_SWAPPABLE_MODEL", "indexes": [], "constraints": []},
|
|
)
|
|
self.assertEqual(author_state.bases, (models.Model,))
|
|
self.assertEqual(author_state.managers, [])
|
|
|
|
@override_settings(TEST_SWAPPABLE_MODEL="migrations.SomeFakeModel")
|
|
def test_create_swappable_from_abstract(self):
|
|
"""
|
|
A swappable model inheriting from a hierarchy:
|
|
concrete -> abstract -> concrete.
|
|
"""
|
|
new_apps = Apps(["migrations"])
|
|
|
|
class SearchableLocation(models.Model):
|
|
keywords = models.CharField(max_length=256)
|
|
|
|
class Meta:
|
|
app_label = "migrations"
|
|
apps = new_apps
|
|
|
|
class Station(SearchableLocation):
|
|
name = models.CharField(max_length=128)
|
|
|
|
class Meta:
|
|
abstract = True
|
|
|
|
class BusStation(Station):
|
|
bus_routes = models.CharField(max_length=128)
|
|
inbound = models.BooleanField(default=False)
|
|
|
|
class Meta(Station.Meta):
|
|
app_label = "migrations"
|
|
apps = new_apps
|
|
swappable = "TEST_SWAPPABLE_MODEL"
|
|
|
|
station_state = ModelState.from_model(BusStation)
|
|
self.assertEqual(station_state.app_label, "migrations")
|
|
self.assertEqual(station_state.name, "BusStation")
|
|
self.assertEqual(
|
|
list(station_state.fields),
|
|
["searchablelocation_ptr", "name", "bus_routes", "inbound"],
|
|
)
|
|
self.assertEqual(station_state.fields["name"].max_length, 128)
|
|
self.assertIs(station_state.fields["bus_routes"].null, False)
|
|
self.assertEqual(
|
|
station_state.options,
|
|
{
|
|
"abstract": False,
|
|
"swappable": "TEST_SWAPPABLE_MODEL",
|
|
"indexes": [],
|
|
"constraints": [],
|
|
},
|
|
)
|
|
self.assertEqual(station_state.bases, ("migrations.searchablelocation",))
|
|
self.assertEqual(station_state.managers, [])
|
|
|
|
@override_settings(TEST_SWAPPABLE_MODEL="migrations.SomeFakeModel")
|
|
def test_custom_manager_swappable(self):
|
|
"""
|
|
Tests making a ProjectState from unused models with custom managers
|
|
"""
|
|
new_apps = Apps(["migrations"])
|
|
|
|
class Food(models.Model):
|
|
food_mgr = FoodManager("a", "b")
|
|
food_qs = FoodQuerySet.as_manager()
|
|
food_no_mgr = NoMigrationFoodManager("x", "y")
|
|
|
|
class Meta:
|
|
app_label = "migrations"
|
|
apps = new_apps
|
|
swappable = "TEST_SWAPPABLE_MODEL"
|
|
|
|
food_state = ModelState.from_model(Food)
|
|
|
|
# The default manager is used in migrations
|
|
self.assertEqual([name for name, mgr in food_state.managers], ["food_mgr"])
|
|
self.assertEqual(food_state.managers[0][1].args, ("a", "b", 1, 2))
|
|
|
|
@isolate_apps("migrations", "django.contrib.contenttypes")
|
|
def test_order_with_respect_to_private_field(self):
|
|
class PrivateFieldModel(models.Model):
|
|
content_type = models.ForeignKey("contenttypes.ContentType", models.CASCADE)
|
|
object_id = models.PositiveIntegerField()
|
|
private = GenericForeignKey()
|
|
|
|
class Meta:
|
|
order_with_respect_to = "private"
|
|
|
|
state = ModelState.from_model(PrivateFieldModel)
|
|
self.assertNotIn("order_with_respect_to", state.options)
|
|
|
|
@isolate_apps("migrations")
|
|
def test_abstract_model_children_inherit_indexes(self):
|
|
class Abstract(models.Model):
|
|
name = models.CharField(max_length=50)
|
|
|
|
class Meta:
|
|
app_label = "migrations"
|
|
abstract = True
|
|
indexes = [models.Index(fields=["name"])]
|
|
|
|
class Child1(Abstract):
|
|
pass
|
|
|
|
class Child2(Abstract):
|
|
pass
|
|
|
|
child1_state = ModelState.from_model(Child1)
|
|
child2_state = ModelState.from_model(Child2)
|
|
index_names = [index.name for index in child1_state.options["indexes"]]
|
|
self.assertEqual(index_names, ["migrations__name_b0afd7_idx"])
|
|
index_names = [index.name for index in child2_state.options["indexes"]]
|
|
self.assertEqual(index_names, ["migrations__name_016466_idx"])
|
|
|
|
# Modifying the state doesn't modify the index on the model.
|
|
child1_state.options["indexes"][0].name = "bar"
|
|
self.assertEqual(Child1._meta.indexes[0].name, "migrations__name_b0afd7_idx")
|
|
|
|
@isolate_apps("migrations")
|
|
def test_explicit_index_name(self):
|
|
class TestModel(models.Model):
|
|
name = models.CharField(max_length=50)
|
|
|
|
class Meta:
|
|
app_label = "migrations"
|
|
indexes = [models.Index(fields=["name"], name="foo_idx")]
|
|
|
|
model_state = ModelState.from_model(TestModel)
|
|
index_names = [index.name for index in model_state.options["indexes"]]
|
|
self.assertEqual(index_names, ["foo_idx"])
|
|
|
|
@isolate_apps("migrations")
|
|
def test_from_model_constraints(self):
|
|
class ModelWithConstraints(models.Model):
|
|
size = models.IntegerField()
|
|
|
|
class Meta:
|
|
constraints = [
|
|
models.CheckConstraint(
|
|
condition=models.Q(size__gt=1), name="size_gt_1"
|
|
)
|
|
]
|
|
|
|
state = ModelState.from_model(ModelWithConstraints)
|
|
model_constraints = ModelWithConstraints._meta.constraints
|
|
state_constraints = state.options["constraints"]
|
|
self.assertEqual(model_constraints, state_constraints)
|
|
self.assertIsNot(model_constraints, state_constraints)
|
|
self.assertIsNot(model_constraints[0], state_constraints[0])
|
|
|
|
|
|
class RelatedModelsTests(SimpleTestCase):
|
|
def setUp(self):
|
|
self.apps = Apps(["migrations.related_models_app"])
|
|
|
|
def create_model(
|
|
self, name, foreign_keys=[], bases=(), abstract=False, proxy=False
|
|
):
|
|
test_name = "related_models_app"
|
|
assert not (abstract and proxy)
|
|
meta_contents = {
|
|
"abstract": abstract,
|
|
"app_label": test_name,
|
|
"apps": self.apps,
|
|
"proxy": proxy,
|
|
}
|
|
meta = type("Meta", (), meta_contents)
|
|
if not bases:
|
|
bases = (models.Model,)
|
|
body = {
|
|
"Meta": meta,
|
|
"__module__": "__fake__",
|
|
}
|
|
fname_base = fname = "%s_%%d" % name.lower()
|
|
for i, fk in enumerate(foreign_keys, 1):
|
|
fname = fname_base % i
|
|
body[fname] = fk
|
|
return type(name, bases, body)
|
|
|
|
def assertRelated(self, model, needle):
|
|
self.assertEqual(
|
|
get_related_models_recursive(model),
|
|
{(n._meta.app_label, n._meta.model_name) for n in needle},
|
|
)
|
|
|
|
def test_unrelated(self):
|
|
A = self.create_model("A")
|
|
B = self.create_model("B")
|
|
self.assertRelated(A, [])
|
|
self.assertRelated(B, [])
|
|
|
|
def test_direct_fk(self):
|
|
A = self.create_model(
|
|
"A", foreign_keys=[models.ForeignKey("B", models.CASCADE)]
|
|
)
|
|
B = self.create_model("B")
|
|
self.assertRelated(A, [B])
|
|
self.assertRelated(B, [A])
|
|
|
|
def test_direct_hidden_fk(self):
|
|
A = self.create_model(
|
|
"A", foreign_keys=[models.ForeignKey("B", models.CASCADE, related_name="+")]
|
|
)
|
|
B = self.create_model("B")
|
|
self.assertRelated(A, [B])
|
|
self.assertRelated(B, [A])
|
|
|
|
def test_fk_through_proxy(self):
|
|
A = self.create_model("A")
|
|
B = self.create_model("B", bases=(A,), proxy=True)
|
|
C = self.create_model("C", bases=(B,), proxy=True)
|
|
D = self.create_model(
|
|
"D", foreign_keys=[models.ForeignKey("C", models.CASCADE)]
|
|
)
|
|
self.assertRelated(A, [B, C, D])
|
|
self.assertRelated(B, [A, C, D])
|
|
self.assertRelated(C, [A, B, D])
|
|
self.assertRelated(D, [A, B, C])
|
|
|
|
def test_nested_fk(self):
|
|
A = self.create_model(
|
|
"A", foreign_keys=[models.ForeignKey("B", models.CASCADE)]
|
|
)
|
|
B = self.create_model(
|
|
"B", foreign_keys=[models.ForeignKey("C", models.CASCADE)]
|
|
)
|
|
C = self.create_model("C")
|
|
self.assertRelated(A, [B, C])
|
|
self.assertRelated(B, [A, C])
|
|
self.assertRelated(C, [A, B])
|
|
|
|
def test_two_sided(self):
|
|
A = self.create_model(
|
|
"A", foreign_keys=[models.ForeignKey("B", models.CASCADE)]
|
|
)
|
|
B = self.create_model(
|
|
"B", foreign_keys=[models.ForeignKey("A", models.CASCADE)]
|
|
)
|
|
self.assertRelated(A, [B])
|
|
self.assertRelated(B, [A])
|
|
|
|
def test_circle(self):
|
|
A = self.create_model(
|
|
"A", foreign_keys=[models.ForeignKey("B", models.CASCADE)]
|
|
)
|
|
B = self.create_model(
|
|
"B", foreign_keys=[models.ForeignKey("C", models.CASCADE)]
|
|
)
|
|
C = self.create_model(
|
|
"C", foreign_keys=[models.ForeignKey("A", models.CASCADE)]
|
|
)
|
|
self.assertRelated(A, [B, C])
|
|
self.assertRelated(B, [A, C])
|
|
self.assertRelated(C, [A, B])
|
|
|
|
def test_base(self):
|
|
A = self.create_model("A")
|
|
B = self.create_model("B", bases=(A,))
|
|
self.assertRelated(A, [B])
|
|
self.assertRelated(B, [A])
|
|
|
|
def test_nested_base(self):
|
|
A = self.create_model("A")
|
|
B = self.create_model("B", bases=(A,))
|
|
C = self.create_model("C", bases=(B,))
|
|
self.assertRelated(A, [B, C])
|
|
self.assertRelated(B, [A, C])
|
|
self.assertRelated(C, [A, B])
|
|
|
|
def test_multiple_bases(self):
|
|
A = self.create_model("A")
|
|
B = self.create_model("B")
|
|
C = self.create_model(
|
|
"C",
|
|
bases=(
|
|
A,
|
|
B,
|
|
),
|
|
)
|
|
self.assertRelated(A, [B, C])
|
|
self.assertRelated(B, [A, C])
|
|
self.assertRelated(C, [A, B])
|
|
|
|
def test_multiple_nested_bases(self):
|
|
A = self.create_model("A")
|
|
B = self.create_model("B")
|
|
C = self.create_model(
|
|
"C",
|
|
bases=(
|
|
A,
|
|
B,
|
|
),
|
|
)
|
|
D = self.create_model("D")
|
|
E = self.create_model("E", bases=(D,))
|
|
F = self.create_model(
|
|
"F",
|
|
bases=(
|
|
C,
|
|
E,
|
|
),
|
|
)
|
|
Y = self.create_model("Y")
|
|
Z = self.create_model("Z", bases=(Y,))
|
|
self.assertRelated(A, [B, C, D, E, F])
|
|
self.assertRelated(B, [A, C, D, E, F])
|
|
self.assertRelated(C, [A, B, D, E, F])
|
|
self.assertRelated(D, [A, B, C, E, F])
|
|
self.assertRelated(E, [A, B, C, D, F])
|
|
self.assertRelated(F, [A, B, C, D, E])
|
|
self.assertRelated(Y, [Z])
|
|
self.assertRelated(Z, [Y])
|
|
|
|
def test_base_to_base_fk(self):
|
|
A = self.create_model(
|
|
"A", foreign_keys=[models.ForeignKey("Y", models.CASCADE)]
|
|
)
|
|
B = self.create_model("B", bases=(A,))
|
|
Y = self.create_model("Y")
|
|
Z = self.create_model("Z", bases=(Y,))
|
|
self.assertRelated(A, [B, Y, Z])
|
|
self.assertRelated(B, [A, Y, Z])
|
|
self.assertRelated(Y, [A, B, Z])
|
|
self.assertRelated(Z, [A, B, Y])
|
|
|
|
def test_base_to_subclass_fk(self):
|
|
A = self.create_model(
|
|
"A", foreign_keys=[models.ForeignKey("Z", models.CASCADE)]
|
|
)
|
|
B = self.create_model("B", bases=(A,))
|
|
Y = self.create_model("Y")
|
|
Z = self.create_model("Z", bases=(Y,))
|
|
self.assertRelated(A, [B, Y, Z])
|
|
self.assertRelated(B, [A, Y, Z])
|
|
self.assertRelated(Y, [A, B, Z])
|
|
self.assertRelated(Z, [A, B, Y])
|
|
|
|
def test_direct_m2m(self):
|
|
A = self.create_model("A", foreign_keys=[models.ManyToManyField("B")])
|
|
B = self.create_model("B")
|
|
self.assertRelated(A, [A.a_1.rel.through, B])
|
|
self.assertRelated(B, [A, A.a_1.rel.through])
|
|
|
|
def test_direct_m2m_self(self):
|
|
A = self.create_model("A", foreign_keys=[models.ManyToManyField("A")])
|
|
self.assertRelated(A, [A.a_1.rel.through])
|
|
|
|
def test_intermediate_m2m_self(self):
|
|
A = self.create_model(
|
|
"A", foreign_keys=[models.ManyToManyField("A", through="T")]
|
|
)
|
|
T = self.create_model(
|
|
"T",
|
|
foreign_keys=[
|
|
models.ForeignKey("A", models.CASCADE),
|
|
models.ForeignKey("A", models.CASCADE),
|
|
],
|
|
)
|
|
self.assertRelated(A, [T])
|
|
self.assertRelated(T, [A])
|
|
|
|
def test_intermediate_m2m(self):
|
|
A = self.create_model(
|
|
"A", foreign_keys=[models.ManyToManyField("B", through="T")]
|
|
)
|
|
B = self.create_model("B")
|
|
T = self.create_model(
|
|
"T",
|
|
foreign_keys=[
|
|
models.ForeignKey("A", models.CASCADE),
|
|
models.ForeignKey("B", models.CASCADE),
|
|
],
|
|
)
|
|
self.assertRelated(A, [B, T])
|
|
self.assertRelated(B, [A, T])
|
|
self.assertRelated(T, [A, B])
|
|
|
|
def test_intermediate_m2m_extern_fk(self):
|
|
A = self.create_model(
|
|
"A", foreign_keys=[models.ManyToManyField("B", through="T")]
|
|
)
|
|
B = self.create_model("B")
|
|
Z = self.create_model("Z")
|
|
T = self.create_model(
|
|
"T",
|
|
foreign_keys=[
|
|
models.ForeignKey("A", models.CASCADE),
|
|
models.ForeignKey("B", models.CASCADE),
|
|
models.ForeignKey("Z", models.CASCADE),
|
|
],
|
|
)
|
|
self.assertRelated(A, [B, T, Z])
|
|
self.assertRelated(B, [A, T, Z])
|
|
self.assertRelated(T, [A, B, Z])
|
|
self.assertRelated(Z, [A, B, T])
|
|
|
|
def test_intermediate_m2m_base(self):
|
|
A = self.create_model(
|
|
"A", foreign_keys=[models.ManyToManyField("B", through="T")]
|
|
)
|
|
B = self.create_model("B")
|
|
S = self.create_model("S")
|
|
T = self.create_model(
|
|
"T",
|
|
foreign_keys=[
|
|
models.ForeignKey("A", models.CASCADE),
|
|
models.ForeignKey("B", models.CASCADE),
|
|
],
|
|
bases=(S,),
|
|
)
|
|
self.assertRelated(A, [B, S, T])
|
|
self.assertRelated(B, [A, S, T])
|
|
self.assertRelated(S, [A, B, T])
|
|
self.assertRelated(T, [A, B, S])
|
|
|
|
def test_generic_fk(self):
|
|
A = self.create_model(
|
|
"A",
|
|
foreign_keys=[
|
|
models.ForeignKey("B", models.CASCADE),
|
|
GenericForeignKey(),
|
|
],
|
|
)
|
|
B = self.create_model(
|
|
"B",
|
|
foreign_keys=[
|
|
models.ForeignKey("C", models.CASCADE),
|
|
],
|
|
)
|
|
self.assertRelated(A, [B])
|
|
self.assertRelated(B, [A])
|
|
|
|
def test_abstract_base(self):
|
|
A = self.create_model("A", abstract=True)
|
|
B = self.create_model("B", bases=(A,))
|
|
self.assertRelated(A, [B])
|
|
self.assertRelated(B, [])
|
|
|
|
def test_nested_abstract_base(self):
|
|
A = self.create_model("A", abstract=True)
|
|
B = self.create_model("B", bases=(A,), abstract=True)
|
|
C = self.create_model("C", bases=(B,))
|
|
self.assertRelated(A, [B, C])
|
|
self.assertRelated(B, [C])
|
|
self.assertRelated(C, [])
|
|
|
|
def test_proxy_base(self):
|
|
A = self.create_model("A")
|
|
B = self.create_model("B", bases=(A,), proxy=True)
|
|
self.assertRelated(A, [B])
|
|
self.assertRelated(B, [])
|
|
|
|
def test_nested_proxy_base(self):
|
|
A = self.create_model("A")
|
|
B = self.create_model("B", bases=(A,), proxy=True)
|
|
C = self.create_model("C", bases=(B,), proxy=True)
|
|
self.assertRelated(A, [B, C])
|
|
self.assertRelated(B, [C])
|
|
self.assertRelated(C, [])
|
|
|
|
def test_multiple_mixed_bases(self):
|
|
A = self.create_model("A", abstract=True)
|
|
M = self.create_model("M")
|
|
P = self.create_model("P")
|
|
Q = self.create_model("Q", bases=(P,), proxy=True)
|
|
Z = self.create_model("Z", bases=(A, M, Q))
|
|
# M has a pointer O2O field p_ptr to P
|
|
self.assertRelated(A, [M, P, Q, Z])
|
|
self.assertRelated(M, [P, Q, Z])
|
|
self.assertRelated(P, [M, Q, Z])
|
|
self.assertRelated(Q, [M, P, Z])
|
|
self.assertRelated(Z, [M, P, Q])
|