test_threads_merge_api.py 33 KB

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