test_thread_merge_api.py 34 KB

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