tests.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472
  1. # -*- coding: utf-8 -*-
  2. from __future__ import unicode_literals
  3. import datetime
  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.contrib.sites.models import Site
  11. from django.core import checks
  12. from django.db import connections, models
  13. from django.test import TestCase, override_settings
  14. from django.test.utils import captured_stdout
  15. from django.utils.encoding import force_str, force_text
  16. from .models import Article, Author, SchemeIncludedURL
  17. @override_settings(ROOT_URLCONF='contenttypes_tests.urls')
  18. class ContentTypesViewsTests(TestCase):
  19. @classmethod
  20. def setUpTestData(cls):
  21. # don't use the manager because we want to ensure the site exists
  22. # with pk=1, regardless of whether or not it already exists.
  23. cls.site1 = Site(pk=1, domain='testserver', name='testserver')
  24. cls.site1.save()
  25. cls.author1 = Author.objects.create(name='Boris')
  26. cls.article1 = Article.objects.create(
  27. title='Old Article', slug='old_article', author=cls.author1,
  28. date_created=datetime.datetime(2001, 1, 1, 21, 22, 23)
  29. )
  30. cls.article2 = Article.objects.create(
  31. title='Current Article', slug='current_article', author=cls.author1,
  32. date_created=datetime.datetime(2007, 9, 17, 21, 22, 23)
  33. )
  34. cls.article3 = Article.objects.create(
  35. title='Future Article', slug='future_article', author=cls.author1,
  36. date_created=datetime.datetime(3000, 1, 1, 21, 22, 23)
  37. )
  38. cls.scheme1 = SchemeIncludedURL.objects.create(url='http://test_scheme_included_http/')
  39. cls.scheme2 = SchemeIncludedURL.objects.create(url='https://test_scheme_included_https/')
  40. cls.scheme3 = SchemeIncludedURL.objects.create(url='//test_default_scheme_kept/')
  41. def test_shortcut_with_absolute_url(self):
  42. "Can view a shortcut for an Author object that has a get_absolute_url method"
  43. for obj in Author.objects.all():
  44. short_url = '/shortcut/%s/%s/' % (ContentType.objects.get_for_model(Author).id, obj.pk)
  45. response = self.client.get(short_url)
  46. self.assertRedirects(response, 'http://testserver%s' % obj.get_absolute_url(),
  47. status_code=302, target_status_code=404)
  48. def test_shortcut_with_absolute_url_including_scheme(self):
  49. """
  50. Can view a shortcut when object's get_absolute_url returns a full URL
  51. the tested URLs are: "http://...", "https://..." and "//..."
  52. """
  53. for obj in SchemeIncludedURL.objects.all():
  54. short_url = '/shortcut/%s/%s/' % (ContentType.objects.get_for_model(SchemeIncludedURL).id, obj.pk)
  55. response = self.client.get(short_url)
  56. self.assertRedirects(response, obj.get_absolute_url(),
  57. status_code=302,
  58. fetch_redirect_response=False)
  59. def test_shortcut_no_absolute_url(self):
  60. "Shortcuts for an object that has no get_absolute_url method raises 404"
  61. for obj in Article.objects.all():
  62. short_url = '/shortcut/%s/%s/' % (ContentType.objects.get_for_model(Article).id, obj.pk)
  63. response = self.client.get(short_url)
  64. self.assertEqual(response.status_code, 404)
  65. def test_wrong_type_pk(self):
  66. short_url = '/shortcut/%s/%s/' % (ContentType.objects.get_for_model(Author).id, 'nobody/expects')
  67. response = self.client.get(short_url)
  68. self.assertEqual(response.status_code, 404)
  69. def test_shortcut_bad_pk(self):
  70. short_url = '/shortcut/%s/%s/' % (ContentType.objects.get_for_model(Author).id, '42424242')
  71. response = self.client.get(short_url)
  72. self.assertEqual(response.status_code, 404)
  73. def test_nonint_content_type(self):
  74. an_author = Author.objects.all()[0]
  75. short_url = '/shortcut/%s/%s/' % ('spam', an_author.pk)
  76. response = self.client.get(short_url)
  77. self.assertEqual(response.status_code, 404)
  78. def test_bad_content_type(self):
  79. an_author = Author.objects.all()[0]
  80. short_url = '/shortcut/%s/%s/' % (42424242, an_author.pk)
  81. response = self.client.get(short_url)
  82. self.assertEqual(response.status_code, 404)
  83. def test_create_contenttype_on_the_spot(self):
  84. """
  85. Make sure ContentTypeManager.get_for_model creates the corresponding
  86. content type if it doesn't exist in the database (for some reason).
  87. """
  88. class ModelCreatedOnTheFly(models.Model):
  89. name = models.CharField()
  90. class Meta:
  91. verbose_name = 'a model created on the fly'
  92. app_label = 'my_great_app'
  93. apps = Apps()
  94. ct = ContentType.objects.get_for_model(ModelCreatedOnTheFly)
  95. self.assertEqual(ct.app_label, 'my_great_app')
  96. self.assertEqual(ct.model, 'modelcreatedonthefly')
  97. self.assertEqual(force_text(ct), 'modelcreatedonthefly')
  98. class IsolatedModelsTestCase(TestCase):
  99. def setUp(self):
  100. # The unmanaged models need to be removed after the test in order to
  101. # prevent bad interactions with the flush operation in other tests.
  102. self._old_models = apps.app_configs['contenttypes_tests'].models.copy()
  103. def tearDown(self):
  104. apps.app_configs['contenttypes_tests'].models = self._old_models
  105. apps.all_models['contenttypes_tests'] = self._old_models
  106. apps.clear_cache()
  107. @override_settings(SILENCED_SYSTEM_CHECKS=['fields.W342']) # ForeignKey(unique=True)
  108. class GenericForeignKeyTests(IsolatedModelsTestCase):
  109. def test_str(self):
  110. class Model(models.Model):
  111. field = GenericForeignKey()
  112. expected = "contenttypes_tests.Model.field"
  113. actual = force_str(Model.field)
  114. self.assertEqual(expected, actual)
  115. def test_missing_content_type_field(self):
  116. class TaggedItem(models.Model):
  117. # no content_type field
  118. object_id = models.PositiveIntegerField()
  119. content_object = GenericForeignKey()
  120. errors = TaggedItem.content_object.check()
  121. expected = [
  122. checks.Error(
  123. "The GenericForeignKey content type references the non-existent field 'TaggedItem.content_type'.",
  124. hint=None,
  125. obj=TaggedItem.content_object,
  126. id='contenttypes.E002',
  127. )
  128. ]
  129. self.assertEqual(errors, expected)
  130. def test_invalid_content_type_field(self):
  131. class Model(models.Model):
  132. content_type = models.IntegerField() # should be ForeignKey
  133. object_id = models.PositiveIntegerField()
  134. content_object = GenericForeignKey(
  135. 'content_type', 'object_id')
  136. errors = Model.content_object.check()
  137. expected = [
  138. checks.Error(
  139. "'Model.content_type' is not a ForeignKey.",
  140. hint="GenericForeignKeys must use a ForeignKey to 'contenttypes.ContentType' as the 'content_type' field.",
  141. obj=Model.content_object,
  142. id='contenttypes.E003',
  143. )
  144. ]
  145. self.assertEqual(errors, expected)
  146. def test_content_type_field_pointing_to_wrong_model(self):
  147. class Model(models.Model):
  148. content_type = models.ForeignKey('self') # should point to ContentType
  149. object_id = models.PositiveIntegerField()
  150. content_object = GenericForeignKey(
  151. 'content_type', 'object_id')
  152. errors = Model.content_object.check()
  153. expected = [
  154. checks.Error(
  155. "'Model.content_type' is not a ForeignKey to 'contenttypes.ContentType'.",
  156. hint="GenericForeignKeys must use a ForeignKey to 'contenttypes.ContentType' as the 'content_type' field.",
  157. obj=Model.content_object,
  158. id='contenttypes.E004',
  159. )
  160. ]
  161. self.assertEqual(errors, expected)
  162. def test_missing_object_id_field(self):
  163. class TaggedItem(models.Model):
  164. content_type = models.ForeignKey(ContentType)
  165. # missing object_id field
  166. content_object = GenericForeignKey()
  167. errors = TaggedItem.content_object.check()
  168. expected = [
  169. checks.Error(
  170. "The GenericForeignKey object ID references the non-existent field 'object_id'.",
  171. hint=None,
  172. obj=TaggedItem.content_object,
  173. id='contenttypes.E001',
  174. )
  175. ]
  176. self.assertEqual(errors, expected)
  177. def test_field_name_ending_with_underscore(self):
  178. class Model(models.Model):
  179. content_type = models.ForeignKey(ContentType)
  180. object_id = models.PositiveIntegerField()
  181. content_object_ = GenericForeignKey(
  182. 'content_type', 'object_id')
  183. errors = Model.content_object_.check()
  184. expected = [
  185. checks.Error(
  186. 'Field names must not end with an underscore.',
  187. hint=None,
  188. obj=Model.content_object_,
  189. id='fields.E001',
  190. )
  191. ]
  192. self.assertEqual(errors, expected)
  193. @override_settings(INSTALLED_APPS=['django.contrib.auth', 'django.contrib.contenttypes', 'contenttypes_tests'])
  194. def test_generic_foreign_key_checks_are_performed(self):
  195. class MyGenericForeignKey(GenericForeignKey):
  196. def check(self, **kwargs):
  197. return ['performed!']
  198. class Model(models.Model):
  199. content_object = MyGenericForeignKey()
  200. errors = checks.run_checks()
  201. self.assertEqual(errors, ['performed!'])
  202. def test_unsaved_instance_on_generic_foreign_key(self):
  203. """
  204. #10811 -- Assigning an unsaved object to GenericForeignKey
  205. should raise an exception.
  206. """
  207. class Model(models.Model):
  208. content_type = models.ForeignKey(ContentType, null=True)
  209. object_id = models.PositiveIntegerField(null=True)
  210. content_object = GenericForeignKey('content_type', 'object_id')
  211. author = Author(name='Author')
  212. model = Model()
  213. model.content_object = None # no error here as content_type allows None
  214. with self.assertRaisesMessage(ValueError,
  215. 'Cannot assign "%r": "%s" instance isn\'t saved in the database.'
  216. % (author, author._meta.object_name)):
  217. model.content_object = author # raised ValueError here as author is unsaved
  218. author.save()
  219. model.content_object = author # no error because the instance is saved
  220. def test_unsaved_instance_on_generic_foreign_key_allowed_when_wanted(self):
  221. """
  222. #24495 - Assigning an unsaved object to a GenericForeignKey
  223. should be allowed when the allow_unsaved_instance_assignment
  224. attribute has been set to True.
  225. """
  226. class UnsavedGenericForeignKey(GenericForeignKey):
  227. # A GenericForeignKey which can point to an unsaved object
  228. allow_unsaved_instance_assignment = True
  229. class Band(models.Model):
  230. name = models.CharField(max_length=50)
  231. class BandMember(models.Model):
  232. band_ct = models.ForeignKey(ContentType)
  233. band_id = models.PositiveIntegerField()
  234. band = UnsavedGenericForeignKey('band_ct', 'band_id')
  235. first_name = models.CharField(max_length=50)
  236. last_name = models.CharField(max_length=50)
  237. beatles = Band(name='The Beatles')
  238. john = BandMember(first_name='John', last_name='Lennon')
  239. # This should not raise an exception as the GenericForeignKey between
  240. # member and band has allow_unsaved_instance_assignment=True.
  241. john.band = beatles
  242. self.assertEqual(john.band, beatles)
  243. class GenericRelationshipTests(IsolatedModelsTestCase):
  244. def test_valid_generic_relationship(self):
  245. class TaggedItem(models.Model):
  246. content_type = models.ForeignKey(ContentType)
  247. object_id = models.PositiveIntegerField()
  248. content_object = GenericForeignKey()
  249. class Bookmark(models.Model):
  250. tags = GenericRelation('TaggedItem')
  251. errors = Bookmark.tags.field.check()
  252. self.assertEqual(errors, [])
  253. def test_valid_generic_relationship_with_explicit_fields(self):
  254. class TaggedItem(models.Model):
  255. custom_content_type = models.ForeignKey(ContentType)
  256. custom_object_id = models.PositiveIntegerField()
  257. content_object = GenericForeignKey(
  258. 'custom_content_type', 'custom_object_id')
  259. class Bookmark(models.Model):
  260. tags = GenericRelation('TaggedItem',
  261. content_type_field='custom_content_type',
  262. object_id_field='custom_object_id')
  263. errors = Bookmark.tags.field.check()
  264. self.assertEqual(errors, [])
  265. def test_pointing_to_missing_model(self):
  266. class Model(models.Model):
  267. rel = GenericRelation('MissingModel')
  268. errors = Model.rel.field.check()
  269. expected = [
  270. checks.Error(
  271. ("Field defines a relation with model 'MissingModel', "
  272. "which is either not installed, or is abstract."),
  273. hint=None,
  274. obj=Model.rel.field,
  275. id='fields.E300',
  276. )
  277. ]
  278. self.assertEqual(errors, expected)
  279. def test_valid_self_referential_generic_relationship(self):
  280. class Model(models.Model):
  281. rel = GenericRelation('Model')
  282. content_type = models.ForeignKey(ContentType)
  283. object_id = models.PositiveIntegerField()
  284. content_object = GenericForeignKey(
  285. 'content_type', 'object_id')
  286. errors = Model.rel.field.check()
  287. self.assertEqual(errors, [])
  288. def test_missing_generic_foreign_key(self):
  289. class TaggedItem(models.Model):
  290. content_type = models.ForeignKey(ContentType)
  291. object_id = models.PositiveIntegerField()
  292. class Bookmark(models.Model):
  293. tags = GenericRelation('TaggedItem')
  294. errors = Bookmark.tags.field.check()
  295. expected = [
  296. checks.Error(
  297. ("The GenericRelation defines a relation with the model "
  298. "'contenttypes_tests.TaggedItem', but that model does not have a "
  299. "GenericForeignKey."),
  300. hint=None,
  301. obj=Bookmark.tags.field,
  302. id='contenttypes.E004',
  303. )
  304. ]
  305. self.assertEqual(errors, expected)
  306. @override_settings(TEST_SWAPPED_MODEL='contenttypes_tests.Replacement')
  307. def test_pointing_to_swapped_model(self):
  308. class Replacement(models.Model):
  309. pass
  310. class SwappedModel(models.Model):
  311. content_type = models.ForeignKey(ContentType)
  312. object_id = models.PositiveIntegerField()
  313. content_object = GenericForeignKey()
  314. class Meta:
  315. swappable = 'TEST_SWAPPED_MODEL'
  316. class Model(models.Model):
  317. rel = GenericRelation('SwappedModel')
  318. errors = Model.rel.field.check()
  319. expected = [
  320. checks.Error(
  321. ("Field defines a relation with the model "
  322. "'contenttypes_tests.SwappedModel', "
  323. "which has been swapped out."),
  324. hint="Update the relation to point at 'settings.TEST_SWAPPED_MODEL'.",
  325. obj=Model.rel.field,
  326. id='fields.E301',
  327. )
  328. ]
  329. self.assertEqual(errors, expected)
  330. def test_field_name_ending_with_underscore(self):
  331. class TaggedItem(models.Model):
  332. content_type = models.ForeignKey(ContentType)
  333. object_id = models.PositiveIntegerField()
  334. content_object = GenericForeignKey()
  335. class InvalidBookmark(models.Model):
  336. tags_ = GenericRelation('TaggedItem')
  337. errors = InvalidBookmark.tags_.field.check()
  338. expected = [
  339. checks.Error(
  340. 'Field names must not end with an underscore.',
  341. hint=None,
  342. obj=InvalidBookmark.tags_.field,
  343. id='fields.E001',
  344. )
  345. ]
  346. self.assertEqual(errors, expected)
  347. class UpdateContentTypesTests(TestCase):
  348. def setUp(self):
  349. self.before_count = ContentType.objects.count()
  350. ContentType.objects.create(app_label='contenttypes_tests', model='Fake')
  351. self.app_config = apps.get_app_config('contenttypes_tests')
  352. def test_interactive_true(self):
  353. """
  354. interactive mode of update_contenttypes() (the default) should delete
  355. stale contenttypes.
  356. """
  357. management.input = lambda x: force_str("yes")
  358. with captured_stdout() as stdout:
  359. management.update_contenttypes(self.app_config)
  360. self.assertIn("Deleting stale content type", stdout.getvalue())
  361. self.assertEqual(ContentType.objects.count(), self.before_count)
  362. def test_interactive_false(self):
  363. """
  364. non-interactive mode of update_contenttypes() shouldn't delete stale
  365. content types.
  366. """
  367. with captured_stdout() as stdout:
  368. management.update_contenttypes(self.app_config, interactive=False)
  369. self.assertIn("Stale content types remain.", stdout.getvalue())
  370. self.assertEqual(ContentType.objects.count(), self.before_count + 1)
  371. class TestRouter(object):
  372. def db_for_read(self, model, **hints):
  373. return 'other'
  374. def db_for_write(self, model, **hints):
  375. return 'default'
  376. @override_settings(DATABASE_ROUTERS=[TestRouter()])
  377. class ContentTypesMultidbTestCase(TestCase):
  378. def setUp(self):
  379. # Whenever a test starts executing, only the "default" database is
  380. # connected. We explicitly connect to the "other" database here. If we
  381. # don't do it, then it will be implicitly connected later when we query
  382. # it, but in that case some database backends may automatically perform
  383. # extra queries upon connecting (notably mysql executes
  384. # "SET SQL_AUTO_IS_NULL = 0"), which will affect assertNumQueries().
  385. connections['other'].ensure_connection()
  386. def test_multidb(self):
  387. """
  388. Test that, when using multiple databases, we use the db_for_read (see
  389. #20401).
  390. """
  391. ContentType.objects.clear_cache()
  392. with self.assertNumQueries(0, using='default'), \
  393. self.assertNumQueries(1, using='other'):
  394. ContentType.objects.get_for_model(Author)