diff --git a/tests/contenttypes_tests/test_checks.py b/tests/contenttypes_tests/test_checks.py new file mode 100644 index 00000000000..6b33107de2e --- /dev/null +++ b/tests/contenttypes_tests/test_checks.py @@ -0,0 +1,218 @@ +from unittest import mock + +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): + + 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', + ) + ]) diff --git a/tests/contenttypes_tests/test_fields.py b/tests/contenttypes_tests/test_fields.py new file mode 100644 index 00000000000..88de0ec7f20 --- /dev/null +++ b/tests/contenttypes_tests/test_fields.py @@ -0,0 +1,13 @@ +from django.contrib.contenttypes.fields import GenericForeignKey +from django.db import models +from django.test import SimpleTestCase +from django.test.utils import isolate_apps + + +@isolate_apps('contenttypes_tests') +class GenericForeignKeyTests(SimpleTestCase): + + def test_str(self): + class Model(models.Model): + field = GenericForeignKey() + self.assertEqual(str(Model.field), 'contenttypes_tests.Model.field') diff --git a/tests/contenttypes_tests/test_management.py b/tests/contenttypes_tests/test_management.py new file mode 100644 index 00000000000..110d1836332 --- /dev/null +++ b/tests/contenttypes_tests/test_management.py @@ -0,0 +1,65 @@ +from unittest import mock + +from django.apps.registry import Apps, apps +from django.contrib.contenttypes import management as contenttypes_management +from django.contrib.contenttypes.models import ContentType +from django.core.management import call_command +from django.test import TestCase +from django.test.utils import captured_stdout + +from .models import ModelWithNullFKToSite, Post + + +class UpdateContentTypesTests(TestCase): + def setUp(self): + self.before_count = ContentType.objects.count() + self.content_type = ContentType.objects.create(app_label='contenttypes_tests', model='Fake') + self.app_config = apps.get_app_config('contenttypes_tests') + + def test_interactive_true_with_dependent_objects(self): + """ + interactive mode of remove_stale_contenttypes (the default) deletes + stale contenttypes and warn of dependent objects. + """ + post = Post.objects.create(title='post', content_type=self.content_type) + # A related object is needed to show that a custom collector with + # can_fast_delete=False is needed. + ModelWithNullFKToSite.objects.create(post=post) + with mock.patch('builtins.input', return_value='yes'): + with captured_stdout() as stdout: + call_command('remove_stale_contenttypes', verbosity=2, stdout=stdout) + self.assertEqual(Post.objects.count(), 0) + output = stdout.getvalue() + self.assertIn('- Content type for contenttypes_tests.Fake', output) + self.assertIn('- 1 contenttypes_tests.Post object(s)', output) + self.assertIn('- 1 contenttypes_tests.ModelWithNullFKToSite', output) + self.assertIn('Deleting stale content type', output) + self.assertEqual(ContentType.objects.count(), self.before_count) + + def test_interactive_true_without_dependent_objects(self): + """ + interactive mode of remove_stale_contenttypes (the default) deletes + stale contenttypes even if there aren't any dependent objects. + """ + with mock.patch('builtins.input', return_value='yes'): + with captured_stdout() as stdout: + call_command('remove_stale_contenttypes', verbosity=2) + self.assertIn("Deleting stale content type", stdout.getvalue()) + self.assertEqual(ContentType.objects.count(), self.before_count) + + def test_interactive_false(self): + """ + non-interactive mode of remove_stale_contenttypes doesn't delete + stale content types. + """ + with captured_stdout() as stdout: + call_command('remove_stale_contenttypes', interactive=False, verbosity=2) + self.assertIn("Stale content types remain.", stdout.getvalue()) + self.assertEqual(ContentType.objects.count(), self.before_count + 1) + + def test_unavailable_content_type_model(self): + """A ContentType isn't created if the model isn't available.""" + apps = Apps() + with self.assertNumQueries(0): + contenttypes_management.create_contenttypes(self.app_config, interactive=False, verbosity=0, apps=apps) + self.assertEqual(ContentType.objects.count(), self.before_count + 1) diff --git a/tests/contenttypes_tests/test_models.py b/tests/contenttypes_tests/test_models.py index 4c16e0b4aa0..a9361c262f3 100644 --- a/tests/contenttypes_tests/test_models.py +++ b/tests/contenttypes_tests/test_models.py @@ -1,11 +1,12 @@ from django.contrib.contenttypes.models import ContentType, ContentTypeManager from django.contrib.contenttypes.views import shortcut from django.contrib.sites.shortcuts import get_current_site +from django.db import connections from django.http import Http404, HttpRequest from django.test import TestCase, override_settings from .models import ( - ConcreteModel, FooWithBrokenAbsoluteUrl, FooWithoutUrl, FooWithUrl, + Author, ConcreteModel, FooWithBrokenAbsoluteUrl, FooWithoutUrl, FooWithUrl, ProxyModel, ) @@ -20,11 +21,10 @@ class ContentTypesTests(TestCase): def test_lookup_cache(self): """ - Make sure that the content type cache (see ContentTypeManager) - works correctly. Lookups for a particular content type -- by model, ID - or natural key -- should hit the database only on the first lookup. + The content type cache (see ContentTypeManager) works correctly. + Lookups for a particular content type -- by model, ID, or natural key + -- should hit the database only on the first lookup. """ - # At this point, a lookup for a ContentType should hit the DB with self.assertNumQueries(1): ContentType.objects.get_for_model(ContentType) @@ -244,8 +244,35 @@ class ContentTypesTests(TestCase): self.assertEqual(str(ct), 'OldModel') self.assertIsNone(ct.model_class()) - # Make sure stale ContentTypes can be fetched like any other object. - # Before Django 1.6 this caused a NoneType error in the caching mechanism. - # Instead, just return the ContentType object and let the app detect stale states. + # Stale ContentTypes can be fetched like any other object. ct_fetched = ContentType.objects.get_for_id(ct.pk) self.assertIsNone(ct_fetched.model_class()) + + +class TestRouter: + def db_for_read(self, model, **hints): + return 'other' + + def db_for_write(self, model, **hints): + return 'default' + + +@override_settings(DATABASE_ROUTERS=[TestRouter()]) +class ContentTypesMultidbTests(TestCase): + + def setUp(self): + # When a test starts executing, only the "default" database is + # connected. Connect to the "other" database here because otherwise it + # will be connected later when it's queried. Some database backends + # perform extra queries upon connecting (MySQL executes + # "SET SQL_AUTO_IS_NULL = 0"), which will affect assertNumQueries(). + connections['other'].ensure_connection() + + def test_multidb(self): + """ + When using multiple databases, ContentType.objects.get_for_model() uses + db_for_read(). + """ + ContentType.objects.clear_cache() + with self.assertNumQueries(0, using='default'), self.assertNumQueries(1, using='other'): + ContentType.objects.get_for_model(Author) diff --git a/tests/contenttypes_tests/test_operations.py b/tests/contenttypes_tests/test_operations.py new file mode 100644 index 00000000000..bdf492fe2a7 --- /dev/null +++ b/tests/contenttypes_tests/test_operations.py @@ -0,0 +1,66 @@ +from django.apps.registry import apps +from django.conf import settings +from django.contrib.contenttypes import management as contenttypes_management +from django.contrib.contenttypes.models import ContentType +from django.core.management import call_command +from django.db import migrations, models +from django.test import TransactionTestCase, override_settings + + +@override_settings( + MIGRATION_MODULES=dict( + settings.MIGRATION_MODULES, + contenttypes_tests='contenttypes_tests.operations_migrations', + ), +) +class ContentTypeOperationsTests(TransactionTestCase): + available_apps = [ + 'contenttypes_tests', + 'django.contrib.contenttypes', + ] + + def setUp(self): + app_config = apps.get_app_config('contenttypes_tests') + models.signals.post_migrate.connect(self.assertOperationsInjected, sender=app_config) + + def tearDown(self): + app_config = apps.get_app_config('contenttypes_tests') + models.signals.post_migrate.disconnect(self.assertOperationsInjected, sender=app_config) + + def assertOperationsInjected(self, plan, **kwargs): + for migration, _backward in plan: + operations = iter(migration.operations) + for operation in operations: + if isinstance(operation, migrations.RenameModel): + next_operation = next(operations) + self.assertIsInstance(next_operation, contenttypes_management.RenameContentType) + self.assertEqual(next_operation.app_label, migration.app_label) + self.assertEqual(next_operation.old_model, operation.old_name_lower) + self.assertEqual(next_operation.new_model, operation.new_name_lower) + + def test_existing_content_type_rename(self): + ContentType.objects.create(app_label='contenttypes_tests', model='foo') + call_command('migrate', 'contenttypes_tests', database='default', interactive=False, verbosity=0,) + self.assertFalse(ContentType.objects.filter(app_label='contenttypes_tests', model='foo').exists()) + self.assertTrue(ContentType.objects.filter(app_label='contenttypes_tests', model='renamedfoo').exists()) + call_command('migrate', 'contenttypes_tests', 'zero', database='default', interactive=False, verbosity=0) + self.assertTrue(ContentType.objects.filter(app_label='contenttypes_tests', model='foo').exists()) + self.assertFalse(ContentType.objects.filter(app_label='contenttypes_tests', model='renamedfoo').exists()) + + def test_missing_content_type_rename_ignore(self): + call_command('migrate', 'contenttypes_tests', database='default', interactive=False, verbosity=0,) + self.assertFalse(ContentType.objects.filter(app_label='contenttypes_tests', model='foo').exists()) + self.assertTrue(ContentType.objects.filter(app_label='contenttypes_tests', model='renamedfoo').exists()) + call_command('migrate', 'contenttypes_tests', 'zero', database='default', interactive=False, verbosity=0) + self.assertTrue(ContentType.objects.filter(app_label='contenttypes_tests', model='foo').exists()) + self.assertFalse(ContentType.objects.filter(app_label='contenttypes_tests', model='renamedfoo').exists()) + + def test_content_type_rename_conflict(self): + ContentType.objects.create(app_label='contenttypes_tests', model='foo') + ContentType.objects.create(app_label='contenttypes_tests', model='renamedfoo') + call_command('migrate', 'contenttypes_tests', database='default', interactive=False, verbosity=0) + self.assertTrue(ContentType.objects.filter(app_label='contenttypes_tests', model='foo').exists()) + self.assertTrue(ContentType.objects.filter(app_label='contenttypes_tests', model='renamedfoo').exists()) + call_command('migrate', 'contenttypes_tests', 'zero', database='default', interactive=False, verbosity=0) + self.assertTrue(ContentType.objects.filter(app_label='contenttypes_tests', model='foo').exists()) + self.assertTrue(ContentType.objects.filter(app_label='contenttypes_tests', model='renamedfoo').exists()) diff --git a/tests/contenttypes_tests/test_views.py b/tests/contenttypes_tests/test_views.py new file mode 100644 index 00000000000..d1165c6818a --- /dev/null +++ b/tests/contenttypes_tests/test_views.py @@ -0,0 +1,121 @@ +import datetime +from unittest import mock + +from django.contrib.contenttypes.models import ContentType +from django.contrib.sites.models import Site +from django.db import models +from django.test import TestCase, override_settings +from django.test.utils import isolate_apps + +from .models import ( + Article, Author, ModelWithNullFKToSite, SchemeIncludedURL, + Site as MockSite, +) + + +@override_settings(ROOT_URLCONF='contenttypes_tests.urls') +class ContentTypesViewsTests(TestCase): + + @classmethod + def setUpTestData(cls): + # Don't use the manager to ensure the site exists with pk=1, regardless + # of whether or not it already exists. + cls.site1 = Site(pk=1, domain='testserver', name='testserver') + cls.site1.save() + cls.author1 = Author.objects.create(name='Boris') + cls.article1 = Article.objects.create( + title='Old Article', slug='old_article', author=cls.author1, + date_created=datetime.datetime(2001, 1, 1, 21, 22, 23), + ) + cls.article2 = Article.objects.create( + title='Current Article', slug='current_article', author=cls.author1, + date_created=datetime.datetime(2007, 9, 17, 21, 22, 23), + ) + cls.article3 = Article.objects.create( + title='Future Article', slug='future_article', author=cls.author1, + date_created=datetime.datetime(3000, 1, 1, 21, 22, 23), + ) + cls.scheme1 = SchemeIncludedURL.objects.create(url='http://test_scheme_included_http/') + cls.scheme2 = SchemeIncludedURL.objects.create(url='https://test_scheme_included_https/') + cls.scheme3 = SchemeIncludedURL.objects.create(url='//test_default_scheme_kept/') + + def setUp(self): + Site.objects.clear_cache() + + def test_shortcut_with_absolute_url(self): + "Can view a shortcut for an Author object that has a get_absolute_url method" + for obj in Author.objects.all(): + short_url = '/shortcut/%s/%s/' % (ContentType.objects.get_for_model(Author).id, obj.pk) + response = self.client.get(short_url) + self.assertRedirects(response, 'http://testserver%s' % obj.get_absolute_url(), target_status_code=404) + + def test_shortcut_with_absolute_url_including_scheme(self): + """ + Can view a shortcut when object's get_absolute_url returns a full URL + the tested URLs are: "http://...", "https://..." and "//..." + """ + for obj in SchemeIncludedURL.objects.all(): + short_url = '/shortcut/%s/%s/' % (ContentType.objects.get_for_model(SchemeIncludedURL).id, obj.pk) + response = self.client.get(short_url) + self.assertRedirects(response, obj.get_absolute_url(), fetch_redirect_response=False) + + def test_shortcut_no_absolute_url(self): + """ + Shortcuts for an object that has no get_absolute_url() method raise + 404. + """ + for obj in Article.objects.all(): + short_url = '/shortcut/%s/%s/' % (ContentType.objects.get_for_model(Article).id, obj.pk) + response = self.client.get(short_url) + self.assertEqual(response.status_code, 404) + + def test_wrong_type_pk(self): + short_url = '/shortcut/%s/%s/' % (ContentType.objects.get_for_model(Author).id, 'nobody/expects') + response = self.client.get(short_url) + self.assertEqual(response.status_code, 404) + + def test_shortcut_bad_pk(self): + short_url = '/shortcut/%s/%s/' % (ContentType.objects.get_for_model(Author).id, '42424242') + response = self.client.get(short_url) + self.assertEqual(response.status_code, 404) + + def test_nonint_content_type(self): + an_author = Author.objects.all()[0] + short_url = '/shortcut/%s/%s/' % ('spam', an_author.pk) + response = self.client.get(short_url) + self.assertEqual(response.status_code, 404) + + def test_bad_content_type(self): + an_author = Author.objects.all()[0] + short_url = '/shortcut/%s/%s/' % (42424242, an_author.pk) + response = self.client.get(short_url) + self.assertEqual(response.status_code, 404) + + @mock.patch('django.apps.apps.get_model') + def test_shortcut_view_with_null_site_fk(self, get_model): + """ + The shortcut view works if a model's ForeignKey to site is None. + """ + get_model.side_effect = lambda *args, **kwargs: MockSite if args[0] == 'sites.Site' else ModelWithNullFKToSite + + obj = ModelWithNullFKToSite.objects.create(title='title') + url = '/shortcut/%s/%s/' % (ContentType.objects.get_for_model(ModelWithNullFKToSite).id, obj.pk) + response = self.client.get(url) + self.assertRedirects(response, '%s' % obj.get_absolute_url(), fetch_redirect_response=False) + + @isolate_apps('contenttypes_tests') + def test_create_contenttype_on_the_spot(self): + """ + ContentTypeManager.get_for_model() creates the corresponding content + type if it doesn't exist in the database. + """ + class ModelCreatedOnTheFly(models.Model): + name = models.CharField() + + class Meta: + verbose_name = 'a model created on the fly' + + ct = ContentType.objects.get_for_model(ModelCreatedOnTheFly) + self.assertEqual(ct.app_label, 'contenttypes_tests') + self.assertEqual(ct.model, 'modelcreatedonthefly') + self.assertEqual(str(ct), 'modelcreatedonthefly') diff --git a/tests/contenttypes_tests/tests.py b/tests/contenttypes_tests/tests.py deleted file mode 100644 index 14e14e09138..00000000000 --- a/tests/contenttypes_tests/tests.py +++ /dev/null @@ -1,530 +0,0 @@ -import datetime -from unittest import mock - -from django.apps.registry import Apps, apps -from django.conf import settings -from django.contrib.contenttypes import management as contenttypes_management -from django.contrib.contenttypes.fields import ( - GenericForeignKey, GenericRelation, -) -from django.contrib.contenttypes.models import ContentType -from django.contrib.sites.models import Site -from django.core import checks, management -from django.core.management import call_command -from django.db import connections, migrations, models -from django.test import ( - SimpleTestCase, TestCase, TransactionTestCase, override_settings, -) -from django.test.utils import captured_stdout, isolate_apps - -from .models import ( - Article, Author, ModelWithNullFKToSite, Post, SchemeIncludedURL, - Site as MockSite, -) - - -@override_settings(ROOT_URLCONF='contenttypes_tests.urls') -class ContentTypesViewsTests(TestCase): - - @classmethod - def setUpTestData(cls): - # don't use the manager because we want to ensure the site exists - # with pk=1, regardless of whether or not it already exists. - cls.site1 = Site(pk=1, domain='testserver', name='testserver') - cls.site1.save() - cls.author1 = Author.objects.create(name='Boris') - cls.article1 = Article.objects.create( - title='Old Article', slug='old_article', author=cls.author1, - date_created=datetime.datetime(2001, 1, 1, 21, 22, 23) - ) - cls.article2 = Article.objects.create( - title='Current Article', slug='current_article', author=cls.author1, - date_created=datetime.datetime(2007, 9, 17, 21, 22, 23) - ) - cls.article3 = Article.objects.create( - title='Future Article', slug='future_article', author=cls.author1, - date_created=datetime.datetime(3000, 1, 1, 21, 22, 23) - ) - cls.scheme1 = SchemeIncludedURL.objects.create(url='http://test_scheme_included_http/') - cls.scheme2 = SchemeIncludedURL.objects.create(url='https://test_scheme_included_https/') - cls.scheme3 = SchemeIncludedURL.objects.create(url='//test_default_scheme_kept/') - - def setUp(self): - Site.objects.clear_cache() - - def test_shortcut_with_absolute_url(self): - "Can view a shortcut for an Author object that has a get_absolute_url method" - for obj in Author.objects.all(): - short_url = '/shortcut/%s/%s/' % (ContentType.objects.get_for_model(Author).id, obj.pk) - response = self.client.get(short_url) - self.assertRedirects(response, 'http://testserver%s' % obj.get_absolute_url(), - status_code=302, target_status_code=404) - - def test_shortcut_with_absolute_url_including_scheme(self): - """ - Can view a shortcut when object's get_absolute_url returns a full URL - the tested URLs are: "http://...", "https://..." and "//..." - """ - for obj in SchemeIncludedURL.objects.all(): - short_url = '/shortcut/%s/%s/' % (ContentType.objects.get_for_model(SchemeIncludedURL).id, obj.pk) - response = self.client.get(short_url) - self.assertRedirects(response, obj.get_absolute_url(), - status_code=302, - fetch_redirect_response=False) - - def test_shortcut_no_absolute_url(self): - "Shortcuts for an object that has no get_absolute_url method raises 404" - for obj in Article.objects.all(): - short_url = '/shortcut/%s/%s/' % (ContentType.objects.get_for_model(Article).id, obj.pk) - response = self.client.get(short_url) - self.assertEqual(response.status_code, 404) - - def test_wrong_type_pk(self): - short_url = '/shortcut/%s/%s/' % (ContentType.objects.get_for_model(Author).id, 'nobody/expects') - response = self.client.get(short_url) - self.assertEqual(response.status_code, 404) - - def test_shortcut_bad_pk(self): - short_url = '/shortcut/%s/%s/' % (ContentType.objects.get_for_model(Author).id, '42424242') - response = self.client.get(short_url) - self.assertEqual(response.status_code, 404) - - def test_nonint_content_type(self): - an_author = Author.objects.all()[0] - short_url = '/shortcut/%s/%s/' % ('spam', an_author.pk) - response = self.client.get(short_url) - self.assertEqual(response.status_code, 404) - - def test_bad_content_type(self): - an_author = Author.objects.all()[0] - short_url = '/shortcut/%s/%s/' % (42424242, an_author.pk) - response = self.client.get(short_url) - self.assertEqual(response.status_code, 404) - - @mock.patch('django.apps.apps.get_model') - def test_shortcut_view_with_null_site_fk(self, get_model): - """ - The shortcut view works if a model's ForeignKey to site is None. - """ - get_model.side_effect = lambda *args, **kwargs: MockSite if args[0] == 'sites.Site' else ModelWithNullFKToSite - - obj = ModelWithNullFKToSite.objects.create(title='title') - url = '/shortcut/%s/%s/' % (ContentType.objects.get_for_model(ModelWithNullFKToSite).id, obj.pk) - response = self.client.get(url) - self.assertRedirects( - response, '%s' % obj.get_absolute_url(), - fetch_redirect_response=False, - ) - - def test_create_contenttype_on_the_spot(self): - """ - Make sure ContentTypeManager.get_for_model creates the corresponding - content type if it doesn't exist in the database (for some reason). - """ - - class ModelCreatedOnTheFly(models.Model): - name = models.CharField() - - class Meta: - verbose_name = 'a model created on the fly' - app_label = 'my_great_app' - apps = Apps() - - ct = ContentType.objects.get_for_model(ModelCreatedOnTheFly) - self.assertEqual(ct.app_label, 'my_great_app') - self.assertEqual(ct.model, 'modelcreatedonthefly') - self.assertEqual(str(ct), 'modelcreatedonthefly') - - -@override_settings(SILENCED_SYSTEM_CHECKS=['fields.W342']) # ForeignKey(unique=True) -@isolate_apps('contenttypes_tests', attr_name='apps') -class GenericForeignKeyTests(SimpleTestCase): - - def test_str(self): - class Model(models.Model): - field = GenericForeignKey() - self.assertEqual(str(Model.field), "contenttypes_tests.Model.field") - - def test_missing_content_type_field(self): - class TaggedItem(models.Model): - # no content_type field - object_id = models.PositiveIntegerField() - content_object = GenericForeignKey() - - errors = TaggedItem.content_object.check() - expected = [ - checks.Error( - "The GenericForeignKey content type references the nonexistent field 'TaggedItem.content_type'.", - obj=TaggedItem.content_object, - id='contenttypes.E002', - ) - ] - self.assertEqual(errors, 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') - - errors = Model.content_object.check() - expected = [ - 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', - ) - ] - self.assertEqual(errors, expected) - - 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') - - errors = Model.content_object.check() - expected = [ - 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', - ) - ] - self.assertEqual(errors, expected) - - 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() - - errors = TaggedItem.content_object.check() - expected = [ - checks.Error( - "The GenericForeignKey object ID references the nonexistent field 'object_id'.", - obj=TaggedItem.content_object, - id='contenttypes.E001', - ) - ] - self.assertEqual(errors, expected) - - 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') - - errors = Model.content_object_.check() - expected = [ - checks.Error( - 'Field names must not end with an underscore.', - obj=Model.content_object_, - id='fields.E001', - ) - ] - self.assertEqual(errors, expected) - - @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 GenericRelationshipTests(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') - - errors = Bookmark.tags.field.check() - self.assertEqual(errors, []) - - 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', - ) - - errors = Bookmark.tags.field.check() - self.assertEqual(errors, []) - - def test_pointing_to_missing_model(self): - class Model(models.Model): - rel = GenericRelation('MissingModel') - - errors = Model.rel.field.check() - expected = [ - checks.Error( - "Field defines a relation with model 'MissingModel', " - "which is either not installed, or is abstract.", - obj=Model.rel.field, - id='fields.E300', - ) - ] - self.assertEqual(errors, expected) - - 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') - - errors = Model.rel.field.check() - self.assertEqual(errors, []) - - 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') - - errors = Bookmark.tags.field.check() - expected = [ - 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', - ) - ] - self.assertEqual(errors, expected) - - @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') - - errors = Model.rel.field.check() - expected = [ - 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', - ) - ] - self.assertEqual(errors, expected) - - 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') - - errors = InvalidBookmark.tags_.field.check() - expected = [ - checks.Error( - 'Field names must not end with an underscore.', - obj=InvalidBookmark.tags_.field, - id='fields.E001', - ) - ] - self.assertEqual(errors, expected) - - -class UpdateContentTypesTests(TestCase): - def setUp(self): - self.before_count = ContentType.objects.count() - self.content_type = ContentType.objects.create(app_label='contenttypes_tests', model='Fake') - self.app_config = apps.get_app_config('contenttypes_tests') - - def test_interactive_true_with_dependent_objects(self): - """ - interactive mode of remove_stale_contenttypes (the default) should - delete stale contenttypes and warn of dependent objects. - """ - post = Post.objects.create(title='post', content_type=self.content_type) - # A related object is needed to show that a custom collector with - # can_fast_delete=False is needed. - ModelWithNullFKToSite.objects.create(post=post) - with mock.patch('builtins.input', return_value='yes'): - with captured_stdout() as stdout: - call_command('remove_stale_contenttypes', verbosity=2, stdout=stdout) - self.assertEqual(Post.objects.count(), 0) - output = stdout.getvalue() - self.assertIn('- Content type for contenttypes_tests.Fake', output) - self.assertIn('- 1 contenttypes_tests.Post object(s)', output) - self.assertIn('- 1 contenttypes_tests.ModelWithNullFKToSite', output) - self.assertIn('Deleting stale content type', output) - self.assertEqual(ContentType.objects.count(), self.before_count) - - def test_interactive_true_without_dependent_objects(self): - """ - interactive mode of remove_stale_contenttypes (the default) should - delete stale contenttypes even if there aren't any dependent objects. - """ - with mock.patch('builtins.input', return_value='yes'): - with captured_stdout() as stdout: - call_command('remove_stale_contenttypes', verbosity=2) - self.assertIn("Deleting stale content type", stdout.getvalue()) - self.assertEqual(ContentType.objects.count(), self.before_count) - - def test_interactive_false(self): - """ - non-interactive mode of remove_stale_contenttypes shouldn't delete - stale content types. - """ - with captured_stdout() as stdout: - call_command('remove_stale_contenttypes', interactive=False, verbosity=2) - self.assertIn("Stale content types remain.", stdout.getvalue()) - self.assertEqual(ContentType.objects.count(), self.before_count + 1) - - def test_unavailable_content_type_model(self): - """ - A ContentType shouldn't be created if the model isn't available. - """ - apps = Apps() - with self.assertNumQueries(0): - contenttypes_management.create_contenttypes(self.app_config, interactive=False, verbosity=0, apps=apps) - self.assertEqual(ContentType.objects.count(), self.before_count + 1) - - -class TestRouter: - def db_for_read(self, model, **hints): - return 'other' - - def db_for_write(self, model, **hints): - return 'default' - - -@override_settings(DATABASE_ROUTERS=[TestRouter()]) -class ContentTypesMultidbTestCase(TestCase): - - def setUp(self): - # Whenever a test starts executing, only the "default" database is - # connected. We explicitly connect to the "other" database here. If we - # don't do it, then it will be implicitly connected later when we query - # it, but in that case some database backends may automatically perform - # extra queries upon connecting (notably mysql executes - # "SET SQL_AUTO_IS_NULL = 0"), which will affect assertNumQueries(). - connections['other'].ensure_connection() - - def test_multidb(self): - """ - When using multiple databases, ContentType.objects.get_for_model() uses - db_for_read(). - """ - ContentType.objects.clear_cache() - - with self.assertNumQueries(0, using='default'), \ - self.assertNumQueries(1, using='other'): - ContentType.objects.get_for_model(Author) - - -@override_settings( - MIGRATION_MODULES=dict(settings.MIGRATION_MODULES, contenttypes_tests='contenttypes_tests.operations_migrations'), -) -class ContentTypeOperationsTests(TransactionTestCase): - available_apps = [ - 'contenttypes_tests', - 'django.contrib.contenttypes', - ] - - def setUp(self): - app_config = apps.get_app_config('contenttypes_tests') - models.signals.post_migrate.connect(self.assertOperationsInjected, sender=app_config) - - def tearDown(self): - app_config = apps.get_app_config('contenttypes_tests') - models.signals.post_migrate.disconnect(self.assertOperationsInjected, sender=app_config) - - def assertOperationsInjected(self, plan, **kwargs): - for migration, _backward in plan: - operations = iter(migration.operations) - for operation in operations: - if isinstance(operation, migrations.RenameModel): - next_operation = next(operations) - self.assertIsInstance(next_operation, contenttypes_management.RenameContentType) - self.assertEqual(next_operation.app_label, migration.app_label) - self.assertEqual(next_operation.old_model, operation.old_name_lower) - self.assertEqual(next_operation.new_model, operation.new_name_lower) - - def test_existing_content_type_rename(self): - ContentType.objects.create(app_label='contenttypes_tests', model='foo') - management.call_command( - 'migrate', 'contenttypes_tests', database='default', interactive=False, verbosity=0, - ) - self.assertFalse(ContentType.objects.filter(app_label='contenttypes_tests', model='foo').exists()) - self.assertTrue(ContentType.objects.filter(app_label='contenttypes_tests', model='renamedfoo').exists()) - management.call_command( - 'migrate', 'contenttypes_tests', 'zero', database='default', interactive=False, verbosity=0, - ) - self.assertTrue(ContentType.objects.filter(app_label='contenttypes_tests', model='foo').exists()) - self.assertFalse(ContentType.objects.filter(app_label='contenttypes_tests', model='renamedfoo').exists()) - - def test_missing_content_type_rename_ignore(self): - management.call_command( - 'migrate', 'contenttypes_tests', database='default', interactive=False, verbosity=0, - ) - self.assertFalse(ContentType.objects.filter(app_label='contenttypes_tests', model='foo').exists()) - self.assertTrue(ContentType.objects.filter(app_label='contenttypes_tests', model='renamedfoo').exists()) - management.call_command( - 'migrate', 'contenttypes_tests', 'zero', database='default', interactive=False, verbosity=0, - ) - self.assertTrue(ContentType.objects.filter(app_label='contenttypes_tests', model='foo').exists()) - self.assertFalse(ContentType.objects.filter(app_label='contenttypes_tests', model='renamedfoo').exists()) - - def test_content_type_rename_conflict(self): - ContentType.objects.create(app_label='contenttypes_tests', model='foo') - ContentType.objects.create(app_label='contenttypes_tests', model='renamedfoo') - management.call_command( - 'migrate', 'contenttypes_tests', database='default', interactive=False, verbosity=0, - ) - self.assertTrue(ContentType.objects.filter(app_label='contenttypes_tests', model='foo').exists()) - self.assertTrue(ContentType.objects.filter(app_label='contenttypes_tests', model='renamedfoo').exists()) - management.call_command( - 'migrate', 'contenttypes_tests', 'zero', database='default', interactive=False, verbosity=0, - ) - self.assertTrue(ContentType.objects.filter(app_label='contenttypes_tests', model='foo').exists()) - self.assertTrue(ContentType.objects.filter(app_label='contenttypes_tests', model='renamedfoo').exists())