tests.py 16.1 KB
Newer Older
1
# -*- coding: utf-8 -*-
2
from __future__ import unicode_literals
3

4
from django.apps.registry import Apps, apps
5
from django.contrib.contenttypes import management
6
from django.contrib.contenttypes.fields import (
7
    GenericForeignKey, GenericRelation,
8
)
9
from django.contrib.contenttypes.models import ContentType
10
from django.core import checks
11 12 13
from django.db import connections, models
from django.test import TestCase, override_settings
from django.test.utils import captured_stdout
14
from django.utils.encoding import force_str, force_text
15

16
from .models import Article, Author, SchemeIncludedURL
17

Jason Myers's avatar
Jason Myers committed
18

19
@override_settings(ROOT_URLCONF='contenttypes_tests.urls')
20 21 22 23 24 25 26 27 28 29 30
class ContentTypesViewsTests(TestCase):
    fixtures = ['testdata.json']

    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)

31 32 33 34 35 36 37 38 39 40 41 42 43
    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 in fixtures/testdata.json :
        "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)

44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
    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)
72 73 74 75 76 77 78 79 80 81 82 83 84

    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'
85
                apps = Apps()
86 87 88 89

        ct = ContentType.objects.get_for_model(ModelCreatedOnTheFly)
        self.assertEqual(ct.app_label, 'my_great_app')
        self.assertEqual(ct.model, 'modelcreatedonthefly')
90
        self.assertEqual(force_text(ct), 'modelcreatedonthefly')
91 92 93 94 95 96 97 98 99 100 101 102 103 104


class IsolatedModelsTestCase(TestCase):
    def setUp(self):
        # The unmanaged models need to be removed after the test in order to
        # prevent bad interactions with the flush operation in other tests.
        self._old_models = apps.app_configs['contenttypes_tests'].models.copy()

    def tearDown(self):
        apps.app_configs['contenttypes_tests'].models = self._old_models
        apps.all_models['contenttypes_tests'] = self._old_models
        apps.clear_cache()


105
@override_settings(SILENCED_SYSTEM_CHECKS=['fields.W342'])  # ForeignKey(unique=True)
106 107 108 109
class GenericForeignKeyTests(IsolatedModelsTestCase):

    def test_str(self):
        class Model(models.Model):
110
            field = GenericForeignKey()
111 112 113 114 115 116 117 118
        expected = "contenttypes_tests.Model.field"
        actual = force_str(Model.field)
        self.assertEqual(expected, actual)

    def test_missing_content_type_field(self):
        class TaggedItem(models.Model):
            # no content_type field
            object_id = models.PositiveIntegerField()
119
            content_object = GenericForeignKey()
120 121 122 123

        errors = TaggedItem.content_object.check()
        expected = [
            checks.Error(
124
                "The GenericForeignKey content type references the non-existent field 'TaggedItem.content_type'.",
125 126
                hint=None,
                obj=TaggedItem.content_object,
127
                id='contenttypes.E002',
128 129 130 131 132 133 134 135
            )
        ]
        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()
136
            content_object = GenericForeignKey(
137 138 139 140 141
                'content_type', 'object_id')

        errors = Model.content_object.check()
        expected = [
            checks.Error(
142 143
                "'Model.content_type' is not a ForeignKey.",
                hint="GenericForeignKeys must use a ForeignKey to 'contenttypes.ContentType' as the 'content_type' field.",
144
                obj=Model.content_object,
145
                id='contenttypes.E003',
146 147 148 149 150 151 152 153
            )
        ]
        self.assertEqual(errors, expected)

    def test_content_type_field_pointing_to_wrong_model(self):
        class Model(models.Model):
            content_type = models.ForeignKey('self')  # should point to ContentType
            object_id = models.PositiveIntegerField()
154
            content_object = GenericForeignKey(
155 156 157 158 159
                'content_type', 'object_id')

        errors = Model.content_object.check()
        expected = [
            checks.Error(
160 161
                "'Model.content_type' is not a ForeignKey to 'contenttypes.ContentType'.",
                hint="GenericForeignKeys must use a ForeignKey to 'contenttypes.ContentType' as the 'content_type' field.",
162
                obj=Model.content_object,
163
                id='contenttypes.E004',
164 165 166 167 168 169 170 171
            )
        ]
        self.assertEqual(errors, expected)

    def test_missing_object_id_field(self):
        class TaggedItem(models.Model):
            content_type = models.ForeignKey(ContentType)
            # missing object_id field
172
            content_object = GenericForeignKey()
173 174 175 176

        errors = TaggedItem.content_object.check()
        expected = [
            checks.Error(
177
                "The GenericForeignKey object ID references the non-existent field 'object_id'.",
178 179
                hint=None,
                obj=TaggedItem.content_object,
180
                id='contenttypes.E001',
181 182 183 184 185 186 187 188
            )
        ]
        self.assertEqual(errors, expected)

    def test_field_name_ending_with_underscore(self):
        class Model(models.Model):
            content_type = models.ForeignKey(ContentType)
            object_id = models.PositiveIntegerField()
189
            content_object_ = GenericForeignKey(
190 191 192 193 194
                'content_type', 'object_id')

        errors = Model.content_object_.check()
        expected = [
            checks.Error(
195
                'Field names must not end with an underscore.',
196 197
                hint=None,
                obj=Model.content_object_,
198
                id='fields.E001',
199 200 201 202
            )
        ]
        self.assertEqual(errors, expected)

203
    @override_settings(INSTALLED_APPS=['django.contrib.auth', 'django.contrib.contenttypes', 'contenttypes_tests'])
204
    def test_generic_foreign_key_checks_are_performed(self):
205
        class MyGenericForeignKey(GenericForeignKey):
206 207 208 209 210 211 212 213 214
            def check(self, **kwargs):
                return ['performed!']

        class Model(models.Model):
            content_object = MyGenericForeignKey()

        errors = checks.run_checks()
        self.assertEqual(errors, ['performed!'])

215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235
    def test_unsaved_instance_on_generic_foreign_key(self):
        """
        #10811 -- Assigning an unsaved object to GenericForeignKey
        should raise an exception.
        """
        class Model(models.Model):
            content_type = models.ForeignKey(ContentType, null=True)
            object_id = models.PositiveIntegerField(null=True)
            content_object = GenericForeignKey('content_type', 'object_id')

        author = Author(name='Author')
        model = Model()
        model.content_object = None   # no error here as content_type allows None
        with self.assertRaisesMessage(ValueError,
                                    'Cannot assign "%r": "%s" instance isn\'t saved in the database.'
                                    % (author, author._meta.object_name)):
            model.content_object = author   # raised ValueError here as author is unsaved

        author.save()
        model.content_object = author   # no error because the instance is saved

236 237 238 239 240 241 242

class GenericRelationshipTests(IsolatedModelsTestCase):

    def test_valid_generic_relationship(self):
        class TaggedItem(models.Model):
            content_type = models.ForeignKey(ContentType)
            object_id = models.PositiveIntegerField()
243
            content_object = GenericForeignKey()
244 245

        class Bookmark(models.Model):
246
            tags = GenericRelation('TaggedItem')
247 248 249 250 251 252 253 254

        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)
            custom_object_id = models.PositiveIntegerField()
255
            content_object = GenericForeignKey(
256 257 258
                'custom_content_type', 'custom_object_id')

        class Bookmark(models.Model):
259
            tags = GenericRelation('TaggedItem',
260 261 262 263 264 265 266 267
                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):
268
            rel = GenericRelation('MissingModel')
269 270 271 272

        errors = Model.rel.field.check()
        expected = [
            checks.Error(
273 274 275
                ("Field defines a relation with model 'MissingModel', "
                 "which is either not installed, or is abstract."),
                hint=None,
276
                obj=Model.rel.field,
277
                id='fields.E300',
278 279 280 281 282 283
            )
        ]
        self.assertEqual(errors, expected)

    def test_valid_self_referential_generic_relationship(self):
        class Model(models.Model):
284
            rel = GenericRelation('Model')
285 286
            content_type = models.ForeignKey(ContentType)
            object_id = models.PositiveIntegerField()
287
            content_object = GenericForeignKey(
288 289 290 291 292 293 294 295 296 297 298
                '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)
            object_id = models.PositiveIntegerField()

        class Bookmark(models.Model):
299
            tags = GenericRelation('TaggedItem')
300 301 302

        errors = Bookmark.tags.field.check()
        expected = [
303
            checks.Error(
304 305 306
                ("The GenericRelation defines a relation with the model "
                 "'contenttypes_tests.TaggedItem', but that model does not have a "
                 "GenericForeignKey."),
307 308 309 310 311 312 313 314 315 316 317 318 319 320 321
                hint=None,
                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)
            object_id = models.PositiveIntegerField()
322
            content_object = GenericForeignKey()
323 324 325 326 327

            class Meta:
                swappable = 'TEST_SWAPPED_MODEL'

        class Model(models.Model):
328
            rel = GenericRelation('SwappedModel')
329 330 331 332

        errors = Model.rel.field.check()
        expected = [
            checks.Error(
333 334 335 336
                ("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'.",
337
                obj=Model.rel.field,
338
                id='fields.E301',
339 340 341 342 343 344 345 346
            )
        ]
        self.assertEqual(errors, expected)

    def test_field_name_ending_with_underscore(self):
        class TaggedItem(models.Model):
            content_type = models.ForeignKey(ContentType)
            object_id = models.PositiveIntegerField()
347
            content_object = GenericForeignKey()
348 349

        class InvalidBookmark(models.Model):
350
            tags_ = GenericRelation('TaggedItem')
351 352 353 354

        errors = InvalidBookmark.tags_.field.check()
        expected = [
            checks.Error(
355
                'Field names must not end with an underscore.',
356 357
                hint=None,
                obj=InvalidBookmark.tags_.field,
358
                id='fields.E001',
359 360 361
            )
        ]
        self.assertEqual(errors, expected)
362 363


364 365 366
class UpdateContentTypesTests(TestCase):
    def setUp(self):
        self.before_count = ContentType.objects.count()
367
        ContentType.objects.create(app_label='contenttypes_tests', model='Fake')
368 369 370 371 372 373 374 375
        self.app_config = apps.get_app_config('contenttypes_tests')

    def test_interactive_true(self):
        """
        interactive mode of update_contenttypes() (the default) should delete
        stale contenttypes.
        """
        management.input = lambda x: force_str("yes")
376 377 378
        with captured_stdout() as stdout:
            management.update_contenttypes(self.app_config)
        self.assertIn("Deleting stale content type", stdout.getvalue())
379 380 381 382 383 384 385
        self.assertEqual(ContentType.objects.count(), self.before_count)

    def test_interactive_false(self):
        """
        non-interactive mode of update_contenttypes() shouldn't delete stale
        content types.
        """
386 387 388
        with captured_stdout() as stdout:
            management.update_contenttypes(self.app_config, interactive=False)
        self.assertIn("Stale content types remain.", stdout.getvalue())
389 390 391
        self.assertEqual(ContentType.objects.count(), self.before_count + 1)


392 393 394 395 396 397 398 399
class TestRouter(object):
    def db_for_read(self, model, **hints):
        return 'other'

    def db_for_write(self, model, **hints):
        return 'default'


400
@override_settings(DATABASE_ROUTERS=[TestRouter()])
401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421
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):
        """
        Test that, when using multiple databases, we use the db_for_read (see
        #20401).
        """
        ContentType.objects.clear_cache()

        with self.assertNumQueries(0, using='default'), \
                self.assertNumQueries(1, using='other'):
            ContentType.objects.get_for_model(Author)