test_thread_postsplit_api.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750
  1. # -*- coding: utf-8 -*-
  2. from __future__ import unicode_literals
  3. import json
  4. from django.urls import reverse
  5. from misago.acl.testutils import override_acl
  6. from misago.categories.models import Category
  7. from misago.readtracker import poststracker
  8. from misago.threads import testutils
  9. from misago.threads.models import Post, Thread
  10. from misago.threads.serializers.moderation import POSTS_LIMIT
  11. from misago.users.testutils import AuthenticatedUserTestCase
  12. class ThreadPostSplitApiTestCase(AuthenticatedUserTestCase):
  13. def setUp(self):
  14. super(ThreadPostSplitApiTestCase, self).setUp()
  15. self.category = Category.objects.get(slug='first-category')
  16. self.thread = testutils.post_thread(category=self.category)
  17. self.posts = [
  18. testutils.reply_thread(self.thread).pk,
  19. testutils.reply_thread(self.thread).pk,
  20. ]
  21. self.api_link = reverse(
  22. 'misago:api:thread-post-split', kwargs={
  23. 'thread_pk': self.thread.pk,
  24. }
  25. )
  26. Category(
  27. name='Category B',
  28. slug='category-b',
  29. ).insert_at(
  30. self.category,
  31. position='last-child',
  32. save=True,
  33. )
  34. self.category_b = Category.objects.get(slug='category-b')
  35. self.override_acl()
  36. self.override_other_acl()
  37. def refresh_thread(self):
  38. self.thread = Thread.objects.get(pk=self.thread.pk)
  39. def override_acl(self, extra_acl=None):
  40. new_acl = self.user.acl_cache
  41. new_acl['categories'][self.category.pk].update({
  42. 'can_see': 1,
  43. 'can_browse': 1,
  44. 'can_start_threads': 1,
  45. 'can_reply_threads': 1,
  46. 'can_edit_posts': 1,
  47. 'can_approve_content': 0,
  48. 'can_move_posts': 1,
  49. })
  50. if extra_acl:
  51. new_acl['categories'][self.category.pk].update(extra_acl)
  52. override_acl(self.user, new_acl)
  53. def override_other_acl(self, acl=None):
  54. other_category_acl = self.user.acl_cache['categories'][self.category.pk].copy()
  55. other_category_acl.update({
  56. 'can_see': 1,
  57. 'can_browse': 1,
  58. 'can_start_threads': 0,
  59. 'can_reply_threads': 0,
  60. 'can_edit_posts': 1,
  61. 'can_approve_content': 0,
  62. 'can_move_posts': 1,
  63. })
  64. if acl:
  65. other_category_acl.update(acl)
  66. categories_acl = self.user.acl_cache['categories']
  67. categories_acl[self.category_b.pk] = other_category_acl
  68. visible_categories = [self.category.pk]
  69. if other_category_acl['can_see']:
  70. visible_categories.append(self.category_b.pk)
  71. override_acl(
  72. self.user, {
  73. 'visible_categories': visible_categories,
  74. 'categories': categories_acl,
  75. }
  76. )
  77. def test_anonymous_user(self):
  78. """you need to authenticate to split posts"""
  79. self.logout_user()
  80. response = self.client.post(self.api_link, json.dumps({}), content_type="application/json")
  81. self.assertEqual(response.status_code, 403)
  82. self.assertEqual(response.json(), {
  83. 'detail': "This action is not available to guests.",
  84. })
  85. def test_no_permission(self):
  86. """api validates permission to split"""
  87. self.override_acl({'can_move_posts': 0})
  88. response = self.client.post(self.api_link, json.dumps({}), content_type="application/json")
  89. self.assertEqual(response.status_code, 403)
  90. self.assertEqual(response.json(), {
  91. 'detail': "You can't split posts from this thread.",
  92. })
  93. def test_empty_data(self):
  94. """api handles empty data"""
  95. response = self.client.post(self.api_link)
  96. self.assertEqual(response.status_code, 400)
  97. self.assertEqual(response.json(), {
  98. 'title': ["This field is required."],
  99. 'category': ["This field is required."],
  100. 'posts': ["You have to specify at least one post to split."],
  101. })
  102. def test_invalid_data(self):
  103. """api handles post that is invalid type"""
  104. self.override_acl()
  105. response = self.client.post(self.api_link, '[]', content_type="application/json")
  106. self.assertEqual(response.status_code, 400)
  107. self.assertEqual(response.json(), {
  108. 'non_field_errors': ["Invalid data. Expected a dictionary, but got list."],
  109. })
  110. self.override_acl()
  111. response = self.client.post(self.api_link, '123', content_type="application/json")
  112. self.assertEqual(response.status_code, 400)
  113. self.assertEqual(response.json(), {
  114. 'non_field_errors': ["Invalid data. Expected a dictionary, but got int."],
  115. })
  116. self.override_acl()
  117. response = self.client.post(self.api_link, '"string"', content_type="application/json")
  118. self.assertEqual(response.status_code, 400)
  119. self.assertEqual(response.json(), {
  120. 'non_field_errors': ["Invalid data. Expected a dictionary, but got str."],
  121. })
  122. self.override_acl()
  123. response = self.client.post(self.api_link, 'malformed', content_type="application/json")
  124. self.assertEqual(response.status_code, 400)
  125. self.assertEqual(response.json(), {
  126. 'detail': "JSON parse error - Expecting value: line 1 column 1 (char 0)",
  127. })
  128. def test_no_posts_ids(self):
  129. """api rejects no posts ids"""
  130. response = self.client.post(
  131. self.api_link,
  132. json.dumps({}),
  133. content_type="application/json",
  134. )
  135. self.assertEqual(response.status_code, 400)
  136. self.assertEqual(response.json(), {
  137. 'title': ["This field is required."],
  138. 'category': ["This field is required."],
  139. 'posts': ["You have to specify at least one post to split."],
  140. })
  141. def test_empty_posts_ids(self):
  142. """api rejects empty posts ids list"""
  143. response = self.client.post(
  144. self.api_link,
  145. json.dumps({
  146. 'posts': [],
  147. }),
  148. content_type="application/json",
  149. )
  150. self.assertEqual(response.status_code, 400)
  151. self.assertEqual(response.json(), {
  152. 'title': ["This field is required."],
  153. 'category': ["This field is required."],
  154. 'posts': ["You have to specify at least one post to split."],
  155. })
  156. def test_invalid_posts_data(self):
  157. """api handles invalid data"""
  158. response = self.client.post(
  159. self.api_link,
  160. json.dumps({
  161. 'posts': 'string',
  162. }),
  163. content_type="application/json",
  164. )
  165. self.assertEqual(response.status_code, 400)
  166. self.assertEqual(response.json(), {
  167. 'title': ["This field is required."],
  168. 'category': ["This field is required."],
  169. 'posts': ['Expected a list of items but got type "str".'],
  170. })
  171. def test_invalid_posts_ids(self):
  172. """api handles invalid post id"""
  173. response = self.client.post(
  174. self.api_link,
  175. json.dumps({
  176. 'posts': [1, 2, 'string'],
  177. }),
  178. content_type="application/json",
  179. )
  180. self.assertEqual(response.status_code, 400)
  181. self.assertEqual(response.json(), {
  182. 'title': ["This field is required."],
  183. 'category': ["This field is required."],
  184. 'posts': ["One or more post ids received were invalid."],
  185. })
  186. def test_split_limit(self):
  187. """api rejects more posts than split limit"""
  188. response = self.client.post(
  189. self.api_link,
  190. json.dumps({
  191. 'posts': list(range(POSTS_LIMIT + 1)),
  192. }),
  193. content_type="application/json",
  194. )
  195. self.assertEqual(response.status_code, 400)
  196. self.assertEqual(response.json(), {
  197. 'title': ["This field is required."],
  198. 'category': ["This field is required."],
  199. 'posts': ["No more than {} posts can be split at single time.".format(POSTS_LIMIT)],
  200. })
  201. def test_split_invisible(self):
  202. """api validates posts visibility"""
  203. response = self.client.post(
  204. self.api_link,
  205. json.dumps({
  206. 'posts': [testutils.reply_thread(self.thread, is_unapproved=True).pk],
  207. }),
  208. content_type="application/json",
  209. )
  210. self.assertEqual(response.status_code, 400)
  211. self.assertEqual(response.json(), {
  212. 'title': ["This field is required."],
  213. 'category': ["This field is required."],
  214. 'posts': ["One or more posts to split could not be found."],
  215. })
  216. def test_split_event(self):
  217. """api rejects events split"""
  218. response = self.client.post(
  219. self.api_link,
  220. json.dumps({
  221. 'posts': [testutils.reply_thread(self.thread, is_event=True).pk],
  222. }),
  223. content_type="application/json",
  224. )
  225. self.assertEqual(response.status_code, 400)
  226. self.assertEqual(response.json(), {
  227. 'title': ["This field is required."],
  228. 'category': ["This field is required."],
  229. 'posts': ["Events can't be split."],
  230. })
  231. def test_split_first_post(self):
  232. """api rejects first post split"""
  233. response = self.client.post(
  234. self.api_link,
  235. json.dumps({
  236. 'posts': [self.thread.first_post_id],
  237. }),
  238. content_type="application/json",
  239. )
  240. self.assertEqual(response.status_code, 400)
  241. self.assertEqual(response.json(), {
  242. 'title': ["This field is required."],
  243. 'category': ["This field is required."],
  244. 'posts': ["You can't split thread's first post."],
  245. })
  246. def test_split_hidden_posts(self):
  247. """api recjects attempt to split urneadable hidden post"""
  248. response = self.client.post(
  249. self.api_link,
  250. json.dumps({
  251. 'posts': [testutils.reply_thread(self.thread, is_hidden=True).pk],
  252. }),
  253. content_type="application/json",
  254. )
  255. self.assertEqual(response.status_code, 400)
  256. self.assertEqual(response.json(), {
  257. 'title': ["This field is required."],
  258. 'category': ["This field is required."],
  259. 'posts': ["You can't split posts the content you can't see."],
  260. })
  261. def test_split_posts_closed_thread_no_permission(self):
  262. """api recjects attempt to split posts from closed thread"""
  263. self.thread.is_closed = True
  264. self.thread.save()
  265. self.override_acl({'can_close_threads': 0})
  266. response = self.client.post(
  267. self.api_link,
  268. json.dumps({
  269. 'posts': [testutils.reply_thread(self.thread).pk],
  270. }),
  271. content_type="application/json",
  272. )
  273. self.assertEqual(response.status_code, 400)
  274. self.assertEqual(response.json(), {
  275. 'title': ["This field is required."],
  276. 'category': ["This field is required."],
  277. 'posts': ["This thread is closed. You can't split posts in it."],
  278. })
  279. def test_split_posts_closed_category_no_permission(self):
  280. """api recjects attempt to split posts from closed thread"""
  281. self.category.is_closed = True
  282. self.category.save()
  283. self.override_acl({'can_close_threads': 0})
  284. response = self.client.post(
  285. self.api_link,
  286. json.dumps({
  287. 'posts': [testutils.reply_thread(self.thread).pk],
  288. }),
  289. content_type="application/json",
  290. )
  291. self.assertEqual(response.status_code, 400)
  292. self.assertEqual(response.json(), {
  293. 'title': ["This field is required."],
  294. 'category': ["This field is required."],
  295. 'posts': ["This category is closed. You can't split posts in it."],
  296. })
  297. def test_split_other_thread_posts(self):
  298. """api recjects attempt to split other thread's post"""
  299. other_thread = testutils.post_thread(self.category)
  300. response = self.client.post(
  301. self.api_link,
  302. json.dumps({
  303. 'posts': [testutils.reply_thread(other_thread, is_hidden=True).pk],
  304. }),
  305. content_type="application/json",
  306. )
  307. self.assertEqual(response.status_code, 400)
  308. self.assertEqual(response.json(), {
  309. 'title': ["This field is required."],
  310. 'category': ["This field is required."],
  311. 'posts': ["One or more posts to split could not be found."],
  312. })
  313. def test_split_empty_new_thread_data(self):
  314. """api handles empty form data"""
  315. response = self.client.post(
  316. self.api_link,
  317. json.dumps({
  318. 'posts': self.posts,
  319. }),
  320. content_type="application/json",
  321. )
  322. self.assertEqual(response.status_code, 400)
  323. response_json = response.json()
  324. self.assertEqual(
  325. response_json, {
  326. 'title': ['This field is required.'],
  327. 'category': ['This field is required.'],
  328. }
  329. )
  330. def test_split_invalid_final_title(self):
  331. """api rejects split because final thread title was invalid"""
  332. response = self.client.post(
  333. self.api_link,
  334. json.dumps({
  335. 'posts': self.posts,
  336. 'title': '$$$',
  337. 'category': self.category.id,
  338. }),
  339. content_type="application/json",
  340. )
  341. self.assertEqual(response.status_code, 400)
  342. response_json = response.json()
  343. self.assertEqual(
  344. response_json, {
  345. 'title': ["Thread title should be at least 5 characters long (it has 3)."],
  346. }
  347. )
  348. def test_split_invalid_category(self):
  349. """api rejects split because final category was invalid"""
  350. self.override_other_acl({'can_see': 0})
  351. response = self.client.post(
  352. self.api_link,
  353. json.dumps({
  354. 'posts': self.posts,
  355. 'title': 'Valid thread title',
  356. 'category': self.category_b.id,
  357. }),
  358. content_type="application/json",
  359. )
  360. self.assertEqual(response.status_code, 400)
  361. response_json = response.json()
  362. self.assertEqual(
  363. response_json, {
  364. 'category': ["Requested category could not be found."],
  365. }
  366. )
  367. def test_split_unallowed_start_thread(self):
  368. """api rejects split because category isn't allowing starting threads"""
  369. self.override_acl({'can_start_threads': 0})
  370. response = self.client.post(
  371. self.api_link,
  372. json.dumps({
  373. 'posts': self.posts,
  374. 'title': 'Valid thread title',
  375. 'category': self.category.id,
  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. 'category': ["You can't create new threads in selected category."],
  384. }
  385. )
  386. def test_split_invalid_weight(self):
  387. """api rejects split because final weight was invalid"""
  388. response = self.client.post(
  389. self.api_link,
  390. json.dumps({
  391. 'posts': self.posts,
  392. 'title': 'Valid thread title',
  393. 'category': self.category.id,
  394. 'weight': 4,
  395. }),
  396. content_type="application/json",
  397. )
  398. self.assertEqual(response.status_code, 400)
  399. response_json = response.json()
  400. self.assertEqual(
  401. response_json, {
  402. 'weight': ["Ensure this value is less than or equal to 2."],
  403. }
  404. )
  405. def test_split_unallowed_global_weight(self):
  406. """api rejects split because global weight was unallowed"""
  407. response = self.client.post(
  408. self.api_link,
  409. json.dumps({
  410. 'posts': self.posts,
  411. 'title': 'Valid thread title',
  412. 'category': self.category.id,
  413. 'weight': 2,
  414. }),
  415. content_type="application/json",
  416. )
  417. self.assertEqual(response.status_code, 400)
  418. response_json = response.json()
  419. self.assertEqual(
  420. response_json, {
  421. 'weight': ["You don't have permission to pin threads globally in this category."],
  422. }
  423. )
  424. def test_split_unallowed_local_weight(self):
  425. """api rejects split because local weight was unallowed"""
  426. response = self.client.post(
  427. self.api_link,
  428. json.dumps({
  429. 'posts': self.posts,
  430. 'title': 'Valid thread title',
  431. 'category': self.category.id,
  432. 'weight': 1,
  433. }),
  434. content_type="application/json"
  435. )
  436. self.assertEqual(response.status_code, 400)
  437. response_json = response.json()
  438. self.assertEqual(
  439. response_json, {
  440. 'weight': ["You don't have permission to pin threads in this category."],
  441. }
  442. )
  443. def test_split_allowed_local_weight(self):
  444. """api allows local weight"""
  445. self.override_acl({'can_pin_threads': 1})
  446. response = self.client.post(
  447. self.api_link,
  448. json.dumps({
  449. 'posts': self.posts,
  450. 'title': '$$$',
  451. 'category': self.category.id,
  452. 'weight': 1,
  453. }),
  454. content_type="application/json",
  455. )
  456. self.assertEqual(response.status_code, 400)
  457. response_json = response.json()
  458. self.assertEqual(
  459. response_json, {
  460. 'title': ["Thread title should be at least 5 characters long (it has 3)."],
  461. }
  462. )
  463. def test_split_allowed_global_weight(self):
  464. """api allows global weight"""
  465. self.override_acl({'can_pin_threads': 2})
  466. response = self.client.post(
  467. self.api_link,
  468. json.dumps({
  469. 'posts': self.posts,
  470. 'title': '$$$',
  471. 'category': self.category.id,
  472. 'weight': 2,
  473. }),
  474. content_type="application/json",
  475. )
  476. self.assertEqual(response.status_code, 400)
  477. response_json = response.json()
  478. self.assertEqual(
  479. response_json, {
  480. 'title': ["Thread title should be at least 5 characters long (it has 3)."],
  481. }
  482. )
  483. def test_split_unallowed_close(self):
  484. """api rejects split because closing thread was unallowed"""
  485. response = self.client.post(
  486. self.api_link,
  487. json.dumps({
  488. 'posts': self.posts,
  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. response_json = response.json()
  497. self.assertEqual(
  498. response_json, {
  499. 'is_closed': ["You don't have permission to close threads in this category."],
  500. }
  501. )
  502. def test_split_with_close(self):
  503. """api allows for closing thread"""
  504. self.override_acl({'can_close_threads': True})
  505. response = self.client.post(
  506. self.api_link,
  507. json.dumps({
  508. 'posts': self.posts,
  509. 'title': '$$$',
  510. 'category': self.category.id,
  511. 'weight': 0,
  512. 'is_closed': True,
  513. }),
  514. content_type="application/json",
  515. )
  516. self.assertEqual(response.status_code, 400)
  517. response_json = response.json()
  518. self.assertEqual(
  519. response_json, {
  520. 'title': ["Thread title should be at least 5 characters long (it has 3)."],
  521. }
  522. )
  523. def test_split_unallowed_hidden(self):
  524. """api rejects split because hidden thread was unallowed"""
  525. response = self.client.post(
  526. self.api_link,
  527. json.dumps({
  528. 'posts': self.posts,
  529. 'title': 'Valid thread title',
  530. 'category': self.category.id,
  531. 'is_hidden': True,
  532. }),
  533. content_type="application/json",
  534. )
  535. self.assertEqual(response.status_code, 400)
  536. response_json = response.json()
  537. self.assertEqual(
  538. response_json, {
  539. 'is_hidden': ["You don't have permission to hide threads in this category."],
  540. }
  541. )
  542. def test_split_with_hide(self):
  543. """api allows for hiding thread"""
  544. self.override_acl({'can_hide_threads': True})
  545. response = self.client.post(
  546. self.api_link,
  547. json.dumps({
  548. 'posts': self.posts,
  549. 'title': '$$$',
  550. 'category': self.category.id,
  551. 'weight': 0,
  552. 'is_hidden': True,
  553. }),
  554. content_type="application/json",
  555. )
  556. self.assertEqual(response.status_code, 400)
  557. response_json = response.json()
  558. self.assertEqual(
  559. response_json, {
  560. 'title': ["Thread title should be at least 5 characters long (it has 3)."],
  561. }
  562. )
  563. def test_split(self):
  564. """api splits posts to new thread"""
  565. self.refresh_thread()
  566. self.assertEqual(self.thread.replies, 2)
  567. response = self.client.post(
  568. self.api_link,
  569. json.dumps({
  570. 'posts': self.posts,
  571. 'title': 'Split thread.',
  572. 'category': self.category.id,
  573. }),
  574. content_type="application/json",
  575. )
  576. self.assertEqual(response.status_code, 200)
  577. # thread was created
  578. split_thread = self.category.thread_set.get(slug='split-thread')
  579. self.assertEqual(split_thread.replies, 1)
  580. # posts were removed from old thread
  581. self.refresh_thread()
  582. self.assertEqual(self.thread.replies, 0)
  583. # posts were moved to new thread
  584. self.assertEqual(split_thread.post_set.filter(pk__in=self.posts).count(), 2)
  585. def test_split_best_answer(self):
  586. """api splits best answer to new thread"""
  587. best_answer = testutils.reply_thread(self.thread)
  588. self.thread.set_best_answer(self.user, best_answer)
  589. self.thread.synchronize()
  590. self.thread.save()
  591. self.refresh_thread()
  592. self.assertEqual(self.thread.best_answer, best_answer)
  593. self.assertEqual(self.thread.replies, 3)
  594. response = self.client.post(
  595. self.api_link,
  596. json.dumps({
  597. 'posts': [best_answer.pk],
  598. 'title': 'Split thread.',
  599. 'category': self.category.id,
  600. }),
  601. content_type="application/json",
  602. )
  603. self.assertEqual(response.status_code, 200)
  604. # best_answer was moved and unmarked
  605. self.refresh_thread()
  606. self.assertEqual(self.thread.replies, 2)
  607. self.assertIsNone(self.thread.best_answer)
  608. split_thread = self.category.thread_set.get(slug='split-thread')
  609. self.assertEqual(split_thread.replies, 0)
  610. self.assertIsNone(split_thread.best_answer)
  611. def test_split_kitchensink(self):
  612. """api splits posts with kitchensink"""
  613. self.refresh_thread()
  614. self.assertEqual(self.thread.replies, 2)
  615. self.override_other_acl({
  616. 'can_start_threads': 2,
  617. 'can_close_threads': True,
  618. 'can_hide_threads': True,
  619. 'can_pin_threads': 2,
  620. })
  621. poststracker.save_read(self.user, self.thread.first_post)
  622. for post in self.posts:
  623. poststracker.save_read(self.user, Post.objects.select_related().get(pk=post))
  624. response = self.client.post(
  625. self.api_link,
  626. json.dumps({
  627. 'posts': self.posts,
  628. 'title': 'Split thread',
  629. 'category': self.category_b.id,
  630. 'weight': 2,
  631. 'is_closed': 1,
  632. 'is_hidden': 1,
  633. }),
  634. content_type="application/json",
  635. )
  636. self.assertEqual(response.status_code, 200)
  637. # thread was created
  638. split_thread = self.category_b.thread_set.get(slug='split-thread')
  639. self.assertEqual(split_thread.replies, 1)
  640. self.assertEqual(split_thread.weight, 2)
  641. self.assertTrue(split_thread.is_closed)
  642. self.assertTrue(split_thread.is_hidden)
  643. # posts were removed from old thread
  644. self.refresh_thread()
  645. self.assertEqual(self.thread.replies, 0)
  646. # posts were moved to new thread
  647. self.assertEqual(split_thread.post_set.filter(pk__in=self.posts).count(), 2)
  648. # postreads were removed
  649. postreads = self.user.postread_set.filter(post__is_event=False).order_by('id')
  650. postreads_threads = list(postreads.values_list('thread_id', flat=True))
  651. self.assertEqual(postreads_threads, [self.thread.pk])
  652. postreads_categories = list(postreads.values_list('category_id', flat=True))
  653. self.assertEqual(postreads_categories, [self.category.pk])