mirror of
				https://github.com/django/django.git
				synced 2025-10-26 07:06:08 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			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),
 | |
|             ],
 | |
|         )
 |