tests.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545
  1. # -*- coding: utf-8 -*-
  2. from __future__ import unicode_literals
  3. import datetime
  4. from django.apps.registry import Apps, apps
  5. from django.conf import settings
  6. from django.contrib.contenttypes import management as contenttypes_management
  7. from django.contrib.contenttypes.fields import (
  8. GenericForeignKey, GenericRelation,
  9. )
  10. from django.contrib.contenttypes.models import ContentType
  11. from django.contrib.sites.models import Site
  12. from django.core import checks, management
  13. from django.core.management import call_command
  14. from django.db import connections, migrations, models
  15. from django.test import (
  16. SimpleTestCase, TestCase, TransactionTestCase, mock, override_settings,
  17. )
  18. from django.test.utils import captured_stdout, isolate_apps
  19. from django.utils.encoding import force_str, force_text
  20. from .models import (
  21. Article, Author, ModelWithNullFKToSite, Post, SchemeIncludedURL,
  22. Site as MockSite,
  23. )
  24. @override_settings(ROOT_URLCONF='contenttypes_tests.urls')
  25. class ContentTypesViewsTests(TestCase):
  26. @classmethod
  27. def setUpTestData(cls):
  28. # don't use the manager because we want to ensure the site exists
  29. # with pk=1, regardless of whether or not it already exists.
  30. cls.site1 = Site(pk=1, domain='testserver', name='testserver')
  31. cls.site1.save()
  32. cls.author1 = Author.objects.create(name='Boris')
  33. cls.article1 = Article.objects.create(
  34. title='Old Article', slug='old_article', author=cls.author1,
  35. date_created=datetime.datetime(2001, 1, 1, 21, 22, 23)
  36. )
  37. cls.article2 = Article.objects.create(
  38. title='Current Article', slug='current_article', author=cls.author1,
  39. date_created=datetime.datetime(2007, 9, 17, 21, 22, 23)
  40. )
  41. cls.article3 = Article.objects.create(
  42. title='Future Article', slug='future_article', author=cls.author1,
  43. date_created=datetime.datetime(3000, 1, 1, 21, 22, 23)
  44. )
  45. cls.scheme1 = SchemeIncludedURL.objects.create(url='http://test_scheme_included_http/')
  46. cls.scheme2 = SchemeIncludedURL.objects.create(url='https://test_scheme_included_https/')
  47. cls.scheme3 = SchemeIncludedURL.objects.create(url='//test_default_scheme_kept/')
  48. def setUp(self):
  49. Site.objects.clear_cache()
  50. def test_shortcut_with_absolute_url(self):
  51. "Can view a shortcut for an Author object that has a get_absolute_url method"
  52. for obj in Author.objects.all():
  53. short_url = '/shortcut/%s/%s/' % (ContentType.objects.get_for_model(Author).id, obj.pk)
  54. response = self.client.get(short_url)
  55. self.assertRedirects(response, 'http://testserver%s' % obj.get_absolute_url(),
  56. status_code=302, target_status_code=404)
  57. def test_shortcut_with_absolute_url_including_scheme(self):
  58. """
  59. Can view a shortcut when object's get_absolute_url returns a full URL
  60. the tested URLs are: "http://...", "https://..." and "//..."
  61. """
  62. for obj in SchemeIncludedURL.objects.all():
  63. short_url = '/shortcut/%s/%s/' % (ContentType.objects.get_for_model(SchemeIncludedURL).id, obj.pk)
  64. response = self.client.get(short_url)
  65. self.assertRedirects(response, obj.get_absolute_url(),
  66. status_code=302,
  67. fetch_redirect_response=False)
  68. def test_shortcut_no_absolute_url(self):
  69. "Shortcuts for an object that has no get_absolute_url method raises 404"
  70. for obj in Article.objects.all():
  71. short_url = '/shortcut/%s/%s/' % (ContentType.objects.get_for_model(Article).id, obj.pk)
  72. response = self.client.get(short_url)
  73. self.assertEqual(response.status_code, 404)
  74. def test_wrong_type_pk(self):
  75. short_url = '/shortcut/%s/%s/' % (ContentType.objects.get_for_model(Author).id, 'nobody/expects')
  76. response = self.client.get(short_url)
  77. self.assertEqual(response.status_code, 404)
  78. def test_shortcut_bad_pk(self):
  79. short_url = '/shortcut/%s/%s/' % (ContentType.objects.get_for_model(Author).id, '42424242')
  80. response = self.client.get(short_url)
  81. self.assertEqual(response.status_code, 404)
  82. def test_nonint_content_type(self):
  83. an_author = Author.objects.all()[0]
  84. short_url = '/shortcut/%s/%s/' % ('spam', an_author.pk)
  85. response = self.client.get(short_url)
  86. self.assertEqual(response.status_code, 404)
  87. def test_bad_content_type(self):
  88. an_author = Author.objects.all()[0]
  89. short_url = '/shortcut/%s/%s/' % (42424242, an_author.pk)
  90. response = self.client.get(short_url)
  91. self.assertEqual(response.status_code, 404)
  92. @mock.patch('django.apps.apps.get_model')
  93. def test_shortcut_view_with_null_site_fk(self, get_model):
  94. """
  95. The shortcut view works if a model's ForeignKey to site is None.
  96. """
  97. get_model.side_effect = lambda *args, **kwargs: MockSite if args[0] == 'sites.Site' else ModelWithNullFKToSite
  98. obj = ModelWithNullFKToSite.objects.create(title='title')
  99. url = '/shortcut/%s/%s/' % (ContentType.objects.get_for_model(ModelWithNullFKToSite).id, obj.pk)
  100. response = self.client.get(url)
  101. self.assertRedirects(
  102. response, '%s' % obj.get_absolute_url(),
  103. fetch_redirect_response=False,
  104. )
  105. def test_create_contenttype_on_the_spot(self):
  106. """
  107. Make sure ContentTypeManager.get_for_model creates the corresponding
  108. content type if it doesn't exist in the database (for some reason).
  109. """
  110. class ModelCreatedOnTheFly(models.Model):
  111. name = models.CharField()
  112. class Meta:
  113. verbose_name = 'a model created on the fly'
  114. app_label = 'my_great_app'
  115. apps = Apps()
  116. ct = ContentType.objects.get_for_model(ModelCreatedOnTheFly)
  117. self.assertEqual(ct.app_label, 'my_great_app')
  118. self.assertEqual(ct.model, 'modelcreatedonthefly')
  119. self.assertEqual(force_text(ct), 'modelcreatedonthefly')
  120. @override_settings(SILENCED_SYSTEM_CHECKS=['fields.W342']) # ForeignKey(unique=True)
  121. @isolate_apps('contenttypes_tests', attr_name='apps')
  122. class GenericForeignKeyTests(SimpleTestCase):
  123. def test_str(self):
  124. class Model(models.Model):
  125. field = GenericForeignKey()
  126. expected = "contenttypes_tests.Model.field"
  127. actual = force_str(Model.field)
  128. self.assertEqual(expected, actual)
  129. def test_missing_content_type_field(self):
  130. class TaggedItem(models.Model):
  131. # no content_type field
  132. object_id = models.PositiveIntegerField()
  133. content_object = GenericForeignKey()
  134. errors = TaggedItem.content_object.check()
  135. expected = [
  136. checks.Error(
  137. "The GenericForeignKey content type references the non-existent field 'TaggedItem.content_type'.",
  138. obj=TaggedItem.content_object,
  139. id='contenttypes.E002',
  140. )
  141. ]
  142. self.assertEqual(errors, expected)
  143. def test_invalid_content_type_field(self):
  144. class Model(models.Model):
  145. content_type = models.IntegerField() # should be ForeignKey
  146. object_id = models.PositiveIntegerField()
  147. content_object = GenericForeignKey(
  148. 'content_type', 'object_id')
  149. errors = Model.content_object.check()
  150. expected = [
  151. checks.Error(
  152. "'Model.content_type' is not a ForeignKey.",
  153. hint=(
  154. "GenericForeignKeys must use a ForeignKey to "
  155. "'contenttypes.ContentType' as the 'content_type' field."
  156. ),
  157. obj=Model.content_object,
  158. id='contenttypes.E003',
  159. )
  160. ]
  161. self.assertEqual(errors, expected)
  162. def test_content_type_field_pointing_to_wrong_model(self):
  163. class Model(models.Model):
  164. content_type = models.ForeignKey('self', models.CASCADE) # should point to ContentType
  165. object_id = models.PositiveIntegerField()
  166. content_object = GenericForeignKey(
  167. 'content_type', 'object_id')
  168. errors = Model.content_object.check()
  169. expected = [
  170. checks.Error(
  171. "'Model.content_type' is not a ForeignKey to 'contenttypes.ContentType'.",
  172. hint=(
  173. "GenericForeignKeys must use a ForeignKey to "
  174. "'contenttypes.ContentType' as the 'content_type' field."
  175. ),
  176. obj=Model.content_object,
  177. id='contenttypes.E004',
  178. )
  179. ]
  180. self.assertEqual(errors, expected)
  181. def test_missing_object_id_field(self):
  182. class TaggedItem(models.Model):
  183. content_type = models.ForeignKey(ContentType, models.CASCADE)
  184. # missing object_id field
  185. content_object = GenericForeignKey()
  186. errors = TaggedItem.content_object.check()
  187. expected = [
  188. checks.Error(
  189. "The GenericForeignKey object ID references the non-existent field 'object_id'.",
  190. obj=TaggedItem.content_object,
  191. id='contenttypes.E001',
  192. )
  193. ]
  194. self.assertEqual(errors, expected)
  195. def test_field_name_ending_with_underscore(self):
  196. class Model(models.Model):
  197. content_type = models.ForeignKey(ContentType, models.CASCADE)
  198. object_id = models.PositiveIntegerField()
  199. content_object_ = GenericForeignKey(
  200. 'content_type', 'object_id')
  201. errors = Model.content_object_.check()
  202. expected = [
  203. checks.Error(
  204. 'Field names must not end with an underscore.',
  205. obj=Model.content_object_,
  206. id='fields.E001',
  207. )
  208. ]
  209. self.assertEqual(errors, expected)
  210. @override_settings(INSTALLED_APPS=['django.contrib.auth', 'django.contrib.contenttypes', 'contenttypes_tests'])
  211. def test_generic_foreign_key_checks_are_performed(self):
  212. class MyGenericForeignKey(GenericForeignKey):
  213. def check(self, **kwargs):
  214. return ['performed!']
  215. class Model(models.Model):
  216. content_object = MyGenericForeignKey()
  217. errors = checks.run_checks(app_configs=self.apps.get_app_configs())
  218. self.assertEqual(errors, ['performed!'])
  219. @isolate_apps('contenttypes_tests')
  220. class GenericRelationshipTests(SimpleTestCase):
  221. def test_valid_generic_relationship(self):
  222. class TaggedItem(models.Model):
  223. content_type = models.ForeignKey(ContentType, models.CASCADE)
  224. object_id = models.PositiveIntegerField()
  225. content_object = GenericForeignKey()
  226. class Bookmark(models.Model):
  227. tags = GenericRelation('TaggedItem')
  228. errors = Bookmark.tags.field.check()
  229. self.assertEqual(errors, [])
  230. def test_valid_generic_relationship_with_explicit_fields(self):
  231. class TaggedItem(models.Model):
  232. custom_content_type = models.ForeignKey(ContentType, models.CASCADE)
  233. custom_object_id = models.PositiveIntegerField()
  234. content_object = GenericForeignKey(
  235. 'custom_content_type', 'custom_object_id')
  236. class Bookmark(models.Model):
  237. tags = GenericRelation(
  238. 'TaggedItem',
  239. content_type_field='custom_content_type',
  240. object_id_field='custom_object_id',
  241. )
  242. errors = Bookmark.tags.field.check()
  243. self.assertEqual(errors, [])
  244. def test_pointing_to_missing_model(self):
  245. class Model(models.Model):
  246. rel = GenericRelation('MissingModel')
  247. errors = Model.rel.field.check()
  248. expected = [
  249. checks.Error(
  250. "Field defines a relation with model 'MissingModel', "
  251. "which is either not installed, or is abstract.",
  252. obj=Model.rel.field,
  253. id='fields.E300',
  254. )
  255. ]
  256. self.assertEqual(errors, expected)
  257. def test_valid_self_referential_generic_relationship(self):
  258. class Model(models.Model):
  259. rel = GenericRelation('Model')
  260. content_type = models.ForeignKey(ContentType, models.CASCADE)
  261. object_id = models.PositiveIntegerField()
  262. content_object = GenericForeignKey(
  263. 'content_type', 'object_id')
  264. errors = Model.rel.field.check()
  265. self.assertEqual(errors, [])
  266. def test_missing_generic_foreign_key(self):
  267. class TaggedItem(models.Model):
  268. content_type = models.ForeignKey(ContentType, models.CASCADE)
  269. object_id = models.PositiveIntegerField()
  270. class Bookmark(models.Model):
  271. tags = GenericRelation('TaggedItem')
  272. errors = Bookmark.tags.field.check()
  273. expected = [
  274. checks.Error(
  275. "The GenericRelation defines a relation with the model "
  276. "'contenttypes_tests.TaggedItem', but that model does not have a "
  277. "GenericForeignKey.",
  278. obj=Bookmark.tags.field,
  279. id='contenttypes.E004',
  280. )
  281. ]
  282. self.assertEqual(errors, expected)
  283. @override_settings(TEST_SWAPPED_MODEL='contenttypes_tests.Replacement')
  284. def test_pointing_to_swapped_model(self):
  285. class Replacement(models.Model):
  286. pass
  287. class SwappedModel(models.Model):
  288. content_type = models.ForeignKey(ContentType, models.CASCADE)
  289. object_id = models.PositiveIntegerField()
  290. content_object = GenericForeignKey()
  291. class Meta:
  292. swappable = 'TEST_SWAPPED_MODEL'
  293. class Model(models.Model):
  294. rel = GenericRelation('SwappedModel')
  295. errors = Model.rel.field.check()
  296. expected = [
  297. checks.Error(
  298. "Field defines a relation with the model "
  299. "'contenttypes_tests.SwappedModel', "
  300. "which has been swapped out.",
  301. hint="Update the relation to point at 'settings.TEST_SWAPPED_MODEL'.",
  302. obj=Model.rel.field,
  303. id='fields.E301',
  304. )
  305. ]
  306. self.assertEqual(errors, expected)
  307. def test_field_name_ending_with_underscore(self):
  308. class TaggedItem(models.Model):
  309. content_type = models.ForeignKey(ContentType, models.CASCADE)
  310. object_id = models.PositiveIntegerField()
  311. content_object = GenericForeignKey()
  312. class InvalidBookmark(models.Model):
  313. tags_ = GenericRelation('TaggedItem')
  314. errors = InvalidBookmark.tags_.field.check()
  315. expected = [
  316. checks.Error(
  317. 'Field names must not end with an underscore.',
  318. obj=InvalidBookmark.tags_.field,
  319. id='fields.E001',
  320. )
  321. ]
  322. self.assertEqual(errors, expected)
  323. class UpdateContentTypesTests(TestCase):
  324. def setUp(self):
  325. self.before_count = ContentType.objects.count()
  326. self.content_type = ContentType.objects.create(app_label='contenttypes_tests', model='Fake')
  327. self.app_config = apps.get_app_config('contenttypes_tests')
  328. def test_interactive_true_with_dependent_objects(self):
  329. """
  330. interactive mode of remove_stale_contenttypes (the default) should
  331. delete stale contenttypes and warn of dependent objects.
  332. """
  333. post = Post.objects.create(title='post', content_type=self.content_type)
  334. # A related object is needed to show that a custom collector with
  335. # can_fast_delete=False is needed.
  336. ModelWithNullFKToSite.objects.create(post=post)
  337. with mock.patch(
  338. 'django.contrib.contenttypes.management.commands.remove_stale_contenttypes.input',
  339. return_value='yes'
  340. ):
  341. with captured_stdout() as stdout:
  342. call_command('remove_stale_contenttypes', verbosity=2, stdout=stdout)
  343. self.assertEqual(Post.objects.count(), 0)
  344. output = stdout.getvalue()
  345. self.assertIn('- Content type for contenttypes_tests.Fake', output)
  346. self.assertIn('- 1 contenttypes_tests.Post object(s)', output)
  347. self.assertIn('- 1 contenttypes_tests.ModelWithNullFKToSite', output)
  348. self.assertIn('Deleting stale content type', output)
  349. self.assertEqual(ContentType.objects.count(), self.before_count)
  350. def test_interactive_true_without_dependent_objects(self):
  351. """
  352. interactive mode of remove_stale_contenttypes (the default) should
  353. delete stale contenttypes even if there aren't any dependent objects.
  354. """
  355. with mock.patch(
  356. 'django.contrib.contenttypes.management.commands.remove_stale_contenttypes.input',
  357. return_value='yes'
  358. ):
  359. with captured_stdout() as stdout:
  360. call_command('remove_stale_contenttypes', verbosity=2)
  361. self.assertIn("Deleting stale content type", stdout.getvalue())
  362. self.assertEqual(ContentType.objects.count(), self.before_count)
  363. def test_interactive_false(self):
  364. """
  365. non-interactive mode of remove_stale_contenttypes shouldn't delete
  366. stale content types.
  367. """
  368. with captured_stdout() as stdout:
  369. call_command('remove_stale_contenttypes', interactive=False, verbosity=2)
  370. self.assertIn("Stale content types remain.", stdout.getvalue())
  371. self.assertEqual(ContentType.objects.count(), self.before_count + 1)
  372. def test_unavailable_content_type_model(self):
  373. """
  374. A ContentType shouldn't be created if the model isn't available.
  375. """
  376. apps = Apps()
  377. with self.assertNumQueries(0):
  378. contenttypes_management.create_contenttypes(self.app_config, interactive=False, verbosity=0, apps=apps)
  379. self.assertEqual(ContentType.objects.count(), self.before_count + 1)
  380. class TestRouter(object):
  381. def db_for_read(self, model, **hints):
  382. return 'other'
  383. def db_for_write(self, model, **hints):
  384. return 'default'
  385. @override_settings(DATABASE_ROUTERS=[TestRouter()])
  386. class ContentTypesMultidbTestCase(TestCase):
  387. def setUp(self):
  388. # Whenever a test starts executing, only the "default" database is
  389. # connected. We explicitly connect to the "other" database here. If we
  390. # don't do it, then it will be implicitly connected later when we query
  391. # it, but in that case some database backends may automatically perform
  392. # extra queries upon connecting (notably mysql executes
  393. # "SET SQL_AUTO_IS_NULL = 0"), which will affect assertNumQueries().
  394. connections['other'].ensure_connection()
  395. def test_multidb(self):
  396. """
  397. Test that, when using multiple databases, we use the db_for_read (see
  398. #20401).
  399. """
  400. ContentType.objects.clear_cache()
  401. with self.assertNumQueries(0, using='default'), \
  402. self.assertNumQueries(1, using='other'):
  403. ContentType.objects.get_for_model(Author)
  404. @override_settings(
  405. MIGRATION_MODULES=dict(settings.MIGRATION_MODULES, contenttypes_tests='contenttypes_tests.operations_migrations'),
  406. )
  407. class ContentTypeOperationsTests(TransactionTestCase):
  408. available_apps = [
  409. 'contenttypes_tests',
  410. 'django.contrib.contenttypes',
  411. 'django.contrib.auth',
  412. ]
  413. def setUp(self):
  414. app_config = apps.get_app_config('contenttypes_tests')
  415. models.signals.post_migrate.connect(self.assertOperationsInjected, sender=app_config)
  416. def tearDown(self):
  417. app_config = apps.get_app_config('contenttypes_tests')
  418. models.signals.post_migrate.disconnect(self.assertOperationsInjected, sender=app_config)
  419. def assertOperationsInjected(self, plan, **kwargs):
  420. for migration, _backward in plan:
  421. operations = iter(migration.operations)
  422. for operation in operations:
  423. if isinstance(operation, migrations.RenameModel):
  424. next_operation = next(operations)
  425. self.assertIsInstance(next_operation, contenttypes_management.RenameContentType)
  426. self.assertEqual(next_operation.app_label, migration.app_label)
  427. self.assertEqual(next_operation.old_model, operation.old_name_lower)
  428. self.assertEqual(next_operation.new_model, operation.new_name_lower)
  429. def test_existing_content_type_rename(self):
  430. ContentType.objects.create(app_label='contenttypes_tests', model='foo')
  431. management.call_command(
  432. 'migrate', 'contenttypes_tests', database='default', interactive=False, verbosity=0,
  433. )
  434. self.assertFalse(ContentType.objects.filter(app_label='contenttypes_tests', model='foo').exists())
  435. self.assertTrue(ContentType.objects.filter(app_label='contenttypes_tests', model='renamedfoo').exists())
  436. management.call_command(
  437. 'migrate', 'contenttypes_tests', 'zero', database='default', interactive=False, verbosity=0,
  438. )
  439. self.assertTrue(ContentType.objects.filter(app_label='contenttypes_tests', model='foo').exists())
  440. self.assertFalse(ContentType.objects.filter(app_label='contenttypes_tests', model='renamedfoo').exists())
  441. def test_missing_content_type_rename_ignore(self):
  442. management.call_command(
  443. 'migrate', 'contenttypes_tests', database='default', interactive=False, verbosity=0,
  444. )
  445. self.assertFalse(ContentType.objects.filter(app_label='contenttypes_tests', model='foo').exists())
  446. self.assertTrue(ContentType.objects.filter(app_label='contenttypes_tests', model='renamedfoo').exists())
  447. management.call_command(
  448. 'migrate', 'contenttypes_tests', 'zero', database='default', interactive=False, verbosity=0,
  449. )
  450. self.assertTrue(ContentType.objects.filter(app_label='contenttypes_tests', model='foo').exists())
  451. self.assertFalse(ContentType.objects.filter(app_label='contenttypes_tests', model='renamedfoo').exists())
  452. def test_content_type_rename_conflict(self):
  453. ContentType.objects.create(app_label='contenttypes_tests', model='foo')
  454. ContentType.objects.create(app_label='contenttypes_tests', model='renamedfoo')
  455. management.call_command(
  456. 'migrate', 'contenttypes_tests', database='default', interactive=False, verbosity=0,
  457. )
  458. self.assertTrue(ContentType.objects.filter(app_label='contenttypes_tests', model='foo').exists())
  459. self.assertTrue(ContentType.objects.filter(app_label='contenttypes_tests', model='renamedfoo').exists())
  460. management.call_command(
  461. 'migrate', 'contenttypes_tests', 'zero', database='default', interactive=False, verbosity=0,
  462. )
  463. self.assertTrue(ContentType.objects.filter(app_label='contenttypes_tests', model='foo').exists())
  464. self.assertTrue(ContentType.objects.filter(app_label='contenttypes_tests', model='renamedfoo').exists())