test_thread_merge_api.py 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848
  1. from django.urls import reverse
  2. from misago.categories.models import Category
  3. from misago.readtracker import poststracker
  4. from misago.threads import testutils
  5. from misago.threads.models import Poll, PollVote, Thread
  6. from misago.threads.test import patch_category_acl, patch_other_category_acl
  7. from .test_threads_api import ThreadsApiTestCase
  8. class ThreadMergeApiTests(ThreadsApiTestCase):
  9. def setUp(self):
  10. super().setUp()
  11. Category(
  12. name='Other Category',
  13. slug='other-category',
  14. ).insert_at(
  15. self.category,
  16. position='last-child',
  17. save=True,
  18. )
  19. self.other_category = Category.objects.get(slug='other-category')
  20. self.api_link = reverse(
  21. 'misago:api:thread-merge', kwargs={
  22. 'pk': self.thread.pk,
  23. }
  24. )
  25. @patch_category_acl({"can_merge_threads": False})
  26. def test_merge_no_permission(self):
  27. """api validates if thread can be merged with other one"""
  28. response = self.client.post(self.api_link)
  29. self.assertEqual(response.status_code, 403)
  30. self.assertEqual(response.json(), {
  31. "detail": "You can't merge threads in this category."
  32. })
  33. @patch_category_acl({"can_merge_threads": True})
  34. def test_merge_no_url(self):
  35. """api validates if thread url was given"""
  36. response = self.client.post(self.api_link)
  37. self.assertEqual(response.status_code, 400)
  38. self.assertEqual(response.json(), {
  39. "detail": "Enter link to new thread."
  40. })
  41. @patch_category_acl({"can_merge_threads": True})
  42. def test_invalid_url(self):
  43. """api validates thread url"""
  44. response = self.client.post(self.api_link, {
  45. 'other_thread': self.user.get_absolute_url(),
  46. })
  47. self.assertEqual(response.status_code, 400)
  48. self.assertEqual(response.json(), {
  49. "detail": "This is not a valid thread link."
  50. })
  51. @patch_category_acl({"can_merge_threads": True})
  52. def test_current_other_thread(self):
  53. """api validates if thread url given is to current thread"""
  54. response = self.client.post(
  55. self.api_link, {
  56. 'other_thread': self.thread.get_absolute_url(),
  57. }
  58. )
  59. self.assertEqual(response.status_code, 400)
  60. self.assertEqual(response.json(), {
  61. "detail": "You can't merge thread with itself."
  62. })
  63. @patch_other_category_acl()
  64. @patch_category_acl({"can_merge_threads": True})
  65. def test_other_thread_exists(self):
  66. """api validates if other thread exists"""
  67. other_thread = testutils.post_thread(self.other_category)
  68. other_other_thread = other_thread.get_absolute_url()
  69. other_thread.delete()
  70. response = self.client.post(self.api_link, {
  71. 'other_thread': other_other_thread,
  72. })
  73. self.assertEqual(response.status_code, 400)
  74. self.assertEqual(response.json(), {
  75. "detail": (
  76. "The thread you have entered link to doesn't exist "
  77. "or you don't have permission to see it."
  78. )
  79. })
  80. @patch_other_category_acl({"can_see": False})
  81. @patch_category_acl({"can_merge_threads": True})
  82. def test_other_thread_is_invisible(self):
  83. """api validates if other thread is visible"""
  84. other_thread = testutils.post_thread(self.other_category)
  85. response = self.client.post(
  86. self.api_link, {
  87. 'other_thread': other_thread.get_absolute_url(),
  88. }
  89. )
  90. self.assertEqual(response.status_code, 400)
  91. self.assertEqual(response.json(), {
  92. "detail": (
  93. "The thread you have entered link to doesn't exist "
  94. "or you don't have permission to see it."
  95. )
  96. })
  97. @patch_other_category_acl({"can_merge_threads": False})
  98. @patch_category_acl({"can_merge_threads": True})
  99. def test_other_thread_isnt_mergeable(self):
  100. """api validates if other thread can be merged"""
  101. other_thread = testutils.post_thread(self.other_category)
  102. response = self.client.post(
  103. self.api_link, {
  104. 'other_thread': other_thread.get_absolute_url(),
  105. }
  106. )
  107. self.assertEqual(response.status_code, 400)
  108. self.assertEqual(response.json(), {
  109. "detail": "Other thread can't be merged with."
  110. })
  111. @patch_other_category_acl({"can_merge_threads": True, "can_close_threads": False})
  112. @patch_category_acl({"can_merge_threads": True})
  113. def test_thread_category_is_closed(self):
  114. """api validates if thread's category is open"""
  115. other_thread = testutils.post_thread(self.other_category)
  116. self.category.is_closed = True
  117. self.category.save()
  118. response = self.client.post(
  119. self.api_link, {
  120. 'other_thread': other_thread.get_absolute_url(),
  121. }
  122. )
  123. self.assertEqual(response.status_code, 403)
  124. self.assertEqual(response.json(), {
  125. "detail": "This category is closed. You can't merge it's threads."
  126. })
  127. @patch_other_category_acl({"can_merge_threads": True, "can_close_threads": False})
  128. @patch_category_acl({"can_merge_threads": True})
  129. def test_thread_is_closed(self):
  130. """api validates if thread is open"""
  131. other_thread = testutils.post_thread(self.other_category)
  132. self.thread.is_closed = True
  133. self.thread.save()
  134. response = self.client.post(
  135. self.api_link, {
  136. 'other_thread': other_thread.get_absolute_url(),
  137. }
  138. )
  139. self.assertEqual(response.status_code, 403)
  140. self.assertEqual(response.json(), {
  141. "detail": "This thread is closed. You can't merge it with other threads."
  142. })
  143. @patch_other_category_acl({"can_merge_threads": True, "can_close_threads": False})
  144. @patch_category_acl({"can_merge_threads": True})
  145. def test_other_thread_category_is_closed(self):
  146. """api validates if other thread's category is open"""
  147. other_thread = testutils.post_thread(self.other_category)
  148. self.other_category.is_closed = True
  149. self.other_category.save()
  150. response = self.client.post(
  151. self.api_link, {
  152. 'other_thread': other_thread.get_absolute_url(),
  153. }
  154. )
  155. self.assertEqual(response.status_code, 400)
  156. self.assertEqual(response.json(), {
  157. "detail": "Other thread's category is closed. You can't merge with it."
  158. })
  159. @patch_other_category_acl({"can_merge_threads": True, "can_close_threads": False})
  160. @patch_category_acl({"can_merge_threads": True})
  161. def test_other_thread_is_closed(self):
  162. """api validates if other thread is open"""
  163. other_thread = testutils.post_thread(self.other_category)
  164. other_thread.is_closed = True
  165. other_thread.save()
  166. response = self.client.post(
  167. self.api_link, {
  168. 'other_thread': other_thread.get_absolute_url(),
  169. }
  170. )
  171. self.assertEqual(response.status_code, 400)
  172. self.assertEqual(response.json(), {
  173. "detail": "Other thread is closed and can't be merged with."
  174. })
  175. @patch_other_category_acl({"can_merge_threads": True, "can_reply_threads": False})
  176. @patch_category_acl({"can_merge_threads": True})
  177. def test_other_thread_isnt_replyable(self):
  178. """api validates if other thread can be replied, which is condition for merge"""
  179. other_thread = testutils.post_thread(self.other_category)
  180. response = self.client.post(
  181. self.api_link, {
  182. 'other_thread': other_thread.get_absolute_url(),
  183. }
  184. )
  185. self.assertEqual(response.status_code, 400)
  186. self.assertEqual(response.json(), {
  187. "detail": "You can't merge this thread into thread you can't reply."
  188. })
  189. @patch_other_category_acl({"can_merge_threads": True})
  190. @patch_category_acl({"can_merge_threads": True})
  191. def test_merge_threads(self):
  192. """api merges two threads successfully"""
  193. other_thread = testutils.post_thread(self.other_category)
  194. response = self.client.post(
  195. self.api_link, {
  196. 'other_thread': other_thread.get_absolute_url(),
  197. }
  198. )
  199. self.assertEqual(response.status_code, 200)
  200. self.assertEqual(response.json(), {
  201. 'id': other_thread.id,
  202. 'title': other_thread.title,
  203. 'url': other_thread.get_absolute_url(),
  204. })
  205. # other thread has two posts and an event now
  206. self.assertEqual(other_thread.post_set.count(), 3)
  207. # first thread is gone
  208. with self.assertRaises(Thread.DoesNotExist):
  209. Thread.objects.get(pk=self.thread.pk)
  210. @patch_other_category_acl({"can_merge_threads": True})
  211. @patch_category_acl({"can_merge_threads": True})
  212. def test_merge_threads_kept_reads(self):
  213. """api keeps both threads readtrackers after merge"""
  214. other_thread = testutils.post_thread(self.other_category)
  215. poststracker.save_read(self.user, self.thread.first_post)
  216. poststracker.save_read(self.user, other_thread.first_post)
  217. response = self.client.post(
  218. self.api_link, {
  219. 'other_thread': other_thread.get_absolute_url(),
  220. }
  221. )
  222. self.assertEqual(response.status_code, 200)
  223. self.assertEqual(response.json(), {
  224. 'id': other_thread.id,
  225. 'title': other_thread.title,
  226. 'url': other_thread.get_absolute_url(),
  227. })
  228. # posts reads are kept
  229. postreads = self.user.postread_set.filter(post__is_event=False).order_by('id')
  230. self.assertEqual(
  231. list(postreads.values_list('post_id', flat=True)),
  232. [self.thread.first_post_id, other_thread.first_post_id]
  233. )
  234. self.assertEqual(postreads.filter(thread=other_thread).count(), 2)
  235. self.assertEqual(postreads.filter(category=self.other_category).count(), 2)
  236. @patch_other_category_acl({"can_merge_threads": True})
  237. @patch_category_acl({"can_merge_threads": True})
  238. def test_merge_threads_kept_subs(self):
  239. """api keeps other thread's subscription after merge"""
  240. other_thread = testutils.post_thread(self.other_category)
  241. self.user.subscription_set.create(
  242. thread=self.thread,
  243. category=self.thread.category,
  244. last_read_on=self.thread.last_post_on,
  245. send_email=False,
  246. )
  247. self.assertEqual(self.user.subscription_set.count(), 1)
  248. self.user.subscription_set.get(thread=self.thread)
  249. self.user.subscription_set.get(category=self.category)
  250. response = self.client.post(
  251. self.api_link, {
  252. 'other_thread': other_thread.get_absolute_url(),
  253. }
  254. )
  255. self.assertEqual(response.status_code, 200)
  256. self.assertEqual(response.json(), {
  257. 'id': other_thread.id,
  258. 'title': other_thread.title,
  259. 'url': other_thread.get_absolute_url(),
  260. })
  261. # subscriptions are kept
  262. self.assertEqual(self.user.subscription_set.count(), 1)
  263. self.user.subscription_set.get(thread=other_thread)
  264. self.user.subscription_set.get(category=self.other_category)
  265. @patch_other_category_acl({"can_merge_threads": True})
  266. @patch_category_acl({"can_merge_threads": True})
  267. def test_merge_threads_moved_subs(self):
  268. """api keeps other thread's subscription after merge"""
  269. other_thread = testutils.post_thread(self.other_category)
  270. self.user.subscription_set.create(
  271. thread=other_thread,
  272. category=other_thread.category,
  273. last_read_on=other_thread.last_post_on,
  274. send_email=False,
  275. )
  276. self.assertEqual(self.user.subscription_set.count(), 1)
  277. self.user.subscription_set.get(thread=other_thread)
  278. self.user.subscription_set.get(category=self.other_category)
  279. response = self.client.post(
  280. self.api_link, {
  281. 'other_thread': other_thread.get_absolute_url(),
  282. }
  283. )
  284. self.assertEqual(response.status_code, 200)
  285. self.assertEqual(response.json(), {
  286. 'id': other_thread.id,
  287. 'title': other_thread.title,
  288. 'url': other_thread.get_absolute_url(),
  289. })
  290. # subscriptions are kept
  291. self.assertEqual(self.user.subscription_set.count(), 1)
  292. self.user.subscription_set.get(thread=other_thread)
  293. self.user.subscription_set.get(category=self.other_category)
  294. @patch_other_category_acl({"can_merge_threads": True})
  295. @patch_category_acl({"can_merge_threads": True})
  296. def test_merge_threads_handle_subs_colision(self):
  297. """api resolves conflicting thread subscriptions after merge"""
  298. self.user.subscription_set.create(
  299. thread=self.thread,
  300. category=self.thread.category,
  301. last_read_on=self.thread.last_post_on,
  302. send_email=False,
  303. )
  304. other_thread = testutils.post_thread(self.other_category)
  305. self.user.subscription_set.create(
  306. thread=other_thread,
  307. category=other_thread.category,
  308. last_read_on=other_thread.last_post_on,
  309. send_email=False,
  310. )
  311. self.assertEqual(self.user.subscription_set.count(), 2)
  312. self.user.subscription_set.get(thread=self.thread)
  313. self.user.subscription_set.get(category=self.category)
  314. self.user.subscription_set.get(thread=other_thread)
  315. self.user.subscription_set.get(category=self.other_category)
  316. response = self.client.post(
  317. self.api_link, {
  318. 'other_thread': other_thread.get_absolute_url(),
  319. }
  320. )
  321. self.assertEqual(response.status_code, 200)
  322. self.assertEqual(response.json(), {
  323. 'id': other_thread.id,
  324. 'title': other_thread.title,
  325. 'url': other_thread.get_absolute_url(),
  326. })
  327. # subscriptions are kept
  328. self.assertEqual(self.user.subscription_set.count(), 1)
  329. self.user.subscription_set.get(thread=other_thread)
  330. self.user.subscription_set.get(category=self.other_category)
  331. @patch_other_category_acl({"can_merge_threads": True})
  332. @patch_category_acl({"can_merge_threads": True})
  333. def test_merge_threads_kept_best_answer(self):
  334. """api merges two threads successfully, keeping best answer from old thread"""
  335. other_thread = testutils.post_thread(self.other_category)
  336. best_answer = testutils.reply_thread(other_thread)
  337. other_thread.set_best_answer(self.user, best_answer)
  338. other_thread.save()
  339. response = self.client.post(
  340. self.api_link, {
  341. 'other_thread': other_thread.get_absolute_url(),
  342. }
  343. )
  344. self.assertEqual(response.status_code, 200)
  345. self.assertEqual(response.json(), {
  346. 'id': other_thread.id,
  347. 'title': other_thread.title,
  348. 'url': other_thread.get_absolute_url(),
  349. })
  350. # other thread has three posts and an event now
  351. self.assertEqual(other_thread.post_set.count(), 4)
  352. # first thread is gone
  353. with self.assertRaises(Thread.DoesNotExist):
  354. Thread.objects.get(pk=self.thread.pk)
  355. # best answer is kept in other thread
  356. other_thread = Thread.objects.get(pk=other_thread.pk)
  357. self.assertEqual(other_thread.best_answer, best_answer)
  358. @patch_other_category_acl({"can_merge_threads": True})
  359. @patch_category_acl({"can_merge_threads": True})
  360. def test_merge_threads_moved_best_answer(self):
  361. """api merges two threads successfully, moving best answer to old thread"""
  362. other_thread = testutils.post_thread(self.other_category)
  363. best_answer = testutils.reply_thread(self.thread)
  364. self.thread.set_best_answer(self.user, best_answer)
  365. self.thread.save()
  366. response = self.client.post(
  367. self.api_link, {
  368. 'other_thread': other_thread.get_absolute_url(),
  369. }
  370. )
  371. self.assertEqual(response.status_code, 200)
  372. self.assertEqual(response.json(), {
  373. 'id': other_thread.id,
  374. 'title': other_thread.title,
  375. 'url': other_thread.get_absolute_url(),
  376. })
  377. # other thread has three posts and an event now
  378. self.assertEqual(other_thread.post_set.count(), 4)
  379. # first thread is gone
  380. with self.assertRaises(Thread.DoesNotExist):
  381. Thread.objects.get(pk=self.thread.pk)
  382. # best answer is kept in other thread
  383. other_thread = Thread.objects.get(pk=other_thread.pk)
  384. self.assertEqual(other_thread.best_answer, best_answer)
  385. @patch_other_category_acl({"can_merge_threads": True})
  386. @patch_category_acl({"can_merge_threads": True})
  387. def test_merge_threads_merge_conflict_best_answer(self):
  388. """api errors on merge conflict, returning list of available best answers"""
  389. best_answer = testutils.reply_thread(self.thread)
  390. self.thread.set_best_answer(self.user, best_answer)
  391. self.thread.save()
  392. other_thread = testutils.post_thread(self.other_category)
  393. other_best_answer = testutils.reply_thread(other_thread)
  394. other_thread.set_best_answer(self.user, other_best_answer)
  395. other_thread.save()
  396. response = self.client.post(
  397. self.api_link, {
  398. 'other_thread': other_thread.get_absolute_url(),
  399. }
  400. )
  401. self.assertEqual(response.status_code, 400)
  402. self.assertEqual(response.json(), {
  403. 'best_answers': [
  404. ['0', "Unmark all best answers"],
  405. [str(self.thread.id), self.thread.title],
  406. [str(other_thread.id), other_thread.title],
  407. ]
  408. })
  409. # best answers were untouched
  410. self.assertEqual(self.thread.post_set.count(), 2)
  411. self.assertEqual(other_thread.post_set.count(), 2)
  412. self.assertEqual(Thread.objects.get(pk=self.thread.pk).best_answer_id, best_answer.id)
  413. self.assertEqual(
  414. Thread.objects.get(pk=other_thread.pk).best_answer_id, other_best_answer.id)
  415. @patch_other_category_acl({"can_merge_threads": True})
  416. @patch_category_acl({"can_merge_threads": True})
  417. def test_threads_merge_conflict_best_answer_invalid_resolution(self):
  418. """api errors on invalid merge conflict resolution"""
  419. best_answer = testutils.reply_thread(self.thread)
  420. self.thread.set_best_answer(self.user, best_answer)
  421. self.thread.save()
  422. other_thread = testutils.post_thread(self.other_category)
  423. other_best_answer = testutils.reply_thread(other_thread)
  424. other_thread.set_best_answer(self.user, other_best_answer)
  425. other_thread.save()
  426. response = self.client.post(
  427. self.api_link, {
  428. 'other_thread': other_thread.get_absolute_url(),
  429. 'best_answer': other_thread.id + 10,
  430. }
  431. )
  432. self.assertEqual(response.status_code, 400)
  433. self.assertEqual(response.json(), {'detail': "Invalid choice."})
  434. # best answers were untouched
  435. self.assertEqual(self.thread.post_set.count(), 2)
  436. self.assertEqual(other_thread.post_set.count(), 2)
  437. self.assertEqual(Thread.objects.get(pk=self.thread.pk).best_answer_id, best_answer.id)
  438. self.assertEqual(
  439. Thread.objects.get(pk=other_thread.pk).best_answer_id, other_best_answer.id)
  440. @patch_other_category_acl({"can_merge_threads": True})
  441. @patch_category_acl({"can_merge_threads": True})
  442. def test_threads_merge_conflict_unmark_all_best_answers(self):
  443. """api unmarks all best answers when unmark all choice is selected"""
  444. best_answer = testutils.reply_thread(self.thread)
  445. self.thread.set_best_answer(self.user, best_answer)
  446. self.thread.save()
  447. other_thread = testutils.post_thread(self.other_category)
  448. other_best_answer = testutils.reply_thread(other_thread)
  449. other_thread.set_best_answer(self.user, other_best_answer)
  450. other_thread.save()
  451. response = self.client.post(
  452. self.api_link, {
  453. 'other_thread': other_thread.get_absolute_url(),
  454. 'best_answer': 0,
  455. }
  456. )
  457. self.assertEqual(response.status_code, 200)
  458. self.assertEqual(response.json(), {
  459. 'id': other_thread.id,
  460. 'title': other_thread.title,
  461. 'url': other_thread.get_absolute_url(),
  462. })
  463. # other thread has four posts and an event now
  464. self.assertEqual(other_thread.post_set.count(), 5)
  465. # first thread is gone
  466. with self.assertRaises(Thread.DoesNotExist):
  467. Thread.objects.get(pk=self.thread.pk)
  468. # final thread has no marked best answer
  469. self.assertIsNone(Thread.objects.get(pk=other_thread.pk).best_answer_id)
  470. @patch_other_category_acl({"can_merge_threads": True})
  471. @patch_category_acl({"can_merge_threads": True})
  472. def test_threads_merge_conflict_keep_first_best_answer(self):
  473. """api unmarks other best answer on merge"""
  474. best_answer = testutils.reply_thread(self.thread)
  475. self.thread.set_best_answer(self.user, best_answer)
  476. self.thread.save()
  477. other_thread = testutils.post_thread(self.other_category)
  478. other_best_answer = testutils.reply_thread(other_thread)
  479. other_thread.set_best_answer(self.user, other_best_answer)
  480. other_thread.save()
  481. response = self.client.post(
  482. self.api_link, {
  483. 'other_thread': other_thread.get_absolute_url(),
  484. 'best_answer': self.thread.pk,
  485. }
  486. )
  487. self.assertEqual(response.status_code, 200)
  488. self.assertEqual(response.json(), {
  489. 'id': other_thread.id,
  490. 'title': other_thread.title,
  491. 'url': other_thread.get_absolute_url(),
  492. })
  493. # other thread has four posts and an event now
  494. self.assertEqual(other_thread.post_set.count(), 5)
  495. # first thread is gone
  496. with self.assertRaises(Thread.DoesNotExist):
  497. Thread.objects.get(pk=self.thread.pk)
  498. # other thread's best answer was unchanged
  499. self.assertEqual(Thread.objects.get(pk=other_thread.pk).best_answer_id, best_answer.id)
  500. @patch_other_category_acl({"can_merge_threads": True})
  501. @patch_category_acl({"can_merge_threads": True})
  502. def test_threads_merge_conflict_keep_other_best_answer(self):
  503. """api unmarks first best answer on merge"""
  504. best_answer = testutils.reply_thread(self.thread)
  505. self.thread.set_best_answer(self.user, best_answer)
  506. self.thread.save()
  507. other_thread = testutils.post_thread(self.other_category)
  508. other_best_answer = testutils.reply_thread(other_thread)
  509. other_thread.set_best_answer(self.user, other_best_answer)
  510. other_thread.save()
  511. response = self.client.post(
  512. self.api_link, {
  513. 'other_thread': other_thread.get_absolute_url(),
  514. 'best_answer': other_thread.pk,
  515. }
  516. )
  517. self.assertEqual(response.status_code, 200)
  518. self.assertEqual(response.json(), {
  519. 'id': other_thread.id,
  520. 'title': other_thread.title,
  521. 'url': other_thread.get_absolute_url(),
  522. })
  523. # other thread has four posts and an event now
  524. self.assertEqual(other_thread.post_set.count(), 5)
  525. # first thread is gone
  526. with self.assertRaises(Thread.DoesNotExist):
  527. Thread.objects.get(pk=self.thread.pk)
  528. # other thread's best answer was changed to merged in thread's answer
  529. self.assertEqual(
  530. Thread.objects.get(pk=other_thread.pk).best_answer_id, other_best_answer.id)
  531. @patch_other_category_acl({"can_merge_threads": True})
  532. @patch_category_acl({"can_merge_threads": True})
  533. def test_merge_threads_kept_poll(self):
  534. """api merges two threads successfully, keeping poll from other thread"""
  535. other_thread = testutils.post_thread(self.other_category)
  536. poll = testutils.post_poll(other_thread, self.user)
  537. response = self.client.post(
  538. self.api_link, {
  539. 'other_thread': other_thread.get_absolute_url(),
  540. }
  541. )
  542. self.assertEqual(response.status_code, 200)
  543. self.assertEqual(response.json(), {
  544. 'id': other_thread.id,
  545. 'title': other_thread.title,
  546. 'url': other_thread.get_absolute_url(),
  547. })
  548. # other thread has two posts and an event now
  549. self.assertEqual(other_thread.post_set.count(), 3)
  550. # first thread is gone
  551. with self.assertRaises(Thread.DoesNotExist):
  552. Thread.objects.get(pk=self.thread.pk)
  553. # poll and its votes were kept
  554. self.assertEqual(Poll.objects.filter(pk=poll.pk, thread=other_thread).count(), 1)
  555. self.assertEqual(PollVote.objects.filter(poll=poll, thread=other_thread).count(), 4)
  556. @patch_other_category_acl({"can_merge_threads": True})
  557. @patch_category_acl({"can_merge_threads": True})
  558. def test_merge_threads_moved_poll(self):
  559. """api merges two threads successfully, moving poll from old thread"""
  560. other_thread = testutils.post_thread(self.other_category)
  561. poll = testutils.post_poll(self.thread, self.user)
  562. response = self.client.post(
  563. self.api_link, {
  564. 'other_thread': other_thread.get_absolute_url(),
  565. }
  566. )
  567. self.assertEqual(response.status_code, 200)
  568. self.assertEqual(response.json(), {
  569. 'id': other_thread.id,
  570. 'title': other_thread.title,
  571. 'url': other_thread.get_absolute_url(),
  572. })
  573. # other thread has two posts and an event now
  574. self.assertEqual(other_thread.post_set.count(), 3)
  575. # first thread is gone
  576. with self.assertRaises(Thread.DoesNotExist):
  577. Thread.objects.get(pk=self.thread.pk)
  578. # poll and its votes were moved
  579. self.assertEqual(Poll.objects.filter(pk=poll.pk, thread=other_thread).count(), 1)
  580. self.assertEqual(PollVote.objects.filter(poll=poll, thread=other_thread).count(), 4)
  581. @patch_other_category_acl({"can_merge_threads": True})
  582. @patch_category_acl({"can_merge_threads": True})
  583. def test_threads_merge_conflict_polls(self):
  584. """api errors on merge conflict, returning list of available polls"""
  585. other_thread = testutils.post_thread(self.other_category)
  586. poll = testutils.post_poll(self.thread, self.user)
  587. other_poll = testutils.post_poll(other_thread, self.user)
  588. response = self.client.post(
  589. self.api_link, {
  590. 'other_thread': other_thread.get_absolute_url(),
  591. }
  592. )
  593. self.assertEqual(response.status_code, 400)
  594. self.assertEqual(
  595. response.json(), {
  596. 'polls': [
  597. ['0', "Delete all polls"],
  598. [
  599. str(poll.pk),
  600. '%s (%s)' % (poll.question, poll.thread.title),
  601. ],
  602. [
  603. str(other_poll.pk),
  604. '%s (%s)' % (other_poll.question, other_poll.thread.title),
  605. ],
  606. ]
  607. }
  608. )
  609. # polls and votes were untouched
  610. self.assertEqual(Poll.objects.count(), 2)
  611. self.assertEqual(PollVote.objects.count(), 8)
  612. @patch_other_category_acl({"can_merge_threads": True})
  613. @patch_category_acl({"can_merge_threads": True})
  614. def test_threads_merge_conflict_poll_invalid_resolution(self):
  615. """api errors on invalid merge conflict resolution"""
  616. other_thread = testutils.post_thread(self.other_category)
  617. testutils.post_poll(self.thread, self.user)
  618. testutils.post_poll(other_thread, self.user)
  619. response = self.client.post(
  620. self.api_link, {
  621. 'other_thread': other_thread.get_absolute_url(),
  622. 'poll': Poll.objects.all()[0].pk + 10,
  623. }
  624. )
  625. self.assertEqual(response.status_code, 400)
  626. self.assertEqual(response.json(), {'detail': "Invalid choice."})
  627. # polls and votes were untouched
  628. self.assertEqual(Poll.objects.count(), 2)
  629. self.assertEqual(PollVote.objects.count(), 8)
  630. @patch_other_category_acl({"can_merge_threads": True})
  631. @patch_category_acl({"can_merge_threads": True})
  632. def test_threads_merge_conflict_delete_all_polls(self):
  633. """api deletes all polls when delete all choice is selected"""
  634. other_thread = testutils.post_thread(self.other_category)
  635. testutils.post_poll(self.thread, self.user)
  636. testutils.post_poll(other_thread, self.user)
  637. response = self.client.post(
  638. self.api_link, {
  639. 'other_thread': other_thread.get_absolute_url(),
  640. 'poll': 0,
  641. }
  642. )
  643. self.assertEqual(response.status_code, 200)
  644. self.assertEqual(response.json(), {
  645. 'id': other_thread.id,
  646. 'title': other_thread.title,
  647. 'url': other_thread.get_absolute_url(),
  648. })
  649. # other thread has two posts and an event now
  650. self.assertEqual(other_thread.post_set.count(), 3)
  651. # first thread is gone
  652. with self.assertRaises(Thread.DoesNotExist):
  653. Thread.objects.get(pk=self.thread.pk)
  654. # polls and votes are gone
  655. self.assertEqual(Poll.objects.count(), 0)
  656. self.assertEqual(PollVote.objects.count(), 0)
  657. @patch_other_category_acl({"can_merge_threads": True})
  658. @patch_category_acl({"can_merge_threads": True})
  659. def test_threads_merge_conflict_keep_first_poll(self):
  660. """api deletes other poll on merge"""
  661. other_thread = testutils.post_thread(self.other_category)
  662. poll = testutils.post_poll(self.thread, self.user)
  663. other_poll = testutils.post_poll(other_thread, self.user)
  664. response = self.client.post(
  665. self.api_link, {
  666. 'other_thread': other_thread.get_absolute_url(),
  667. 'poll': poll.pk,
  668. }
  669. )
  670. self.assertEqual(response.status_code, 200)
  671. self.assertEqual(response.json(), {
  672. 'id': other_thread.id,
  673. 'title': other_thread.title,
  674. 'url': other_thread.get_absolute_url(),
  675. })
  676. # other thread has two posts and an event now
  677. self.assertEqual(other_thread.post_set.count(), 3)
  678. # first thread is gone
  679. with self.assertRaises(Thread.DoesNotExist):
  680. Thread.objects.get(pk=self.thread.pk)
  681. # other poll and its votes are gone
  682. self.assertEqual(Poll.objects.filter(thread=self.thread).count(), 0)
  683. self.assertEqual(PollVote.objects.filter(thread=self.thread).count(), 0)
  684. self.assertEqual(Poll.objects.filter(thread=other_thread).count(), 1)
  685. self.assertEqual(PollVote.objects.filter(thread=other_thread).count(), 4)
  686. Poll.objects.get(pk=poll.pk)
  687. with self.assertRaises(Poll.DoesNotExist):
  688. Poll.objects.get(pk=other_poll.pk)
  689. @patch_other_category_acl({"can_merge_threads": True})
  690. @patch_category_acl({"can_merge_threads": True})
  691. def test_threads_merge_conflict_keep_other_poll(self):
  692. """api deletes first poll on merge"""
  693. other_thread = testutils.post_thread(self.other_category)
  694. poll = testutils.post_poll(self.thread, self.user)
  695. other_poll = testutils.post_poll(other_thread, self.user)
  696. response = self.client.post(
  697. self.api_link, {
  698. 'other_thread': other_thread.get_absolute_url(),
  699. 'poll': other_poll.pk,
  700. }
  701. )
  702. self.assertEqual(response.status_code, 200)
  703. self.assertEqual(response.json(), {
  704. 'id': other_thread.id,
  705. 'title': other_thread.title,
  706. 'url': other_thread.get_absolute_url(),
  707. })
  708. # other thread has two posts and an event now
  709. self.assertEqual(other_thread.post_set.count(), 3)
  710. # first thread is gone
  711. with self.assertRaises(Thread.DoesNotExist):
  712. Thread.objects.get(pk=self.thread.pk)
  713. # other poll and its votes are gone
  714. self.assertEqual(Poll.objects.filter(thread=self.thread).count(), 0)
  715. self.assertEqual(PollVote.objects.filter(thread=self.thread).count(), 0)
  716. self.assertEqual(Poll.objects.filter(thread=other_thread).count(), 1)
  717. self.assertEqual(PollVote.objects.filter(thread=other_thread).count(), 4)
  718. Poll.objects.get(pk=other_poll.pk)
  719. with self.assertRaises(Poll.DoesNotExist):
  720. Poll.objects.get(pk=poll.pk)