test_threads_merge_api.py 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987
  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, 403)
  147. response_json = response.json()
  148. self.assertEqual(
  149. response_json, [
  150. {
  151. 'id': thread.pk,
  152. 'title': thread.title,
  153. 'errors': ["You can't merge threads in this category."],
  154. },
  155. {
  156. 'id': self.thread.pk,
  157. 'title': self.thread.title,
  158. 'errors': ["You can't merge threads in this category."],
  159. },
  160. ]
  161. )
  162. def test_thread_category_is_closed(self):
  163. """api validates if thread's category is open"""
  164. self.override_acl({
  165. 'can_merge_threads': 1,
  166. 'can_close_threads': 0,
  167. })
  168. self.override_other_category()
  169. other_thread = testutils.post_thread(self.category)
  170. self.category.is_closed = True
  171. self.category.save()
  172. response = self.client.post(
  173. self.api_link,
  174. json.dumps({
  175. 'category': self.category_b.pk,
  176. 'title': 'Lorem ipsum dolor',
  177. 'threads': [self.thread.id, other_thread.id],
  178. }),
  179. content_type="application/json",
  180. )
  181. self.assertContains(
  182. response,
  183. "This category is closed. You can't merge it's threads.",
  184. status_code=403,
  185. )
  186. def test_thread_is_closed(self):
  187. """api validates if thread is open"""
  188. self.override_acl({
  189. 'can_merge_threads': 1,
  190. 'can_close_threads': 0,
  191. })
  192. self.override_other_category()
  193. other_thread = testutils.post_thread(self.category)
  194. other_thread.is_closed = True
  195. other_thread.save()
  196. response = self.client.post(
  197. self.api_link,
  198. json.dumps({
  199. 'category': self.category_b.pk,
  200. 'title': 'Lorem ipsum dolor',
  201. 'threads': [self.thread.id, other_thread.id],
  202. }),
  203. content_type="application/json",
  204. )
  205. self.assertContains(
  206. response,
  207. "This thread is closed. You can't merge it with other threads.",
  208. status_code=403,
  209. )
  210. def test_merge_too_many_threads(self):
  211. """api rejects too many threads to merge"""
  212. threads = []
  213. for _ in range(THREADS_LIMIT + 1):
  214. threads.append(testutils.post_thread(category=self.category).pk)
  215. self.override_acl({
  216. 'can_merge_threads': True,
  217. 'can_close_threads': False,
  218. 'can_edit_threads': False,
  219. 'can_reply_threads': False,
  220. })
  221. response = self.client.post(
  222. self.api_link,
  223. json.dumps({
  224. 'threads': threads,
  225. }),
  226. content_type="application/json",
  227. )
  228. self.assertEqual(response.status_code, 400)
  229. response_json = response.json()
  230. self.assertEqual(
  231. response_json['threads'],
  232. ["No more than %s threads can be merged at single time." % THREADS_LIMIT],
  233. )
  234. def test_merge_no_final_thread(self):
  235. """api rejects merge because no data to merge threads was specified"""
  236. self.override_acl({
  237. 'can_merge_threads': True,
  238. 'can_close_threads': False,
  239. 'can_edit_threads': False,
  240. 'can_reply_threads': False,
  241. })
  242. thread = testutils.post_thread(category=self.category)
  243. response = self.client.post(
  244. self.api_link,
  245. json.dumps({
  246. 'threads': [self.thread.id, thread.id],
  247. }),
  248. content_type="application/json",
  249. )
  250. self.assertEqual(response.status_code, 400)
  251. response_json = response.json()
  252. self.assertEqual(
  253. response_json, {
  254. 'title': ['This field is required.'],
  255. 'category': ['This field is required.'],
  256. }
  257. )
  258. def test_merge_invalid_final_title(self):
  259. """api rejects merge because final thread title was invalid"""
  260. self.override_acl({
  261. 'can_merge_threads': True,
  262. 'can_close_threads': False,
  263. 'can_edit_threads': False,
  264. 'can_reply_threads': False,
  265. })
  266. thread = testutils.post_thread(category=self.category)
  267. response = self.client.post(
  268. self.api_link,
  269. json.dumps({
  270. 'threads': [self.thread.id, thread.id],
  271. 'title': '$$$',
  272. 'category': self.category.id,
  273. }),
  274. content_type="application/json",
  275. )
  276. self.assertEqual(response.status_code, 400)
  277. response_json = response.json()
  278. self.assertEqual(
  279. response_json, {
  280. 'title': ["Thread title should be at least 5 characters long (it has 3)."],
  281. }
  282. )
  283. def test_merge_invalid_category(self):
  284. """api rejects merge because final category was invalid"""
  285. self.override_acl({
  286. 'can_merge_threads': True,
  287. 'can_close_threads': False,
  288. 'can_edit_threads': False,
  289. 'can_reply_threads': False,
  290. })
  291. thread = testutils.post_thread(category=self.category)
  292. response = self.client.post(
  293. self.api_link,
  294. json.dumps({
  295. 'threads': [self.thread.id, thread.id],
  296. 'title': 'Valid thread title',
  297. 'category': self.category_b.id,
  298. }),
  299. content_type="application/json",
  300. )
  301. self.assertEqual(response.status_code, 400)
  302. response_json = response.json()
  303. self.assertEqual(
  304. response_json, {
  305. 'category': ["Requested category could not be found."],
  306. }
  307. )
  308. def test_merge_unallowed_start_thread(self):
  309. """api rejects merge because category isn't allowing starting threads"""
  310. self.override_acl({
  311. 'can_merge_threads': True,
  312. 'can_close_threads': False,
  313. 'can_edit_threads': False,
  314. 'can_reply_threads': False,
  315. 'can_start_threads': 0,
  316. })
  317. thread = testutils.post_thread(category=self.category)
  318. response = self.client.post(
  319. self.api_link,
  320. json.dumps({
  321. 'threads': [self.thread.id, thread.id],
  322. 'title': 'Valid thread title',
  323. 'category': self.category.id,
  324. }),
  325. content_type="application/json",
  326. )
  327. self.assertEqual(response.status_code, 400)
  328. response_json = response.json()
  329. self.assertEqual(
  330. response_json, {
  331. 'category': ["You can't create new threads in selected category."],
  332. }
  333. )
  334. def test_merge_invalid_weight(self):
  335. """api rejects merge because final weight was invalid"""
  336. self.override_acl({
  337. 'can_merge_threads': True,
  338. 'can_close_threads': False,
  339. 'can_edit_threads': False,
  340. 'can_reply_threads': False,
  341. })
  342. thread = testutils.post_thread(category=self.category)
  343. response = self.client.post(
  344. self.api_link,
  345. json.dumps({
  346. 'threads': [self.thread.id, thread.id],
  347. 'title': 'Valid thread title',
  348. 'category': self.category.id,
  349. 'weight': 4,
  350. }),
  351. content_type="application/json",
  352. )
  353. self.assertEqual(response.status_code, 400)
  354. response_json = response.json()
  355. self.assertEqual(
  356. response_json, {
  357. 'weight': ["Ensure this value is less than or equal to 2."],
  358. }
  359. )
  360. def test_merge_unallowed_global_weight(self):
  361. """api rejects merge because global weight was unallowed"""
  362. self.override_acl({
  363. 'can_merge_threads': True,
  364. 'can_close_threads': False,
  365. 'can_edit_threads': False,
  366. 'can_reply_threads': False,
  367. })
  368. thread = testutils.post_thread(category=self.category)
  369. response = self.client.post(
  370. self.api_link,
  371. json.dumps({
  372. 'threads': [self.thread.id, thread.id],
  373. 'title': 'Valid thread title',
  374. 'category': self.category.id,
  375. 'weight': 2,
  376. }),
  377. content_type="application/json",
  378. )
  379. self.assertEqual(response.status_code, 400)
  380. response_json = response.json()
  381. self.assertEqual(
  382. response_json, {
  383. 'weight': ["You don't have permission to pin threads globally in this category."],
  384. }
  385. )
  386. def test_merge_unallowed_local_weight(self):
  387. """api rejects merge because local weight was unallowed"""
  388. self.override_acl({
  389. 'can_merge_threads': True,
  390. 'can_close_threads': False,
  391. 'can_edit_threads': False,
  392. 'can_reply_threads': False,
  393. })
  394. thread = testutils.post_thread(category=self.category)
  395. response = self.client.post(
  396. self.api_link,
  397. json.dumps({
  398. 'threads': [self.thread.id, thread.id],
  399. 'title': 'Valid thread title',
  400. 'category': self.category.id,
  401. 'weight': 1,
  402. }),
  403. content_type="application/json",
  404. )
  405. self.assertEqual(response.status_code, 400)
  406. response_json = response.json()
  407. self.assertEqual(
  408. response_json, {
  409. 'weight': ["You don't have permission to pin threads in this category."],
  410. }
  411. )
  412. def test_merge_allowed_local_weight(self):
  413. """api allows local weight"""
  414. self.override_acl({
  415. 'can_merge_threads': True,
  416. 'can_close_threads': False,
  417. 'can_edit_threads': False,
  418. 'can_reply_threads': False,
  419. 'can_pin_threads': 1,
  420. })
  421. thread = testutils.post_thread(category=self.category)
  422. response = self.client.post(
  423. self.api_link,
  424. json.dumps({
  425. 'threads': [self.thread.id, thread.id],
  426. 'title': '$$$',
  427. 'category': self.category.id,
  428. 'weight': 1,
  429. }),
  430. content_type="application/json",
  431. )
  432. self.assertEqual(response.status_code, 400)
  433. response_json = response.json()
  434. self.assertEqual(
  435. response_json, {
  436. 'title': ["Thread title should be at least 5 characters long (it has 3)."],
  437. }
  438. )
  439. def test_merge_allowed_global_weight(self):
  440. """api allows global weight"""
  441. self.override_acl({
  442. 'can_merge_threads': True,
  443. 'can_close_threads': False,
  444. 'can_edit_threads': False,
  445. 'can_reply_threads': False,
  446. 'can_pin_threads': 2,
  447. })
  448. thread = testutils.post_thread(category=self.category)
  449. response = self.client.post(
  450. self.api_link,
  451. json.dumps({
  452. 'threads': [self.thread.id, thread.id],
  453. 'title': '$$$',
  454. 'category': self.category.id,
  455. 'weight': 2,
  456. }),
  457. content_type="application/json",
  458. )
  459. self.assertEqual(response.status_code, 400)
  460. response_json = response.json()
  461. self.assertEqual(
  462. response_json, {
  463. 'title': ["Thread title should be at least 5 characters long (it has 3)."],
  464. }
  465. )
  466. def test_merge_unallowed_close(self):
  467. """api rejects merge because closing thread was unallowed"""
  468. self.override_acl({
  469. 'can_merge_threads': True,
  470. 'can_close_threads': False,
  471. 'can_edit_threads': False,
  472. 'can_reply_threads': False,
  473. })
  474. thread = testutils.post_thread(category=self.category)
  475. response = self.client.post(
  476. self.api_link,
  477. json.dumps({
  478. 'threads': [self.thread.id, thread.id],
  479. 'title': 'Valid thread title',
  480. 'category': self.category.id,
  481. 'is_closed': True,
  482. }),
  483. content_type="application/json",
  484. )
  485. self.assertEqual(response.status_code, 400)
  486. response_json = response.json()
  487. self.assertEqual(
  488. response_json, {
  489. 'is_closed': ["You don't have permission to close threads in this category."],
  490. }
  491. )
  492. def test_merge_with_close(self):
  493. """api allows for closing thread"""
  494. self.override_acl({
  495. 'can_merge_threads': True,
  496. 'can_edit_threads': False,
  497. 'can_reply_threads': False,
  498. 'can_close_threads': True,
  499. })
  500. thread = testutils.post_thread(category=self.category)
  501. response = self.client.post(
  502. self.api_link,
  503. json.dumps({
  504. 'threads': [self.thread.id, thread.id],
  505. 'title': '$$$',
  506. 'category': self.category.id,
  507. 'weight': 0,
  508. 'is_closed': True,
  509. }),
  510. content_type="application/json",
  511. )
  512. self.assertEqual(response.status_code, 400)
  513. response_json = response.json()
  514. self.assertEqual(
  515. response_json, {
  516. 'title': ["Thread title should be at least 5 characters long (it has 3)."],
  517. }
  518. )
  519. def test_merge_unallowed_hidden(self):
  520. """api rejects merge because hidden thread was unallowed"""
  521. self.override_acl({
  522. 'can_merge_threads': True,
  523. 'can_close_threads': False,
  524. 'can_edit_threads': False,
  525. 'can_reply_threads': False,
  526. 'can_hide_threads': 0,
  527. })
  528. thread = testutils.post_thread(category=self.category)
  529. response = self.client.post(
  530. self.api_link,
  531. json.dumps({
  532. 'threads': [self.thread.id, thread.id],
  533. 'title': 'Valid thread title',
  534. 'category': self.category.id,
  535. 'is_hidden': True,
  536. }),
  537. content_type="application/json",
  538. )
  539. self.assertEqual(response.status_code, 400)
  540. response_json = response.json()
  541. self.assertEqual(
  542. response_json, {
  543. 'is_hidden': ["You don't have permission to hide threads in this category."],
  544. }
  545. )
  546. def test_merge_with_hide(self):
  547. """api allows for hiding thread"""
  548. self.override_acl({
  549. 'can_merge_threads': True,
  550. 'can_close_threads': False,
  551. 'can_edit_threads': False,
  552. 'can_reply_threads': False,
  553. 'can_hide_threads': 1,
  554. })
  555. thread = testutils.post_thread(category=self.category)
  556. response = self.client.post(
  557. self.api_link,
  558. json.dumps({
  559. 'threads': [self.thread.id, thread.id],
  560. 'title': '$$$',
  561. 'category': self.category.id,
  562. 'weight': 0,
  563. 'is_hidden': True,
  564. }),
  565. content_type="application/json",
  566. )
  567. self.assertEqual(response.status_code, 400)
  568. response_json = response.json()
  569. self.assertEqual(
  570. response_json, {
  571. 'title': ["Thread title should be at least 5 characters long (it has 3)."],
  572. }
  573. )
  574. def test_merge(self):
  575. """api performs basic merge"""
  576. posts_ids = [p.id for p in Post.objects.all()]
  577. self.override_acl({
  578. 'can_merge_threads': True,
  579. 'can_close_threads': False,
  580. 'can_edit_threads': False,
  581. 'can_reply_threads': False,
  582. })
  583. thread = testutils.post_thread(category=self.category)
  584. response = self.client.post(
  585. self.api_link,
  586. json.dumps({
  587. 'threads': [self.thread.id, thread.id],
  588. 'title': 'Merged thread!',
  589. 'category': self.category.id,
  590. }),
  591. content_type="application/json",
  592. )
  593. self.assertEqual(response.status_code, 200)
  594. # is response json with new thread?
  595. response_json = response.json()
  596. new_thread = Thread.objects.get(pk=response_json['id'])
  597. new_thread.is_read = False
  598. new_thread.subscription = None
  599. add_acl(self.user, new_thread.category)
  600. add_acl(self.user, new_thread)
  601. self.assertEqual(response_json, ThreadsListSerializer(new_thread).data)
  602. # did posts move to new thread?
  603. for post in Post.objects.filter(id__in=posts_ids):
  604. self.assertEqual(post.thread_id, new_thread.id)
  605. # are old threads gone?
  606. self.assertEqual([t.pk for t in Thread.objects.all()], [new_thread.pk])
  607. def test_merge_kitchensink(self):
  608. """api performs merge"""
  609. posts_ids = [p.id for p in Post.objects.all()]
  610. self.override_acl({
  611. 'can_merge_threads': True,
  612. 'can_close_threads': True,
  613. 'can_hide_threads': 1,
  614. 'can_pin_threads': 2,
  615. })
  616. thread = testutils.post_thread(category=self.category)
  617. poststracker.save_read(self.user, self.thread.first_post)
  618. poststracker.save_read(self.user, thread.first_post)
  619. self.user.subscription_set.create(
  620. thread=self.thread,
  621. category=self.thread.category,
  622. last_read_on=self.thread.last_post_on,
  623. send_email=False,
  624. )
  625. self.user.subscription_set.create(
  626. thread=thread,
  627. category=thread.category,
  628. last_read_on=thread.last_post_on,
  629. send_email=False,
  630. )
  631. response = self.client.post(
  632. self.api_link,
  633. json.dumps({
  634. 'threads': [self.thread.id, thread.id],
  635. 'title': 'Merged thread!',
  636. 'category': self.category.id,
  637. 'is_closed': 1,
  638. 'is_hidden': 1,
  639. 'weight': 2,
  640. }),
  641. content_type="application/json",
  642. )
  643. self.assertEqual(response.status_code, 200)
  644. # is response json with new thread?
  645. response_json = response.json()
  646. new_thread = Thread.objects.get(pk=response_json['id'])
  647. new_thread.is_read = False
  648. new_thread.subscription = None
  649. self.assertEqual(new_thread.weight, 2)
  650. self.assertTrue(new_thread.is_closed)
  651. self.assertTrue(new_thread.is_hidden)
  652. add_acl(self.user, new_thread.category)
  653. add_acl(self.user, new_thread)
  654. self.assertEqual(response_json, ThreadsListSerializer(new_thread).data)
  655. # did posts move to new thread?
  656. for post in Post.objects.filter(id__in=posts_ids):
  657. self.assertEqual(post.thread_id, new_thread.id)
  658. # are old threads gone?
  659. self.assertEqual([t.pk for t in Thread.objects.all()], [new_thread.pk])
  660. # posts reads are kept
  661. postreads = self.user.postread_set.filter(post__is_event=False).order_by('id')
  662. self.assertEqual(
  663. list(postreads.values_list('post_id', flat=True)),
  664. [self.thread.first_post_id, thread.first_post_id]
  665. )
  666. self.assertEqual(postreads.filter(thread=new_thread).count(), 2)
  667. self.assertEqual(postreads.filter(category=self.category).count(), 2)
  668. # subscriptions are kept
  669. self.assertEqual(self.user.subscription_set.count(), 1)
  670. self.user.subscription_set.get(thread=new_thread)
  671. self.user.subscription_set.get(category=self.category)
  672. def test_merge_threads_kept_poll(self):
  673. """api merges two threads successfully, keeping poll from old thread"""
  674. self.override_acl({'can_merge_threads': True})
  675. other_thread = testutils.post_thread(self.category)
  676. poll = testutils.post_poll(other_thread, self.user)
  677. response = self.client.post(
  678. self.api_link,
  679. json.dumps({
  680. 'threads': [self.thread.id, other_thread.id],
  681. 'title': 'Merged thread!',
  682. 'category': self.category.id,
  683. }),
  684. content_type="application/json",
  685. )
  686. self.assertEqual(response.status_code, 200)
  687. response_json = response.json()
  688. new_thread = Thread.objects.get(pk=response_json['id'])
  689. # poll and its votes were kept
  690. self.assertEqual(Poll.objects.filter(pk=poll.pk, thread=new_thread).count(), 1)
  691. self.assertEqual(PollVote.objects.filter(poll=poll, thread=new_thread).count(), 4)
  692. self.assertEqual(Poll.objects.count(), 1)
  693. self.assertEqual(PollVote.objects.count(), 4)
  694. def test_merge_threads_moved_poll(self):
  695. """api merges two threads successfully, moving poll from other thread"""
  696. self.override_acl({'can_merge_threads': True})
  697. other_thread = testutils.post_thread(self.category)
  698. poll = testutils.post_poll(self.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_threads_merge_conflict(self):
  717. """api errors on merge conflict, returning list of available polls"""
  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. other_poll = testutils.post_poll(other_thread, self.user)
  722. response = self.client.post(
  723. self.api_link,
  724. json.dumps({
  725. 'threads': [self.thread.id, other_thread.id],
  726. 'title': 'Merged thread!',
  727. 'category': self.category.id,
  728. }),
  729. content_type="application/json",
  730. )
  731. self.assertEqual(response.status_code, 400)
  732. self.assertEqual(
  733. response.json(), {
  734. 'polls': [
  735. [0, "Delete all polls"],
  736. [poll.pk, poll.question],
  737. [other_poll.pk, other_poll.question],
  738. ],
  739. }
  740. )
  741. # polls and votes were untouched
  742. self.assertEqual(Poll.objects.count(), 2)
  743. self.assertEqual(PollVote.objects.count(), 8)
  744. def test_threads_merge_conflict_invalid_resolution(self):
  745. """api errors on invalid merge conflict resolution"""
  746. self.override_acl({'can_merge_threads': True})
  747. other_thread = testutils.post_thread(self.category)
  748. testutils.post_poll(self.thread, self.user)
  749. testutils.post_poll(other_thread, self.user)
  750. response = self.client.post(
  751. self.api_link,
  752. json.dumps({
  753. 'threads': [self.thread.id, other_thread.id],
  754. 'title': 'Merged thread!',
  755. 'category': self.category.id,
  756. 'poll': 'dsa7dsadsa9789',
  757. }),
  758. content_type="application/json",
  759. )
  760. self.assertEqual(response.status_code, 400)
  761. self.assertEqual(response.json(), {
  762. 'detail': "Invalid choice.",
  763. })
  764. # polls and votes were untouched
  765. self.assertEqual(Poll.objects.count(), 2)
  766. self.assertEqual(PollVote.objects.count(), 8)
  767. def test_threads_merge_conflict_delete_all(self):
  768. """api deletes all polls when delete all choice is selected"""
  769. self.override_acl({'can_merge_threads': True})
  770. other_thread = testutils.post_thread(self.category)
  771. testutils.post_poll(self.thread, self.user)
  772. testutils.post_poll(other_thread, self.user)
  773. response = self.client.post(
  774. self.api_link,
  775. json.dumps({
  776. 'threads': [self.thread.id, other_thread.id],
  777. 'title': 'Merged thread!',
  778. 'category': self.category.id,
  779. 'poll': 0,
  780. }),
  781. content_type="application/json",
  782. )
  783. self.assertEqual(response.status_code, 200)
  784. # polls and votes are gone
  785. self.assertEqual(Poll.objects.count(), 0)
  786. self.assertEqual(PollVote.objects.count(), 0)
  787. def test_threads_merge_conflict_keep_first_poll(self):
  788. """api deletes other poll on merge"""
  789. self.override_acl({'can_merge_threads': True})
  790. other_thread = testutils.post_thread(self.category)
  791. poll = testutils.post_poll(self.thread, self.user)
  792. other_poll = testutils.post_poll(other_thread, self.user)
  793. response = self.client.post(
  794. self.api_link,
  795. json.dumps({
  796. 'threads': [self.thread.id, other_thread.id],
  797. 'title': 'Merged thread!',
  798. 'category': self.category.id,
  799. 'poll': poll.pk,
  800. }),
  801. content_type="application/json",
  802. )
  803. self.assertEqual(response.status_code, 200)
  804. # other poll and its votes are gone
  805. self.assertEqual(Poll.objects.count(), 1)
  806. self.assertEqual(PollVote.objects.count(), 4)
  807. Poll.objects.get(pk=poll.pk)
  808. with self.assertRaises(Poll.DoesNotExist):
  809. Poll.objects.get(pk=other_poll.pk)
  810. def test_threads_merge_conflict_keep_other_poll(self):
  811. """api deletes first poll on merge"""
  812. self.override_acl({'can_merge_threads': True})
  813. other_thread = testutils.post_thread(self.category)
  814. poll = testutils.post_poll(self.thread, self.user)
  815. other_poll = testutils.post_poll(other_thread, self.user)
  816. response = self.client.post(
  817. self.api_link,
  818. json.dumps({
  819. 'threads': [self.thread.id, other_thread.id],
  820. 'title': 'Merged thread!',
  821. 'category': self.category.id,
  822. 'poll': other_poll.pk,
  823. }),
  824. content_type="application/json",
  825. )
  826. self.assertEqual(response.status_code, 200)
  827. # other poll and its votes are gone
  828. self.assertEqual(Poll.objects.count(), 1)
  829. self.assertEqual(PollVote.objects.count(), 4)
  830. Poll.objects.get(pk=other_poll.pk)
  831. with self.assertRaises(Poll.DoesNotExist):
  832. Poll.objects.get(pk=poll.pk)