test_threads_merge_api.py 40 KB

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