450 lines
14 KiB
Python
450 lines
14 KiB
Python
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
|
|
from django.contrib.contenttypes.models import ContentType
|
|
from django.core.checks import Error
|
|
from django.core.exceptions import FieldDoesNotExist, FieldError
|
|
from django.db import models
|
|
from django.test import SimpleTestCase
|
|
from django.test.utils import isolate_apps
|
|
|
|
|
|
@isolate_apps("model_inheritance")
|
|
class AbstractInheritanceTests(SimpleTestCase):
|
|
def test_single_parent(self):
|
|
class AbstractBase(models.Model):
|
|
name = models.CharField(max_length=30)
|
|
|
|
class Meta:
|
|
abstract = True
|
|
|
|
class AbstractDescendant(AbstractBase):
|
|
name = models.CharField(max_length=50)
|
|
|
|
class Meta:
|
|
abstract = True
|
|
|
|
class DerivedChild(AbstractBase):
|
|
name = models.CharField(max_length=50)
|
|
|
|
class DerivedGrandChild(AbstractDescendant):
|
|
pass
|
|
|
|
self.assertEqual(AbstractDescendant._meta.get_field("name").max_length, 50)
|
|
self.assertEqual(DerivedChild._meta.get_field("name").max_length, 50)
|
|
self.assertEqual(DerivedGrandChild._meta.get_field("name").max_length, 50)
|
|
|
|
def test_multiple_inheritance_allows_inherited_field(self):
|
|
"""
|
|
Single layer multiple inheritance is as expected, deriving the
|
|
inherited field from the first base.
|
|
"""
|
|
|
|
class ParentA(models.Model):
|
|
name = models.CharField(max_length=255)
|
|
|
|
class Meta:
|
|
abstract = True
|
|
|
|
class ParentB(models.Model):
|
|
name = models.IntegerField()
|
|
|
|
class Meta:
|
|
abstract = True
|
|
|
|
class Child(ParentA, ParentB):
|
|
pass
|
|
|
|
self.assertEqual(Child.check(), [])
|
|
inherited_field = Child._meta.get_field("name")
|
|
self.assertIsInstance(inherited_field, models.CharField)
|
|
self.assertEqual(inherited_field.max_length, 255)
|
|
|
|
def test_diamond_shaped_multiple_inheritance_is_depth_first(self):
|
|
"""
|
|
In contrast to standard Python MRO, resolution of inherited fields is
|
|
strictly depth-first, rather than breadth-first in diamond-shaped cases.
|
|
|
|
This is because a copy of the parent field descriptor is placed onto
|
|
the model class in ModelBase.__new__(), rather than the attribute
|
|
lookup going via bases. (It only **looks** like inheritance.)
|
|
|
|
Here, Child inherits name from Root, rather than ParentB.
|
|
"""
|
|
|
|
class Root(models.Model):
|
|
name = models.CharField(max_length=255)
|
|
|
|
class Meta:
|
|
abstract = True
|
|
|
|
class ParentA(Root):
|
|
class Meta:
|
|
abstract = True
|
|
|
|
class ParentB(Root):
|
|
name = models.IntegerField()
|
|
|
|
class Meta:
|
|
abstract = True
|
|
|
|
class Child(ParentA, ParentB):
|
|
pass
|
|
|
|
self.assertEqual(Child.check(), [])
|
|
inherited_field = Child._meta.get_field("name")
|
|
self.assertIsInstance(inherited_field, models.CharField)
|
|
self.assertEqual(inherited_field.max_length, 255)
|
|
|
|
def test_target_field_may_be_pushed_down(self):
|
|
"""
|
|
Where the Child model needs to inherit a field from a different base
|
|
than that given by depth-first resolution, the target field can be
|
|
**pushed down** by being re-declared.
|
|
"""
|
|
|
|
class Root(models.Model):
|
|
name = models.CharField(max_length=255)
|
|
|
|
class Meta:
|
|
abstract = True
|
|
|
|
class ParentA(Root):
|
|
class Meta:
|
|
abstract = True
|
|
|
|
class ParentB(Root):
|
|
name = models.IntegerField()
|
|
|
|
class Meta:
|
|
abstract = True
|
|
|
|
class Child(ParentA, ParentB):
|
|
name = models.IntegerField()
|
|
|
|
self.assertEqual(Child.check(), [])
|
|
inherited_field = Child._meta.get_field("name")
|
|
self.assertIsInstance(inherited_field, models.IntegerField)
|
|
|
|
def test_multiple_inheritance_cannot_shadow_concrete_inherited_field(self):
|
|
class ConcreteParent(models.Model):
|
|
name = models.CharField(max_length=255)
|
|
|
|
class AbstractParent(models.Model):
|
|
name = models.IntegerField()
|
|
|
|
class Meta:
|
|
abstract = True
|
|
|
|
class FirstChild(ConcreteParent, AbstractParent):
|
|
pass
|
|
|
|
class AnotherChild(AbstractParent, ConcreteParent):
|
|
pass
|
|
|
|
self.assertIsInstance(FirstChild._meta.get_field("name"), models.CharField)
|
|
self.assertEqual(
|
|
AnotherChild.check(),
|
|
[
|
|
Error(
|
|
"The field 'name' clashes with the field 'name' "
|
|
"from model 'model_inheritance.concreteparent'.",
|
|
obj=AnotherChild._meta.get_field("name"),
|
|
id="models.E006",
|
|
)
|
|
],
|
|
)
|
|
|
|
def test_virtual_field(self):
|
|
class RelationModel(models.Model):
|
|
content_type = models.ForeignKey(ContentType, models.CASCADE)
|
|
object_id = models.PositiveIntegerField()
|
|
content_object = GenericForeignKey("content_type", "object_id")
|
|
|
|
class RelatedModelAbstract(models.Model):
|
|
field = GenericRelation(RelationModel)
|
|
|
|
class Meta:
|
|
abstract = True
|
|
|
|
class ModelAbstract(models.Model):
|
|
field = models.CharField(max_length=100)
|
|
|
|
class Meta:
|
|
abstract = True
|
|
|
|
class OverrideRelatedModelAbstract(RelatedModelAbstract):
|
|
field = models.CharField(max_length=100)
|
|
|
|
class ExtendModelAbstract(ModelAbstract):
|
|
field = GenericRelation(RelationModel)
|
|
|
|
self.assertIsInstance(
|
|
OverrideRelatedModelAbstract._meta.get_field("field"), models.CharField
|
|
)
|
|
self.assertIsInstance(
|
|
ExtendModelAbstract._meta.get_field("field"), GenericRelation
|
|
)
|
|
|
|
def test_cannot_override_indirect_abstract_field(self):
|
|
class AbstractBase(models.Model):
|
|
name = models.CharField(max_length=30)
|
|
|
|
class Meta:
|
|
abstract = True
|
|
|
|
class ConcreteDescendant(AbstractBase):
|
|
pass
|
|
|
|
msg = (
|
|
"Local field 'name' in class 'Descendant' clashes with field of "
|
|
"the same name from base class 'ConcreteDescendant'."
|
|
)
|
|
with self.assertRaisesMessage(FieldError, msg):
|
|
|
|
class Descendant(ConcreteDescendant):
|
|
name = models.IntegerField()
|
|
|
|
def test_override_field_with_attr(self):
|
|
class AbstractBase(models.Model):
|
|
first_name = models.CharField(max_length=50)
|
|
last_name = models.CharField(max_length=50)
|
|
middle_name = models.CharField(max_length=30)
|
|
full_name = models.CharField(max_length=150)
|
|
|
|
class Meta:
|
|
abstract = True
|
|
|
|
class Descendant(AbstractBase):
|
|
middle_name = None
|
|
|
|
def full_name(self):
|
|
return self.first_name + self.last_name
|
|
|
|
msg = "Descendant has no field named %r"
|
|
with self.assertRaisesMessage(FieldDoesNotExist, msg % "middle_name"):
|
|
Descendant._meta.get_field("middle_name")
|
|
|
|
with self.assertRaisesMessage(FieldDoesNotExist, msg % "full_name"):
|
|
Descendant._meta.get_field("full_name")
|
|
|
|
def test_overriding_field_removed_by_concrete_model(self):
|
|
class AbstractModel(models.Model):
|
|
foo = models.CharField(max_length=30)
|
|
|
|
class Meta:
|
|
abstract = True
|
|
|
|
class RemovedAbstractModelField(AbstractModel):
|
|
foo = None
|
|
|
|
class OverrideRemovedFieldByConcreteModel(RemovedAbstractModelField):
|
|
foo = models.CharField(max_length=50)
|
|
|
|
self.assertEqual(
|
|
OverrideRemovedFieldByConcreteModel._meta.get_field("foo").max_length, 50
|
|
)
|
|
|
|
def test_shadowed_fkey_id(self):
|
|
class Foo(models.Model):
|
|
pass
|
|
|
|
class AbstractBase(models.Model):
|
|
foo = models.ForeignKey(Foo, models.CASCADE)
|
|
|
|
class Meta:
|
|
abstract = True
|
|
|
|
class Descendant(AbstractBase):
|
|
foo_id = models.IntegerField()
|
|
|
|
self.assertEqual(
|
|
Descendant.check(),
|
|
[
|
|
Error(
|
|
"The field 'foo_id' clashes with the field 'foo' "
|
|
"from model 'model_inheritance.descendant'.",
|
|
obj=Descendant._meta.get_field("foo_id"),
|
|
id="models.E006",
|
|
)
|
|
],
|
|
)
|
|
|
|
def test_shadow_related_name_when_set_to_none(self):
|
|
class AbstractBase(models.Model):
|
|
bar = models.IntegerField()
|
|
|
|
class Meta:
|
|
abstract = True
|
|
|
|
class Foo(AbstractBase):
|
|
bar = None
|
|
foo = models.IntegerField()
|
|
|
|
class Bar(models.Model):
|
|
bar = models.ForeignKey(Foo, models.CASCADE, related_name="bar")
|
|
|
|
self.assertEqual(Bar.check(), [])
|
|
|
|
def test_reverse_foreign_key(self):
|
|
class AbstractBase(models.Model):
|
|
foo = models.CharField(max_length=100)
|
|
|
|
class Meta:
|
|
abstract = True
|
|
|
|
class Descendant(AbstractBase):
|
|
pass
|
|
|
|
class Foo(models.Model):
|
|
foo = models.ForeignKey(Descendant, models.CASCADE, related_name="foo")
|
|
|
|
self.assertEqual(
|
|
Foo._meta.get_field("foo").check(),
|
|
[
|
|
Error(
|
|
"Reverse accessor 'Descendant.foo' for "
|
|
"'model_inheritance.Foo.foo' clashes with field name "
|
|
"'model_inheritance.Descendant.foo'.",
|
|
hint=(
|
|
"Rename field 'model_inheritance.Descendant.foo', or "
|
|
"add/change a related_name argument to the definition "
|
|
"for field 'model_inheritance.Foo.foo'."
|
|
),
|
|
obj=Foo._meta.get_field("foo"),
|
|
id="fields.E302",
|
|
),
|
|
Error(
|
|
"Reverse query name for 'model_inheritance.Foo.foo' "
|
|
"clashes with field name "
|
|
"'model_inheritance.Descendant.foo'.",
|
|
hint=(
|
|
"Rename field 'model_inheritance.Descendant.foo', or "
|
|
"add/change a related_name argument to the definition "
|
|
"for field 'model_inheritance.Foo.foo'."
|
|
),
|
|
obj=Foo._meta.get_field("foo"),
|
|
id="fields.E303",
|
|
),
|
|
],
|
|
)
|
|
|
|
def test_multi_inheritance_field_clashes(self):
|
|
class AbstractBase(models.Model):
|
|
name = models.CharField(max_length=30)
|
|
|
|
class Meta:
|
|
abstract = True
|
|
|
|
class ConcreteBase(AbstractBase):
|
|
pass
|
|
|
|
class AbstractDescendant(ConcreteBase):
|
|
class Meta:
|
|
abstract = True
|
|
|
|
class ConcreteDescendant(AbstractDescendant):
|
|
name = models.CharField(max_length=100)
|
|
|
|
self.assertEqual(
|
|
ConcreteDescendant.check(),
|
|
[
|
|
Error(
|
|
"The field 'name' clashes with the field 'name' from "
|
|
"model 'model_inheritance.concretebase'.",
|
|
obj=ConcreteDescendant._meta.get_field("name"),
|
|
id="models.E006",
|
|
)
|
|
],
|
|
)
|
|
|
|
def test_override_one2one_relation_auto_field_clashes(self):
|
|
class ConcreteParent(models.Model):
|
|
name = models.CharField(max_length=255)
|
|
|
|
class AbstractParent(models.Model):
|
|
name = models.IntegerField()
|
|
|
|
class Meta:
|
|
abstract = True
|
|
|
|
msg = (
|
|
"Auto-generated field 'concreteparent_ptr' in class 'Descendant' "
|
|
"for parent_link to base class 'ConcreteParent' clashes with "
|
|
"declared field of the same name."
|
|
)
|
|
with self.assertRaisesMessage(FieldError, msg):
|
|
|
|
class Descendant(ConcreteParent, AbstractParent):
|
|
concreteparent_ptr = models.CharField(max_length=30)
|
|
|
|
def test_abstract_model_with_regular_python_mixin_mro(self):
|
|
class AbstractModel(models.Model):
|
|
name = models.CharField(max_length=255)
|
|
age = models.IntegerField()
|
|
|
|
class Meta:
|
|
abstract = True
|
|
|
|
class Mixin:
|
|
age = None
|
|
|
|
class Mixin2:
|
|
age = 2
|
|
|
|
class DescendantMixin(Mixin):
|
|
pass
|
|
|
|
class ConcreteModel(models.Model):
|
|
foo = models.IntegerField()
|
|
|
|
class ConcreteModel2(ConcreteModel):
|
|
age = models.SmallIntegerField()
|
|
|
|
def fields(model):
|
|
if not hasattr(model, "_meta"):
|
|
return []
|
|
return [(f.name, f.__class__) for f in model._meta.get_fields()]
|
|
|
|
model_dict = {"__module__": "model_inheritance"}
|
|
model1 = type("Model1", (AbstractModel, Mixin), model_dict.copy())
|
|
model2 = type("Model2", (Mixin2, AbstractModel), model_dict.copy())
|
|
model3 = type("Model3", (DescendantMixin, AbstractModel), model_dict.copy())
|
|
model4 = type("Model4", (Mixin2, Mixin, AbstractModel), model_dict.copy())
|
|
model5 = type(
|
|
"Model5", (Mixin2, ConcreteModel2, Mixin, AbstractModel), model_dict.copy()
|
|
)
|
|
|
|
self.assertEqual(
|
|
fields(model1),
|
|
[
|
|
("id", models.AutoField),
|
|
("name", models.CharField),
|
|
("age", models.IntegerField),
|
|
],
|
|
)
|
|
|
|
self.assertEqual(
|
|
fields(model2), [("id", models.AutoField), ("name", models.CharField)]
|
|
)
|
|
self.assertEqual(getattr(model2, "age"), 2)
|
|
|
|
self.assertEqual(
|
|
fields(model3), [("id", models.AutoField), ("name", models.CharField)]
|
|
)
|
|
|
|
self.assertEqual(
|
|
fields(model4), [("id", models.AutoField), ("name", models.CharField)]
|
|
)
|
|
self.assertEqual(getattr(model4, "age"), 2)
|
|
|
|
self.assertEqual(
|
|
fields(model5),
|
|
[
|
|
("id", models.AutoField),
|
|
("foo", models.IntegerField),
|
|
("concretemodel_ptr", models.OneToOneField),
|
|
("age", models.SmallIntegerField),
|
|
("concretemodel2_ptr", models.OneToOneField),
|
|
("name", models.CharField),
|
|
],
|
|
)
|