test_threads_merge_api.py 36 KB

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