from unittest import mock from django.contrib.contenttypes.checks import check_model_name_lengths from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation from django.contrib.contenttypes.models import ContentType from django.core import checks from django.db import models from django.test import SimpleTestCase, override_settings from django.test.utils import isolate_apps @isolate_apps("contenttypes_tests", attr_name="apps") class GenericForeignKeyTests(SimpleTestCase): databases = "__all__" def test_missing_content_type_field(self): class TaggedItem(models.Model): # no content_type field object_id = models.PositiveIntegerField() content_object = GenericForeignKey() expected = [ checks.Error( "The GenericForeignKey content type references the nonexistent " "field 'TaggedItem.content_type'.", obj=TaggedItem.content_object, id="contenttypes.E002", ) ] self.assertEqual(TaggedItem.content_object.check(), expected) def test_invalid_content_type_field(self): class Model(models.Model): content_type = models.IntegerField() # should be ForeignKey object_id = models.PositiveIntegerField() content_object = GenericForeignKey("content_type", "object_id") self.assertEqual( Model.content_object.check(), [ checks.Error( "'Model.content_type' is not a ForeignKey.", hint=( "GenericForeignKeys must use a ForeignKey to " "'contenttypes.ContentType' as the 'content_type' field." ), obj=Model.content_object, id="contenttypes.E003", ) ], ) def test_content_type_field_pointing_to_wrong_model(self): class Model(models.Model): content_type = models.ForeignKey( "self", models.CASCADE ) # should point to ContentType object_id = models.PositiveIntegerField() content_object = GenericForeignKey("content_type", "object_id") self.assertEqual( Model.content_object.check(), [ checks.Error( "'Model.content_type' is not a ForeignKey to " "'contenttypes.ContentType'.", hint=( "GenericForeignKeys must use a ForeignKey to " "'contenttypes.ContentType' as the 'content_type' field." ), obj=Model.content_object, id="contenttypes.E004", ) ], ) def test_missing_object_id_field(self): class TaggedItem(models.Model): content_type = models.ForeignKey(ContentType, models.CASCADE) # missing object_id field content_object = GenericForeignKey() self.assertEqual( TaggedItem.content_object.check(), [ checks.Error( "The GenericForeignKey object ID references the nonexistent " "field 'object_id'.", obj=TaggedItem.content_object, id="contenttypes.E001", ) ], ) def test_field_name_ending_with_underscore(self): class Model(models.Model): content_type = models.ForeignKey(ContentType, models.CASCADE) object_id = models.PositiveIntegerField() content_object_ = GenericForeignKey("content_type", "object_id") self.assertEqual( Model.content_object_.check(), [ checks.Error( "Field names must not end with an underscore.", obj=Model.content_object_, id="fields.E001", ) ], ) @override_settings( INSTALLED_APPS=[ "django.contrib.auth", "django.contrib.contenttypes", "contenttypes_tests", ] ) def test_generic_foreign_key_checks_are_performed(self): class Model(models.Model): content_object = GenericForeignKey() with mock.patch.object(GenericForeignKey, "check") as check: checks.run_checks(app_configs=self.apps.get_app_configs()) check.assert_called_once_with() @isolate_apps("contenttypes_tests") class GenericRelationTests(SimpleTestCase): def test_valid_generic_relationship(self): class TaggedItem(models.Model): content_type = models.ForeignKey(ContentType, models.CASCADE) object_id = models.PositiveIntegerField() content_object = GenericForeignKey() class Bookmark(models.Model): tags = GenericRelation("TaggedItem") self.assertEqual(Bookmark.tags.field.check(), []) def test_valid_generic_relationship_with_explicit_fields(self): class TaggedItem(models.Model): custom_content_type = models.ForeignKey(ContentType, models.CASCADE) custom_object_id = models.PositiveIntegerField() content_object = GenericForeignKey( "custom_content_type", "custom_object_id" ) class Bookmark(models.Model): tags = GenericRelation( "TaggedItem", content_type_field="custom_content_type", object_id_field="custom_object_id", ) self.assertEqual(Bookmark.tags.field.check(), []) def test_pointing_to_missing_model(self): class Model(models.Model): rel = GenericRelation("MissingModel") self.assertEqual( Model.rel.field.check(), [ checks.Error( "Field defines a relation with model 'MissingModel', " "which is either not installed, or is abstract.", obj=Model.rel.field, id="fields.E300", ) ], ) def test_valid_self_referential_generic_relationship(self): class Model(models.Model): rel = GenericRelation("Model") content_type = models.ForeignKey(ContentType, models.CASCADE) object_id = models.PositiveIntegerField() content_object = GenericForeignKey("content_type", "object_id") self.assertEqual(Model.rel.field.check(), []) def test_missing_generic_foreign_key(self): class TaggedItem(models.Model): content_type = models.ForeignKey(ContentType, models.CASCADE) object_id = models.PositiveIntegerField() class Bookmark(models.Model): tags = GenericRelation("TaggedItem") self.assertEqual( Bookmark.tags.field.check(), [ checks.Error( "The GenericRelation defines a relation with the model " "'contenttypes_tests.TaggedItem', but that model does not have a " "GenericForeignKey.", obj=Bookmark.tags.field, id="contenttypes.E004", ) ], ) @override_settings(TEST_SWAPPED_MODEL="contenttypes_tests.Replacement") def test_pointing_to_swapped_model(self): class Replacement(models.Model): pass class SwappedModel(models.Model): content_type = models.ForeignKey(ContentType, models.CASCADE) object_id = models.PositiveIntegerField() content_object = GenericForeignKey() class Meta: swappable = "TEST_SWAPPED_MODEL" class Model(models.Model): rel = GenericRelation("SwappedModel") self.assertEqual( Model.rel.field.check(), [ checks.Error( "Field defines a relation with the model " "'contenttypes_tests.SwappedModel', " "which has been swapped out.", hint=( "Update the relation to point at 'settings.TEST_SWAPPED_MODEL'." ), obj=Model.rel.field, id="fields.E301", ) ], ) def test_field_name_ending_with_underscore(self): class TaggedItem(models.Model): content_type = models.ForeignKey(ContentType, models.CASCADE) object_id = models.PositiveIntegerField() content_object = GenericForeignKey() class InvalidBookmark(models.Model): tags_ = GenericRelation("TaggedItem") self.assertEqual( InvalidBookmark.tags_.field.check(), [ checks.Error( "Field names must not end with an underscore.", obj=InvalidBookmark.tags_.field, id="fields.E001", ) ], ) @isolate_apps("contenttypes_tests", attr_name="apps") class ModelCheckTests(SimpleTestCase): def test_model_name_too_long(self): model = type("A" * 101, (models.Model,), {"__module__": self.__module__}) self.assertEqual( check_model_name_lengths(self.apps.get_app_configs()), [ checks.Error( "Model names must be at most 100 characters (got 101).", obj=model, id="contenttypes.E005", ) ], ) def test_model_name_max_length(self): type("A" * 100, (models.Model,), {"__module__": self.__module__}) self.assertEqual(check_model_name_lengths(self.apps.get_app_configs()), [])