test_threads_merge_api.py 37 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019
  1. import json
  2. from django.urls import reverse
  3. from misago.acl import add_acl, useracl
  4. from misago.categories.models import Category
  5. from misago.readtracker import poststracker
  6. from misago.threads import testutils
  7. from misago.threads.models import Poll, PollVote, Post, Thread
  8. from misago.threads.serializers import ThreadsListSerializer
  9. from misago.threads.serializers.moderation import THREADS_LIMIT
  10. from misago.threads.test import patch_category_acl, patch_other_category_acl
  11. from .test_threads_api import ThreadsApiTestCase
  12. cache_versions = {"acl": "abcdefgh"}
  13. class ThreadsMergeApiTests(ThreadsApiTestCase):
  14. def setUp(self):
  15. super().setUp()
  16. self.api_link = reverse('misago:api:thread-merge')
  17. Category(
  18. name='Other Category',
  19. slug='other-category',
  20. ).insert_at(
  21. self.category,
  22. position='last-child',
  23. save=True,
  24. )
  25. self.other_category = Category.objects.get(slug='other-category')
  26. def test_merge_no_threads(self):
  27. """api validates if we are trying to merge no threads"""
  28. response = self.client.post(self.api_link, content_type="application/json")
  29. self.assertEqual(response.status_code, 403)
  30. self.assertEqual(
  31. response.json(), {
  32. 'detail': "You have to select at least two threads to merge.",
  33. }
  34. )
  35. def test_merge_empty_threads(self):
  36. """api validates if we are trying to empty threads list"""
  37. response = self.client.post(
  38. self.api_link,
  39. json.dumps({
  40. 'threads': [],
  41. }),
  42. content_type="application/json",
  43. )
  44. self.assertEqual(response.status_code, 403)
  45. self.assertEqual(
  46. response.json(), {
  47. 'detail': "You have to select at least two threads to merge.",
  48. }
  49. )
  50. def test_merge_invalid_threads(self):
  51. """api validates if we are trying to merge invalid thread ids"""
  52. response = self.client.post(
  53. self.api_link,
  54. json.dumps({
  55. 'threads': 'abcd',
  56. }),
  57. content_type="application/json",
  58. )
  59. self.assertEqual(response.status_code, 403)
  60. self.assertEqual(response.json(), {
  61. "detail": 'Expected a list of items but got type "str".',
  62. })
  63. response = self.client.post(
  64. self.api_link,
  65. json.dumps({
  66. 'threads': ['a', '-', 'c'],
  67. }),
  68. content_type="application/json",
  69. )
  70. self.assertEqual(response.status_code, 403)
  71. self.assertEqual(
  72. response.json(), {
  73. 'detail': "One or more thread ids received were invalid.",
  74. }
  75. )
  76. def test_merge_single_thread(self):
  77. """api validates if we are trying to merge single thread"""
  78. response = self.client.post(
  79. self.api_link,
  80. json.dumps({
  81. 'threads': [self.thread.id],
  82. }),
  83. content_type="application/json",
  84. )
  85. self.assertEqual(response.status_code, 403)
  86. self.assertEqual(
  87. response.json(), {
  88. 'detail': "You have to select at least two threads to merge.",
  89. }
  90. )
  91. def test_merge_with_nonexisting_thread(self):
  92. """api validates if we are trying to merge with invalid thread"""
  93. response = self.client.post(
  94. self.api_link,
  95. json.dumps({
  96. 'threads': [self.thread.id, self.thread.id + 1000],
  97. }),
  98. content_type="application/json",
  99. )
  100. self.assertEqual(response.status_code, 403)
  101. self.assertEqual(
  102. response.json(), {
  103. 'detail': "One or more threads to merge could not be found.",
  104. }
  105. )
  106. def test_merge_with_invisible_thread(self):
  107. """api validates if we are trying to merge with inaccesible thread"""
  108. unaccesible_thread = testutils.post_thread(category=self.other_category)
  109. response = self.client.post(
  110. self.api_link,
  111. json.dumps({
  112. 'threads': [self.thread.id, unaccesible_thread.id],
  113. }),
  114. content_type="application/json",
  115. )
  116. self.assertEqual(response.status_code, 403)
  117. self.assertEqual(
  118. response.json(), {
  119. 'detail': "One or more threads to merge could not be found.",
  120. }
  121. )
  122. def test_merge_no_permission(self):
  123. """api validates permission to merge threads"""
  124. thread = testutils.post_thread(category=self.category)
  125. response = self.client.post(
  126. self.api_link,
  127. json.dumps({
  128. 'category': self.category.id,
  129. 'title': 'Lorem ipsum dolor',
  130. 'threads': [self.thread.id, thread.id],
  131. }),
  132. content_type="application/json",
  133. )
  134. self.assertEqual(response.status_code, 403)
  135. self.assertEqual(
  136. response.json(), [
  137. {
  138. 'id': thread.pk,
  139. 'title': thread.title,
  140. 'errors': ["You can't merge threads in this category."],
  141. },
  142. {
  143. 'id': self.thread.pk,
  144. 'title': self.thread.title,
  145. 'errors': ["You can't merge threads in this category."],
  146. },
  147. ]
  148. )
  149. @patch_other_category_acl()
  150. @patch_category_acl({"can_merge_threads": True, "can_close_threads": False})
  151. def test_thread_category_is_closed(self):
  152. """api validates if thread's category is open"""
  153. other_thread = testutils.post_thread(self.category)
  154. self.category.is_closed = True
  155. self.category.save()
  156. response = self.client.post(
  157. self.api_link,
  158. json.dumps({
  159. 'category': self.other_category.id,
  160. 'title': 'Lorem ipsum dolor',
  161. 'threads': [self.thread.id, other_thread.id],
  162. }),
  163. content_type="application/json",
  164. )
  165. self.assertEqual(response.status_code, 403)
  166. self.assertEqual(response.json(), [
  167. {
  168. "id": other_thread.id,
  169. "title": other_thread.title,
  170. "errors": ["This category is closed. You can't merge it's threads."],
  171. },
  172. {
  173. "id": self.thread.id,
  174. "title": self.thread.title,
  175. "errors": ["This category is closed. You can't merge it's threads."],
  176. },
  177. ])
  178. @patch_other_category_acl()
  179. @patch_category_acl({"can_merge_threads": True, "can_close_threads": False})
  180. def test_thread_is_closed(self):
  181. """api validates if thread is open"""
  182. other_thread = testutils.post_thread(self.category)
  183. other_thread.is_closed = True
  184. other_thread.save()
  185. response = self.client.post(
  186. self.api_link,
  187. json.dumps({
  188. 'category': self.other_category.id,
  189. 'title': 'Lorem ipsum dolor',
  190. 'threads': [self.thread.id, other_thread.id],
  191. }),
  192. content_type="application/json",
  193. )
  194. self.assertEqual(response.status_code, 403)
  195. self.assertEqual(response.json(), [
  196. {
  197. "id": other_thread.id,
  198. "title": other_thread.title,
  199. "errors": ["This thread is closed. You can't merge it with other threads."],
  200. },
  201. ])
  202. @patch_category_acl({"can_merge_threads": True})
  203. def test_merge_too_many_threads(self):
  204. """api rejects too many threads to merge"""
  205. threads = []
  206. for _ in range(THREADS_LIMIT + 1):
  207. threads.append(testutils.post_thread(category=self.category).pk)
  208. response = self.client.post(
  209. self.api_link,
  210. json.dumps({
  211. 'threads': threads,
  212. }),
  213. content_type="application/json",
  214. )
  215. self.assertEqual(response.status_code, 403)
  216. self.assertEqual(
  217. response.json(), {
  218. 'detail': "No more than %s threads can be merged at single time." % THREADS_LIMIT,
  219. }
  220. )
  221. @patch_category_acl({"can_merge_threads": True})
  222. def test_merge_no_final_thread(self):
  223. """api rejects merge because no data to merge threads was specified"""
  224. thread = testutils.post_thread(category=self.category)
  225. response = self.client.post(
  226. self.api_link,
  227. json.dumps({
  228. 'threads': [self.thread.id, thread.id],
  229. }),
  230. content_type="application/json",
  231. )
  232. self.assertEqual(response.status_code, 400)
  233. self.assertEqual(
  234. response.json(), {
  235. 'title': ['This field is required.'],
  236. 'category': ['This field is required.'],
  237. }
  238. )
  239. @patch_category_acl({"can_merge_threads": True})
  240. def test_merge_invalid_final_title(self):
  241. """api rejects merge because final thread title was invalid"""
  242. thread = testutils.post_thread(category=self.category)
  243. response = self.client.post(
  244. self.api_link,
  245. json.dumps({
  246. 'threads': [self.thread.id, thread.id],
  247. 'title': '$$$',
  248. 'category': self.category.id,
  249. }),
  250. content_type="application/json",
  251. )
  252. self.assertEqual(response.status_code, 400)
  253. self.assertEqual(
  254. response.json(), {
  255. 'title': ["Thread title should be at least 5 characters long (it has 3)."],
  256. }
  257. )
  258. @patch_category_acl({"can_merge_threads": True})
  259. def test_merge_invalid_category(self):
  260. """api rejects merge because final category was invalid"""
  261. thread = testutils.post_thread(category=self.category)
  262. response = self.client.post(
  263. self.api_link,
  264. json.dumps({
  265. 'threads': [self.thread.id, thread.id],
  266. 'title': 'Valid thread title',
  267. 'category': self.other_category.id,
  268. }),
  269. content_type="application/json",
  270. )
  271. self.assertEqual(response.status_code, 400)
  272. self.assertEqual(
  273. response.json(), {
  274. 'category': ["Requested category could not be found."],
  275. }
  276. )
  277. @patch_category_acl({"can_merge_threads": True, "can_start_threads": False})
  278. def test_merge_unallowed_start_thread(self):
  279. """api rejects merge because category isn't allowing starting threads"""
  280. thread = testutils.post_thread(category=self.category)
  281. response = self.client.post(
  282. self.api_link,
  283. json.dumps({
  284. 'threads': [self.thread.id, thread.id],
  285. 'title': 'Valid thread title',
  286. 'category': self.category.id,
  287. }),
  288. content_type="application/json",
  289. )
  290. self.assertEqual(response.status_code, 400)
  291. self.assertEqual(
  292. response.json(), {
  293. 'category': ["You can't create new threads in selected category."],
  294. }
  295. )
  296. @patch_category_acl({"can_merge_threads": True})
  297. def test_merge_invalid_weight(self):
  298. """api rejects merge because final weight was invalid"""
  299. thread = testutils.post_thread(category=self.category)
  300. response = self.client.post(
  301. self.api_link,
  302. json.dumps({
  303. 'threads': [self.thread.id, thread.id],
  304. 'title': 'Valid thread title',
  305. 'category': self.category.id,
  306. 'weight': 4,
  307. }),
  308. content_type="application/json",
  309. )
  310. self.assertEqual(response.status_code, 400)
  311. self.assertEqual(
  312. response.json(), {
  313. 'weight': ["Ensure this value is less than or equal to 2."],
  314. }
  315. )
  316. @patch_category_acl({"can_merge_threads": True})
  317. def test_merge_unallowed_global_weight(self):
  318. """api rejects merge because global weight was unallowed"""
  319. thread = testutils.post_thread(category=self.category)
  320. response = self.client.post(
  321. self.api_link,
  322. json.dumps({
  323. 'threads': [self.thread.id, thread.id],
  324. 'title': 'Valid thread title',
  325. 'category': self.category.id,
  326. 'weight': 2,
  327. }),
  328. content_type="application/json",
  329. )
  330. self.assertEqual(response.status_code, 400)
  331. self.assertEqual(
  332. response.json(), {
  333. 'weight': ["You don't have permission to pin threads globally in this category."],
  334. }
  335. )
  336. @patch_category_acl({"can_merge_threads": True})
  337. def test_merge_unallowed_local_weight(self):
  338. """api rejects merge because local weight was unallowed"""
  339. thread = testutils.post_thread(category=self.category)
  340. response = self.client.post(
  341. self.api_link,
  342. json.dumps({
  343. 'threads': [self.thread.id, thread.id],
  344. 'title': 'Valid thread title',
  345. 'category': self.category.id,
  346. 'weight': 1,
  347. }),
  348. content_type="application/json",
  349. )
  350. self.assertEqual(response.status_code, 400)
  351. self.assertEqual(
  352. response.json(), {
  353. 'weight': ["You don't have permission to pin threads in this category."],
  354. }
  355. )
  356. @patch_category_acl({"can_merge_threads": True, "can_pin_threads": 1})
  357. def test_merge_allowed_local_weight(self):
  358. """api allows local weight"""
  359. thread = testutils.post_thread(category=self.category)
  360. response = self.client.post(
  361. self.api_link,
  362. json.dumps({
  363. 'threads': [self.thread.id, thread.id],
  364. 'title': '$$$',
  365. 'category': self.category.id,
  366. 'weight': 1,
  367. }),
  368. content_type="application/json",
  369. )
  370. self.assertEqual(response.status_code, 400)
  371. self.assertEqual(
  372. response.json(), {
  373. 'title': ["Thread title should be at least 5 characters long (it has 3)."],
  374. }
  375. )
  376. @patch_category_acl({"can_merge_threads": True, "can_pin_threads": 2})
  377. def test_merge_allowed_global_weight(self):
  378. """api allows global weight"""
  379. thread = testutils.post_thread(category=self.category)
  380. response = self.client.post(
  381. self.api_link,
  382. json.dumps({
  383. 'threads': [self.thread.id, thread.id],
  384. 'title': '$$$',
  385. 'category': self.category.id,
  386. 'weight': 2,
  387. }),
  388. content_type="application/json",
  389. )
  390. self.assertEqual(response.status_code, 400)
  391. self.assertEqual(
  392. response.json(), {
  393. 'title': ["Thread title should be at least 5 characters long (it has 3)."],
  394. }
  395. )
  396. @patch_category_acl({"can_merge_threads": True, "can_close_threads": False})
  397. def test_merge_unallowed_close(self):
  398. """api rejects merge because closing thread was unallowed"""
  399. thread = testutils.post_thread(category=self.category)
  400. response = self.client.post(
  401. self.api_link,
  402. json.dumps({
  403. 'threads': [self.thread.id, thread.id],
  404. 'title': 'Valid thread title',
  405. 'category': self.category.id,
  406. 'is_closed': True,
  407. }),
  408. content_type="application/json",
  409. )
  410. self.assertEqual(response.status_code, 400)
  411. self.assertEqual(
  412. response.json(), {
  413. 'is_closed': ["You don't have permission to close threads in this category."],
  414. }
  415. )
  416. @patch_category_acl({"can_merge_threads": True, "can_close_threads": True})
  417. def test_merge_with_close(self):
  418. """api allows for closing thread"""
  419. thread = testutils.post_thread(category=self.category)
  420. response = self.client.post(
  421. self.api_link,
  422. json.dumps({
  423. 'threads': [self.thread.id, thread.id],
  424. 'title': '$$$',
  425. 'category': self.category.id,
  426. 'weight': 0,
  427. 'is_closed': True,
  428. }),
  429. content_type="application/json",
  430. )
  431. self.assertEqual(response.status_code, 400)
  432. self.assertEqual(
  433. response.json(), {
  434. 'title': ["Thread title should be at least 5 characters long (it has 3)."],
  435. }
  436. )
  437. @patch_category_acl({"can_merge_threads": True, "can_hide_threads": 0})
  438. def test_merge_unallowed_hidden(self):
  439. """api rejects merge because hidden thread was unallowed"""
  440. thread = testutils.post_thread(category=self.category)
  441. response = self.client.post(
  442. self.api_link,
  443. json.dumps({
  444. 'threads': [self.thread.id, thread.id],
  445. 'title': 'Valid thread title',
  446. 'category': self.category.id,
  447. 'is_hidden': True,
  448. }),
  449. content_type="application/json",
  450. )
  451. self.assertEqual(response.status_code, 400)
  452. self.assertEqual(
  453. response.json(), {
  454. 'is_hidden': ["You don't have permission to hide threads in this category."],
  455. }
  456. )
  457. @patch_category_acl({"can_merge_threads": True, "can_hide_threads": 1})
  458. def test_merge_with_hide(self):
  459. """api allows for hiding thread"""
  460. thread = testutils.post_thread(category=self.category)
  461. response = self.client.post(
  462. self.api_link,
  463. json.dumps({
  464. 'threads': [self.thread.id, thread.id],
  465. 'title': '$$$',
  466. 'category': self.category.id,
  467. 'weight': 0,
  468. 'is_hidden': True,
  469. }),
  470. content_type="application/json",
  471. )
  472. self.assertEqual(response.status_code, 400)
  473. self.assertEqual(
  474. response.json(), {
  475. 'title': ["Thread title should be at least 5 characters long (it has 3)."],
  476. }
  477. )
  478. @patch_category_acl({"can_merge_threads": True})
  479. def test_merge(self):
  480. """api performs basic merge"""
  481. posts_ids = [p.id for p in Post.objects.all()]
  482. thread = testutils.post_thread(category=self.category)
  483. response = self.client.post(
  484. self.api_link,
  485. json.dumps({
  486. 'threads': [self.thread.id, thread.id],
  487. 'title': 'Merged thread!',
  488. 'category': self.category.id,
  489. }),
  490. content_type="application/json",
  491. )
  492. self.assertEqual(response.status_code, 200)
  493. # is response json with new thread?
  494. response_json = response.json()
  495. new_thread = Thread.objects.get(pk=response_json['id'])
  496. new_thread.is_read = False
  497. new_thread.subscription = None
  498. user_acl = useracl.get_user_acl(self.user, cache_versions)
  499. add_acl(user_acl, new_thread.category)
  500. add_acl(user_acl, new_thread)
  501. self.assertEqual(response_json, ThreadsListSerializer(new_thread).data)
  502. # did posts move to new thread?
  503. for post in Post.objects.filter(id__in=posts_ids):
  504. self.assertEqual(post.thread_id, new_thread.id)
  505. # are old threads gone?
  506. self.assertEqual([t.pk for t in Thread.objects.all()], [new_thread.pk])
  507. @patch_category_acl({
  508. "can_merge_threads": True,
  509. "can_close_threads": True,
  510. "can_hide_threads": 1,
  511. "can_pin_threads": 2,
  512. })
  513. def test_merge_kitchensink(self):
  514. """api performs merge"""
  515. posts_ids = [p.id for p in Post.objects.all()]
  516. thread = testutils.post_thread(category=self.category)
  517. poststracker.save_read(self.user, self.thread.first_post)
  518. poststracker.save_read(self.user, thread.first_post)
  519. self.user.subscription_set.create(
  520. thread=self.thread,
  521. category=self.thread.category,
  522. last_read_on=self.thread.last_post_on,
  523. send_email=False,
  524. )
  525. self.user.subscription_set.create(
  526. thread=thread,
  527. category=thread.category,
  528. last_read_on=thread.last_post_on,
  529. send_email=False,
  530. )
  531. response = self.client.post(
  532. self.api_link,
  533. json.dumps({
  534. 'threads': [self.thread.id, thread.id],
  535. 'title': 'Merged thread!',
  536. 'category': self.category.id,
  537. 'is_closed': 1,
  538. 'is_hidden': 1,
  539. 'weight': 2,
  540. }),
  541. content_type="application/json",
  542. )
  543. self.assertEqual(response.status_code, 200)
  544. # is response json with new thread?
  545. response_json = response.json()
  546. new_thread = Thread.objects.get(pk=response_json['id'])
  547. new_thread.is_read = False
  548. new_thread.subscription = None
  549. self.assertEqual(new_thread.weight, 2)
  550. self.assertTrue(new_thread.is_closed)
  551. self.assertTrue(new_thread.is_hidden)
  552. user_acl = useracl.get_user_acl(self.user, cache_versions)
  553. add_acl(user_acl, new_thread.category)
  554. add_acl(user_acl, new_thread)
  555. self.assertEqual(response_json, ThreadsListSerializer(new_thread).data)
  556. # did posts move to new thread?
  557. for post in Post.objects.filter(id__in=posts_ids):
  558. self.assertEqual(post.thread_id, new_thread.id)
  559. # are old threads gone?
  560. self.assertEqual([t.pk for t in Thread.objects.all()], [new_thread.pk])
  561. # posts reads are kept
  562. postreads = self.user.postread_set.filter(post__is_event=False).order_by('id')
  563. self.assertEqual(
  564. list(postreads.values_list('post_id', flat=True)),
  565. [self.thread.first_post_id, thread.first_post_id],
  566. )
  567. self.assertEqual(postreads.filter(thread=new_thread).count(), 2)
  568. self.assertEqual(postreads.filter(category=self.category).count(), 2)
  569. # subscriptions are kept
  570. self.assertEqual(self.user.subscription_set.count(), 1)
  571. self.user.subscription_set.get(thread=new_thread)
  572. self.user.subscription_set.get(category=self.category)
  573. @patch_category_acl({"can_merge_threads": True})
  574. def test_merge_threads_merged_best_answer(self):
  575. """api merges two threads successfully, moving best answer to old thread"""
  576. other_thread = testutils.post_thread(self.category)
  577. best_answer = testutils.reply_thread(self.thread)
  578. self.thread.set_best_answer(self.user, best_answer)
  579. self.thread.save()
  580. response = self.client.post(
  581. self.api_link,
  582. json.dumps({
  583. 'threads': [self.thread.id, other_thread.id],
  584. 'title': 'Merged thread!',
  585. 'category': self.category.id,
  586. }),
  587. content_type="application/json",
  588. )
  589. self.assertEqual(response.status_code, 200)
  590. # best answer is set on new thread
  591. new_thread = Thread.objects.get(pk=response.json()['id'])
  592. self.assertEqual(new_thread.best_answer_id, best_answer.id)
  593. @patch_category_acl({"can_merge_threads": True})
  594. def test_merge_threads_merge_conflict_best_answer(self):
  595. """api errors on merge conflict, returning list of available best answers"""
  596. best_answer = testutils.reply_thread(self.thread)
  597. self.thread.set_best_answer(self.user, best_answer)
  598. self.thread.save()
  599. other_thread = testutils.post_thread(self.category)
  600. other_best_answer = testutils.reply_thread(other_thread)
  601. other_thread.set_best_answer(self.user, other_best_answer)
  602. other_thread.save()
  603. response = self.client.post(
  604. self.api_link,
  605. json.dumps({
  606. 'threads': [self.thread.id, other_thread.id],
  607. 'title': 'Merged thread!',
  608. 'category': self.category.id,
  609. }),
  610. content_type="application/json",
  611. )
  612. self.assertEqual(response.status_code, 400)
  613. self.assertEqual(response.json(), {
  614. 'best_answers': [
  615. ['0', "Unmark all best answers"],
  616. [str(self.thread.id), self.thread.title],
  617. [str(other_thread.id), other_thread.title],
  618. ]
  619. })
  620. # best answers were untouched
  621. self.assertEqual(self.thread.post_set.count(), 2)
  622. self.assertEqual(other_thread.post_set.count(), 2)
  623. self.assertEqual(Thread.objects.get(pk=self.thread.pk).best_answer_id, best_answer.id)
  624. self.assertEqual(
  625. Thread.objects.get(pk=other_thread.pk).best_answer_id, other_best_answer.id)
  626. @patch_category_acl({"can_merge_threads": True})
  627. def test_threads_merge_conflict_best_answer_invalid_resolution(self):
  628. """api errors on invalid merge conflict resolution"""
  629. best_answer = testutils.reply_thread(self.thread)
  630. self.thread.set_best_answer(self.user, best_answer)
  631. self.thread.save()
  632. other_thread = testutils.post_thread(self.category)
  633. other_best_answer = testutils.reply_thread(other_thread)
  634. other_thread.set_best_answer(self.user, other_best_answer)
  635. other_thread.save()
  636. response = self.client.post(
  637. self.api_link,
  638. json.dumps({
  639. 'threads': [self.thread.id, other_thread.id],
  640. 'title': 'Merged thread!',
  641. 'category': self.category.id,
  642. 'best_answer': other_thread.id + 10,
  643. }),
  644. content_type="application/json",
  645. )
  646. self.assertEqual(response.status_code, 400)
  647. self.assertEqual(response.json(), {'best_answer': ["Invalid choice."]})
  648. # best answers were untouched
  649. self.assertEqual(self.thread.post_set.count(), 2)
  650. self.assertEqual(other_thread.post_set.count(), 2)
  651. self.assertEqual(Thread.objects.get(pk=self.thread.pk).best_answer_id, best_answer.id)
  652. self.assertEqual(
  653. Thread.objects.get(pk=other_thread.pk).best_answer_id, other_best_answer.id)
  654. @patch_category_acl({"can_merge_threads": True})
  655. def test_threads_merge_conflict_unmark_all_best_answers(self):
  656. """api unmarks all best answers when unmark all choice is selected"""
  657. best_answer = testutils.reply_thread(self.thread)
  658. self.thread.set_best_answer(self.user, best_answer)
  659. self.thread.save()
  660. other_thread = testutils.post_thread(self.category)
  661. other_best_answer = testutils.reply_thread(other_thread)
  662. other_thread.set_best_answer(self.user, other_best_answer)
  663. other_thread.save()
  664. response = self.client.post(
  665. self.api_link,
  666. json.dumps({
  667. 'threads': [self.thread.id, other_thread.id],
  668. 'title': 'Merged thread!',
  669. 'category': self.category.id,
  670. 'best_answer': 0,
  671. }),
  672. content_type="application/json",
  673. )
  674. self.assertEqual(response.status_code, 200)
  675. # best answer is not set on new thread
  676. new_thread = Thread.objects.get(pk=response.json()['id'])
  677. self.assertFalse(new_thread.has_best_answer)
  678. self.assertIsNone(new_thread.best_answer_id)
  679. @patch_category_acl({"can_merge_threads": True})
  680. def test_threads_merge_conflict_keep_first_best_answer(self):
  681. """api unmarks other best answer on merge"""
  682. best_answer = testutils.reply_thread(self.thread)
  683. self.thread.set_best_answer(self.user, best_answer)
  684. self.thread.save()
  685. other_thread = testutils.post_thread(self.category)
  686. other_best_answer = testutils.reply_thread(other_thread)
  687. other_thread.set_best_answer(self.user, other_best_answer)
  688. other_thread.save()
  689. response = self.client.post(
  690. self.api_link,
  691. json.dumps({
  692. 'threads': [self.thread.id, other_thread.id],
  693. 'title': 'Merged thread!',
  694. 'category': self.category.id,
  695. 'best_answer': self.thread.pk,
  696. }),
  697. content_type="application/json",
  698. )
  699. self.assertEqual(response.status_code, 200)
  700. # selected best answer is set on new thread
  701. new_thread = Thread.objects.get(pk=response.json()['id'])
  702. self.assertEqual(new_thread.best_answer_id, best_answer.id)
  703. @patch_category_acl({"can_merge_threads": True})
  704. def test_threads_merge_conflict_keep_other_best_answer(self):
  705. """api unmarks first best answer on merge"""
  706. best_answer = testutils.reply_thread(self.thread)
  707. self.thread.set_best_answer(self.user, best_answer)
  708. self.thread.save()
  709. other_thread = testutils.post_thread(self.category)
  710. other_best_answer = testutils.reply_thread(other_thread)
  711. other_thread.set_best_answer(self.user, other_best_answer)
  712. other_thread.save()
  713. response = self.client.post(
  714. self.api_link,
  715. json.dumps({
  716. 'threads': [self.thread.id, other_thread.id],
  717. 'title': 'Merged thread!',
  718. 'category': self.category.id,
  719. 'best_answer': other_thread.pk,
  720. }),
  721. content_type="application/json",
  722. )
  723. self.assertEqual(response.status_code, 200)
  724. # selected best answer is set on new thread
  725. new_thread = Thread.objects.get(pk=response.json()['id'])
  726. self.assertEqual(new_thread.best_answer_id, other_best_answer.id)
  727. @patch_category_acl({"can_merge_threads": True})
  728. def test_merge_threads_kept_poll(self):
  729. """api merges two threads successfully, keeping poll from other thread"""
  730. other_thread = testutils.post_thread(self.category)
  731. poll = testutils.post_poll(other_thread, self.user)
  732. response = self.client.post(
  733. self.api_link,
  734. json.dumps({
  735. 'threads': [self.thread.id, other_thread.id],
  736. 'title': 'Merged thread!',
  737. 'category': self.category.id,
  738. }),
  739. content_type="application/json",
  740. )
  741. self.assertEqual(response.status_code, 200)
  742. new_thread = Thread.objects.get(pk=response.json()['id'])
  743. # poll and its votes were kept
  744. self.assertEqual(Poll.objects.filter(pk=poll.pk, thread=new_thread).count(), 1)
  745. self.assertEqual(PollVote.objects.filter(poll=poll, thread=new_thread).count(), 4)
  746. self.assertEqual(Poll.objects.count(), 1)
  747. self.assertEqual(PollVote.objects.count(), 4)
  748. @patch_category_acl({"can_merge_threads": True})
  749. def test_merge_threads_moved_poll(self):
  750. """api merges two threads successfully, moving poll from old thread"""
  751. other_thread = testutils.post_thread(self.category)
  752. poll = testutils.post_poll(self.thread, self.user)
  753. response = self.client.post(
  754. self.api_link,
  755. json.dumps({
  756. 'threads': [self.thread.id, other_thread.id],
  757. 'title': 'Merged thread!',
  758. 'category': self.category.id,
  759. }),
  760. content_type="application/json",
  761. )
  762. self.assertEqual(response.status_code, 200)
  763. new_thread = Thread.objects.get(pk=response.json()['id'])
  764. # poll and its votes were kept
  765. self.assertEqual(Poll.objects.filter(pk=poll.pk, thread=new_thread).count(), 1)
  766. self.assertEqual(PollVote.objects.filter(poll=poll, thread=new_thread).count(), 4)
  767. self.assertEqual(Poll.objects.count(), 1)
  768. self.assertEqual(PollVote.objects.count(), 4)
  769. @patch_category_acl({"can_merge_threads": True})
  770. def test_threads_merge_conflict_poll(self):
  771. """api errors on merge conflict, returning list of available polls"""
  772. other_thread = testutils.post_thread(self.category)
  773. poll = testutils.post_poll(self.thread, self.user)
  774. other_poll = testutils.post_poll(other_thread, self.user)
  775. response = self.client.post(
  776. self.api_link,
  777. json.dumps({
  778. 'threads': [self.thread.id, other_thread.id],
  779. 'title': 'Merged thread!',
  780. 'category': self.category.id,
  781. }),
  782. content_type="application/json",
  783. )
  784. self.assertEqual(response.status_code, 400)
  785. self.assertEqual(
  786. response.json(), {
  787. 'polls': [
  788. ['0', "Delete all polls"],
  789. [
  790. str(other_poll.pk),
  791. '%s (%s)' % (other_poll.question, other_poll.thread.title),
  792. ],
  793. [
  794. str(poll.pk),
  795. '%s (%s)' % (poll.question, poll.thread.title),
  796. ],
  797. ],
  798. }
  799. )
  800. # polls and votes were untouched
  801. self.assertEqual(Poll.objects.count(), 2)
  802. self.assertEqual(PollVote.objects.count(), 8)
  803. @patch_category_acl({"can_merge_threads": True})
  804. def test_threads_merge_conflict_poll_invalid_resolution(self):
  805. """api errors on invalid merge conflict resolution"""
  806. other_thread = testutils.post_thread(self.category)
  807. testutils.post_poll(self.thread, self.user)
  808. testutils.post_poll(other_thread, self.user)
  809. response = self.client.post(
  810. self.api_link,
  811. json.dumps({
  812. 'threads': [self.thread.id, other_thread.id],
  813. 'title': 'Merged thread!',
  814. 'category': self.category.id,
  815. 'poll': other_thread.poll.id + 10,
  816. }),
  817. content_type="application/json",
  818. )
  819. self.assertEqual(response.status_code, 400)
  820. self.assertEqual(response.json(), {
  821. 'poll': ["Invalid choice."],
  822. })
  823. # polls and votes were untouched
  824. self.assertEqual(Poll.objects.count(), 2)
  825. self.assertEqual(PollVote.objects.count(), 8)
  826. @patch_category_acl({"can_merge_threads": True})
  827. def test_threads_merge_conflict_delete_all_polls(self):
  828. """api deletes all polls when delete all choice is selected"""
  829. other_thread = testutils.post_thread(self.category)
  830. testutils.post_poll(self.thread, self.user)
  831. testutils.post_poll(other_thread, self.user)
  832. response = self.client.post(
  833. self.api_link,
  834. json.dumps({
  835. 'threads': [self.thread.id, other_thread.id],
  836. 'title': 'Merged thread!',
  837. 'category': self.category.id,
  838. 'poll': 0,
  839. }),
  840. content_type="application/json",
  841. )
  842. self.assertEqual(response.status_code, 200)
  843. # polls and votes are gone
  844. self.assertEqual(Poll.objects.count(), 0)
  845. self.assertEqual(PollVote.objects.count(), 0)
  846. @patch_category_acl({"can_merge_threads": True})
  847. def test_threads_merge_conflict_keep_first_poll(self):
  848. """api deletes other poll on merge"""
  849. other_thread = testutils.post_thread(self.category)
  850. poll = testutils.post_poll(self.thread, self.user)
  851. other_poll = testutils.post_poll(other_thread, self.user)
  852. response = self.client.post(
  853. self.api_link,
  854. json.dumps({
  855. 'threads': [self.thread.id, other_thread.id],
  856. 'title': 'Merged thread!',
  857. 'category': self.category.id,
  858. 'poll': poll.pk,
  859. }),
  860. content_type="application/json",
  861. )
  862. self.assertEqual(response.status_code, 200)
  863. # other poll and its votes are gone
  864. self.assertEqual(Poll.objects.count(), 1)
  865. self.assertEqual(PollVote.objects.count(), 4)
  866. Poll.objects.get(pk=poll.pk)
  867. with self.assertRaises(Poll.DoesNotExist):
  868. Poll.objects.get(pk=other_poll.pk)
  869. @patch_category_acl({"can_merge_threads": True})
  870. def test_threads_merge_conflict_keep_other_poll(self):
  871. """api deletes first poll on merge"""
  872. other_thread = testutils.post_thread(self.category)
  873. poll = testutils.post_poll(self.thread, self.user)
  874. other_poll = testutils.post_poll(other_thread, self.user)
  875. response = self.client.post(
  876. self.api_link,
  877. json.dumps({
  878. 'threads': [self.thread.id, other_thread.id],
  879. 'title': 'Merged thread!',
  880. 'category': self.category.id,
  881. 'poll': other_poll.pk,
  882. }),
  883. content_type="application/json",
  884. )
  885. self.assertEqual(response.status_code, 200)
  886. # other poll and its votes are gone
  887. self.assertEqual(Poll.objects.count(), 1)
  888. self.assertEqual(PollVote.objects.count(), 4)
  889. Poll.objects.get(pk=other_poll.pk)
  890. with self.assertRaises(Poll.DoesNotExist):
  891. Poll.objects.get(pk=poll.pk)