test_thread_merge_api.py 34 KB

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