mirror of
https://github.com/django/django.git
synced 2025-06-05 03:29:12 +00:00
Split up contenttypes_tests.
This commit is contained in:
parent
29f607927f
commit
aa14528910
218
tests/contenttypes_tests/test_checks.py
Normal file
218
tests/contenttypes_tests/test_checks.py
Normal file
@ -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',
|
||||||
|
)
|
||||||
|
])
|
13
tests/contenttypes_tests/test_fields.py
Normal file
13
tests/contenttypes_tests/test_fields.py
Normal file
@ -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')
|
65
tests/contenttypes_tests/test_management.py
Normal file
65
tests/contenttypes_tests/test_management.py
Normal file
@ -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)
|
@ -1,11 +1,12 @@
|
|||||||
from django.contrib.contenttypes.models import ContentType, ContentTypeManager
|
from django.contrib.contenttypes.models import ContentType, ContentTypeManager
|
||||||
from django.contrib.contenttypes.views import shortcut
|
from django.contrib.contenttypes.views import shortcut
|
||||||
from django.contrib.sites.shortcuts import get_current_site
|
from django.contrib.sites.shortcuts import get_current_site
|
||||||
|
from django.db import connections
|
||||||
from django.http import Http404, HttpRequest
|
from django.http import Http404, HttpRequest
|
||||||
from django.test import TestCase, override_settings
|
from django.test import TestCase, override_settings
|
||||||
|
|
||||||
from .models import (
|
from .models import (
|
||||||
ConcreteModel, FooWithBrokenAbsoluteUrl, FooWithoutUrl, FooWithUrl,
|
Author, ConcreteModel, FooWithBrokenAbsoluteUrl, FooWithoutUrl, FooWithUrl,
|
||||||
ProxyModel,
|
ProxyModel,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -20,11 +21,10 @@ class ContentTypesTests(TestCase):
|
|||||||
|
|
||||||
def test_lookup_cache(self):
|
def test_lookup_cache(self):
|
||||||
"""
|
"""
|
||||||
Make sure that the content type cache (see ContentTypeManager)
|
The content type cache (see ContentTypeManager) works correctly.
|
||||||
works correctly. Lookups for a particular content type -- by model, ID
|
Lookups for a particular content type -- by model, ID, or natural key
|
||||||
or natural key -- should hit the database only on the first lookup.
|
-- should hit the database only on the first lookup.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# At this point, a lookup for a ContentType should hit the DB
|
# At this point, a lookup for a ContentType should hit the DB
|
||||||
with self.assertNumQueries(1):
|
with self.assertNumQueries(1):
|
||||||
ContentType.objects.get_for_model(ContentType)
|
ContentType.objects.get_for_model(ContentType)
|
||||||
@ -244,8 +244,35 @@ class ContentTypesTests(TestCase):
|
|||||||
self.assertEqual(str(ct), 'OldModel')
|
self.assertEqual(str(ct), 'OldModel')
|
||||||
self.assertIsNone(ct.model_class())
|
self.assertIsNone(ct.model_class())
|
||||||
|
|
||||||
# Make sure stale ContentTypes can be fetched like any other object.
|
# 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.
|
|
||||||
ct_fetched = ContentType.objects.get_for_id(ct.pk)
|
ct_fetched = ContentType.objects.get_for_id(ct.pk)
|
||||||
self.assertIsNone(ct_fetched.model_class())
|
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)
|
||||||
|
66
tests/contenttypes_tests/test_operations.py
Normal file
66
tests/contenttypes_tests/test_operations.py
Normal file
@ -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())
|
121
tests/contenttypes_tests/test_views.py
Normal file
121
tests/contenttypes_tests/test_views.py
Normal file
@ -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')
|
@ -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())
|
|
Loading…
x
Reference in New Issue
Block a user