tests.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456
  1. from __future__ import unicode_literals
  2. from django.db import transaction
  3. from django.test import TestCase
  4. from django.utils import six
  5. from .models import Article, Publication
  6. class ManyToManyTests(TestCase):
  7. def setUp(self):
  8. # Create a couple of Publications.
  9. self.p1 = Publication.objects.create(id=None, title='The Python Journal')
  10. self.p2 = Publication.objects.create(id=None, title='Science News')
  11. self.p3 = Publication.objects.create(id=None, title='Science Weekly')
  12. self.p4 = Publication.objects.create(title='Highlights for Children')
  13. self.a1 = Article.objects.create(id=None, headline='Django lets you build Web apps easily')
  14. self.a1.publications.add(self.p1)
  15. self.a2 = Article.objects.create(id=None, headline='NASA uses Python')
  16. self.a2.publications.add(self.p1, self.p2, self.p3, self.p4)
  17. self.a3 = Article.objects.create(headline='NASA finds intelligent life on Earth')
  18. self.a3.publications.add(self.p2)
  19. self.a4 = Article.objects.create(headline='Oxygen-free diet works wonders')
  20. self.a4.publications.add(self.p2)
  21. def test_add(self):
  22. # Create an Article.
  23. a5 = Article(id=None, headline='Django lets you reate Web apps easily')
  24. # You can't associate it with a Publication until it's been saved.
  25. self.assertRaises(ValueError, getattr, a5, 'publications')
  26. # Save it!
  27. a5.save()
  28. # Associate the Article with a Publication.
  29. a5.publications.add(self.p1)
  30. self.assertQuerysetEqual(a5.publications.all(),
  31. ['<Publication: The Python Journal>'])
  32. # Create another Article, and set it to appear in both Publications.
  33. a6 = Article(id=None, headline='ESA uses Python')
  34. a6.save()
  35. a6.publications.add(self.p1, self.p2)
  36. a6.publications.add(self.p3)
  37. # Adding a second time is OK
  38. a6.publications.add(self.p3)
  39. self.assertQuerysetEqual(a6.publications.all(),
  40. [
  41. '<Publication: Science News>',
  42. '<Publication: Science Weekly>',
  43. '<Publication: The Python Journal>',
  44. ])
  45. # Adding an object of the wrong type raises TypeError
  46. with six.assertRaisesRegex(self, TypeError, "'Publication' instance expected, got <Article.*"):
  47. with transaction.atomic():
  48. a6.publications.add(a5)
  49. # Add a Publication directly via publications.add by using keyword arguments.
  50. a6.publications.create(title='Highlights for Adults')
  51. self.assertQuerysetEqual(a6.publications.all(),
  52. [
  53. '<Publication: Highlights for Adults>',
  54. '<Publication: Science News>',
  55. '<Publication: Science Weekly>',
  56. '<Publication: The Python Journal>',
  57. ])
  58. def test_reverse_add(self):
  59. # Adding via the 'other' end of an m2m
  60. a5 = Article(headline='NASA finds intelligent life on Mars')
  61. a5.save()
  62. self.p2.article_set.add(a5)
  63. self.assertQuerysetEqual(self.p2.article_set.all(),
  64. [
  65. '<Article: NASA finds intelligent life on Earth>',
  66. '<Article: NASA finds intelligent life on Mars>',
  67. '<Article: NASA uses Python>',
  68. '<Article: Oxygen-free diet works wonders>',
  69. ])
  70. self.assertQuerysetEqual(a5.publications.all(),
  71. ['<Publication: Science News>'])
  72. # Adding via the other end using keywords
  73. self.p2.article_set.create(headline='Carbon-free diet works wonders')
  74. self.assertQuerysetEqual(
  75. self.p2.article_set.all(),
  76. [
  77. '<Article: Carbon-free diet works wonders>',
  78. '<Article: NASA finds intelligent life on Earth>',
  79. '<Article: NASA finds intelligent life on Mars>',
  80. '<Article: NASA uses Python>',
  81. '<Article: Oxygen-free diet works wonders>',
  82. ])
  83. a6 = self.p2.article_set.all()[3]
  84. self.assertQuerysetEqual(a6.publications.all(),
  85. [
  86. '<Publication: Highlights for Children>',
  87. '<Publication: Science News>',
  88. '<Publication: Science Weekly>',
  89. '<Publication: The Python Journal>',
  90. ])
  91. def test_related_sets(self):
  92. # Article objects have access to their related Publication objects.
  93. self.assertQuerysetEqual(self.a1.publications.all(),
  94. ['<Publication: The Python Journal>'])
  95. self.assertQuerysetEqual(self.a2.publications.all(),
  96. [
  97. '<Publication: Highlights for Children>',
  98. '<Publication: Science News>',
  99. '<Publication: Science Weekly>',
  100. '<Publication: The Python Journal>',
  101. ])
  102. # Publication objects have access to their related Article objects.
  103. self.assertQuerysetEqual(self.p2.article_set.all(),
  104. [
  105. '<Article: NASA finds intelligent life on Earth>',
  106. '<Article: NASA uses Python>',
  107. '<Article: Oxygen-free diet works wonders>',
  108. ])
  109. self.assertQuerysetEqual(self.p1.article_set.all(),
  110. [
  111. '<Article: Django lets you build Web apps easily>',
  112. '<Article: NASA uses Python>',
  113. ])
  114. self.assertQuerysetEqual(Publication.objects.get(id=self.p4.id).article_set.all(),
  115. ['<Article: NASA uses Python>'])
  116. def test_selects(self):
  117. # We can perform kwarg queries across m2m relationships
  118. self.assertQuerysetEqual(
  119. Article.objects.filter(publications__id__exact=self.p1.id),
  120. [
  121. '<Article: Django lets you build Web apps easily>',
  122. '<Article: NASA uses Python>',
  123. ])
  124. self.assertQuerysetEqual(
  125. Article.objects.filter(publications__pk=self.p1.id),
  126. [
  127. '<Article: Django lets you build Web apps easily>',
  128. '<Article: NASA uses Python>',
  129. ])
  130. self.assertQuerysetEqual(
  131. Article.objects.filter(publications=self.p1.id),
  132. [
  133. '<Article: Django lets you build Web apps easily>',
  134. '<Article: NASA uses Python>',
  135. ])
  136. self.assertQuerysetEqual(
  137. Article.objects.filter(publications=self.p1),
  138. [
  139. '<Article: Django lets you build Web apps easily>',
  140. '<Article: NASA uses Python>',
  141. ])
  142. self.assertQuerysetEqual(
  143. Article.objects.filter(publications__title__startswith="Science"),
  144. [
  145. '<Article: NASA finds intelligent life on Earth>',
  146. '<Article: NASA uses Python>',
  147. '<Article: NASA uses Python>',
  148. '<Article: Oxygen-free diet works wonders>',
  149. ])
  150. self.assertQuerysetEqual(
  151. Article.objects.filter(publications__title__startswith="Science").distinct(),
  152. [
  153. '<Article: NASA finds intelligent life on Earth>',
  154. '<Article: NASA uses Python>',
  155. '<Article: Oxygen-free diet works wonders>',
  156. ])
  157. # The count() function respects distinct() as well.
  158. self.assertEqual(Article.objects.filter(publications__title__startswith="Science").count(), 4)
  159. self.assertEqual(Article.objects.filter(publications__title__startswith="Science").distinct().count(), 3)
  160. self.assertQuerysetEqual(
  161. Article.objects.filter(publications__in=[self.p1.id, self.p2.id]).distinct(),
  162. [
  163. '<Article: Django lets you build Web apps easily>',
  164. '<Article: NASA finds intelligent life on Earth>',
  165. '<Article: NASA uses Python>',
  166. '<Article: Oxygen-free diet works wonders>',
  167. ])
  168. self.assertQuerysetEqual(
  169. Article.objects.filter(publications__in=[self.p1.id, self.p2]).distinct(),
  170. [
  171. '<Article: Django lets you build Web apps easily>',
  172. '<Article: NASA finds intelligent life on Earth>',
  173. '<Article: NASA uses Python>',
  174. '<Article: Oxygen-free diet works wonders>',
  175. ])
  176. self.assertQuerysetEqual(
  177. Article.objects.filter(publications__in=[self.p1, self.p2]).distinct(),
  178. [
  179. '<Article: Django lets you build Web apps easily>',
  180. '<Article: NASA finds intelligent life on Earth>',
  181. '<Article: NASA uses Python>',
  182. '<Article: Oxygen-free diet works wonders>',
  183. ])
  184. # Excluding a related item works as you would expect, too (although the SQL
  185. # involved is a little complex).
  186. self.assertQuerysetEqual(Article.objects.exclude(publications=self.p2),
  187. ['<Article: Django lets you build Web apps easily>'])
  188. def test_reverse_selects(self):
  189. # Reverse m2m queries are supported (i.e., starting at the table that
  190. # doesn't have a ManyToManyField).
  191. self.assertQuerysetEqual(Publication.objects.filter(id__exact=self.p1.id),
  192. ['<Publication: The Python Journal>'])
  193. self.assertQuerysetEqual(Publication.objects.filter(pk=self.p1.id),
  194. ['<Publication: The Python Journal>'])
  195. self.assertQuerysetEqual(
  196. Publication.objects.filter(article__headline__startswith="NASA"),
  197. [
  198. '<Publication: Highlights for Children>',
  199. '<Publication: Science News>',
  200. '<Publication: Science News>',
  201. '<Publication: Science Weekly>',
  202. '<Publication: The Python Journal>',
  203. ])
  204. self.assertQuerysetEqual(Publication.objects.filter(article__id__exact=self.a1.id),
  205. ['<Publication: The Python Journal>'])
  206. self.assertQuerysetEqual(Publication.objects.filter(article__pk=self.a1.id),
  207. ['<Publication: The Python Journal>'])
  208. self.assertQuerysetEqual(Publication.objects.filter(article=self.a1.id),
  209. ['<Publication: The Python Journal>'])
  210. self.assertQuerysetEqual(Publication.objects.filter(article=self.a1),
  211. ['<Publication: The Python Journal>'])
  212. self.assertQuerysetEqual(
  213. Publication.objects.filter(article__in=[self.a1.id, self.a2.id]).distinct(),
  214. [
  215. '<Publication: Highlights for Children>',
  216. '<Publication: Science News>',
  217. '<Publication: Science Weekly>',
  218. '<Publication: The Python Journal>',
  219. ])
  220. self.assertQuerysetEqual(
  221. Publication.objects.filter(article__in=[self.a1.id, self.a2]).distinct(),
  222. [
  223. '<Publication: Highlights for Children>',
  224. '<Publication: Science News>',
  225. '<Publication: Science Weekly>',
  226. '<Publication: The Python Journal>',
  227. ])
  228. self.assertQuerysetEqual(
  229. Publication.objects.filter(article__in=[self.a1, self.a2]).distinct(),
  230. [
  231. '<Publication: Highlights for Children>',
  232. '<Publication: Science News>',
  233. '<Publication: Science Weekly>',
  234. '<Publication: The Python Journal>',
  235. ])
  236. def test_delete(self):
  237. # If we delete a Publication, its Articles won't be able to access it.
  238. self.p1.delete()
  239. self.assertQuerysetEqual(Publication.objects.all(),
  240. [
  241. '<Publication: Highlights for Children>',
  242. '<Publication: Science News>',
  243. '<Publication: Science Weekly>',
  244. ])
  245. self.assertQuerysetEqual(self.a1.publications.all(), [])
  246. # If we delete an Article, its Publications won't be able to access it.
  247. self.a2.delete()
  248. self.assertQuerysetEqual(Article.objects.all(),
  249. [
  250. '<Article: Django lets you build Web apps easily>',
  251. '<Article: NASA finds intelligent life on Earth>',
  252. '<Article: Oxygen-free diet works wonders>',
  253. ])
  254. self.assertQuerysetEqual(self.p2.article_set.all(),
  255. [
  256. '<Article: NASA finds intelligent life on Earth>',
  257. '<Article: Oxygen-free diet works wonders>',
  258. ])
  259. def test_bulk_delete(self):
  260. # Bulk delete some Publications - references to deleted publications should go
  261. Publication.objects.filter(title__startswith='Science').delete()
  262. self.assertQuerysetEqual(Publication.objects.all(),
  263. [
  264. '<Publication: Highlights for Children>',
  265. '<Publication: The Python Journal>',
  266. ])
  267. self.assertQuerysetEqual(Article.objects.all(),
  268. [
  269. '<Article: Django lets you build Web apps easily>',
  270. '<Article: NASA finds intelligent life on Earth>',
  271. '<Article: NASA uses Python>',
  272. '<Article: Oxygen-free diet works wonders>',
  273. ])
  274. self.assertQuerysetEqual(self.a2.publications.all(),
  275. [
  276. '<Publication: Highlights for Children>',
  277. '<Publication: The Python Journal>',
  278. ])
  279. # Bulk delete some articles - references to deleted objects should go
  280. q = Article.objects.filter(headline__startswith='Django')
  281. self.assertQuerysetEqual(q, ['<Article: Django lets you build Web apps easily>'])
  282. q.delete()
  283. # After the delete, the QuerySet cache needs to be cleared,
  284. # and the referenced objects should be gone
  285. self.assertQuerysetEqual(q, [])
  286. self.assertQuerysetEqual(self.p1.article_set.all(),
  287. ['<Article: NASA uses Python>'])
  288. def test_remove(self):
  289. # Removing publication from an article:
  290. self.assertQuerysetEqual(self.p2.article_set.all(),
  291. [
  292. '<Article: NASA finds intelligent life on Earth>',
  293. '<Article: NASA uses Python>',
  294. '<Article: Oxygen-free diet works wonders>',
  295. ])
  296. self.a4.publications.remove(self.p2)
  297. self.assertQuerysetEqual(self.p2.article_set.all(),
  298. [
  299. '<Article: NASA finds intelligent life on Earth>',
  300. '<Article: NASA uses Python>',
  301. ])
  302. self.assertQuerysetEqual(self.a4.publications.all(), [])
  303. # And from the other end
  304. self.p2.article_set.remove(self.a3)
  305. self.assertQuerysetEqual(self.p2.article_set.all(),
  306. [
  307. '<Article: NASA uses Python>',
  308. ])
  309. self.assertQuerysetEqual(self.a3.publications.all(), [])
  310. def test_set(self):
  311. self.p2.article_set.set([self.a4, self.a3])
  312. self.assertQuerysetEqual(self.p2.article_set.all(),
  313. [
  314. '<Article: NASA finds intelligent life on Earth>',
  315. '<Article: Oxygen-free diet works wonders>',
  316. ])
  317. self.assertQuerysetEqual(self.a4.publications.all(),
  318. ['<Publication: Science News>'])
  319. self.a4.publications.set([self.p3.id])
  320. self.assertQuerysetEqual(self.p2.article_set.all(),
  321. ['<Article: NASA finds intelligent life on Earth>'])
  322. self.assertQuerysetEqual(self.a4.publications.all(),
  323. ['<Publication: Science Weekly>'])
  324. self.p2.article_set.set([])
  325. self.assertQuerysetEqual(self.p2.article_set.all(), [])
  326. self.a4.publications.set([])
  327. self.assertQuerysetEqual(self.a4.publications.all(), [])
  328. self.p2.article_set.set([self.a4, self.a3], clear=True)
  329. self.assertQuerysetEqual(self.p2.article_set.all(),
  330. [
  331. '<Article: NASA finds intelligent life on Earth>',
  332. '<Article: Oxygen-free diet works wonders>',
  333. ])
  334. self.assertQuerysetEqual(self.a4.publications.all(),
  335. ['<Publication: Science News>'])
  336. self.a4.publications.set([self.p3.id], clear=True)
  337. self.assertQuerysetEqual(self.p2.article_set.all(),
  338. ['<Article: NASA finds intelligent life on Earth>'])
  339. self.assertQuerysetEqual(self.a4.publications.all(),
  340. ['<Publication: Science Weekly>'])
  341. self.p2.article_set.set([], clear=True)
  342. self.assertQuerysetEqual(self.p2.article_set.all(), [])
  343. self.a4.publications.set([], clear=True)
  344. self.assertQuerysetEqual(self.a4.publications.all(), [])
  345. def test_assign(self):
  346. # Relation sets can be assigned. Assignment clears any existing set members
  347. self.p2.article_set = [self.a4, self.a3]
  348. self.assertQuerysetEqual(self.p2.article_set.all(),
  349. [
  350. '<Article: NASA finds intelligent life on Earth>',
  351. '<Article: Oxygen-free diet works wonders>',
  352. ])
  353. self.assertQuerysetEqual(self.a4.publications.all(),
  354. ['<Publication: Science News>'])
  355. self.a4.publications = [self.p3.id]
  356. self.assertQuerysetEqual(self.p2.article_set.all(),
  357. ['<Article: NASA finds intelligent life on Earth>'])
  358. self.assertQuerysetEqual(self.a4.publications.all(),
  359. ['<Publication: Science Weekly>'])
  360. # An alternate to calling clear() is to assign the empty set
  361. self.p2.article_set = []
  362. self.assertQuerysetEqual(self.p2.article_set.all(), [])
  363. self.a4.publications = []
  364. self.assertQuerysetEqual(self.a4.publications.all(), [])
  365. def test_assign_ids(self):
  366. # Relation sets can also be set using primary key values
  367. self.p2.article_set = [self.a4.id, self.a3.id]
  368. self.assertQuerysetEqual(self.p2.article_set.all(),
  369. [
  370. '<Article: NASA finds intelligent life on Earth>',
  371. '<Article: Oxygen-free diet works wonders>',
  372. ])
  373. self.assertQuerysetEqual(self.a4.publications.all(),
  374. ['<Publication: Science News>'])
  375. self.a4.publications = [self.p3.id]
  376. self.assertQuerysetEqual(self.p2.article_set.all(),
  377. ['<Article: NASA finds intelligent life on Earth>'])
  378. self.assertQuerysetEqual(self.a4.publications.all(),
  379. ['<Publication: Science Weekly>'])
  380. def test_forward_assign_with_queryset(self):
  381. # Ensure that querysets used in m2m assignments are pre-evaluated
  382. # so their value isn't affected by the clearing operation in
  383. # ManyRelatedObjectsDescriptor.__set__. Refs #19816.
  384. self.a1.publications = [self.p1, self.p2]
  385. qs = self.a1.publications.filter(title='The Python Journal')
  386. self.a1.publications = qs
  387. self.assertEqual(1, self.a1.publications.count())
  388. self.assertEqual(1, qs.count())
  389. def test_reverse_assign_with_queryset(self):
  390. # Ensure that querysets used in M2M assignments are pre-evaluated
  391. # so their value isn't affected by the clearing operation in
  392. # ReverseManyRelatedObjectsDescriptor.__set__. Refs #19816.
  393. self.p1.article_set = [self.a1, self.a2]
  394. qs = self.p1.article_set.filter(headline='Django lets you build Web apps easily')
  395. self.p1.article_set = qs
  396. self.assertEqual(1, self.p1.article_set.count())
  397. self.assertEqual(1, qs.count())
  398. def test_clear(self):
  399. # Relation sets can be cleared:
  400. self.p2.article_set.clear()
  401. self.assertQuerysetEqual(self.p2.article_set.all(), [])
  402. self.assertQuerysetEqual(self.a4.publications.all(), [])
  403. # And you can clear from the other end
  404. self.p2.article_set.add(self.a3, self.a4)
  405. self.assertQuerysetEqual(self.p2.article_set.all(),
  406. [
  407. '<Article: NASA finds intelligent life on Earth>',
  408. '<Article: Oxygen-free diet works wonders>',
  409. ])
  410. self.assertQuerysetEqual(self.a4.publications.all(),
  411. [
  412. '<Publication: Science News>',
  413. ])
  414. self.a4.publications.clear()
  415. self.assertQuerysetEqual(self.a4.publications.all(), [])
  416. self.assertQuerysetEqual(self.p2.article_set.all(),
  417. ['<Article: NASA finds intelligent life on Earth>'])