test_thread_merge_api.py 33 KB

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