Browse Source

Merge pull request #742 from rafalp/yapf

Autoformat python codebase, fixes #702
Rafał Pitoń 8 years ago
parent
commit
0498f4e629
454 changed files with 12524 additions and 10046 deletions
  1. 2 2
      .gitignore
  2. 3 3
      .pylintrc
  3. 11 0
      .style.yapf
  4. 6 0
      cleansource
  5. 0 0
      extras/__init__.py
  6. 5 0
      extras/config.py
  7. 1 1
      extras/fixabsoluteimports.py
  8. 160 0
      extras/fixdictsformatting.py
  9. 2 1
      extras/fixrelativeimports.py
  10. 5 6
      misago/acl/algebra.py
  11. 4 14
      misago/acl/api.py
  12. 1 3
      misago/acl/builder.py
  13. 1 0
      misago/acl/decorators.py
  14. 11 9
      misago/acl/forms.py
  15. 7 4
      misago/acl/migrations/0001_initial.py
  16. 1 1
      misago/acl/migrations/0002_acl_version_tracker.py
  17. 0 1
      misago/acl/migrations/0003_default_roles.py
  18. 2 4
      misago/acl/panels.py
  19. 4 7
      misago/acl/providers.py
  20. 4 4
      misago/acl/tests/test_acl_algebra.py
  21. 1 2
      misago/acl/tests/test_api.py
  22. 33 13
      misago/acl/tests/test_roleadmin_views.py
  23. 6 8
      misago/acl/views.py
  24. 0 1
      misago/admin/__init__.py
  25. 0 1
      misago/admin/admin.py
  26. 4 0
      misago/admin/auth.py
  27. 8 6
      misago/admin/discoverer.py
  28. 20 17
      misago/admin/hierarchy.py
  29. 30 21
      misago/admin/tests/test_admin_views.py
  30. 7 4
      misago/admin/testutils.py
  31. 10 10
      misago/admin/urlpatterns.py
  32. 1 2
      misago/admin/urls.py
  33. 0 1
      misago/admin/views/__init__.py
  34. 2 4
      misago/admin/views/auth.py
  35. 7 5
      misago/admin/views/errorpages.py
  36. 1 2
      misago/admin/views/generic/__init__.py
  37. 1 3
      misago/admin/views/generic/base.py
  38. 13 14
      misago/admin/views/generic/formsbuttons.py
  39. 14 34
      misago/admin/views/generic/list.py
  40. 1 3
      misago/admin/views/generic/mixin.py
  41. 20 12
      misago/admin/views/index.py
  42. 0 1
      misago/categories/__init__.py
  43. 9 6
      misago/categories/admin.py
  44. 1 1
      misago/categories/apps.py
  45. 61 60
      misago/categories/forms.py
  46. 54 13
      misago/categories/migrations/0001_initial.py
  47. 2 2
      misago/categories/migrations/0002_default_categories.py
  48. 5 10
      misago/categories/migrations/0003_categories_roles.py
  49. 7 1
      misago/categories/migrations/0004_category_last_thread.py
  50. 4 4
      misago/categories/models.py
  51. 10 15
      misago/categories/permissions.py
  52. 13 11
      misago/categories/serializers.py
  53. 1 4
      misago/categories/signals.py
  54. 160 130
      misago/categories/tests/test_categories_admin_views.py
  55. 3 9
      misago/categories/tests/test_category_model.py
  56. 130 90
      misago/categories/tests/test_permissions_admin_views.py
  57. 8 9
      misago/categories/tests/test_prunecategories.py
  58. 2 3
      misago/categories/tests/test_synchronizecategories.py
  59. 44 25
      misago/categories/tests/test_utils.py
  60. 0 2
      misago/categories/urls/__init__.py
  61. 1 6
      misago/categories/utils.py
  62. 11 9
      misago/categories/views/categoriesadmin.py
  63. 1 1
      misago/categories/views/categorieslist.py
  64. 23 25
      misago/categories/views/permsadmin.py
  65. 0 1
      misago/conf/__init__.py
  66. 2 1
      misago/conf/admin.py
  67. 0 13
      misago/conf/context_processors.py
  68. 0 5
      misago/conf/defaults.py
  69. 13 19
      misago/conf/forms.py
  70. 15 10
      misago/conf/migrations/0001_initial.py
  71. 4 5
      misago/conf/migrationutils.py
  72. 1 1
      misago/conf/models.py
  73. 17 15
      misago/conf/tests/test_admin_views.py
  74. 24 24
      misago/conf/tests/test_migrationutils.py
  75. 28 19
      misago/conf/tests/test_models.py
  76. 28 33
      misago/conf/tests/test_settings.py
  77. 3 6
      misago/conf/utils.py
  78. 10 11
      misago/conf/views.py
  79. 3 4
      misago/core/__init__.py
  80. 1 1
      misago/core/apipatch.py
  81. 2 2
      misago/core/apirouter.py
  82. 1 2
      misago/core/cachebuster.py
  83. 5 5
      misago/core/context_processors.py
  84. 2 0
      misago/core/decorators.py
  85. 14 8
      misago/core/errorpages.py
  86. 15 8
      misago/core/exceptionhandler.py
  87. 4 3
      misago/core/exceptions.py
  88. 6 2
      misago/core/forms.py
  89. 1 2
      misago/core/mail.py
  90. 6 6
      misago/core/management/commands/misagodbrelations.py
  91. 0 30
      misago/core/management/commands/testemailsetup.py
  92. 1 2
      misago/core/middleware/exceptionhandler.py
  93. 8 6
      misago/core/migrations/0001_initial.py
  94. 66 67
      misago/core/migrations/0002_basic_settings.py
  95. 14 15
      misago/core/page.py
  96. 7 16
      misago/core/pgutils.py
  97. 6 12
      misago/core/serializers.py
  98. 10 6
      misago/core/setup.py
  99. 13 10
      misago/core/shortcuts.py
  100. 1 1
      misago/core/templatetags/misago_capture.py
  101. 22 22
      misago/core/templatetags/misago_forms.py
  102. 0 2
      misago/core/templatetags/misago_pagetitle.py
  103. 2 2
      misago/core/testproject/serializers.py
  104. 35 11
      misago/core/testproject/urls.py
  105. 7 11
      misago/core/testproject/views.py
  106. 135 52
      misago/core/tests/test_apipatch.py
  107. 1 2
      misago/core/tests/test_cachebuster.py
  108. 2 2
      misago/core/tests/test_checks.py
  109. 1 2
      misago/core/tests/test_common_middleware_redirect.py
  110. 42 28
      misago/core/tests/test_context_processors.py
  111. 0 1
      misago/core/tests/test_decorators.py
  112. 1 1
      misago/core/tests/test_deprecations.py
  113. 2 6
      misago/core/tests/test_errorpages.py
  114. 10 18
      misago/core/tests/test_exceptionhandlers.py
  115. 1 4
      misago/core/tests/test_mailer.py
  116. 2 7
      misago/core/tests/test_migrationutils.py
  117. 4 19
      misago/core/tests/test_momentjs.py
  118. 6 3
      misago/core/tests/test_page.py
  119. 22 23
      misago/core/tests/test_serializers.py
  120. 4 4
      misago/core/tests/test_setup.py
  121. 117 91
      misago/core/tests/test_shortcuts.py
  122. 33 39
      misago/core/tests/test_templatetags.py
  123. 0 32
      misago/core/tests/test_testmailsetup.py
  124. 103 93
      misago/core/tests/test_utils.py
  125. 7 5
      misago/core/tests/test_validators.py
  126. 2 3
      misago/core/testutils.py
  127. 17 34
      misago/core/utils.py
  128. 1 2
      misago/core/validators.py
  129. 1 3
      misago/core/views.py
  130. 7 17
      misago/datamover/attachments.py
  131. 5 5
      misago/datamover/avatars.py
  132. 4 8
      misago/datamover/bans.py
  133. 21 13
      misago/datamover/categories.py
  134. 1 3
      misago/datamover/db.py
  135. 1 3
      misago/datamover/management/commands/buildmovesindex.py
  136. 2 4
      misago/datamover/management/commands/movecategories.py
  137. 1 2
      misago/datamover/management/commands/movesettings.py
  138. 11 19
      misago/datamover/management/commands/movethreads.py
  139. 7 16
      misago/datamover/management/commands/moveusers.py
  140. 3 5
      misago/datamover/management/commands/runmigration.py
  141. 0 3
      misago/datamover/markup/__init__.py
  142. 1 1
      misago/datamover/markup/quotes.py
  143. 11 4
      misago/datamover/migrations/0001_initial.py
  144. 1 0
      misago/datamover/movedids.py
  145. 1 1
      misago/datamover/polls.py
  146. 10 6
      misago/datamover/settings.py
  147. 21 33
      misago/datamover/threads.py
  148. 194 51
      misago/datamover/urls.py
  149. 17 16
      misago/datamover/users.py
  150. 1 2
      misago/faker/management/commands/createfakebans.py
  151. 12 12
      misago/faker/management/commands/createfakecategories.py
  152. 1 1
      misago/faker/management/commands/createfakefollowers.py
  153. 8 14
      misago/faker/management/commands/createfakethreads.py
  154. 8 11
      misago/faker/management/commands/createfakeusers.py
  155. 1 2
      misago/legal/context_processors.py
  156. 97 97
      misago/legal/migrations/0001_initial.py
  157. 10 8
      misago/legal/tests.py
  158. 8 4
      misago/legal/views.py
  159. 0 1
      misago/markup/__init__.py
  160. 2 7
      misago/markup/api.py
  161. 18 11
      misago/markup/bbcode/blocks.py
  162. 4 5
      misago/markup/bbcode/inline.py
  163. 1 1
      misago/markup/context_processors.py
  164. 4 2
      misago/markup/finalise.py
  165. 25 7
      misago/markup/flavours.py
  166. 2 3
      misago/markup/md/shortimgs.py
  167. 2 1
      misago/markup/md/striketrough.py
  168. 1 1
      misago/markup/mentions.py
  169. 14 9
      misago/markup/parser.py
  170. 3 3
      misago/markup/pipeline.py
  171. 4 0
      misago/markup/templatetags/misago_editor.py
  172. 8 12
      misago/markup/tests/test_api.py
  173. 2 4
      misago/markup/tests/test_checksums.py
  174. 17 36
      misago/markup/tests/test_mentions.py
  175. 1 3
      misago/markup/urls.py
  176. 1 1
      misago/readtracker/apps.py
  177. 9 10
      misago/readtracker/categoriestracker.py
  178. 14 8
      misago/readtracker/migrations/0001_initial.py
  179. 0 3
      misago/readtracker/models.py
  180. 0 3
      misago/readtracker/signals.py
  181. 2 4
      misago/readtracker/tests/test_dates.py
  182. 5 5
      misago/readtracker/tests/test_readtracker.py
  183. 6 5
      misago/readtracker/threadstracker.py
  184. 3 16
      misago/search/permissions.py
  185. 2 1
      misago/search/searchprovider.py
  186. 2 4
      misago/search/searchproviders.py
  187. 11 11
      misago/search/tests/test_api.py
  188. 7 13
      misago/search/tests/test_searchproviders.py
  189. 21 20
      misago/search/tests/test_views.py
  190. 0 2
      misago/search/urls/__init__.py
  191. 1 1
      misago/search/views.py
  192. 6 4
      misago/threads/admin.py
  193. 16 12
      misago/threads/api/attachments.py
  194. 3 2
      misago/threads/api/pollvotecreateendpoint.py
  195. 1 2
      misago/threads/api/postendpoints/edits.py
  196. 1 3
      misago/threads/api/postendpoints/likes.py
  197. 7 3
      misago/threads/api/postendpoints/merge.py
  198. 8 2
      misago/threads/api/postendpoints/move.py
  199. 4 0
      misago/threads/api/postendpoints/patch_event.py
  200. 12 2
      misago/threads/api/postendpoints/patch_post.py
  201. 1 1
      misago/threads/api/postendpoints/read.py
  202. 3 4
      misago/threads/api/postendpoints/split.py
  203. 12 11
      misago/threads/api/postingendpoint/__init__.py
  204. 28 20
      misago/threads/api/postingendpoint/attachments.py
  205. 11 10
      misago/threads/api/postingendpoint/category.py
  206. 4 7
      misago/threads/api/postingendpoint/emailnotification.py
  207. 2 1
      misago/threads/api/postingendpoint/floodprotection.py
  208. 13 15
      misago/threads/api/postingendpoint/participants.py
  209. 2 3
      misago/threads/api/postingendpoint/privatethread.py
  210. 4 8
      misago/threads/api/postingendpoint/recordedit.py
  211. 2 4
      misago/threads/api/postingendpoint/reply.py
  212. 3 4
      misago/threads/api/postingendpoint/subscribe.py
  213. 3 4
      misago/threads/api/postingendpoint/syncprivatethreads.py
  214. 5 3
      misago/threads/api/threadendpoints/editor.py
  215. 1 1
      misago/threads/api/threadendpoints/list.py
  216. 38 33
      misago/threads/api/threadendpoints/merge.py
  217. 42 31
      misago/threads/api/threadendpoints/patch.py
  218. 3 4
      misago/threads/api/threadendpoints/read.py
  219. 3 2
      misago/threads/api/threadpoll.py
  220. 25 16
      misago/threads/api/threadposts.py
  221. 11 8
      misago/threads/api/threads.py
  222. 1 1
      misago/threads/apps.py
  223. 0 4
      misago/threads/context_processors.py
  224. 24 16
      misago/threads/forms.py
  225. 1 1
      misago/threads/management/commands/clearattachments.py
  226. 7 9
      misago/threads/middleware.py
  227. 280 58
      misago/threads/migrations/0001_initial.py
  228. 57 56
      misago/threads/migrations/0002_threads_settings.py
  229. 27 31
      misago/threads/migrations/0003_attachment_types.py
  230. 56 54
      misago/threads/migrations/0004_update_settings.py
  231. 21 41
      misago/threads/models/attachment.py
  232. 2 2
      misago/threads/models/attachmenttype.py
  233. 2 5
      misago/threads/models/poll.py
  234. 5 5
      misago/threads/models/post.py
  235. 1 3
      misago/threads/models/subscription.py
  236. 10 13
      misago/threads/models/thread.py
  237. 4 18
      misago/threads/models/threadparticipant.py
  238. 9 7
      misago/threads/moderation/posts.py
  239. 17 14
      misago/threads/moderation/threads.py
  240. 6 14
      misago/threads/paginator.py
  241. 45 48
      misago/threads/participants.py
  242. 9 12
      misago/threads/permissions/attachments.py
  243. 51 40
      misago/threads/permissions/polls.py
  244. 41 36
      misago/threads/permissions/privatethreads.py
  245. 171 119
      misago/threads/permissions/threads.py
  246. 13 8
      misago/threads/search.py
  247. 8 9
      misago/threads/serializers/attachment.py
  248. 4 16
      misago/threads/serializers/feed.py
  249. 12 6
      misago/threads/serializers/moderation.py
  250. 27 25
      misago/threads/serializers/poll.py
  251. 8 8
      misago/threads/serializers/pollvote.py
  252. 14 13
      misago/threads/serializers/post.py
  253. 8 8
      misago/threads/serializers/postedit.py
  254. 8 8
      misago/threads/serializers/postlike.py
  255. 18 18
      misago/threads/serializers/thread.py
  256. 1 7
      misago/threads/serializers/threadparticipant.py
  257. 15 13
      misago/threads/signals.py
  258. 1 1
      misago/threads/subscriptions.py
  259. 3 11
      misago/threads/templatetags/misago_poststags.py
  260. 48 19
      misago/threads/tests/test_attachmentadmin_views.py
  261. 90 72
      misago/threads/tests/test_attachments_api.py
  262. 28 34
      misago/threads/tests/test_attachments_middleware.py
  263. 86 53
      misago/threads/tests/test_attachmenttypeadmin_views.py
  264. 34 21
      misago/threads/tests/test_attachmentview.py
  265. 3 3
      misago/threads/tests/test_clearattachments.py
  266. 51 36
      misago/threads/tests/test_emailnotification_middleware.py
  267. 3 7
      misago/threads/tests/test_events.py
  268. 19 11
      misago/threads/tests/test_floodprotection.py
  269. 2 5
      misago/threads/tests/test_floodprotection_middleware.py
  270. 58 26
      misago/threads/tests/test_gotoviews.py
  271. 57 42
      misago/threads/tests/test_paginator.py
  272. 26 8
      misago/threads/tests/test_participants.py
  273. 74 44
      misago/threads/tests/test_post_mentions.py
  274. 51 30
      misago/threads/tests/test_post_model.py
  275. 319 135
      misago/threads/tests/test_privatethread_patch_api.py
  276. 7 5
      misago/threads/tests/test_privatethread_reply_api.py
  277. 195 167
      misago/threads/tests/test_privatethread_start_api.py
  278. 3 9
      misago/threads/tests/test_privatethread_view.py
  279. 5 4
      misago/threads/tests/test_privatethreads.py
  280. 37 48
      misago/threads/tests/test_privatethreads_api.py
  281. 5 7
      misago/threads/tests/test_privatethreads_lists.py
  282. 13 5
      misago/threads/tests/test_search.py
  283. 55 34
      misago/threads/tests/test_subscription_middleware.py
  284. 5 24
      misago/threads/tests/test_subscriptions.py
  285. 2 1
      misago/threads/tests/test_sync_unread_private_threads.py
  286. 2 3
      misago/threads/tests/test_synchronizethreads.py
  287. 72 89
      misago/threads/tests/test_thread_editreply_api.py
  288. 143 153
      misago/threads/tests/test_thread_merge_api.py
  289. 23 24
      misago/threads/tests/test_thread_model.py
  290. 468 295
      misago/threads/tests/test_thread_patch_api.py
  291. 13 8
      misago/threads/tests/test_thread_poll_api.py
  292. 134 131
      misago/threads/tests/test_thread_pollcreate_api.py
  293. 40 46
      misago/threads/tests/test_thread_polldelete_api.py
  294. 282 264
      misago/threads/tests/test_thread_polledit_api.py
  295. 63 51
      misago/threads/tests/test_thread_pollvotes_api.py
  296. 39 37
      misago/threads/tests/test_thread_postdelete_api.py
  297. 13 20
      misago/threads/tests/test_thread_postedits_api.py
  298. 15 14
      misago/threads/tests/test_thread_postlikes_api.py
  299. 234 126
      misago/threads/tests/test_thread_postmerge_api.py
  300. 162 103
      misago/threads/tests/test_thread_postmove_api.py
  301. 538 315
      misago/threads/tests/test_thread_postpatch_api.py
  302. 9 7
      misago/threads/tests/test_thread_postread_api.py
  303. 346 237
      misago/threads/tests/test_thread_postsplit_api.py
  304. 44 43
      misago/threads/tests/test_thread_reply_api.py
  305. 139 109
      misago/threads/tests/test_thread_start_api.py
  306. 3 4
      misago/threads/tests/test_threadparticipant_model.py
  307. 38 52
      misago/threads/tests/test_threads_api.py
  308. 204 248
      misago/threads/tests/test_threads_editor_api.py
  309. 434 288
      misago/threads/tests/test_threads_merge_api.py
  310. 12 10
      misago/threads/tests/test_threads_moderation.py
  311. 257 274
      misago/threads/tests/test_threadslists.py
  312. 39 70
      misago/threads/tests/test_threadview.py
  313. 15 4
      misago/threads/tests/test_treesmap.py
  314. 58 32
      misago/threads/tests/test_utils.py
  315. 3 3
      misago/threads/tests/test_validators.py
  316. 45 30
      misago/threads/testutils.py
  317. 103 62
      misago/threads/threadtypes/privatethread.py
  318. 142 92
      misago/threads/threadtypes/thread.py
  319. 1 0
      misago/threads/threadtypes/treesmap.py
  320. 54 58
      misago/threads/urls/__init__.py
  321. 8 2
      misago/threads/urls/api.py
  322. 4 7
      misago/threads/utils.py
  323. 32 20
      misago/threads/validators.py
  324. 18 11
      misago/threads/viewmodels/category.py
  325. 2 7
      misago/threads/viewmodels/post.py
  326. 7 5
      misago/threads/viewmodels/posts.py
  327. 20 13
      misago/threads/viewmodels/thread.py
  328. 23 24
      misago/threads/viewmodels/threads.py
  329. 6 9
      misago/threads/views/admin/attachments.py
  330. 4 4
      misago/threads/views/admin/attachmenttypes.py
  331. 0 3
      misago/threads/views/attachment.py
  332. 16 6
      misago/threads/views/goto.py
  333. 6 7
      misago/threads/views/list.py
  334. 7 8
      misago/threads/views/thread.py
  335. 4 7
      misago/urls.py
  336. 3 8
      misago/users/activepostersranking.py
  337. 16 5
      misago/users/admin.py
  338. 78 64
      misago/users/api/auth.py
  339. 2 1
      misago/users/api/rest_permissions.py
  340. 38 30
      misago/users/api/userendpoints/avatar.py
  341. 5 11
      misago/users/api/userendpoints/changeemail.py
  342. 9 12
      misago/users/api/userendpoints/changepassword.py
  343. 8 15
      misago/users/api/userendpoints/create.py
  344. 2 10
      misago/users/api/userendpoints/list.py
  345. 11 15
      misago/users/api/userendpoints/signature.py
  346. 11 20
      misago/users/api/userendpoints/username.py
  347. 9 11
      misago/users/api/usernamechanges.py
  348. 35 22
      misago/users/api/users.py
  349. 3 5
      misago/users/apps.py
  350. 1 3
      misago/users/avatars/__init__.py
  351. 12 31
      misago/users/avatars/dynamic.py
  352. 5 8
      misago/users/avatars/gallery.py
  353. 8 7
      misago/users/avatars/store.py
  354. 2 8
      misago/users/avatars/uploaded.py
  355. 11 25
      misago/users/bans.py
  356. 10 6
      misago/users/captcha.py
  357. 2 10
      misago/users/context_processors.py
  358. 3 8
      misago/users/credentialchange.py
  359. 6 8
      misago/users/decorators.py
  360. 23 51
      misago/users/djangoadmin.py
  361. 46 87
      misago/users/forms/admin.py
  362. 27 47
      misago/users/forms/auth.py
  363. 1 1
      misago/users/forms/register.py
  364. 0 1
      misago/users/management/commands/buildactivepostersranking.py
  365. 45 25
      misago/users/management/commands/createsuperuser.py
  366. 5 2
      misago/users/management/commands/synchronizeusers.py
  367. 0 3
      misago/users/middleware.py
  368. 173 42
      misago/users/migrations/0001_initial.py
  369. 228 214
      misago/users/migrations/0002_users_settings.py
  370. 1 1
      misago/users/migrations/0003_bans_version_tracker.py
  371. 2 5
      misago/users/migrations/0004_default_ranks.py
  372. 8 1
      misago/users/migrations/0005_dj_19_update.py
  373. 145 131
      misago/users/migrations/0006_update_settings.py
  374. 9 3
      misago/users/migrations/0007_auto_20170219_1639.py
  375. 1 4
      misago/users/models/avatar.py
  376. 11 9
      misago/users/models/ban.py
  377. 58 64
      misago/users/models/user.py
  378. 0 1
      misago/users/online/utils.py
  379. 14 15
      misago/users/permissions/account.py
  380. 2 0
      misago/users/permissions/decorators.py
  381. 18 24
      misago/users/permissions/delete.py
  382. 21 22
      misago/users/permissions/moderation.py
  383. 22 48
      misago/users/permissions/profiles.py
  384. 9 14
      misago/users/search.py
  385. 15 15
      misago/users/serializers/auth.py
  386. 10 3
      misago/users/serializers/ban.py
  387. 9 5
      misago/users/serializers/moderation.py
  388. 4 8
      misago/users/serializers/options.py
  389. 2 2
      misago/users/serializers/rank.py
  390. 17 12
      misago/users/serializers/user.py
  391. 4 6
      misago/users/serializers/usernamechange.py
  392. 1 6
      misago/users/signals.py
  393. 1 1
      misago/users/signatures.py
  394. 0 3
      misago/users/templatetags/misago_avatars.py
  395. 55 28
      misago/users/tests/test_activation_views.py
  396. 2 4
      misago/users/tests/test_activepostersranking.py
  397. 154 78
      misago/users/tests/test_auth_api.py
  398. 6 7
      misago/users/tests/test_auth_backend.py
  399. 20 13
      misago/users/tests/test_auth_views.py
  400. 19 22
      misago/users/tests/test_avatars.py
  401. 26 17
      misago/users/tests/test_avatarserver_views.py
  402. 3 12
      misago/users/tests/test_ban_model.py
  403. 67 40
      misago/users/tests/test_banadmin_views.py
  404. 18 20
      misago/users/tests/test_bans.py
  405. 1 4
      misago/users/tests/test_captcha_api.py
  406. 1 2
      misago/users/tests/test_commands.py
  407. 4 2
      misago/users/tests/test_createsuperuser.py
  408. 4 8
      misago/users/tests/test_credentialchange.py
  409. 3 4
      misago/users/tests/test_decorators.py
  410. 7 4
      misago/users/tests/test_djangoadmin_auth.py
  411. 33 20
      misago/users/tests/test_forgottenpassword_views.py
  412. 7 5
      misago/users/tests/test_invalidatebans.py
  413. 35 11
      misago/users/tests/test_lists_views.py
  414. 33 17
      misago/users/tests/test_options_views.py
  415. 53 30
      misago/users/tests/test_profile_views.py
  416. 86 33
      misago/users/tests/test_rankadmin_views.py
  417. 1 2
      misago/users/tests/test_realip_middleware.py
  418. 32 20
      misago/users/tests/test_rest_permissions.py
  419. 11 7
      misago/users/tests/test_search.py
  420. 1 2
      misago/users/tests/test_signatures.py
  421. 5 8
      misago/users/tests/test_testutils.py
  422. 144 131
      misago/users/tests/test_user_avatar_api.py
  423. 53 40
      misago/users/tests/test_user_changeemail_api.py
  424. 49 42
      misago/users/tests/test_user_changepassword_api.py
  425. 68 49
      misago/users/tests/test_user_create_api.py
  426. 66 14
      misago/users/tests/test_user_feeds_api.py
  427. 6 2
      misago/users/tests/test_user_model.py
  428. 27 27
      misago/users/tests/test_user_signature_api.py
  429. 66 53
      misago/users/tests/test_user_username_api.py
  430. 265 239
      misago/users/tests/test_useradmin_views.py
  431. 3 8
      misago/users/tests/test_usernamechanges_api.py
  432. 179 156
      misago/users/tests/test_users_api.py
  433. 2 4
      misago/users/tests/test_utils.py
  434. 5 8
      misago/users/tests/test_validators.py
  435. 3 3
      misago/users/testutils.py
  436. 13 21
      misago/users/tokens.py
  437. 53 29
      misago/users/urls/__init__.py
  438. 5 3
      misago/users/urls/api.py
  439. 15 27
      misago/users/validators.py
  440. 3 2
      misago/users/viewmodels/activeposters.py
  441. 3 4
      misago/users/viewmodels/followers.py
  442. 2 3
      misago/users/viewmodels/posts.py
  443. 5 5
      misago/users/viewmodels/rankusers.py
  444. 10 15
      misago/users/viewmodels/threads.py
  445. 24 16
      misago/users/views/activation.py
  446. 8 10
      misago/users/views/admin/bans.py
  447. 1 1
      misago/users/views/admin/ranks.py
  448. 41 48
      misago/users/views/admin/users.py
  449. 1 2
      misago/users/views/auth.py
  450. 18 18
      misago/users/views/forgottenpassword.py
  451. 2 5
      misago/users/views/lists.py
  452. 18 13
      misago/users/views/options.py
  453. 11 19
      misago/users/views/profile.py
  454. 38 0
      pycodestyle.py

+ 2 - 2
.gitignore

@@ -39,8 +39,8 @@ coverage.xml
 report.txt
 report.txt
 flake8.txt
 flake8.txt
 
 
-# Translations
-*.mo
+# Pylint report
+pylint.txt
 
 
 # Mr Developer
 # Mr Developer
 .mr.developer.cfg
 .mr.developer.cfg

+ 3 - 3
.pylintrc

@@ -1,4 +1,4 @@
 [Basic]
 [Basic]
-disable=abstract-method,cyclic-import,duplicate-code,file-ignored,invalid-name,locally-disabled,missing-docstring,no-init,no-member,no-self-use,old-style-class,super-on-old-class,too-few-public-methods,too-many-ancestors,unused-argument
-max-line-length=120
-max-locals=20
+disable=all
+enable=function-redefined,import-self,redefined-outer-name,reimported,return-in-init,undefined-all-variable,undefined-variable,unreachable,unused-import,unused-variable
+reports=no

+ 11 - 0
.style.yapf

@@ -0,0 +1,11 @@
+[style]
+coalesce_brackets = true
+column_limit=99
+dedent_closing_brackets = true
+each_dict_entry_on_separate_line = true
+indent_dictionary_value = true
+join_multiple_lines = false
+split_arguments_when_comma_terminated = true
+split_before_first_argument = true
+split_before_logical_operator = true
+split_before_named_assigns = true

+ 6 - 0
cleansource

@@ -0,0 +1,6 @@
+#!/bin/bash
+
+yapf -ir ${1:-misago} -e '*/project_template/**/*.py' -e '*/conf/defaults.py'
+isort -rc ${1:-misago}
+pylint ${1:-misago}
+python pycodestyle.py ${1:-misago}

+ 0 - 0
extras/__init__.py


+ 5 - 0
extras/config.py

@@ -0,0 +1,5 @@
+from django.utils.six.moves import configparser
+
+
+yapf = configparser.ConfigParser()
+yapf.read('.style.yapf')

+ 1 - 1
fixabsoluteimports.py → extras/fixabsoluteimports.py

@@ -87,7 +87,7 @@ def clean_import(package, import_path):
 
 
 
 
 if __name__ == '__main__':
 if __name__ == '__main__':
-    for args in os.walk('misago'):
+    for args in os.walk('../misago'):
         walk_directory(*args)
         walk_directory(*args)
 
 
     print "\nDone! Don't forget to run isort to fix imports ordering!"
     print "\nDone! Don't forget to run isort to fix imports ordering!"

+ 160 - 0
extras/fixdictsformatting.py

@@ -0,0 +1,160 @@
+from __future__ import unicode_literals
+
+import sys
+
+from lib2to3.pytree import Node, Leaf
+from lib2to3.fixer_util import token, syms
+
+from yapf.yapflib import pytree_utils
+
+from django.utils import six
+
+from .config import yapf as yapf_config
+
+
+MAX_LINE_LENGTH = yapf_config.getint('style', 'column_limit') + 1
+
+
+def fix_formatting(filesource):
+    if not ('{'  in filesource and ('[' in filesource or '(' in filesource)):
+        return filesource
+
+    tree = pytree_utils.ParseCodeToTree(filesource)
+    for node in tree.children:
+        walk_tree(node, node.children)
+    return six.text_type(tree)
+
+
+def walk_tree(node, children):
+    for item in children:
+        if item.type == syms.dictsetmaker:
+            indent = item.parent.children[-1].column
+            walk_dict_tree(item, item.children, indent)
+        else:
+            walk_tree(item, item.children)
+
+
+def walk_dict_tree(node, children, indent):
+    for item in children:
+        prev = item.prev_sibling
+        if isinstance(prev, Leaf) and prev.value == ':':
+            if isinstance(item, Leaf):
+                if six.text_type(item).startswith("\n"):
+                    item.replace(Leaf(
+                        item.type,
+                        item.value,
+                        prefix=' ',
+                    ))
+            elif six.text_type(item).strip()[0] in ('[', '{'):
+                walk_tree(item, item.children)
+            else:
+                walk_dedent_tree(item, item.children, indent)
+
+
+def walk_dedent_tree(node, children, indent):
+    force_split_next = False
+    for item in children:
+        prev = item.prev_sibling
+        if not prev:
+            if isinstance(item, Leaf) and six.text_type(item).startswith("\n"):
+                prev = node.prev_sibling
+                next = node.next_sibling
+                final_length = 0
+
+                if prev and "\n" not in six.text_type(node).strip():
+                    final_length = prev.column + len(six.text_type(node).strip()) + 3
+
+                item.replace(Leaf(
+                    item.type,
+                    item.value,
+                    prefix=' ',
+                ))
+
+                if final_length and final_length > MAX_LINE_LENGTH:
+                    # tell next call to walk_dedent_tree_node that we need
+                    # different stringformat tactic
+                    force_split_next = True
+        elif isinstance(item, Node):
+            if node.type == syms.power:
+                for subitem in item.children[1:]:
+                    walk_dedent_power_node(subitem, subitem.children, indent)
+            else:
+                for subitem in item.children[1:]:
+                    walk_dedent_tree_node(subitem, subitem.children, indent, force_split_next)
+                    force_split_next = False
+
+
+def walk_dedent_tree_node(node, children, indent, force_split_next=False):
+    if six.text_type(node).startswith("\n"):
+        if isinstance(node, Leaf):
+            prev = node.prev_sibling
+            next = node.next_sibling
+
+            is_followup = prev and prev.type == token.STRING and node.type == token.STRING
+            if is_followup:
+                new_value = node.value
+
+                # insert linebreak after last string in braces, so its closing brace moves to new line
+                if not node.next_sibling:
+                    closing_bracket = node.parent.parent.children[-1]
+                    if not six.text_type(closing_bracket).startswith("\n"):
+                        new_value = "%s\n%s" % (node.value, (' ' * (indent + 4)))
+
+                node.replace(Leaf(
+                    node.type,
+                    new_value,
+                    prefix="\n%s" % (' ' * (indent + 8)),
+                ))
+            else:
+                if six.text_type(node).strip() in (')', '}'):
+                    new_prefix = "\n%s" % (' ' * (indent + 4))
+                else:
+                    new_prefix = "\n%s" % (' ' * (indent + 8))
+
+                node.replace(Leaf(
+                    node.type,
+                    node.value,
+                    prefix=new_prefix
+                ))
+        else:
+            for item in children:
+                walk_dedent_tree_node(item, item.children, indent)
+    elif isinstance(node, Leaf):
+        if node.type == token.STRING:
+            strings_tuple = node.parent.parent
+
+            prev = node.prev_sibling
+            next = node.next_sibling
+
+            is_opening = prev is None and six.text_type(strings_tuple).strip()[0] == '('
+            has_followup = next and next.type == token.STRING
+
+            if is_opening and has_followup:
+                node.replace(Leaf(
+                    node.type,
+                    node.value,
+                    prefix="\n%s" % (' ' * (indent + 8)),
+                ))
+            elif force_split_next:
+                node.replace(Leaf(
+                    node.type,
+                    "%s\n%s" % (node.value, (' ' * (indent + 4))),
+                    prefix="\n%s" % (' ' * (indent + 8)),
+                ))
+    else:
+        for item in children:
+            walk_dedent_tree_node(item, item.children, indent)
+
+
+def walk_dedent_power_node(node, children, indent):
+    if isinstance(node, Leaf):
+        if six.text_type(node).startswith("\n"):
+            node.replace(Leaf(
+                node.type,
+                node.value,
+                prefix=node.prefix[:-4],
+            ))
+    else:
+        for item in children:
+            walk_dedent_power_node(item, item.children, indent)
+

+ 2 - 1
fixrelativeimports.py → extras/fixrelativeimports.py

@@ -45,8 +45,9 @@ def clean_import(package, match):
 
 
     return '.'.join(import_path)
     return '.'.join(import_path)
 
 
+
 if __name__ == '__main__':
 if __name__ == '__main__':
-    for args in os.walk('misago'):
+    for args in os.walk('../misago'):
         walk_directory(*args)
         walk_directory(*args)
 
 
     print "\nDone! Don't forget to run isort to fix imports ordering!"
     print "\nDone! Don't forget to run isort to fix imports ordering!"

+ 5 - 6
misago/acl/algebra.py

@@ -9,17 +9,16 @@ def _roles_acls(key_name, roles):
 
 
 def sum_acls(result_acl, acls=None, roles=None, key=None, **permissions):
 def sum_acls(result_acl, acls=None, roles=None, key=None, **permissions):
     if acls and roles:
     if acls and roles:
-        raise ValueError(
-            'You can not provide both "acls" and "roles" arguments')
+        raise ValueError('You can not provide both "acls" and "roles" arguments')
 
 
     if (acls is None) and (roles is None):
     if (acls is None) and (roles is None):
-        raise ValueError(
-            'You have to provide either "acls" and "roles" argument')
+        raise ValueError('You have to provide either "acls" and "roles" argument')
 
 
     if roles is not None:
     if roles is not None:
         if not key:
         if not key:
-            raise ValueError('You have to provide "key" argument if '
-                             'you are passing roles instead of acls')
+            raise ValueError(
+                'You have to provide "key" argument if you are passing roles instead of acls'
+            )
         acls = _roles_acls(key, roles)
         acls = _roles_acls(key, roles)
 
 
     for permission, compare in permissions.items():
     for permission, compare in permissions.items():

+ 4 - 14
misago/acl/api.py

@@ -10,8 +10,6 @@ properties defined by ACL providers within their "add_acl_to_target"
 """
 """
 import copy
 import copy
 
 
-from django.contrib.auth import get_user_model
-
 from misago.core import threadstore
 from misago.core import threadstore
 from misago.core.cache import cache
 from misago.core.cache import cache
 
 
@@ -21,9 +19,7 @@ from .providers import providers
 
 
 
 
 def get_user_acl(user):
 def get_user_acl(user):
-    """
-    Get ACL for User
-    """
+    """get ACL for User"""
     acl_key = 'acl_%s' % user.acl_key
     acl_key = 'acl_%s' % user.acl_key
 
 
     acl_cache = threadstore.get(acl_key)
     acl_cache = threadstore.get(acl_key)
@@ -43,9 +39,7 @@ def get_user_acl(user):
 
 
 
 
 def add_acl(user, target):
 def add_acl(user, target):
-    """
-    Add valid ACL to target (iterable of objects or single object)
-    """
+    """add valid ACL to target (iterable of objects or single object)"""
     if hasattr(target, '__iter__'):
     if hasattr(target, '__iter__'):
         for item in target:
         for item in target:
             _add_acl_to_target(user, item)
             _add_acl_to_target(user, item)
@@ -54,9 +48,7 @@ def add_acl(user, target):
 
 
 
 
 def _add_acl_to_target(user, target):
 def _add_acl_to_target(user, target):
-    """
-    Add valid ACL to single target, helper for add_acl function
-    """
+    """add valid ACL to single target, helper for add_acl function"""
     target.acl = {}
     target.acl = {}
 
 
     for annotator in providers.get_type_annotators(target):
     for annotator in providers.get_type_annotators(target):
@@ -64,9 +56,7 @@ def _add_acl_to_target(user, target):
 
 
 
 
 def serialize_acl(target):
 def serialize_acl(target):
-    """
-    Serialize authenticated user's ACL
-    """
+    """serialize authenticated user's ACL"""
     serialized_acl = copy.deepcopy(target.acl_cache)
     serialized_acl = copy.deepcopy(target.acl_cache)
 
 
     for serializer in providers.get_type_serializers(target):
     for serializer in providers.get_type_serializers(target):

+ 1 - 3
misago/acl/builder.py

@@ -2,9 +2,7 @@ from .providers import providers
 
 
 
 
 def build_acl(roles):
 def build_acl(roles):
-    """
-    Build ACL for given roles
-    """
+    """build ACL for given roles"""
     acl = {}
     acl = {}
 
 
     for extension, module in providers.list():
     for extension, module in providers.list():

+ 1 - 0
misago/acl/decorators.py

@@ -10,4 +10,5 @@ def return_boolean(f):
             return False
             return False
         else:
         else:
             return True
             return True
+
     return decorator
     return decorator

+ 11 - 9
misago/acl/forms.py

@@ -14,9 +14,7 @@ class RoleForm(forms.ModelForm):
 
 
 
 
 def get_permissions_forms(role, data=None):
 def get_permissions_forms(role, data=None):
-    """
-    Utility function for building forms in admin
-    """
+    """utility function for building forms in admin"""
     role_permissions = role.permissions
     role_permissions = role.permissions
 
 
     perms_forms = []
     perms_forms = []
@@ -25,18 +23,22 @@ def get_permissions_forms(role, data=None):
             module.change_permissions_form
             module.change_permissions_form
         except AttributeError:
         except AttributeError:
             message = "'%s' object has no attribute '%s'"
             message = "'%s' object has no attribute '%s'"
-            raise AttributeError(
-                message % (extension, 'change_permissions_form'))
+            raise AttributeError(message % (extension, 'change_permissions_form'))
 
 
         FormType = module.change_permissions_form(role)
         FormType = module.change_permissions_form(role)
 
 
         if FormType:
         if FormType:
             if data:
             if data:
-                perms_forms.append(FormType(data, prefix=extension))
-            else:
                 perms_forms.append(FormType(
                 perms_forms.append(FormType(
-                    initial=role_permissions.get(extension),
-                    prefix=extension
+                    data,
+                    prefix=extension,
                 ))
                 ))
+            else:
+                perms_forms.append(
+                    FormType(
+                        initial=role_permissions.get(extension),
+                        prefix=extension,
+                    )
+                )
 
 
     return perms_forms
     return perms_forms

+ 7 - 4
misago/acl/migrations/0001_initial.py

@@ -9,14 +9,17 @@ from misago.acl.models import permissions_default
 
 
 class Migration(migrations.Migration):
 class Migration(migrations.Migration):
 
 
-    dependencies = [
-    ]
+    dependencies = []
 
 
     operations = [
     operations = [
         migrations.CreateModel(
         migrations.CreateModel(
             name='Role',
             name='Role',
             fields=[
             fields=[
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+                (
+                    'id', models.AutoField(
+                        verbose_name='ID', serialize=False, auto_created=True, primary_key=True
+                    )
+                ),
                 ('name', models.CharField(max_length=255)),
                 ('name', models.CharField(max_length=255)),
                 ('special_role', models.CharField(max_length=255, null=True, blank=True)),
                 ('special_role', models.CharField(max_length=255, null=True, blank=True)),
                 ('permissions', JSONField(default=permissions_default)),
                 ('permissions', JSONField(default=permissions_default)),
@@ -24,6 +27,6 @@ class Migration(migrations.Migration):
             options={
             options={
                 'abstract': False,
                 'abstract': False,
             },
             },
-            bases=(models.Model,),
+            bases=(models.Model, ),
         ),
         ),
     ]
     ]

+ 1 - 1
misago/acl/migrations/0002_acl_version_tracker.py

@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 # -*- coding: utf-8 -*-
 from __future__ import unicode_literals
 from __future__ import unicode_literals
 
 
-from django.db import migrations, models
+from django.db import migrations
 
 
 from misago.acl.constants import ACL_CACHEBUSTER
 from misago.acl.constants import ACL_CACHEBUSTER
 from misago.core.migrationutils import cachebuster_register_cache
 from misago.core.migrationutils import cachebuster_register_cache

+ 0 - 1
misago/acl/migrations/0003_default_roles.py

@@ -163,7 +163,6 @@ def create_default_roles(apps, schema_editor):
             'misago.users.permissions.profiles': {
             'misago.users.permissions.profiles': {
                 'can_see_ban_details': 1,
                 'can_see_ban_details': 1,
             },
             },
-
             'misago.users.permissions.moderation': {
             'misago.users.permissions.moderation': {
                 'can_ban_users': 1,
                 'can_ban_users': 1,
                 'max_ban_length': 14,
                 'max_ban_length': 14,

+ 2 - 4
misago/acl/panels.py

@@ -4,10 +4,8 @@ from django.utils.translation import ugettext_lazy as _
 
 
 
 
 class MisagoACLPanel(Panel):
 class MisagoACLPanel(Panel):
-    """
-    Panel that displays current user's ACL
-    """
-    title = _('Misago User ACL')
+    """panel that displays current user's ACL"""
+    title = _("Misago User ACL")
     template = 'misago/acl_debug.html'
     template = 'misago/acl_debug.html'
 
 
     @property
     @property

+ 4 - 7
misago/acl/providers.py

@@ -3,8 +3,9 @@ from importlib import import_module
 from misago.conf import settings
 from misago.conf import settings
 
 
 
 
-# Manager for permission providers
 class PermissionProviders(object):
 class PermissionProviders(object):
+    """manager for permission providers"""
+
     def __init__(self):
     def __init__(self):
         self._initialized = False
         self._initialized = False
         self._providers = []
         self._providers = []
@@ -33,15 +34,11 @@ class PermissionProviders(object):
             types_dict[hashType] = tuple(types_dict[hashType])
             types_dict[hashType] = tuple(types_dict[hashType])
 
 
     def acl_annotator(self, hashable_type, func):
     def acl_annotator(self, hashable_type, func):
-        """
-        registers ACL annotator for specified types
-        """
+        """registers ACL annotator for specified types"""
         self._annotators.setdefault(hashable_type, []).append(func)
         self._annotators.setdefault(hashable_type, []).append(func)
 
 
     def acl_serializer(self, hashable_type, func):
     def acl_serializer(self, hashable_type, func):
-        """
-        registers ACL serializer for specified types
-        """
+        """registers ACL serializer for specified types"""
         self._serializers.setdefault(hashable_type, []).append(func)
         self._serializers.setdefault(hashable_type, []).append(func)
 
 
     def get_type_annotators(self, obj):
     def get_type_annotators(self, obj):

+ 4 - 4
misago/acl/tests/test_acl_algebra.py

@@ -28,7 +28,6 @@ class ComparisionsTests(TestCase):
         self.assertEqual(algebra.lower(2, 2), 2)
         self.assertEqual(algebra.lower(2, 2), 2)
         self.assertEqual(algebra.lower(True, False), False)
         self.assertEqual(algebra.lower(True, False), False)
 
 
-
     def test_lower_non_zero(self):
     def test_lower_non_zero(self):
         """lower non-zero wins test"""
         """lower non-zero wins test"""
         self.assertEqual(algebra.lower_non_zero(1, 3), 1)
         self.assertEqual(algebra.lower_non_zero(1, 3), 1)
@@ -73,13 +72,14 @@ class SumACLTests(TestCase):
         }
         }
 
 
         acl = algebra.sum_acls(
         acl = algebra.sum_acls(
-            defaults, acls=test_acls,
+            defaults,
+            acls=test_acls,
             can_see=algebra.greater,
             can_see=algebra.greater,
             can_hear=algebra.greater,
             can_hear=algebra.greater,
             max_speed=algebra.greater,
             max_speed=algebra.greater,
             min_age=algebra.lower,
             min_age=algebra.lower,
-            speed_limit=algebra.greater_or_zero
-            )
+            speed_limit=algebra.greater_or_zero,
+        )
 
 
         self.assertEqual(acl['can_see'], 1)
         self.assertEqual(acl['can_see'], 1)
         self.assertEqual(acl['can_hear'], 1)
         self.assertEqual(acl['can_hear'], 1)

+ 1 - 2
misago/acl/tests/test_api.py

@@ -11,8 +11,7 @@ UserModel = get_user_model()
 class GetUserACLTests(TestCase):
 class GetUserACLTests(TestCase):
     def test_get_authenticated_acl(self):
     def test_get_authenticated_acl(self):
         """get ACL for authenticated user"""
         """get ACL for authenticated user"""
-        test_user = UserModel.objects.create_user(
-            'Bob', 'bob@bob.com', 'pass123')
+        test_user = UserModel.objects.create_user('Bob', 'bob@bob.com', 'pass123')
 
 
         acl = get_user_acl(test_user)
         acl = get_user_acl(test_user)
 
 

+ 33 - 13
misago/acl/tests/test_roleadmin_views.py

@@ -26,8 +26,9 @@ class RoleAdminViewsTests(AdminTestCase):
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         response = self.client.post(
         response = self.client.post(
-            reverse('misago:admin:permissions:users:new'),
-            data=fake_data({'name': 'Test Role'})
+            reverse('misago:admin:permissions:users:new'), data=fake_data({
+                'name': 'Test Role',
+            })
         )
         )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
@@ -39,19 +40,28 @@ class RoleAdminViewsTests(AdminTestCase):
     def test_edit_view(self):
     def test_edit_view(self):
         """edit role view has no showstoppers"""
         """edit role view has no showstoppers"""
         self.client.post(
         self.client.post(
-            reverse('misago:admin:permissions:users:new'),
-            data=fake_data({'name': 'Test Role'})
+            reverse('misago:admin:permissions:users:new'), data=fake_data({
+                'name': 'Test Role',
+            })
         )
         )
 
 
         test_role = Role.objects.get(name='Test Role')
         test_role = Role.objects.get(name='Test Role')
 
 
-        response = self.client.get(reverse('misago:admin:permissions:users:edit', kwargs={'pk': test_role.pk}))
+        response = self.client.get(
+            reverse('misago:admin:permissions:users:edit', kwargs={
+                'pk': test_role.pk,
+            })
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
         self.assertContains(response, 'Test Role')
         self.assertContains(response, 'Test Role')
 
 
         response = self.client.post(
         response = self.client.post(
-            reverse('misago:admin:permissions:users:edit', kwargs={'pk': test_role.pk}),
-            data=fake_data({'name': 'Top Lel'})
+            reverse('misago:admin:permissions:users:edit', kwargs={
+                'pk': test_role.pk,
+            }),
+            data=fake_data({
+                'name': 'Top Lel',
+            })
         )
         )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
@@ -63,23 +73,33 @@ class RoleAdminViewsTests(AdminTestCase):
     def test_users_view(self):
     def test_users_view(self):
         """users with this role view has no showstoppers"""
         """users with this role view has no showstoppers"""
         response = self.client.post(
         response = self.client.post(
-            reverse('misago:admin:permissions:users:new'),
-            data=fake_data({'name': 'Test Role'})
+            reverse('misago:admin:permissions:users:new'), data=fake_data({
+                'name': 'Test Role',
+            })
         )
         )
         test_role = Role.objects.get(name='Test Role')
         test_role = Role.objects.get(name='Test Role')
 
 
-        response = self.client.get(reverse('misago:admin:permissions:users:users', kwargs={'pk': test_role.pk}))
+        response = self.client.get(
+            reverse('misago:admin:permissions:users:users', kwargs={
+                'pk': test_role.pk,
+            })
+        )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
     def test_delete_view(self):
     def test_delete_view(self):
         """delete role view has no showstoppers"""
         """delete role view has no showstoppers"""
         self.client.post(
         self.client.post(
-            reverse('misago:admin:permissions:users:new'),
-            data=fake_data({'name': 'Test Role'})
+            reverse('misago:admin:permissions:users:new'), data=fake_data({
+                'name': 'Test Role',
+            })
         )
         )
 
 
         test_role = Role.objects.get(name='Test Role')
         test_role = Role.objects.get(name='Test Role')
-        response = self.client.post(reverse('misago:admin:permissions:users:delete', kwargs={'pk': test_role.pk}))
+        response = self.client.post(
+            reverse('misago:admin:permissions:users:delete', kwargs={
+                'pk': test_role.pk,
+            })
+        )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
         # Get the page twice so no alert is renderered on second request
         # Get the page twice so no alert is renderered on second request

+ 6 - 8
misago/acl/views.py

@@ -17,7 +17,7 @@ class RoleAdmin(generic.AdminBaseMixin):
 
 
 
 
 class RolesList(RoleAdmin, generic.ListView):
 class RolesList(RoleAdmin, generic.ListView):
-    ordering = (('name', None),)
+    ordering = (('name', None), )
 
 
 
 
 class RoleFormMixin(object):
 class RoleFormMixin(object):
@@ -43,8 +43,7 @@ class RoleFormMixin(object):
                 form.instance.permissions = new_permissions
                 form.instance.permissions = new_permissions
                 form.instance.save()
                 form.instance.save()
 
 
-                messages.success(
-                    request, self.message_submit % {'name': target.name})
+                messages.success(request, self.message_submit % {'name': target.name})
 
 
                 if 'stay' in request.POST:
                 if 'stay' in request.POST:
                     return redirect(request.path)
                     return redirect(request.path)
@@ -54,12 +53,12 @@ class RoleFormMixin(object):
                 form.add_error(None, _("Form contains errors."))
                 form.add_error(None, _("Form contains errors."))
 
 
         return self.render(
         return self.render(
-            request,
-            {
+            request, {
                 'form': form,
                 'form': form,
                 'target': target,
                 'target': target,
                 'perms_forms': perms_forms,
                 'perms_forms': perms_forms,
-            })
+            }
+        )
 
 
 
 
 class NewRole(RoleFormMixin, RoleAdmin, generic.ModelFormView):
 class NewRole(RoleFormMixin, RoleAdmin, generic.ModelFormView):
@@ -73,8 +72,7 @@ class EditRole(RoleFormMixin, RoleAdmin, generic.ModelFormView):
 class DeleteRole(RoleAdmin, generic.ButtonView):
 class DeleteRole(RoleAdmin, generic.ButtonView):
     def check_permissions(self, request, target):
     def check_permissions(self, request, target):
         if target.special_role:
         if target.special_role:
-            message = _('Role "%(name)s" is special role '
-                        'and can\'t be deleted.')
+            message = _('Role "%(name)s" is special role and can\'t be deleted.')
             return message % {'name': target.name}
             return message % {'name': target.name}
 
 
     def button_action(self, request, target):
     def button_action(self, request, target):

+ 0 - 1
misago/admin/__init__.py

@@ -2,5 +2,4 @@ from .hierarchy import site  # noqa
 from .urlpatterns import urlpatterns  # noqa
 from .urlpatterns import urlpatterns  # noqa
 from .discoverer import discover_misago_admin  # noqa
 from .discoverer import discover_misago_admin  # noqa
 
 
-
 default_app_config = 'misago.admin.apps.MisagoAdminConfig'
 default_app_config = 'misago.admin.apps.MisagoAdminConfig'

+ 0 - 1
misago/admin/admin.py

@@ -1,4 +1,3 @@
-from django.conf.urls import url
 from django.utils.deprecation import MiddlewareMixin
 from django.utils.deprecation import MiddlewareMixin
 from django.utils.translation import ugettext_lazy as _
 from django.utils.translation import ugettext_lazy as _
 
 

+ 4 - 0
misago/admin/auth.py

@@ -67,9 +67,13 @@ def django_login_handler(sender, **kwargs):
         admin_namespace = False
         admin_namespace = False
     if admin_namespace and user.is_staff:
     if admin_namespace and user.is_staff:
         start_admin_session(request, user)
         start_admin_session(request, user)
+
+
 dj_auth.signals.user_logged_in.connect(django_login_handler)
 dj_auth.signals.user_logged_in.connect(django_login_handler)
 
 
 
 
 def django_logout_handler(sender, **kwargs):
 def django_logout_handler(sender, **kwargs):
     close_admin_session(kwargs['request'])
     close_admin_session(kwargs['request'])
+
+
 dj_auth.signals.user_logged_out.connect(django_logout_handler)
 dj_auth.signals.user_logged_out.connect(django_logout_handler)

+ 8 - 6
misago/admin/discoverer.py

@@ -9,9 +9,11 @@ from .urlpatterns import urlpatterns
 def discover_misago_admin():
 def discover_misago_admin():
     for app in apps.get_app_configs():
     for app in apps.get_app_configs():
         module = import_module(app.name)
         module = import_module(app.name)
-        if hasattr(module, 'admin'):
-            admin_module = import_module('%s.admin' % app.name)
-            if hasattr(admin_module, 'MisagoAdminExtension'):
-                extension = getattr(admin_module, 'MisagoAdminExtension')()
-                extension.register_navigation_nodes(site)
-                extension.register_urlpatterns(urlpatterns)
+        if not hasattr(module, 'admin'):
+            continue
+
+        admin_module = import_module('%s.admin' % app.name)
+        if hasattr(admin_module, 'MisagoAdminExtension'):
+            extension = getattr(admin_module, 'MisagoAdminExtension')()
+            extension.register_navigation_nodes(site)
+            extension.register_urlpatterns(urlpatterns)

+ 20 - 17
misago/admin/hierarchy.py

@@ -81,8 +81,7 @@ class Node(object):
         try:
         try:
             return self._children_dict[namespace]
             return self._children_dict[namespace]
         except KeyError:
         except KeyError:
-            raise ValueError(
-                "Node %s is not a child of node %s" % (namespace, self.name))
+            raise ValueError("Node %s is not a child of node %s" % (namespace, self.name))
 
 
     def is_root(self):
     def is_root(self):
         return False
         return False
@@ -100,25 +99,20 @@ class AdminHierarchyBuilder(object):
         while self.nodes_record:
         while self.nodes_record:
             iterations += 1
             iterations += 1
             if iterations > 512:
             if iterations > 512:
-                message = ("Misago Admin hierarchy is invalid or too complex "
-                           "to resolve. Nodes left: %s")
+                message = (
+                    "Misago Admin hierarchy is invalid or too complex to resolve. Nodes left: %s"
+                )
                 raise ValueError(message % self.nodes_record)
                 raise ValueError(message % self.nodes_record)
 
 
             for index, node in enumerate(self.nodes_record):
             for index, node in enumerate(self.nodes_record):
                 if node['parent'] in nodes_dict:
                 if node['parent'] in nodes_dict:
-                    node_obj = Node(
-                        name=node['name'],
-                        icon=node['icon'],
-                        link=node['link']
-                    )
+                    node_obj = Node(name=node['name'], icon=node['icon'], link=node['link'])
 
 
                     parent = nodes_dict[node['parent']]
                     parent = nodes_dict[node['parent']]
                     if node['after']:
                     if node['after']:
-                        node_added = parent.add_node(
-                            node_obj, after=node['after'])
+                        node_added = parent.add_node(node_obj, after=node['after'])
                     elif node['before']:
                     elif node['before']:
-                        node_added = parent.add_node(
-                            node_obj, before=node['before'])
+                        node_added = parent.add_node(node_obj, before=node['before'])
                     else:
                     else:
                         node_added = parent.add_node(node_obj)
                         node_added = parent.add_node(node_obj)
 
 
@@ -133,11 +127,20 @@ class AdminHierarchyBuilder(object):
 
 
         return nodes_dict
         return nodes_dict
 
 
-    def add_node(self, name=None, icon=None, parent='misago:admin', after=None,
-                 before=None, namespace=None, link=None):
+    def add_node(
+            self,
+            name=None,
+            icon=None,
+            parent='misago:admin',
+            after=None,
+            before=None,
+            namespace=None,
+            link=None
+    ):
         if self.nodes_dict:
         if self.nodes_dict:
-            raise RuntimeError("Misago admin site has already been "
-                               "initialized. You can't add new nodes to it.")
+            raise RuntimeError(
+                "Misago admin site has already been initialized. You can't add new nodes to it."
+            )
 
 
         if after and before:
         if after and before:
             raise ValueError("after and before arguments are exclusive")
             raise ValueError("after and before arguments are exclusive")

+ 30 - 21
misago/admin/tests/test_admin_views.py

@@ -22,11 +22,7 @@ class AdminProtectedNamespaceTests(TestCase):
     def test_valid_cases(self):
     def test_valid_cases(self):
         """get_protected_namespace returns true for protected links"""
         """get_protected_namespace returns true for protected links"""
         links_prefix = reverse('misago:admin:index')
         links_prefix = reverse('misago:admin:index')
-        TEST_CASES = (
-            '',
-            'somewhere/',
-            'ejksajdlksajldjskajdlksajlkdas',
-        )
+        TEST_CASES = ('', 'somewhere/', 'ejksajdlksajldjskajdlksajlkdas', )
 
 
         for case in TEST_CASES:
         for case in TEST_CASES:
             request = FakeRequest(links_prefix + case)
             request = FakeRequest(links_prefix + case)
@@ -34,11 +30,7 @@ class AdminProtectedNamespaceTests(TestCase):
 
 
     def test_invalid_cases(self):
     def test_invalid_cases(self):
         """get_protected_namespace returns none for other links"""
         """get_protected_namespace returns none for other links"""
-        TEST_CASES = (
-            '/',
-            '/somewhere/',
-            '/ejksajdlksajldjskajdlksajlkdas',
-        )
+        TEST_CASES = ('/', '/somewhere/', '/ejksajdlksajldjskajdlksajlkdas', )
 
 
         for case in TEST_CASES:
         for case in TEST_CASES:
             request = FakeRequest(case)
             request = FakeRequest(case)
@@ -58,7 +50,11 @@ class AdminLoginViewTests(TestCase):
         """form handles invalid data gracefully"""
         """form handles invalid data gracefully"""
         response = self.client.post(
         response = self.client.post(
             reverse('misago:admin:index'),
             reverse('misago:admin:index'),
-            data={'username': 'Nope', 'password': 'Nope'})
+            data={
+                'username': 'Nope',
+                'password': 'Nope',
+            },
+        )
 
 
         self.assertContains(response, "Login or password is incorrect.")
         self.assertContains(response, "Login or password is incorrect.")
         self.assertContains(response, "Sign in")
         self.assertContains(response, "Sign in")
@@ -75,7 +71,11 @@ class AdminLoginViewTests(TestCase):
 
 
         response = self.client.post(
         response = self.client.post(
             reverse('misago:admin:index'),
             reverse('misago:admin:index'),
-            data={'username': 'Bob', 'password': 'Pass.123'})
+            data={
+                'username': 'Bob',
+                'password': 'Pass.123',
+            },
+        )
 
 
         self.assertContains(response, "Your account does not have admin privileges.")
         self.assertContains(response, "Your account does not have admin privileges.")
 
 
@@ -89,7 +89,11 @@ class AdminLoginViewTests(TestCase):
 
 
         response = self.client.post(
         response = self.client.post(
             reverse('misago:admin:index'),
             reverse('misago:admin:index'),
-            data={'username': 'Bob', 'password': 'Pass.123'})
+            data={
+                'username': 'Bob',
+                'password': 'Pass.123',
+            },
+        )
 
 
         self.assertContains(response, "Your account does not have admin privileges.")
         self.assertContains(response, "Your account does not have admin privileges.")
 
 
@@ -103,7 +107,11 @@ class AdminLoginViewTests(TestCase):
 
 
         response = self.client.post(
         response = self.client.post(
             reverse('misago:admin:index'),
             reverse('misago:admin:index'),
-            data={'username': 'Bob', 'password': 'Pass.123'})
+            data={
+                'username': 'Bob',
+                'password': 'Pass.123',
+            },
+        )
 
 
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
@@ -117,7 +125,11 @@ class AdminLoginViewTests(TestCase):
 
 
         response = self.client.post(
         response = self.client.post(
             reverse('misago:admin:index'),
             reverse('misago:admin:index'),
-            data={'username': 'Bob', 'password': 'Pass.123'})
+            data={
+                'username': 'Bob',
+                'password': 'Pass.123',
+            },
+        )
 
 
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
@@ -199,8 +211,7 @@ class Admin404ErrorTests(AdminTestCase):
 
 
         response = self.client.get(test_link)
         response = self.client.get(test_link)
 
 
-        self.assertContains(
-            response, "Requested page could not be found.", status_code=404)
+        self.assertContains(response, "Requested page could not be found.", status_code=404)
 
 
 
 
 class AdminGenericViewsTests(AdminTestCase):
 class AdminGenericViewsTests(AdminTestCase):
@@ -214,13 +225,11 @@ class AdminGenericViewsTests(AdminTestCase):
         self.assertIn('redirected=1', response['location'])
         self.assertIn('redirected=1', response['location'])
 
 
         # request with flag muted redirect
         # request with flag muted redirect
-        response = self.client.get(
-            '%s?redirected=1&username=lorem' % test_link)
+        response = self.client.get('%s?redirected=1&username=lorem' % test_link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
     def test_list_search_unicode_handling(self):
     def test_list_search_unicode_handling(self):
         """querystring creation handles unicode strings"""
         """querystring creation handles unicode strings"""
         test_link = reverse('misago:admin:users:accounts:index')
         test_link = reverse('misago:admin:users:accounts:index')
-        response = self.client.get(
-            '%s?redirected=1&username=%s' % (test_link, 'łut'))
+        response = self.client.get('%s?redirected=1&username=%s' % (test_link, 'łut'))
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)

+ 7 - 4
misago/admin/testutils.py

@@ -9,8 +9,11 @@ class AdminTestCase(SuperUserTestCase):
         self.login_admin(self.user)
         self.login_admin(self.user)
 
 
     def login_admin(self, user):
     def login_admin(self, user):
-        self.client.post(reverse('misago:admin:index'), data={
-            'username': user.email,
-            'password': self.USER_PASSWORD
-        })
+        self.client.post(
+            reverse('misago:admin:index'),
+            data={
+                'username': user.email,
+                'password': self.USER_PASSWORD,
+            }
+        )
         self.client.get(reverse('misago:admin:index'))
         self.client.get(reverse('misago:admin:index'))

+ 10 - 10
misago/admin/urlpatterns.py

@@ -11,13 +11,13 @@ class URLPatterns(object):
             'path': path,
             'path': path,
             'parent': parent,
             'parent': parent,
             'namespace': namespace,
             'namespace': namespace,
-            })
+        })
 
 
-    def patterns(self, namespace, *urlpatterns):
+    def patterns(self, namespace, *new_patterns):
         self._patterns.append({
         self._patterns.append({
             'namespace': namespace,
             'namespace': namespace,
-            'urlpatterns': urlpatterns,
-            })
+            'urlpatterns': new_patterns,
+        })
 
 
     def get_child_patterns(self, parent):
     def get_child_patterns(self, parent):
         prefix = '%s:' % parent if parent else ''
         prefix = '%s:' % parent if parent else ''
@@ -26,8 +26,8 @@ class URLPatterns(object):
         for namespace in self._namespaces:
         for namespace in self._namespaces:
             if namespace['parent'] == parent:
             if namespace['parent'] == parent:
                 prefixed_namespace = prefix + namespace['namespace']
                 prefixed_namespace = prefix + namespace['namespace']
-                urlpatterns = self.get_child_patterns(prefixed_namespace)
-                included_patterns = include(urlpatterns, namespace=namespace['namespace'])
+                child_patterns = self.get_child_patterns(prefixed_namespace)
+                included_patterns = include(child_patterns, namespace=namespace['namespace'])
                 namespace_urlpatterns.append(url(namespace['path'], included_patterns))
                 namespace_urlpatterns.append(url(namespace['path'], included_patterns))
 
 
         return namespace_urlpatterns
         return namespace_urlpatterns
@@ -36,8 +36,8 @@ class URLPatterns(object):
         all_patterns = {}
         all_patterns = {}
         for urls in self._patterns:
         for urls in self._patterns:
             namespace = urls['namespace']
             namespace = urls['namespace']
-            urlpatterns = urls['urlpatterns']
-            all_patterns.setdefault(namespace, []).extend(urlpatterns)
+            added_patterns = urls['urlpatterns']
+            all_patterns.setdefault(namespace, []).extend(added_patterns)
 
 
         self.namespace_patterns = all_patterns
         self.namespace_patterns = all_patterns
 
 
@@ -45,8 +45,8 @@ class URLPatterns(object):
         root_urlpatterns = []
         root_urlpatterns = []
         for namespace in self._namespaces:
         for namespace in self._namespaces:
             if not namespace['parent']:
             if not namespace['parent']:
-                urlpatterns = self.get_child_patterns(namespace['namespace'])
-                included_patterns = include(urlpatterns, namespace=namespace['namespace'])
+                child_patterns = self.get_child_patterns(namespace['namespace'])
+                included_patterns = include(child_patterns, namespace=namespace['namespace'])
                 root_urlpatterns.append(url(namespace['path'], included_patterns))
                 root_urlpatterns.append(url(namespace['path'], included_patterns))
 
 
         return root_urlpatterns
         return root_urlpatterns

+ 1 - 2
misago/admin/urls.py

@@ -1,4 +1,4 @@
-from django.conf.urls import include, url
+from django.conf.urls import url
 
 
 from misago import admin
 from misago import admin
 
 
@@ -14,7 +14,6 @@ urlpatterns = [
     url(r'^logout/$', auth.logout, name='logout'),
     url(r'^logout/$', auth.logout, name='logout'),
 ]
 ]
 
 
-
 # Discover admin and register patterns
 # Discover admin and register patterns
 admin.discover_misago_admin()
 admin.discover_misago_admin()
 urlpatterns += admin.urlpatterns()
 urlpatterns += admin.urlpatterns()

+ 0 - 1
misago/admin/views/__init__.py

@@ -8,7 +8,6 @@ from misago.admin.auth import is_admin_session, update_admin_session
 from .auth import login
 from .auth import login
 
 
 
 
-
 def get_protected_namespace(request):
 def get_protected_namespace(request):
     for namespace in settings.MISAGO_ADMIN_NAMESPACES:
     for namespace in settings.MISAGO_ADMIN_NAMESPACES:
         try:
         try:

+ 2 - 4
misago/admin/views/auth.py

@@ -28,8 +28,7 @@ def login(request):
             auth.login(request, form.user_cache)
             auth.login(request, form.user_cache)
             return redirect('%s:index' % request.admin_namespace)
             return redirect('%s:index' % request.admin_namespace)
 
 
-    return render(request, 'misago/admin/login.html',
-                  {'form': form, 'target': target})
+    return render(request, 'misago/admin/login.html', {'form': form, 'target': target})
 
 
 
 
 @csrf_protect
 @csrf_protect
@@ -37,8 +36,7 @@ def login(request):
 def logout(request):
 def logout(request):
     if request.method == 'POST':
     if request.method == 'POST':
         auth.close_admin_session(request)
         auth.close_admin_session(request)
-        messages.info(request,
-                      _("Your admin session has been closed."))
+        messages.info(request, _("Your admin session has been closed."))
         return redirect('misago:index')
         return redirect('misago:index')
     else:
     else:
         return redirect('misago:admin:index')
         return redirect('misago:admin:index')

+ 7 - 5
misago/admin/views/errorpages.py

@@ -11,9 +11,7 @@ def _error_page(request, code, message=None):
     if is_admin_session(request):
     if is_admin_session(request):
         template_pattern = 'misago/admin/errorpages/%s.html' % code
         template_pattern = 'misago/admin/errorpages/%s.html' % code
 
 
-        response = render(request, template_pattern, {
-            'message': message
-        }, error_page=True)
+        response = render(request, template_pattern, {'message': message}, error_page=True)
         response.status_code = code
         response.status_code = code
         return response
         return response
     else:
     else:
@@ -27,6 +25,7 @@ def admin_error_page(f):
             return _error_page(request, *args, **kwargs)
             return _error_page(request, *args, **kwargs)
         else:
         else:
             return f(request, *args, **kwargs)
             return f(request, *args, **kwargs)
+
     return decorator
     return decorator
 
 
 
 
@@ -35,8 +34,10 @@ def _csrf_failure(request, reason=""):
     if is_admin_session(request):
     if is_admin_session(request):
         update_admin_session(request)
         update_admin_session(request)
         response = render(
         response = render(
-            request, 'misago/admin/errorpages/csrf_failure_authenticated.html',
-            error_page=True)
+            request,
+            'misago/admin/errorpages/csrf_failure_authenticated.html',
+            error_page=True,
+        )
     else:
     else:
         response = render(request, 'misago/admin/errorpages/csrf_failure.html')
         response = render(request, 'misago/admin/errorpages/csrf_failure.html')
 
 
@@ -50,4 +51,5 @@ def admin_csrf_failure(f):
             return _csrf_failure(request, *args, **kwargs)
             return _csrf_failure(request, *args, **kwargs)
         else:
         else:
             return f(request, *args, **kwargs)
             return f(request, *args, **kwargs)
+
     return decorator
     return decorator

+ 1 - 2
misago/admin/views/generic/__init__.py

@@ -1,5 +1,4 @@
 from .mixin import AdminBaseMixin
 from .mixin import AdminBaseMixin
 from .base import AdminView
 from .base import AdminView
 from .list import ListView, MassActionError
 from .list import ListView, MassActionError
-from .formsbuttons import (
-    TargetedView, FormView, ModelFormView, ButtonView)
+from .formsbuttons import (TargetedView, FormView, ModelFormView, ButtonView)

+ 1 - 3
misago/admin/views/generic/base.py

@@ -12,9 +12,7 @@ class AdminView(View):
         return '%s:%s' % (request.resolver_match.namespace, matched_url)
         return '%s:%s' % (request.resolver_match.namespace, matched_url)
 
 
     def process_context(self, request, context):
     def process_context(self, request, context):
-        """
-        Simple hook for extending and manipulating template context.
-        """
+        """simple hook for extending and manipulating template context."""
         return context
         return context
 
 
     def render(self, request, context=None, template=None):
     def render(self, request, context=None, template=None):

+ 13 - 14
misago/admin/views/generic/formsbuttons.py

@@ -18,7 +18,7 @@ class TargetedView(AdminView):
                 select_for_update = select_for_update.select_for_update()
                 select_for_update = select_for_update.select_for_update()
             # Does not work on Python 3:
             # Does not work on Python 3:
             # return select_for_update.get(pk=kwargs[kwargs.keys()[0]])
             # return select_for_update.get(pk=kwargs[kwargs.keys()[0]])
-            (pk,) = kwargs.values()
+            (pk, ) = kwargs.values()
             return select_for_update.get(pk=pk)
             return select_for_update.get(pk=pk)
         else:
         else:
             return self.get_model()()
             return self.get_model()()
@@ -37,17 +37,17 @@ class TargetedView(AdminView):
             return self.wrapped_dispatch(request, *args, **kwargs)
             return self.wrapped_dispatch(request, *args, **kwargs)
 
 
     def wrapped_dispatch(self, request, *args, **kwargs):
     def wrapped_dispatch(self, request, *args, **kwargs):
-            target = self.get_target_or_none(request, kwargs)
-            if not target:
-                messages.error(request, self.message_404)
-                return redirect(self.root_link)
+        target = self.get_target_or_none(request, kwargs)
+        if not target:
+            messages.error(request, self.message_404)
+            return redirect(self.root_link)
 
 
-            error = self.check_permissions(request, target)
-            if error:
-                messages.error(request, error)
-                return redirect(self.root_link)
+        error = self.check_permissions(request, target)
+        if error:
+            messages.error(request, error)
+            return redirect(self.root_link)
 
 
-            return self.real_dispatch(request, target)
+        return self.real_dispatch(request, target)
 
 
     def real_dispatch(self, request, target):
     def real_dispatch(self, request, target):
         pass
         pass
@@ -68,8 +68,8 @@ class FormView(TargetedView):
 
 
     def handle_form(self, form, request):
     def handle_form(self, form, request):
         raise NotImplementedError(
         raise NotImplementedError(
-            "You have to define your own handle_form method to handle "
-            "form submissions.")
+            "You have to define your own handle_form method to handle form submissions."
+        )
 
 
     def real_dispatch(self, request, target):
     def real_dispatch(self, request, target):
         FormType = self.create_form_type(request)
         FormType = self.create_form_type(request)
@@ -103,8 +103,7 @@ class ModelFormView(FormView):
     def handle_form(self, form, request, target):
     def handle_form(self, form, request, target):
         form.instance.save()
         form.instance.save()
         if self.message_submit:
         if self.message_submit:
-            format = {'name': target.name}
-            messages.success(request, self.message_submit % format)
+            messages.success(request, self.message_submit % {'name': target.name})
 
 
     def real_dispatch(self, request, target):
     def real_dispatch(self, request, target):
         FormType = self.create_form_type(request, target)
         FormType = self.create_form_type(request, target)

+ 14 - 34
misago/admin/views/generic/list.py

@@ -65,9 +65,6 @@ class ListView(AdminView):
     def get_queryset(self):
     def get_queryset(self):
         return self.get_model().objects.all()
         return self.get_model().objects.all()
 
 
-    """
-    Dispatch response
-    """
     def dispatch(self, request, *args, **kwargs):
     def dispatch(self, request, *args, **kwargs):
         mass_actions_list = self.mass_actions or []
         mass_actions_list = self.mass_actions or []
         extra_actions_list = self.extra_actions or []
         extra_actions_list = self.extra_actions or []
@@ -76,25 +73,19 @@ class ListView(AdminView):
 
 
         context = {
         context = {
             'items': self.get_queryset(),
             'items': self.get_queryset(),
-
             'paginator': None,
             'paginator': None,
             'page': None,
             'page': None,
-
             'order_by': [],
             'order_by': [],
             'order': None,
             'order': None,
-
             'search_form': None,
             'search_form': None,
             'active_filters': {},
             'active_filters': {},
-
             'querystring': '',
             'querystring': '',
             'query_order': {},
             'query_order': {},
             'query_filters': {},
             'query_filters': {},
-
             'selected_items': [],
             'selected_items': [],
             'selection_label': self.selection_label,
             'selection_label': self.selection_label,
             'empty_selection_label': self.empty_selection_label,
             'empty_selection_label': self.empty_selection_label,
             'mass_actions': mass_actions_list,
             'mass_actions': mass_actions_list,
-
             'extra_actions': extra_actions_list,
             'extra_actions': extra_actions_list,
             'extra_actions_len': len(extra_actions_list),
             'extra_actions_len': len(extra_actions_list),
         }
         }
@@ -114,8 +105,7 @@ class ListView(AdminView):
             used_method = self.get_ordering_method_to_use(ordering_methods)
             used_method = self.get_ordering_method_to_use(ordering_methods)
             self.set_ordering_in_context(context, used_method)
             self.set_ordering_in_context(context, used_method)
 
 
-            if (ordering_methods['GET'] and
-                    ordering_methods['GET'] != ordering_methods['session']):
+            if (ordering_methods['GET'] and ordering_methods['GET'] != ordering_methods['session']):
                 # Store GET ordering in session for future requests
                 # Store GET ordering in session for future requests
                 session_key = self.ordering_session_key
                 session_key = self.ordering_session_key
                 request.session[session_key] = ordering_methods['GET']
                 request.session[session_key] = ordering_methods['GET']
@@ -128,14 +118,12 @@ class ListView(AdminView):
         SearchForm = self.get_search_form(request)
         SearchForm = self.get_search_form(request)
         if SearchForm:
         if SearchForm:
             filtering_methods = self.get_filtering_methods(request)
             filtering_methods = self.get_filtering_methods(request)
-            active_filters = self.get_filtering_method_to_use(
-                filtering_methods)
+            active_filters = self.get_filtering_method_to_use(filtering_methods)
             if request.GET.get('clear_filters'):
             if request.GET.get('clear_filters'):
                 # Clear filters from querystring
                 # Clear filters from querystring
                 request.session.pop(self.filters_session_key, None)
                 request.session.pop(self.filters_session_key, None)
                 active_filters = {}
                 active_filters = {}
-            self.apply_filtering_on_context(
-                context, active_filters, SearchForm)
+            self.apply_filtering_on_context(context, active_filters, SearchForm)
 
 
             if (filtering_methods['GET'] and
             if (filtering_methods['GET'] and
                     filtering_methods['GET'] != filtering_methods['session']):
                     filtering_methods['GET'] != filtering_methods['session']):
@@ -159,8 +147,7 @@ class ListView(AdminView):
             try:
             try:
                 self.paginate_items(context, kwargs.get('page', 0))
                 self.paginate_items(context, kwargs.get('page', 0))
             except EmptyPage:
             except EmptyPage:
-                return redirect(
-                    '%s%s' % (reverse(self.root_link), context['querystring']))
+                return redirect('%s%s' % (reverse(self.root_link), context['querystring']))
 
 
         if refresh_querystring and not request.GET.get('redirected'):
         if refresh_querystring and not request.GET.get('redirected'):
             return redirect('%s%s' % (request.path_info, context['querystring']))
             return redirect('%s%s' % (request.path_info, context['querystring']))
@@ -178,13 +165,12 @@ class ListView(AdminView):
             page = 1
             page = 1
 
 
         context['paginator'] = Paginator(
         context['paginator'] = Paginator(
-            context['items'], self.items_per_page, allow_empty_first_page=True)
+            context['items'], self.items_per_page, allow_empty_first_page=True
+        )
         context['page'] = context['paginator'].page(page)
         context['page'] = context['paginator'].page(page)
         context['items'] = context['page'].object_list
         context['items'] = context['page'].object_list
 
 
-    """
-    Filter list items
-    """
+    # Filter list items
     search_form = None
     search_form = None
 
 
     def get_search_form(self, request):
     def get_search_form(self, request):
@@ -236,11 +222,10 @@ class ListView(AdminView):
 
 
         if context['active_filters']:
         if context['active_filters']:
             context['items'] = context['search_form'].filter_queryset(
             context['items'] = context['search_form'].filter_queryset(
-                active_filters, context['items'])
+                active_filters, context['items']
+            )
 
 
-    """
-    Order list items
-    """
+    # Order list items
     @property
     @property
     def ordering_session_key(self):
     def ordering_session_key(self):
         return 'misago_admin_%s_order_by' % self.root_link
         return 'misago_admin_%s_order_by' % self.root_link
@@ -262,7 +247,7 @@ class ListView(AdminView):
         return self.clean_ordering(new_ordering)
         return self.clean_ordering(new_ordering)
 
 
     def clean_ordering(self, new_ordering):
     def clean_ordering(self, new_ordering):
-        for order_by, name in self.ordering:
+        for order_by, _ in self.ordering:
             if order_by == new_ordering:
             if order_by == new_ordering:
                 return order_by
                 return order_by
         else:
         else:
@@ -290,16 +275,13 @@ class ListView(AdminView):
 
 
             if order_by == method:
             if order_by == method:
                 context['order'] = order_as_dict
                 context['order'] = order_as_dict
-                context['items'] = context['items'].order_by(
-                    order_as_dict['order_by'])
+                context['items'] = context['items'].order_by(order_as_dict['order_by'])
             elif order_as_dict['name']:
             elif order_as_dict['name']:
                 if order_as_dict['type'] == 'desc':
                 if order_as_dict['type'] == 'desc':
                     order_as_dict['order_by'] = order_as_dict['order_by'][1:]
                     order_as_dict['order_by'] = order_as_dict['order_by'][1:]
                 context['order_by'].append(order_as_dict)
                 context['order_by'].append(order_as_dict)
 
 
-    """
-    Mass actions
-    """
+    # Mass actions
     def handle_mass_action(self, request, context):
     def handle_mass_action(self, request, context):
         limit = self.items_per_page or 64
         limit = self.items_per_page or 64
         action = self.select_mass_action(request.POST.get('action'))
         action = self.select_mass_action(request.POST.get('action'))
@@ -329,9 +311,7 @@ class ListView(AdminView):
         else:
         else:
             raise MassActionError(_("Action is not allowed."))
             raise MassActionError(_("Action is not allowed."))
 
 
-    """
-    Querystring builder
-    """
+    # Querystring builder
     def make_querystring(self, context):
     def make_querystring(self, context):
         values = {}
         values = {}
         filter_values = {}
         filter_values = {}

+ 1 - 3
misago/admin/views/generic/mixin.py

@@ -15,7 +15,5 @@ class AdminBaseMixin(object):
     message_404 = None
     message_404 = None
 
 
     def get_model(self):
     def get_model(self):
-        """
-        Basic method for retrieving Model, used in cases such as User model.
-        """
+        """basic method for retrieving Model, used in cases such as User model."""
         return self.model
         return self.model

+ 20 - 12
misago/admin/views/index.py

@@ -5,7 +5,6 @@ from requests.exceptions import RequestException
 
 
 from django.contrib.auth import get_user_model
 from django.contrib.auth import get_user_model
 from django.http import Http404, JsonResponse
 from django.http import Http404, JsonResponse
-from django.utils.six.moves import range
 from django.utils.translation import ugettext as _
 from django.utils.translation import ugettext as _
 
 
 from misago import __version__
 from misago import __version__
@@ -21,25 +20,31 @@ UserModel = get_user_model()
 
 
 
 
 def admin_index(request):
 def admin_index(request):
+    inactive_users_queryset = UserModel.objects.exclude(
+        requires_activation=UserModel.ACTIVATION_NONE,
+    )
+
     db_stats = {
     db_stats = {
         'threads': Thread.objects.count(),
         'threads': Thread.objects.count(),
         'posts': Post.objects.count(),
         'posts': Post.objects.count(),
         'users': UserModel.objects.count(),
         'users': UserModel.objects.count(),
-        'inactive_users': UserModel.objects.exclude(
-            requires_activation=UserModel.ACTIVATION_NONE
-        ).count()
+        'inactive_users': inactive_users_queryset.count()
     }
     }
 
 
-    return render(request, 'misago/admin/index.html', {
-        'db_stats': db_stats,
-        'version_check': cache.get(VERSION_CHECK_CACHE_KEY)
-    })
+    return render(
+        request, 'misago/admin/index.html', {
+            'db_stats': db_stats,
+            'version_check': cache.get(VERSION_CHECK_CACHE_KEY),
+        }
+    )
 
 
 
 
 def check_version(request):
 def check_version(request):
     if request.method != "POST":
     if request.method != "POST":
         raise Http404()
         raise Http404()
+
     version = cache.get(VERSION_CHECK_CACHE_KEY, 'nada')
     version = cache.get(VERSION_CHECK_CACHE_KEY, 'nada')
+
     if version == 'nada':
     if version == 'nada':
         try:
         try:
             api_url = 'https://api.github.com/repos/rafalp/Misago/releases'
             api_url = 'https://api.github.com/repos/rafalp/Misago/releases'
@@ -57,16 +62,18 @@ def check_version(request):
                     message = _("Outdated: %(current)s < %(latest)s")
                     message = _("Outdated: %(current)s < %(latest)s")
                     formats = {
                     formats = {
                         'latest': latest_version,
                         'latest': latest_version,
-                        'current': __version__
+                        'current': __version__,
                     }
                     }
 
 
                     version = {
                     version = {
                         'is_error': True,
                         'is_error': True,
-                        'message': message % formats
+                        'message': message % formats,
                     }
                     }
                     break
                     break
             else:
             else:
-                formats = {'current': __version__}
+                formats = {
+                    'current': __version__,
+                }
                 version = {
                 version = {
                     'is_error': False,
                     'is_error': False,
                     'message': _("Up to date! (%(current)s)") % formats,
                     'message': _("Up to date! (%(current)s)") % formats,
@@ -77,6 +84,7 @@ def check_version(request):
             message = _("Failed to connect to GitHub API. Try again later.")
             message = _("Failed to connect to GitHub API. Try again later.")
             version = {
             version = {
                 'is_error': True,
                 'is_error': True,
-                'message': message
+                'message': message,
             }
             }
+
     return JsonResponse(version)
     return JsonResponse(version)

+ 0 - 1
misago/categories/__init__.py

@@ -1,4 +1,3 @@
 from .constants import *
 from .constants import *
 
 
-
 default_app_config = 'misago.categories.apps.MisagoCategoriesConfig'
 default_app_config = 'misago.categories.apps.MisagoCategoriesConfig'

+ 9 - 6
misago/categories/admin.py

@@ -15,7 +15,8 @@ class MisagoAdminExtension(object):
 
 
         # Nodes
         # Nodes
         urlpatterns.namespace(r'^nodes/', 'nodes', 'categories')
         urlpatterns.namespace(r'^nodes/', 'nodes', 'categories')
-        urlpatterns.patterns('categories:nodes',
+        urlpatterns.patterns(
+            'categories:nodes',
             url(r'^$', CategoriesList.as_view(), name='index'),
             url(r'^$', CategoriesList.as_view(), name='index'),
             url(r'^new/$', NewCategory.as_view(), name='new'),
             url(r'^new/$', NewCategory.as_view(), name='new'),
             url(r'^edit/(?P<pk>\d+)/$', EditCategory.as_view(), name='edit'),
             url(r'^edit/(?P<pk>\d+)/$', EditCategory.as_view(), name='edit'),
@@ -27,7 +28,8 @@ class MisagoAdminExtension(object):
 
 
         # Category Roles
         # Category Roles
         urlpatterns.namespace(r'^categories/', 'categories', 'permissions')
         urlpatterns.namespace(r'^categories/', 'categories', 'permissions')
-        urlpatterns.patterns('permissions:categories',
+        urlpatterns.patterns(
+            'permissions:categories',
             url(r'^$', CategoryRolesList.as_view(), name='index'),
             url(r'^$', CategoryRolesList.as_view(), name='index'),
             url(r'^new/$', NewCategoryRole.as_view(), name='new'),
             url(r'^new/$', NewCategoryRole.as_view(), name='new'),
             url(r'^edit/(?P<pk>\d+)/$', EditCategoryRole.as_view(), name='edit'),
             url(r'^edit/(?P<pk>\d+)/$', EditCategoryRole.as_view(), name='edit'),
@@ -35,7 +37,8 @@ class MisagoAdminExtension(object):
         )
         )
 
 
         # Change Role Category Permissions
         # Change Role Category Permissions
-        urlpatterns.patterns('permissions:users',
+        urlpatterns.patterns(
+            'permissions:users',
             url(r'^categories/(?P<pk>\d+)/$', RoleCategoriesACL.as_view(), name='categories'),
             url(r'^categories/(?P<pk>\d+)/$', RoleCategoriesACL.as_view(), name='categories'),
         )
         )
 
 
@@ -46,14 +49,14 @@ class MisagoAdminExtension(object):
             parent='misago:admin',
             parent='misago:admin',
             before='misago:admin:permissions:users:index',
             before='misago:admin:permissions:users:index',
             namespace='misago:admin:categories',
             namespace='misago:admin:categories',
-            link='misago:admin:categories:nodes:index'
+            link='misago:admin:categories:nodes:index',
         )
         )
         site.add_node(
         site.add_node(
             name=_("Categories hierarchy"),
             name=_("Categories hierarchy"),
             icon='fa fa-sitemap',
             icon='fa fa-sitemap',
             parent='misago:admin:categories',
             parent='misago:admin:categories',
             namespace='misago:admin:categories:nodes',
             namespace='misago:admin:categories:nodes',
-            link='misago:admin:categories:nodes:index'
+            link='misago:admin:categories:nodes:index',
         )
         )
         site.add_node(
         site.add_node(
             name=_("Category roles"),
             name=_("Category roles"),
@@ -61,5 +64,5 @@ class MisagoAdminExtension(object):
             parent='misago:admin:permissions',
             parent='misago:admin:permissions',
             after='misago:admin:permissions:users:index',
             after='misago:admin:permissions:users:index',
             namespace='misago:admin:permissions:categories',
             namespace='misago:admin:permissions:categories',
-            link='misago:admin:permissions:categories:index'
+            link='misago:admin:permissions:categories:index',
         )
         )

+ 1 - 1
misago/categories/apps.py

@@ -7,4 +7,4 @@ class MisagoCategoriesConfig(AppConfig):
     verbose_name = "Misago Categories"
     verbose_name = "Misago Categories"
 
 
     def ready(self):
     def ready(self):
-        from . import signals
+        from . import signals as _

+ 61 - 60
misago/categories/forms.py

@@ -13,9 +13,6 @@ from . import THREADS_ROOT_NAME
 from .models import Category, CategoryRole
 from .models import Category, CategoryRole
 
 
 
 
-"""
-Fields
-"""
 class AdminCategoryFieldMixin(object):
 class AdminCategoryFieldMixin(object):
     def __init__(self, *args, **kwargs):
     def __init__(self, *args, **kwargs):
         self.base_level = kwargs.pop('base_level', 1)
         self.base_level = kwargs.pop('base_level', 1)
@@ -42,57 +39,53 @@ class AdminCategoryChoiceField(AdminCategoryFieldMixin, TreeNodeChoiceField):
     pass
     pass
 
 
 
 
-class AdminCategoryMultipleChoiceField(
-        AdminCategoryFieldMixin, TreeNodeMultipleChoiceField):
+class AdminCategoryMultipleChoiceField(AdminCategoryFieldMixin, TreeNodeMultipleChoiceField):
     pass
     pass
 
 
 
 
-"""
-Forms
-"""
 class CategoryFormBase(forms.ModelForm):
 class CategoryFormBase(forms.ModelForm):
-    name = forms.CharField(
-        label=_("Name"),
-        validators=[validate_sluggable()]
-    )
+    name = forms.CharField(label=_("Name"), validators=[validate_sluggable()])
     description = forms.CharField(
     description = forms.CharField(
         label=_("Description"),
         label=_("Description"),
         max_length=2048,
         max_length=2048,
         required=False,
         required=False,
         widget=forms.Textarea(attrs={'rows': 3}),
         widget=forms.Textarea(attrs={'rows': 3}),
-        help_text=_("Optional description explaining category intented purpose.")
+        help_text=_("Optional description explaining category intented purpose."),
     )
     )
     css_class = forms.CharField(
     css_class = forms.CharField(
         label=_("CSS class"),
         label=_("CSS class"),
         required=False,
         required=False,
-        help_text=_("Optional CSS class used to customize this category "
-                    "appearance from templates.")
+        help_text=_(
+            "Optional CSS class used to customize this category appearance from templates."
+        ),
     )
     )
     is_closed = YesNoSwitch(
     is_closed = YesNoSwitch(
         label=_("Closed category"),
         label=_("Closed category"),
         required=False,
         required=False,
-        help_text=_("Only members with valid permissions can post in "
-                    "closed categories.")
+        help_text=_("Only members with valid permissions can post in closed categories."),
     )
     )
     css_class = forms.CharField(
     css_class = forms.CharField(
         label=_("CSS class"),
         label=_("CSS class"),
         required=False,
         required=False,
-        help_text=_("Optional CSS class used to customize this category "
-                    "appearance from templates.")
+        help_text=_(
+            "Optional CSS class used to customize this category appearance from templates."
+        ),
     )
     )
     prune_started_after = forms.IntegerField(
     prune_started_after = forms.IntegerField(
         label=_("Thread age"),
         label=_("Thread age"),
         min_value=0,
         min_value=0,
-        help_text=_("Prune thread if number of days since its creation is "
-                    "greater than specified. Enter 0 to disable this "
-                    "pruning criteria.")
+        help_text=_(
+            "Prune thread if number of days since its creation is greater than specified. "
+            "Enter 0 to disable this pruning criteria."
+        ),
     )
     )
     prune_replied_after = forms.IntegerField(
     prune_replied_after = forms.IntegerField(
         label=_("Last reply"),
         label=_("Last reply"),
         min_value=0,
         min_value=0,
-        help_text=_("Prune thread if number of days since last reply is "
-                    "greater than specified. Enter 0 to disable this "
-                    "pruning criteria.")
+        help_text=_(
+            "Prune thread if number of days since last reply is greater than specified. "
+            "Enter 0 to disable this pruning criteria."
+        ),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -134,29 +127,36 @@ def CategoryFormFactory(instance):
         not_siblings = not_siblings | models.Q(rght__gt=instance.rght)
         not_siblings = not_siblings | models.Q(rght__gt=instance.rght)
         parent_queryset = parent_queryset.filter(not_siblings)
         parent_queryset = parent_queryset.filter(not_siblings)
 
 
-    return type('CategoryFormFinal', (CategoryFormBase,), {
-        'new_parent': AdminCategoryChoiceField(
-            label=_("Parent category"),
-            queryset=parent_queryset,
-            initial=instance.parent,
-            empty_label=None),
-
-        'copy_permissions': AdminCategoryChoiceField(
-            label=_("Copy permissions"),
-            help_text=_("You can replace this category permissions with "
-                        "permissions copied from category selected here."),
-            queryset=Category.objects.all_categories(),
-            empty_label=_("Don't copy permissions"),
-            required=False),
-
-        'archive_pruned_in': AdminCategoryChoiceField(
-            label=_("Archive"),
-            help_text=_("Instead of being deleted, pruned threads can be "
-                        "moved to designated category."),
-            queryset=Category.objects.all_categories(),
-            empty_label=_("Don't archive pruned threads"),
-            required=False),
-        })
+    return type(
+        'CategoryFormFinal', (CategoryFormBase, ), {
+            'new_parent': AdminCategoryChoiceField(
+                label=_("Parent category"),
+                queryset=parent_queryset,
+                initial=instance.parent,
+                empty_label=None,
+            ),
+            'copy_permissions': AdminCategoryChoiceField(
+                label=_("Copy permissions"),
+                help_text=_(
+                    "You can replace this category permissions with "
+                    "permissions copied from category selected here."
+                ),
+                queryset=Category.objects.all_categories(),
+                empty_label=_("Don't copy permissions"),
+                required=False,
+            ),
+            'archive_pruned_in': AdminCategoryChoiceField(
+                label=_("Archive"),
+                help_text=_(
+                    "Instead of being deleted, pruned threads can be "
+                    "moved to designated category."
+                ),
+                queryset=Category.objects.all_categories(),
+                empty_label=_("Don't archive pruned threads"),
+                required=False,
+            ),
+        }
+    )
 
 
 
 
 class DeleteCategoryFormBase(forms.ModelForm):
 class DeleteCategoryFormBase(forms.ModelForm):
@@ -169,15 +169,16 @@ class DeleteCategoryFormBase(forms.ModelForm):
 
 
         if data.get('move_threads_to'):
         if data.get('move_threads_to'):
             if data['move_threads_to'].pk == self.instance.pk:
             if data['move_threads_to'].pk == self.instance.pk:
-                message = _("You are trying to move this category threads to "
-                            "itself.")
+                message = _("You are trying to move this category threads to itself.")
                 raise forms.ValidationError(message)
                 raise forms.ValidationError(message)
 
 
             moving_to_child = self.instance.has_child(data['move_threads_to'])
             moving_to_child = self.instance.has_child(data['move_threads_to'])
             if moving_to_child and not data.get('move_children_to'):
             if moving_to_child and not data.get('move_children_to'):
-                message = _("You are trying to move this category threads to a "
-                            "child category that will be deleted together with "
-                            "this category.")
+                message = _(
+                    "You are trying to move this category threads to a "
+                    "child category that will be deleted together with "
+                    "this category."
+                )
                 raise forms.ValidationError(message)
                 raise forms.ValidationError(message)
 
 
         return data
         return data
@@ -191,7 +192,7 @@ def DeleteFormFactory(instance):
             queryset=content_queryset,
             queryset=content_queryset,
             initial=instance.parent,
             initial=instance.parent,
             empty_label=_('Delete with category'),
             empty_label=_('Delete with category'),
-            required=False
+            required=False,
         )
         )
     }
     }
 
 
@@ -205,10 +206,10 @@ def DeleteFormFactory(instance):
             label=_("Move child categories to"),
             label=_("Move child categories to"),
             queryset=children_queryset,
             queryset=children_queryset,
             empty_label=_('Delete with category'),
             empty_label=_('Delete with category'),
-            required=False
+            required=False,
         )
         )
 
 
-    return type('DeleteCategoryFormFinal', (DeleteCategoryFormBase,), fields)
+    return type('DeleteCategoryFormFinal', (DeleteCategoryFormBase, ), fields)
 
 
 
 
 class CategoryRoleForm(forms.ModelForm):
 class CategoryRoleForm(forms.ModelForm):
@@ -227,11 +228,11 @@ def RoleCategoryACLFormFactory(category, category_roles, selected_role):
             required=False,
             required=False,
             queryset=category_roles,
             queryset=category_roles,
             initial=selected_role,
             initial=selected_role,
-            empty_label=_("No access")
+            empty_label=_("No access"),
         )
         )
     }
     }
 
 
-    return type('RoleCategoryACLForm', (forms.Form,), attrs)
+    return type('RoleCategoryACLForm', (forms.Form, ), attrs)
 
 
 
 
 def CategoryRolesACLFormFactory(role, category_roles, selected_role):
 def CategoryRolesACLFormFactory(role, category_roles, selected_role):
@@ -242,8 +243,8 @@ def CategoryRolesACLFormFactory(role, category_roles, selected_role):
             required=False,
             required=False,
             queryset=category_roles,
             queryset=category_roles,
             initial=selected_role,
             initial=selected_role,
-            empty_label=_("No access")
+            empty_label=_("No access"),
         )
         )
     }
     }
 
 
-    return type('CategoryRolesACLForm', (forms.Form,), attrs)
+    return type('CategoryRolesACLForm', (forms.Form, ), attrs)

+ 54 - 13
misago/categories/migrations/0001_initial.py

@@ -22,7 +22,11 @@ class Migration(migrations.Migration):
         migrations.CreateModel(
         migrations.CreateModel(
             name='Category',
             name='Category',
             fields=[
             fields=[
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+                (
+                    'id', models.AutoField(
+                        verbose_name='ID', serialize=False, auto_created=True, primary_key=True
+                    )
+                ),
                 ('special_role', models.CharField(max_length=255, null=True, blank=True)),
                 ('special_role', models.CharField(max_length=255, null=True, blank=True)),
                 ('name', models.CharField(max_length=255)),
                 ('name', models.CharField(max_length=255)),
                 ('slug', models.CharField(max_length=255)),
                 ('slug', models.CharField(max_length=255)),
@@ -42,19 +46,46 @@ class Migration(migrations.Migration):
                 ('rght', models.PositiveIntegerField(editable=False, db_index=True)),
                 ('rght', models.PositiveIntegerField(editable=False, db_index=True)),
                 ('tree_id', models.PositiveIntegerField(editable=False, db_index=True)),
                 ('tree_id', models.PositiveIntegerField(editable=False, db_index=True)),
                 ('level', models.PositiveIntegerField(editable=False, db_index=True)),
                 ('level', models.PositiveIntegerField(editable=False, db_index=True)),
-                ('archive_pruned_in', models.ForeignKey(related_name='pruned_archive', on_delete=django.db.models.deletion.SET_NULL, blank=True, to='misago_categories.Category', null=True)),
-                ('last_poster', models.ForeignKey(related_name='+', on_delete=django.db.models.deletion.SET_NULL, blank=True, to=settings.AUTH_USER_MODEL, null=True)),
-                ('parent', mptt.fields.TreeForeignKey(related_name='children', blank=True, to='misago_categories.Category', null=True)),
+                (
+                    'archive_pruned_in', models.ForeignKey(
+                        related_name='pruned_archive',
+                        on_delete=django.db.models.deletion.SET_NULL,
+                        blank=True,
+                        to='misago_categories.Category',
+                        null=True
+                    )
+                ),
+                (
+                    'last_poster', models.ForeignKey(
+                        related_name='+',
+                        on_delete=django.db.models.deletion.SET_NULL,
+                        blank=True,
+                        to=settings.AUTH_USER_MODEL,
+                        null=True
+                    )
+                ),
+                (
+                    'parent', mptt.fields.TreeForeignKey(
+                        related_name='children',
+                        blank=True,
+                        to='misago_categories.Category',
+                        null=True
+                    )
+                ),
             ],
             ],
             options={
             options={
                 'abstract': False,
                 'abstract': False,
             },
             },
-            bases=(models.Model,),
+            bases=(models.Model, ),
         ),
         ),
         migrations.CreateModel(
         migrations.CreateModel(
             name='CategoryRole',
             name='CategoryRole',
             fields=[
             fields=[
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+                (
+                    'id', models.AutoField(
+                        verbose_name='ID', serialize=False, auto_created=True, primary_key=True
+                    )
+                ),
                 ('name', models.CharField(max_length=255)),
                 ('name', models.CharField(max_length=255)),
                 ('special_role', models.CharField(max_length=255, null=True, blank=True)),
                 ('special_role', models.CharField(max_length=255, null=True, blank=True)),
                 ('permissions', JSONField(default=permissions_default)),
                 ('permissions', JSONField(default=permissions_default)),
@@ -62,18 +93,28 @@ class Migration(migrations.Migration):
             options={
             options={
                 'abstract': False,
                 'abstract': False,
             },
             },
-            bases=(models.Model,),
+            bases=(models.Model, ),
         ),
         ),
         migrations.CreateModel(
         migrations.CreateModel(
             name='RoleCategoryACL',
             name='RoleCategoryACL',
             fields=[
             fields=[
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
-                ('category', models.ForeignKey(related_name='category_role_set', to='misago_categories.Category')),
-                ('category_role', models.ForeignKey(to='misago_categories.CategoryRole', to_field='id')),
+                (
+                    'id', models.AutoField(
+                        verbose_name='ID', serialize=False, auto_created=True, primary_key=True
+                    )
+                ),
+                (
+                    'category', models.ForeignKey(
+                        related_name='category_role_set', to='misago_categories.Category'
+                    )
+                ),
+                (
+                    'category_role',
+                    models.ForeignKey(to='misago_categories.CategoryRole', to_field='id')
+                ),
                 ('role', models.ForeignKey(related_name='categories_acls', to='misago_acl.Role')),
                 ('role', models.ForeignKey(related_name='categories_acls', to='misago_acl.Role')),
             ],
             ],
-            options={
-            },
-            bases=(models.Model,),
+            options={},
+            bases=(models.Model, ),
         ),
         ),
     ]
     ]

+ 2 - 2
misago/categories/migrations/0002_default_categories.py

@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 # -*- coding: utf-8 -*-
 from __future__ import unicode_literals
 from __future__ import unicode_literals
 
 
-from django.db import migrations, models
+from django.db import migrations
 from django.utils.translation import ugettext as _
 from django.utils.translation import ugettext as _
 
 
 from misago.core.utils import slugify
 from misago.core.utils import slugify
@@ -32,7 +32,7 @@ def create_default_categories_tree(apps, schema_editor):
 
 
     category_name = _("First category")
     category_name = _("First category")
 
 
-    category = Category.objects.create(
+    Category.objects.create(
         parent=root,
         parent=root,
         lft=4,
         lft=4,
         rght=5,
         rght=5,

+ 5 - 10
misago/categories/migrations/0003_categories_roles.py

@@ -1,17 +1,14 @@
 # -*- coding: utf-8 -*-
 # -*- coding: utf-8 -*-
 from __future__ import unicode_literals
 from __future__ import unicode_literals
 
 
-from django.db import migrations, models
+from django.db import migrations
 from django.utils.translation import ugettext as _
 from django.utils.translation import ugettext as _
 
 
 
 
 def create_default_categories_roles(apps, schema_editor):
 def create_default_categories_roles(apps, schema_editor):
-    """
-    Crete roles
-    """
     CategoryRole = apps.get_model('misago_categories', 'CategoryRole')
     CategoryRole = apps.get_model('misago_categories', 'CategoryRole')
 
 
-    see_only = CategoryRole.objects.create(
+    CategoryRole.objects.create(
         name=_('See only'),
         name=_('See only'),
         permissions={
         permissions={
             # categories perms
             # categories perms
@@ -41,7 +38,7 @@ def create_default_categories_roles(apps, schema_editor):
         }
         }
     )
     )
 
 
-    reply_only = CategoryRole.objects.create(
+    CategoryRole.objects.create(
         name=_('Reply to threads'),
         name=_('Reply to threads'),
         permissions={
         permissions={
             # categories perms
             # categories perms
@@ -87,7 +84,7 @@ def create_default_categories_roles(apps, schema_editor):
         }
         }
     )
     )
 
 
-    standard_with_polls = CategoryRole.objects.create(
+    CategoryRole.objects.create(
         name=_('Start and reply threads, make polls'),
         name=_('Start and reply threads, make polls'),
         permissions={
         permissions={
             # categories perms
             # categories perms
@@ -162,9 +159,7 @@ def create_default_categories_roles(apps, schema_editor):
     category = Category.objects.get(tree_id=1, level=1)
     category = Category.objects.get(tree_id=1, level=1)
 
 
     RoleCategoryACL.objects.create(
     RoleCategoryACL.objects.create(
-        role=Role.objects.get(name=_('Moderator')),
-        category=category,
-        category_role=moderator
+        role=Role.objects.get(name=_('Moderator')), category=category, category_role=moderator
     )
     )
 
 
     RoleCategoryACL.objects.create(
     RoleCategoryACL.objects.create(

+ 7 - 1
misago/categories/migrations/0004_category_last_thread.py

@@ -16,7 +16,13 @@ class Migration(migrations.Migration):
         migrations.AddField(
         migrations.AddField(
             model_name='category',
             model_name='category',
             name='last_thread',
             name='last_thread',
-            field=models.ForeignKey(related_name='+', on_delete=django.db.models.deletion.SET_NULL, blank=True, to='misago_threads.Thread', null=True),
+            field=models.ForeignKey(
+                related_name='+',
+                on_delete=django.db.models.deletion.SET_NULL,
+                blank=True,
+                to='misago_threads.Thread',
+                null=True
+            ),
             preserve_default=True,
             preserve_default=True,
         ),
         ),
     ]
     ]

+ 4 - 4
misago/categories/models.py

@@ -64,7 +64,7 @@ class Category(MPTTModel):
         'self',
         'self',
         null=True,
         null=True,
         blank=True,
         blank=True,
-        related_name='children'
+        related_name='children',
     )
     )
     special_role = models.CharField(max_length=255, null=True, blank=True)
     special_role = models.CharField(max_length=255, null=True, blank=True)
     name = models.CharField(max_length=255)
     name = models.CharField(max_length=255)
@@ -79,7 +79,7 @@ class Category(MPTTModel):
         related_name='+',
         related_name='+',
         null=True,
         null=True,
         blank=True,
         blank=True,
-        on_delete=models.SET_NULL
+        on_delete=models.SET_NULL,
     )
     )
     last_thread_title = models.CharField(max_length=255, null=True, blank=True)
     last_thread_title = models.CharField(max_length=255, null=True, blank=True)
     last_thread_slug = models.CharField(max_length=255, null=True, blank=True)
     last_thread_slug = models.CharField(max_length=255, null=True, blank=True)
@@ -88,7 +88,7 @@ class Category(MPTTModel):
         related_name='+',
         related_name='+',
         null=True,
         null=True,
         blank=True,
         blank=True,
-        on_delete=models.SET_NULL
+        on_delete=models.SET_NULL,
     )
     )
     last_poster_name = models.CharField(max_length=255, null=True, blank=True)
     last_poster_name = models.CharField(max_length=255, null=True, blank=True)
     last_poster_slug = models.CharField(max_length=255, null=True, blank=True)
     last_poster_slug = models.CharField(max_length=255, null=True, blank=True)
@@ -99,7 +99,7 @@ class Category(MPTTModel):
         related_name='pruned_archive',
         related_name='pruned_archive',
         null=True,
         null=True,
         blank=True,
         blank=True,
-        on_delete=models.SET_NULL
+        on_delete=models.SET_NULL,
     )
     )
     css_class = models.CharField(max_length=255, null=True, blank=True)
     css_class = models.CharField(max_length=255, null=True, blank=True)
 
 

+ 10 - 15
misago/categories/permissions.py

@@ -12,9 +12,6 @@ from misago.users.models import AnonymousUser
 from .models import Category, CategoryRole, RoleCategoryACL
 from .models import Category, CategoryRole, RoleCategoryACL
 
 
 
 
-"""
-Admin Permissions Form
-"""
 class PermissionsForm(forms.Form):
 class PermissionsForm(forms.Form):
     legend = _("Category access")
     legend = _("Category access")
 
 
@@ -29,9 +26,6 @@ def change_permissions_form(role):
         return None
         return None
 
 
 
 
-"""
-ACL Builder
-"""
 def build_acl(acl, roles, key_name):
 def build_acl(acl, roles, key_name):
     new_acl = {
     new_acl = {
         'visible_categories': [],
         'visible_categories': [],
@@ -75,9 +69,12 @@ def build_category_acl(acl, category, categories_roles, key_name):
         'can_browse': 0,
         'can_browse': 0,
     }
     }
 
 
-    algebra.sum_acls(final_acl, roles=category_roles, key=key_name,
+    algebra.sum_acls(
+        final_acl,
+        roles=category_roles,
+        key=key_name,
         can_see=algebra.greater,
         can_see=algebra.greater,
-        can_browse=algebra.greater
+        can_browse=algebra.greater,
     )
     )
 
 
     if final_acl['can_see']:
     if final_acl['can_see']:
@@ -88,9 +85,6 @@ def build_category_acl(acl, category, categories_roles, key_name):
             acl['browseable_categories'].append(category.pk)
             acl['browseable_categories'].append(category.pk)
 
 
 
 
-"""
-ACL's for targets
-"""
 def add_acl_to_category(user, target):
 def add_acl_to_category(user, target):
     target.acl['can_see'] = can_see_category(user, target)
     target.acl['can_see'] = can_see_category(user, target)
     target.acl['can_browse'] = can_browse_category(user, target)
     target.acl['can_browse'] = can_browse_category(user, target)
@@ -106,7 +100,7 @@ def serialize_categories_alcs(serialized_acl):
                 'can_reply_threads': acl.get('can_reply_threads', False),
                 'can_reply_threads': acl.get('can_reply_threads', False),
                 'can_pin_threads': acl.get('can_pin_threads', 0),
                 'can_pin_threads': acl.get('can_pin_threads', 0),
                 'can_hide_threads': acl.get('can_hide_threads', 0),
                 'can_hide_threads': acl.get('can_hide_threads', 0),
-                'can_close_threads': acl.get('can_close_threads', False)
+                'can_close_threads': acl.get('can_close_threads', False),
             })
             })
     serialized_acl['categories'] = categories_acl
     serialized_acl['categories'] = categories_acl
 
 
@@ -118,9 +112,6 @@ def register_with(registry):
     registry.acl_serializer(AnonymousUser, serialize_categories_alcs)
     registry.acl_serializer(AnonymousUser, serialize_categories_alcs)
 
 
 
 
-"""
-ACL tests
-"""
 def allow_see_category(user, target):
 def allow_see_category(user, target):
     try:
     try:
         category_id = target.pk
         category_id = target.pk
@@ -129,6 +120,8 @@ def allow_see_category(user, target):
 
 
     if not category_id in user.acl_cache['visible_categories']:
     if not category_id in user.acl_cache['visible_categories']:
         raise Http404()
         raise Http404()
+
+
 can_see_category = return_boolean(allow_see_category)
 can_see_category = return_boolean(allow_see_category)
 
 
 
 
@@ -137,4 +130,6 @@ def allow_browse_category(user, target):
     if not target_acl['can_browse']:
     if not target_acl['can_browse']:
         message = _('You don\'t have permission to browse "%(category)s" contents.')
         message = _('You don\'t have permission to browse "%(category)s" contents.')
         raise PermissionDenied(message % {'category': target.name})
         raise PermissionDenied(message % {'category': target.name})
+
+
 can_browse_category = return_boolean(allow_browse_category)
 can_browse_category = return_boolean(allow_browse_category)

+ 13 - 11
misago/categories/serializers.py

@@ -13,19 +13,19 @@ __all__ = ['CategorySerializer']
 
 
 def last_activity_detail(f):
 def last_activity_detail(f):
     """util for serializing last activity details"""
     """util for serializing last activity details"""
+
     def decorator(self, obj):
     def decorator(self, obj):
         if not obj.last_thread_id:
         if not obj.last_thread_id:
             return None
             return None
 
 
         acl = self.get_acl(obj)
         acl = self.get_acl(obj)
-        if not all((
-                    acl.get('can_see'),
-                    acl.get('can_browse'),
-                    acl.get('can_see_all_threads')
-                )):
+        tested_acls = (acl.get('can_see'), acl.get('can_browse'), acl.get('can_see_all_threads'), )
+
+        if not all(tested_acls):
             return None
             return None
 
 
         return f(self, obj)
         return f(self, obj)
+
     return decorator
     return decorator
 
 
 
 
@@ -43,7 +43,7 @@ class CategorySerializer(serializers.ModelSerializer, MutableFields):
 
 
     class Meta:
     class Meta:
         model = Category
         model = Category
-        fields = (
+        fields = [
             'id',
             'id',
             'parent',
             'parent',
             'name',
             'name',
@@ -66,7 +66,7 @@ class CategorySerializer(serializers.ModelSerializer, MutableFields):
             'level',
             'level',
             'lft',
             'lft',
             'rght',
             'rght',
-        )
+        ]
 
 
     def get_description(self, obj):
     def get_description(self, obj):
         if obj.description:
         if obj.description:
@@ -103,10 +103,12 @@ class CategorySerializer(serializers.ModelSerializer, MutableFields):
     @last_activity_detail
     @last_activity_detail
     def get_last_poster_url(self, obj):
     def get_last_poster_url(self, obj):
         if obj.last_poster_id:
         if obj.last_poster_id:
-            return reverse('misago:user', kwargs={
-                'slug': obj.last_poster_slug,
-                'pk': obj.last_poster_id,
-            })
+            return reverse(
+                'misago:user', kwargs={
+                    'slug': obj.last_poster_slug,
+                    'pk': obj.last_poster_id,
+                }
+            )
         else:
         else:
             return None
             return None
 
 

+ 1 - 4
misago/categories/signals.py

@@ -9,12 +9,9 @@ delete_category_content = Signal()
 move_category_content = Signal(providing_args=["new_category"])
 move_category_content = Signal(providing_args=["new_category"])
 
 
 
 
-"""
-Signal handlers
-"""
 @receiver(username_changed)
 @receiver(username_changed)
 def update_usernames(sender, **kwargs):
 def update_usernames(sender, **kwargs):
     Category.objects.filter(last_poster=sender).update(
     Category.objects.filter(last_poster=sender).update(
         last_poster_name=sender.username,
         last_poster_name=sender.username,
-        last_poster_slug=sender.slug
+        last_poster_slug=sender.slug,
     )
     )

+ 160 - 130
misago/categories/tests/test_categories_admin_views.py

@@ -14,52 +14,42 @@ class CategoryAdminTestCate(AdminTestCase):
         current_tree = []
         current_tree = []
         for category in queryset:
         for category in queryset:
             current_tree.append((
             current_tree.append((
-                category,
-                category.level,
-                category.lft - root.lft + 1,
+                category, category.level, category.lft - root.lft + 1,
                 category.rght - root.lft + 1,
                 category.rght - root.lft + 1,
             ))
             ))
 
 
         if len(expected_tree) != len(current_tree):
         if len(expected_tree) != len(current_tree):
-            self.fail('nodes tree is %s items long, should be %s' % (
-                len(current_tree), len(expected_tree)))
+            self.fail(
+                'nodes tree is %s items long, should be %s' %
+                (len(current_tree), len(expected_tree))
+            )
 
 
         for i, category in enumerate(expected_tree):
         for i, category in enumerate(expected_tree):
             _category = current_tree[i]
             _category = current_tree[i]
             if category[0] != _category[0]:
             if category[0] != _category[0]:
-                self.fail((
-                    'expected category at index #%s to be %s, '
-                    'found %s instead'
-                ) % (i, category[0], _category[0]))
+                self.fail(('expected category at index #%s to be %s, '
+                           'found %s instead') % (i, category[0], _category[0]))
             if category[1] != _category[1]:
             if category[1] != _category[1]:
-                self.fail((
-                    'expected level at index #%s to be %s, '
-                    'found %s instead'
-                ) % (i, category[1], _category[1]))
+                self.fail(('expected level at index #%s to be %s, '
+                           'found %s instead') % (i, category[1], _category[1]))
             if category[2] != _category[2]:
             if category[2] != _category[2]:
-                self.fail((
-                    'expected lft at index #%s to be %s, '
-                    'found %s instead'
-                ) % (i, category[2], _category[2]))
+                self.fail(('expected lft at index #%s to be %s, '
+                           'found %s instead') % (i, category[2], _category[2]))
             if category[3] != _category[3]:
             if category[3] != _category[3]:
-                self.fail((
-                    'expected lft at index #%s to be %s, '
-                    'found %s instead'
-                ) % (i, category[3], _category[3]))
+                self.fail(('expected lft at index #%s to be %s, '
+                           'found %s instead') % (i, category[3], _category[3]))
 
 
 
 
 class CategoryAdminViewsTests(CategoryAdminTestCate):
 class CategoryAdminViewsTests(CategoryAdminTestCate):
     def test_link_registered(self):
     def test_link_registered(self):
         """admin nav contains categories link"""
         """admin nav contains categories link"""
-        response = self.client.get(
-            reverse('misago:admin:categories:nodes:index'))
+        response = self.client.get(reverse('misago:admin:categories:nodes:index'))
 
 
         self.assertContains(response, reverse('misago:admin:categories:nodes:index'))
         self.assertContains(response, reverse('misago:admin:categories:nodes:index'))
 
 
     def test_list_view(self):
     def test_list_view(self):
         """categories list view returns 200"""
         """categories list view returns 200"""
-        response = self.client.get(
-            reverse('misago:admin:categories:nodes:index'))
+        response = self.client.get(reverse('misago:admin:categories:nodes:index'))
 
 
         self.assertContains(response, 'First category')
         self.assertContains(response, 'First category')
 
 
@@ -68,8 +58,7 @@ class CategoryAdminViewsTests(CategoryAdminTestCate):
         for descendant in root.get_descendants():
         for descendant in root.get_descendants():
             descendant.delete()
             descendant.delete()
 
 
-        response = self.client.get(
-            reverse('misago:admin:categories:nodes:index'))
+        response = self.client.get(reverse('misago:admin:categories:nodes:index'))
 
 
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
         self.assertContains(response, 'No categories')
         self.assertContains(response, 'No categories')
@@ -79,8 +68,7 @@ class CategoryAdminViewsTests(CategoryAdminTestCate):
         root = Category.objects.root_category()
         root = Category.objects.root_category()
         first_category = Category.objects.get(slug='first-category')
         first_category = Category.objects.get(slug='first-category')
 
 
-        response = self.client.get(
-            reverse('misago:admin:categories:nodes:new'))
+        response = self.client.get(reverse('misago:admin:categories:nodes:new'))
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         response = self.client.post(
         response = self.client.post(
@@ -91,11 +79,11 @@ class CategoryAdminViewsTests(CategoryAdminTestCate):
                 'new_parent': root.pk,
                 'new_parent': root.pk,
                 'prune_started_after': 0,
                 'prune_started_after': 0,
                 'prune_replied_after': 0,
                 'prune_replied_after': 0,
-            })
+            },
+        )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
-        response = self.client.get(
-            reverse('misago:admin:categories:nodes:index'))
+        response = self.client.get(reverse('misago:admin:categories:nodes:index'))
         self.assertContains(response, 'Test Category')
         self.assertContains(response, 'Test Category')
 
 
         test_category = Category.objects.get(slug='test-category')
         test_category = Category.objects.get(slug='test-category')
@@ -114,7 +102,8 @@ class CategoryAdminViewsTests(CategoryAdminTestCate):
                 'new_parent': root.pk,
                 'new_parent': root.pk,
                 'prune_started_after': 0,
                 'prune_started_after': 0,
                 'prune_replied_after': 0,
                 'prune_replied_after': 0,
-            })
+            },
+        )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
         test_other_category = Category.objects.get(slug='test-other-category')
         test_other_category = Category.objects.get(slug='test-other-category')
@@ -134,7 +123,8 @@ class CategoryAdminViewsTests(CategoryAdminTestCate):
                 'copy_permissions': test_category.pk,
                 'copy_permissions': test_category.pk,
                 'prune_started_after': 0,
                 'prune_started_after': 0,
                 'prune_replied_after': 0,
                 'prune_replied_after': 0,
-            })
+            },
+        )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
         test_subcategory = Category.objects.get(slug='test-subcategory')
         test_subcategory = Category.objects.get(slug='test-subcategory')
@@ -147,8 +137,7 @@ class CategoryAdminViewsTests(CategoryAdminTestCate):
             (test_other_category, 1, 8, 9),
             (test_other_category, 1, 8, 9),
         ])
         ])
 
 
-        response = self.client.get(
-            reverse('misago:admin:categories:nodes:index'))
+        response = self.client.get(reverse('misago:admin:categories:nodes:index'))
         self.assertContains(response, 'Test Subcategory')
         self.assertContains(response, 'Test Subcategory')
 
 
     def test_edit_view(self):
     def test_edit_view(self):
@@ -159,14 +148,16 @@ class CategoryAdminViewsTests(CategoryAdminTestCate):
 
 
         response = self.client.get(
         response = self.client.get(
             reverse('misago:admin:categories:nodes:edit', kwargs={
             reverse('misago:admin:categories:nodes:edit', kwargs={
-                'pk': private_threads.pk
-            }))
+                'pk': private_threads.pk,
+            })
+        )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
         response = self.client.get(
         response = self.client.get(
             reverse('misago:admin:categories:nodes:edit', kwargs={
             reverse('misago:admin:categories:nodes:edit', kwargs={
-                'pk': root.pk
-            }))
+                'pk': root.pk,
+            })
+        )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
         response = self.client.post(
         response = self.client.post(
@@ -177,21 +168,23 @@ class CategoryAdminViewsTests(CategoryAdminTestCate):
                 'new_parent': root.pk,
                 'new_parent': root.pk,
                 'prune_started_after': 0,
                 'prune_started_after': 0,
                 'prune_replied_after': 0,
                 'prune_replied_after': 0,
-            })
+            },
+        )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
         test_category = Category.objects.get(slug='test-category')
         test_category = Category.objects.get(slug='test-category')
 
 
         response = self.client.get(
         response = self.client.get(
             reverse('misago:admin:categories:nodes:edit', kwargs={
             reverse('misago:admin:categories:nodes:edit', kwargs={
-                'pk': test_category.pk
-            }))
+                'pk': test_category.pk,
+            })
+        )
 
 
         self.assertContains(response, 'Test Category')
         self.assertContains(response, 'Test Category')
 
 
         response = self.client.post(
         response = self.client.post(
             reverse('misago:admin:categories:nodes:edit', kwargs={
             reverse('misago:admin:categories:nodes:edit', kwargs={
-                'pk': test_category.pk
+                'pk': test_category.pk,
             }),
             }),
             data={
             data={
                 'name': 'Test Category Edited',
                 'name': 'Test Category Edited',
@@ -199,7 +192,8 @@ class CategoryAdminViewsTests(CategoryAdminTestCate):
                 'role': 'category',
                 'role': 'category',
                 'prune_started_after': 0,
                 'prune_started_after': 0,
                 'prune_replied_after': 0,
                 'prune_replied_after': 0,
-            })
+            },
+        )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
         self.assertValidTree([
         self.assertValidTree([
@@ -208,13 +202,12 @@ class CategoryAdminViewsTests(CategoryAdminTestCate):
             (test_category, 1, 4, 5),
             (test_category, 1, 4, 5),
         ])
         ])
 
 
-        response = self.client.get(
-            reverse('misago:admin:categories:nodes:index'))
+        response = self.client.get(reverse('misago:admin:categories:nodes:index'))
         self.assertContains(response, 'Test Category Edited')
         self.assertContains(response, 'Test Category Edited')
 
 
         response = self.client.post(
         response = self.client.post(
             reverse('misago:admin:categories:nodes:edit', kwargs={
             reverse('misago:admin:categories:nodes:edit', kwargs={
-                'pk': test_category.pk
+                'pk': test_category.pk,
             }),
             }),
             data={
             data={
                 'name': 'Test Category Edited',
                 'name': 'Test Category Edited',
@@ -222,7 +215,8 @@ class CategoryAdminViewsTests(CategoryAdminTestCate):
                 'role': 'category',
                 'role': 'category',
                 'prune_started_after': 0,
                 'prune_started_after': 0,
                 'prune_replied_after': 0,
                 'prune_replied_after': 0,
-            })
+            },
+        )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
         self.assertValidTree([
         self.assertValidTree([
@@ -231,8 +225,7 @@ class CategoryAdminViewsTests(CategoryAdminTestCate):
             (test_category, 2, 3, 4),
             (test_category, 2, 3, 4),
         ])
         ])
 
 
-        response = self.client.get(
-            reverse('misago:admin:categories:nodes:index'))
+        response = self.client.get(reverse('misago:admin:categories:nodes:index'))
         self.assertContains(response, 'Test Category Edited')
         self.assertContains(response, 'Test Category Edited')
 
 
     def test_move_views(self):
     def test_move_views(self):
@@ -240,26 +233,33 @@ class CategoryAdminViewsTests(CategoryAdminTestCate):
         root = Category.objects.root_category()
         root = Category.objects.root_category()
         first_category = Category.objects.get(slug='first-category')
         first_category = Category.objects.get(slug='first-category')
 
 
-        self.client.post(reverse('misago:admin:categories:nodes:new'), data={
-            'name': 'Category A',
-            'new_parent': root.pk,
-            'prune_started_after': 0,
-            'prune_replied_after': 0,
-        })
+        self.client.post(
+            reverse('misago:admin:categories:nodes:new'),
+            data={
+                'name': 'Category A',
+                'new_parent': root.pk,
+                'prune_started_after': 0,
+                'prune_replied_after': 0,
+            },
+        )
         category_a = Category.objects.get(slug='category-a')
         category_a = Category.objects.get(slug='category-a')
 
 
-        self.client.post(reverse('misago:admin:categories:nodes:new'), data={
-            'name': 'Category B',
-            'new_parent': root.pk,
-            'prune_started_after': 0,
-            'prune_replied_after': 0,
-        })
+        self.client.post(
+            reverse('misago:admin:categories:nodes:new'),
+            data={
+                'name': 'Category B',
+                'new_parent': root.pk,
+                'prune_started_after': 0,
+                'prune_replied_after': 0,
+            },
+        )
         category_b = Category.objects.get(slug='category-b')
         category_b = Category.objects.get(slug='category-b')
 
 
         response = self.client.post(
         response = self.client.post(
             reverse('misago:admin:categories:nodes:up', kwargs={
             reverse('misago:admin:categories:nodes:up', kwargs={
-                'pk': category_b.pk
-            }))
+                'pk': category_b.pk,
+            })
+        )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
         self.assertValidTree([
         self.assertValidTree([
@@ -271,8 +271,9 @@ class CategoryAdminViewsTests(CategoryAdminTestCate):
 
 
         response = self.client.post(
         response = self.client.post(
             reverse('misago:admin:categories:nodes:up', kwargs={
             reverse('misago:admin:categories:nodes:up', kwargs={
-                'pk': category_b.pk
-            }))
+                'pk': category_b.pk,
+            })
+        )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
         self.assertValidTree([
         self.assertValidTree([
@@ -284,8 +285,9 @@ class CategoryAdminViewsTests(CategoryAdminTestCate):
 
 
         response = self.client.post(
         response = self.client.post(
             reverse('misago:admin:categories:nodes:down', kwargs={
             reverse('misago:admin:categories:nodes:down', kwargs={
-                'pk': category_b.pk
-            }))
+                'pk': category_b.pk,
+            })
+        )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
         self.assertValidTree([
         self.assertValidTree([
@@ -297,8 +299,9 @@ class CategoryAdminViewsTests(CategoryAdminTestCate):
 
 
         response = self.client.post(
         response = self.client.post(
             reverse('misago:admin:categories:nodes:down', kwargs={
             reverse('misago:admin:categories:nodes:down', kwargs={
-                'pk': category_b.pk
-            }))
+                'pk': category_b.pk,
+            })
+        )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
         self.assertValidTree([
         self.assertValidTree([
@@ -310,8 +313,9 @@ class CategoryAdminViewsTests(CategoryAdminTestCate):
 
 
         response = self.client.post(
         response = self.client.post(
             reverse('misago:admin:categories:nodes:down', kwargs={
             reverse('misago:admin:categories:nodes:down', kwargs={
-                'pk': category_b.pk
-            }))
+                'pk': category_b.pk,
+            })
+        )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
         self.assertValidTree([
         self.assertValidTree([
@@ -324,10 +328,6 @@ class CategoryAdminViewsTests(CategoryAdminTestCate):
 
 
 class CategoryAdminDeleteViewTests(CategoryAdminTestCate):
 class CategoryAdminDeleteViewTests(CategoryAdminTestCate):
     def setUp(self):
     def setUp(self):
-        super(CategoryAdminDeleteViewTests, self).setUp()
-        self.root = Category.objects.root_category()
-        self.first_category = Category.objects.get(slug='first-category')
-
         """
         """
         Create categories tree for test cases:
         Create categories tree for test cases:
 
 
@@ -341,75 +341,101 @@ class CategoryAdminDeleteViewTests(CategoryAdminTestCate):
         Category E
         Category E
           + Category F
           + Category F
         """
         """
-        self.client.post(reverse('misago:admin:categories:nodes:new'), data={
-            'name': 'Category A',
-            'new_parent': self.root.pk,
-            'prune_started_after': 0,
-            'prune_replied_after': 0,
-        })
-
-        self.client.post(reverse('misago:admin:categories:nodes:new'), data={
-            'name': 'Category E',
-            'new_parent': self.root.pk,
-            'prune_started_after': 0,
-            'prune_replied_after': 0,
-        })
+
+        super(CategoryAdminDeleteViewTests, self).setUp()
+
+        self.root = Category.objects.root_category()
+        self.first_category = Category.objects.get(slug='first-category')
+
+        self.client.post(
+            reverse('misago:admin:categories:nodes:new'),
+            data={
+                'name': 'Category A',
+                'new_parent': self.root.pk,
+                'prune_started_after': 0,
+                'prune_replied_after': 0,
+            },
+        )
+
+        self.client.post(
+            reverse('misago:admin:categories:nodes:new'),
+            data={
+                'name': 'Category E',
+                'new_parent': self.root.pk,
+                'prune_started_after': 0,
+                'prune_replied_after': 0,
+            },
+        )
 
 
         self.category_a = Category.objects.get(slug='category-a')
         self.category_a = Category.objects.get(slug='category-a')
         self.category_e = Category.objects.get(slug='category-e')
         self.category_e = Category.objects.get(slug='category-e')
 
 
-        self.client.post(reverse('misago:admin:categories:nodes:new'), data={
-            'name': 'Category B',
-            'new_parent': self.category_a.pk,
-            'prune_started_after': 0,
-            'prune_replied_after': 0,
-        })
+        self.client.post(
+            reverse('misago:admin:categories:nodes:new'),
+            data={
+                'name': 'Category B',
+                'new_parent': self.category_a.pk,
+                'prune_started_after': 0,
+                'prune_replied_after': 0,
+            },
+        )
         self.category_b = Category.objects.get(slug='category-b')
         self.category_b = Category.objects.get(slug='category-b')
 
 
-        self.client.post(reverse('misago:admin:categories:nodes:new'), data={
-            'name': 'Subcategory C',
-            'new_parent': self.category_b.pk,
-            'prune_started_after': 0,
-            'prune_replied_after': 0,
-        })
+        self.client.post(
+            reverse('misago:admin:categories:nodes:new'),
+            data={
+                'name': 'Subcategory C',
+                'new_parent': self.category_b.pk,
+                'prune_started_after': 0,
+                'prune_replied_after': 0,
+            },
+        )
         self.category_c = Category.objects.get(slug='subcategory-c')
         self.category_c = Category.objects.get(slug='subcategory-c')
 
 
-        self.client.post(reverse('misago:admin:categories:nodes:new'), data={
-            'name': 'Subcategory D',
-            'new_parent': self.category_b.pk,
-            'prune_started_after': 0,
-            'prune_replied_after': 0,
-        })
+        self.client.post(
+            reverse('misago:admin:categories:nodes:new'),
+            data={
+                'name': 'Subcategory D',
+                'new_parent': self.category_b.pk,
+                'prune_started_after': 0,
+                'prune_replied_after': 0,
+            },
+        )
         self.category_d = Category.objects.get(slug='subcategory-d')
         self.category_d = Category.objects.get(slug='subcategory-d')
 
 
-        self.client.post(reverse('misago:admin:categories:nodes:new'), data={
-            'name': 'Category F',
-            'new_parent': self.category_e.pk,
-            'prune_started_after': 0,
-            'prune_replied_after': 0,
-        })
+        self.client.post(
+            reverse('misago:admin:categories:nodes:new'),
+            data={
+                'name': 'Category F',
+                'new_parent': self.category_e.pk,
+                'prune_started_after': 0,
+                'prune_replied_after': 0,
+            },
+        )
         self.category_f = Category.objects.get(slug='category-f')
         self.category_f = Category.objects.get(slug='category-f')
 
 
     def test_delete_category_move_contents(self):
     def test_delete_category_move_contents(self):
         """category was deleted and its contents were moved"""
         """category was deleted and its contents were moved"""
-        for i in range(10):
+        for _ in range(10):
             testutils.post_thread(self.category_b)
             testutils.post_thread(self.category_b)
         self.assertEqual(Thread.objects.count(), 10)
         self.assertEqual(Thread.objects.count(), 10)
 
 
         response = self.client.get(
         response = self.client.get(
             reverse('misago:admin:categories:nodes:delete', kwargs={
             reverse('misago:admin:categories:nodes:delete', kwargs={
-                'pk': self.category_b.pk
-            }))
+                'pk': self.category_b.pk,
+            })
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         response = self.client.post(
         response = self.client.post(
             reverse('misago:admin:categories:nodes:delete', kwargs={
             reverse('misago:admin:categories:nodes:delete', kwargs={
-                'pk': self.category_b.pk
+                'pk': self.category_b.pk,
             }),
             }),
             data={
             data={
                 'move_children_to': self.category_e.pk,
                 'move_children_to': self.category_e.pk,
                 'move_threads_to': self.category_d.pk,
                 'move_threads_to': self.category_d.pk,
-            })
+            },
+        )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
         self.assertEqual(Category.objects.all_categories().count(), 6)
         self.assertEqual(Category.objects.all_categories().count(), 6)
         self.assertEqual(Thread.objects.count(), 10)
         self.assertEqual(Thread.objects.count(), 10)
@@ -426,23 +452,25 @@ class CategoryAdminDeleteViewTests(CategoryAdminTestCate):
 
 
     def test_delete_category_and_contents(self):
     def test_delete_category_and_contents(self):
         """category and its contents were deleted"""
         """category and its contents were deleted"""
-        for i in range(10):
+        for _ in range(10):
             testutils.post_thread(self.category_b)
             testutils.post_thread(self.category_b)
 
 
         response = self.client.get(
         response = self.client.get(
             reverse('misago:admin:categories:nodes:delete', kwargs={
             reverse('misago:admin:categories:nodes:delete', kwargs={
-                'pk': self.category_b.pk
-            }))
+                'pk': self.category_b.pk,
+            })
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         response = self.client.post(
         response = self.client.post(
             reverse('misago:admin:categories:nodes:delete', kwargs={
             reverse('misago:admin:categories:nodes:delete', kwargs={
-                'pk': self.category_b.pk
+                'pk': self.category_b.pk,
             }),
             }),
             data={
             data={
                 'move_children_to': '',
                 'move_children_to': '',
-                'move_threads_to': ''
-            })
+                'move_threads_to': '',
+            }
+        )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
         self.assertEqual(Category.objects.all_categories().count(), 4)
         self.assertEqual(Category.objects.all_categories().count(), 4)
@@ -458,24 +486,26 @@ class CategoryAdminDeleteViewTests(CategoryAdminTestCate):
 
 
     def test_delete_leaf_category(self):
     def test_delete_leaf_category(self):
         """category was deleted and its contents were moved"""
         """category was deleted and its contents were moved"""
-        for i in range(10):
+        for _ in range(10):
             testutils.post_thread(self.category_d)
             testutils.post_thread(self.category_d)
         self.assertEqual(Thread.objects.count(), 10)
         self.assertEqual(Thread.objects.count(), 10)
 
 
         response = self.client.get(
         response = self.client.get(
             reverse('misago:admin:categories:nodes:delete', kwargs={
             reverse('misago:admin:categories:nodes:delete', kwargs={
-                'pk': self.category_d.pk
-            }))
+                'pk': self.category_d.pk,
+            })
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         response = self.client.post(
         response = self.client.post(
             reverse('misago:admin:categories:nodes:delete', kwargs={
             reverse('misago:admin:categories:nodes:delete', kwargs={
-                'pk': self.category_d.pk
+                'pk': self.category_d.pk,
             }),
             }),
             data={
             data={
                 'move_children_to': '',
                 'move_children_to': '',
                 'move_threads_to': '',
                 'move_threads_to': '',
-            })
+            }
+        )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
         self.assertEqual(Category.objects.all_categories().count(), 6)
         self.assertEqual(Category.objects.all_categories().count(), 6)

+ 3 - 9
misago/categories/tests/test_category_model.py

@@ -1,5 +1,3 @@
-from django.utils import timezone
-
 from misago.categories import THREADS_ROOT_NAME
 from misago.categories import THREADS_ROOT_NAME
 from misago.categories.models import Category
 from misago.categories.models import Category
 from misago.core.testutils import MisagoTestCase
 from misago.core.testutils import MisagoTestCase
@@ -54,11 +52,7 @@ class CategoryModelTests(MisagoTestCase):
         self.category = Category.objects.all_categories()[:1][0]
         self.category = Category.objects.all_categories()[:1][0]
 
 
     def create_thread(self):
     def create_thread(self):
-        datetime = timezone.now()
-
-        thread = testutils.post_thread(self.category)
-
-        return thread
+        return testutils.post_thread(self.category)
 
 
     def assertCategoryIsEmpty(self):
     def assertCategoryIsEmpty(self):
         self.assertIsNone(self.category.last_post_on)
         self.assertIsNone(self.category.last_post_on)
@@ -114,7 +108,7 @@ class CategoryModelTests(MisagoTestCase):
 
 
     def test_delete_content(self):
     def test_delete_content(self):
         """delete_content empties category"""
         """delete_content empties category"""
-        for i in range(10):
+        for _ in range(10):
             self.create_thread()
             self.create_thread()
 
 
         self.category.synchronize()
         self.category.synchronize()
@@ -131,7 +125,7 @@ class CategoryModelTests(MisagoTestCase):
 
 
     def test_move_content(self):
     def test_move_content(self):
         """move_content moves category threads and posts to other category"""
         """move_content moves category threads and posts to other category"""
-        for i in range(10):
+        for _ in range(10):
             self.create_thread()
             self.create_thread()
         self.category.synchronize()
         self.category.synchronize()
 
 

+ 130 - 90
misago/categories/tests/test_permissions_admin_views.py

@@ -13,76 +13,84 @@ def fake_data(data_dict):
 class CategoryRoleAdminViewsTests(AdminTestCase):
 class CategoryRoleAdminViewsTests(AdminTestCase):
     def test_link_registered(self):
     def test_link_registered(self):
         """admin nav contains category roles link"""
         """admin nav contains category roles link"""
-        response = self.client.get(
-            reverse('misago:admin:permissions:categories:index'))
+        response = self.client.get(reverse('misago:admin:permissions:categories:index'))
 
 
         self.assertContains(response, reverse('misago:admin:permissions:categories:index'))
         self.assertContains(response, reverse('misago:admin:permissions:categories:index'))
 
 
     def test_list_view(self):
     def test_list_view(self):
         """roles list view returns 200"""
         """roles list view returns 200"""
-        response = self.client.get(
-            reverse('misago:admin:permissions:categories:index'))
+        response = self.client.get(reverse('misago:admin:permissions:categories:index'))
 
 
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
     def test_new_view(self):
     def test_new_view(self):
         """new role view has no showstoppers"""
         """new role view has no showstoppers"""
-        response = self.client.get(
-            reverse('misago:admin:permissions:categories:new'))
+        response = self.client.get(reverse('misago:admin:permissions:categories:new'))
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         response = self.client.post(
         response = self.client.post(
             reverse('misago:admin:permissions:categories:new'),
             reverse('misago:admin:permissions:categories:new'),
-            data=fake_data({'name': 'Test CategoryRole'}))
+            data=fake_data({
+                'name': 'Test CategoryRole',
+            }),
+        )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
         test_role = CategoryRole.objects.get(name='Test CategoryRole')
         test_role = CategoryRole.objects.get(name='Test CategoryRole')
-        response = self.client.get(
-            reverse('misago:admin:permissions:categories:index'))
+        response = self.client.get(reverse('misago:admin:permissions:categories:index'))
         self.assertContains(response, test_role.name)
         self.assertContains(response, test_role.name)
 
 
     def test_edit_view(self):
     def test_edit_view(self):
         """edit role view has no showstoppers"""
         """edit role view has no showstoppers"""
         self.client.post(
         self.client.post(
             reverse('misago:admin:permissions:categories:new'),
             reverse('misago:admin:permissions:categories:new'),
-            data=fake_data({'name': 'Test CategoryRole'}))
+            data=fake_data({
+                'name': 'Test CategoryRole',
+            }),
+        )
 
 
         test_role = CategoryRole.objects.get(name='Test CategoryRole')
         test_role = CategoryRole.objects.get(name='Test CategoryRole')
 
 
         response = self.client.get(
         response = self.client.get(
             reverse('misago:admin:permissions:categories:edit', kwargs={
             reverse('misago:admin:permissions:categories:edit', kwargs={
-                'pk': test_role.pk
-            }))
+                'pk': test_role.pk,
+            })
+        )
         self.assertContains(response, 'Test CategoryRole')
         self.assertContains(response, 'Test CategoryRole')
 
 
         response = self.client.post(
         response = self.client.post(
             reverse('misago:admin:permissions:categories:edit', kwargs={
             reverse('misago:admin:permissions:categories:edit', kwargs={
-                'pk': test_role.pk
+                'pk': test_role.pk,
+            }),
+            data=fake_data({
+                'name': 'Top Lel',
             }),
             }),
-            data=fake_data({'name': 'Top Lel'}))
+        )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
         test_role = CategoryRole.objects.get(name='Top Lel')
         test_role = CategoryRole.objects.get(name='Top Lel')
-        response = self.client.get(
-            reverse('misago:admin:permissions:categories:index'))
+        response = self.client.get(reverse('misago:admin:permissions:categories:index'))
         self.assertContains(response, test_role.name)
         self.assertContains(response, test_role.name)
 
 
     def test_delete_view(self):
     def test_delete_view(self):
         """delete role view has no showstoppers"""
         """delete role view has no showstoppers"""
         self.client.post(
         self.client.post(
             reverse('misago:admin:permissions:categories:new'),
             reverse('misago:admin:permissions:categories:new'),
-            data=fake_data({'name': 'Test CategoryRole'}))
+            data=fake_data({
+                'name': 'Test CategoryRole',
+            }),
+        )
 
 
         test_role = CategoryRole.objects.get(name='Test CategoryRole')
         test_role = CategoryRole.objects.get(name='Test CategoryRole')
         response = self.client.post(
         response = self.client.post(
             reverse('misago:admin:permissions:categories:delete', kwargs={
             reverse('misago:admin:permissions:categories:delete', kwargs={
-                'pk': test_role.pk
-            }))
+                'pk': test_role.pk,
+            })
+        )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
         self.client.get(reverse('misago:admin:permissions:categories:index'))
         self.client.get(reverse('misago:admin:permissions:categories:index'))
-        response = self.client.get(
-            reverse('misago:admin:permissions:categories:index'))
+        response = self.client.get(reverse('misago:admin:permissions:categories:index'))
         self.assertNotContains(response, test_role.name)
         self.assertNotContains(response, test_role.name)
 
 
     def test_change_category_roles_view(self):
     def test_change_category_roles_view(self):
@@ -90,7 +98,6 @@ class CategoryRoleAdminViewsTests(AdminTestCase):
         root = Category.objects.root_category()
         root = Category.objects.root_category()
         for descendant in root.get_descendants():
         for descendant in root.get_descendants():
             descendant.delete()
             descendant.delete()
-
         """
         """
         Create categories tree for test cases:
         Create categories tree for test cases:
 
 
@@ -100,46 +107,59 @@ class CategoryRoleAdminViewsTests(AdminTestCase):
           + Category D
           + Category D
         """
         """
         root = Category.objects.root_category()
         root = Category.objects.root_category()
-        self.client.post(reverse('misago:admin:categories:nodes:new'), data={
-            'name': 'Category A',
-            'new_parent': root.pk,
-            'prune_started_after': 0,
-            'prune_replied_after': 0,
-        })
+        self.client.post(
+            reverse('misago:admin:categories:nodes:new'),
+            data={
+                'name': 'Category A',
+                'new_parent': root.pk,
+                'prune_started_after': 0,
+                'prune_replied_after': 0,
+            },
+        )
         test_category = Category.objects.get(slug='category-a')
         test_category = Category.objects.get(slug='category-a')
 
 
         self.assertEqual(Category.objects.count(), 3)
         self.assertEqual(Category.objects.count(), 3)
-
         """
         """
         Create test roles
         Create test roles
         """
         """
         self.client.post(
         self.client.post(
             reverse('misago:admin:permissions:users:new'),
             reverse('misago:admin:permissions:users:new'),
-            data=fake_post_data(Role(), {'name': 'Test Role A'}))
+            data=fake_post_data(Role(), {'name': 'Test Role A'})
+        )
         self.client.post(
         self.client.post(
             reverse('misago:admin:permissions:users:new'),
             reverse('misago:admin:permissions:users:new'),
-            data=fake_post_data(Role(), {'name': 'Test Role B'}))
+            data=fake_post_data(Role(), {'name': 'Test Role B'})
+        )
 
 
         test_role_a = Role.objects.get(name='Test Role A')
         test_role_a = Role.objects.get(name='Test Role A')
         test_role_b = Role.objects.get(name='Test Role B')
         test_role_b = Role.objects.get(name='Test Role B')
 
 
         self.client.post(
         self.client.post(
             reverse('misago:admin:permissions:categories:new'),
             reverse('misago:admin:permissions:categories:new'),
-            data=fake_data({'name': 'Test Comments'}))
+            data=fake_data({
+                'name': 'Test Comments',
+            }),
+        )
         self.client.post(
         self.client.post(
             reverse('misago:admin:permissions:categories:new'),
             reverse('misago:admin:permissions:categories:new'),
-            data=fake_data({'name': 'Test Full'}))
+            data=fake_data({
+                'name': 'Test Full',
+            }),
+        )
 
 
         role_comments = CategoryRole.objects.get(name='Test Comments')
         role_comments = CategoryRole.objects.get(name='Test Comments')
         role_full = CategoryRole.objects.get(name='Test Full')
         role_full = CategoryRole.objects.get(name='Test Full')
-
         """
         """
         Test view itself
         Test view itself
         """
         """
         # See if form page is rendered
         # See if form page is rendered
         response = self.client.get(
         response = self.client.get(
-            reverse('misago:admin:categories:nodes:permissions',
-                    kwargs={'pk': test_category.pk}))
+            reverse(
+                'misago:admin:categories:nodes:permissions', kwargs={
+                    'pk': test_category.pk,
+                }
+            )
+        )
         self.assertContains(response, test_category.name)
         self.assertContains(response, test_category.name)
         self.assertContains(response, test_role_a.name)
         self.assertContains(response, test_role_a.name)
         self.assertContains(response, test_role_b.name)
         self.assertContains(response, test_role_b.name)
@@ -148,28 +168,31 @@ class CategoryRoleAdminViewsTests(AdminTestCase):
 
 
         # Assign roles to categories
         # Assign roles to categories
         response = self.client.post(
         response = self.client.post(
-            reverse('misago:admin:categories:nodes:permissions',
-                    kwargs={'pk': test_category.pk}),
+            reverse(
+                'misago:admin:categories:nodes:permissions', kwargs={
+                    'pk': test_category.pk,
+                }
+            ),
             data={
             data={
                 ('%s-category_role' % test_role_a.pk): role_full.pk,
                 ('%s-category_role' % test_role_a.pk): role_full.pk,
                 ('%s-category_role' % test_role_b.pk): role_comments.pk,
                 ('%s-category_role' % test_role_b.pk): role_comments.pk,
-            })
+            },
+        )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
         # Check that roles were assigned
         # Check that roles were assigned
         category_role_set = test_category.category_role_set
         category_role_set = test_category.category_role_set
+        self.assertEqual(category_role_set.get(role=test_role_a).category_role_id, role_full.pk)
         self.assertEqual(
         self.assertEqual(
-            category_role_set.get(role=test_role_a).category_role_id,
-            role_full.pk)
-        self.assertEqual(
-            category_role_set.get(role=test_role_b).category_role_id,
-            role_comments.pk)
+            category_role_set.get(role=test_role_b).category_role_id, role_comments.pk
+        )
 
 
     def test_change_role_categories_permissions_view(self):
     def test_change_role_categories_permissions_view(self):
         """change role categories perms view works"""
         """change role categories perms view works"""
         self.client.post(
         self.client.post(
             reverse('misago:admin:permissions:users:new'),
             reverse('misago:admin:permissions:users:new'),
-            data=fake_post_data(Role(), {'name': 'Test CategoryRole'}))
+            data=fake_post_data(Role(), {'name': 'Test CategoryRole'})
+        )
 
 
         test_role = Role.objects.get(name='Test CategoryRole')
         test_role = Role.objects.get(name='Test CategoryRole')
 
 
@@ -180,10 +203,10 @@ class CategoryRoleAdminViewsTests(AdminTestCase):
         self.assertEqual(Category.objects.count(), 2)
         self.assertEqual(Category.objects.count(), 2)
         response = self.client.get(
         response = self.client.get(
             reverse('misago:admin:permissions:users:categories', kwargs={
             reverse('misago:admin:permissions:users:categories', kwargs={
-                'pk': test_role.pk
-            }))
+                'pk': test_role.pk,
+            })
+        )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
-
         """
         """
         Create categories tree for test cases:
         Create categories tree for test cases:
 
 
@@ -193,36 +216,48 @@ class CategoryRoleAdminViewsTests(AdminTestCase):
           + Category D
           + Category D
         """
         """
         root = Category.objects.root_category()
         root = Category.objects.root_category()
-        self.client.post(reverse('misago:admin:categories:nodes:new'), data={
-            'name': 'Category A',
-            'new_parent': root.pk,
-            'prune_started_after': 0,
-            'prune_replied_after': 0,
-        })
-        self.client.post(reverse('misago:admin:categories:nodes:new'), data={
-            'name': 'Category C',
-            'new_parent': root.pk,
-            'prune_started_after': 0,
-            'prune_replied_after': 0,
-        })
+        self.client.post(
+            reverse('misago:admin:categories:nodes:new'),
+            data={
+                'name': 'Category A',
+                'new_parent': root.pk,
+                'prune_started_after': 0,
+                'prune_replied_after': 0,
+            },
+        )
+        self.client.post(
+            reverse('misago:admin:categories:nodes:new'),
+            data={
+                'name': 'Category C',
+                'new_parent': root.pk,
+                'prune_started_after': 0,
+                'prune_replied_after': 0,
+            },
+        )
 
 
         category_a = Category.objects.get(slug='category-a')
         category_a = Category.objects.get(slug='category-a')
         category_c = Category.objects.get(slug='category-c')
         category_c = Category.objects.get(slug='category-c')
 
 
-        self.client.post(reverse('misago:admin:categories:nodes:new'), data={
-            'name': 'Category B',
-            'new_parent': category_a.pk,
-            'prune_started_after': 0,
-            'prune_replied_after': 0,
-        })
+        self.client.post(
+            reverse('misago:admin:categories:nodes:new'),
+            data={
+                'name': 'Category B',
+                'new_parent': category_a.pk,
+                'prune_started_after': 0,
+                'prune_replied_after': 0,
+            },
+        )
         category_b = Category.objects.get(slug='category-b')
         category_b = Category.objects.get(slug='category-b')
 
 
-        self.client.post(reverse('misago:admin:categories:nodes:new'), data={
-            'name': 'Category D',
-            'new_parent': category_c.pk,
-            'prune_started_after': 0,
-            'prune_replied_after': 0,
-        })
+        self.client.post(
+            reverse('misago:admin:categories:nodes:new'),
+            data={
+                'name': 'Category D',
+                'new_parent': category_c.pk,
+                'prune_started_after': 0,
+                'prune_replied_after': 0,
+            },
+        )
         category_d = Category.objects.get(slug='category-d')
         category_d = Category.objects.get(slug='category-d')
 
 
         self.assertEqual(Category.objects.count(), 6)
         self.assertEqual(Category.objects.count(), 6)
@@ -230,8 +265,9 @@ class CategoryRoleAdminViewsTests(AdminTestCase):
         # See if form page is rendered
         # See if form page is rendered
         response = self.client.get(
         response = self.client.get(
             reverse('misago:admin:permissions:users:categories', kwargs={
             reverse('misago:admin:permissions:users:categories', kwargs={
-                'pk': test_role.pk
-            }))
+                'pk': test_role.pk,
+            })
+        )
         self.assertContains(response, category_a.name)
         self.assertContains(response, category_a.name)
         self.assertContains(response, category_b.name)
         self.assertContains(response, category_b.name)
         self.assertContains(response, category_c.name)
         self.assertContains(response, category_c.name)
@@ -240,46 +276,50 @@ class CategoryRoleAdminViewsTests(AdminTestCase):
         # Set test roles
         # Set test roles
         self.client.post(
         self.client.post(
             reverse('misago:admin:permissions:categories:new'),
             reverse('misago:admin:permissions:categories:new'),
-            data=fake_data({'name': 'Test Comments'}))
+            data=fake_data({
+                'name': 'Test Comments',
+            }),
+        )
         role_comments = CategoryRole.objects.get(name='Test Comments')
         role_comments = CategoryRole.objects.get(name='Test Comments')
 
 
         self.client.post(
         self.client.post(
             reverse('misago:admin:permissions:categories:new'),
             reverse('misago:admin:permissions:categories:new'),
-            data=fake_data({'name': 'Test Full'}))
+            data=fake_data({
+                'name': 'Test Full',
+            }),
+        )
         role_full = CategoryRole.objects.get(name='Test Full')
         role_full = CategoryRole.objects.get(name='Test Full')
 
 
         # See if form contains those roles
         # See if form contains those roles
         response = self.client.get(
         response = self.client.get(
             reverse('misago:admin:permissions:users:categories', kwargs={
             reverse('misago:admin:permissions:users:categories', kwargs={
-                'pk': test_role.pk
-            }))
+                'pk': test_role.pk,
+            })
+        )
         self.assertContains(response, role_comments.name)
         self.assertContains(response, role_comments.name)
         self.assertContains(response, role_full.name)
         self.assertContains(response, role_full.name)
 
 
         # Assign roles to categories
         # Assign roles to categories
         response = self.client.post(
         response = self.client.post(
             reverse('misago:admin:permissions:users:categories', kwargs={
             reverse('misago:admin:permissions:users:categories', kwargs={
-                'pk': test_role.pk
+                'pk': test_role.pk,
             }),
             }),
             data={
             data={
                 ('%s-role' % category_a.pk): role_comments.pk,
                 ('%s-role' % category_a.pk): role_comments.pk,
                 ('%s-role' % category_b.pk): role_comments.pk,
                 ('%s-role' % category_b.pk): role_comments.pk,
                 ('%s-role' % category_c.pk): role_full.pk,
                 ('%s-role' % category_c.pk): role_full.pk,
                 ('%s-role' % category_d.pk): role_full.pk,
                 ('%s-role' % category_d.pk): role_full.pk,
-            })
+            },
+        )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
         # Check that roles were assigned
         # Check that roles were assigned
         categories_acls = test_role.categories_acls
         categories_acls = test_role.categories_acls
         self.assertEqual(
         self.assertEqual(
-            categories_acls.get(category=category_a).category_role_id,
-            role_comments.pk)
-        self.assertEqual(
-            categories_acls.get(category=category_b).category_role_id,
-            role_comments.pk)
-        self.assertEqual(
-            categories_acls.get(category=category_c).category_role_id,
-            role_full.pk)
+            categories_acls.get(category=category_a).category_role_id, role_comments.pk
+        )
         self.assertEqual(
         self.assertEqual(
-            categories_acls.get(category=category_d).category_role_id,
-            role_full.pk)
+            categories_acls.get(category=category_b).category_role_id, role_comments.pk
+        )
+        self.assertEqual(categories_acls.get(category=category_c).category_role_id, role_full.pk)
+        self.assertEqual(categories_acls.get(category=category_d).category_role_id, role_full.pk)

+ 8 - 9
misago/categories/tests/test_prunecategories.py

@@ -4,7 +4,6 @@ from django.core.management import call_command
 from django.test import TestCase
 from django.test import TestCase
 from django.utils import timezone
 from django.utils import timezone
 from django.utils.six import StringIO
 from django.utils.six import StringIO
-from django.utils.six.moves import range
 
 
 from misago.categories.management.commands import prunecategories
 from misago.categories.management.commands import prunecategories
 from misago.categories.models import Category
 from misago.categories.models import Category
@@ -22,12 +21,12 @@ class PruneCategoriesTests(TestCase):
         # post old threads with recent replies
         # post old threads with recent replies
         started_on = timezone.now() - timedelta(days=30)
         started_on = timezone.now() - timedelta(days=30)
         posted_on = timezone.now()
         posted_on = timezone.now()
-        for t in range(10):
+        for _ in range(10):
             thread = testutils.post_thread(category, started_on=started_on)
             thread = testutils.post_thread(category, started_on=started_on)
             testutils.reply_thread(thread, posted_on=posted_on)
             testutils.reply_thread(thread, posted_on=posted_on)
 
 
         # post recent threads that will be preserved
         # post recent threads that will be preserved
-        threads = [testutils.post_thread(category) for t in range(10)]
+        threads = [testutils.post_thread(category) for _ in range(10)]
 
 
         category.synchronize()
         category.synchronize()
         self.assertEqual(category.threads, 20)
         self.assertEqual(category.threads, 20)
@@ -58,12 +57,12 @@ class PruneCategoriesTests(TestCase):
 
 
         # post old threads with recent replies
         # post old threads with recent replies
         started_on = timezone.now() - timedelta(days=30)
         started_on = timezone.now() - timedelta(days=30)
-        for t in range(10):
+        for _ in range(10):
             thread = testutils.post_thread(category, started_on=started_on)
             thread = testutils.post_thread(category, started_on=started_on)
             testutils.reply_thread(thread)
             testutils.reply_thread(thread)
 
 
         # post recent threads that will be preserved
         # post recent threads that will be preserved
-        threads = [testutils.post_thread(category) for t in range(10)]
+        threads = [testutils.post_thread(category) for _ in range(10)]
 
 
         category.synchronize()
         category.synchronize()
         self.assertEqual(category.threads, 20)
         self.assertEqual(category.threads, 20)
@@ -104,12 +103,12 @@ class PruneCategoriesTests(TestCase):
         # post old threads with recent replies
         # post old threads with recent replies
         started_on = timezone.now() - timedelta(days=30)
         started_on = timezone.now() - timedelta(days=30)
         posted_on = timezone.now()
         posted_on = timezone.now()
-        for t in range(10):
+        for _ in range(10):
             thread = testutils.post_thread(category, started_on=started_on)
             thread = testutils.post_thread(category, started_on=started_on)
             testutils.reply_thread(thread, posted_on=posted_on)
             testutils.reply_thread(thread, posted_on=posted_on)
 
 
         # post recent threads that will be preserved
         # post recent threads that will be preserved
-        threads = [testutils.post_thread(category) for t in range(10)]
+        threads = [testutils.post_thread(category) for _ in range(10)]
 
 
         category.synchronize()
         category.synchronize()
         self.assertEqual(category.threads, 20)
         self.assertEqual(category.threads, 20)
@@ -153,12 +152,12 @@ class PruneCategoriesTests(TestCase):
 
 
         # post old threads with recent replies
         # post old threads with recent replies
         started_on = timezone.now() - timedelta(days=30)
         started_on = timezone.now() - timedelta(days=30)
-        for t in range(10):
+        for _ in range(10):
             thread = testutils.post_thread(category, started_on=started_on)
             thread = testutils.post_thread(category, started_on=started_on)
             testutils.reply_thread(thread)
             testutils.reply_thread(thread)
 
 
         # post recent threads that will be preserved
         # post recent threads that will be preserved
-        threads = [testutils.post_thread(category) for t in range(10)]
+        threads = [testutils.post_thread(category) for _ in range(10)]
 
 
         category.synchronize()
         category.synchronize()
         self.assertEqual(category.threads, 20)
         self.assertEqual(category.threads, 20)

+ 2 - 3
misago/categories/tests/test_synchronizecategories.py

@@ -1,7 +1,6 @@
 from django.core.management import call_command
 from django.core.management import call_command
 from django.test import TestCase
 from django.test import TestCase
 from django.utils.six import StringIO
 from django.utils.six import StringIO
-from django.utils.six.moves import range
 
 
 from misago.categories.management.commands import synchronizecategories
 from misago.categories.management.commands import synchronizecategories
 from misago.categories.models import Category
 from misago.categories.models import Category
@@ -13,9 +12,9 @@ class SynchronizeCategoriesTests(TestCase):
         """command synchronizes categories"""
         """command synchronizes categories"""
         category = Category.objects.all_categories()[:1][0]
         category = Category.objects.all_categories()[:1][0]
 
 
-        threads = [testutils.post_thread(category) for t in range(10)]
+        threads = [testutils.post_thread(category) for _ in range(10)]
         for thread in threads:
         for thread in threads:
-            [testutils.reply_thread(thread) for r in range(5)]
+            [testutils.reply_thread(thread) for _ in range(5)]
 
 
         category.threads = 0
         category.threads = 0
         category.posts = 0
         category.posts = 0

+ 44 - 25
misago/categories/tests/test_utils.py

@@ -7,12 +7,6 @@ from misago.users.testutils import AuthenticatedUserTestCase
 
 
 class CategoriesUtilsTests(AuthenticatedUserTestCase):
 class CategoriesUtilsTests(AuthenticatedUserTestCase):
     def setUp(self):
     def setUp(self):
-        super(CategoriesUtilsTests, self).setUp()
-        threadstore.clear()
-
-        self.root = Category.objects.root_category()
-        self.first_category = Category.objects.get(slug='first-category')
-
         """
         """
         Create categories tree for test cases:
         Create categories tree for test cases:
 
 
@@ -26,46 +20,74 @@ class CategoriesUtilsTests(AuthenticatedUserTestCase):
         Category E
         Category E
           + Subcategory F
           + Subcategory F
         """
         """
+
+        super(CategoriesUtilsTests, self).setUp()
+        threadstore.clear()
+
+        self.root = Category.objects.root_category()
+        self.first_category = Category.objects.get(slug='first-category')
+
         Category(
         Category(
             name='Category A',
             name='Category A',
             slug='category-a',
             slug='category-a',
-        ).insert_at(self.root, position='last-child', save=True)
+        ).insert_at(
+            self.root,
+            position='last-child',
+            save=True,
+        )
         Category(
         Category(
             name='Category E',
             name='Category E',
             slug='category-e',
             slug='category-e',
-        ).insert_at(self.root, position='last-child', save=True)
+        ).insert_at(
+            self.root,
+            position='last-child',
+            save=True,
+        )
 
 
         self.category_a = Category.objects.get(slug='category-a')
         self.category_a = Category.objects.get(slug='category-a')
 
 
         Category(
         Category(
             name='Category B',
             name='Category B',
             slug='category-b',
             slug='category-b',
-        ).insert_at(self.category_a, position='last-child', save=True)
+        ).insert_at(
+            self.category_a,
+            position='last-child',
+            save=True,
+        )
 
 
         self.category_b = Category.objects.get(slug='category-b')
         self.category_b = Category.objects.get(slug='category-b')
 
 
         Category(
         Category(
             name='Subcategory C',
             name='Subcategory C',
             slug='subcategory-c',
             slug='subcategory-c',
-        ).insert_at(self.category_b, position='last-child', save=True)
+        ).insert_at(
+            self.category_b,
+            position='last-child',
+            save=True,
+        )
         Category(
         Category(
             name='Subcategory D',
             name='Subcategory D',
             slug='subcategory-d',
             slug='subcategory-d',
-        ).insert_at(self.category_b, position='last-child', save=True)
+        ).insert_at(
+            self.category_b,
+            position='last-child',
+            save=True,
+        )
 
 
         self.category_e = Category.objects.get(slug='category-e')
         self.category_e = Category.objects.get(slug='category-e')
         Category(
         Category(
             name='Subcategory F',
             name='Subcategory F',
             slug='subcategory-f',
             slug='subcategory-f',
-        ).insert_at(self.category_e, position='last-child', save=True)
+        ).insert_at(
+            self.category_e,
+            position='last-child',
+            save=True,
+        )
 
 
         categories_acl = {'categories': {}, 'visible_categories': []}
         categories_acl = {'categories': {}, 'visible_categories': []}
         for category in Category.objects.all_categories():
         for category in Category.objects.all_categories():
             categories_acl['visible_categories'].append(category.pk)
             categories_acl['visible_categories'].append(category.pk)
-            categories_acl['categories'][category.pk] = {
-                'can_see': 1,
-                'can_browse': 1
-            }
+            categories_acl['categories'][category.pk] = {'can_see': 1, 'can_browse': 1}
         override_acl(self.user, categories_acl)
         override_acl(self.user, categories_acl)
 
 
     def test_root_categories_tree_no_parent(self):
     def test_root_categories_tree_no_parent(self):
@@ -73,24 +95,21 @@ class CategoriesUtilsTests(AuthenticatedUserTestCase):
         categories_tree = get_categories_tree(self.user)
         categories_tree = get_categories_tree(self.user)
         self.assertEqual(len(categories_tree), 3)
         self.assertEqual(len(categories_tree), 3)
 
 
-        self.assertEqual(
-            categories_tree[0], Category.objects.get(slug='first-category'))
-        self.assertEqual(
-            categories_tree[1], Category.objects.get(slug='category-a'))
-        self.assertEqual(
-            categories_tree[2], Category.objects.get(slug='category-e'))
+        self.assertEqual(categories_tree[0], Category.objects.get(slug='first-category'))
+        self.assertEqual(categories_tree[1], Category.objects.get(slug='category-a'))
+        self.assertEqual(categories_tree[2], Category.objects.get(slug='category-e'))
 
 
     def test_root_categories_tree_with_parent(self):
     def test_root_categories_tree_with_parent(self):
         """get_categories_tree returns all children of given node"""
         """get_categories_tree returns all children of given node"""
         categories_tree = get_categories_tree(self.user, self.category_a)
         categories_tree = get_categories_tree(self.user, self.category_a)
         self.assertEqual(len(categories_tree), 1)
         self.assertEqual(len(categories_tree), 1)
-        self.assertEqual(
-            categories_tree[0], Category.objects.get(slug='category-b'))
+        self.assertEqual(categories_tree[0], Category.objects.get(slug='category-b'))
 
 
     def test_root_categories_tree_with_leaf(self):
     def test_root_categories_tree_with_leaf(self):
         """get_categories_tree returns all children of given node"""
         """get_categories_tree returns all children of given node"""
         categories_tree = get_categories_tree(
         categories_tree = get_categories_tree(
-            self.user, Category.objects.get(slug='subcategory-f'))
+            self.user, Category.objects.get(slug='subcategory-f')
+        )
         self.assertEqual(len(categories_tree), 0)
         self.assertEqual(len(categories_tree), 0)
 
 
     def test_get_category_path(self):
     def test_get_category_path(self):

+ 0 - 2
misago/categories/urls/__init__.py

@@ -5,13 +5,11 @@ from misago.core.views import home_redirect
 
 
 from misago.categories.views.categorieslist import categories
 from misago.categories.views.categorieslist import categories
 
 
-
 if settings.MISAGO_THREADS_ON_INDEX:
 if settings.MISAGO_THREADS_ON_INDEX:
     URL_PATH = r'^categories/$'
     URL_PATH = r'^categories/$'
 else:
 else:
     URL_PATH = r'^$'
     URL_PATH = r'^$'
 
 
-
 urlpatterns = [
 urlpatterns = [
     url(URL_PATH, categories, name='categories'),
     url(URL_PATH, categories, name='categories'),
 
 

+ 1 - 6
misago/categories/utils.py

@@ -1,5 +1,4 @@
 from misago.acl import add_acl
 from misago.acl import add_acl
-from misago.core import threadstore
 from misago.readtracker import categoriestracker
 from misago.readtracker import categoriestracker
 
 
 from .models import Category
 from .models import Category
@@ -14,9 +13,7 @@ def get_categories_tree(user, parent=None):
     else:
     else:
         queryset = Category.objects.all_categories()
         queryset = Category.objects.all_categories()
 
 
-    queryset_with_acl = queryset.filter(
-        id__in=user.acl_cache['visible_categories']
-    )
+    queryset_with_acl = queryset.filter(id__in=user.acl_cache['visible_categories'])
 
 
     visible_categories = list(queryset_with_acl)
     visible_categories = list(queryset_with_acl)
 
 
@@ -74,8 +71,6 @@ def get_category_path(category):
     if category.special_role:
     if category.special_role:
         return [category]
         return [category]
 
 
-    categories_dict = Category.objects.get_cached_categories_dict()
-
     category_path = []
     category_path = []
     while category and category.level > 0:
     while category and category.level > 0:
         category_path.append(category)
         category_path.append(category)

+ 11 - 9
misago/categories/views/categoriesadmin.py

@@ -39,7 +39,7 @@ class CategoriesList(CategoryAdmin, generic.ListView):
 
 
         children_lists = {}
         children_lists = {}
 
 
-        for i, item in enumerate(context['items']):
+        for item in context['items']:
             item.level_range = range(item.level - 1)
             item.level_range = range(item.level - 1)
             item.first = False
             item.first = False
             item.last = False
             item.last = False
@@ -59,13 +59,13 @@ class CategoryFormMixin(object):
     def handle_form(self, form, request, target):
     def handle_form(self, form, request, target):
         if form.instance.pk:
         if form.instance.pk:
             if form.instance.parent_id != form.cleaned_data['new_parent'].pk:
             if form.instance.parent_id != form.cleaned_data['new_parent'].pk:
-                form.instance.move_to(
-                    form.cleaned_data['new_parent'], position='last-child')
+                form.instance.move_to(form.cleaned_data['new_parent'], position='last-child')
             form.instance.save()
             form.instance.save()
             if form.instance.parent_id != form.cleaned_data['new_parent'].pk:
             if form.instance.parent_id != form.cleaned_data['new_parent'].pk:
                 Category.objects.clear_cache()
                 Category.objects.clear_cache()
         else:
         else:
-            form.instance.insert_at(form.cleaned_data['new_parent'],
+            form.instance.insert_at(
+                form.cleaned_data['new_parent'],
                 position='last-child',
                 position='last-child',
                 save=True,
                 save=True,
             )
             )
@@ -77,11 +77,13 @@ class CategoryFormMixin(object):
 
 
             copied_acls = []
             copied_acls = []
             for acl in copy_from.category_role_set.all():
             for acl in copy_from.category_role_set.all():
-                copied_acls.append(RoleCategoryACL(
-                    role_id=acl.role_id,
-                    category=form.instance,
-                    category_role_id=acl.category_role_id,
-                ))
+                copied_acls.append(
+                    RoleCategoryACL(
+                        role_id=acl.role_id,
+                        category=form.instance,
+                        category_role_id=acl.category_role_id,
+                    )
+                )
 
 
             if copied_acls:
             if copied_acls:
                 RoleCategoryACL.objects.bulk_create(copied_acls)
                 RoleCategoryACL.objects.bulk_create(copied_acls)

+ 1 - 1
misago/categories/views/categorieslist.py

@@ -10,7 +10,7 @@ def categories(request):
 
 
     request.frontend_context.update({
     request.frontend_context.update({
         'CATEGORIES': CategorySerializer(categories_tree, many=True).data,
         'CATEGORIES': CategorySerializer(categories_tree, many=True).data,
-        'CATEGORIES_API': reverse('misago:api:category-list')
+        'CATEGORIES_API': reverse('misago:api:category-list'),
     })
     })
 
 
     return render(request, 'misago/categories/list.html', {
     return render(request, 'misago/categories/list.html', {

+ 23 - 25
misago/categories/views/permsadmin.py

@@ -22,7 +22,7 @@ class CategoryRoleAdmin(generic.AdminBaseMixin):
 
 
 
 
 class CategoryRolesList(CategoryRoleAdmin, generic.ListView):
 class CategoryRolesList(CategoryRoleAdmin, generic.ListView):
-    ordering = (('name', None),)
+    ordering = (('name', None), )
 
 
 
 
 class RoleFormMixin(object):
 class RoleFormMixin(object):
@@ -48,8 +48,7 @@ class RoleFormMixin(object):
                 form.instance.permissions = new_permissions
                 form.instance.permissions = new_permissions
                 form.instance.save()
                 form.instance.save()
 
 
-                messages.success(
-                    request, self.message_submit % {'name': target.name})
+                messages.success(request, self.message_submit % {'name': target.name})
 
 
                 if 'stay' in request.POST:
                 if 'stay' in request.POST:
                     return redirect(request.path)
                     return redirect(request.path)
@@ -64,7 +63,8 @@ class RoleFormMixin(object):
                 'form': form,
                 'form': form,
                 'target': target,
                 'target': target,
                 'perms_forms': perms_forms,
                 'perms_forms': perms_forms,
-            })
+            },
+        )
 
 
 
 
 class NewCategoryRole(RoleFormMixin, CategoryRoleAdmin, generic.ModelFormView):
 class NewCategoryRole(RoleFormMixin, CategoryRoleAdmin, generic.ModelFormView):
@@ -78,8 +78,7 @@ class EditCategoryRole(RoleFormMixin, CategoryRoleAdmin, generic.ModelFormView):
 class DeleteCategoryRole(CategoryRoleAdmin, generic.ButtonView):
 class DeleteCategoryRole(CategoryRoleAdmin, generic.ButtonView):
     def check_permissions(self, request, target):
     def check_permissions(self, request, target):
         if target.special_role:
         if target.special_role:
-            message = _('Role "%(name)s" is special '
-                        'role and can\'t be deleted.')
+            message = _('Role "%(name)s" is special role and can\'t be deleted.')
             return message % {'name': target.name}
             return message % {'name': target.name}
 
 
     def button_action(self, request, target):
     def button_action(self, request, target):
@@ -88,11 +87,8 @@ class DeleteCategoryRole(CategoryRoleAdmin, generic.ButtonView):
         messages.success(request, message % {'name': target.name})
         messages.success(request, message % {'name': target.name})
 
 
 
 
-"""
-Create category roles view for assinging roles to category,
-add link to it in categories list
-"""
 class CategoryPermissions(CategoryAdmin, generic.ModelFormView):
 class CategoryPermissions(CategoryAdmin, generic.ModelFormView):
+    """category roles view for assinging roles to category, add link to it in categories list"""
     templates_dir = 'misago/admin/categoryroles'
     templates_dir = 'misago/admin/categoryroles'
     template = 'categoryroles.html'
     template = 'categoryroles.html'
 
 
@@ -107,7 +103,8 @@ class CategoryPermissions(CategoryAdmin, generic.ModelFormView):
         forms_are_valid = True
         forms_are_valid = True
         for role in Role.objects.order_by('name'):
         for role in Role.objects.order_by('name'):
             FormType = CategoryRolesACLFormFactory(
             FormType = CategoryRolesACLFormFactory(
-                role, category_roles, assigned_roles.get(role.pk))
+                role, category_roles, assigned_roles.get(role.pk)
+            )
 
 
             if request.method == 'POST':
             if request.method == 'POST':
                 forms.append(FormType(request.POST, prefix=role.pk))
                 forms.append(FormType(request.POST, prefix=role.pk))
@@ -125,8 +122,9 @@ class CategoryPermissions(CategoryAdmin, generic.ModelFormView):
                         RoleCategoryACL(
                         RoleCategoryACL(
                             role=form.role,
                             role=form.role,
                             category=target,
                             category=target,
-                            category_role=form.cleaned_data['category_role']
-                        ))
+                            category_role=form.cleaned_data['category_role'],
+                        )
+                    )
             if new_permissions:
             if new_permissions:
                 RoleCategoryACL.objects.bulk_create(new_permissions)
                 RoleCategoryACL.objects.bulk_create(new_permissions)
 
 
@@ -144,19 +142,17 @@ class CategoryPermissions(CategoryAdmin, generic.ModelFormView):
             'target': target,
             'target': target,
         })
         })
 
 
+
 CategoriesList.add_item_action(
 CategoriesList.add_item_action(
     name=_("Category permissions"),
     name=_("Category permissions"),
     icon='fa fa-adjust',
     icon='fa fa-adjust',
     link='misago:admin:categories:nodes:permissions',
     link='misago:admin:categories:nodes:permissions',
-    style='success'
+    style='success',
 )
 )
 
 
 
 
-"""
-Create role categories view for assinging categories to role,
-add link to it in user roles list
-"""
 class RoleCategoriesACL(RoleAdmin, generic.ModelFormView):
 class RoleCategoriesACL(RoleAdmin, generic.ModelFormView):
+    """role categories view for assinging categories to role, add link to it in user roles list"""
     templates_dir = 'misago/admin/categoryroles'
     templates_dir = 'misago/admin/categoryroles'
     template = 'rolecategories.html'
     template = 'rolecategories.html'
 
 
@@ -176,9 +172,7 @@ class RoleCategoriesACL(RoleAdmin, generic.ModelFormView):
         forms_are_valid = True
         forms_are_valid = True
         for category in categories:
         for category in categories:
             category.level_range = range(category.level - 1)
             category.level_range = range(category.level - 1)
-            FormType = RoleCategoryACLFormFactory(category,
-                                               roles,
-                                               choices.get(category.pk))
+            FormType = RoleCategoryACLFormFactory(category, roles, choices.get(category.pk))
 
 
             if request.method == 'POST':
             if request.method == 'POST':
                 forms.append(FormType(request.POST, prefix=category.pk))
                 forms.append(FormType(request.POST, prefix=category.pk))
@@ -193,9 +187,12 @@ class RoleCategoriesACL(RoleAdmin, generic.ModelFormView):
             for form in forms:
             for form in forms:
                 if form.cleaned_data['role']:
                 if form.cleaned_data['role']:
                     new_permissions.append(
                     new_permissions.append(
-                        RoleCategoryACL(role=target,
-                                     category=form.category,
-                                     category_role=form.cleaned_data['role']))
+                        RoleCategoryACL(
+                            role=target,
+                            category=form.category,
+                            category_role=form.cleaned_data['role'],
+                        )
+                    )
             if new_permissions:
             if new_permissions:
                 RoleCategoryACL.objects.bulk_create(new_permissions)
                 RoleCategoryACL.objects.bulk_create(new_permissions)
 
 
@@ -213,9 +210,10 @@ class RoleCategoriesACL(RoleAdmin, generic.ModelFormView):
             'target': target,
             'target': target,
         })
         })
 
 
+
 RolesList.add_item_action(
 RolesList.add_item_action(
     name=_("Categories permissions"),
     name=_("Categories permissions"),
     icon='fa fa-comments-o',
     icon='fa fa-comments-o',
     link='misago:admin:permissions:users:categories',
     link='misago:admin:permissions:users:categories',
-    style='success'
+    style='success',
 )
 )

+ 0 - 1
misago/conf/__init__.py

@@ -1,4 +1,3 @@
 from .gateway import settings, db_settings  # noqa
 from .gateway import settings, db_settings  # noqa
 
 
-
 default_app_config = 'misago.conf.apps.MisagoConfConfig'
 default_app_config = 'misago.conf.apps.MisagoConfConfig'

+ 2 - 1
misago/conf/admin.py

@@ -8,7 +8,8 @@ class MisagoAdminExtension(object):
     def register_urlpatterns(self, urlpatterns):
     def register_urlpatterns(self, urlpatterns):
         urlpatterns.namespace(r'^settings/', 'settings', 'system')
         urlpatterns.namespace(r'^settings/', 'settings', 'system')
 
 
-        urlpatterns.patterns('system:settings',
+        urlpatterns.patterns(
+            'system:settings',
             url(r'^$', views.index, name='index'),
             url(r'^$', views.index, name='index'),
             url(r'^(?P<key>(\w|-)+)/$', views.group, name='group'),
             url(r'^(?P<key>(\w|-)+)/$', views.group, name='group'),
         )
         )

+ 0 - 13
misago/conf/context_processors.py

@@ -1,5 +1,3 @@
-import json
-
 from django.contrib.staticfiles.templatetags.staticfiles import static
 from django.contrib.staticfiles.templatetags.staticfiles import static
 from django.urls import reverse
 from django.urls import reverse
 from django.utils.translation import get_language
 from django.utils.translation import get_language
@@ -14,15 +12,10 @@ BLANK_AVATAR_URL = static(misago_settings.MISAGO_BLANK_AVATAR)
 def settings(request):
 def settings(request):
     return {
     return {
         'DEBUG': misago_settings.DEBUG,
         'DEBUG': misago_settings.DEBUG,
-
         'LANGUAGE_CODE_SHORT': get_language()[:2],
         'LANGUAGE_CODE_SHORT': get_language()[:2],
-
         'misago_settings': db_settings,
         'misago_settings': db_settings,
-
         'BLANK_AVATAR_URL': BLANK_AVATAR_URL,
         'BLANK_AVATAR_URL': BLANK_AVATAR_URL,
-
         'THREADS_ON_INDEX': misago_settings.MISAGO_THREADS_ON_INDEX,
         'THREADS_ON_INDEX': misago_settings.MISAGO_THREADS_ON_INDEX,
-
         'LOGIN_REDIRECT_URL': misago_settings.LOGIN_REDIRECT_URL,
         'LOGIN_REDIRECT_URL': misago_settings.LOGIN_REDIRECT_URL,
         'LOGIN_URL': misago_settings.LOGIN_URL,
         'LOGIN_URL': misago_settings.LOGIN_URL,
         'LOGOUT_URL': misago_settings.LOGOUT_URL,
         'LOGOUT_URL': misago_settings.LOGOUT_URL,
@@ -34,23 +27,17 @@ def preload_settings_json(request):
 
 
     preloaded_settings.update({
     preloaded_settings.update({
         'LOGIN_API_URL': misago_settings.MISAGO_LOGIN_API_URL,
         'LOGIN_API_URL': misago_settings.MISAGO_LOGIN_API_URL,
-
         'LOGIN_REDIRECT_URL': reverse(misago_settings.LOGIN_REDIRECT_URL),
         'LOGIN_REDIRECT_URL': reverse(misago_settings.LOGIN_REDIRECT_URL),
         'LOGIN_URL': reverse(misago_settings.LOGIN_URL),
         'LOGIN_URL': reverse(misago_settings.LOGIN_URL),
-
         'LOGOUT_URL': reverse(misago_settings.LOGOUT_URL),
         'LOGOUT_URL': reverse(misago_settings.LOGOUT_URL),
     })
     })
 
 
     request.frontend_context.update({
     request.frontend_context.update({
         'SETTINGS': preloaded_settings,
         'SETTINGS': preloaded_settings,
-
         'MISAGO_PATH': reverse('misago:index'),
         'MISAGO_PATH': reverse('misago:index'),
-
         'BLANK_AVATAR_URL': BLANK_AVATAR_URL,
         'BLANK_AVATAR_URL': BLANK_AVATAR_URL,
         'STATIC_URL': misago_settings.STATIC_URL,
         'STATIC_URL': misago_settings.STATIC_URL,
-
         'CSRF_COOKIE_NAME': misago_settings.CSRF_COOKIE_NAME,
         'CSRF_COOKIE_NAME': misago_settings.CSRF_COOKIE_NAME,
-
         'THREADS_ON_INDEX': misago_settings.MISAGO_THREADS_ON_INDEX,
         'THREADS_ON_INDEX': misago_settings.MISAGO_THREADS_ON_INDEX,
     })
     })
 
 

+ 0 - 5
misago/conf/defaults.py

@@ -6,11 +6,6 @@ If you rely on any of those in your code, make sure you use `misago.conf.setting
 instead of Django's `django.conf.settings`.
 instead of Django's `django.conf.settings`.
 """
 """
 
 
-# Default JS debug to false
-# This setting used exclusively by test runner and isn't part of public API
-
-_MISAGO_JS_DEBUG = False
-
 
 
 # Permissions system extensions
 # Permissions system extensions
 # https://misago.readthedocs.io/en/latest/developers/acls.html#extending-permissions-system
 # https://misago.readthedocs.io/en/latest/developers/acls.html#extending-permissions-system

+ 13 - 19
misago/conf/forms.py

@@ -20,17 +20,17 @@ class ValidateChoicesNum(object):
             message = ungettext(
             message = ungettext(
                 'You have to select at least %(choices)d option.',
                 'You have to select at least %(choices)d option.',
                 'You have to select at least %(choices)d options.',
                 'You have to select at least %(choices)d options.',
-                self.min_choices)
-            message = message % {'choices': self.min_choices}
-            raise forms.ValidationError(message)
+                self.min_choices,
+            )
+            raise forms.ValidationError(message % {'choices': self.min_choices})
 
 
         if self.max_choices and self.max_choices < data_len:
         if self.max_choices and self.max_choices < data_len:
             message = ungettext(
             message = ungettext(
                 'You cannot select more than %(choices)d option.',
                 'You cannot select more than %(choices)d option.',
                 'You cannot select more than %(choices)d options.',
                 'You cannot select more than %(choices)d options.',
-                self.max_choices)
-            message = message % {'choices': self.max_choices}
-            raise forms.ValidationError(message)
+                self.max_choices,
+            )
+            raise forms.ValidationError(message % {'choices': self.max_choices})
 
 
         return data
         return data
 
 
@@ -69,9 +69,7 @@ def create_checkbox(setting, kwargs, extra):
     kwargs['choices'] = localise_choices(extra)
     kwargs['choices'] = localise_choices(extra)
 
 
     if extra.get('min') or extra.get('max'):
     if extra.get('min') or extra.get('max'):
-        kwargs['validators'] = [
-            ValidateChoicesNum(extra.pop('min', 0), extra.pop('max', 0))
-        ]
+        kwargs['validators'] = [ValidateChoicesNum(extra.pop('min', 0), extra.pop('max', 0))]
 
 
     if setting.python_type == 'int':
     if setting.python_type == 'int':
         return forms.TypedMultipleChoiceField(coerce='int', **kwargs)
         return forms.TypedMultipleChoiceField(coerce='int', **kwargs)
@@ -130,20 +128,16 @@ def setting_field(FormType, setting):
     field_factory = FIELD_STYPES[setting.form_field]
     field_factory = FIELD_STYPES[setting.form_field]
     field_extra = setting.field_extra
     field_extra = setting.field_extra
 
 
-    form_field = field_factory(
-        setting, basic_kwargs(setting, field_extra), field_extra)
+    form_field = field_factory(setting, basic_kwargs(setting, field_extra), field_extra)
 
 
-    FormType = type('FormType%s' % setting.pk, (FormType,), {
-        setting.setting: form_field
-    })
+    FormType = type('FormType%s' % setting.pk, (FormType, ), {setting.setting: form_field})
 
 
     return FormType
     return FormType
 
 
 
 
 def ChangeSettingsForm(data=None, group=None):
 def ChangeSettingsForm(data=None, group=None):
-    """
-    Factory method that builds valid form for settings group
-    """
+    """factory method that builds valid form for settings group"""
+
     class FormType(forms.Form):
     class FormType(forms.Form):
         pass
         pass
 
 
@@ -157,7 +151,7 @@ def ChangeSettingsForm(data=None, group=None):
             if fieldset_fields:
             if fieldset_fields:
                 fieldsets.append({
                 fieldsets.append({
                     'legend': fieldset_legend,
                     'legend': fieldset_legend,
-                    'form': fieldset_form(data)
+                    'form': fieldset_form(data),
                 })
                 })
             fieldset_legend = setting.legend
             fieldset_legend = setting.legend
             fieldset_form = FormType
             fieldset_form = FormType
@@ -168,7 +162,7 @@ def ChangeSettingsForm(data=None, group=None):
     if fieldset_fields:
     if fieldset_fields:
         fieldsets.append({
         fieldsets.append({
             'legend': fieldset_legend,
             'legend': fieldset_legend,
-            'form': fieldset_form(data)
+            'form': fieldset_form(data),
         })
         })
 
 
     return fieldsets
     return fieldsets

+ 15 - 10
misago/conf/migrations/0001_initial.py

@@ -7,14 +7,17 @@ from django.db import migrations, models
 
 
 class Migration(migrations.Migration):
 class Migration(migrations.Migration):
 
 
-    dependencies = [
-    ]
+    dependencies = []
 
 
     operations = [
     operations = [
         migrations.CreateModel(
         migrations.CreateModel(
             name='Setting',
             name='Setting',
             fields=[
             fields=[
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+                (
+                    'id', models.AutoField(
+                        verbose_name='ID', serialize=False, auto_created=True, primary_key=True
+                    )
+                ),
                 ('setting', models.CharField(unique=True, max_length=255)),
                 ('setting', models.CharField(unique=True, max_length=255)),
                 ('name', models.CharField(max_length=255)),
                 ('name', models.CharField(max_length=255)),
                 ('description', models.TextField(null=True, blank=True)),
                 ('description', models.TextField(null=True, blank=True)),
@@ -28,21 +31,23 @@ class Migration(migrations.Migration):
                 ('form_field', models.CharField(default='text', max_length=255)),
                 ('form_field', models.CharField(default='text', max_length=255)),
                 ('field_extra', JSONField()),
                 ('field_extra', JSONField()),
             ],
             ],
-            options={
-            },
-            bases=(models.Model,),
+            options={},
+            bases=(models.Model, ),
         ),
         ),
         migrations.CreateModel(
         migrations.CreateModel(
             name='SettingsGroup',
             name='SettingsGroup',
             fields=[
             fields=[
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+                (
+                    'id', models.AutoField(
+                        verbose_name='ID', serialize=False, auto_created=True, primary_key=True
+                    )
+                ),
                 ('key', models.CharField(unique=True, max_length=255)),
                 ('key', models.CharField(unique=True, max_length=255)),
                 ('name', models.CharField(max_length=255)),
                 ('name', models.CharField(max_length=255)),
                 ('description', models.TextField(null=True, blank=True)),
                 ('description', models.TextField(null=True, blank=True)),
             ],
             ],
-            options={
-            },
-            bases=(models.Model,),
+            options={},
+            bases=(models.Model, ),
         ),
         ),
         migrations.AddField(
         migrations.AddField(
             model_name='setting',
             model_name='setting',

+ 4 - 5
misago/conf/migrationutils.py

@@ -65,14 +65,12 @@ def migrate_setting(Setting, group, setting_fixture, order, old_value):
     if setting_fixture.get('description'):
     if setting_fixture.get('description'):
         setting_fixture['description'] = setting_fixture.get('description')
         setting_fixture['description'] = setting_fixture.get('description')
 
 
-    if (setting_fixture.get('field_extra') and
-            setting_fixture.get('field_extra').get('choices')):
+    if setting_fixture.get('field_extra') and setting_fixture.get('field_extra').get('choices'):
         untranslated_choices = setting_fixture['field_extra']['choices']
         untranslated_choices = setting_fixture['field_extra']['choices']
         translated_choices = []
         translated_choices = []
         for val, name in untranslated_choices:
         for val, name in untranslated_choices:
             translated_choices.append((val, name))
             translated_choices.append((val, name))
-        setting_fixture['field_extra']['choices'] = tuple(
-            translated_choices)
+        setting_fixture['field_extra']['choices'] = tuple(translated_choices)
 
 
     if old_value is None:
     if old_value is None:
         value = setting_fixture.pop('value', None)
         value = setting_fixture.pop('value', None)
@@ -87,7 +85,8 @@ def migrate_setting(Setting, group, setting_fixture, order, old_value):
 
 
     if setting_fixture.get("default_value"):
     if setting_fixture.get("default_value"):
         setting.default_value = dehydrate_value(
         setting.default_value = dehydrate_value(
-            setting.python_type, setting_fixture.get("default_value"))
+            setting.python_type, setting_fixture.get("default_value")
+        )
 
 
     setting.field_extra = field_extra or {}
     setting.field_extra = field_extra or {}
 
 

+ 1 - 1
misago/conf/models.py

@@ -1,7 +1,7 @@
 from django.contrib.postgres.fields import JSONField
 from django.contrib.postgres.fields import JSONField
 from django.db import models
 from django.db import models
 
 
-from . import hydrators, utils
+from . import utils
 
 
 
 
 class SettingsGroupsManager(models.Manager):
 class SettingsGroupsManager(models.Manager):

+ 17 - 15
misago/conf/tests/test_admin_views.py

@@ -17,31 +17,33 @@ class AdminSettingsViewsTests(AdminTestCase):
 
 
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
         for group in SettingsGroup.objects.all():
         for group in SettingsGroup.objects.all():
-            group_link = reverse('misago:admin:system:settings:group', kwargs={
-                'key': group.key
-            })
+            group_link = reverse(
+                'misago:admin:system:settings:group', kwargs={
+                    'key': group.key,
+                }
+            )
             self.assertContains(response, group.name)
             self.assertContains(response, group.name)
             self.assertContains(response, group_link)
             self.assertContains(response, group_link)
 
 
     def test_invalid_group_handling(self):
     def test_invalid_group_handling(self):
-        """
-        invalid group results in redirect to settings list
-        """
-        group_link = reverse('misago:admin:system:settings:group', kwargs={
-            'key': 'invalid-group'
-        })
+        """invalid group results in redirect to settings list"""
+        group_link = reverse(
+            'misago:admin:system:settings:group', kwargs={
+                'key': 'invalid-group',
+            }
+        )
         response = self.client.get(group_link)
         response = self.client.get(group_link)
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
         self.assertTrue(reverse('misago:admin:system:settings:index') in response['location'])
         self.assertTrue(reverse('misago:admin:system:settings:index') in response['location'])
 
 
     def test_groups_views(self):
     def test_groups_views(self):
-        """
-        each settings group view returns 200 and contains all settings in group
-        """
+        """each settings group view returns 200 and contains all settings in group"""
         for group in SettingsGroup.objects.all():
         for group in SettingsGroup.objects.all():
-            group_link = reverse('misago:admin:system:settings:group', kwargs={
-                'key': group.key
-            })
+            group_link = reverse(
+                'misago:admin:system:settings:group', kwargs={
+                    'key': group.key,
+                }
+            )
             response = self.client.get(group_link)
             response = self.client.get(group_link)
 
 
             self.assertEqual(response.status_code, 200)
             self.assertEqual(response.status_code, 200)

+ 24 - 24
misago/conf/tests/test_migrationutils.py

@@ -12,25 +12,25 @@ class DBConfMigrationUtilsTests(TestCase):
             'key': 'test_group',
             'key': 'test_group',
             'name': "Test settings",
             'name': "Test settings",
             'description': "Those are test settings.",
             'description': "Those are test settings.",
-            'settings': (
+            'settings': [
                 {
                 {
                     'setting': 'fish_name',
                     'setting': 'fish_name',
                     'name': "Fish's name",
                     'name': "Fish's name",
                     'value': "Eric",
                     'value': "Eric",
                     'field_extra': {
                     'field_extra': {
-                           'min_length': 2,
-                           'max_length': 255
-                        },
+                        'min_length': 2,
+                        'max_length': 255,
+                    },
                 },
                 },
                 {
                 {
                     'setting': 'fish_license_no',
                     'setting': 'fish_license_no',
                     'name': "Fish's license number",
                     'name': "Fish's license number",
                     'default_value': '123-456',
                     'default_value': '123-456',
                     'field_extra': {
                     'field_extra': {
-                            'max_length': 255
-                        },
+                        'max_length': 255,
+                    },
                 },
                 },
-            )
+            ],
         }
         }
 
 
         migrationutils.migrate_settings_group(apps, self.test_group)
         migrationutils.migrate_settings_group(apps, self.test_group)
@@ -42,16 +42,14 @@ class DBConfMigrationUtilsTests(TestCase):
     def test_get_custom_group_and_settings(self):
     def test_get_custom_group_and_settings(self):
         """tests setup created settings group"""
         """tests setup created settings group"""
         custom_group = migrationutils.get_group(
         custom_group = migrationutils.get_group(
-            apps.get_model('misago_conf', 'SettingsGroup'),
-            self.test_group['key'])
+            apps.get_model('misago_conf', 'SettingsGroup'), self.test_group['key']
+        )
 
 
         self.assertEqual(custom_group.key, self.test_group['key'])
         self.assertEqual(custom_group.key, self.test_group['key'])
         self.assertEqual(custom_group.name, self.test_group['name'])
         self.assertEqual(custom_group.name, self.test_group['name'])
-        self.assertEqual(custom_group.description,
-                         self.test_group['description'])
+        self.assertEqual(custom_group.description, self.test_group['description'])
 
 
-        custom_settings = migrationutils.get_custom_settings_values(
-            custom_group)
+        custom_settings = migrationutils.get_custom_settings_values(custom_group)
 
 
         self.assertEqual(custom_settings['fish_name'], 'Eric')
         self.assertEqual(custom_settings['fish_name'], 'Eric')
         self.assertTrue('fish_license_no' not in custom_settings)
         self.assertTrue('fish_license_no' not in custom_settings)
@@ -63,37 +61,39 @@ class DBConfMigrationUtilsTests(TestCase):
             'key': 'new_test_group',
             'key': 'new_test_group',
             'name': "New test settings",
             'name': "New test settings",
             'description': "Those are updated test settings.",
             'description': "Those are updated test settings.",
-            'settings': (
+            'settings': [
                 {
                 {
                     'setting': 'fish_new_name',
                     'setting': 'fish_new_name',
                     'name': "Fish's new name",
                     'name': "Fish's new name",
                     'value': "Eric",
                     'value': "Eric",
                     'field_extra': {
                     'field_extra': {
-                            'min_length': 2,
-                            'max_length': 255
-                        },
+                        'min_length': 2,
+                        'max_length': 255,
+                    },
                 },
                 },
                 {
                 {
                     'setting': 'fish_new_license_no',
                     'setting': 'fish_new_license_no',
                     'name': "Fish's changed license number",
                     'name': "Fish's changed license number",
                     'default_value': '123-456',
                     'default_value': '123-456',
                     'field_extra': {
                     'field_extra': {
-                            'max_length': 255
-                        },
+                        'max_length': 255,
+                    },
                 },
                 },
-            )
+            ],
         }
         }
 
 
         migrationutils.migrate_settings_group(
         migrationutils.migrate_settings_group(
-            apps, new_group, old_group_key=self.test_group['key'])
+            apps, new_group, old_group_key=self.test_group['key']
+        )
+
         db_group = migrationutils.get_group(
         db_group = migrationutils.get_group(
-            apps.get_model('misago_conf', 'SettingsGroup'), new_group['key'])
+            apps.get_model('misago_conf', 'SettingsGroup'), new_group['key']
+        )
 
 
         self.assertEqual(SettingsGroup.objects.count(), self.groups_count)
         self.assertEqual(SettingsGroup.objects.count(), self.groups_count)
         self.assertEqual(db_group.key, new_group['key'])
         self.assertEqual(db_group.key, new_group['key'])
         self.assertEqual(db_group.name, new_group['name'])
         self.assertEqual(db_group.name, new_group['name'])
-        self.assertEqual(db_group.description,
-                         new_group['description'])
+        self.assertEqual(db_group.description, new_group['description'])
 
 
         for setting in new_group['settings']:
         for setting in new_group['settings']:
             db_setting = db_group.setting_set.get(setting=setting['setting'])
             db_setting = db_group.setting_set.get(setting=setting['setting'])

+ 28 - 19
misago/conf/tests/test_models.py

@@ -6,34 +6,43 @@ from misago.conf.models import Setting
 class SettingModelTests(TestCase):
 class SettingModelTests(TestCase):
     def test_real_value(self):
     def test_real_value(self):
         """setting returns real value correctyly"""
         """setting returns real value correctyly"""
-        setting_model = Setting(python_type='list', dry_value='')
+        setting_model = Setting(
+            python_type='list',
+            dry_value='',
+        )
         self.assertEqual(setting_model.value, [])
         self.assertEqual(setting_model.value, [])
 
 
-        setting_model = Setting(python_type='list',
-                                dry_value='Arthur,Lancelot,Patsy')
-        self.assertEqual(setting_model.value,
-                         ['Arthur', 'Lancelot', 'Patsy'])
-
-        setting_model = Setting(python_type='list',
-                                default_value='Arthur,Patsy')
-        self.assertEqual(setting_model.value,
-                         ['Arthur', 'Patsy'])
-
-        setting_model = Setting(python_type='list',
-                                dry_value='Arthur,Robin,Patsy',
-                                default_value='Arthur,Patsy')
-        self.assertEqual(setting_model.value,
-                         ['Arthur', 'Robin', 'Patsy'])
+        setting_model = Setting(
+            python_type='list',
+            dry_value='Arthur,Lancelot,Patsy',
+        )
+        self.assertEqual(setting_model.value, ['Arthur', 'Lancelot', 'Patsy'])
+
+        setting_model = Setting(
+            python_type='list',
+            default_value='Arthur,Patsy',
+        )
+        self.assertEqual(setting_model.value, ['Arthur', 'Patsy'])
+
+        setting_model = Setting(
+            python_type='list',
+            dry_value='Arthur,Robin,Patsy',
+            default_value='Arthur,Patsy',
+        )
+        self.assertEqual(setting_model.value, ['Arthur', 'Robin', 'Patsy'])
 
 
     def test_set_value(self):
     def test_set_value(self):
         """setting sets value correctyly"""
         """setting sets value correctyly"""
-        setting_model = Setting(python_type='int',
-                                dry_value='42',
-                                default_value='9001')
+        setting_model = Setting(
+            python_type='int',
+            dry_value='42',
+            default_value='9001',
+        )
 
 
         setting_model.value = 3000
         setting_model.value = 3000
         self.assertEqual(setting_model.value, 3000)
         self.assertEqual(setting_model.value, 3000)
         self.assertEqual(setting_model.dry_value, '3000')
         self.assertEqual(setting_model.dry_value, '3000')
+
         setting_model.value = None
         setting_model.value = None
         self.assertEqual(setting_model.value, 9001)
         self.assertEqual(setting_model.value, 9001)
         self.assertEqual(setting_model.dry_value, None)
         self.assertEqual(setting_model.dry_value, None)

+ 28 - 33
misago/conf/tests/test_settings.py

@@ -27,10 +27,8 @@ class GatewaySettingsTests(TestCase):
     def test_get_existing_setting(self):
     def test_get_existing_setting(self):
         """forum_name is defined"""
         """forum_name is defined"""
         self.assertEqual(gateway.forum_name, db_settings.forum_name)
         self.assertEqual(gateway.forum_name, db_settings.forum_name)
-        self.assertEqual(gateway.INSTALLED_APPS,
-                         dj_settings.INSTALLED_APPS)
-        self.assertEqual(gateway.MISAGO_THREADS_PER_PAGE,
-                         defaults.MISAGO_THREADS_PER_PAGE)
+        self.assertEqual(gateway.INSTALLED_APPS, dj_settings.INSTALLED_APPS)
+        self.assertEqual(gateway.MISAGO_THREADS_PER_PAGE, defaults.MISAGO_THREADS_PER_PAGE)
 
 
         with self.assertRaises(AttributeError):
         with self.assertRaises(AttributeError):
             gateway.LoremIpsum
             gateway.LoremIpsum
@@ -46,28 +44,28 @@ class GatewaySettingsTests(TestCase):
             'key': 'test_group',
             'key': 'test_group',
             'name': "Test settings",
             'name': "Test settings",
             'description': "Those are test settings.",
             'description': "Those are test settings.",
-            'settings': (
+            'settings': [
                 {
                 {
                     'setting': 'fish_name',
                     'setting': 'fish_name',
                     'name': "Fish's name",
                     'name': "Fish's name",
                     'value': "Public Eric",
                     'value': "Public Eric",
                     'field_extra': {
                     'field_extra': {
-                            'min_length': 2,
-                            'max_length': 255
-                        },
-                    'is_public': True
+                        'min_length': 2,
+                        'max_length': 255,
+                    },
+                    'is_public': True,
                 },
                 },
                 {
                 {
                     'setting': 'private_fish_name',
                     'setting': 'private_fish_name',
                     'name': "Fish's name",
                     'name': "Fish's name",
                     'value': "Private Eric",
                     'value': "Private Eric",
                     'field_extra': {
                     'field_extra': {
-                            'min_length': 2,
-                            'max_length': 255
-                        },
-                    'is_public': False
+                        'min_length': 2,
+                        'max_length': 255,
+                    },
+                    'is_public': False,
                 },
                 },
-            )
+            ],
         }
         }
 
 
         migrate_settings_group(apps, test_group)
         migrate_settings_group(apps, test_group)
@@ -79,44 +77,43 @@ class GatewaySettingsTests(TestCase):
         self.assertIn('fish_name', public_settings)
         self.assertIn('fish_name', public_settings)
         self.assertNotIn('private_fish_name', public_settings)
         self.assertNotIn('private_fish_name', public_settings)
 
 
-
     def test_setting_lazy(self):
     def test_setting_lazy(self):
         """lazy settings work"""
         """lazy settings work"""
         test_group = {
         test_group = {
             'key': 'test_group',
             'key': 'test_group',
             'name': "Test settings",
             'name': "Test settings",
             'description': "Those are test settings.",
             'description': "Those are test settings.",
-            'settings': (
+            'settings': [
                 {
                 {
                     'setting': 'fish_name',
                     'setting': 'fish_name',
                     'name': "Fish's name",
                     'name': "Fish's name",
                     'value': "Greedy Eric",
                     'value': "Greedy Eric",
                     'field_extra': {
                     'field_extra': {
-                            'min_length': 2,
-                            'max_length': 255
-                        },
-                    'is_lazy': False
+                        'min_length': 2,
+                        'max_length': 255,
+                    },
+                    'is_lazy': False,
                 },
                 },
                 {
                 {
                     'setting': 'lazy_fish_name',
                     'setting': 'lazy_fish_name',
                     'name': "Fish's name",
                     'name': "Fish's name",
                     'value': "Lazy Eric",
                     'value': "Lazy Eric",
                     'field_extra': {
                     'field_extra': {
-                            'min_length': 2,
-                            'max_length': 255
-                        },
-                    'is_lazy': True
+                        'min_length': 2,
+                        'max_length': 255,
+                    },
+                    'is_lazy': True,
                 },
                 },
                 {
                 {
                     'setting': 'lazy_empty_setting',
                     'setting': 'lazy_empty_setting',
                     'name': "Fish's name",
                     'name': "Fish's name",
                     'field_extra': {
                     'field_extra': {
-                            'min_length': 2,
-                            'max_length': 255
-                        },
-                    'is_lazy': True
+                        'min_length': 2,
+                        'max_length': 255,
+                    },
+                    'is_lazy': True,
                 },
                 },
-            )
+            ],
         }
         }
 
 
         migrate_settings_group(apps, test_group)
         migrate_settings_group(apps, test_group)
@@ -125,11 +122,9 @@ class GatewaySettingsTests(TestCase):
         self.assertTrue(db_settings.lazy_fish_name)
         self.assertTrue(db_settings.lazy_fish_name)
 
 
         self.assertTrue(gateway.lazy_fish_name)
         self.assertTrue(gateway.lazy_fish_name)
-        self.assertEqual(
-            gateway.get_lazy_setting('lazy_fish_name'), 'Lazy Eric')
+        self.assertEqual(gateway.get_lazy_setting('lazy_fish_name'), 'Lazy Eric')
         self.assertTrue(db_settings.lazy_fish_name)
         self.assertTrue(db_settings.lazy_fish_name)
-        self.assertEqual(
-            db_settings.get_lazy_setting('lazy_fish_name'), 'Lazy Eric')
+        self.assertEqual(db_settings.get_lazy_setting('lazy_fish_name'), 'Lazy Eric')
 
 
         self.assertTrue(gateway.lazy_empty_setting is None)
         self.assertTrue(gateway.lazy_empty_setting is None)
         self.assertTrue(db_settings.lazy_empty_setting is None)
         self.assertTrue(db_settings.lazy_empty_setting is None)

+ 3 - 6
misago/conf/utils.py

@@ -3,17 +3,14 @@ from . import hydrators
 
 
 def get_setting_value(setting):
 def get_setting_value(setting):
     if not setting.dry_value and setting.default_value:
     if not setting.dry_value and setting.default_value:
-        return hydrators.hydrate_value(
-            setting.python_type, setting.default_value)
+        return hydrators.hydrate_value(setting.python_type, setting.default_value)
     else:
     else:
-        return hydrators.hydrate_value(
-            setting.python_type, setting.dry_value)
+        return hydrators.hydrate_value(setting.python_type, setting.dry_value)
 
 
 
 
 def set_setting_value(setting, new_value):
 def set_setting_value(setting, new_value):
     if new_value is not None:
     if new_value is not None:
-        setting.dry_value = hydrators.dehydrate_value(
-            setting.python_type, new_value)
+        setting.dry_value = hydrators.dehydrate_value(setting.python_type, new_value)
     else:
     else:
         setting.dry_value = None
         setting.dry_value = None
     return setting.value
     return setting.value

+ 10 - 11
misago/conf/views.py

@@ -34,8 +34,7 @@ def group(request, key):
     fieldsets = ChangeSettingsForm(group=active_group)
     fieldsets = ChangeSettingsForm(group=active_group)
     if request.method == 'POST':
     if request.method == 'POST':
         fieldsets = ChangeSettingsForm(request.POST, group=active_group)
         fieldsets = ChangeSettingsForm(request.POST, group=active_group)
-        valid_fieldsets = len([True for fieldset in fieldsets if
-                               fieldset['form'].is_valid()])
+        valid_fieldsets = len([True for fieldset in fieldsets if fieldset['form'].is_valid()])
         if len(fieldsets) == valid_fieldsets:
         if len(fieldsets) == valid_fieldsets:
             new_values = {}
             new_values = {}
             for fieldset in fieldsets:
             for fieldset in fieldsets:
@@ -47,15 +46,15 @@ def group(request, key):
 
 
             db_settings.flush_cache()
             db_settings.flush_cache()
 
 
-            messages.success(
-                request, _("Changes in settings have been saved!"))
+            messages.success(request, _("Changes in settings have been saved!"))
             return redirect('misago:admin:system:settings:group', key=key)
             return redirect('misago:admin:system:settings:group', key=key)
 
 
-    use_single_form_template = (
-        len(fieldsets) == 1 and not fieldsets[0]['legend'])
+    use_single_form_template = (len(fieldsets) == 1 and not fieldsets[0]['legend'])
 
 
-    return render(request, 'misago/admin/conf/group.html',{
-        'active_group': active_group,
-        'fieldsets': fieldsets,
-        'use_single_form_template': use_single_form_template,
-    })
+    return render(
+        request, 'misago/admin/conf/group.html', {
+            'active_group': active_group,
+            'fieldsets': fieldsets,
+            'use_single_form_template': use_single_form_template,
+        }
+    )

+ 3 - 4
misago/core/__init__.py

@@ -1,11 +1,10 @@
 from django.conf import settings
 from django.conf import settings
 from django.core.checks import register, Critical
 from django.core.checks import register, Critical
 
 
-
-SUPPORTED_ENGINES = (
+SUPPORTED_ENGINES = [
     'django.db.backends.postgresql',
     'django.db.backends.postgresql',
     'django.db.backends.postgresql_psycopg2',
     'django.db.backends.postgresql_psycopg2',
-)
+]
 
 
 
 
 @register()
 @register()
@@ -18,7 +17,7 @@ def check_db_engine(app_configs, **kwargs):
     except (AttributeError, KeyError, ValueError):
     except (AttributeError, KeyError, ValueError):
         errors.append(Critical(
         errors.append(Critical(
             msg='Misago requires PostgreSQL database.',
             msg='Misago requires PostgreSQL database.',
-            id='misago.001'
+            id='misago.001',
         ))
         ))
 
 
     return errors
     return errors

+ 1 - 1
misago/core/apipatch.py

@@ -40,7 +40,7 @@ class ApiPatch(object):
     def dispatch(self, request, target):
     def dispatch(self, request, target):
         if not isinstance(request.data, list):
         if not isinstance(request.data, list):
             return Response({
             return Response({
-                'detail': "PATCH request should be list of operations"
+                'detail': "PATCH request should be list of operations",
             }, status=400)
             }, status=400)
 
 
         detail = []
         detail = []

+ 2 - 2
misago/core/apirouter.py

@@ -11,7 +11,7 @@ class MisagoApiRouter(DefaultRouter):
             url=r'^{prefix}{trailing_slash}$',
             url=r'^{prefix}{trailing_slash}$',
             mapping={
             mapping={
                 'get': 'list',
                 'get': 'list',
-                'post': 'create'
+                'post': 'create',
             },
             },
             name='{basename}-list',
             name='{basename}-list',
             initkwargs={'suffix': 'List'}
             initkwargs={'suffix': 'List'}
@@ -31,7 +31,7 @@ class MisagoApiRouter(DefaultRouter):
                 'get': 'retrieve',
                 'get': 'retrieve',
                 'put': 'update',
                 'put': 'update',
                 'patch': 'partial_update',
                 'patch': 'partial_update',
-                'delete': 'destroy'
+                'delete': 'destroy',
             },
             },
             name='{basename}-detail',
             name='{basename}-detail',
             initkwargs={'suffix': 'Instance'}
             initkwargs={'suffix': 'Instance'}

+ 1 - 2
misago/core/cachebuster.py

@@ -65,8 +65,7 @@ class CacheBusterController(object):
         from .models import CacheVersion
         from .models import CacheVersion
 
 
         self.cache[cache] += 1
         self.cache[cache] += 1
-        CacheVersion.objects.filter(cache=cache).update(
-            version=F('version') + 1)
+        CacheVersion.objects.filter(cache=cache).update(version=F('version') + 1)
         default_cache.delete(CACHE_KEY)
         default_cache.delete(CACHE_KEY)
 
 
     def invalidate_all(self):
     def invalidate_all(self):

+ 5 - 5
misago/core/context_processors.py

@@ -32,21 +32,21 @@ def current_link(request):
     else:
     else:
         link_name = url_name
         link_name = url_name
 
 
-    request.frontend_context.update({
-        'CURRENT_LINK': link_name
-    })
+    request.frontend_context.update({'CURRENT_LINK': link_name})
 
 
     return {}
     return {}
 
 
 
 
 def momentjs_locale(request):
 def momentjs_locale(request):
     return {
     return {
-        'MOMENTJS_LOCALE_URL': get_locale_url(get_language())
+        'MOMENTJS_LOCALE_URL': get_locale_url(get_language()),
     }
     }
 
 
 
 
 def frontend_context(request):
 def frontend_context(request):
     if request.include_frontend_context:
     if request.include_frontend_context:
-        return {'frontend_context': request.frontend_context}
+        return {
+            'frontend_context': request.frontend_context,
+        }
     else:
     else:
         return {}
         return {}

+ 2 - 0
misago/core/decorators.py

@@ -7,6 +7,7 @@ def ajax_only(f):
             return not_allowed(request)
             return not_allowed(request)
         else:
         else:
             return f(request, *args, **kwargs)
             return f(request, *args, **kwargs)
+
     return decorator
     return decorator
 
 
 
 
@@ -16,4 +17,5 @@ def require_POST(f):
             return not_allowed(request)
             return not_allowed(request)
         else:
         else:
             return f(request, *args, **kwargs)
             return f(request, *args, **kwargs)
+
     return decorator
     return decorator

+ 14 - 8
misago/core/errorpages.py

@@ -14,23 +14,27 @@ def _ajax_error(code=406, message=None):
 @admin_error_page
 @admin_error_page
 def _error_page(request, code, message=None):
 def _error_page(request, code, message=None):
     request.frontend_context.update({
     request.frontend_context.update({
-        'CURRENT_LINK': 'misago:error-%s' % code
+        'CURRENT_LINK': 'misago:error-%s' % code,
     })
     })
 
 
-    return render(request, 'misago/errorpages/%s.html' % code, {
-        'message': message
-    }, status=code)
+    return render(
+        request, 'misago/errorpages/%s.html' % code, {
+            'message': message,
+        }, status=code
+    )
 
 
 
 
 def banned(request, ban):
 def banned(request, ban):
     request.frontend_context.update({
     request.frontend_context.update({
         'MESSAGE': ban.get_serialized_message(),
         'MESSAGE': ban.get_serialized_message(),
-        'CURRENT_LINK': 'misago:error-banned'
+        'CURRENT_LINK': 'misago:error-banned',
     })
     })
 
 
-    return render(request, 'misago/errorpages/banned.html', {
-        'ban': ban
-    }, status=403)
+    return render(
+        request, 'misago/errorpages/banned.html', {
+            'ban': ban,
+        }, status=403
+    )
 
 
 
 
 def permission_denied(request, message=None):
 def permission_denied(request, message=None):
@@ -68,6 +72,7 @@ def shared_403_exception_handler(f):
             return permission_denied(request)
             return permission_denied(request)
         else:
         else:
             return f(request, *args, **kwargs)
             return f(request, *args, **kwargs)
+
     return page_decorator
     return page_decorator
 
 
 
 
@@ -77,4 +82,5 @@ def shared_404_exception_handler(f):
             return page_not_found(request)
             return page_not_found(request)
         else:
         else:
             return f(request, *args, **kwargs)
             return f(request, *args, **kwargs)
+
     return page_decorator
     return page_decorator

+ 15 - 8
misago/core/exceptionhandler.py

@@ -4,13 +4,19 @@ from django.core.exceptions import PermissionDenied
 from django.http import Http404, HttpResponsePermanentRedirect, JsonResponse
 from django.http import Http404, HttpResponsePermanentRedirect, JsonResponse
 from django.urls import reverse
 from django.urls import reverse
 from django.utils import six
 from django.utils import six
-from django.utils.translation import gettext as _
 
 
 from . import errorpages
 from . import errorpages
 from .exceptions import AjaxError, Banned, ExplicitFirstPage, OutdatedSlug
 from .exceptions import AjaxError, Banned, ExplicitFirstPage, OutdatedSlug
 
 
 
 
-HANDLED_EXCEPTIONS = (AjaxError, Banned, ExplicitFirstPage, Http404, OutdatedSlug, PermissionDenied)
+HANDLED_EXCEPTIONS = [
+    AjaxError,
+    Banned,
+    ExplicitFirstPage,
+    Http404,
+    OutdatedSlug,
+    PermissionDenied,
+]
 
 
 
 
 def is_misago_exception(exception):
 def is_misago_exception(exception):
@@ -18,7 +24,10 @@ def is_misago_exception(exception):
 
 
 
 
 def handle_ajax_error(request, exception):
 def handle_ajax_error(request, exception):
-    json = {'is_error': 1, 'message': six.text_type(exception.message)}
+    json = {
+        'is_error': 1,
+        'message': six.text_type(exception.message),
+    }
     return JsonResponse(json, status=exception.code)
     return JsonResponse(json, status=exception.code)
 
 
 
 
@@ -46,7 +55,6 @@ def handle_outdated_slug_exception(request, exception):
     view_name = request.resolver_match.view_name
     view_name = request.resolver_match.view_name
 
 
     model = exception.args[0]
     model = exception.args[0]
-    model_name = model.__class__.__name__.lower()
     url_kwargs = request.resolver_match.kwargs
     url_kwargs = request.resolver_match.kwargs
     url_kwargs['slug'] = model.slug
     url_kwargs['slug'] = model.slug
 
 
@@ -63,14 +71,14 @@ def handle_permission_denied_exception(request, exception):
     return errorpages.permission_denied(request, error_message)
     return errorpages.permission_denied(request, error_message)
 
 
 
 
-EXCEPTION_HANDLERS = (
+EXCEPTION_HANDLERS = [
     (AjaxError, handle_ajax_error),
     (AjaxError, handle_ajax_error),
     (Banned, handle_banned_exception),
     (Banned, handle_banned_exception),
     (Http404, handle_http404_exception),
     (Http404, handle_http404_exception),
     (ExplicitFirstPage, handle_explicit_first_page_exception),
     (ExplicitFirstPage, handle_explicit_first_page_exception),
     (OutdatedSlug, handle_outdated_slug_exception),
     (OutdatedSlug, handle_outdated_slug_exception),
     (PermissionDenied, handle_permission_denied_exception),
     (PermissionDenied, handle_permission_denied_exception),
-)
+]
 
 
 
 
 def get_exception_handler(exception):
 def get_exception_handler(exception):
@@ -78,8 +86,7 @@ def get_exception_handler(exception):
         if isinstance(exception, exception_type):
         if isinstance(exception, exception_type):
             return handler
             return handler
     else:
     else:
-        raise ValueError(
-            "%s is not Misago exception" % exception.__class__.__name__)
+        raise ValueError("%s is not Misago exception" % exception.__class__.__name__)
 
 
 
 
 def handle_misago_exception(request, exception):
 def handle_misago_exception(request, exception):

+ 4 - 3
misago/core/exceptions.py

@@ -2,7 +2,8 @@ from django.core.exceptions import PermissionDenied
 
 
 
 
 class AjaxError(Exception):
 class AjaxError(Exception):
-    """You've tried to do something over AJAX but misago blurped"""
+    """you've tried to do something over AJAX but misago blurped"""
+
     def __init__(self, message=None, code=406):
     def __init__(self, message=None, code=406):
         self.message = message
         self.message = message
         self.code = code
         self.code = code
@@ -15,10 +16,10 @@ class Banned(PermissionDenied):
 
 
 
 
 class ExplicitFirstPage(Exception):
 class ExplicitFirstPage(Exception):
-    """The url that was used to reach view contained explicit first page"""
+    """the url that was used to reach view contained explicit first page"""
     pass
     pass
 
 
 
 
 class OutdatedSlug(Exception):
 class OutdatedSlug(Exception):
-    """The url that was used to reach view contained outdated slug"""
+    """the url that was used to reach view contained outdated slug"""
     pass
     pass

+ 6 - 2
misago/core/forms.py

@@ -45,6 +45,10 @@ def YesNoSwitch(**kwargs):
 
 
     return YesNoSwitchBase(
     return YesNoSwitchBase(
         coerce=int,
         coerce=int,
-        choices=((1, yes_label), (0, no_label)),
+        choices=[
+            (1, yes_label),
+            (0, no_label),
+        ],
         widget=RadioSelect(attrs={'class': 'yesno-switch'}),
         widget=RadioSelect(attrs={'class': 'yesno-switch'}),
-        **kwargs)
+        **kwargs
+    )

+ 1 - 2
misago/core/mail.py

@@ -11,8 +11,7 @@ def build_mail(request, recipient, subject, template, context=None):
     message_plain = render_to_string('%s.txt' % template, context, request=request)
     message_plain = render_to_string('%s.txt' % template, context, request=request)
     message_html = render_to_string('%s.html' % template, context, request=request)
     message_html = render_to_string('%s.html' % template, context, request=request)
 
 
-    message = djmail.EmailMultiAlternatives(
-        subject, message_plain, to=[recipient.email])
+    message = djmail.EmailMultiAlternatives(subject, message_plain, to=[recipient.email])
     message.attach_alternative(message_html, "text/html")
     message.attach_alternative(message_html, "text/html")
 
 
     return message
     return message

+ 6 - 6
misago/core/management/commands/misagodbrelations.py

@@ -32,12 +32,12 @@ class Command(BaseCommand):
 
 
                         # Finally list model relations
                         # Finally list model relations
                         for field in model_relations:
                         for field in model_relations:
-                            self.stdout.write(field_pattern % (
-                                field.name,
-                                field.__class__.__name__,
-                                field.related_model.__name__,
-                                field.rel.on_delete.__name__,
-                            ))
+                            self.stdout.write(
+                                field_pattern % (
+                                    field.name, field.__class__.__name__,
+                                    field.related_model.__name__, field.rel.on_delete.__name__,
+                                )
+                            )
 
 
     def print_app_header(self, app):
     def print_app_header(self, app):
         # Fancy title
         # Fancy title

+ 0 - 30
misago/core/management/commands/testemailsetup.py

@@ -1,30 +0,0 @@
-from django.conf import settings
-from django.core import mail
-from django.core.exceptions import ValidationError
-from django.core.management.base import BaseCommand
-from django.core.validators import validate_email
-
-
-class Command(BaseCommand):
-    help = 'Sends test e-mail to given address'
-
-    def add_arguments(self, parser):
-        parser.add_argument('email', type=str)
-
-    def handle(self, *args, **options):
-        try:
-            email = options['email']
-            validate_email(email)
-            self.send_message(email)
-        except ValidationError:
-            self.stderr.write("This isn't valid e-mail address")
-
-    def send_message(self, email):
-        mail.send_mail(
-            'Test Message',
-            ("This message was sent to test if your "
-             "site e-mail is configured correctly."),
-            settings.DEFAULT_FROM_EMAIL,
-            [email],
-            fail_silently=False)
-        self.stdout.write("Test message was sent to %s" % email)

+ 1 - 2
misago/core/middleware/exceptionhandler.py

@@ -7,8 +7,7 @@ from misago.core.utils import is_request_to_misago
 class ExceptionHandlerMiddleware(MiddlewareMixin):
 class ExceptionHandlerMiddleware(MiddlewareMixin):
     def process_exception(self, request, exception):
     def process_exception(self, request, exception):
         request_is_to_misago = is_request_to_misago(request)
         request_is_to_misago = is_request_to_misago(request)
-        misago_can_handle_exception = exceptionhandler.is_misago_exception(
-            exception)
+        misago_can_handle_exception = exceptionhandler.is_misago_exception(exception)
 
 
         if request_is_to_misago and misago_can_handle_exception:
         if request_is_to_misago and misago_can_handle_exception:
             return exceptionhandler.handle_misago_exception(request, exception)
             return exceptionhandler.handle_misago_exception(request, exception)

+ 8 - 6
misago/core/migrations/0001_initial.py

@@ -6,19 +6,21 @@ from django.db import migrations, models
 
 
 class Migration(migrations.Migration):
 class Migration(migrations.Migration):
 
 
-    dependencies = [
-    ]
+    dependencies = []
 
 
     operations = [
     operations = [
         migrations.CreateModel(
         migrations.CreateModel(
             name='CacheVersion',
             name='CacheVersion',
             fields=[
             fields=[
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+                (
+                    'id', models.AutoField(
+                        verbose_name='ID', serialize=False, auto_created=True, primary_key=True
+                    )
+                ),
                 ('cache', models.CharField(max_length=128)),
                 ('cache', models.CharField(max_length=128)),
                 ('version', models.PositiveIntegerField(default=0)),
                 ('version', models.PositiveIntegerField(default=0)),
             ],
             ],
-            options={
-            },
-            bases=(models.Model,),
+            options={},
+            bases=(models.Model, ),
         ),
         ),
     ]
     ]

+ 66 - 67
misago/core/migrations/0002_basic_settings.py

@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 # -*- coding: utf-8 -*-
 from __future__ import unicode_literals
 from __future__ import unicode_literals
 
 
-from django.db import migrations, models
+from django.db import migrations
 
 
 from misago.conf.migrationutils import migrate_settings_group
 from misago.conf.migrationutils import migrate_settings_group
 
 
@@ -10,77 +10,76 @@ _ = lambda x: x
 
 
 
 
 def create_basic_settings_group(apps, schema_editor):
 def create_basic_settings_group(apps, schema_editor):
-    migrate_settings_group(apps, {
-        'key': 'basic',
-        'name': _("Basic forum settings"),
-        'description': _("Those settings control most basic properties "
-                         "of your forum like its name or description."),
-        'settings': (
-            {
-                'setting': 'forum_name',
-                'name': _("Forum name"),
-                'legend': _("General"),
-                'value': "Misago",
-                'field_extra': {
-                    'min_length': 2,
-                    'max_length': 255
+    migrate_settings_group(
+        apps, {
+            'key': 'basic',
+            'name': _("Basic forum settings"),
+            'description': _(
+                "Those settings control most basic properties "
+                "of your forum like its name or description."
+            ),
+            'settings': [
+                {
+                    'setting': 'forum_name',
+                    'name': _("Forum name"),
+                    'legend': _("General"),
+                    'value': "Misago",
+                    'field_extra': {
+                        'min_length': 2,
+                        'max_length': 255
+                    },
+                    'is_public': True,
                 },
                 },
-                'is_public': True,
-            },
-            {
-                'setting': 'forum_index_title',
-                'name': _("Index title"),
-                'description': _("You may set custon title on "
-                                 "forum index by typing it here."),
-                'legend': _("Forum index"),
-                'field_extra': {
-                    'max_length': 255
+                {
+                    'setting': 'forum_index_title',
+                    'name': _("Index title"),
+                    'description': _("You may set custon title on forum index by typing it here."),
+                    'legend': _("Forum index"),
+                    'field_extra': {
+                        'max_length': 255
+                    },
+                    'is_public': True,
                 },
                 },
-                'is_public': True,
-            },
-            {
-                'setting': 'forum_index_meta_description',
-                'name': _("Meta Description"),
-                'description': _("Short description of your forum "
-                                 "for internet crawlers."),
-                'field_extra': {
-                    'max_length': 255
+                {
+                    'setting': 'forum_index_meta_description',
+                    'name': _("Meta Description"),
+                    'description': _("Short description of your forum for internet crawlers."),
+                    'field_extra': {
+                        'max_length': 255
+                    },
                 },
                 },
-            },
-            {
-                'setting': 'forum_branding_display',
-                'name': _("Display branding"),
-                'description': _("Switch branding in forum's navbar."),
-                'legend': _("Branding"),
-                'value': True,
-                'python_type': 'bool',
-                'form_field': 'yesno',
-                'is_public': True,
-            },
-            {
-                'setting': 'forum_branding_text',
-                'name': _("Branding text"),
-                'description': _("Optional text displayed besides "
-                                 "brand image in navbar."),
-                'value': "Misago",
-                'field_extra': {
-                    'max_length': 255
+                {
+                    'setting': 'forum_branding_display',
+                    'name': _("Display branding"),
+                    'description': _("Switch branding in forum's navbar."),
+                    'legend': _("Branding"),
+                    'value': True,
+                    'python_type': 'bool',
+                    'form_field': 'yesno',
+                    'is_public': True,
                 },
                 },
-                'is_public': True,
-            },
-            {
-                'setting': 'email_footer',
-                'name': _("E-mails footer"),
-                'description': _("Optional short message included "
-                                 "at the end of e-mails sent by "
-                                 "forum"),
-                'legend': _("Forum e-mails"),
-                'field_extra': {
-                    'max_length': 255
+                {
+                    'setting': 'forum_branding_text',
+                    'name': _("Branding text"),
+                    'description': _("Optional text displayed besides brand image in navbar."),
+                    'value': "Misago",
+                    'field_extra': {
+                        'max_length': 255
+                    },
+                    'is_public': True,
                 },
                 },
-            },
-        )
-    })
+                {
+                    'setting': 'email_footer',
+                    'name': _("E-mails footer"),
+                    'description': _("Optional short message included at the end of e-mails sent by forum."),
+                    'legend': _("Forum e-mails"),
+                    'field_extra': {
+                        'max_length': 255
+                    },
+                },
+            ],
+        }
+    )
 
 
 
 
 class Migration(migrations.Migration):
 class Migration(migrations.Migration):

+ 14 - 15
misago/core/page.py

@@ -5,6 +5,7 @@ class Page(object):
     Allows for adding custom views to "sectioned" pages like
     Allows for adding custom views to "sectioned" pages like
     User Control Panel, Users List or Threads Lists
     User Control Panel, Users List or Threads Lists
     """
     """
+
     def __init__(self, name):
     def __init__(self, name):
         self._finalized = False
         self._finalized = False
         self.name = name
         self.name = name
@@ -21,17 +22,16 @@ class Page(object):
         while self._unsorted_list:
         while self._unsorted_list:
             iterations += 1
             iterations += 1
             if iterations > 512:
             if iterations > 512:
-                message = ("%s page hierarchy is invalid or too complex  to "
-                           "resolve. Sections left: %s" % self._unsorted_list)
-                raise ValueError(message)
+                message = (
+                    "%s page hierarchy is invalid or too complex  to resolve. Sections left: %s"
+                )
+                raise ValueError(message % self._unsorted_list)
 
 
             for index, section in enumerate(self._unsorted_list):
             for index, section in enumerate(self._unsorted_list):
                 if section['after']:
                 if section['after']:
-                    section_added = self._insert_section(
-                        section, after=section['after'])
+                    section_added = self._insert_section(section, after=section['after'])
                 elif section['before']:
                 elif section['before']:
-                    section_added = self._insert_section(
-                        section, before=section['before'])
+                    section_added = self._insert_section(section, before=section['before'])
                 else:
                 else:
                     section_added = self._insert_section(section)
                     section_added = self._insert_section(section)
 
 
@@ -42,7 +42,7 @@ class Page(object):
     def _insert_section(self, inserted_section, after=None, before=None):
     def _insert_section(self, inserted_section, after=None, before=None):
         if after:
         if after:
             new_sorted_list = []
             new_sorted_list = []
-            for index, section in enumerate(self._sorted_list):
+            for section in self._sorted_list:
                 new_sorted_list.append(section)
                 new_sorted_list.append(section)
                 if section['link'] == after:
                 if section['link'] == after:
                     new_sorted_list.append(inserted_section)
                     new_sorted_list.append(inserted_section)
@@ -52,7 +52,7 @@ class Page(object):
                 return False
                 return False
         elif before:
         elif before:
             new_sorted_list = []
             new_sorted_list = []
-            for index, section in enumerate(self._sorted_list):
+            for section in self._sorted_list:
                 if section['link'] == before:
                 if section['link'] == before:
                     new_sorted_list.append(inserted_section)
                     new_sorted_list.append(inserted_section)
                     new_sorted_list.append(section)
                     new_sorted_list.append(section)
@@ -66,11 +66,11 @@ class Page(object):
             self._sorted_list.append(inserted_section)
             self._sorted_list.append(inserted_section)
             return True
             return True
 
 
-    def add_section(self, link, after=None, before=None,
-                visible_if=None, get_metadata=None, **kwargs):
+    def add_section(
+            self, link, after=None, before=None, visible_if=None, get_metadata=None, **kwargs
+    ):
         if self._finalized:
         if self._finalized:
-            message = ("%s page was initialized already and no longer "
-                       "accepts new sections")
+            message = "%s page was initialized already and no longer accepts new sections"
             raise RuntimeError(message % self.name)
             raise RuntimeError(message % self.name)
 
 
         if after and before:
         if after and before:
@@ -110,8 +110,7 @@ class Page(object):
 
 
             if is_visible:
             if is_visible:
                 if section['get_metadata']:
                 if section['get_metadata']:
-                    section['metadata'] = section['get_metadata'](
-                        request, *args)
+                    section['metadata'] = section['get_metadata'](request, *args)
                 section['is_active'] = active_link.startswith(section['link'])
                 section['is_active'] = active_link.startswith(section['link'])
                 visible_sections.append(section)
                 visible_sections.append(section)
         return visible_sections
         return visible_sections

+ 7 - 16
misago/core/pgutils.py

@@ -24,8 +24,7 @@ DROP INDEX %(index_name)s
     def state_forwards(self, app_label, state):
     def state_forwards(self, app_label, state):
         pass
         pass
 
 
-    def database_forwards(self, app_label, schema_editor,
-                          from_state, to_state):
+    def database_forwards(self, app_label, schema_editor, from_state, to_state):
         model = from_state.apps.get_model(app_label, self.model)
         model = from_state.apps.get_model(app_label, self.model)
 
 
         statement = self.CREATE_SQL % {
         statement = self.CREATE_SQL % {
@@ -37,10 +36,8 @@ DROP INDEX %(index_name)s
 
 
         schema_editor.execute(statement)
         schema_editor.execute(statement)
 
 
-    def database_backwards(self, app_label, schema_editor,
-                           from_state, to_state):
-        schema_editor.execute(
-            self.REMOVE_SQL % {'index_name': self.index_name})
+    def database_backwards(self, app_label, schema_editor, from_state, to_state):
+        schema_editor.execute(self.REMOVE_SQL % {'index_name': self.index_name})
 
 
     def describe(self):
     def describe(self):
         message = "Create PostgreSQL partial index on field %s in %s for %s"
         message = "Create PostgreSQL partial index on field %s in %s for %s"
@@ -49,9 +46,7 @@ DROP INDEX %(index_name)s
 
 
 
 
 def batch_update(queryset, step=50):
 def batch_update(queryset, step=50):
-    """
-    Util because psycopg2 iterators aren't really memory effective
-    """
+    """util because psycopg2 iterators aren't memory effective in Dj<1.11"""
     paginator = Paginator(queryset.order_by('pk'), step)
     paginator = Paginator(queryset.order_by('pk'), step)
     for page_number in paginator.page_range:
     for page_number in paginator.page_range:
         for obj in paginator.page(page_number).object_list:
         for obj in paginator.page(page_number).object_list:
@@ -59,9 +54,7 @@ def batch_update(queryset, step=50):
 
 
 
 
 def batch_delete(queryset, step=50):
 def batch_delete(queryset, step=50):
-    """
-    Another util cos paginator goes bobbins when you are deleting
-    """
+    """another util cos paginator goes bobbins when you are deleting"""
     queryset_exists = True
     queryset_exists = True
     while queryset_exists:
     while queryset_exists:
         for obj in queryset[:step]:
         for obj in queryset[:step]:
@@ -85,8 +78,7 @@ DROP INDEX %(index_name)s
         self.index_name = index_name
         self.index_name = index_name
         self.condition = condition
         self.condition = condition
 
 
-    def database_forwards(self, app_label, schema_editor,
-                          from_state, to_state):
+    def database_forwards(self, app_label, schema_editor, from_state, to_state):
         model = from_state.apps.get_model(app_label, self.model)
         model = from_state.apps.get_model(app_label, self.model)
 
 
         statement = self.CREATE_SQL % {
         statement = self.CREATE_SQL % {
@@ -99,7 +91,6 @@ DROP INDEX %(index_name)s
         schema_editor.execute(statement)
         schema_editor.execute(statement)
 
 
     def describe(self):
     def describe(self):
-        message = ("Create PostgreSQL partial composite "
-                   "index on fields %s in %s for %s")
+        message = ("Create PostgreSQL partial composite index on fields %s in %s for %s")
         formats = (', '.join(self.fields), self.model_name, self.values)
         formats = (', '.join(self.fields), self.model_name, self.values)
         return message % formats
         return message % formats

+ 6 - 12
misago/core/serializers.py

@@ -7,11 +7,9 @@ class MutableFields(object):
         class Meta(cls.Meta):
         class Meta(cls.Meta):
             pass
             pass
 
 
-        Meta.fields = tuple(fields)
+        Meta.fields = list(fields)
 
 
-        return type(name, (cls,), {
-            'Meta': Meta
-        })
+        return type(name, (cls, ), {'Meta': Meta})
 
 
     @classmethod
     @classmethod
     def exclude_fields(cls, *fields):
     def exclude_fields(cls, *fields):
@@ -26,11 +24,9 @@ class MutableFields(object):
         class Meta(cls.Meta):
         class Meta(cls.Meta):
             pass
             pass
 
 
-        Meta.fields = tuple(final_fields)
+        Meta.fields = list(final_fields)
 
 
-        return type(name, (cls,), {
-            'Meta': Meta
-        })
+        return type(name, (cls, ), {'Meta': Meta})
 
 
     @classmethod
     @classmethod
     def extend_fields(cls, *fields):
     def extend_fields(cls, *fields):
@@ -45,8 +41,6 @@ class MutableFields(object):
         class Meta(cls.Meta):
         class Meta(cls.Meta):
             pass
             pass
 
 
-        Meta.fields = tuple(final_fields)
+        Meta.fields = list(final_fields)
 
 
-        return type(name, (cls,), {
-            'Meta': Meta
-        })
+        return type(name, (cls, ), {'Meta': Meta})

+ 10 - 6
misago/core/setup.py

@@ -22,9 +22,11 @@ def validate_project_name(parser, project_name):
     except ImportError:
     except ImportError:
         pass
         pass
     else:
     else:
-        parser.error("'%s' conflicts with the name of an existing "
-                     "Python module and cannot be used as a project "
-                     "name. Please try another name." % project_name)
+        parser.error((
+            "'%s' conflicts with the name of an existing "
+            "Python module and cannot be used as a project "
+            "name. Please try another name."
+        ) % project_name)
 
 
     return project_name
     return project_name
 
 
@@ -36,14 +38,16 @@ def get_misago_project_template():
 
 
 def start_misago_project():
 def start_misago_project():
     parser = OptionParser(usage="usage: %prog project_name")
     parser = OptionParser(usage="usage: %prog project_name")
-    (options, args) = parser.parse_args()
+    _, args = parser.parse_args()
 
 
     if len(args) != 1:
     if len(args) != 1:
         parser.error("project_name must be specified")
         parser.error("project_name must be specified")
 
 
     project_name = validate_project_name(parser, args[0])
     project_name = validate_project_name(parser, args[0])
 
 
-    argv = ['start-misago.py', 'startproject', project_name,
-            '--template=%s' % get_misago_project_template()]
+    argv = [
+        'start-misago.py', 'startproject', project_name,
+        '--template=%s' % get_misago_project_template()
+    ]
 
 
     management.execute_from_command_line(argv)
     management.execute_from_command_line(argv)

+ 13 - 10
misago/core/shortcuts.py

@@ -5,10 +5,15 @@ from django.http import Http404
 import six
 import six
 
 
 
 
-def paginate(object_list, page, per_page, orphans=0,
-             allow_empty_first_page=True,
-             allow_explicit_first_page=False,
-             paginator=None):
+def paginate(
+        object_list,
+        page,
+        per_page,
+        orphans=0,
+        allow_empty_first_page=True,
+        allow_explicit_first_page=False,
+        paginator=None
+):
     from django.core.paginator import Paginator, EmptyPage, InvalidPage
     from django.core.paginator import Paginator, EmptyPage, InvalidPage
     from .exceptions import ExplicitFirstPage
     from .exceptions import ExplicitFirstPage
 
 
@@ -21,9 +26,9 @@ def paginate(object_list, page, per_page, orphans=0,
 
 
     try:
     try:
         return paginator(
         return paginator(
-            object_list, per_page, orphans=orphans,
-            allow_empty_first_page=allow_empty_first_page).page(page)
-    except (EmptyPage, InvalidPage) as e:
+            object_list, per_page, orphans=orphans, allow_empty_first_page=allow_empty_first_page
+        ).page(page)
+    except (EmptyPage, InvalidPage):
         raise Http404()
         raise Http404()
 
 
 
 
@@ -62,9 +67,7 @@ def paginated_response(page, serializer=None, data=None, extra=None):
     if serializer:
     if serializer:
         results = serializer(results, many=True).data
         results = serializer(results, many=True).data
 
 
-    response_data.update({
-        'results': results
-    })
+    response_data.update({'results': results})
 
 
     if extra:
     if extra:
         response_data.update(extra)
         response_data.update(extra)

+ 1 - 1
misago/core/templatetags/misago_capture.py

@@ -31,7 +31,7 @@ def capture(parser, token):
     else:
     else:
         raise template.TemplateSyntaxError(SYNTAX_ERROR)
         raise template.TemplateSyntaxError(SYNTAX_ERROR)
 
 
-    nodelist = parser.parse(('endcapture',))
+    nodelist = parser.parse(('endcapture', ))
     parser.delete_first_token()
     parser.delete_first_token()
     return CaptureNode(variable, nodelist, trim=is_trimmed)
     return CaptureNode(variable, nodelist, trim=is_trimmed)
 
 

+ 22 - 22
misago/core/templatetags/misago_forms.py

@@ -7,25 +7,25 @@ from django.template.loader import render_to_string
 register = template.Library()
 register = template.Library()
 
 
 
 
-"""
-Form row: renders single row in form
-
-Syntax:
-{% form_row form.field %} # renders vertical field
-{% form_row form.field "col-md-3" "col-md-9" %} # renders horizontal field
-"""
 @register.tag
 @register.tag
 def form_row(parser, token):
 def form_row(parser, token):
+    """
+    Form row: renders single row in form
+
+    Syntax:
+    {% form_row form.field %} # renders vertical field
+    {% form_row form.field "col-md-3" "col-md-9" %} # renders horizontal field
+    """
     args = token.split_contents()
     args = token.split_contents()
 
 
     if len(args) < 2:
     if len(args) < 2:
-        raise template.TemplateSyntaxError(
-            "form_row tag requires at least one argument")
+        raise template.TemplateSyntaxError("form_row tag requires at least one argument")
 
 
     if len(args) == 3 or len(args) > 4:
     if len(args) == 3 or len(args) > 4:
         raise template.TemplateSyntaxError(
         raise template.TemplateSyntaxError(
             "form_row tag supports either one argument (form field) or "
             "form_row tag supports either one argument (form field) or "
-            "four arguments (form field, label class, field class)")
+            "four arguments (form field, label class, field class)"
+        )
 
 
     form_field = args[1]
     form_field = args[1]
 
 
@@ -61,18 +61,18 @@ class FormRowNode(template.Node):
             field_class = None
             field_class = None
 
 
         template_pack = crispy_forms_filters.TEMPLATE_PACK
         template_pack = crispy_forms_filters.TEMPLATE_PACK
-        return render_to_string('%s/field.html' % template_pack, {
-            'field': field,
-            'form_show_errors': True,
-            'form_show_labels': True,
-            'label_class': label_class or '',
-            'field_class': field_class or ''
-        })
-
-
-"""
-Form input: renders given field input
-"""
+        return render_to_string(
+            '%s/field.html' % template_pack, {
+                'field': field,
+                'form_show_errors': True,
+                'form_show_labels': True,
+                'label_class': label_class or '',
+                'field_class': field_class or '',
+            }
+        )
+
+
 @register.tag
 @register.tag
 def form_input(parser, token):
 def form_input(parser, token):
+    """form input: renders given field input"""
     return crispy_forms_field.crispy_field(parser, token)
     return crispy_forms_field.crispy_field(parser, token)

+ 0 - 2
misago/core/templatetags/misago_pagetitle.py

@@ -1,8 +1,6 @@
 from django import template
 from django import template
 from django.utils.translation import gettext as _
 from django.utils.translation import gettext as _
 
 
-from misago.conf import settings
-
 
 
 register = template.Library()
 register = template.Library()
 
 

+ 2 - 2
misago/core/testproject/serializers.py

@@ -5,9 +5,9 @@ class MockSerializer(serializers.Serializer):
     id = serializers.SerializerMethodField()
     id = serializers.SerializerMethodField()
 
 
     class Meta:
     class Meta:
-        fields = (
+        fields = [
             'id',
             'id',
-        )
+        ]
 
 
     def get_id(self, obj):
     def get_id(self, obj):
         return obj * 2
         return obj * 2

+ 35 - 11
misago/core/testproject/urls.py

@@ -11,24 +11,48 @@ from . import views
 admin.autodiscover()
 admin.autodiscover()
 admin.site.login_form = AdminAuthenticationForm
 admin.site.login_form = AdminAuthenticationForm
 
 
-
-
 urlpatterns = [
 urlpatterns = [
     url(r'^forum/', include('misago.urls', namespace='misago')),
     url(r'^forum/', include('misago.urls', namespace='misago')),
     url(r'^django-admin/', include(admin.site.urls)),
     url(r'^django-admin/', include(admin.site.urls)),
-
     url(r'^django-i18n.js$', javascript_catalog, name='django-i18n'),
     url(r'^django-i18n.js$', javascript_catalog, name='django-i18n'),
-
     url(r'^forum/test-mail-user/$', views.test_mail_user, name='test-mail-user'),
     url(r'^forum/test-mail-user/$', views.test_mail_user, name='test-mail-user'),
     url(r'^forum/test-mail-users/$', views.test_mail_users, name='test-mail-users'),
     url(r'^forum/test-mail-users/$', views.test_mail_users, name='test-mail-users'),
     url(r'^forum/test-pagination/$', views.test_pagination, name='test-pagination'),
     url(r'^forum/test-pagination/$', views.test_pagination, name='test-pagination'),
-    url(r'^forum/test-pagination/(?P<page>[1-9][0-9]*)/$', views.test_pagination, name='test-pagination'),
-    url(r'^forum/test-paginated-response/$', views.test_paginated_response, name='test-paginated-response'),
-    url(r'^forum/test-paginated-response-data/$', views.test_paginated_response_data, name='test-paginated-response-data'),
-    url(r'^forum/test-paginated-response-serializer/$', views.test_paginated_response_serializer, name='test-paginated-response-serializer'),
-    url(r'^forum/test-paginated-response-data-serializer/$', views.test_paginated_response_data_serializer, name='test-paginated-response-data-serializer'),
-    url(r'^forum/test-paginated-response-data-extra/$', views.test_paginated_response_data_extra, name='test-paginated-response-data-extra'),
-    url(r'^forum/test-valid-slug/(?P<slug>[a-z0-9\-]+)-(?P<pk>\d+)/$', views.validate_slug_view, name='validate-slug-view'),
+    url(
+        r'^forum/test-pagination/(?P<page>[1-9][0-9]*)/$',
+        views.test_pagination,
+        name='test-pagination'
+    ),
+    url(
+        r'^forum/test-paginated-response/$',
+        views.test_paginated_response,
+        name='test-paginated-response'
+    ),
+    url(
+        r'^forum/test-paginated-response-data/$',
+        views.test_paginated_response_data,
+        name='test-paginated-response-data'
+    ),
+    url(
+        r'^forum/test-paginated-response-serializer/$',
+        views.test_paginated_response_serializer,
+        name='test-paginated-response-serializer'
+    ),
+    url(
+        r'^forum/test-paginated-response-data-serializer/$',
+        views.test_paginated_response_data_serializer,
+        name='test-paginated-response-data-serializer'
+    ),
+    url(
+        r'^forum/test-paginated-response-data-extra/$',
+        views.test_paginated_response_data_extra,
+        name='test-paginated-response-data-extra'
+    ),
+    url(
+        r'^forum/test-valid-slug/(?P<slug>[a-z0-9\-]+)-(?P<pk>\d+)/$',
+        views.validate_slug_view,
+        name='validate-slug-view'
+    ),
     url(r'^forum/test-banned/$', views.raise_misago_banned, name='raise-misago-banned'),
     url(r'^forum/test-banned/$', views.raise_misago_banned, name='raise-misago-banned'),
     url(r'^forum/test-403/$', views.raise_misago_403, name='raise-misago-403'),
     url(r'^forum/test-403/$', views.raise_misago_403, name='raise-misago-403'),
     url(r'^forum/test-404/$', views.raise_misago_404, name='raise-misago-404'),
     url(r'^forum/test-404/$', views.raise_misago_404, name='raise-misago-404'),

+ 7 - 11
misago/core/testproject/views.py

@@ -20,19 +20,15 @@ UserModel = get_user_model()
 
 
 def test_mail_user(request):
 def test_mail_user(request):
     test_user = UserModel.objects.all().first()
     test_user = UserModel.objects.all().first()
-    mail.mail_user(request,
-                   test_user,
-                   "Misago Test Mail",
-                   "misago/emails/base")
+    mail.mail_user(request, test_user, "Misago Test Mail", "misago/emails/base")
 
 
     return HttpResponse("Mailed user!")
     return HttpResponse("Mailed user!")
 
 
 
 
 def test_mail_users(request):
 def test_mail_users(request):
-    mail.mail_users(request,
-                    UserModel.objects.iterator(),
-                    "Misago Test Spam",
-                    "misago/emails/base")
+    mail.mail_users(
+        request, UserModel.objects.iterator(), "Misago Test Spam", "misago/emails/base"
+    )
 
 
     return HttpResponse("Mailed users!")
     return HttpResponse("Mailed users!")
 
 
@@ -75,7 +71,7 @@ def test_paginated_response_data_serializer(request):
     return paginated_response(
     return paginated_response(
         page,
         page,
         data=['a', 'b', 'c', 'd'],
         data=['a', 'b', 'c', 'd'],
-        serializer=MockSerializer
+        serializer=MockSerializer,
     )
     )
 
 
 
 
@@ -89,8 +85,8 @@ def test_paginated_response_data_extra(request):
         data=['a', 'b', 'c', 'd'],
         data=['a', 'b', 'c', 'd'],
         extra={
         extra={
             'next': 'EXTRA',
             'next': 'EXTRA',
-            'lorem': 'ipsum'
-        }
+            'lorem': 'ipsum',
+        },
     )
     )
 
 
 
 

+ 135 - 52
misago/core/tests/test_apipatch.py

@@ -23,6 +23,7 @@ class ApiPatchTests(TestCase):
 
 
         def mock_function():
         def mock_function():
             pass
             pass
+
         patch.add('test-add', mock_function)
         patch.add('test-add', mock_function)
 
 
         self.assertEqual(len(patch._actions), 1)
         self.assertEqual(len(patch._actions), 1)
@@ -36,6 +37,7 @@ class ApiPatchTests(TestCase):
 
 
         def mock_function():
         def mock_function():
             pass
             pass
+
         patch.remove('test-remove', mock_function)
         patch.remove('test-remove', mock_function)
 
 
         self.assertEqual(len(patch._actions), 1)
         self.assertEqual(len(patch._actions), 1)
@@ -49,6 +51,7 @@ class ApiPatchTests(TestCase):
 
 
         def mock_function():
         def mock_function():
             pass
             pass
+
         patch.replace('test-replace', mock_function)
         patch.replace('test-replace', mock_function)
 
 
         self.assertEqual(len(patch._actions), 1)
         self.assertEqual(len(patch._actions), 1)
@@ -60,21 +63,29 @@ class ApiPatchTests(TestCase):
         """validate_action method validates action dict"""
         """validate_action method validates action dict"""
         patch = ApiPatch()
         patch = ApiPatch()
 
 
-        VALID_ACTIONS = (
-            {'op': 'add', 'path': 'test', 'value': 42},
-            {'op': 'remove', 'path': 'other-test', 'value': 'Lorem'},
-            {'op': 'replace', 'path': 'false-test', 'value': None},
-        )
+        VALID_ACTIONS = [
+            {
+                'op': 'add',
+                'path': 'test',
+                'value': 42
+            },
+            {
+                'op': 'remove',
+                'path': 'other-test',
+                'value': 'Lorem'
+            },
+            {
+                'op': 'replace',
+                'path': 'false-test',
+                'value': None
+            },
+        ]
 
 
         for action in VALID_ACTIONS:
         for action in VALID_ACTIONS:
             patch.validate_action(action)
             patch.validate_action(action)
 
 
         # undefined op
         # undefined op
-        UNSUPPORTED_ACTIONS = (
-            {},
-            {'op': ''},
-            {'no': 'op'},
-        )
+        UNSUPPORTED_ACTIONS = ({}, {'op': ''}, {'no': 'op'}, )
 
 
         for action in UNSUPPORTED_ACTIONS:
         for action in UNSUPPORTED_ACTIONS:
             try:
             try:
@@ -96,13 +107,20 @@ class ApiPatchTests(TestCase):
 
 
         # op lacking value
         # op lacking value
         try:
         try:
-            patch.validate_action({'op': 'add', 'path': 'yolo'})
+            patch.validate_action({
+                'op': 'add',
+                'path': 'yolo',
+            })
         except InvalidAction as e:
         except InvalidAction as e:
             self.assertEqual(e.args[0], u'"add" op has to specify value')
             self.assertEqual(e.args[0], u'"add" op has to specify value')
 
 
         # empty value is allowed
         # empty value is allowed
         try:
         try:
-            patch.validate_action({'op': 'add', 'path': 'yolo', 'value': ''})
+            patch.validate_action({
+                'op': 'add',
+                'path': 'yolo',
+                'value': '',
+            })
         except InvalidAction as e:
         except InvalidAction as e:
             self.assertEqual(e.args[0], u'"add" op has to specify value')
             self.assertEqual(e.args[0], u'"add" op has to specify value')
 
 
@@ -116,12 +134,14 @@ class ApiPatchTests(TestCase):
             self.assertEqual(request, 'request')
             self.assertEqual(request, 'request')
             self.assertEqual(target, mock_target)
             self.assertEqual(target, mock_target)
             return {'a': value * 2, 'b': 111}
             return {'a': value * 2, 'b': 111}
+
         patch.replace('abc', action_a)
         patch.replace('abc', action_a)
 
 
         def action_b(request, target, value):
         def action_b(request, target, value):
             self.assertEqual(request, 'request')
             self.assertEqual(request, 'request')
             self.assertEqual(target, mock_target)
             self.assertEqual(target, mock_target)
             return {'b': value * 10}
             return {'b': value * 10}
+
         patch.replace('abc', action_b)
         patch.replace('abc', action_b)
 
 
         def action_fail(request, target, value):
         def action_fail(request, target, value):
@@ -131,15 +151,15 @@ class ApiPatchTests(TestCase):
         patch.remove('c', action_fail)
         patch.remove('c', action_fail)
         patch.replace('c', action_fail)
         patch.replace('c', action_fail)
 
 
-        patch_dict = {
-            'id': 123
-        }
+        patch_dict = {'id': 123}
 
 
-        patch.dispatch_action(patch_dict, 'request', mock_target, {
-            'op': 'replace',
-            'path': 'abc',
-            'value': 5,
-        })
+        patch.dispatch_action(
+            patch_dict, 'request', mock_target, {
+                'op': 'replace',
+                'path': 'abc',
+                'value': 5,
+            }
+        )
 
 
         self.assertEqual(len(patch_dict), 3)
         self.assertEqual(len(patch_dict), 3)
         self.assertEqual(patch_dict['id'], 123)
         self.assertEqual(patch_dict['id'], 123)
@@ -155,26 +175,40 @@ class ApiPatchTests(TestCase):
                 raise Http404()
                 raise Http404()
             if value == 'perm':
             if value == 'perm':
                 raise PermissionDenied("yo ain't doing that!")
                 raise PermissionDenied("yo ain't doing that!")
+
         patch.replace('error', action_error)
         patch.replace('error', action_error)
 
 
         def action_mutate(request, target, value):
         def action_mutate(request, target, value):
             return {'value': value * 2}
             return {'value': value * 2}
+
         patch.replace('mutate', action_mutate)
         patch.replace('mutate', action_mutate)
 
 
         # dispatch requires list as an argument
         # dispatch requires list as an argument
         response = patch.dispatch(MockRequest({}), {})
         response = patch.dispatch(MockRequest({}), {})
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
-        self.assertEqual(
-            response.data['detail'],
-            "PATCH request should be list of operations")
+        self.assertEqual(response.data['detail'], "PATCH request should be list of operations")
 
 
         # valid dispatch
         # valid dispatch
-        response = patch.dispatch(MockRequest([
-            {'op': 'replace', 'path': 'mutate', 'value': 2},
-            {'op': 'replace', 'path': 'mutate', 'value': 6},
-            {'op': 'replace', 'path': 'mutate', 'value': 7},
-        ]), MockObject(13))
+        response = patch.dispatch(
+            MockRequest([
+                {
+                    'op': 'replace',
+                    'path': 'mutate',
+                    'value': 2,
+                },
+                {
+                    'op': 'replace',
+                    'path': 'mutate',
+                    'value': 6,
+                },
+                {
+                    'op': 'replace',
+                    'path': 'mutate',
+                    'value': 7,
+                },
+            ]), MockObject(13)
+        )
 
 
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
@@ -186,47 +220,97 @@ class ApiPatchTests(TestCase):
         self.assertEqual(response.data['value'], 14)
         self.assertEqual(response.data['value'], 14)
 
 
         # invalid action in dispatch
         # invalid action in dispatch
-        response = patch.dispatch(MockRequest([
-            {'op': 'replace', 'path': 'mutate', 'value': 2},
-            {'op': 'replace', 'path': 'mutate', 'value': 6},
-            {'op': 'replace'},
-            {'op': 'replace', 'path': 'mutate', 'value': 7},
-        ]), MockObject(13))
+        response = patch.dispatch(
+            MockRequest([
+                {
+                    'op': 'replace',
+                    'path': 'mutate',
+                    'value': 2,
+                },
+                {
+                    'op': 'replace',
+                    'path': 'mutate',
+                    'value': 6,
+                },
+                {
+                    'op': 'replace',
+                },
+                {
+                    'op': 'replace',
+                    'path': 'mutate',
+                    'value': 7,
+                },
+            ]), MockObject(13)
+        )
 
 
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         self.assertEqual(len(response.data['detail']), 3)
         self.assertEqual(len(response.data['detail']), 3)
         self.assertEqual(response.data['detail'][0], 'ok')
         self.assertEqual(response.data['detail'][0], 'ok')
         self.assertEqual(response.data['detail'][1], 'ok')
         self.assertEqual(response.data['detail'][1], 'ok')
-        self.assertEqual(
-            response.data['detail'][2], '"replace" op has to specify path')
+        self.assertEqual(response.data['detail'][2], '"replace" op has to specify path')
         self.assertEqual(response.data['id'], 13)
         self.assertEqual(response.data['id'], 13)
         self.assertEqual(response.data['value'], 12)
         self.assertEqual(response.data['value'], 12)
 
 
         # action in dispatch raised 404
         # action in dispatch raised 404
-        response = patch.dispatch(MockRequest([
-            {'op': 'replace', 'path': 'mutate', 'value': 2},
-            {'op': 'replace', 'path': 'error', 'value': '404'},
-            {'op': 'replace', 'path': 'mutate', 'value': 6},
-            {'op': 'replace', 'path': 'mutate', 'value': 7},
-        ]), MockObject(13))
+        response = patch.dispatch(
+            MockRequest([
+                {
+                    'op': 'replace',
+                    'path': 'mutate',
+                    'value': 2,
+                },
+                {
+                    'op': 'replace',
+                    'path': 'error',
+                    'value': '404',
+                },
+                {
+                    'op': 'replace',
+                    'path': 'mutate',
+                    'value': 6,
+                },
+                {
+                    'op': 'replace',
+                    'path': 'mutate',
+                    'value': 7,
+                },
+            ]), MockObject(13)
+        )
 
 
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         self.assertEqual(len(response.data['detail']), 2)
         self.assertEqual(len(response.data['detail']), 2)
         self.assertEqual(response.data['detail'][0], 'ok')
         self.assertEqual(response.data['detail'][0], 'ok')
-        self.assertEqual(
-            response.data['detail'][1], "NOT FOUND")
+        self.assertEqual(response.data['detail'][1], "NOT FOUND")
         self.assertEqual(response.data['id'], 13)
         self.assertEqual(response.data['id'], 13)
         self.assertEqual(response.data['value'], 4)
         self.assertEqual(response.data['value'], 4)
 
 
         # action in dispatch raised perm denied
         # action in dispatch raised perm denied
-        response = patch.dispatch(MockRequest([
-            {'op': 'replace', 'path': 'mutate', 'value': 2},
-            {'op': 'replace', 'path': 'mutate', 'value': 6},
-            {'op': 'replace', 'path': 'mutate', 'value': 9},
-            {'op': 'replace', 'path': 'error', 'value': 'perm'},
-        ]), MockObject(13))
+        response = patch.dispatch(
+            MockRequest([
+                {
+                    'op': 'replace',
+                    'path': 'mutate',
+                    'value': 2,
+                },
+                {
+                    'op': 'replace',
+                    'path': 'mutate',
+                    'value': 6,
+                },
+                {
+                    'op': 'replace',
+                    'path': 'mutate',
+                    'value': 9,
+                },
+                {
+                    'op': 'replace',
+                    'path': 'error',
+                    'value': 'perm',
+                },
+            ]), MockObject(13)
+        )
 
 
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
@@ -234,7 +318,6 @@ class ApiPatchTests(TestCase):
         self.assertEqual(response.data['detail'][0], 'ok')
         self.assertEqual(response.data['detail'][0], 'ok')
         self.assertEqual(response.data['detail'][1], 'ok')
         self.assertEqual(response.data['detail'][1], 'ok')
         self.assertEqual(response.data['detail'][2], 'ok')
         self.assertEqual(response.data['detail'][2], 'ok')
-        self.assertEqual(
-            response.data['detail'][3], "yo ain't doing that!")
+        self.assertEqual(response.data['detail'][3], "yo ain't doing that!")
         self.assertEqual(response.data['id'], 13)
         self.assertEqual(response.data['id'], 13)
         self.assertEqual(response.data['value'], 18)
         self.assertEqual(response.data['value'], 18)

+ 1 - 2
misago/core/tests/test_cachebuster.py

@@ -1,5 +1,4 @@
-from misago.core import cachebuster, threadstore
-from misago.core.cache import cache
+from misago.core import cachebuster
 from misago.core.models import CacheVersion
 from misago.core.models import CacheVersion
 from misago.core.testutils import MisagoTestCase
 from misago.core.testutils import MisagoTestCase
 
 

+ 2 - 2
misago/core/tests/test_checks.py

@@ -5,11 +5,11 @@ from django.test import TestCase
 from misago.core import SUPPORTED_ENGINES, check_db_engine
 from misago.core import SUPPORTED_ENGINES, check_db_engine
 
 
 
 
-INVALID_ENGINES = (
+INVALID_ENGINES = [
     'django.db.backends.sqlite3',
     'django.db.backends.sqlite3',
     'django.db.backends.mysql',
     'django.db.backends.mysql',
     'django.db.backends.oracle',
     'django.db.backends.oracle',
-)
+]
 
 
 
 
 class TestCheckDBEngine(TestCase):
 class TestCheckDBEngine(TestCase):

+ 1 - 2
misago/core/tests/test_common_middleware_redirect.py

@@ -10,5 +10,4 @@ class CommonMiddlewareRedirectTests(AuthenticatedUserTestCase):
         """
         """
         response = self.client.get(self.user.get_absolute_url()[:-1])
         response = self.client.get(self.user.get_absolute_url()[:-1])
         self.assertEqual(response.status_code, 301)
         self.assertEqual(response.status_code, 301)
-        self.assertTrue(
-            response['location'].endswith(self.user.get_absolute_url()))
+        self.assertTrue(response['location'].endswith(self.user.get_absolute_url()))

+ 42 - 28
misago/core/tests/test_context_processors.py

@@ -27,46 +27,58 @@ class MomentjsLocaleTests(TestCase):
     def test_momentjs_locale(self):
     def test_momentjs_locale(self):
         """momentjs_locale adds MOMENTJS_LOCALE_URL to context"""
         """momentjs_locale adds MOMENTJS_LOCALE_URL to context"""
         with translation.override('no-no'):
         with translation.override('no-no'):
-            self.assertEqual(context_processors.momentjs_locale(True), {
-                'MOMENTJS_LOCALE_URL': None,
-            })
+            self.assertEqual(
+                context_processors.momentjs_locale(True), {
+                    'MOMENTJS_LOCALE_URL': None,
+                }
+            )
 
 
         with translation.override('en-us'):
         with translation.override('en-us'):
-            self.assertEqual(context_processors.momentjs_locale(True), {
-                'MOMENTJS_LOCALE_URL': None,
-            })
+            self.assertEqual(
+                context_processors.momentjs_locale(True), {
+                    'MOMENTJS_LOCALE_URL': None,
+                }
+            )
 
 
         with translation.override('de'):
         with translation.override('de'):
-            self.assertEqual(context_processors.momentjs_locale(True), {
-                'MOMENTJS_LOCALE_URL': 'misago/momentjs/de.js',
-            })
+            self.assertEqual(
+                context_processors.momentjs_locale(True), {
+                    'MOMENTJS_LOCALE_URL': 'misago/momentjs/de.js',
+                }
+            )
 
 
         with translation.override('pl-de'):
         with translation.override('pl-de'):
-            self.assertEqual(context_processors.momentjs_locale(True), {
-                'MOMENTJS_LOCALE_URL': 'misago/momentjs/pl.js',
-            })
+            self.assertEqual(
+                context_processors.momentjs_locale(True), {
+                    'MOMENTJS_LOCALE_URL': 'misago/momentjs/pl.js',
+                }
+            )
 
 
 
 
 class SiteAddressTests(TestCase):
 class SiteAddressTests(TestCase):
     def test_site_address_for_http(self):
     def test_site_address_for_http(self):
         """Correct SITE_ADDRESS set for HTTP request"""
         """Correct SITE_ADDRESS set for HTTP request"""
         mock_request = MockRequest(False, 'somewhere.com')
         mock_request = MockRequest(False, 'somewhere.com')
-        self.assertEqual(context_processors.site_address(mock_request), {
-            'REQUEST_PATH': '/',
-            'SITE_ADDRESS': 'http://somewhere.com',
-            'SITE_HOST': 'somewhere.com',
-            'SITE_PROTOCOL': 'http',
-        })
+        self.assertEqual(
+            context_processors.site_address(mock_request), {
+                'REQUEST_PATH': '/',
+                'SITE_ADDRESS': 'http://somewhere.com',
+                'SITE_HOST': 'somewhere.com',
+                'SITE_PROTOCOL': 'http',
+            }
+        )
 
 
     def test_site_address_for_https(self):
     def test_site_address_for_https(self):
         """Correct SITE_ADDRESS set for HTTPS request"""
         """Correct SITE_ADDRESS set for HTTPS request"""
         mock_request = MockRequest(True, 'somewhere.com')
         mock_request = MockRequest(True, 'somewhere.com')
-        self.assertEqual(context_processors.site_address(mock_request), {
-            'REQUEST_PATH': '/',
-            'SITE_ADDRESS': 'https://somewhere.com',
-            'SITE_HOST': 'somewhere.com',
-            'SITE_PROTOCOL': 'https',
-        })
+        self.assertEqual(
+            context_processors.site_address(mock_request), {
+                'REQUEST_PATH': '/',
+                'SITE_ADDRESS': 'https://somewhere.com',
+                'SITE_HOST': 'somewhere.com',
+                'SITE_PROTOCOL': 'https',
+            }
+        )
 
 
 
 
 class FrontendContextTests(TestCase):
 class FrontendContextTests(TestCase):
@@ -76,11 +88,13 @@ class FrontendContextTests(TestCase):
         mock_request.include_frontend_context = True
         mock_request.include_frontend_context = True
         mock_request.frontend_context = {'someValue': 'Something'}
         mock_request.frontend_context = {'someValue': 'Something'}
 
 
-        self.assertEqual(context_processors.frontend_context(mock_request), {
-            'frontend_context': {
-                'someValue': 'Something'
+        self.assertEqual(
+            context_processors.frontend_context(mock_request), {
+                'frontend_context': {
+                    'someValue': 'Something',
+                },
             }
             }
-        })
+        )
 
 
         mock_request.include_frontend_context = False
         mock_request.include_frontend_context = False
         self.assertEqual(context_processors.frontend_context(mock_request), {})
         self.assertEqual(context_processors.frontend_context(mock_request), {})

+ 0 - 1
misago/core/tests/test_decorators.py

@@ -1,4 +1,3 @@
-from django.contrib.auth import get_user_model
 from django.test import TestCase, override_settings
 from django.test import TestCase, override_settings
 from django.urls import reverse
 from django.urls import reverse
 
 

+ 1 - 1
misago/core/tests/test_deprecations.py

@@ -1,6 +1,6 @@
 import warnings
 import warnings
 
 
-from django.test import TestCase, override_settings
+from django.test import TestCase
 from django.utils import six
 from django.utils import six
 
 
 from misago.core.deprecations import RemovedInMisagoWarning, warn
 from misago.core.deprecations import RemovedInMisagoWarning, warn

+ 2 - 6
misago/core/tests/test_errorpages.py

@@ -11,21 +11,17 @@ class CSRFErrorViewTests(TestCase):
     def test_csrf_failure(self):
     def test_csrf_failure(self):
         """csrf_failure error page has no show-stoppers"""
         """csrf_failure error page has no show-stoppers"""
         csrf_client = Client(enforce_csrf_checks=True)
         csrf_client = Client(enforce_csrf_checks=True)
-        response = csrf_client.post(reverse('misago:index'), data={
-            'eric': 'fish'
-        })
+        response = csrf_client.post(reverse('misago:index'), data={'eric': 'fish'})
         self.assertContains(response, "Request blocked", status_code=403)
         self.assertContains(response, "Request blocked", status_code=403)
 
 
 
 
-
 @override_settings(ROOT_URLCONF='misago.core.testproject.urls')
 @override_settings(ROOT_URLCONF='misago.core.testproject.urls')
 class ErrorPageViewsTests(TestCase):
 class ErrorPageViewsTests(TestCase):
     def test_banned_returns_403(self):
     def test_banned_returns_403(self):
         """banned error page has no show-stoppers"""
         """banned error page has no show-stoppers"""
         response = self.client.get(reverse('raise-misago-banned'))
         response = self.client.get(reverse('raise-misago-banned'))
         self.assertContains(response, "misago:error-banned", status_code=403)
         self.assertContains(response, "misago:error-banned", status_code=403)
-        self.assertContains(
-            response, encode_json_html("<p>Banned for test!</p>"), status_code=403)
+        self.assertContains(response, encode_json_html("<p>Banned for test!</p>"), status_code=403)
 
 
     def test_permission_denied_returns_403(self):
     def test_permission_denied_returns_403(self):
         """permission_denied error page has no show-stoppers"""
         """permission_denied error page has no show-stoppers"""

+ 10 - 18
misago/core/tests/test_exceptionhandlers.py

@@ -8,28 +8,23 @@ from misago.core.exceptions import Banned
 from misago.users.models import Ban
 from misago.users.models import Ban
 
 
 
 
-INVALID_EXCEPTIONS = (
+INVALID_EXCEPTIONS = [
     django_exceptions.ObjectDoesNotExist,
     django_exceptions.ObjectDoesNotExist,
     django_exceptions.ViewDoesNotExist,
     django_exceptions.ViewDoesNotExist,
     TypeError,
     TypeError,
     ValueError,
     ValueError,
     KeyError,
     KeyError,
-)
+]
 
 
 
 
 class IsMisagoExceptionTests(TestCase):
 class IsMisagoExceptionTests(TestCase):
     def test_is_misago_exception_true_for_handled_exceptions(self):
     def test_is_misago_exception_true_for_handled_exceptions(self):
-        """
-        exceptionhandler.is_misago_exception recognizes handled exceptions
-        """
+        """exceptionhandler.is_misago_exception recognizes handled exceptions"""
         for exception in exceptionhandler.HANDLED_EXCEPTIONS:
         for exception in exceptionhandler.HANDLED_EXCEPTIONS:
             self.assertTrue(exceptionhandler.is_misago_exception(exception()))
             self.assertTrue(exceptionhandler.is_misago_exception(exception()))
 
 
     def test_is_misago_exception_false_for_not_handled_exceptions(self):
     def test_is_misago_exception_false_for_not_handled_exceptions(self):
-        """
-        exceptionhandler.is_misago_exception fails to recognize other
-        exceptions
-        """
+        """exceptionhandler.is_misago_exception fails to recognize other exceptions"""
         for exception in INVALID_EXCEPTIONS:
         for exception in INVALID_EXCEPTIONS:
             self.assertFalse(exceptionhandler.is_misago_exception(exception()))
             self.assertFalse(exceptionhandler.is_misago_exception(exception()))
 
 
@@ -37,8 +32,9 @@ class IsMisagoExceptionTests(TestCase):
 class GetExceptionHandlerTests(TestCase):
 class GetExceptionHandlerTests(TestCase):
     def test_exception_handlers_list(self):
     def test_exception_handlers_list(self):
         """HANDLED_EXCEPTIONS length matches that of EXCEPTION_HANDLERS"""
         """HANDLED_EXCEPTIONS length matches that of EXCEPTION_HANDLERS"""
-        self.assertEqual(len(exceptionhandler.HANDLED_EXCEPTIONS),
-                         len(exceptionhandler.EXCEPTION_HANDLERS))
+        self.assertEqual(
+            len(exceptionhandler.HANDLED_EXCEPTIONS), len(exceptionhandler.EXCEPTION_HANDLERS)
+        )
 
 
     def test_get_exception_handler_for_handled_exceptions(self):
     def test_get_exception_handler_for_handled_exceptions(self):
         """Exception handler has correct handler for every Misago exception"""
         """Exception handler has correct handler for every Misago exception"""
@@ -57,18 +53,14 @@ class HandleAPIExceptionTests(TestCase):
         """banned exception is correctly handled"""
         """banned exception is correctly handled"""
         ban = Ban(user_message="This is test ban!")
         ban = Ban(user_message="This is test ban!")
 
 
-        response = exceptionhandler.handle_api_exception(
-            Banned(ban), None)
+        response = exceptionhandler.handle_api_exception(Banned(ban), None)
 
 
         self.assertEqual(response.status_code, 403)
         self.assertEqual(response.status_code, 403)
-        self.assertEqual(
-            response.data['ban']['message']['html'],
-            "<p>This is test ban!</p>")
+        self.assertEqual(response.data['ban']['message']['html'], "<p>This is test ban!</p>")
 
 
     def test_permission_denied(self):
     def test_permission_denied(self):
         """permission denied exception is correctly handled"""
         """permission denied exception is correctly handled"""
-        response = exceptionhandler.handle_api_exception(
-            PermissionDenied(), None)
+        response = exceptionhandler.handle_api_exception(PermissionDenied(), None)
         self.assertEqual(response.status_code, 403)
         self.assertEqual(response.status_code, 403)
         self.assertEqual(response.data['detail'], "Permission denied.")
         self.assertEqual(response.data['detail'], "Permission denied.")
 
 

+ 1 - 4
misago/core/tests/test_mailer.py

@@ -20,10 +20,7 @@ class MisagoMailerTests(TestCase):
 
 
         # assert that url to user's avatar is valid
         # assert that url to user's avatar is valid
         html_body = mail.outbox[0].alternatives[0][0]
         html_body = mail.outbox[0].alternatives[0][0]
-        user_avatar_url = reverse('misago:user-avatar', kwargs={
-            'pk': user.pk,
-            'size': 32
-        })
+        user_avatar_url = reverse('misago:user-avatar', kwargs={'pk': user.pk, 'size': 32})
 
 
         self.assertIn(user_avatar_url, html_body)
         self.assertIn(user_avatar_url, html_body)
 
 

+ 2 - 7
misago/core/tests/test_migrationutils.py

@@ -7,18 +7,13 @@ from misago.core.models import CacheVersion
 
 
 class CacheBusterUtilsTests(TestCase):
 class CacheBusterUtilsTests(TestCase):
     def test_cachebuster_register_cache(self):
     def test_cachebuster_register_cache(self):
-        """
-        cachebuster_register_cache registers cache on migration successfully
-        """
+        """cachebuster_register_cache registers cache on migration successfully"""
         cache_name = 'eric_licenses'
         cache_name = 'eric_licenses'
         migrationutils.cachebuster_register_cache(apps, cache_name)
         migrationutils.cachebuster_register_cache(apps, cache_name)
         CacheVersion.objects.get(cache=cache_name)
         CacheVersion.objects.get(cache=cache_name)
 
 
     def test_cachebuster_unregister_cache(self):
     def test_cachebuster_unregister_cache(self):
-        """
-        cachebuster_unregister_cache removes cache on migration successfully
-        """
-
+        """cachebuster_unregister_cache removes cache on migration successfully"""
         cache_name = 'eric_licenses'
         cache_name = 'eric_licenses'
         migrationutils.cachebuster_register_cache(apps, cache_name)
         migrationutils.cachebuster_register_cache(apps, cache_name)
         CacheVersion.objects.get(cache=cache_name)
         CacheVersion.objects.get(cache=cache_name)

+ 4 - 19
misago/core/tests/test_momentjs.py

@@ -1,4 +1,3 @@
-from django.conf import settings
 from django.test import TestCase
 from django.test import TestCase
 
 
 from misago.core.momentjs import clean_language_name, get_locale_url
 from misago.core.momentjs import clean_language_name, get_locale_url
@@ -7,14 +6,14 @@ from misago.core.momentjs import clean_language_name, get_locale_url
 class MomentJSTests(TestCase):
 class MomentJSTests(TestCase):
     def test_clean_language_name(self):
     def test_clean_language_name(self):
         """clean_language_name returns valid name"""
         """clean_language_name returns valid name"""
-        TEST_CASES = (
+        TEST_CASES = [
             ('AF', 'af'),
             ('AF', 'af'),
             ('ar-SA', 'ar-sa'),
             ('ar-SA', 'ar-sa'),
             ('de', 'de'),
             ('de', 'de'),
             ('de-NO', 'de'),
             ('de-NO', 'de'),
             ('pl-pl', 'pl'),
             ('pl-pl', 'pl'),
             ('zz', None),
             ('zz', None),
-        )
+        ]
 
 
         for dirty, clean in TEST_CASES:
         for dirty, clean in TEST_CASES:
             self.assertEqual(clean_language_name(dirty), clean)
             self.assertEqual(clean_language_name(dirty), clean)
@@ -22,27 +21,13 @@ class MomentJSTests(TestCase):
     def test_get_locale_path(self):
     def test_get_locale_path(self):
         """get_locale_path returns path to locale or null if it doesnt exist"""
         """get_locale_path returns path to locale or null if it doesnt exist"""
         EXISTING_LOCALES = (
         EXISTING_LOCALES = (
-            'af',
-            'ar-sa',
-            'ar-sasa',
-            'de',
-            'et',
-            'pl',
-            'pl-pl',
-            'ru',
-            'pt-br',
-            'zh-tw'
+            'af', 'ar-sa', 'ar-sasa', 'de', 'et', 'pl', 'pl-pl', 'ru', 'pt-br', 'zh-tw'
         )
         )
 
 
         for language in EXISTING_LOCALES:
         for language in EXISTING_LOCALES:
             self.assertIsNotNone(get_locale_url(language))
             self.assertIsNotNone(get_locale_url(language))
 
 
-        NONEXISTING_LOCALES = (
-            'ga',
-            'en',
-            'en-us',
-            'martian',
-        )
+        NONEXISTING_LOCALES = ('ga', 'en', 'en-us', 'martian', )
 
 
         for language in NONEXISTING_LOCALES:
         for language in NONEXISTING_LOCALES:
             self.assertIsNone(get_locale_url(language))
             self.assertIsNone(get_locale_url(language))

+ 6 - 3
misago/core/tests/test_page.py

@@ -12,16 +12,19 @@ class SiteTests(TestCase):
         self.page.add_section(
         self.page.add_section(
             link='misago:user-posts',
             link='misago:user-posts',
             name='Posts',
             name='Posts',
-            after='misago:user-threads')
+            after='misago:user-threads',
+        )
 
 
         self.page.add_section(
         self.page.add_section(
             link='misago:user-threads',
             link='misago:user-threads',
-            name='Threads')
+            name='Threads',
+        )
 
 
         self.page.add_section(
         self.page.add_section(
             link='misago:user-follows',
             link='misago:user-follows',
             name='Follows',
             name='Follows',
-            before='misago:user-posts')
+            before='misago:user-posts',
+        )
 
 
         self.page.assert_is_finalized()
         self.page.assert_is_finalized()
 
 

+ 22 - 23
misago/core/tests/test_serializers.py

@@ -14,22 +14,21 @@ class MutableFieldsSerializerTests(TestCase):
         category = Category.objects.get(slug='first-category')
         category = Category.objects.get(slug='first-category')
         thread = testutils.post_thread(category=category)
         thread = testutils.post_thread(category=category)
 
 
-        fields = ('id', 'title', 'replies', 'last_poster_name')
+        fields = ['id', 'title', 'replies', 'last_poster_name']
 
 
         serializer = TestSerializer.subset_fields(*fields)
         serializer = TestSerializer.subset_fields(*fields)
-        self.assertEqual(
-            serializer.__name__,
-            'TestSerializerIdTitleRepliesLastPosterNameSubset'
-        )
+        self.assertEqual(serializer.__name__, 'TestSerializerIdTitleRepliesLastPosterNameSubset')
         self.assertEqual(serializer.Meta.fields, fields)
         self.assertEqual(serializer.Meta.fields, fields)
 
 
         serialized_thread = serializer(thread).data
         serialized_thread = serializer(thread).data
-        self.assertEqual(serialized_thread, {
-            'id': thread.id,
-            'title': thread.title,
-            'replies': thread.replies,
-            'last_poster_name': thread.last_poster_name,
-        })
+        self.assertEqual(
+            serialized_thread, {
+                'id': thread.id,
+                'title': thread.title,
+                'replies': thread.replies,
+                'last_poster_name': thread.last_poster_name,
+            }
+        )
 
 
         self.assertFalse(TestSerializer.Meta.fields == serializer.Meta.fields)
         self.assertFalse(TestSerializer.Meta.fields == serializer.Meta.fields)
 
 
@@ -38,19 +37,21 @@ class MutableFieldsSerializerTests(TestCase):
         category = Category.objects.get(slug='first-category')
         category = Category.objects.get(slug='first-category')
         thread = testutils.post_thread(category=category)
         thread = testutils.post_thread(category=category)
 
 
-        kept_fields = ('id', 'title', 'weight')
-        removed_fields = tuple(set(TestSerializer.Meta.fields) - set(kept_fields))
+        kept_fields = ['id', 'title', 'weight']
+        removed_fields = list(set(TestSerializer.Meta.fields) - set(kept_fields))
 
 
         serializer = TestSerializer.exclude_fields(*removed_fields)
         serializer = TestSerializer.exclude_fields(*removed_fields)
         self.assertEqual(serializer.__name__, 'TestSerializerIdTitleWeightSubset')
         self.assertEqual(serializer.__name__, 'TestSerializerIdTitleWeightSubset')
         self.assertEqual(serializer.Meta.fields, kept_fields)
         self.assertEqual(serializer.Meta.fields, kept_fields)
 
 
         serialized_thread = serializer(thread).data
         serialized_thread = serializer(thread).data
-        self.assertEqual(serialized_thread, {
-            'id': thread.id,
-            'title': thread.title,
-            'weight': thread.weight,
-        })
+        self.assertEqual(
+            serialized_thread, {
+                'id': thread.id,
+                'title': thread.title,
+                'weight': thread.weight,
+            }
+        )
 
 
         self.assertFalse(TestSerializer.Meta.fields == serializer.Meta.fields)
         self.assertFalse(TestSerializer.Meta.fields == serializer.Meta.fields)
 
 
@@ -59,9 +60,7 @@ class MutableFieldsSerializerTests(TestCase):
         category = Category.objects.get(slug='first-category')
         category = Category.objects.get(slug='first-category')
         thread = testutils.post_thread(category=category)
         thread = testutils.post_thread(category=category)
 
 
-        added_fields = ('category',)
-
-        serializer = TestSerializer.extend_fields(*added_fields)
+        serializer = TestSerializer.extend_fields('category')
 
 
         serialized_thread = serializer(thread).data
         serialized_thread = serializer(thread).data
         self.assertEqual(serialized_thread['category'], category.pk)
         self.assertEqual(serialized_thread['category'], category.pk)
@@ -72,7 +71,7 @@ class TestSerializer(serializers.ModelSerializer, MutableFields):
 
 
     class Meta:
     class Meta:
         model = Thread
         model = Thread
-        fields = (
+        fields = [
             'id',
             'id',
             'title',
             'title',
             'replies',
             'replies',
@@ -86,4 +85,4 @@ class TestSerializer(serializers.ModelSerializer, MutableFields):
             'is_hidden',
             'is_hidden',
             'is_closed',
             'is_closed',
             'weight',
             'weight',
-        )
+        ]

+ 4 - 4
misago/core/tests/test_setup.py

@@ -33,9 +33,9 @@ class SetupTests(TestCase):
 
 
     def test_get_misago_project_template(self):
     def test_get_misago_project_template(self):
         """get_misago_project_template returns correct path to template"""
         """get_misago_project_template returns correct path to template"""
-        misago_path = os.path.dirname(
-            os.path.dirname(os.path.dirname(__file__)))
+        misago_path = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
         test_project_path = os.path.join(misago_path, 'project_template')
         test_project_path = os.path.join(misago_path, 'project_template')
 
 
-        self.assertEqual(smart_str(setup.get_misago_project_template()),
-                         smart_str(test_project_path))
+        self.assertEqual(
+            smart_str(setup.get_misago_project_template()), smart_str(test_project_path)
+        )

+ 117 - 91
misago/core/tests/test_shortcuts.py

@@ -9,26 +9,22 @@ from misago.core.shortcuts import get_int_or_404
 class PaginateTests(TestCase):
 class PaginateTests(TestCase):
     def test_valid_page_handling(self):
     def test_valid_page_handling(self):
         """Valid page number causes no errors"""
         """Valid page number causes no errors"""
-        response = self.client.get(
-            reverse('test-pagination', kwargs={'page': 2}))
+        response = self.client.get(reverse('test-pagination', kwargs={'page': 2}))
         self.assertEqual("5,6,7,8,9", response.content.decode())
         self.assertEqual("5,6,7,8,9", response.content.decode())
 
 
     def test_invalid_page_handling(self):
     def test_invalid_page_handling(self):
         """Invalid page number results in 404 error"""
         """Invalid page number results in 404 error"""
-        response = self.client.get(
-            reverse('test-pagination', kwargs={'page': 42}))
+        response = self.client.get(reverse('test-pagination', kwargs={'page': 42}))
         self.assertEqual(response.status_code, 404)
         self.assertEqual(response.status_code, 404)
 
 
     def test_implicit_page_handling(self):
     def test_implicit_page_handling(self):
         """Implicit page number causes no errors"""
         """Implicit page number causes no errors"""
-        response = self.client.get(
-            reverse('test-pagination'))
+        response = self.client.get(reverse('test-pagination'))
         self.assertEqual("0,1,2,3,4", response.content.decode())
         self.assertEqual("0,1,2,3,4", response.content.decode())
 
 
     def test_explicit_page_handling(self):
     def test_explicit_page_handling(self):
         """Explicit page number results in redirect"""
         """Explicit page number results in redirect"""
-        response = self.client.get(
-            reverse('test-pagination', kwargs={'page': 1}))
+        response = self.client.get(reverse('test-pagination', kwargs={'page': 1}))
         valid_url = "/forum/test-pagination/"
         valid_url = "/forum/test-pagination/"
         self.assertEqual(response['Location'], valid_url)
         self.assertEqual(response['Location'], valid_url)
 
 
@@ -37,18 +33,22 @@ class PaginateTests(TestCase):
 class ValidateSlugTests(TestCase):
 class ValidateSlugTests(TestCase):
     def test_valid_slug_handling(self):
     def test_valid_slug_handling(self):
         """Valid slug causes no interruption in view processing"""
         """Valid slug causes no interruption in view processing"""
-        response = self.client.get(reverse('validate-slug-view', kwargs={
-            'slug': 'eric-the-fish',
-            'pk': 1,
-        }))
+        response = self.client.get(
+            reverse('validate-slug-view', kwargs={
+                'slug': 'eric-the-fish',
+                'pk': 1,
+            })
+        )
         self.assertContains(response, "Allright")
         self.assertContains(response, "Allright")
 
 
     def test_invalid_slug_handling(self):
     def test_invalid_slug_handling(self):
         """Invalid slug returns in redirect to valid page"""
         """Invalid slug returns in redirect to valid page"""
-        response = self.client.get(reverse('validate-slug-view', kwargs={
-            'slug': 'lion-the-eric',
-            'pk': 1,
-        }))
+        response = self.client.get(
+            reverse('validate-slug-view', kwargs={
+                'slug': 'lion-the-eric',
+                'pk': 1,
+            })
+        )
 
 
         valid_url = "/forum/test-valid-slug/eric-the-fish-1/"
         valid_url = "/forum/test-valid-slug/eric-the-fish-1/"
         self.assertEqual(response['Location'], valid_url)
         self.assertEqual(response['Location'], valid_url)
@@ -57,19 +57,19 @@ class ValidateSlugTests(TestCase):
 class GetIntOr404Tests(TestCase):
 class GetIntOr404Tests(TestCase):
     def test_valid_inputs(self):
     def test_valid_inputs(self):
         """get_int_or_404 returns int for valid values"""
         """get_int_or_404 returns int for valid values"""
-        VALID_VALUES = (
+        VALID_VALUES = [
             ('0', 0),
             ('0', 0),
             ('123', 123),
             ('123', 123),
             ('000123', 123),
             ('000123', 123),
             ('1', 1),
             ('1', 1),
-        )
+        ]
 
 
         for value, result in VALID_VALUES:
         for value, result in VALID_VALUES:
             self.assertEqual(get_int_or_404(value), result)
             self.assertEqual(get_int_or_404(value), result)
 
 
     def test_invalid_inputs(self):
     def test_invalid_inputs(self):
         """get_int_or_404 raises Http404 for invalid values"""
         """get_int_or_404 raises Http404 for invalid values"""
-        INVALID_VALUES = (
+        INVALID_VALUES = [
             None,
             None,
             '',
             '',
             'bob',
             'bob',
@@ -79,7 +79,7 @@ class GetIntOr404Tests(TestCase):
             '12.321',
             '12.321',
             '.4',
             '.4',
             '5.',
             '5.',
-        )
+        ]
 
 
         for value in INVALID_VALUES:
         for value in INVALID_VALUES:
             with self.assertRaises(Http404):
             with self.assertRaises(Http404):
@@ -92,94 +92,120 @@ class PaginatedResponseTests(TestCase):
         """utility returns response for only page arg"""
         """utility returns response for only page arg"""
         response = self.client.get(reverse('test-paginated-response'))
         response = self.client.get(reverse('test-paginated-response'))
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
-        self.assertEqual(response.json(), {
-            'results': [i + 10 for i in range(10)],
-            'page': 2,
-            'pages': 10,
-            'count': 100,
-            'first': 1,
-            'previous': 1,
-            'next': 3,
-            'last': 10,
-            'before': 10,
-            'more': 80,
-        })
+        self.assertEqual(
+            response.json(), {
+                'results': [i + 10 for i in range(10)],
+                'page': 2,
+                'pages': 10,
+                'count': 100,
+                'first': 1,
+                'previous': 1,
+                'next': 3,
+                'last': 10,
+                'before': 10,
+                'more': 80,
+            }
+        )
 
 
     def test_explicit_data_response(self):
     def test_explicit_data_response(self):
         """utility returns response with explicit data"""
         """utility returns response with explicit data"""
         response = self.client.get(reverse('test-paginated-response-data'))
         response = self.client.get(reverse('test-paginated-response-data'))
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
-        self.assertEqual(response.json(), {
-            'results': ['a', 'b', 'c', 'd', 'e'],
-            'page': 2,
-            'pages': 10,
-            'count': 100,
-            'first': 1,
-            'previous': 1,
-            'next': 3,
-            'last': 10,
-            'before': 10,
-            'more': 80,
-        })
+        self.assertEqual(
+            response.json(), {
+                'results': ['a', 'b', 'c', 'd', 'e'],
+                'page': 2,
+                'pages': 10,
+                'count': 100,
+                'first': 1,
+                'previous': 1,
+                'next': 3,
+                'last': 10,
+                'before': 10,
+                'more': 80,
+            }
+        )
 
 
     def test_explicit_serializer_response(self):
     def test_explicit_serializer_response(self):
         """utility returns response with data serialized via serializer"""
         """utility returns response with data serialized via serializer"""
         response = self.client.get(reverse('test-paginated-response-serializer'))
         response = self.client.get(reverse('test-paginated-response-serializer'))
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
-        self.assertEqual(response.json(), {
-            'results': [
-                {'id': 0},
-                {'id': 2},
-                {'id': 4},
-                {'id': 6},
-            ],
-            'page': 1,
-            'pages': 1,
-            'count': 4,
-            'first': None,
-            'previous': None,
-            'next': None,
-            'last': None,
-            'before': 0,
-            'more': 0,
-        })
+        self.assertEqual(
+            response.json(), {
+                'results': [
+                    {
+                        'id': 0
+                    },
+                    {
+                        'id': 2
+                    },
+                    {
+                        'id': 4
+                    },
+                    {
+                        'id': 6
+                    },
+                ],
+                'page': 1,
+                'pages': 1,
+                'count': 4,
+                'first': None,
+                'previous': None,
+                'next': None,
+                'last': None,
+                'before': 0,
+                'more': 0,
+            }
+        )
 
 
     def test_explicit_data_serializer_response(self):
     def test_explicit_data_serializer_response(self):
         """utility returns response with explicit data serialized via serializer"""
         """utility returns response with explicit data serialized via serializer"""
         response = self.client.get(reverse('test-paginated-response-data-serializer'))
         response = self.client.get(reverse('test-paginated-response-data-serializer'))
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
-        self.assertEqual(response.json(), {
-            'results': [
-                {'id': 'aa'},
-                {'id': 'bb'},
-                {'id': 'cc'},
-                {'id': 'dd'},
-            ],
-            'page': 1,
-            'pages': 1,
-            'count': 4,
-            'first': None,
-            'previous': None,
-            'next': None,
-            'last': None,
-            'before': 0,
-            'more': 0,
-        })
+        self.assertEqual(
+            response.json(), {
+                'results': [
+                    {
+                        'id': 'aa'
+                    },
+                    {
+                        'id': 'bb'
+                    },
+                    {
+                        'id': 'cc'
+                    },
+                    {
+                        'id': 'dd'
+                    },
+                ],
+                'page': 1,
+                'pages': 1,
+                'count': 4,
+                'first': None,
+                'previous': None,
+                'next': None,
+                'last': None,
+                'before': 0,
+                'more': 0,
+            }
+        )
 
 
     def test_explicit_data_extra_response(self):
     def test_explicit_data_extra_response(self):
         """utility returns response with explicit data and extra"""
         """utility returns response with explicit data and extra"""
         response = self.client.get(reverse('test-paginated-response-data-extra'))
         response = self.client.get(reverse('test-paginated-response-data-extra'))
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
-        self.assertEqual(response.json(), {
-            'results': ['a', 'b', 'c', 'd'],
-            'page': 1,
-            'pages': 1,
-            'count': 4,
-            'first': None,
-            'previous': None,
-            'next': 'EXTRA',
-            'last': None,
-            'before': 0,
-            'more': 0,
-            'lorem': 'ipsum'
-        })
+        self.assertEqual(
+            response.json(), {
+                'results': ['a', 'b', 'c', 'd'],
+                'page': 1,
+                'pages': 1,
+                'count': 4,
+                'first': None,
+                'previous': None,
+                'next': 'EXTRA',
+                'last': None,
+                'before': 0,
+                'more': 0,
+                'lorem': 'ipsum'
+            }
+        )

+ 33 - 39
misago/core/tests/test_templatetags.py

@@ -2,9 +2,7 @@ from django import forms
 from django.template import Context, Template, TemplateSyntaxError
 from django.template import Context, Template, TemplateSyntaxError
 from django.test import TestCase
 from django.test import TestCase
 
 
-from misago.core.shortcuts import paginate
 from misago.core.templatetags import misago_batch
 from misago.core.templatetags import misago_batch
-from misago.core.utils import encode_json_html
 
 
 
 
 class CaptureTests(TestCase):
 class CaptureTests(TestCase):
@@ -47,12 +45,12 @@ class BatchTests(TestCase):
     def test_batch(self):
     def test_batch(self):
         """standard batch yields valid results"""
         """standard batch yields valid results"""
         batch = 'loremipsum'
         batch = 'loremipsum'
-        yields = (
+        yields = [
             ['l', 'o', 'r'],
             ['l', 'o', 'r'],
             ['e', 'm', 'i'],
             ['e', 'm', 'i'],
             ['p', 's', 'u'],
             ['p', 's', 'u'],
             ['m'],
             ['m'],
-        )
+        ]
 
 
         for i, test_yield in enumerate(misago_batch.batch(batch, 3)):
         for i, test_yield in enumerate(misago_batch.batch(batch, 3)):
             self.assertEqual(test_yield, yields[i])
             self.assertEqual(test_yield, yields[i])
@@ -60,12 +58,12 @@ class BatchTests(TestCase):
     def test_batchnonefilled(self):
     def test_batchnonefilled(self):
         """none-filled batch yields valid results"""
         """none-filled batch yields valid results"""
         batch = 'loremipsum'
         batch = 'loremipsum'
-        yields = (
+        yields = [
             ['l', 'o', 'r'],
             ['l', 'o', 'r'],
             ['e', 'm', 'i'],
             ['e', 'm', 'i'],
             ['p', 's', 'u'],
             ['p', 's', 'u'],
             ['m', None, None],
             ['m', None, None],
-        )
+        ]
 
 
         for i, test_yield in enumerate(misago_batch.batchnonefilled(batch, 3)):
         for i, test_yield in enumerate(misago_batch.batchnonefilled(batch, 3)):
             self.assertEqual(test_yield, yields[i])
             self.assertEqual(test_yield, yields[i])
@@ -173,10 +171,7 @@ class ShorthandsTests(TestCase):
 """
 """
 
 
         tpl = Template(tpl_content)
         tpl = Template(tpl_content)
-        self.assertEqual(tpl.render(Context({
-            'result': 'Ok!',
-            'value': True
-        })).strip(), 'Ok!')
+        self.assertEqual(tpl.render(Context({'result': 'Ok!', 'value': True})).strip(), 'Ok!')
 
 
     def test_iftrue_for_false(self):
     def test_iftrue_for_false(self):
         """iftrue isnt rendering value for false"""
         """iftrue isnt rendering value for false"""
@@ -187,10 +182,7 @@ class ShorthandsTests(TestCase):
 """
 """
 
 
         tpl = Template(tpl_content)
         tpl = Template(tpl_content)
-        self.assertEqual(tpl.render(Context({
-            'result': 'Ok!',
-            'value': False
-        })).strip(), '')
+        self.assertEqual(tpl.render(Context({'result': 'Ok!', 'value': False})).strip(), '')
 
 
     def test_iffalse_for_true(self):
     def test_iffalse_for_true(self):
         """iffalse isnt rendering value for true"""
         """iffalse isnt rendering value for true"""
@@ -201,10 +193,7 @@ class ShorthandsTests(TestCase):
 """
 """
 
 
         tpl = Template(tpl_content)
         tpl = Template(tpl_content)
-        self.assertEqual(tpl.render(Context({
-            'result': 'Ok!',
-            'value': True
-        })).strip(), '')
+        self.assertEqual(tpl.render(Context({'result': 'Ok!', 'value': True})).strip(), '')
 
 
     def test_iffalse_for_false(self):
     def test_iffalse_for_false(self):
         """iffalse renders value for false"""
         """iffalse renders value for false"""
@@ -215,10 +204,7 @@ class ShorthandsTests(TestCase):
 """
 """
 
 
         tpl = Template(tpl_content)
         tpl = Template(tpl_content)
-        self.assertEqual(tpl.render(Context({
-            'result': 'Ok!',
-            'value': False
-        })).strip(), 'Ok!')
+        self.assertEqual(tpl.render(Context({'result': 'Ok!', 'value': False})).strip(), 'Ok!')
 
 
 
 
 class JSONTests(TestCase):
 class JSONTests(TestCase):
@@ -231,9 +217,13 @@ class JSONTests(TestCase):
 """
 """
 
 
         tpl = Template(tpl_content)
         tpl = Template(tpl_content)
-        self.assertEqual(tpl.render(Context({
-            'value': {'he</script>llo': 'bo"b!'}
-        })).strip(), r'{"he\u003C/script>llo": "bo\"b!"}')
+        self.assertEqual(
+            tpl.render(Context({
+                'value': {
+                    'he</script>llo': 'bo"b!'
+                }
+            })).strip(), r'{"he\u003C/script>llo": "bo\"b!"}'
+        )
 
 
 
 
 class PageTitleTests(TestCase):
 class PageTitleTests(TestCase):
@@ -246,9 +236,7 @@ class PageTitleTests(TestCase):
         """
         """
 
 
         tpl = Template(tpl_content)
         tpl = Template(tpl_content)
-        self.assertEqual(tpl.render(Context({
-            'item': 'Lorem Ipsum'
-        })).strip(), 'Lorem Ipsum')
+        self.assertEqual(tpl.render(Context({'item': 'Lorem Ipsum'})).strip(), 'Lorem Ipsum')
 
 
     def test_parent_title(self):
     def test_parent_title(self):
         """tag builds full title from title and parent name"""
         """tag builds full title from title and parent name"""
@@ -259,10 +247,12 @@ class PageTitleTests(TestCase):
         """
         """
 
 
         tpl = Template(tpl_content)
         tpl = Template(tpl_content)
-        self.assertEqual(tpl.render(Context({
-            'item': 'Lorem Ipsum',
-            'parent': 'Some Thread'
-        })).strip(), 'Lorem Ipsum | Some Thread')
+        self.assertEqual(
+            tpl.render(Context({
+                'item': 'Lorem Ipsum',
+                'parent': 'Some Thread',
+            })).strip(), 'Lorem Ipsum | Some Thread'
+        )
 
 
     def test_paged_title(self):
     def test_paged_title(self):
         """tag builds full title from title and page number"""
         """tag builds full title from title and page number"""
@@ -273,9 +263,11 @@ class PageTitleTests(TestCase):
         """
         """
 
 
         tpl = Template(tpl_content)
         tpl = Template(tpl_content)
-        self.assertEqual(tpl.render(Context({
-            'item': 'Lorem Ipsum'
-        })).strip(), 'Lorem Ipsum (page: 3)')
+        self.assertEqual(
+            tpl.render(Context({
+                'item': 'Lorem Ipsum',
+            })).strip(), 'Lorem Ipsum (page: 3)'
+        )
 
 
     def test_kitchensink_title(self):
     def test_kitchensink_title(self):
         """tag builds full title from all options"""
         """tag builds full title from all options"""
@@ -286,7 +278,9 @@ class PageTitleTests(TestCase):
         """
         """
 
 
         tpl = Template(tpl_content)
         tpl = Template(tpl_content)
-        self.assertEqual(tpl.render(Context({
-            'item': 'Lorem Ipsum',
-            'parent': 'Some Thread'
-        })).strip(), 'Lorem Ipsum (page: 3) | Some Thread')
+        self.assertEqual(
+            tpl.render(Context({
+                'item': 'Lorem Ipsum',
+                'parent': 'Some Thread',
+            })).strip(), 'Lorem Ipsum (page: 3) | Some Thread'
+        )

+ 0 - 32
misago/core/tests/test_testmailsetup.py

@@ -1,32 +0,0 @@
-from django.core import mail
-from django.core.management import call_command
-from django.test import TestCase
-from django.utils.six import StringIO
-
-from misago.core.management.commands import testemailsetup
-
-
-class TestEmailSetupTests(TestCase):
-    def test_email_setup(self):
-        """command sets test email in outbox"""
-        command = testemailsetup.Command()
-
-        out = StringIO()
-        call_command(command, "t@mail.com", stdout=out)
-        command_output = out.getvalue().splitlines()[0].strip()
-
-        self.assertEqual(command_output, 'Test message was sent to t@mail.com')
-        self.assertEqual('Test Message', mail.outbox[0].subject)
-
-    def test_invalid_args(self):
-        """
-        there are no unhandled exceptions when command receives invalid args
-        """
-        command = testemailsetup.Command()
-
-        out = StringIO()
-        err = StringIO()
-
-        call_command(command, "bawww", stdout=out, stderr=err)
-        command_output = err.getvalue().splitlines()[-1].strip()
-        self.assertEqual(command_output, "This isn't valid e-mail address")

+ 103 - 93
misago/core/tests/test_utils.py

@@ -4,22 +4,16 @@ from __future__ import unicode_literals
 from django.test import TestCase
 from django.test import TestCase
 from django.test.client import RequestFactory
 from django.test.client import RequestFactory
 from django.urls import reverse
 from django.urls import reverse
-from django.utils import six, timezone
+from django.utils import six
 
 
 from misago.core.utils import (
 from misago.core.utils import (
     clean_return_path, format_plaintext_for_html, is_referer_local, is_request_to_misago,
     clean_return_path, format_plaintext_for_html, is_referer_local, is_request_to_misago,
     parse_iso8601_string, resolve_slugify, slugify)
     parse_iso8601_string, resolve_slugify, slugify)
 
 
 
 
-VALID_PATHS = (
-    "/",
-    "/threads/",
-)
+VALID_PATHS = ("/", "/threads/", )
 
 
-INVALID_PATHS = (
-    "",
-    "somewhere/",
-)
+INVALID_PATHS = ("", "somewhere/", )
 
 
 
 
 class IsRequestToMisagoTests(TestCase):
 class IsRequestToMisagoTests(TestCase):
@@ -34,14 +28,16 @@ class IsRequestToMisagoTests(TestCase):
             request.path_info = path
             request.path_info = path
             self.assertTrue(
             self.assertTrue(
                 is_request_to_misago(request),
                 is_request_to_misago(request),
-                '"%s" is not overlapped by "%s"' % (path, misago_prefix))
+                '"%s" is not overlapped by "%s"' % (path, misago_prefix)
+            )
 
 
         for path in INVALID_PATHS:
         for path in INVALID_PATHS:
             request = RequestFactory().get('/')
             request = RequestFactory().get('/')
             request.path_info = path
             request.path_info = path
             self.assertFalse(
             self.assertFalse(
                 is_request_to_misago(request),
                 is_request_to_misago(request),
-                '"%s" is overlapped by "%s"' % (path, misago_prefix))
+                '"%s" is overlapped by "%s"' % (path, misago_prefix)
+            )
 
 
 
 
 class SlugifyTests(TestCase):
 class SlugifyTests(TestCase):
@@ -65,8 +61,7 @@ class SlugifyTests(TestCase):
             resolve_slugify('misago.threads.invalidname')
             resolve_slugify('misago.threads.invalidname')
         except ImportError as e:
         except ImportError as e:
             error_message = six.text_type(e)
             error_message = six.text_type(e)
-            self.assertEqual(
-                error_message, 'name invalidname not found in misago.threads module')
+            self.assertEqual(error_message, 'name invalidname not found in misago.threads module')
 
 
     def test_resolve_valid_name(self):
     def test_resolve_valid_name(self):
         """resolve_slugify resolves valid paths"""
         """resolve_slugify resolves valid paths"""
@@ -75,7 +70,7 @@ class SlugifyTests(TestCase):
 
 
     def test_valid_slugify_output(self):
     def test_valid_slugify_output(self):
         """Misago's slugify correctly slugifies string"""
         """Misago's slugify correctly slugifies string"""
-        test_cases = (
+        test_cases = [
             ('Bob', 'bob'),
             ('Bob', 'bob'),
             ('Eric The Fish', 'eric-the-fish'),
             ('Eric The Fish', 'eric-the-fish'),
             ('John   Snow', 'john-snow'),
             ('John   Snow', 'john-snow'),
@@ -83,7 +78,7 @@ class SlugifyTests(TestCase):
             ('An###ne', 'anne'),
             ('An###ne', 'anne'),
             ('S**t', 'st'),
             ('S**t', 'st'),
             ('Łók', 'lok'),
             ('Łók', 'lok'),
-        )
+        ]
 
 
         for original, slug in test_cases:
         for original, slug in test_cases:
             self.assertEqual(slugify(original), slug)
             self.assertEqual(slugify(original), slug)
@@ -92,56 +87,41 @@ class SlugifyTests(TestCase):
 class ParseIso8601StringTests(TestCase):
 class ParseIso8601StringTests(TestCase):
     def test_valid_input(self):
     def test_valid_input(self):
         """util parses iso 8601 strings"""
         """util parses iso 8601 strings"""
-        INPUTS = (
+        INPUTS = [
             '2016-10-22T20:55:39.185085Z',
             '2016-10-22T20:55:39.185085Z',
             '2016-10-22T20:55:39.185085-01:00',
             '2016-10-22T20:55:39.185085-01:00',
             '2016-10-22T20:55:39-01:00',
             '2016-10-22T20:55:39-01:00',
             '2016-10-22T20:55:39.185085+01:00',
             '2016-10-22T20:55:39.185085+01:00',
-        )
+        ]
 
 
         for test_input in INPUTS:
         for test_input in INPUTS:
             self.assertTrue(parse_iso8601_string(test_input))
             self.assertTrue(parse_iso8601_string(test_input))
 
 
     def test_invalid_input(self):
     def test_invalid_input(self):
         """util throws ValueError on invalid input"""
         """util throws ValueError on invalid input"""
-        INPUTS = (
+        INPUTS = [
             '',
             '',
             '2016-10-22',
             '2016-10-22',
             '2016-10-22T30:55:39.185085+11:00',
             '2016-10-22T30:55:39.185085+11:00',
             '2016-10-22T20:55:39.18SSSSS5085Z',
             '2016-10-22T20:55:39.18SSSSS5085Z',
-        )
+        ]
 
 
         for test_input in INPUTS:
         for test_input in INPUTS:
             with self.assertRaises(ValueError):
             with self.assertRaises(ValueError):
                 self.assertTrue(parse_iso8601_string(test_input))
                 self.assertTrue(parse_iso8601_string(test_input))
 
 
 
 
-PLAINTEXT_FORMAT_CASES = (
-    (
-        'Lorem ipsum.',
-        '<p>Lorem ipsum.</p>'
-    ),
-    (
-        'Lorem <b>ipsum</b>.',
-        '<p>Lorem &lt;b&gt;ipsum&lt;/b&gt;.</p>'
-    ),
-    (
-        'Lorem "ipsum" dolor met.',
-        '<p>Lorem &quot;ipsum&quot; dolor met.</p>'
-    ),
-    (
-        'Lorem ipsum.\nDolor met.',
-        '<p>Lorem ipsum.<br />Dolor met.</p>'
-    ),
-    (
-        'Lorem ipsum.\n\nDolor met.',
-        '<p>Lorem ipsum.</p>\n\n<p>Dolor met.</p>'
-    ),
+PLAINTEXT_FORMAT_CASES = [
+    ('Lorem ipsum.', '<p>Lorem ipsum.</p>'),
+    ('Lorem <b>ipsum</b>.', '<p>Lorem &lt;b&gt;ipsum&lt;/b&gt;.</p>'),
+    ('Lorem "ipsum" dolor met.', '<p>Lorem &quot;ipsum&quot; dolor met.</p>'),
+    ('Lorem ipsum.\nDolor met.', '<p>Lorem ipsum.<br />Dolor met.</p>'),
+    ('Lorem ipsum.\n\nDolor met.', '<p>Lorem ipsum.</p>\n\n<p>Dolor met.</p>'),
     (
     (
         'http://misago-project.org/login/',
         'http://misago-project.org/login/',
         '<p><a href="http://misago-project.org/login/">http://misago-project.org/login/</a></p>'
         '<p><a href="http://misago-project.org/login/">http://misago-project.org/login/</a></p>'
     ),
     ),
-)
+]
 
 
 
 
 class FormatPlaintextForHtmlTests(TestCase):
 class FormatPlaintextForHtmlTests(TestCase):
@@ -171,88 +151,118 @@ class MockRequest(object):
 class CleanReturnPathTests(TestCase):
 class CleanReturnPathTests(TestCase):
     def test_get_request(self):
     def test_get_request(self):
         """clean_return_path works for GET requests"""
         """clean_return_path works for GET requests"""
-        bad_request = MockRequest('GET', {
-            'HTTP_REFERER': 'http://cookies.com',
-            'HTTP_HOST': 'misago-project.org'
-        })
+        bad_request = MockRequest(
+            'GET', {
+                'HTTP_REFERER': 'http://cookies.com',
+                'HTTP_HOST': 'misago-project.org',
+            }
+        )
         self.assertIsNone(clean_return_path(bad_request))
         self.assertIsNone(clean_return_path(bad_request))
 
 
-        bad_request = MockRequest('GET', {
-            'HTTP_REFERER': 'https://misago-project.org/',
-            'HTTP_HOST': 'misago-project.org/'
-        })
+        bad_request = MockRequest(
+            'GET', {
+                'HTTP_REFERER': 'https://misago-project.org/',
+                'HTTP_HOST': 'misago-project.org/',
+            }
+        )
         self.assertIsNone(clean_return_path(bad_request))
         self.assertIsNone(clean_return_path(bad_request))
 
 
-        bad_request = MockRequest('GET', {
-            'HTTP_REFERER': 'https://misago-project.org/',
-            'HTTP_HOST': 'misago-project.org/assadsa/'
-        })
+        bad_request = MockRequest(
+            'GET', {
+                'HTTP_REFERER': 'https://misago-project.org/',
+                'HTTP_HOST': 'misago-project.org/assadsa/',
+            }
+        )
         self.assertIsNone(clean_return_path(bad_request))
         self.assertIsNone(clean_return_path(bad_request))
 
 
-        ok_request = MockRequest('GET', {
-            'HTTP_REFERER': 'http://misago-project.org/',
-            'HTTP_HOST': 'misago-project.org/'
-        })
+        ok_request = MockRequest(
+            'GET', {
+                'HTTP_REFERER': 'http://misago-project.org/',
+                'HTTP_HOST': 'misago-project.org/',
+            }
+        )
         self.assertEqual(clean_return_path(ok_request), '/')
         self.assertEqual(clean_return_path(ok_request), '/')
 
 
-        ok_request = MockRequest('GET', {
-            'HTTP_REFERER': 'http://misago-project.org/login/',
-            'HTTP_HOST': 'misago-project.org/'
-        })
+        ok_request = MockRequest(
+            'GET', {
+                'HTTP_REFERER': 'http://misago-project.org/login/',
+                'HTTP_HOST': 'misago-project.org/',
+            }
+        )
         self.assertEqual(clean_return_path(ok_request), '/login/')
         self.assertEqual(clean_return_path(ok_request), '/login/')
 
 
     def test_post_request(self):
     def test_post_request(self):
         """clean_return_path works for POST requests"""
         """clean_return_path works for POST requests"""
-        bad_request = MockRequest('POST', {
-            'HTTP_REFERER': 'http://misago-project.org/',
-            'HTTP_HOST': 'misago-project.org/'
-        }, {'return_path': '/sdasdsa/'})
+        bad_request = MockRequest(
+            'POST', {
+                'HTTP_REFERER': 'http://misago-project.org/',
+                'HTTP_HOST': 'misago-project.org/',
+            }, {
+                'return_path': '/sdasdsa/',
+            }
+        )
         self.assertIsNone(clean_return_path(bad_request))
         self.assertIsNone(clean_return_path(bad_request))
 
 
-        ok_request = MockRequest('POST', {
-            'HTTP_REFERER': 'http://misago-project.org/',
-            'HTTP_HOST': 'misago-project.org/'
-        }, {'return_path': '/login/'})
+        ok_request = MockRequest(
+            'POST', {
+                'HTTP_REFERER': 'http://misago-project.org/',
+                'HTTP_HOST': 'misago-project.org/',
+            }, {
+                'return_path': '/login/',
+            }
+        )
         self.assertEqual(clean_return_path(ok_request), '/login/')
         self.assertEqual(clean_return_path(ok_request), '/login/')
 
 
 
 
 class IsRefererLocalTests(TestCase):
 class IsRefererLocalTests(TestCase):
     def test_local_referers(self):
     def test_local_referers(self):
         """local referers return true"""
         """local referers return true"""
-        ok_request = MockRequest('GET', {
-            'HTTP_REFERER': 'http://misago-project.org/',
-            'HTTP_HOST': 'misago-project.org/'
-        })
+        ok_request = MockRequest(
+            'GET', {
+                'HTTP_REFERER': 'http://misago-project.org/',
+                'HTTP_HOST': 'misago-project.org/',
+            }
+        )
         self.assertTrue(is_referer_local(ok_request))
         self.assertTrue(is_referer_local(ok_request))
 
 
-        ok_request = MockRequest('GET', {
-            'HTTP_REFERER': 'http://misago-project.org/',
-            'HTTP_HOST': 'misago-project.org/'
-        })
+        ok_request = MockRequest(
+            'GET', {
+                'HTTP_REFERER': 'http://misago-project.org/',
+                'HTTP_HOST': 'misago-project.org/',
+            }
+        )
         self.assertTrue(is_referer_local(ok_request))
         self.assertTrue(is_referer_local(ok_request))
 
 
-        ok_request = MockRequest('GET', {
-            'HTTP_REFERER': 'http://misago-project.org/login/',
-            'HTTP_HOST': 'misago-project.org/'
-        })
+        ok_request = MockRequest(
+            'GET', {
+                'HTTP_REFERER': 'http://misago-project.org/login/',
+                'HTTP_HOST': 'misago-project.org/',
+            }
+        )
         self.assertTrue(is_referer_local(ok_request))
         self.assertTrue(is_referer_local(ok_request))
 
 
     def test_foreign_referers(self):
     def test_foreign_referers(self):
         """non-local referers return false"""
         """non-local referers return false"""
-        bad_request = MockRequest('GET', {
-            'HTTP_REFERER': 'http://else-project.org/',
-            'HTTP_HOST': 'misago-project.org/'
-        })
+        bad_request = MockRequest(
+            'GET', {
+                'HTTP_REFERER': 'http://else-project.org/',
+                'HTTP_HOST': 'misago-project.org/',
+            }
+        )
         self.assertFalse(is_referer_local(bad_request))
         self.assertFalse(is_referer_local(bad_request))
 
 
-        bad_request = MockRequest('GET', {
-            'HTTP_REFERER': 'https://misago-project.org/',
-            'HTTP_HOST': 'misago-project.org/'
-        })
+        bad_request = MockRequest(
+            'GET', {
+                'HTTP_REFERER': 'https://misago-project.org/',
+                'HTTP_HOST': 'misago-project.org/',
+            }
+        )
         self.assertFalse(is_referer_local(bad_request))
         self.assertFalse(is_referer_local(bad_request))
 
 
-        bad_request = MockRequest('GET', {
-            'HTTP_REFERER': 'http://misago-project.org/',
-            'HTTP_HOST': 'misago-project.org/assadsa/'
-        })
+        bad_request = MockRequest(
+            'GET', {
+                'HTTP_REFERER': 'http://misago-project.org/',
+                'HTTP_HOST': 'misago-project.org/assadsa/',
+            }
+        )
         self.assertFalse(is_referer_local(bad_request))
         self.assertFalse(is_referer_local(bad_request))

+ 7 - 5
misago/core/tests/test_validators.py

@@ -22,11 +22,13 @@ class ValidateSluggableTests(TestCase):
         with self.assertRaises(ValidationError):
         with self.assertRaises(ValidationError):
             validator('!#@! !@#@')
             validator('!#@! !@#@')
         with self.assertRaises(ValidationError):
         with self.assertRaises(ValidationError):
-            validator('!#@! !@#@ 1234567890 1234567890 1234567890 1234567890'
-                      '1234567890 1234567890 1234567890 1234567890 1234567890'
-                      '1234567890 1234567890 1234567890 1234567890 1234567890'
-                      '1234567890 1234567890 1234567890 1234567890 1234567890'
-                      '1234567890 1234567890 1234567890 1234567890 1234567890')
+            validator(
+                '!#@! !@#@ 1234567890 1234567890 1234567890 1234567890'
+                '1234567890 1234567890 1234567890 1234567890 1234567890'
+                '1234567890 1234567890 1234567890 1234567890 1234567890'
+                '1234567890 1234567890 1234567890 1234567890 1234567890'
+                '1234567890 1234567890 1234567890 1234567890 1234567890'
+            )
 
 
     def test_valid_input_validation(self):
     def test_valid_input_validation(self):
         """valid values don't raise errors"""
         """valid values don't raise errors"""

+ 2 - 3
misago/core/testutils.py

@@ -5,9 +5,8 @@ from .cache import cache
 
 
 
 
 class MisagoTestCase(TestCase):
 class MisagoTestCase(TestCase):
-    """
-    TestCase class that empties global state before and after each test
-    """
+    """TestCase class that empties global state before and after each test"""
+
     def clear_state(self):
     def clear_state(self):
         cache.clear()
         cache.clear()
         threadstore.clear()
         threadstore.clear()

+ 17 - 34
misago/core/utils.py

@@ -6,19 +6,11 @@ from django.http import Http404
 from django.urls import resolve, reverse
 from django.urls import resolve, reverse
 from django.utils import html, timezone
 from django.utils import html, timezone
 from django.utils.encoding import force_text
 from django.utils.encoding import force_text
-from django.utils.translation import ugettext_lazy as _
-from django.utils.translation import ungettext_lazy
 
 
 
 
 MISAGO_SLUGIFY = getattr(settings, 'MISAGO_SLUGIFY', 'misago.core.slugify.default')
 MISAGO_SLUGIFY = getattr(settings, 'MISAGO_SLUGIFY', 'misago.core.slugify.default')
 
 
 
 
-def slugify(string):
-    string = six.text_type(string)
-    string = unidecode(string)
-    return django_slugify(string.replace('_', ' ').strip())
-
-
 def resolve_slugify(path):
 def resolve_slugify(path):
     path_bits = path.split('.')
     path_bits = path.split('.')
     module, name = '.'.join(path_bits[:-1]), path_bits[-1]
     module, name = '.'.join(path_bits[:-1]), path_bits[-1]
@@ -42,15 +34,11 @@ def encode_json_html(string):
     return string.replace('<', r'\u003C')
     return string.replace('<', r'\u003C')
 
 
 
 
-"""
-Turn ISO 8601 string into datetime object
-"""
-ISO8601_FORMATS = (
-    "%Y-%m-%dT%H:%M:%S",
-    "%Y-%m-%dT%H:%M:%S.%f",
-)
+ISO8601_FORMATS = ("%Y-%m-%dT%H:%M:%S", "%Y-%m-%dT%H:%M:%S.%f", )
+
 
 
 def parse_iso8601_string(value):
 def parse_iso8601_string(value):
+    """turns ISO 8601 string into datetime object"""
     value = force_text(value, strings_only=True).rstrip('Z')
     value = force_text(value, strings_only=True).rstrip('Z')
 
 
     for format in ISO8601_FORMATS:
     for format in ISO8601_FORMATS:
@@ -79,19 +67,17 @@ def parse_iso8601_string(value):
     return timezone.make_aware(parsed_value, tz_correction)
     return timezone.make_aware(parsed_value, tz_correction)
 
 
 
 
-"""
-Mark request as having sensitive parameters
-We can't use decorator because of DRF uses custom HttpRequest
-that is incompatibile with Django's decorator
-"""
 def hide_post_parameters(request):
 def hide_post_parameters(request):
+    """
+    Mark request as having sensitive parameters
+    We can't use decorator because of DRF uses custom HttpRequest
+    that is incompatibile with Django's decorator
+    """
     request.sensitive_post_parameters = '__ALL__'
     request.sensitive_post_parameters = '__ALL__'
 
 
 
 
-"""
-Return path utility
-"""
 def clean_return_path(request):
 def clean_return_path(request):
+    """return path utility that returns return path from referer or POST"""
     if request.method == 'POST' and 'return_path' in request.POST:
     if request.method == 'POST' and 'return_path' in request.POST:
         return _get_return_path_from_post(request)
         return _get_return_path_from_post(request)
     else:
     else:
@@ -130,9 +116,14 @@ def _get_return_path_from_referer(request):
         return None
         return None
 
 
 
 
-"""
-Utils for resolving requests destination
-"""
+def is_request_to_misago(request):
+    try:
+        return request._request_to_misago
+    except AttributeError:
+        request._request_to_misago = _is_request_path_under_misago(request)
+        return request._request_to_misago
+
+
 def _is_request_path_under_misago(request):
 def _is_request_path_under_misago(request):
     # We are assuming that forum_index link is root of all Misago links
     # We are assuming that forum_index link is root of all Misago links
     forum_index = reverse('misago:index')
     forum_index = reverse('misago:index')
@@ -143,14 +134,6 @@ def _is_request_path_under_misago(request):
     return path_info[:len(forum_index)] == forum_index
     return path_info[:len(forum_index)] == forum_index
 
 
 
 
-def is_request_to_misago(request):
-    try:
-        return request._request_to_misago
-    except AttributeError:
-        request._request_to_misago = _is_request_path_under_misago(request)
-        return request._request_to_misago
-
-
 def is_referer_local(request):
 def is_referer_local(request):
     referer = request.META.get('HTTP_REFERER')
     referer = request.META.get('HTTP_REFERER')
 
 

+ 1 - 2
misago/core/validators.py

@@ -6,8 +6,7 @@ from .utils import slugify
 
 
 class validate_sluggable(object):
 class validate_sluggable(object):
     def __init__(self, error_short=None, error_long=None):
     def __init__(self, error_short=None, error_long=None):
-        self.error_short = error_short or _(
-            "Value has to contain alpha-numerical characters.")
+        self.error_short = error_short or _("Value has to contain alpha-numerical characters.")
         self.error_long = error_long or _("Value is too long.")
         self.error_long = error_long or _("Value is too long.")
 
 
     def __call__(self, value):
     def __call__(self, value):

+ 1 - 3
misago/core/views.py

@@ -4,11 +4,9 @@ from django.views import i18n
 from django.views.decorators.cache import cache_page
 from django.views.decorators.cache import cache_page
 from django.views.decorators.http import last_modified
 from django.views.decorators.http import last_modified
 
 
-from . import momentjs
-
 
 
 def forum_index(request):
 def forum_index(request):
-    return # blow up as this view is normally non-reachable!
+    return  # blow up as this view is normally non-reachable!
 
 
 
 
 def home_redirect(*args, **kwargs):
 def home_redirect(*args, **kwargs):

+ 7 - 17
misago/datamover/attachments.py

@@ -5,7 +5,7 @@ import os
 from django.contrib.auth import get_user_model
 from django.contrib.auth import get_user_model
 from django.core.files import File
 from django.core.files import File
 
 
-from misago.threads.models import Attachment, AttachmentType, Post, Thread
+from misago.threads.models import Attachment, AttachmentType, Post
 from misago.threads.serializers import AttachmentSerializer
 from misago.threads.serializers import AttachmentSerializer
 
 
 from . import OLD_FORUM, fetch_assoc, localise_datetime, movedids
 from . import OLD_FORUM, fetch_assoc, localise_datetime, movedids
@@ -13,21 +13,11 @@ from . import OLD_FORUM, fetch_assoc, localise_datetime, movedids
 
 
 UserModel = get_user_model()
 UserModel = get_user_model()
 
 
-IMAGE_TYPES = (
-    'image/gif',
-    'image/jpeg',
-    'image/png',
-)
+IMAGE_TYPES = ('image/gif', 'image/jpeg', 'image/png', )
 
 
 
 
 def move_attachments(stdout, style):
 def move_attachments(stdout, style):
-    query = '''
-        SELECT *
-        FROM
-            misago_attachment
-        ORDER BY
-            id
-    '''
+    query = 'SELECT * FROM misago_attachment ORDER BY id'
 
 
     posts = []
     posts = []
 
 
@@ -38,13 +28,13 @@ def move_attachments(stdout, style):
 
 
     for attachment in fetch_assoc(query):
     for attachment in fetch_assoc(query):
         if attachment['content_type'] not in attachment_types:
         if attachment['content_type'] not in attachment_types:
-            stdout.write(style.WARNING(
-                "Skipping attachment: %s (invalid type)" % attachment['name']))
+            stdout.write(
+                style.WARNING("Skipping attachment: %s (invalid type)" % attachment['name'])
+            )
             continue
             continue
 
 
         if not attachment['post_id']:
         if not attachment['post_id']:
-            stdout.write(style.WARNING(
-                "Skipping attachment: %s (orphaned)" % attachment['name']))
+            stdout.write(style.WARNING("Skipping attachment: %s (orphaned)" % attachment['name']))
             continue
             continue
 
 
         filetype = attachment_types[attachment['content_type']]
         filetype = attachment_types[attachment['content_type']]

+ 5 - 5
misago/datamover/avatars.py

@@ -6,7 +6,7 @@ from django.contrib.auth import get_user_model
 from django.core.exceptions import ValidationError
 from django.core.exceptions import ValidationError
 
 
 from misago.conf import settings
 from misago.conf import settings
-from misago.users.avatars import dynamic, gallery, gravatar, store, uploaded
+from misago.users.avatars import dynamic, gravatar, store, uploaded
 
 
 from . import OLD_FORUM, fetch_assoc, movedids
 from . import OLD_FORUM, fetch_assoc, movedids
 
 
@@ -26,15 +26,15 @@ def move_avatars(stdout, style):
                     gravatar.set_avatar(user)
                     gravatar.set_avatar(user)
                 except gravatar.GravatarError:
                 except gravatar.GravatarError:
                     dynamic.set_avatar(user)
                     dynamic.set_avatar(user)
-                    print_warning(
-                        '%s: failed to download Gravatar' % user, stdout, style)
+                    print_warning('%s: failed to download Gravatar' % user, stdout, style)
             else:
             else:
                 try:
                 try:
                     if not old_user['avatar_original'] or not old_user['avatar_crop']:
                     if not old_user['avatar_original'] or not old_user['avatar_crop']:
                         raise ValidationError("Invalid avatar upload data.")
                         raise ValidationError("Invalid avatar upload data.")
 
 
                     image_path = os.path.join(
                     image_path = os.path.join(
-                        OLD_FORUM['MEDIA'], 'avatars', old_user['avatar_original'])
+                        OLD_FORUM['MEDIA'], 'avatars', old_user['avatar_original']
+                    )
                     image = uploaded.validate_dimensions(image_path)
                     image = uploaded.validate_dimensions(image_path)
 
 
                     cleaned_crop = convert_crop(image, old_user)
                     cleaned_crop = convert_crop(image, old_user)
@@ -64,5 +64,5 @@ def convert_crop(image, user):
             'x': x * zoom * -1,
             'x': x * zoom * -1,
             'y': y * zoom * -1,
             'y': y * zoom * -1,
         },
         },
-        'zoom': zoom
+        'zoom': zoom,
     }
     }

+ 4 - 8
misago/datamover/bans.py

@@ -5,11 +5,7 @@ from misago.users.models import Ban
 from . import fetch_assoc, localise_datetime
 from . import fetch_assoc, localise_datetime
 
 
 
 
-CHECK_MAPPING = {
-  1: 0,
-  2: 1,
-  3: 2
-}
+CHECK_MAPPING = {1: 0, 2: 1, 3: 2}
 
 
 
 
 def move_bans():
 def move_bans():
@@ -20,7 +16,7 @@ def move_bans():
                 banned_value=ban['ban'],
                 banned_value=ban['ban'],
                 user_message=ban['reason_user'],
                 user_message=ban['reason_user'],
                 staff_message=ban['reason_admin'],
                 staff_message=ban['reason_admin'],
-                expires_on=localise_datetime(ban['expires'])
+                expires_on=localise_datetime(ban['expires']),
             )
             )
         else:
         else:
             Ban.objects.create(
             Ban.objects.create(
@@ -28,7 +24,7 @@ def move_bans():
                 banned_value=ban['ban'],
                 banned_value=ban['ban'],
                 user_message=ban['reason_user'],
                 user_message=ban['reason_user'],
                 staff_message=ban['reason_admin'],
                 staff_message=ban['reason_admin'],
-                expires_on=localise_datetime(ban['expires'])
+                expires_on=localise_datetime(ban['expires']),
             )
             )
 
 
             Ban.objects.create(
             Ban.objects.create(
@@ -36,7 +32,7 @@ def move_bans():
                 banned_value=ban['ban'],
                 banned_value=ban['ban'],
                 user_message=ban['reason_user'],
                 user_message=ban['reason_user'],
                 staff_message=ban['reason_admin'],
                 staff_message=ban['reason_admin'],
-                expires_on=localise_datetime(ban['expires'])
+                expires_on=localise_datetime(ban['expires']),
             )
             )
 
 
     Ban.objects.invalidate_cache()
     Ban.objects.invalidate_cache()

+ 21 - 13
misago/datamover/categories.py

@@ -28,14 +28,18 @@ def move_categories(stdout, style):
             new_parent_id = movedids.get('category', forum['parent_id'])
             new_parent_id = movedids.get('category', forum['parent_id'])
             parent = Category.objects.get(pk=new_parent_id)
             parent = Category.objects.get(pk=new_parent_id)
 
 
-        category = Category.objects.insert_node(Category(
-            name=forum['name'],
-            slug=forum['slug'],
-            description=forum['description'],
-            is_closed=forum['closed'],
-            prune_started_after=forum['prune_start'],
-            prune_replied_after=forum['prune_last'],
-        ), parent, save=True)
+        category = Category.objects.insert_node(
+            Category(
+                name=forum['name'],
+                slug=forum['slug'],
+                description=forum['description'],
+                is_closed=forum['closed'],
+                prune_started_after=forum['prune_start'],
+                prune_replied_after=forum['prune_last'],
+            ),
+            parent,
+            save=True,
+        )
 
 
         movedids.set('category', forum['id'], category.pk)
         movedids.set('category', forum['id'], category.pk)
 
 
@@ -48,7 +52,7 @@ def move_categories(stdout, style):
         new_archive_pk = movedids.get('category', forum['pruned_archive_id'])
         new_archive_pk = movedids.get('category', forum['pruned_archive_id'])
 
 
         Category.objects.filter(pk=new_category_pk).update(
         Category.objects.filter(pk=new_category_pk).update(
-            archive_pruned_in=Category.objects.get(pk=new_archive_pk)
+            archive_pruned_in=Category.objects.get(pk=new_archive_pk),
         )
         )
 
 
 
 
@@ -69,10 +73,14 @@ def move_labels():
             parent_id = movedids.get('category', parent_row['forum_id'])
             parent_id = movedids.get('category', parent_row['forum_id'])
             parent = Category.objects.get(pk=parent_id)
             parent = Category.objects.get(pk=parent_id)
 
 
-            category = Category.objects.insert_node(Category(
-                name=label['name'],
-                slug=label['slug'],
-            ), parent, save=True)
+            category = Category.objects.insert_node(
+                Category(
+                    name=label['name'],
+                    slug=label['slug'],
+                ),
+                parent,
+                save=True,
+            )
 
 
             label_id = '%s-%s' % (label['id'], parent_row['forum_id'])
             label_id = '%s-%s' % (label['id'], parent_row['forum_id'])
             movedids.set('label', label_id, category.pk)
             movedids.set('label', label_id, category.pk)

+ 1 - 3
misago/datamover/db.py

@@ -2,9 +2,7 @@ from django.db import connections
 
 
 
 
 def fetch_assoc(query, *args):
 def fetch_assoc(query, *args):
-    """
-    Return all rows from a cursor as a dict
-    """
+    """return all rows from a cursor as a dict"""
     with connections['misago05'].cursor() as cursor:
     with connections['misago05'].cursor() as cursor:
         cursor.execute(query, *args)
         cursor.execute(query, *args)
 
 

+ 1 - 3
misago/datamover/management/commands/buildmovesindex.py

@@ -12,9 +12,7 @@ MAPPINGS = {
 
 
 
 
 class Command(BaseCommand):
 class Command(BaseCommand):
-    help = (
-        "Builds moves index for redirects from old urls to new ones."
-    )
+    help = ("Builds moves index for redirects from old urls to new ones.")
 
 
     def handle(self, *args, **options):
     def handle(self, *args, **options):
         self.stdout.write("Building moves index...")
         self.stdout.write("Building moves index...")

+ 2 - 4
misago/datamover/management/commands/movecategories.py

@@ -10,10 +10,8 @@ class Command(BaseCommand):
 
 
         self.start_timer()
         self.start_timer()
         categories.move_categories(self.stdout, self.style)
         categories.move_categories(self.stdout, self.style)
-        self.stdout.write(
-            self.style.SUCCESS("Moved categories in %s" % self.stop_timer()))
+        self.stdout.write(self.style.SUCCESS("Moved categories in %s" % self.stop_timer()))
 
 
         self.start_timer()
         self.start_timer()
         categories.move_labels()
         categories.move_labels()
-        self.stdout.write(
-            self.style.SUCCESS("Moved labels in %s" % self.stop_timer()))
+        self.stdout.write(self.style.SUCCESS("Moved labels in %s" % self.stop_timer()))

+ 1 - 2
misago/datamover/management/commands/movesettings.py

@@ -11,5 +11,4 @@ class Command(BaseCommand):
         self.start_timer()
         self.start_timer()
         move_settings(self.stdout)
         move_settings(self.stdout)
 
 
-        self.stdout.write(
-            self.style.SUCCESS("Moved settings in %s" % self.stop_timer()))
+        self.stdout.write(self.style.SUCCESS("Moved settings in %s" % self.stop_timer()))

+ 11 - 19
misago/datamover/management/commands/movethreads.py

@@ -10,50 +10,42 @@ class Command(BaseCommand):
 
 
         self.start_timer()
         self.start_timer()
         threads.move_threads(self.stdout, self.style)
         threads.move_threads(self.stdout, self.style)
-        self.stdout.write(
-            self.style.SUCCESS("Moved threads in %s" % self.stop_timer()))
+        self.stdout.write(self.style.SUCCESS("Moved threads in %s" % self.stop_timer()))
 
 
         self.start_timer()
         self.start_timer()
         threads.move_posts()
         threads.move_posts()
-        self.stdout.write(
-            self.style.SUCCESS("Moved posts in %s" % self.stop_timer()))
+        self.stdout.write(self.style.SUCCESS("Moved posts in %s" % self.stop_timer()))
 
 
         self.start_timer()
         self.start_timer()
         threads.move_mentions()
         threads.move_mentions()
-        self.stdout.write(
-            self.style.SUCCESS("Moved mentions in %s" % self.stop_timer()))
+        self.stdout.write(self.style.SUCCESS("Moved mentions in %s" % self.stop_timer()))
 
 
         self.start_timer()
         self.start_timer()
         threads.move_edits()
         threads.move_edits()
-        self.stdout.write(
-            self.style.SUCCESS("Moved edits histories in %s" % self.stop_timer()))
+        self.stdout.write(self.style.SUCCESS("Moved edits histories in %s" % self.stop_timer()))
 
 
         self.start_timer()
         self.start_timer()
         threads.move_likes()
         threads.move_likes()
-        self.stdout.write(
-            self.style.SUCCESS("Moved likes in %s" % self.stop_timer()))
+        self.stdout.write(self.style.SUCCESS("Moved likes in %s" % self.stop_timer()))
 
 
         self.start_timer()
         self.start_timer()
         attachments.move_attachments(self.stdout, self.style)
         attachments.move_attachments(self.stdout, self.style)
-        self.stdout.write(
-            self.style.SUCCESS("Moved attachments in %s" % self.stop_timer()))
+        self.stdout.write(self.style.SUCCESS("Moved attachments in %s" % self.stop_timer()))
 
 
         self.start_timer()
         self.start_timer()
         polls.move_polls()
         polls.move_polls()
-        self.stdout.write(
-            self.style.SUCCESS("Moved polls in %s" % self.stop_timer()))
+        self.stdout.write(self.style.SUCCESS("Moved polls in %s" % self.stop_timer()))
 
 
         self.start_timer()
         self.start_timer()
         threads.move_participants()
         threads.move_participants()
         self.stdout.write(
         self.stdout.write(
-            self.style.SUCCESS("Moved threads participants in %s" % self.stop_timer()))
+            self.style.SUCCESS("Moved threads participants in %s" % self.stop_timer())
+        )
 
 
         self.start_timer()
         self.start_timer()
         threads.clean_private_threads(self.stdout, self.style)
         threads.clean_private_threads(self.stdout, self.style)
-        self.stdout.write(
-            self.style.SUCCESS("Cleaned private threads in %s" % self.stop_timer()))
+        self.stdout.write(self.style.SUCCESS("Cleaned private threads in %s" % self.stop_timer()))
 
 
         self.start_timer()
         self.start_timer()
         markup.clean_posts()
         markup.clean_posts()
-        self.stdout.write(
-            self.style.SUCCESS("Cleaned markup in %s" % self.stop_timer()))
+        self.stdout.write(self.style.SUCCESS("Cleaned markup in %s" % self.stop_timer()))

+ 7 - 16
misago/datamover/management/commands/moveusers.py

@@ -3,40 +3,31 @@ from misago.datamover.management.base import BaseCommand
 
 
 
 
 class Command(BaseCommand):
 class Command(BaseCommand):
-    help = (
-        "Moves users, avatars, followers, blocks "
-        "and bans from Misago 0.5"
-    )
+    help = ("Moves users, avatars, followers, blocks and bans from Misago 0.5")
 
 
     def handle(self, *args, **options):
     def handle(self, *args, **options):
         self.stdout.write("Moving users from Misago 0.5:")
         self.stdout.write("Moving users from Misago 0.5:")
 
 
         self.start_timer()
         self.start_timer()
         users.move_users(self.stdout, self.style)
         users.move_users(self.stdout, self.style)
-        self.stdout.write(
-            self.style.SUCCESS("Moved users in %s" % self.stop_timer()))
+        self.stdout.write(self.style.SUCCESS("Moved users in %s" % self.stop_timer()))
 
 
         self.start_timer()
         self.start_timer()
         avatars.move_avatars(self.stdout, self.style)
         avatars.move_avatars(self.stdout, self.style)
-        self.stdout.write(
-            self.style.SUCCESS("Moved avatars in %s" % self.stop_timer()))
+        self.stdout.write(self.style.SUCCESS("Moved avatars in %s" % self.stop_timer()))
 
 
         self.start_timer()
         self.start_timer()
         users.move_followers()
         users.move_followers()
-        self.stdout.write(
-            self.style.SUCCESS("Moved followers in %s" % self.stop_timer()))
+        self.stdout.write(self.style.SUCCESS("Moved followers in %s" % self.stop_timer()))
 
 
         self.start_timer()
         self.start_timer()
         users.move_blocks()
         users.move_blocks()
-        self.stdout.write(
-            self.style.SUCCESS("Moved blocks in %s" % self.stop_timer()))
+        self.stdout.write(self.style.SUCCESS("Moved blocks in %s" % self.stop_timer()))
 
 
         self.start_timer()
         self.start_timer()
         users.move_namehistory()
         users.move_namehistory()
-        self.stdout.write(
-            self.style.SUCCESS("Moved name history in %s" % self.stop_timer()))
+        self.stdout.write(self.style.SUCCESS("Moved name history in %s" % self.stop_timer()))
 
 
         self.start_timer()
         self.start_timer()
         bans.move_bans()
         bans.move_bans()
-        self.stdout.write(
-            self.style.SUCCESS("Moved bans in %s" % self.stop_timer()))
+        self.stdout.write(self.style.SUCCESS("Moved bans in %s" % self.stop_timer()))

+ 3 - 5
misago/datamover/management/commands/runmigration.py

@@ -3,7 +3,7 @@ from django.core.management import call_command
 from misago.datamover.management.base import BaseCommand
 from misago.datamover.management.base import BaseCommand
 
 
 
 
-MOVE_COMMANDS = (
+MOVE_COMMANDS = [
     'movesettings',
     'movesettings',
     'moveusers',
     'moveusers',
     'movecategories',
     'movecategories',
@@ -15,13 +15,11 @@ MOVE_COMMANDS = (
     'invalidatebans',
     'invalidatebans',
     'populateonlinetracker',
     'populateonlinetracker',
     'synchronizeusers',
     'synchronizeusers',
-)
+]
 
 
 
 
 class Command(BaseCommand):
 class Command(BaseCommand):
-    help = (
-        "Executes complete migration from Misago 0.5 together with cleanups."
-    )
+    help = ("Executes complete migration from Misago 0.5 together with cleanups.")
 
 
     def handle(self, *args, **options):
     def handle(self, *args, **options):
         self.stdout.write("Running complete migration...")
         self.stdout.write("Running complete migration...")

+ 0 - 3
misago/datamover/markup/__init__.py

@@ -29,9 +29,6 @@ def clean_original(post):
     return post
     return post
 
 
 
 
-"""
-Fake request and user for parser
-"""
 class FakeUser(object):
 class FakeUser(object):
     slug = get_random_string(40)
     slug = get_random_string(40)
 
 

+ 1 - 1
misago/datamover/markup/quotes.py

@@ -11,7 +11,7 @@ def convert_quotes_to_bbcode(post):
     quote_author = None
     quote_author = None
     quote = []
     quote = []
 
 
-    for i, line in enumerate(post.splitlines() + ['']):
+    for line in post.splitlines() + ['']:
         if in_quote:
         if in_quote:
             if line.startswith('>'):
             if line.startswith('>'):
                 quote.append(line[1:].lstrip())
                 quote.append(line[1:].lstrip())

+ 11 - 4
misago/datamover/migrations/0001_initial.py

@@ -9,14 +9,17 @@ class Migration(migrations.Migration):
 
 
     initial = True
     initial = True
 
 
-    dependencies = [
-    ]
+    dependencies = []
 
 
     operations = [
     operations = [
         migrations.CreateModel(
         migrations.CreateModel(
             name='MovedId',
             name='MovedId',
             fields=[
             fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                (
+                    'id', models.AutoField(
+                        auto_created=True, primary_key=True, serialize=False, verbose_name='ID'
+                    )
+                ),
                 ('model', models.CharField(max_length=255)),
                 ('model', models.CharField(max_length=255)),
                 ('old_id', models.CharField(max_length=255)),
                 ('old_id', models.CharField(max_length=255)),
                 ('new_id', models.CharField(max_length=255)),
                 ('new_id', models.CharField(max_length=255)),
@@ -25,7 +28,11 @@ class Migration(migrations.Migration):
         migrations.CreateModel(
         migrations.CreateModel(
             name='OldIdRedirect',
             name='OldIdRedirect',
             fields=[
             fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                (
+                    'id', models.AutoField(
+                        auto_created=True, primary_key=True, serialize=False, verbose_name='ID'
+                    )
+                ),
                 ('model', models.PositiveIntegerField()),
                 ('model', models.PositiveIntegerField()),
                 ('old_id', models.PositiveIntegerField()),
                 ('old_id', models.PositiveIntegerField()),
                 ('new_id', models.PositiveIntegerField()),
                 ('new_id', models.PositiveIntegerField()),

+ 1 - 0
misago/datamover/movedids.py

@@ -5,6 +5,7 @@ def get(model, id):
     except MovedId.DoesNotExist:
     except MovedId.DoesNotExist:
         return None
         return None
 
 
+
 def set(model, old_id, new_id):
 def set(model, old_id, new_id):
     from .models import MovedId
     from .models import MovedId
     MovedId.objects.create(
     MovedId.objects.create(

+ 1 - 1
misago/datamover/polls.py

@@ -27,7 +27,7 @@ def move_polls():
             choices.append({
             choices.append({
                 'hash': get_random_string(12),
                 'hash': get_random_string(12),
                 'label': choice['name'],
                 'label': choice['name'],
-                'votes': choice['votes']
+                'votes': choice['votes'],
             })
             })
 
 
             choices_map[choice['id']] = choices[-1]['hash']
             choices_map[choice['id']] = choices[-1]['hash']

+ 10 - 6
misago/datamover/settings.py

@@ -12,6 +12,7 @@ def copy_value(setting):
         setting_obj.value = old_value
         setting_obj.value = old_value
         setting_obj.save()
         setting_obj.save()
         return setting_obj
         return setting_obj
+
     return closure
     return closure
 
 
 
 
@@ -21,6 +22,7 @@ def map_value(setting, translation):
         setting_obj.value = translation[old_value]
         setting_obj.value = translation[old_value]
         setting_obj.save()
         setting_obj.save()
         return setting_obj
         return setting_obj
+
     return closure
     return closure
 
 
 
 
@@ -44,12 +46,14 @@ SETTING_CONVERTER = {
     'thread_name_min': copy_value('thread_title_length_min'),
     'thread_name_min': copy_value('thread_title_length_min'),
     'thread_name_max': copy_value('thread_title_length_max'),
     'thread_name_max': copy_value('thread_title_length_max'),
     'post_length_min': copy_value('post_length_min'),
     'post_length_min': copy_value('post_length_min'),
-    'account_activation': map_value('account_activation', {
-        'none': 'none',
-        'user': 'user',
-        'admin': 'admin',
-        'block': 'closed',
-    }),
+    'account_activation': map_value(
+        'account_activation', {
+            'none': 'none',
+            'user': 'user',
+            'admin': 'admin',
+            'block': 'closed',
+        }
+    ),
     'username_length_min': copy_value('username_length_min'),
     'username_length_min': copy_value('username_length_min'),
     'username_length_max': copy_value('username_length_max'),
     'username_length_max': copy_value('username_length_max'),
     'password_length': copy_value('password_length_min'),
     'password_length': copy_value('password_length_min'),

+ 21 - 33
misago/datamover/threads.py

@@ -4,7 +4,6 @@ from django.contrib.auth import get_user_model
 from django.utils import timezone
 from django.utils import timezone
 
 
 from misago.categories.models import Category
 from misago.categories.models import Category
-from misago.threads.checksums import update_post_checksum
 from misago.threads.models import Post, PostEdit, PostLike, Thread, ThreadParticipant
 from misago.threads.models import Post, PostEdit, PostLike, Thread, ThreadParticipant
 
 
 from . import fetch_assoc, localise_datetime, markup, movedids
 from . import fetch_assoc, localise_datetime, markup, movedids
@@ -18,13 +17,11 @@ def move_threads(stdout, style):
 
 
     for thread in fetch_assoc('SELECT * FROM misago_thread ORDER BY id'):
     for thread in fetch_assoc('SELECT * FROM misago_thread ORDER BY id'):
         if special_categories.get(thread['forum_id']) == 'reports':
         if special_categories.get(thread['forum_id']) == 'reports':
-            stdout.write(style.WARNING(
-                "Skipping report: %s" % thread['name']))
+            stdout.write(style.WARNING("Skipping report: %s" % thread['name']))
             continue
             continue
 
 
         if not thread['start_post_id']:
         if not thread['start_post_id']:
-            stdout.write(style.ERROR(
-                "Corrupted thread: %s" % thread['name']))
+            stdout.write(style.ERROR("Corrupted thread: %s" % thread['name']))
             continue
             continue
 
 
         if special_categories.get(thread['forum_id']) == 'private_threads':
         if special_categories.get(thread['forum_id']) == 'private_threads':
@@ -78,9 +75,7 @@ def move_posts():
         deleter_name = None
         deleter_name = None
         deleter_slug = None
         deleter_slug = None
         if post['deleted']:
         if post['deleted']:
-            deleter = UserModel.objects.filter(
-                is_staff=True
-            ).order_by('id').last()
+            deleter = UserModel.objects.filter(is_staff=True).order_by('id').last()
 
 
             if deleter:
             if deleter:
                 deleter_name = deleter.username
                 deleter_name = deleter.username
@@ -161,18 +156,20 @@ def move_post_edits(post, old_id):
         if changelog:
         if changelog:
             changelog[-1].edited_to = markup.clean_original(edit['post_content'])
             changelog[-1].edited_to = markup.clean_original(edit['post_content'])
 
 
-        changelog.append(PostEdit(
-            category=post.category,
-            thread=post.thread,
-            post=post,
-            edited_on=localise_datetime(edit['date']),
-            editor=editor,
-            editor_name=edit['user_name'],
-            editor_slug=edit['user_slug'],
-            editor_ip=edit['ip'],
-            edited_from=markup.clean_original(edit['post_content']),
-            edited_to=markup.clean_original(post.original),
-        ))
+        changelog.append(
+            PostEdit(
+                category=post.category,
+                thread=post.thread,
+                post=post,
+                edited_on=localise_datetime(edit['date']),
+                editor=editor,
+                editor_name=edit['user_name'],
+                editor_slug=edit['user_slug'],
+                editor_ip=edit['ip'],
+                edited_from=markup.clean_original(edit['post_content']),
+                edited_to=markup.clean_original(post.original),
+            )
+        )
 
 
     if changelog:
     if changelog:
         PostEdit.objects.bulk_create(changelog)
         PostEdit.objects.bulk_create(changelog)
@@ -216,10 +213,7 @@ def move_likes():
     for post in Post.objects.filter(id__in=posts).iterator():
     for post in Post.objects.filter(id__in=posts).iterator():
         post.last_likes = []
         post.last_likes = []
         for like in post.postlike_set.all()[:4]:
         for like in post.postlike_set.all()[:4]:
-            post.last_likes.append({
-                'id': like.liker_id,
-                'username': like.liker_name
-            })
+            post.last_likes.append({'id': like.liker_id, 'username': like.liker_name})
         post.save(update_fields=['last_likes'])
         post.save(update_fields=['last_likes'])
 
 
 
 
@@ -233,19 +227,14 @@ def move_participants():
 
 
         starter = thread.post_set.order_by('id').first().poster
         starter = thread.post_set.order_by('id').first().poster
 
 
-        ThreadParticipant.objects.create(
-            thread=thread,
-            user=user,
-            is_owner=(user == starter)
-        )
+        ThreadParticipant.objects.create(thread=thread, user=user, is_owner=(user == starter))
 
 
 
 
 def clean_private_threads(stdout, style):
 def clean_private_threads(stdout, style):
     category = Category.objects.private_threads()
     category = Category.objects.private_threads()
 
 
     # prune threads without participants
     # prune threads without participants
-    participated_threads = ThreadParticipant.objects.values_list(
-        'thread_id', flat=True).distinct()
+    participated_threads = ThreadParticipant.objects.values_list('thread_id', flat=True).distinct()
     for thread in category.thread_set.exclude(pk__in=participated_threads):
     for thread in category.thread_set.exclude(pk__in=participated_threads):
         thread.delete()
         thread.delete()
 
 
@@ -257,8 +246,7 @@ def clean_private_threads(stdout, style):
             thread.save()
             thread.save()
         elif participants_count == 0:
         elif participants_count == 0:
             thread.delete()
             thread.delete()
-            stdout.write(style.ERROR(
-                "Delete empty private thread: %s" % thread.title))
+            stdout.write(style.ERROR("Delete empty private thread: %s" % thread.title))
 
 
 
 
 def get_special_categories_dict():
 def get_special_categories_dict():

+ 194 - 51
misago/datamover/urls.py

@@ -6,9 +6,18 @@ from . import views
 urlpatterns = [
 urlpatterns = [
     url(r'^category/(?P<slug>(\w|-)+)-(?P<forum>\d+)/$', views.category_redirect),
     url(r'^category/(?P<slug>(\w|-)+)-(?P<forum>\d+)/$', views.category_redirect),
     url(r'^forum/(?P<slug>(\w|-)+)-(?P<forum>\d+)/$', views.category_redirect),
     url(r'^forum/(?P<slug>(\w|-)+)-(?P<forum>\d+)/$', views.category_redirect),
-    url(r'^forum/(?P<slug>(\w|-)+)-(?P<forum>\d+)/(?P<page>[1-9]([0-9]+)?)/$', views.category_redirect),
-    url(r'^forum/(?P<slug>(\w|-)+)-(?P<forum>\d+)/prefix/(?P<prefix>(\w|-)+)/$', views.category_redirect),
-    url(r'^forum/(?P<slug>(\w|-)+)-(?P<forum>\d+)/prefix/(?P<prefix>(\w|-)+)/(?P<page>[1-9]([0-9]+)?)/$', views.category_redirect),
+    url(
+        r'^forum/(?P<slug>(\w|-)+)-(?P<forum>\d+)/(?P<page>[1-9]([0-9]+)?)/$',
+        views.category_redirect
+    ),
+    url(
+        r'^forum/(?P<slug>(\w|-)+)-(?P<forum>\d+)/prefix/(?P<prefix>(\w|-)+)/$',
+        views.category_redirect
+    ),
+    url(
+        r'^forum/(?P<slug>(\w|-)+)-(?P<forum>\d+)/prefix/(?P<prefix>(\w|-)+)/(?P<page>[1-9]([0-9]+)?)/$',
+        views.category_redirect
+    ),
     url(r'^forum/(?P<slug>(\w|-)+)-(?P<forum>\d+)/start/$', views.category_redirect),
     url(r'^forum/(?P<slug>(\w|-)+)-(?P<forum>\d+)/start/$', views.category_redirect),
 ]
 ]
 
 
@@ -20,7 +29,10 @@ urlpatterns += [
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/reply/$', views.thread_redirect),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/reply/$', views.thread_redirect),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/edit/$', views.thread_redirect),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/edit/$', views.thread_redirect),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/$', views.thread_redirect),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/$', views.thread_redirect),
-    url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<page>[1-9]([0-9]+)?)/$', views.thread_redirect),
+    url(
+        r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<page>[1-9]([0-9]+)?)/$',
+        views.thread_redirect
+    ),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/last/$', views.thread_redirect),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/last/$', views.thread_redirect),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/find-(?P<post>\d+)/$', views.thread_redirect),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/find-(?P<post>\d+)/$', views.thread_redirect),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/new/$', views.thread_redirect),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/new/$', views.thread_redirect),
@@ -31,68 +43,199 @@ urlpatterns += [
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/watch/email/$', views.thread_redirect),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/watch/email/$', views.thread_redirect),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/unwatch/$', views.thread_redirect),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/unwatch/$', views.thread_redirect),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/unwatch/email/$', views.thread_redirect),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/unwatch/email/$', views.thread_redirect),
-    url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/upvote/$', views.thread_redirect),
-    url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/downvote/$', views.thread_redirect),
-    url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/report/$', views.thread_redirect),
-    url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/show-report/$', views.thread_redirect),
+    url(
+        r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/upvote/$', views.thread_redirect
+    ),
+    url(
+        r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/downvote/$',
+        views.thread_redirect
+    ),
+    url(
+        r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/report/$', views.thread_redirect
+    ),
+    url(
+        r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/show-report/$',
+        views.thread_redirect
+    ),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/delete/$', views.thread_redirect),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/delete/$', views.thread_redirect),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/hide/$', views.thread_redirect),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/hide/$', views.thread_redirect),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/show/$', views.thread_redirect),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/show/$', views.thread_redirect),
-    url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/delete/$', views.thread_redirect),
+    url(
+        r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/delete/$', views.thread_redirect
+    ),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/hide/$', views.thread_redirect),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/hide/$', views.thread_redirect),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/show/$', views.thread_redirect),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/show/$', views.thread_redirect),
-    url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/checkpoint/(?P<checkpoint>\d+)/delete/$', views.thread_redirect),
-    url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/checkpoint/(?P<checkpoint>\d+)/hide/$', views.thread_redirect),
-    url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/checkpoint/(?P<checkpoint>\d+)/show/$', views.thread_redirect),
+    url(
+        r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/checkpoint/(?P<checkpoint>\d+)/delete/$',
+        views.thread_redirect
+    ),
+    url(
+        r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/checkpoint/(?P<checkpoint>\d+)/hide/$',
+        views.thread_redirect
+    ),
+    url(
+        r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/checkpoint/(?P<checkpoint>\d+)/show/$',
+        views.thread_redirect
+    ),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/info/$', views.thread_redirect),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/info/$', views.thread_redirect),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/votes/$', views.thread_redirect),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/votes/$', views.thread_redirect),
-    url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/changelog/$', views.thread_redirect),
-    url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/changelog/(?P<change>\d+)/$', views.thread_redirect),
-    url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/changelog/(?P<change>\d+)/revert/$', views.thread_redirect),
+    url(
+        r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/changelog/$',
+        views.thread_redirect
+    ),
+    url(
+        r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/changelog/(?P<change>\d+)/$',
+        views.thread_redirect
+    ),
+    url(
+        r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/changelog/(?P<change>\d+)/revert/$',
+        views.thread_redirect
+    ),
 ]
 ]
 
 
 urlpatterns += [
 urlpatterns += [
-    url(r'^private-threads/(?P<slug>(\w|-)+)-(?P<thread>\d+)/edit/$', views.private_thread_redirect),
-    url(r'^private-threads/(?P<slug>(\w|-)+)-(?P<thread>\d+)/reply/$', views.private_thread_redirect),
-    url(r'^private-threads/(?P<slug>(\w|-)+)-(?P<thread>\d+)/vote/$', views.private_thread_redirect),
-    url(r'^private-threads/(?P<slug>(\w|-)+)-(?P<thread>\d+)/poll/$', views.private_thread_redirect),
-    url(r'^private-threads/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/reply/$', views.private_thread_redirect),
-    url(r'^private-threads/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/edit/$', views.private_thread_redirect),
+    url(
+        r'^private-threads/(?P<slug>(\w|-)+)-(?P<thread>\d+)/edit/$', views.private_thread_redirect
+    ),
+    url(
+        r'^private-threads/(?P<slug>(\w|-)+)-(?P<thread>\d+)/reply/$',
+        views.private_thread_redirect
+    ),
+    url(
+        r'^private-threads/(?P<slug>(\w|-)+)-(?P<thread>\d+)/vote/$', views.private_thread_redirect
+    ),
+    url(
+        r'^private-threads/(?P<slug>(\w|-)+)-(?P<thread>\d+)/poll/$', views.private_thread_redirect
+    ),
+    url(
+        r'^private-threads/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/reply/$',
+        views.private_thread_redirect
+    ),
+    url(
+        r'^private-threads/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/edit/$',
+        views.private_thread_redirect
+    ),
     url(r'^private-threads/(?P<slug>(\w|-)+)-(?P<thread>\d+)/$', views.private_thread_redirect),
     url(r'^private-threads/(?P<slug>(\w|-)+)-(?P<thread>\d+)/$', views.private_thread_redirect),
-    url(r'^private-threads/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<page>[1-9]([0-9]+)?)/$', views.private_thread_redirect),
-    url(r'^private-threads/(?P<slug>(\w|-)+)-(?P<thread>\d+)/last/$', views.private_thread_redirect),
-    url(r'^private-threads/(?P<slug>(\w|-)+)-(?P<thread>\d+)/find-(?P<post>\d+)/$', views.private_thread_redirect),
-    url(r'^private-threads/(?P<slug>(\w|-)+)-(?P<thread>\d+)/new/$', views.private_thread_redirect),
-    url(r'^private-threads/(?P<slug>(\w|-)+)-(?P<thread>\d+)/moderated/$', views.private_thread_redirect),
-    url(r'^private-threads/(?P<slug>(\w|-)+)-(?P<thread>\d+)/reported/$', views.private_thread_redirect),
-    url(r'^private-threads/(?P<slug>(\w|-)+)-(?P<thread>\d+)/show-hidden/$', views.private_thread_redirect),
-    url(r'^private-threads/(?P<slug>(\w|-)+)-(?P<thread>\d+)/watch/$', views.private_thread_redirect),
-    url(r'^private-threads/(?P<slug>(\w|-)+)-(?P<thread>\d+)/watch/email/$', views.private_thread_redirect),
-    url(r'^private-threads/(?P<slug>(\w|-)+)-(?P<thread>\d+)/unwatch/$', views.private_thread_redirect),
-    url(r'^private-threads/(?P<slug>(\w|-)+)-(?P<thread>\d+)/unwatch/email/$', views.private_thread_redirect),
-    url(r'^private-threads/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/upvote/$', views.private_thread_redirect),
-    url(r'^private-threads/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/downvote/$', views.private_thread_redirect),
-    url(r'^private-threads/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/report/$', views.private_thread_redirect),
-    url(r'^private-threads/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/show-report/$', views.private_thread_redirect),
-    url(r'^private-threads/(?P<slug>(\w|-)+)-(?P<thread>\d+)/delete/$', views.private_thread_redirect),
-    url(r'^private-threads/(?P<slug>(\w|-)+)-(?P<thread>\d+)/hide/$', views.private_thread_redirect),
-    url(r'^private-threads/(?P<slug>(\w|-)+)-(?P<thread>\d+)/show/$', views.private_thread_redirect),
-    url(r'^private-threads/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/delete/$', views.private_thread_redirect),
-    url(r'^private-threads/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/hide/$', views.private_thread_redirect),
-    url(r'^private-threads/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/show/$', views.private_thread_redirect),
-    url(r'^private-threads/(?P<slug>(\w|-)+)-(?P<thread>\d+)/checkpoint/(?P<checkpoint>\d+)/delete/$', views.private_thread_redirect),
-    url(r'^private-threads/(?P<slug>(\w|-)+)-(?P<thread>\d+)/checkpoint/(?P<checkpoint>\d+)/hide/$', views.private_thread_redirect),
-    url(r'^private-threads/(?P<slug>(\w|-)+)-(?P<thread>\d+)/checkpoint/(?P<checkpoint>\d+)/show/$', views.private_thread_redirect),
-    url(r'^private-threads/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/info/$', views.private_thread_redirect),
-    url(r'^private-threads/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/votes/$', views.private_thread_redirect),
-    url(r'^private-threads/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/changelog/$', views.private_thread_redirect),
-    url(r'^private-threads/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/changelog/(?P<change>\d+)/$', views.private_thread_redirect),
-    url(r'^private-threads/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/changelog/(?P<change>\d+)/revert/$', views.private_thread_redirect),
+    url(
+        r'^private-threads/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<page>[1-9]([0-9]+)?)/$',
+        views.private_thread_redirect
+    ),
+    url(
+        r'^private-threads/(?P<slug>(\w|-)+)-(?P<thread>\d+)/last/$', views.private_thread_redirect
+    ),
+    url(
+        r'^private-threads/(?P<slug>(\w|-)+)-(?P<thread>\d+)/find-(?P<post>\d+)/$',
+        views.private_thread_redirect
+    ),
+    url(
+        r'^private-threads/(?P<slug>(\w|-)+)-(?P<thread>\d+)/new/$', views.private_thread_redirect
+    ),
+    url(
+        r'^private-threads/(?P<slug>(\w|-)+)-(?P<thread>\d+)/moderated/$',
+        views.private_thread_redirect
+    ),
+    url(
+        r'^private-threads/(?P<slug>(\w|-)+)-(?P<thread>\d+)/reported/$',
+        views.private_thread_redirect
+    ),
+    url(
+        r'^private-threads/(?P<slug>(\w|-)+)-(?P<thread>\d+)/show-hidden/$',
+        views.private_thread_redirect
+    ),
+    url(
+        r'^private-threads/(?P<slug>(\w|-)+)-(?P<thread>\d+)/watch/$',
+        views.private_thread_redirect
+    ),
+    url(
+        r'^private-threads/(?P<slug>(\w|-)+)-(?P<thread>\d+)/watch/email/$',
+        views.private_thread_redirect
+    ),
+    url(
+        r'^private-threads/(?P<slug>(\w|-)+)-(?P<thread>\d+)/unwatch/$',
+        views.private_thread_redirect
+    ),
+    url(
+        r'^private-threads/(?P<slug>(\w|-)+)-(?P<thread>\d+)/unwatch/email/$',
+        views.private_thread_redirect
+    ),
+    url(
+        r'^private-threads/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/upvote/$',
+        views.private_thread_redirect
+    ),
+    url(
+        r'^private-threads/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/downvote/$',
+        views.private_thread_redirect
+    ),
+    url(
+        r'^private-threads/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/report/$',
+        views.private_thread_redirect
+    ),
+    url(
+        r'^private-threads/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/show-report/$',
+        views.private_thread_redirect
+    ),
+    url(
+        r'^private-threads/(?P<slug>(\w|-)+)-(?P<thread>\d+)/delete/$',
+        views.private_thread_redirect
+    ),
+    url(
+        r'^private-threads/(?P<slug>(\w|-)+)-(?P<thread>\d+)/hide/$', views.private_thread_redirect
+    ),
+    url(
+        r'^private-threads/(?P<slug>(\w|-)+)-(?P<thread>\d+)/show/$', views.private_thread_redirect
+    ),
+    url(
+        r'^private-threads/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/delete/$',
+        views.private_thread_redirect
+    ),
+    url(
+        r'^private-threads/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/hide/$',
+        views.private_thread_redirect
+    ),
+    url(
+        r'^private-threads/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/show/$',
+        views.private_thread_redirect
+    ),
+    url(
+        r'^private-threads/(?P<slug>(\w|-)+)-(?P<thread>\d+)/checkpoint/(?P<checkpoint>\d+)/delete/$',
+        views.private_thread_redirect
+    ),
+    url(
+        r'^private-threads/(?P<slug>(\w|-)+)-(?P<thread>\d+)/checkpoint/(?P<checkpoint>\d+)/hide/$',
+        views.private_thread_redirect
+    ),
+    url(
+        r'^private-threads/(?P<slug>(\w|-)+)-(?P<thread>\d+)/checkpoint/(?P<checkpoint>\d+)/show/$',
+        views.private_thread_redirect
+    ),
+    url(
+        r'^private-threads/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/info/$',
+        views.private_thread_redirect
+    ),
+    url(
+        r'^private-threads/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/votes/$',
+        views.private_thread_redirect
+    ),
+    url(
+        r'^private-threads/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/changelog/$',
+        views.private_thread_redirect
+    ),
+    url(
+        r'^private-threads/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/changelog/(?P<change>\d+)/$',
+        views.private_thread_redirect
+    ),
+    url(
+        r'^private-threads/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/changelog/(?P<change>\d+)/revert/$',
+        views.private_thread_redirect
+    ),
 ]
 ]
 
 
 urlpatterns += [
 urlpatterns += [
     url(r'^users/(?P<username>\w+)-(?P<user>\d+)/', views.user_redirect),
     url(r'^users/(?P<username>\w+)-(?P<user>\d+)/', views.user_redirect),
     url(r'^users/(?P<username>\w+)-(?P<user>\d+)/(?P<page>\d+)/', views.user_redirect),
     url(r'^users/(?P<username>\w+)-(?P<user>\d+)/(?P<page>\d+)/', views.user_redirect),
     url(r'^users/(?P<username>\w+)-(?P<user>\d+)/(?P<subpage>(\w|-)+)/', views.user_redirect),
     url(r'^users/(?P<username>\w+)-(?P<user>\d+)/(?P<subpage>(\w|-)+)/', views.user_redirect),
-    url(r'^users/(?P<username>\w+)-(?P<user>\d+)/(?P<subpage>(\w|-)+)/(?P<page>\d+)/', views.user_redirect),
+    url(
+        r'^users/(?P<username>\w+)-(?P<user>\d+)/(?P<subpage>(\w|-)+)/(?P<page>\d+)/',
+        views.user_redirect
+    ),
 ]
 ]

+ 17 - 16
misago/datamover/users.py

@@ -12,12 +12,11 @@ from . import fetch_assoc, localise_datetime, movedids
 
 
 UserModel = get_user_model()
 UserModel = get_user_model()
 
 
-
 PRIVATE_THREAD_INVITES = {
 PRIVATE_THREAD_INVITES = {
     0: 0,
     0: 0,
     1: 0,
     1: 0,
     2: 1,
     2: 1,
-    3: 2
+    3: 2,
 }
 }
 
 
 
 
@@ -31,15 +30,14 @@ def move_users(stdout, style):
         else:
         else:
             try:
             try:
                 new_user = UserModel.objects.create_user(
                 new_user = UserModel.objects.create_user(
-                    user['username'], user['email'], 'Pass.123')
+                    user['username'], user['email'], 'Pass.123'
+                )
             except ValidationError:
             except ValidationError:
                 new_name = ''.join([user['username'][:10], get_random_string(4)])
                 new_name = ''.join([user['username'][:10], get_random_string(4)])
-                new_user = UserModel.objects.create_user(
-                    new_name, user['email'], 'Pass.123')
+                new_user = UserModel.objects.create_user(new_name, user['email'], 'Pass.123')
 
 
                 formats = (user['username'], new_name)
                 formats = (user['username'], new_name)
-                stdout.write(style.ERROR(
-                    '"%s" has been registered as "%s"' % formats))
+                stdout.write(style.ERROR('"%s" has been registered as "%s"' % formats))
 
 
             new_user.password = user['password']
             new_user.password = user['password']
 
 
@@ -64,7 +62,8 @@ def move_users(stdout, style):
             new_user.signature = user['signature']
             new_user.signature = user['signature']
             new_user.signature_parsed = user['signature_preparsed']
             new_user.signature_parsed = user['signature_preparsed']
             new_user.signature_checksum = make_signature_checksum(
             new_user.signature_checksum = make_signature_checksum(
-                user['signature_preparsed'], new_user)
+                user['signature_preparsed'], new_user
+            )
 
 
         new_user.is_signature_locked = user['signature_ban']
         new_user.is_signature_locked = user['signature_ban']
         new_user.signature_lock_user_message = user['signature_ban_reason_user'] or None
         new_user.signature_lock_user_message = user['signature_ban_reason_user'] or None
@@ -125,13 +124,15 @@ def move_users_namehistory(user, old_id):
         if username_history:
         if username_history:
             username_history[-1].new_username = namechange['old_username']
             username_history[-1].new_username = namechange['old_username']
 
 
-        username_history.append(UsernameChange(
-            user=user,
-            changed_by=user,
-            changed_by_username=user.username,
-            changed_on=localise_datetime(namechange['date']),
-            new_username=user.username,
-            old_username=namechange['old_username']
-        ))
+        username_history.append(
+            UsernameChange(
+                user=user,
+                changed_by=user,
+                changed_by_username=user.username,
+                changed_on=localise_datetime(namechange['date']),
+                new_username=user.username,
+                old_username=namechange['old_username']
+            )
+        )
 
 
     UsernameChange.objects.bulk_create(username_history)
     UsernameChange.objects.bulk_create(username_history)

+ 1 - 2
misago/faker/management/commands/createfakebans.py

@@ -6,7 +6,6 @@ from faker import Factory
 
 
 from django.core.management.base import BaseCommand
 from django.core.management.base import BaseCommand
 from django.utils import timezone
 from django.utils import timezone
-from django.utils.six.moves import range
 
 
 from misago.core.management.progressbar import show_progress
 from misago.core.management.progressbar import show_progress
 from misago.users.models import Ban
 from misago.users.models import Ban
@@ -98,7 +97,7 @@ class Command(BaseCommand):
 
 
         created_count = 0
         created_count = 0
         show_progress(self, created_count, fake_bans_to_create)
         show_progress(self, created_count, fake_bans_to_create)
-        for i in range(fake_bans_to_create):
+        for _ in range(fake_bans_to_create):
             ban = Ban(check_type=random.randint(Ban.USERNAME, Ban.IP))
             ban = Ban(check_type=random.randint(Ban.USERNAME, Ban.IP))
             ban.banned_value = create_fake_test(fake, ban.check_type)
             ban.banned_value = create_fake_test(fake, ban.check_type)
 
 

+ 12 - 12
misago/faker/management/commands/createfakecategories.py

@@ -1,11 +1,9 @@
 import random
 import random
-import sys
 import time
 import time
 
 
 from faker import Factory
 from faker import Factory
 
 
 from django.core.management.base import BaseCommand
 from django.core.management.base import BaseCommand
-from django.utils.six.moves import range
 
 
 from misago.acl import version as acl_version
 from misago.acl import version as acl_version
 from misago.categories.models import Category, RoleCategoryACL
 from misago.categories.models import Category, RoleCategoryACL
@@ -21,7 +19,7 @@ class Command(BaseCommand):
             help="number of categories to create",
             help="number of categories to create",
             nargs='?',
             nargs='?',
             type=int,
             type=int,
-            default=5
+            default=5,
         )
         )
 
 
         parser.add_argument(
         parser.add_argument(
@@ -29,7 +27,7 @@ class Command(BaseCommand):
             help="min. level of created categories",
             help="min. level of created categories",
             nargs='?',
             nargs='?',
             type=int,
             type=int,
-            default=0
+            default=0,
         )
         )
 
 
     def handle(self, *args, **options):
     def handle(self, *args, **options):
@@ -67,25 +65,27 @@ class Command(BaseCommand):
                 else:
                 else:
                     new_category.description = fake.paragraph()
                     new_category.description = fake.paragraph()
 
 
-            new_category.insert_at(parent,
+            new_category.insert_at(
+                parent,
                 position='last-child',
                 position='last-child',
                 save=True,
                 save=True,
             )
             )
 
 
             copied_acls = []
             copied_acls = []
             for acl in copy_acl_from.category_role_set.all():
             for acl in copy_acl_from.category_role_set.all():
-                copied_acls.append(RoleCategoryACL(
-                    role_id=acl.role_id,
-                    category=new_category,
-                    category_role_id=acl.category_role_id,
-                ))
+                copied_acls.append(
+                    RoleCategoryACL(
+                        role_id=acl.role_id,
+                        category=new_category,
+                        category_role_id=acl.category_role_id,
+                    )
+                )
 
 
             if copied_acls:
             if copied_acls:
                 RoleCategoryACL.objects.bulk_create(copied_acls)
                 RoleCategoryACL.objects.bulk_create(copied_acls)
 
 
             created_count += 1
             created_count += 1
-            show_progress(
-                self, created_count, items_to_create, start_time)
+            show_progress(self, created_count, items_to_create, start_time)
 
 
         acl_version.invalidate()
         acl_version.invalidate()
 
 

+ 1 - 1
misago/faker/management/commands/createfakefollowers.py

@@ -32,7 +32,7 @@ class Command(BaseCommand):
             if random.randint(1, 100) > 10:
             if random.randint(1, 100) > 10:
                 processed_count += 1
                 processed_count += 1
                 show_progress(self, processed_count, total_users)
                 show_progress(self, processed_count, total_users)
-                continue # 10% active users
+                continue  # 10% active users
 
 
             users_to_add = random.randint(1, total_users / 5)
             users_to_add = random.randint(1, total_users / 5)
             random_queryset = UserModel.objects.exclude(id=user.id).order_by('?')
             random_queryset = UserModel.objects.exclude(id=user.id).order_by('?')

+ 8 - 14
misago/faker/management/commands/createfakethreads.py

@@ -9,7 +9,6 @@ from django.contrib.auth import get_user_model
 from django.core.management.base import BaseCommand
 from django.core.management.base import BaseCommand
 from django.db.transaction import atomic
 from django.db.transaction import atomic
 from django.utils import timezone
 from django.utils import timezone
-from django.utils.six.moves import range
 
 
 from misago.categories.models import Category
 from misago.categories.models import Category
 from misago.core.management.progressbar import show_progress
 from misago.core.management.progressbar import show_progress
@@ -20,10 +19,8 @@ from misago.threads.models import Post, Thread
 
 
 PLACEKITTEN_URL = 'https://placekitten.com/g/%s/%s'
 PLACEKITTEN_URL = 'https://placekitten.com/g/%s/%s'
 
 
-
 UserModel = get_user_model()
 UserModel = get_user_model()
 
 
-
 corpus = EnglishCorpus()
 corpus = EnglishCorpus()
 corpus_short = EnglishCorpus(max_length=150)
 corpus_short = EnglishCorpus(max_length=150)
 
 
@@ -37,7 +34,7 @@ class Command(BaseCommand):
             help="number of threads to create",
             help="number of threads to create",
             nargs='?',
             nargs='?',
             type=int,
             type=int,
-            default=5
+            default=5,
         )
         )
 
 
     def handle(self, *args, **options):
     def handle(self, *args, **options):
@@ -47,8 +44,6 @@ class Command(BaseCommand):
 
 
         fake = Factory.create()
         fake = Factory.create()
 
 
-        total_users = UserModel.objects.count()
-
         self.stdout.write('Creating fake threads...\n')
         self.stdout.write('Creating fake threads...\n')
 
 
         message = '\nSuccessfully created %s fake threads in %s'
         message = '\nSuccessfully created %s fake threads in %s'
@@ -78,7 +73,7 @@ class Command(BaseCommand):
                     replies=0,
                     replies=0,
                     is_unapproved=thread_is_unapproved,
                     is_unapproved=thread_is_unapproved,
                     is_hidden=thread_is_hidden,
                     is_hidden=thread_is_hidden,
-                    is_closed=thread_is_closed
+                    is_closed=thread_is_closed,
                 )
                 )
                 thread.set_title(corpus_short.random_choice())
                 thread.set_title(corpus_short.random_choice())
                 thread.save()
                 thread.save()
@@ -94,7 +89,7 @@ class Command(BaseCommand):
                     original=original,
                     original=original,
                     parsed=parsed,
                     parsed=parsed,
                     posted_on=datetime,
                     posted_on=datetime,
-                    updated_on=datetime
+                    updated_on=datetime,
                 )
                 )
                 update_post_checksum(post)
                 update_post_checksum(post)
                 post.save(update_fields=['checksum'])
                 post.save(update_fields=['checksum'])
@@ -115,7 +110,7 @@ class Command(BaseCommand):
                 else:
                 else:
                     thread_replies = random.randint(0, 10)
                     thread_replies = random.randint(0, 10)
 
 
-                for x in range(thread_replies):
+                for _ in range(thread_replies):
                     datetime = timezone.now()
                     datetime = timezone.now()
                     user = UserModel.objects.order_by('?')[:1][0]
                     user = UserModel.objects.order_by('?')[:1][0]
 
 
@@ -133,7 +128,7 @@ class Command(BaseCommand):
                         parsed=parsed,
                         parsed=parsed,
                         is_unapproved=is_unapproved,
                         is_unapproved=is_unapproved,
                         posted_on=datetime,
                         posted_on=datetime,
-                        updated_on=datetime
+                        updated_on=datetime,
                     )
                     )
 
 
                     if not is_unapproved:
                     if not is_unapproved:
@@ -163,12 +158,11 @@ class Command(BaseCommand):
                 thread.save()
                 thread.save()
 
 
                 created_threads += 1
                 created_threads += 1
-                show_progress(
-                    self, created_threads, items_to_create, start_time)
+                show_progress(self, created_threads, items_to_create, start_time)
 
 
         pinned_threads = random.randint(0, int(created_threads * 0.025)) or 1
         pinned_threads = random.randint(0, int(created_threads * 0.025)) or 1
         self.stdout.write('\nPinning %s threads...' % pinned_threads)
         self.stdout.write('\nPinning %s threads...' % pinned_threads)
-        for i in range(0, pinned_threads):
+        for _ in range(0, pinned_threads):
             thread = Thread.objects.order_by('?')[:1][0]
             thread = Thread.objects.order_by('?')[:1][0]
             if random.randint(0, 100) > 75:
             if random.randint(0, 100) > 75:
                 thread.weight = 2
                 thread.weight = 2
@@ -193,7 +187,7 @@ class Command(BaseCommand):
         else:
         else:
             paragraphs_to_make = random.randint(1, 5)
             paragraphs_to_make = random.randint(1, 5)
 
 
-        for i in range(paragraphs_to_make):
+        for _ in range(paragraphs_to_make):
             if random.randint(0, 100) > 95:
             if random.randint(0, 100) > 95:
                 cat_width = random.randint(1, 16) * random.choice([100, 90, 80])
                 cat_width = random.randint(1, 16) * random.choice([100, 90, 80])
                 cat_height = random.randint(1, 12) * random.choice([100, 90, 80])
                 cat_height = random.randint(1, 12) * random.choice([100, 90, 80])

+ 8 - 11
misago/faker/management/commands/createfakeusers.py

@@ -1,5 +1,4 @@
 import random
 import random
-import sys
 import time
 import time
 
 
 from faker import Factory
 from faker import Factory
@@ -8,7 +7,6 @@ from django.contrib.auth import get_user_model
 from django.core.exceptions import ValidationError
 from django.core.exceptions import ValidationError
 from django.core.management.base import BaseCommand
 from django.core.management.base import BaseCommand
 from django.db import IntegrityError
 from django.db import IntegrityError
-from django.utils.six.moves import range
 
 
 from misago.core.management.progressbar import show_progress
 from misago.core.management.progressbar import show_progress
 from misago.users.avatars import dynamic, gallery
 from misago.users.avatars import dynamic, gallery
@@ -27,7 +25,7 @@ class Command(BaseCommand):
             help="number of users to create",
             help="number of users to create",
             nargs='?',
             nargs='?',
             type=int,
             type=int,
-            default=5
+            default=5,
         )
         )
 
 
     def handle(self, *args, **options):
     def handle(self, *args, **options):
@@ -48,13 +46,13 @@ class Command(BaseCommand):
 
 
         while created_count < items_to_create:
         while created_count < items_to_create:
             try:
             try:
-                kwargs = {
-                    'rank': random.choice(ranks),
-                }
-
                 user = UserModel.objects.create_user(
                 user = UserModel.objects.create_user(
-                    fake.first_name(), fake.email(), 'pass123',
-                    set_default_avatar=False, **kwargs)
+                    fake.first_name(),
+                    fake.email(),
+                    'pass123',
+                    set_default_avatar=False,
+                    rank=random.choice(ranks),
+                )
 
 
                 if random.randint(0, 100) > 90:
                 if random.randint(0, 100) > 90:
                     dynamic.set_avatar(user)
                     dynamic.set_avatar(user)
@@ -65,8 +63,7 @@ class Command(BaseCommand):
                 pass
                 pass
             else:
             else:
                 created_count += 1
                 created_count += 1
-                show_progress(
-                    self, created_count, items_to_create, start_time)
+                show_progress(self, created_count, items_to_create, start_time)
 
 
         total_time = time.time() - start_time
         total_time = time.time() - start_time
         total_humanized = time.strftime('%H:%M:%S', time.gmtime(total_time))
         total_humanized = time.strftime('%H:%M:%S', time.gmtime(total_time))

+ 1 - 2
misago/legal/context_processors.py

@@ -9,8 +9,7 @@ def legal_links(request):
     if settings.terms_of_service_link:
     if settings.terms_of_service_link:
         legal_context['TERMS_OF_SERVICE_URL'] = settings.terms_of_service_link
         legal_context['TERMS_OF_SERVICE_URL'] = settings.terms_of_service_link
     elif settings.terms_of_service:
     elif settings.terms_of_service:
-        legal_context['TERMS_OF_SERVICE_URL'] = reverse(
-            'misago:terms-of-service')
+        legal_context['TERMS_OF_SERVICE_URL'] = reverse('misago:terms-of-service')
 
 
     if settings.privacy_policy_link:
     if settings.privacy_policy_link:
         legal_context['PRIVACY_POLICY_URL'] = settings.privacy_policy_link
         legal_context['PRIVACY_POLICY_URL'] = settings.privacy_policy_link

+ 97 - 97
misago/legal/migrations/0001_initial.py

@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 # -*- coding: utf-8 -*-
 from __future__ import unicode_literals
 from __future__ import unicode_literals
 
 
-from django.db import migrations, models
+from django.db import migrations
 
 
 from misago.conf.migrationutils import migrate_settings_group
 from misago.conf.migrationutils import migrate_settings_group
 
 
@@ -10,109 +10,109 @@ _ = lambda x: x
 
 
 
 
 def create_legal_settings_group(apps, schema_editor):
 def create_legal_settings_group(apps, schema_editor):
-    migrate_settings_group(apps, {
-        'key': 'legal',
-        'name': _("Legal information"),
-        'description': _("Those settings allow you to set forum terms of "
-                         "service and privacy policy"),
-        'settings': (
-            {
-                'setting': 'terms_of_service_title',
-                'name': _("Terms title"),
-                'legend': _("Terms of Service"),
-                'description': _("Leave this field empty to "
-                                 "use default title."),
-                'value': "",
-                'field_extra': {
-                    'max_length': 255,
-                    'required': False,
+    migrate_settings_group(
+        apps, {
+            'key': 'legal',
+            'name': _("Legal information"),
+            'description': _("Those settings allow you to set forum terms of service and privacy policy."),
+            'settings': [
+                {
+                    'setting': 'terms_of_service_title',
+                    'name': _("Terms title"),
+                    'legend': _("Terms of Service"),
+                    'description': _("Leave this field empty to use default title."),
+                    'value': "",
+                    'field_extra': {
+                        'max_length': 255,
+                        'required': False,
+                    },
+                    'is_public': True,
                 },
                 },
-                'is_public': True,
-            },
-            {
-                'setting': 'terms_of_service_link',
-                'name': _("Terms link"),
-                'description': _("If terms of service are located "
-                                 "on other page, enter there its link."),
-                'value': "",
-                'field_extra': {
-                    'max_length': 255,
-                    'required': False,
+                {
+                    'setting': 'terms_of_service_link',
+                    'name': _("Terms link"),
+                    'description': _("If terms of service are located on other page, enter there its link."),
+                    'value': "",
+                    'field_extra': {
+                        'max_length': 255,
+                        'required': False,
+                    },
+                    'is_public': True,
                 },
                 },
-                'is_public': True,
-            },
-            {
-                'setting': 'terms_of_service',
-                'name': _("Terms contents"),
-                'description': _("Your forums can have custom terms of "
-                                 "service page. To create it, write or "
-                                 "paste here its contents. Full Misago "
-                                 "markup is available for formatting."),
-                'value': "",
-                'form_field': 'textarea',
-                'field_extra': {
-                    'max_length': 128000,
-                    'required': False,
-                    'rows': 8,
+                {
+                    'setting': 'terms_of_service',
+                    'name': _("Terms contents"),
+                    'description': _(
+                        "Your forums can have custom terms of "
+                        "service page. To create it, write or "
+                        "paste here its contents. Full Misago "
+                        "markup is available for formatting."
+                    ),
+                    'value': "",
+                    'form_field': 'textarea',
+                    'field_extra': {
+                        'max_length': 128000,
+                        'required': False,
+                        'rows': 8,
+                    },
+                    'is_public': True,
+                    'is_lazy': True,
                 },
                 },
-                'is_public': True,
-                'is_lazy': True,
-            },
-            {
-                'setting': 'privacy_policy_title',
-                'name': _("Policy title"),
-                'legend': _("Privacy policy"),
-                'description': _("Leave this field empty to "
-                                 "use default title."),
-                'value': "",
-                'field_extra': {
-                    'max_length': 255,
-                    'required': False,
+                {
+                    'setting': 'privacy_policy_title',
+                    'name': _("Policy title"),
+                    'legend': _("Privacy policy"),
+                    'description': _("Leave this field empty to use default title."),
+                    'value': "",
+                    'field_extra': {
+                        'max_length': 255,
+                        'required': False,
+                    },
+                    'is_public': True,
                 },
                 },
-                'is_public': True,
-            },
-            {
-                'setting': 'privacy_policy_link',
-                'name': _("Policy link"),
-                'description': _("If privacy policy is located on "
-                                 "other page, enter there its link."),
-                'value': "",
-                'field_extra': {
-                    'max_length': 255,
-                    'required': False,
+                {
+                    'setting': 'privacy_policy_link',
+                    'name': _("Policy link"),
+                    'description': _("If privacy policy is located on other page, enter there its link."),
+                    'value': "",
+                    'field_extra': {
+                        'max_length': 255,
+                        'required': False,
+                    },
+                    'is_public': True,
                 },
                 },
-                'is_public': True,
-            },
-            {
-                'setting': 'privacy_policy',
-                'name': _("Policy contents"),
-                'description': _("Your forums can have custom privacy "
-                                 "policy page. To create it, write or "
-                                 "paste here its contents. Full Misago "
-                                 "markup is available for formatting."),
-                'value': "",
-                'form_field': 'textarea',
-                'field_extra': {
-                    'max_length': 128000,
-                    'required': False,
-                    'rows': 8,
+                {
+                    'setting': 'privacy_policy',
+                    'name': _("Policy contents"),
+                    'description': _(
+                        "Your forums can have custom privacy "
+                        "policy page. To create it, write or "
+                        "paste here its contents. Full Misago "
+                        "markup is available for formatting."
+                    ),
+                    'value': "",
+                    'form_field': 'textarea',
+                    'field_extra': {
+                        'max_length': 128000,
+                        'required': False,
+                        'rows': 8,
+                    },
+                    'is_public': True,
+                    'is_lazy': True,
                 },
                 },
-                'is_public': True,
-                'is_lazy': True,
-            },
-            {
-                'setting': 'forum_footnote',
-                'name': _("Footnote"),
-                'description': _("Short message displayed "
-                                 "in forum footer."),
-                'legend': _("Forum footer"),
-                'field_extra': {
-                    'max_length': 300
+                {
+                    'setting': 'forum_footnote',
+                    'name': _("Footnote"),
+                    'description': _("Short message displayed in forum footer."),
+                    'legend': _("Forum footer"),
+                    'field_extra': {
+                        'max_length': 300,
+                    },
+                    'is_public': True,
                 },
                 },
-                'is_public': True,
-            },
-        )
-    })
+            ],
+        }
+    )
 
 
 
 
 class Migration(migrations.Migration):
 class Migration(migrations.Migration):

+ 10 - 8
misago/legal/tests.py

@@ -57,7 +57,7 @@ class PrivacyPolicyTests(TestCase):
         context_dict = legal_links(MockRequest())
         context_dict = legal_links(MockRequest())
 
 
         self.assertEqual(context_dict, {
         self.assertEqual(context_dict, {
-            'PRIVACY_POLICY_URL': reverse('misago:privacy-policy')
+            'PRIVACY_POLICY_URL': reverse('misago:privacy-policy'),
         })
         })
 
 
     def test_context_processor_remote_policy(self):
     def test_context_processor_remote_policy(self):
@@ -66,7 +66,7 @@ class PrivacyPolicyTests(TestCase):
         context_dict = legal_links(MockRequest())
         context_dict = legal_links(MockRequest())
 
 
         self.assertEqual(context_dict, {
         self.assertEqual(context_dict, {
-            'PRIVACY_POLICY_URL': 'http://test.com'
+            'PRIVACY_POLICY_URL': 'http://test.com',
         })
         })
 
 
         # set misago view too
         # set misago view too
@@ -74,7 +74,7 @@ class PrivacyPolicyTests(TestCase):
         context_dict = legal_links(MockRequest())
         context_dict = legal_links(MockRequest())
 
 
         self.assertEqual(context_dict, {
         self.assertEqual(context_dict, {
-            'PRIVACY_POLICY_URL': 'http://test.com'
+            'PRIVACY_POLICY_URL': 'http://test.com',
         })
         })
 
 
 
 
@@ -123,9 +123,11 @@ class TermsOfServiceTests(TestCase):
         settings.override_setting('terms_of_service', 'Lorem ipsum')
         settings.override_setting('terms_of_service', 'Lorem ipsum')
         context_dict = legal_links(MockRequest())
         context_dict = legal_links(MockRequest())
 
 
-        self.assertEqual(context_dict, {
-            'TERMS_OF_SERVICE_URL': reverse('misago:terms-of-service')
-        })
+        self.assertEqual(
+            context_dict, {
+                'TERMS_OF_SERVICE_URL': reverse('misago:terms-of-service'),
+            }
+        )
 
 
     def test_context_processor_remote_tos(self):
     def test_context_processor_remote_tos(self):
         """context processor has TOS link to remote url"""
         """context processor has TOS link to remote url"""
@@ -133,7 +135,7 @@ class TermsOfServiceTests(TestCase):
         context_dict = legal_links(MockRequest())
         context_dict = legal_links(MockRequest())
 
 
         self.assertEqual(context_dict, {
         self.assertEqual(context_dict, {
-            'TERMS_OF_SERVICE_URL': 'http://test.com'
+            'TERMS_OF_SERVICE_URL': 'http://test.com',
         })
         })
 
 
         # set misago view too
         # set misago view too
@@ -141,5 +143,5 @@ class TermsOfServiceTests(TestCase):
         context_dict = legal_links(MockRequest())
         context_dict = legal_links(MockRequest())
 
 
         self.assertEqual(context_dict, {
         self.assertEqual(context_dict, {
-            'TERMS_OF_SERVICE_URL': 'http://test.com'
+            'TERMS_OF_SERVICE_URL': 'http://test.com',
         })
         })

+ 8 - 4
misago/legal/views.py

@@ -40,12 +40,14 @@ def privacy_policy(request):
 
 
     parsed_content = get_parsed_content(request, 'privacy_policy')
     parsed_content = get_parsed_content(request, 'privacy_policy')
 
 
-    return render(request, 'misago/privacy_policy.html', {
+    return render(
+        request, 'misago/privacy_policy.html', {
             'id': 'privacy-policy',
             'id': 'privacy-policy',
             'title': settings.privacy_policy_title or _("Privacy policy"),
             'title': settings.privacy_policy_title or _("Privacy policy"),
             'link': settings.privacy_policy_link,
             'link': settings.privacy_policy_link,
             'body': parsed_content,
             'body': parsed_content,
-        })
+        }
+    )
 
 
 
 
 def terms_of_service(request):
 def terms_of_service(request):
@@ -57,9 +59,11 @@ def terms_of_service(request):
 
 
     parsed_content = get_parsed_content(request, 'terms_of_service')
     parsed_content = get_parsed_content(request, 'terms_of_service')
 
 
-    return render(request, 'misago/terms_of_service.html', {
+    return render(
+        request, 'misago/terms_of_service.html', {
             'id': 'terms-of-service',
             'id': 'terms-of-service',
             'title': settings.terms_of_service_title or _("Terms of service"),
             'title': settings.terms_of_service_title or _("Terms of service"),
             'link': settings.terms_of_service_link,
             'link': settings.terms_of_service_link,
             'body': parsed_content,
             'body': parsed_content,
-        })
+        }
+    )

+ 0 - 1
misago/markup/__init__.py

@@ -2,5 +2,4 @@ from .finalise import finalise_markup
 from .flavours import common as common_flavour, signature as signature_flavour
 from .flavours import common as common_flavour, signature as signature_flavour
 from .parser import parse
 from .parser import parse
 
 
-
 default_app_config = 'misago.markup.apps.MisagoMarkupConfig'
 default_app_config = 'misago.markup.apps.MisagoMarkupConfig'

+ 2 - 7
misago/markup/api.py

@@ -4,7 +4,6 @@ from rest_framework.response import Response
 
 
 from django.core.exceptions import ValidationError
 from django.core.exceptions import ValidationError
 from django.utils import six
 from django.utils import six
-from django.utils.translation import ugettext as _
 
 
 from misago.threads.validators import validate_post
 from misago.threads.validators import validate_post
 
 
@@ -18,13 +17,9 @@ def parse_markup(request):
     try:
     try:
         validate_post(post)
         validate_post(post)
     except ValidationError as e:
     except ValidationError as e:
-        return Response({
-            'detail': e.args[0]
-        }, status=status.HTTP_400_BAD_REQUEST)
+        return Response({'detail': e.args[0]}, status=status.HTTP_400_BAD_REQUEST)
 
 
     parsed = common_flavour(request, request.user, post, force_shva=True)['parsed_text']
     parsed = common_flavour(request, request.user, post, force_shva=True)['parsed_text']
     finalised = finalise_markup(parsed)
     finalised = finalise_markup(parsed)
 
 
-    return Response({
-        'parsed': finalised
-    })
+    return Response({'parsed': finalised})

+ 18 - 11
misago/markup/bbcode/blocks.py

@@ -27,17 +27,22 @@ class QuoteExtension(markdown.Extension):
         md.registerExtension(self)
         md.registerExtension(self)
 
 
         md.preprocessors.add('misago_bbcode_quote', QuotePreprocessor(md), '_end')
         md.preprocessors.add('misago_bbcode_quote', QuotePreprocessor(md), '_end')
-        md.parser.blockprocessors.add('misago_bbcode_quote', QuoteBlockProcessor(md.parser), '>code')
+        md.parser.blockprocessors.add(
+            'misago_bbcode_quote', QuoteBlockProcessor(md.parser), '>code'
+        )
 
 
 
 
 class QuotePreprocessor(Preprocessor):
 class QuotePreprocessor(Preprocessor):
-    QUOTE_BLOCK_RE = re.compile(r'''
+    QUOTE_BLOCK_RE = re.compile(
+        r'''
 \[quote\](?P<text>.*?)\[/quote\]
 \[quote\](?P<text>.*?)\[/quote\]
-'''.strip(), re.IGNORECASE | re.MULTILINE | re.DOTALL);
-    QUOTE_BLOCK_TITLE_RE = re.compile(r'''
+'''.strip(), re.IGNORECASE | re.MULTILINE | re.DOTALL
+    )
+    QUOTE_BLOCK_TITLE_RE = re.compile(
+        r'''
 \[quote=("?)(?P<title>.*?)("?)](?P<text>.*?)\[/quote\]
 \[quote=("?)(?P<title>.*?)("?)](?P<text>.*?)\[/quote\]
-'''.strip(), re.IGNORECASE | re.MULTILINE | re.DOTALL);
-
+'''.strip(), re.IGNORECASE | re.MULTILINE | re.DOTALL
+    )
 
 
     def run(self, lines):
     def run(self, lines):
         text = '\n'.join(lines)
         text = '\n'.join(lines)
@@ -106,12 +111,14 @@ class CodeBlockExtension(markdown.Extension):
     def extendMarkdown(self, md):
     def extendMarkdown(self, md):
         md.registerExtension(self)
         md.registerExtension(self)
 
 
-        md.preprocessors.add('misago_code_bbcode',
-                             CodeBlockPreprocessor(md),
-                             ">normalize_whitespace")
+        md.preprocessors.add(
+            'misago_code_bbcode', CodeBlockPreprocessor(md), ">normalize_whitespace"
+        )
 
 
 
 
 class CodeBlockPreprocessor(FencedBlockPreprocessor):
 class CodeBlockPreprocessor(FencedBlockPreprocessor):
-        FENCED_BLOCK_RE = re.compile(r'''
+    FENCED_BLOCK_RE = re.compile(
+        r'''
 \[code(=("?)(?P<lang>.*?)("?))?](([ ]*\n)+)?(?P<code>.*?)((\s|\n)+)?\[/code\]
 \[code(=("?)(?P<lang>.*?)("?))?](([ ]*\n)+)?(?P<code>.*?)((\s|\n)+)?\[/code\]
-''', re.IGNORECASE | re.MULTILINE | re.DOTALL | re.VERBOSE)
+''', re.IGNORECASE | re.MULTILINE | re.DOTALL | re.VERBOSE
+    )

+ 4 - 5
misago/markup/bbcode/inline.py

@@ -10,10 +10,12 @@ class SimpleBBCodePattern(SimpleTagPattern):
     """
     """
     Case insensitive simple BBCode
     Case insensitive simple BBCode
     """
     """
+
     def __init__(self, bbcode, tag=None):
     def __init__(self, bbcode, tag=None):
         self.pattern = r'(\[%s\](.*?)\[/%s\])' % (bbcode, bbcode)
         self.pattern = r'(\[%s\](.*?)\[/%s\])' % (bbcode, bbcode)
-        self.compiled_re = re.compile("^(.*?)%s(.*?)$" % self.pattern,
-                                      re.DOTALL | re.UNICODE | re.IGNORECASE)
+        self.compiled_re = re.compile(
+            "^(.*?)%s(.*?)$" % self.pattern, re.DOTALL | re.UNICODE | re.IGNORECASE
+        )
 
 
         # Api for Markdown to pass safe_mode into instance
         # Api for Markdown to pass safe_mode into instance
         self.safe_mode = False
         self.safe_mode = False
@@ -22,9 +24,6 @@ class SimpleBBCodePattern(SimpleTagPattern):
         self.tag = tag or bbcode.lower()
         self.tag = tag or bbcode.lower()
 
 
 
 
-"""
-Register basic BBCodes
-"""
 bold = SimpleBBCodePattern('b')
 bold = SimpleBBCodePattern('b')
 italics = SimpleBBCodePattern('i')
 italics = SimpleBBCodePattern('i')
 underline = SimpleBBCodePattern('u')
 underline = SimpleBBCodePattern('u')

+ 1 - 1
misago/markup/context_processors.py

@@ -3,7 +3,7 @@ from django.urls import reverse
 
 
 def preload_api_url(request):
 def preload_api_url(request):
     request.frontend_context.update({
     request.frontend_context.update({
-        'PARSE_MARKUP_API': reverse('misago:api:parse-markup')
+        'PARSE_MARKUP_API': reverse('misago:api:parse-markup'),
     })
     })
 
 
     return {}
     return {}

+ 4 - 2
misago/markup/finalise.py

@@ -5,9 +5,11 @@ import re
 from django.utils.translation import gettext as _
 from django.utils.translation import gettext as _
 
 
 
 
-HEADER_RE = re.compile(r'''
+HEADER_RE = re.compile(
+    r'''
 <div class="quote-heading">(?P<title>.*?)</div>
 <div class="quote-heading">(?P<title>.*?)</div>
-'''.strip(), re.IGNORECASE | re.MULTILINE | re.DOTALL);
+'''.strip(), re.IGNORECASE | re.MULTILINE | re.DOTALL
+)
 
 
 
 
 def finalise_markup(post):
 def finalise_markup(post):

+ 25 - 7
misago/markup/flavours.py

@@ -12,7 +12,13 @@ def common(request, poster, text, allow_mentions=True, force_shva=False):
 
 
     Returns dict object
     Returns dict object
     """
     """
-    return parse(text, request, poster, allow_mentions=allow_mentions, force_shva=force_shva)
+    return parse(
+        text,
+        request,
+        poster,
+        allow_mentions=allow_mentions,
+        force_shva=force_shva,
+    )
 
 
 
 
 def limited(request, text):
 def limited(request, text):
@@ -24,16 +30,28 @@ def limited(request, text):
 
 
     Returns parsed text
     Returns parsed text
     """
     """
-    result = parse(text, request, request.user, allow_mentions=False,
-                   allow_links=True, allow_images=False, allow_blocks=False)
+    result = parse(
+        text,
+        request,
+        request.user,
+        allow_mentions=False,
+        allow_links=True,
+        allow_images=False,
+        allow_blocks=False,
+    )
 
 
     return result['parsed_text']
     return result['parsed_text']
 
 
 
 
 def signature(request, owner, text):
 def signature(request, owner, text):
-    result = parse(text, request, owner, allow_mentions=False,
-                   allow_blocks=owner.acl_cache['allow_signature_blocks'],
-                   allow_links=owner.acl_cache['allow_signature_links'],
-                   allow_images=owner.acl_cache['allow_signature_images'])
+    result = parse(
+        text,
+        request,
+        owner,
+        allow_mentions=False,
+        allow_blocks=owner.acl_cache['allow_signature_blocks'],
+        allow_links=owner.acl_cache['allow_signature_links'],
+        allow_images=owner.acl_cache['allow_signature_images'],
+    )
 
 
     return result['parsed_text']
     return result['parsed_text']

+ 2 - 3
misago/markup/md/shortimgs.py

@@ -3,14 +3,13 @@ from markdown.inlinepatterns import LinkPattern
 from markdown.util import etree
 from markdown.util import etree
 
 
 
 
-IMAGES_RE =  r'\!(\s?)\((<.*?>|([^\)]*))\)'
+IMAGES_RE = r'\!(\s?)\((<.*?>|([^\)]*))\)'
 
 
 
 
 class ShortImagesExtension(markdown.Extension):
 class ShortImagesExtension(markdown.Extension):
     def extendMarkdown(self, md):
     def extendMarkdown(self, md):
         md.registerExtension(self)
         md.registerExtension(self)
-        md.inlinePatterns.add(
-            'misago_short_images', ShortImagePattern(IMAGES_RE, md), '_end')
+        md.inlinePatterns.add('misago_short_images', ShortImagePattern(IMAGES_RE, md), '_end')
 
 
 
 
 class ShortImagePattern(LinkPattern):
 class ShortImagePattern(LinkPattern):

+ 2 - 1
misago/markup/md/striketrough.py

@@ -9,4 +9,5 @@ class StriketroughExtension(markdown.Extension):
     def extendMarkdown(self, md):
     def extendMarkdown(self, md):
         md.registerExtension(self)
         md.registerExtension(self)
         md.inlinePatterns.add(
         md.inlinePatterns.add(
-            'misago_striketrough', SimpleTagPattern(STRIKETROUGH_RE, 'del'), '_end')
+            'misago_striketrough', SimpleTagPattern(STRIKETROUGH_RE, 'del'), '_end'
+        )

+ 1 - 1
misago/markup/mentions.py

@@ -1,6 +1,6 @@
 import re
 import re
 
 
-from bs4 import BeautifulSoup, NavigableString
+from bs4 import BeautifulSoup
 
 
 from django.contrib.auth import get_user_model
 from django.contrib.auth import get_user_model
 from django.utils import six
 from django.utils import six

+ 14 - 9
misago/markup/parser.py

@@ -22,8 +22,17 @@ from .pipeline import pipeline
 MISAGO_ATTACHMENT_VIEWS = ('misago:attachment', 'misago:attachment-thumbnail')
 MISAGO_ATTACHMENT_VIEWS = ('misago:attachment', 'misago:attachment-thumbnail')
 
 
 
 
-def parse(text, request, poster, allow_mentions=True, allow_links=True,
-          allow_images=True, allow_blocks=True, force_shva=False, minify=True):
+def parse(
+        text,
+        request,
+        poster,
+        allow_mentions=True,
+        allow_links=True,
+        allow_images=True,
+        allow_blocks=True,
+        force_shva=False,
+        minify=True
+):
     """
     """
     Message parser
     Message parser
 
 
@@ -47,7 +56,7 @@ def parse(text, request, poster, allow_mentions=True, allow_links=True,
         'mentions': [],
         'mentions': [],
         'images': [],
         'images': [],
         'outgoing_links': [],
         'outgoing_links': [],
-        'inside_links': []
+        'inside_links': [],
     }
     }
 
 
     # Parse text
     # Parse text
@@ -73,9 +82,7 @@ def parse(text, request, poster, allow_mentions=True, allow_links=True,
 
 
 
 
 def md_factory(allow_links=True, allow_images=True, allow_blocks=True):
 def md_factory(allow_links=True, allow_images=True, allow_blocks=True):
-    """
-    Create and configure markdown object
-    """
+    """creates and configures markdown object"""
     md = markdown.Markdown(safe_mode='escape', extensions=['nl2br'])
     md = markdown.Markdown(safe_mode='escape', extensions=['nl2br'])
 
 
     # Remove references
     # Remove references
@@ -133,8 +140,7 @@ def md_factory(allow_links=True, allow_images=True, allow_blocks=True):
 
 
 
 
 def linkify_paragraphs(result):
 def linkify_paragraphs(result):
-    result['parsed_text'] = bleach.linkify(
-        result['parsed_text'], skip_pre=True, parse_email=True)
+    result['parsed_text'] = bleach.linkify(result['parsed_text'], skip_pre=True, parse_email=True)
 
 
     # dirty fix for
     # dirty fix for
     if '<code>' in result['parsed_text'] and '<a' in result['parsed_text']:
     if '<code>' in result['parsed_text'] and '<a' in result['parsed_text']:
@@ -150,7 +156,6 @@ def linkify_paragraphs(result):
 
 
 def clean_links(request, result, force_shva=False):
 def clean_links(request, result, force_shva=False):
     host = request.get_host()
     host = request.get_host()
-    site_address = '%s://%s' % (request.scheme, request.get_host())
 
 
     soup = BeautifulSoup(result['parsed_text'], 'html5lib')
     soup = BeautifulSoup(result['parsed_text'], 'html5lib')
     for link in soup.find_all('a'):
     for link in soup.find_all('a'):

+ 3 - 3
misago/markup/pipeline.py

@@ -8,9 +8,8 @@ from misago.conf import settings
 
 
 
 
 class MarkupPipeline(object):
 class MarkupPipeline(object):
-    """
-    Small framework for extending parser
-    """
+    """small framework for extending parser"""
+
     def extend_markdown(self, md):
     def extend_markdown(self, md):
         for extension in settings.MISAGO_MARKUP_EXTENSIONS:
         for extension in settings.MISAGO_MARKUP_EXTENSIONS:
             module = import_module(extension)
             module = import_module(extension)
@@ -31,4 +30,5 @@ class MarkupPipeline(object):
         result['parsed_text'] = souped_text.strip()
         result['parsed_text'] = souped_text.strip()
         return result
         return result
 
 
+
 pipeline = MarkupPipeline()
 pipeline = MarkupPipeline()

+ 4 - 0
misago/markup/templatetags/misago_editor.py

@@ -15,9 +15,13 @@ def _render_editor_template(context, editor, tpl):
 
 
 def editor_body(context, editor):
 def editor_body(context, editor):
     return _render_editor_template(context, editor, editor.body_template)
     return _render_editor_template(context, editor, editor.body_template)
+
+
 register.simple_tag(takes_context=True)(editor_body)
 register.simple_tag(takes_context=True)(editor_body)
 
 
 
 
 def editor_js(context, editor):
 def editor_js(context, editor):
     return _render_editor_template(context, editor, editor.js_template)
     return _render_editor_template(context, editor, editor.js_template)
+
+
 register.simple_tag(takes_context=True)(editor_js)
 register.simple_tag(takes_context=True)(editor_js)

+ 8 - 12
misago/markup/tests/test_api.py

@@ -14,7 +14,7 @@ class ParseMarkupApiTests(AuthenticatedUserTestCase):
 
 
     def test_is_anonymous(self):
     def test_is_anonymous(self):
         """api requires authentication"""
         """api requires authentication"""
-        self.logout_user();
+        self.logout_user()
 
 
         response = self.client.post(self.api_link)
         response = self.client.post(self.api_link)
         self.assertContains(response, "This action is not available to guests.", status_code=403)
         self.assertContains(response, "This action is not available to guests.", status_code=403)
@@ -26,23 +26,19 @@ class ParseMarkupApiTests(AuthenticatedUserTestCase):
 
 
     def test_empty_post(self):
     def test_empty_post(self):
         """api handles empty post"""
         """api handles empty post"""
-        response = self.client.post(self.api_link, {
-            'post': ''
-        })
+        response = self.client.post(self.api_link, {'post': ''})
         self.assertContains(response, "You have to enter a message.", status_code=400)
         self.assertContains(response, "You have to enter a message.", status_code=400)
 
 
     def test_invalid_post(self):
     def test_invalid_post(self):
         """api handles invalid post type"""
         """api handles invalid post type"""
-        response = self.client.post(self.api_link, {
-            'post': 123
-        })
+        response = self.client.post(self.api_link, {'post': 123})
         self.assertContains(
         self.assertContains(
-            response, "Posted message should be at least 5 characters long (it has 3).",
-            status_code=400)
+            response,
+            "Posted message should be at least 5 characters long (it has 3).",
+            status_code=400
+        )
 
 
     def test_valid_post(self):
     def test_valid_post(self):
         """api returns parsed markup for valid post"""
         """api returns parsed markup for valid post"""
-        response = self.client.post(self.api_link, {
-            'post': 'Lorem ipsum dolor met!'
-        })
+        response = self.client.post(self.api_link, {'post': 'Lorem ipsum dolor met!'})
         self.assertContains(response, "<p>Lorem ipsum dolor met!</p>")
         self.assertContains(response, "<p>Lorem ipsum dolor met!</p>")

+ 2 - 4
misago/markup/tests/test_checksums.py

@@ -10,7 +10,5 @@ class ChecksumsTests(TestCase):
 
 
         checksum = checksums.make_checksum(fake_message, [post_pk])
         checksum = checksums.make_checksum(fake_message, [post_pk])
 
 
-        self.assertTrue(
-            checksums.is_checksum_valid(fake_message, checksum, [post_pk]))
-        self.assertFalse(
-            checksums.is_checksum_valid(fake_message, checksum, [3]))
+        self.assertTrue(checksums.is_checksum_valid(fake_message, checksum, [post_pk]))
+        self.assertFalse(checksums.is_checksum_valid(fake_message, checksum, [3]))

+ 17 - 36
misago/markup/tests/test_mentions.py

@@ -10,19 +10,10 @@ class MockRequest(object):
 class MentionsTests(AuthenticatedUserTestCase):
 class MentionsTests(AuthenticatedUserTestCase):
     def test_single_mention(self):
     def test_single_mention(self):
         """markup extension parses single mention"""
         """markup extension parses single mention"""
-        TEST_CASES = (
-            (
-                '<p>Hello, @{}!</p>',
-                '<p>Hello, <a href="{}">@{}</a>!</p>'
-            ),
-            (
-                '<h1>Hello, @{}!</h1>',
-                '<h1>Hello, <a href="{}">@{}</a>!</h1>'
-            ),
-            (
-                '<div>Hello, @{}!</div>',
-                '<div>Hello, <a href="{}">@{}</a>!</div>'
-            ),
+        TEST_CASES = [
+            ('<p>Hello, @{}!</p>', '<p>Hello, <a href="{}">@{}</a>!</p>'),
+            ('<h1>Hello, @{}!</h1>', '<h1>Hello, <a href="{}">@{}</a>!</h1>'),
+            ('<div>Hello, @{}!</div>', '<div>Hello, <a href="{}">@{}</a>!</div>'),
             (
             (
                 '<h1>Hello, <strong>@{}!</strong></h1>',
                 '<h1>Hello, <strong>@{}!</strong></h1>',
                 '<h1>Hello, <strong><a href="{}">@{}</a>!</strong></h1>'
                 '<h1>Hello, <strong><a href="{}">@{}</a>!</strong></h1>'
@@ -31,13 +22,10 @@ class MentionsTests(AuthenticatedUserTestCase):
                 '<h1>Hello, <strong>@{}</strong>!</h1>',
                 '<h1>Hello, <strong>@{}</strong>!</h1>',
                 '<h1>Hello, <strong><a href="{}">@{}</a></strong>!</h1>'
                 '<h1>Hello, <strong><a href="{}">@{}</a></strong>!</h1>'
             ),
             ),
-        )
+        ]
 
 
         for before, after in TEST_CASES:
         for before, after in TEST_CASES:
-            result = {
-                'parsed_text': before.format(self.user.username),
-                'mentions': []
-            }
+            result = {'parsed_text': before.format(self.user.username), 'mentions': []}
 
 
             add_mentions(MockRequest(self.user), result)
             add_mentions(MockRequest(self.user), result)
 
 
@@ -47,18 +35,15 @@ class MentionsTests(AuthenticatedUserTestCase):
 
 
     def test_invalid_mentions(self):
     def test_invalid_mentions(self):
         """markup extension leaves invalid mentions alone"""
         """markup extension leaves invalid mentions alone"""
-        TEST_CASES = (
+        TEST_CASES = [
             '<p>Hello, Bob!</p>',
             '<p>Hello, Bob!</p>',
             '<p>Hello, @Bob!</p>',
             '<p>Hello, @Bob!</p>',
             '<p>Hello, <a href="/">@{}</a>!</p>'.format(self.user.username),
             '<p>Hello, <a href="/">@{}</a>!</p>'.format(self.user.username),
             '<p>Hello, <a href="/"><b>@{}</b></a>!</p>'.format(self.user.username),
             '<p>Hello, <a href="/"><b>@{}</b></a>!</p>'.format(self.user.username),
-        )
+        ]
 
 
         for markup in TEST_CASES:
         for markup in TEST_CASES:
-            result = {
-                'parsed_text': markup,
-                'mentions': []
-            }
+            result = {'parsed_text': markup, 'mentions': []}
 
 
             add_mentions(MockRequest(self.user), result)
             add_mentions(MockRequest(self.user), result)
 
 
@@ -69,13 +54,11 @@ class MentionsTests(AuthenticatedUserTestCase):
         """markup extension handles multiple mentions"""
         """markup extension handles multiple mentions"""
         before = '<p>Hello @{0} and @{0}, how is it going?</p>'.format(self.user.username)
         before = '<p>Hello @{0} and @{0}, how is it going?</p>'.format(self.user.username)
 
 
-        formats = (self.user.get_absolute_url(), self.user.username)
-        after = '<p>Hello <a href="{0}">@{1}</a> and <a href="{0}">@{1}</a>, how is it going?</p>'.format(*formats)
+        after = '<p>Hello <a href="{0}">@{1}</a> and <a href="{0}">@{1}</a>, how is it going?</p>'.format(
+            self.user.get_absolute_url(), self.user.username
+        )
 
 
-        result = {
-            'parsed_text': before,
-            'mentions': []
-        }
+        result = {'parsed_text': before, 'mentions': []}
 
 
         add_mentions(MockRequest(self.user), result)
         add_mentions(MockRequest(self.user), result)
         self.assertEqual(result['parsed_text'], after)
         self.assertEqual(result['parsed_text'], after)
@@ -85,13 +68,11 @@ class MentionsTests(AuthenticatedUserTestCase):
         """markup extension handles mentions across document"""
         """markup extension handles mentions across document"""
         before = '<p>Hello @{0}</p><p>@{0}, how is it going?</p>'.format(self.user.username)
         before = '<p>Hello @{0}</p><p>@{0}, how is it going?</p>'.format(self.user.username)
 
 
-        formats = (self.user.get_absolute_url(), self.user.username)
-        after = '<p>Hello <a href="{0}">@{1}</a></p><p><a href="{0}">@{1}</a>, how is it going?</p>'.format(*formats)
+        after = '<p>Hello <a href="{0}">@{1}</a></p><p><a href="{0}">@{1}</a>, how is it going?</p>'.format(
+            self.user.get_absolute_url(), self.user.username
+        )
 
 
-        result = {
-            'parsed_text': before,
-            'mentions': []
-        }
+        result = {'parsed_text': before, 'mentions': []}
 
 
         add_mentions(MockRequest(self.user), result)
         add_mentions(MockRequest(self.user), result)
         self.assertEqual(result['parsed_text'], after)
         self.assertEqual(result['parsed_text'], after)

+ 1 - 3
misago/markup/urls.py

@@ -3,6 +3,4 @@ from django.conf.urls import url
 from .api import parse_markup
 from .api import parse_markup
 
 
 
 
-urlpatterns = [
-    url(r'^parse-markup/$', parse_markup, name='parse-markup')
-]
+urlpatterns = [url(r'^parse-markup/$', parse_markup, name='parse-markup')]

+ 1 - 1
misago/readtracker/apps.py

@@ -7,4 +7,4 @@ class MisagoReadTrackerConfig(AppConfig):
     verbose_name = "Misago Read Tracker"
     verbose_name = "Misago Read Tracker"
 
 
     def ready(self):
     def ready(self):
-        from . import signals
+        from . import signals as _

+ 9 - 10
misago/readtracker/categoriestracker.py

@@ -24,9 +24,7 @@ def make_read_aware(user, categories):
             categories_dict[category.pk] = category
             categories_dict[category.pk] = category
 
 
     if categories_dict:
     if categories_dict:
-        categories_records = user.categoryread_set.filter(
-            category__in=categories_dict.keys()
-        )
+        categories_records = user.categoryread_set.filter(category__in=categories_dict.keys())
 
 
         for record in categories_records:
         for record in categories_records:
             category = categories_dict[record.category_id]
             category = categories_dict[record.category_id]
@@ -61,14 +59,13 @@ def sync_record(user, category):
         category_record = None
         category_record = None
 
 
     all_threads = category.thread_set.filter(last_post_on__gt=cutoff_date)
     all_threads = category.thread_set.filter(last_post_on__gt=cutoff_date)
-    all_threads_count = exclude_invisible_threads(
-        user, [category], all_threads).count()
+    all_threads_count = exclude_invisible_threads(user, [category], all_threads).count()
 
 
     read_threads_count = user.threadread_set.filter(
     read_threads_count = user.threadread_set.filter(
         category=category,
         category=category,
         thread__in=all_threads,
         thread__in=all_threads,
         last_read_on__gt=cutoff_date,
         last_read_on__gt=cutoff_date,
-        thread__last_post_on__lte=F("last_read_on")
+        thread__last_post_on__lte=F("last_read_on"),
     ).count()
     ).count()
 
 
     category_is_read = read_threads_count == all_threads_count
     category_is_read = read_threads_count == all_threads_count
@@ -88,8 +85,7 @@ def sync_record(user, category):
         else:
         else:
             last_read_on = cutoff_date
             last_read_on = cutoff_date
         category_record = user.categoryread_set.create(
         category_record = user.categoryread_set.create(
-            category=category,
-            last_read_on=last_read_on
+            category=category, last_read_on=last_read_on
         )
         )
 
 
 
 
@@ -97,8 +93,11 @@ def read_category(user, category):
     categories = [category.pk]
     categories = [category.pk]
     if not category.is_leaf_node():
     if not category.is_leaf_node():
         categories += category.get_descendants().filter(
         categories += category.get_descendants().filter(
-            id__in=user.acl_cache['visible_categories']
-        ).values_list('id', flat=True)
+            id__in=user.acl_cache['visible_categories'],
+        ).values_list(
+            'id',
+            flat=True,
+        )
 
 
     user.categoryread_set.filter(category_id__in=categories).delete()
     user.categoryread_set.filter(category_id__in=categories).delete()
     user.threadread_set.filter(category_id__in=categories).delete()
     user.threadread_set.filter(category_id__in=categories).delete()

+ 14 - 8
misago/readtracker/migrations/0001_initial.py

@@ -16,26 +16,32 @@ class Migration(migrations.Migration):
         migrations.CreateModel(
         migrations.CreateModel(
             name='CategoryRead',
             name='CategoryRead',
             fields=[
             fields=[
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+                (
+                    'id', models.AutoField(
+                        verbose_name='ID', serialize=False, auto_created=True, primary_key=True
+                    )
+                ),
                 ('last_read_on', models.DateTimeField()),
                 ('last_read_on', models.DateTimeField()),
                 ('category', models.ForeignKey(to='misago_categories.Category')),
                 ('category', models.ForeignKey(to='misago_categories.Category')),
                 ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
                 ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
             ],
             ],
-            options={
-            },
-            bases=(models.Model,),
+            options={},
+            bases=(models.Model, ),
         ),
         ),
         migrations.CreateModel(
         migrations.CreateModel(
             name='ThreadRead',
             name='ThreadRead',
             fields=[
             fields=[
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+                (
+                    'id', models.AutoField(
+                        verbose_name='ID', serialize=False, auto_created=True, primary_key=True
+                    )
+                ),
                 ('last_read_on', models.DateTimeField()),
                 ('last_read_on', models.DateTimeField()),
                 ('category', models.ForeignKey(to='misago_categories.Category')),
                 ('category', models.ForeignKey(to='misago_categories.Category')),
                 ('thread', models.ForeignKey(to='misago_threads.Thread')),
                 ('thread', models.ForeignKey(to='misago_threads.Thread')),
                 ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
                 ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
             ],
             ],
-            options={
-            },
-            bases=(models.Model,),
+            options={},
+            bases=(models.Model, ),
         ),
         ),
     ]
     ]

+ 0 - 3
misago/readtracker/models.py

@@ -1,8 +1,5 @@
-from datetime import timedelta
-
 from django.conf import settings
 from django.conf import settings
 from django.db import models
 from django.db import models
-from django.utils import timezone
 
 
 
 
 class CategoryRead(models.Model):
 class CategoryRead(models.Model):

+ 0 - 3
misago/readtracker/signals.py

@@ -11,9 +11,6 @@ thread_tracked = Signal(providing_args=["thread"])
 thread_read = Signal(providing_args=["thread"])
 thread_read = Signal(providing_args=["thread"])
 
 
 
 
-"""
-Signal handlers
-"""
 @receiver(delete_category_content)
 @receiver(delete_category_content)
 def delete_category_threads(sender, **kwargs):
 def delete_category_threads(sender, **kwargs):
     sender.categoryread_set.all().delete()
     sender.categoryread_set.all().delete()

+ 2 - 4
misago/readtracker/tests/test_dates.py

@@ -28,9 +28,7 @@ class ReadTrackerDatesTests(TestCase):
         past_date = timezone.now() + timedelta(minutes=10)
         past_date = timezone.now() + timedelta(minutes=10)
 
 
         category_cutoff = timezone.now() + timedelta(minutes=20)
         category_cutoff = timezone.now() + timedelta(minutes=20)
-        self.assertFalse(
-            is_date_tracked(past_date, MockUser(), category_cutoff))
+        self.assertFalse(is_date_tracked(past_date, MockUser(), category_cutoff))
 
 
         category_cutoff = timezone.now() - timedelta(minutes=20)
         category_cutoff = timezone.now() - timedelta(minutes=20)
-        self.assertTrue(
-            is_date_tracked(past_date, MockUser(), category_cutoff))
+        self.assertTrue(is_date_tracked(past_date, MockUser(), category_cutoff))

+ 5 - 5
misago/readtracker/tests/test_readtracker.py

@@ -19,14 +19,13 @@ class ReadTrackerTests(TestCase):
         self.categories = list(Category.objects.all_categories()[:1])
         self.categories = list(Category.objects.all_categories()[:1])
         self.category = self.categories[0]
         self.category = self.categories[0]
 
 
-        self.user = UserModel.objects.create_user(
-            "Bob", "bob@test.com", "Pass.123")
+        self.user = UserModel.objects.create_user("Bob", "bob@test.com", "Pass.123")
         self.anon = AnonymousUser()
         self.anon = AnonymousUser()
 
 
     def post_thread(self, datetime):
     def post_thread(self, datetime):
         return testutils.post_thread(
         return testutils.post_thread(
             category=self.category,
             category=self.category,
-            started_on=datetime
+            started_on=datetime,
         )
         )
 
 
 
 
@@ -93,7 +92,8 @@ class CategoriesTrackerTests(ReadTrackerTests):
         categoriestracker.make_read_aware(self.user, self.categories)
         categoriestracker.make_read_aware(self.user, self.categories)
         self.assertTrue(self.category.is_read)
         self.assertTrue(self.category.is_read)
 
 
-        thread = self.post_thread(self.user.joined_on + timedelta(days=1))
+        self.post_thread(self.user.joined_on + timedelta(days=1))
+
         categoriestracker.sync_record(self.user, self.category)
         categoriestracker.sync_record(self.user, self.category)
         categoriestracker.make_read_aware(self.user, self.categories)
         categoriestracker.make_read_aware(self.user, self.categories)
         self.assertFalse(self.category.is_read)
         self.assertFalse(self.category.is_read)
@@ -209,7 +209,7 @@ class ThreadsTrackerTests(ReadTrackerTests):
             thread=self.thread,
             thread=self.thread,
             is_hidden=is_hidden,
             is_hidden=is_hidden,
             is_unapproved=is_unapproved,
             is_unapproved=is_unapproved,
-            posted_on=timezone.now()
+            posted_on=timezone.now(),
         )
         )
         return self.post
         return self.post
 
 

+ 6 - 5
misago/readtracker/threadstracker.py

@@ -88,8 +88,7 @@ def make_thread_read_aware(user, thread):
         thread.is_new = True
         thread.is_new = True
 
 
         try:
         try:
-            category_record = user.categoryread_set.get(
-                category_id=thread.category_id)
+            category_record = user.categoryread_set.get(category_id=thread.category_id)
             thread.last_read_on = category_record.last_read_on
             thread.last_read_on = category_record.last_read_on
 
 
             if thread.last_post_on > category_record.last_read_on:
             if thread.last_post_on > category_record.last_read_on:
@@ -113,8 +112,10 @@ def make_posts_read_aware(user, thread, posts):
     try:
     try:
         is_thread_read = thread.is_read
         is_thread_read = thread.is_read
     except AttributeError:
     except AttributeError:
-        raise ValueError("thread passed make_posts_read_aware should be "
-                         "made read aware via make_thread_read_aware")
+        raise ValueError(
+            "thread passed make_posts_read_aware should be "
+            "made read aware via make_thread_read_aware"
+        )
 
 
     if is_thread_read:
     if is_thread_read:
         for post in posts:
         for post in posts:
@@ -146,7 +147,7 @@ def sync_record(user, thread, last_read_reply):
         user.threadread_set.create(
         user.threadread_set.create(
             category=thread.category,
             category=thread.category,
             thread=thread,
             thread=thread,
-            last_read_on=last_read_reply.posted_on
+            last_read_on=last_read_reply.posted_on,
         )
         )
         signals.thread_tracked.send(sender=user, thread=thread)
         signals.thread_tracked.send(sender=user, thread=thread)
         notification_triggers.append('see_thread_%s' % thread.pk)
         notification_triggers.append('see_thread_%s' % thread.pk)

+ 3 - 16
misago/search/permissions.py

@@ -6,16 +6,10 @@ from misago.acl.models import Role
 from misago.core.forms import YesNoSwitch
 from misago.core.forms import YesNoSwitch
 
 
 
 
-"""
-Admin Permissions Form
-"""
 class PermissionsForm(forms.Form):
 class PermissionsForm(forms.Form):
     legend = _("Search")
     legend = _("Search")
 
 
-    can_search = YesNoSwitch(
-        label=_("Can search site"),
-        initial=1
-    )
+    can_search = YesNoSwitch(label=_("Can search site"), initial=1)
 
 
 
 
 def change_permissions_form(role):
 def change_permissions_form(role):
@@ -25,15 +19,8 @@ def change_permissions_form(role):
         return None
         return None
 
 
 
 
-"""
-ACL Builder
-"""
 def build_acl(acl, roles, key_name):
 def build_acl(acl, roles, key_name):
-    new_acl = {
-        'can_search': 0
-    }
+    new_acl = {'can_search': 0}
     new_acl.update(acl)
     new_acl.update(acl)
 
 
-    return algebra.sum_acls(new_acl, roles=roles, key=key_name,
-        can_search=algebra.greater
-    )
+    return algebra.sum_acls(new_acl, roles=roles, key=key_name, can_search=algebra.greater)

+ 2 - 1
misago/search/searchprovider.py

@@ -7,4 +7,5 @@ class SearchProvider(object):
 
 
     def search(self, query, page=1):
     def search(self, query, page=1):
         raise NotImplementedError(
         raise NotImplementedError(
-            '%s has to define search(query, page=1) method' % self.__class__.__name__)
+            '%s has to define search(query, page=1) method' % self.__class__.__name__
+        )

+ 2 - 4
misago/search/searchproviders.py

@@ -24,15 +24,13 @@ class SearchProviders(object):
             try:
             try:
                 module = import_module(module_path)
                 module = import_module(module_path)
             except ImportError:
             except ImportError:
-                raise ImportError(
-                    'search module %s could not be imported' % modulename)
+                raise ImportError('search module %s could not be imported' % modulename)
 
 
             try:
             try:
                 classdef = getattr(module, classname)
                 classdef = getattr(module, classname)
                 self._providers.append(classdef)
                 self._providers.append(classdef)
             except AttributeError:
             except AttributeError:
-                raise ImportError(
-                    'search module %s could not be imported' % modulename)
+                raise ImportError('search module %s could not be imported' % modulename)
 
 
     def get_providers(self, request):
     def get_providers(self, request):
         if not self._initialized:
         if not self._initialized:

+ 11 - 11
misago/search/tests/test_api.py

@@ -14,14 +14,11 @@ class SearchApiTests(AuthenticatedUserTestCase):
 
 
     def test_no_permission(self):
     def test_no_permission(self):
         """api validates permission to search"""
         """api validates permission to search"""
-        override_acl(self.user, {
-            'can_search': 0
-        })
+        override_acl(self.user, {'can_search': 0})
 
 
         response = self.client.get(self.test_link)
         response = self.client.get(self.test_link)
 
 
-        self.assertContains(
-            response, "have permission to search site", status_code=403)
+        self.assertContains(response, "have permission to search site", status_code=403)
 
 
     def test_no_phrase(self):
     def test_no_phrase(self):
         """api handles no search query"""
         """api handles no search query"""
@@ -30,9 +27,11 @@ class SearchApiTests(AuthenticatedUserTestCase):
 
 
         providers = searchproviders.get_providers(True)
         providers = searchproviders.get_providers(True)
         for i, provider in enumerate(response.json()):
         for i, provider in enumerate(response.json()):
-            provider_api = reverse('misago:api:search', kwargs={
-                'search_provider': providers[i].url
-            })
+            provider_api = reverse(
+                'misago:api:search', kwargs={
+                    'search_provider': providers[i].url,
+                }
+            )
             self.assertEqual(provider_api, provider['api'])
             self.assertEqual(provider_api, provider['api'])
 
 
             self.assertEqual(six.text_type(providers[i].name), provider['name'])
             self.assertEqual(six.text_type(providers[i].name), provider['name'])
@@ -46,9 +45,10 @@ class SearchApiTests(AuthenticatedUserTestCase):
 
 
         providers = searchproviders.get_providers(True)
         providers = searchproviders.get_providers(True)
         for i, provider in enumerate(response.json()):
         for i, provider in enumerate(response.json()):
-            provider_api = reverse('misago:api:search', kwargs={
-                'search_provider': providers[i].url
-            })
+            provider_api = reverse(
+                'misago:api:search',
+                kwargs={'search_provider': providers[i].url},
+            )
             self.assertEqual(provider_api, provider['api'])
             self.assertEqual(provider_api, provider['api'])
 
 
             self.assertEqual(six.text_type(providers[i].name), provider['name'])
             self.assertEqual(six.text_type(providers[i].name), provider['name'])

+ 7 - 13
misago/search/tests/test_searchproviders.py

@@ -23,8 +23,7 @@ class SearchProvidersTests(TestCase):
 
 
         self.assertTrue(searchproviders._initialized)
         self.assertTrue(searchproviders._initialized)
 
 
-        self.assertEqual(
-            len(searchproviders._providers), len(settings.MISAGO_SEARCH_EXTENSIONS))
+        self.assertEqual(len(searchproviders._providers), len(settings.MISAGO_SEARCH_EXTENSIONS))
 
 
         for i, provider in enumerate(searchproviders._providers):
         for i, provider in enumerate(searchproviders._providers):
             classname = settings.MISAGO_SEARCH_EXTENSIONS[i].split('.')[-1]
             classname = settings.MISAGO_SEARCH_EXTENSIONS[i].split('.')[-1]
@@ -37,9 +36,8 @@ class SearchProvidersTests(TestCase):
         searchproviders._initialized = True
         searchproviders._initialized = True
         searchproviders._providers = [MockProvider, MockProvider, MockProvider]
         searchproviders._providers = [MockProvider, MockProvider, MockProvider]
 
 
-        self.assertEqual(
-            [m.__class__ for m in searchproviders.get_providers(True)],
-            searchproviders._providers)
+        self.assertEqual([m.__class__ for m in searchproviders.get_providers(True)],
+                         searchproviders._providers)
 
 
     def test_providers_are_init_with_request(self):
     def test_providers_are_init_with_request(self):
         """providers constructor is provided with request"""
         """providers constructor is provided with request"""
@@ -48,18 +46,14 @@ class SearchProvidersTests(TestCase):
         searchproviders._initialized = True
         searchproviders._initialized = True
         searchproviders._providers = [MockProvider]
         searchproviders._providers = [MockProvider]
 
 
-        self.assertEqual(
-            searchproviders.get_providers('REQUEST')[0].request, 'REQUEST')
+        self.assertEqual(searchproviders.get_providers('REQUEST')[0].request, 'REQUEST')
 
 
     def test_get_allowed_providers(self):
     def test_get_allowed_providers(self):
-        """
-        get_allowed_providers returns only providers that didn't raise in allow_search
-        """
+        """get_allowed_providers returns only providers that didn't raise in allow_search"""
         searchproviders = SearchProviders([])
         searchproviders = SearchProviders([])
 
 
         searchproviders._initialized = True
         searchproviders._initialized = True
         searchproviders._providers = [MockProvider, DisallowedProvider, MockProvider]
         searchproviders._providers = [MockProvider, DisallowedProvider, MockProvider]
 
 
-        self.assertEqual(
-            [m.__class__ for m in searchproviders.get_allowed_providers(True)],
-            [MockProvider, MockProvider])
+        self.assertEqual([m.__class__ for m in searchproviders.get_allowed_providers(True)],
+                         [MockProvider, MockProvider])

+ 21 - 20
misago/search/tests/test_views.py

@@ -13,14 +13,11 @@ class LandingTests(AuthenticatedUserTestCase):
 
 
     def test_no_permission(self):
     def test_no_permission(self):
         """view validates permission to search forum"""
         """view validates permission to search forum"""
-        override_acl(self.user, {
-            'can_search': 0
-        })
+        override_acl(self.user, {'can_search': 0})
 
 
         response = self.client.get(self.test_link)
         response = self.client.get(self.test_link)
 
 
-        self.assertContains(
-            response, "have permission to search site", status_code=403)
+        self.assertContains(response, "have permission to search site", status_code=403)
 
 
     def test_redirect_to_provider(self):
     def test_redirect_to_provider(self):
         """view validates permission to search forum"""
         """view validates permission to search forum"""
@@ -33,38 +30,42 @@ class LandingTests(AuthenticatedUserTestCase):
 class SearchTests(AuthenticatedUserTestCase):
 class SearchTests(AuthenticatedUserTestCase):
     def test_no_permission(self):
     def test_no_permission(self):
         """view validates permission to search forum"""
         """view validates permission to search forum"""
-        override_acl(self.user, {
-            'can_search': 0
-        })
+        override_acl(self.user, {'can_search': 0})
 
 
         response = self.client.get(
         response = self.client.get(
-            reverse('misago:search', kwargs={'search_provider': 'users'}))
+            reverse('misago:search', kwargs={
+                'search_provider': 'users',
+            })
+        )
 
 
-        self.assertContains(
-            response, "have permission to search site", status_code=403)
+        self.assertContains(response, "have permission to search site", status_code=403)
 
 
     def test_not_found(self):
     def test_not_found(self):
         """view raises 404 for not found provider"""
         """view raises 404 for not found provider"""
-        response = self.client.get(
-            reverse('misago:search', kwargs={'search_provider': 'nada'}))
+        response = self.client.get(reverse('misago:search', kwargs={
+            'search_provider': 'nada',
+        }))
 
 
         self.assertEqual(response.status_code, 404)
         self.assertEqual(response.status_code, 404)
 
 
     def test_provider_no_permission(self):
     def test_provider_no_permission(self):
         """provider raises 403 without permission"""
         """provider raises 403 without permission"""
-        override_acl(self.user, {
-            'can_search_users': 0
-        })
+        override_acl(self.user, {'can_search_users': 0})
 
 
         response = self.client.get(
         response = self.client.get(
-            reverse('misago:search', kwargs={'search_provider': 'users'}))
+            reverse('misago:search', kwargs={
+                'search_provider': 'users',
+            })
+        )
 
 
-        self.assertContains(
-            response, "have permission to search users", status_code=403)
+        self.assertContains(response, "have permission to search users", status_code=403)
 
 
     def test_provider(self):
     def test_provider(self):
         """provider displays no script page"""
         """provider displays no script page"""
         response = self.client.get(
         response = self.client.get(
-            reverse('misago:search', kwargs={'search_provider': 'threads'}))
+            reverse('misago:search', kwargs={
+                'search_provider': 'threads',
+            })
+        )
 
 
         self.assertContains(response, "Loading search...")
         self.assertContains(response, "Loading search...")

+ 0 - 2
misago/search/urls/__init__.py

@@ -2,9 +2,7 @@ from django.conf.urls import url
 
 
 from misago.search.views import landing, search
 from misago.search.views import landing, search
 
 
-
 urlpatterns = [
 urlpatterns = [
     url(r'^search/$', landing, name='search'),
     url(r'^search/$', landing, name='search'),
     url(r'^search/(?P<search_provider>[-a-zA-Z0-9]+)/$', search, name='search'),
     url(r'^search/(?P<search_provider>[-a-zA-Z0-9]+)/$', search, name='search'),
 ]
 ]
-

+ 1 - 1
misago/search/views.py

@@ -39,7 +39,7 @@ def search(request, search_provider):
             'url': reverse('misago:search', kwargs={'search_provider': provider.url}),
             'url': reverse('misago:search', kwargs={'search_provider': provider.url}),
             'api': reverse('misago:api:search', kwargs={'search_provider': provider.url}),
             'api': reverse('misago:api:search', kwargs={'search_provider': provider.url}),
             'results': None,
             'results': None,
-            'time': None
+            'time': None,
         })
         })
 
 
     return render(request, 'misago/search.html')
     return render(request, 'misago/search.html')

+ 6 - 4
misago/threads/admin.py

@@ -10,7 +10,8 @@ class MisagoAdminExtension(object):
     def register_urlpatterns(self, urlpatterns):
     def register_urlpatterns(self, urlpatterns):
         # Attachment
         # Attachment
         urlpatterns.namespace(r'^attachments/', 'attachments', 'system')
         urlpatterns.namespace(r'^attachments/', 'attachments', 'system')
-        urlpatterns.patterns('system:attachments',
+        urlpatterns.patterns(
+            'system:attachments',
             url(r'^$', AttachmentsList.as_view(), name='index'),
             url(r'^$', AttachmentsList.as_view(), name='index'),
             url(r'^(?P<page>\d+)/$', AttachmentsList.as_view(), name='index'),
             url(r'^(?P<page>\d+)/$', AttachmentsList.as_view(), name='index'),
             url(r'^delete/(?P<pk>\d+)/$', DeleteAttachment.as_view(), name='delete'),
             url(r'^delete/(?P<pk>\d+)/$', DeleteAttachment.as_view(), name='delete'),
@@ -18,7 +19,8 @@ class MisagoAdminExtension(object):
 
 
         # AttachmentType
         # AttachmentType
         urlpatterns.namespace(r'^attachment-types/', 'attachment-types', 'system')
         urlpatterns.namespace(r'^attachment-types/', 'attachment-types', 'system')
-        urlpatterns.patterns('system:attachment-types',
+        urlpatterns.patterns(
+            'system:attachment-types',
             url(r'^$', AttachmentTypesList.as_view(), name='index'),
             url(r'^$', AttachmentTypesList.as_view(), name='index'),
             url(r'^new/$', NewAttachmentType.as_view(), name='new'),
             url(r'^new/$', NewAttachmentType.as_view(), name='new'),
             url(r'^edit/(?P<pk>\d+)/$', EditAttachmentType.as_view(), name='edit'),
             url(r'^edit/(?P<pk>\d+)/$', EditAttachmentType.as_view(), name='edit'),
@@ -31,12 +33,12 @@ class MisagoAdminExtension(object):
             icon='fa fa-cubes',
             icon='fa fa-cubes',
             parent='misago:admin:system',
             parent='misago:admin:system',
             after='misago:admin:system:settings:index',
             after='misago:admin:system:settings:index',
-            link='misago:admin:system:attachments:index'
+            link='misago:admin:system:attachments:index',
         )
         )
         site.add_node(
         site.add_node(
             name=_("Attachment types"),
             name=_("Attachment types"),
             icon='fa fa-cube',
             icon='fa fa-cube',
             parent='misago:admin:system',
             parent='misago:admin:system',
             after='misago:admin:system:attachments:index',
             after='misago:admin:system:attachments:index',
-            link='misago:admin:system:attachment-types:index'
+            link='misago:admin:system:attachment-types:index',
         )
         )

+ 16 - 12
misago/threads/api/attachments.py

@@ -21,9 +21,7 @@ class AttachmentViewSet(viewsets.ViewSet):
         try:
         try:
             return self.create_attachment(request)
             return self.create_attachment(request)
         except ValidationError as e:
         except ValidationError as e:
-            return Response({
-                'detail': e.args[0]
-            }, status=400)
+            return Response({'detail': e.args[0]}, status=400)
 
 
     def create_attachment(self, request):
     def create_attachment(self, request):
         upload = request.FILES.get('upload')
         upload = request.FILES.get('upload')
@@ -86,17 +84,23 @@ def validate_filetype(upload, user_roles):
 def validate_filesize(upload, filetype, hard_limit):
 def validate_filesize(upload, filetype, hard_limit):
     if upload.size > hard_limit * 1024:
     if upload.size > hard_limit * 1024:
         message = _("You can't upload files larger than %(limit)s (your file has %(upload)s).")
         message = _("You can't upload files larger than %(limit)s (your file has %(upload)s).")
-        raise ValidationError(message % {
-            'upload': filesizeformat(upload.size).rstrip('.0'),
-            'limit': filesizeformat(hard_limit * 1024).rstrip('.0')
-        })
+        raise ValidationError(
+            message % {
+                'upload': filesizeformat(upload.size).rstrip('.0'),
+                'limit': filesizeformat(hard_limit * 1024).rstrip('.0'),
+            }
+        )
 
 
     if filetype.size_limit and upload.size > filetype.size_limit * 1024:
     if filetype.size_limit and upload.size > filetype.size_limit * 1024:
-        message = _("You can't upload files of this type larger than %(limit)s (your file has %(upload)s).")
-        raise ValidationError(message % {
-            'upload': filesizeformat(upload.size).rstrip('.0'),
-            'limit': filesizeformat(filetype.size_limit * 1024).rstrip('.0')
-        })
+        message = _(
+            "You can't upload files of this type larger than %(limit)s (your file has %(upload)s)."
+        )
+        raise ValidationError(
+            message % {
+                'upload': filesizeformat(upload.size).rstrip('.0'),
+                'limit': filesizeformat(filetype.size_limit * 1024).rstrip('.0'),
+            }
+        )
 
 
 
 
 def is_upload_image(upload):
 def is_upload_image(upload):

+ 3 - 2
misago/threads/api/pollvotecreateendpoint.py

@@ -46,7 +46,8 @@ def validate_votes(poll, votes):
             message = ungettext(
             message = ungettext(
                 "This poll disallows voting for more than %(choices)s choice.",
                 "This poll disallows voting for more than %(choices)s choice.",
                 "This poll disallows voting for more than %(choices)s choices.",
                 "This poll disallows voting for more than %(choices)s choices.",
-                poll.allowed_choices)
+                poll.allowed_choices,
+            )
             raise ValidationError(message % {'choices': poll.allowed_choices})
             raise ValidationError(message % {'choices': poll.allowed_choices})
     except TypeError:
     except TypeError:
         raise ValidationError(_("One or more of poll choices were invalid."))
         raise ValidationError(_("One or more of poll choices were invalid."))
@@ -94,5 +95,5 @@ def set_new_votes(request, poll, final_votes):
                 voter_name=request.user.username,
                 voter_name=request.user.username,
                 voter_slug=request.user.slug,
                 voter_slug=request.user.slug,
                 choice_hash=choice['hash'],
                 choice_hash=choice['hash'],
-                voter_ip=request.user_ip
+                voter_ip=request.user_ip,
             )
             )

+ 1 - 2
misago/threads/api/postendpoints/edits.py

@@ -2,7 +2,6 @@ from rest_framework.response import Response
 
 
 from django.core.exceptions import PermissionDenied
 from django.core.exceptions import PermissionDenied
 from django.db.models import F
 from django.db.models import F
-from django.http import Http404
 from django.shortcuts import get_object_or_404
 from django.shortcuts import get_object_or_404
 from django.utils import timezone
 from django.utils import timezone
 from django.utils.translation import ugettext as _
 from django.utils.translation import ugettext as _
@@ -50,7 +49,7 @@ def revert_post_endpoint(request, post):
         editor_slug=request.user.slug,
         editor_slug=request.user.slug,
         editor_ip=request.user_ip,
         editor_ip=request.user_ip,
         edited_from=post.original,
         edited_from=post.original,
-        edited_to=edit.edited_from
+        edited_to=edit.edited_from,
     )
     )
 
 
     parsing_result = common_flavour(request, post.poster, edit.edited_from)
     parsing_result = common_flavour(request, post.poster, edit.edited_from)

+ 1 - 3
misago/threads/api/postendpoints/likes.py

@@ -4,9 +4,7 @@ from misago.threads.serializers import PostLikeSerializer
 
 
 
 
 def likes_list_endpoint(request, post):
 def likes_list_endpoint(request, post):
-    queryset = post.postlike_set.values(
-        'id', 'liker_id', 'liker_name', 'liker_slug', 'liked_on'
-    )
+    queryset = post.postlike_set.values('id', 'liker_id', 'liker_name', 'liker_slug', 'liked_on')
 
 
     likes = []
     likes = []
     for like in queryset.iterator():
     for like in queryset.iterator():

+ 7 - 3
misago/threads/api/postendpoints/merge.py

@@ -68,7 +68,8 @@ def clean_posts_for_merge(request, thread):
         message = ungettext(
         message = ungettext(
             "No more than %(limit)s post can be merged at single time.",
             "No more than %(limit)s post can be merged at single time.",
             "No more than %(limit)s posts can be merged at single time.",
             "No more than %(limit)s posts can be merged at single time.",
-            MERGE_LIMIT)
+            MERGE_LIMIT,
+        )
         raise MergeError(message % {'limit': MERGE_LIMIT})
         raise MergeError(message % {'limit': MERGE_LIMIT})
 
 
     posts_queryset = exclude_invisible_posts(request.user, thread.category, thread.post_set)
     posts_queryset = exclude_invisible_posts(request.user, thread.category, thread.post_set)
@@ -78,7 +79,9 @@ def clean_posts_for_merge(request, thread):
     for post in posts_queryset:
     for post in posts_queryset:
         if post.is_event:
         if post.is_event:
             raise MergeError(_("Events can't be merged."))
             raise MergeError(_("Events can't be merged."))
-        if post.is_hidden and not (post.pk == thread.first_post_id or thread.category.acl['can_hide_posts']):
+        if post.is_hidden and not (
+                post.pk == thread.first_post_id or thread.category.acl['can_hide_posts']
+        ):
             raise MergeError(_("You can't merge posts the content you can't see."))
             raise MergeError(_("You can't merge posts the content you can't see."))
 
 
         if not posts:
         if not posts:
@@ -93,7 +96,8 @@ def clean_posts_for_merge(request, thread):
                     raise MergeError(authorship_error)
                     raise MergeError(authorship_error)
 
 
             if posts[0].pk != thread.first_post_id:
             if posts[0].pk != thread.first_post_id:
-                if posts[0].is_hidden != post.is_hidden or posts[0].is_unapproved != post.is_unapproved:
+                if (posts[0].is_hidden != post.is_hidden or
+                        posts[0].is_unapproved != post.is_unapproved):
                     raise MergeError(_("Posts with different visibility can't be merged."))
                     raise MergeError(_("Posts with different visibility can't be merged."))
 
 
             posts.append(post)
             posts.append(post)

+ 8 - 2
misago/threads/api/postendpoints/move.py

@@ -54,7 +54,12 @@ def clean_thread_for_move(request, thread, viewmodel):
     try:
     try:
         new_thread = viewmodel(request, new_thread_id, select_for_update=True).unwrap()
         new_thread = viewmodel(request, new_thread_id, select_for_update=True).unwrap()
     except Http404:
     except Http404:
-        raise PermissionDenied(_("The thread you have entered link to doesn't exist or you don't have permission to see it."))
+        raise PermissionDenied(
+            _(
+                "The thread you have entered link to doesn't "
+                "exist or you don't have permission to see it."
+            )
+        )
 
 
     if not new_thread.acl['can_reply']:
     if not new_thread.acl['can_reply']:
         raise PermissionDenied(_("You can't move posts to threads you can't reply."))
         raise PermissionDenied(_("You can't move posts to threads you can't reply."))
@@ -74,7 +79,8 @@ def clean_posts_for_move(request, thread):
         message = ungettext(
         message = ungettext(
             "No more than %(limit)s post can be moved at single time.",
             "No more than %(limit)s post can be moved at single time.",
             "No more than %(limit)s posts can be moved at single time.",
             "No more than %(limit)s posts can be moved at single time.",
-            MOVE_LIMIT)
+            MOVE_LIMIT,
+        )
         raise PermissionDenied(message % {'limit': MOVE_LIMIT})
         raise PermissionDenied(message % {'limit': MOVE_LIMIT})
 
 
     posts_queryset = exclude_invisible_posts(request.user, thread.category, thread.post_set)
     posts_queryset = exclude_invisible_posts(request.user, thread.category, thread.post_set)

+ 4 - 0
misago/threads/api/postendpoints/patch_event.py

@@ -16,6 +16,8 @@ def patch_acl(request, event, value):
         return {'acl': event.acl}
         return {'acl': event.acl}
     else:
     else:
         return {'acl': None}
         return {'acl': None}
+
+
 event_patch_dispatcher.add('acl', patch_acl)
 event_patch_dispatcher.add('acl', patch_acl)
 
 
 
 
@@ -29,6 +31,8 @@ def patch_is_hidden(request, event, value):
         return {'is_hidden': event.is_hidden}
         return {'is_hidden': event.is_hidden}
     else:
     else:
         raise PermissionDenied(_("You don't have permission to hide this event."))
         raise PermissionDenied(_("You don't have permission to hide this event."))
+
+
 event_patch_dispatcher.replace('is-hidden', patch_is_hidden)
 event_patch_dispatcher.replace('is-hidden', patch_is_hidden)
 
 
 
 

+ 12 - 2
misago/threads/api/postendpoints/patch_post.py

@@ -19,6 +19,8 @@ def patch_acl(request, post, value):
         return {'acl': post.acl}
         return {'acl': post.acl}
     else:
     else:
         return {'acl': None}
         return {'acl': None}
+
+
 post_patch_dispatcher.add('acl', patch_acl)
 post_patch_dispatcher.add('acl', patch_acl)
 
 
 
 
@@ -48,7 +50,7 @@ def patch_is_liked(request, post, value):
             liker=request.user,
             liker=request.user,
             liker_name=request.user.username,
             liker_name=request.user.username,
             liker_slug=request.user.slug,
             liker_slug=request.user.slug,
-            liker_ip=request.user_ip
+            liker_ip=request.user_ip,
         )
         )
         post.likes += 1
         post.likes += 1
 
 
@@ -61,7 +63,7 @@ def patch_is_liked(request, post, value):
     for like in post.postlike_set.all()[:4]:
     for like in post.postlike_set.all()[:4]:
         post.last_likes.append({
         post.last_likes.append({
             'id': like.liker_id,
             'id': like.liker_id,
-            'username': like.liker_name
+            'username': like.liker_name,
         })
         })
 
 
     post.save(update_fields=['likes', 'last_likes'])
     post.save(update_fields=['likes', 'last_likes'])
@@ -71,6 +73,8 @@ def patch_is_liked(request, post, value):
         'last_likes': post.last_likes or [],
         'last_likes': post.last_likes or [],
         'is_liked': value,
         'is_liked': value,
     }
     }
+
+
 post_patch_dispatcher.replace('is-liked', patch_is_liked)
 post_patch_dispatcher.replace('is-liked', patch_is_liked)
 
 
 
 
@@ -81,6 +85,8 @@ def patch_is_protected(request, post, value):
     else:
     else:
         moderation.unprotect_post(request.user, post)
         moderation.unprotect_post(request.user, post)
     return {'is_protected': post.is_protected}
     return {'is_protected': post.is_protected}
+
+
 post_patch_dispatcher.replace('is-protected', patch_is_protected)
 post_patch_dispatcher.replace('is-protected', patch_is_protected)
 
 
 
 
@@ -89,6 +95,8 @@ def patch_is_unapproved(request, post, value):
         allow_approve_post(request.user, post)
         allow_approve_post(request.user, post)
         moderation.approve_post(request.user, post)
         moderation.approve_post(request.user, post)
     return {'is_unapproved': post.is_unapproved}
     return {'is_unapproved': post.is_unapproved}
+
+
 post_patch_dispatcher.replace('is-unapproved', patch_is_unapproved)
 post_patch_dispatcher.replace('is-unapproved', patch_is_unapproved)
 
 
 
 
@@ -101,6 +109,8 @@ def patch_is_hidden(request, post, value):
         moderation.unhide_post(request.user, post)
         moderation.unhide_post(request.user, post)
 
 
     return {'is_hidden': post.is_hidden}
     return {'is_hidden': post.is_hidden}
+
+
 post_patch_dispatcher.replace('is-hidden', patch_is_hidden)
 post_patch_dispatcher.replace('is-hidden', patch_is_hidden)
 
 
 
 

+ 1 - 1
misago/threads/api/postendpoints/read.py

@@ -11,6 +11,6 @@ def post_read_endpoint(request, thread, post):
             thread.subscription.last_read_on = post.posted_on
             thread.subscription.last_read_on = post.posted_on
             thread.subscription.save()
             thread.subscription.save()
         return Response({
         return Response({
-            'thread_is_read': thread.last_post_on <= post.posted_on
+            'thread_is_read': thread.last_post_on <= post.posted_on,
         })
         })
     return Response({'thread_is_read': True})
     return Response({'thread_is_read': True})

+ 3 - 4
misago/threads/api/postendpoints/split.py

@@ -1,4 +1,3 @@
-from rest_framework import serializers
 from rest_framework.response import Response
 from rest_framework.response import Response
 
 
 from django.core.exceptions import PermissionDenied
 from django.core.exceptions import PermissionDenied
@@ -6,7 +5,6 @@ from django.utils.translation import ugettext as _
 from django.utils.translation import ungettext
 from django.utils.translation import ungettext
 
 
 from misago.conf import settings
 from misago.conf import settings
-from misago.threads.events import record_event
 from misago.threads.models import Thread
 from misago.threads.models import Thread
 from misago.threads.moderation import threads as moderation
 from misago.threads.moderation import threads as moderation
 from misago.threads.permissions import exclude_invisible_posts
 from misago.threads.permissions import exclude_invisible_posts
@@ -50,7 +48,8 @@ def clean_posts_for_split(request, thread):
         message = ungettext(
         message = ungettext(
             "No more than %(limit)s post can be split at single time.",
             "No more than %(limit)s post can be split at single time.",
             "No more than %(limit)s posts can be split at single time.",
             "No more than %(limit)s posts can be split at single time.",
-            SPLIT_LIMIT)
+            SPLIT_LIMIT,
+        )
         raise SplitError(message % {'limit': SPLIT_LIMIT})
         raise SplitError(message % {'limit': SPLIT_LIMIT})
 
 
     posts_queryset = exclude_invisible_posts(request.user, thread.category, thread.post_set)
     posts_queryset = exclude_invisible_posts(request.user, thread.category, thread.post_set)
@@ -77,7 +76,7 @@ def split_posts_to_new_thread(request, thread, validated_data, posts):
     new_thread = Thread(
     new_thread = Thread(
         category=validated_data['category'],
         category=validated_data['category'],
         started_on=thread.started_on,
         started_on=thread.started_on,
-        last_post_on=thread.last_post_on
+        last_post_on=thread.last_post_on,
     )
     )
 
 
     new_thread.set_title(validated_data['title'])
     new_thread.set_title(validated_data['title'])

+ 12 - 11
misago/threads/api/postingendpoint/__init__.py

@@ -20,11 +20,7 @@ class PostingEndpoint(object):
 
 
     def __init__(self, request, mode, **kwargs):
     def __init__(self, request, mode, **kwargs):
         self.kwargs = kwargs
         self.kwargs = kwargs
-        self.kwargs.update({
-            'mode': mode,
-            'request': request,
-            'user': request.user
-        })
+        self.kwargs.update({'mode': mode, 'request': request, 'user': request.user})
 
 
         self.__dict__.update(kwargs)
         self.__dict__.update(kwargs)
 
 
@@ -102,13 +98,17 @@ class PostingEndpoint(object):
     def save(self):
     def save(self):
         """save new state to backend"""
         """save new state to backend"""
         if not self._is_validated or self.errors:
         if not self._is_validated or self.errors:
-            raise RuntimeError("You need to validate posting data successfully before calling save")
+            raise RuntimeError(
+                "You need to validate posting data successfully before calling save"
+            )
 
 
         try:
         try:
             for middleware, obj in self.middlewares:
             for middleware, obj in self.middlewares:
                 obj.pre_save(self._serializers.get(middleware))
                 obj.pre_save(self._serializers.get(middleware))
         except PostingInterrupt as e:
         except PostingInterrupt as e:
-            raise ValueError("Posting process can only be interrupted from within interrupt_posting method")
+            raise ValueError(
+                "Posting process can only be interrupted from within interrupt_posting method"
+            )
 
 
         try:
         try:
             for middleware, obj in self.middlewares:
             for middleware, obj in self.middlewares:
@@ -122,13 +122,14 @@ class PostingEndpoint(object):
             for middleware, obj in self.middlewares:
             for middleware, obj in self.middlewares:
                 obj.post_save(self._serializers.get(middleware))
                 obj.post_save(self._serializers.get(middleware))
         except PostingInterrupt as e:
         except PostingInterrupt as e:
-            raise ValueError("Posting process can only be interrupted from within interrupt_posting method")
+            raise ValueError(
+                "Posting process can only be interrupted from within interrupt_posting method"
+            )
 
 
 
 
 class PostingMiddleware(object):
 class PostingMiddleware(object):
-    """
-    Abstract middleware class
-    """
+    """abstract middleware class"""
+
     def __init__(self, **kwargs):
     def __init__(self, **kwargs):
         self.kwargs = kwargs
         self.kwargs = kwargs
         self.__dict__.update(kwargs)
         self.__dict__.update(kwargs)

+ 28 - 20
misago/threads/api/postingendpoint/attachments.py

@@ -7,7 +7,7 @@ from misago.acl import add_acl
 from misago.conf import settings
 from misago.conf import settings
 from misago.threads.serializers import AttachmentSerializer
 from misago.threads.serializers import AttachmentSerializer
 
 
-from . import PostingEndpoint, PostingInterrupt, PostingMiddleware
+from . import PostingEndpoint, PostingMiddleware
 
 
 
 
 class AttachmentsMiddleware(PostingMiddleware):
 class AttachmentsMiddleware(PostingMiddleware):
@@ -15,21 +15,21 @@ class AttachmentsMiddleware(PostingMiddleware):
         return bool(self.user.acl_cache['max_attachment_size'])
         return bool(self.user.acl_cache['max_attachment_size'])
 
 
     def get_serializer(self):
     def get_serializer(self):
-        return AttachmentsSerializer(data=self.request.data, context={
-            'mode': self.mode,
-            'user': self.user,
-            'post': self.post,
-        })
+        return AttachmentsSerializer(
+            data=self.request.data,
+            context={
+                'mode': self.mode,
+                'user': self.user,
+                'post': self.post,
+            }
+        )
 
 
     def save(self, serializer):
     def save(self, serializer):
         serializer.save()
         serializer.save()
 
 
 
 
 class AttachmentsSerializer(serializers.Serializer):
 class AttachmentsSerializer(serializers.Serializer):
-    attachments = serializers.ListField(
-       child=serializers.IntegerField(),
-       required=False
-    )
+    attachments = serializers.ListField(child=serializers.IntegerField(), required=False)
 
 
     def validate_attachments(self, ids):
     def validate_attachments(self, ids):
         self.update_attachments = False
         self.update_attachments = False
@@ -41,11 +41,12 @@ class AttachmentsSerializer(serializers.Serializer):
         validate_attachments_count(ids)
         validate_attachments_count(ids)
 
 
         attachments = self.get_initial_attachments(
         attachments = self.get_initial_attachments(
-            self.context['mode'], self.context['user'], self.context['post'])
+            self.context['mode'], self.context['user'], self.context['post']
+        )
         new_attachments = self.get_new_attachments(self.context['user'], ids)
         new_attachments = self.get_new_attachments(self.context['user'], ids)
 
 
         if not attachments and not new_attachments:
         if not attachments and not new_attachments:
-            return [] # no attachments
+            return []  # no attachments
 
 
         # clean existing attachments
         # clean existing attachments
         for attachment in attachments:
         for attachment in attachments:
@@ -56,8 +57,12 @@ class AttachmentsSerializer(serializers.Serializer):
                     self.update_attachments = True
                     self.update_attachments = True
                     self.removed_attachments.append(attachment)
                     self.removed_attachments.append(attachment)
                 else:
                 else:
-                    message = _("You don't have permission to remove \"%(attachment)s\" attachment.")
-                    raise serializers.ValidationError(message % {'attachment': attachment.filename})
+                    message = _(
+                        "You don't have permission to remove \"%(attachment)s\" attachment."
+                    )
+                    raise serializers.ValidationError(
+                        message % {'attachment': attachment.filename}
+                    )
 
 
         if new_attachments:
         if new_attachments:
             self.update_attachments = True
             self.update_attachments = True
@@ -78,7 +83,7 @@ class AttachmentsSerializer(serializers.Serializer):
 
 
         queryset = user.attachment_set.select_related('filetype').filter(
         queryset = user.attachment_set.select_related('filetype').filter(
             post__isnull=True,
             post__isnull=True,
-            id__in=ids
+            id__in=ids,
         )
         )
 
 
         return list(queryset)
         return list(queryset)
@@ -122,8 +127,11 @@ def validate_attachments_count(data):
         message = ungettext(
         message = ungettext(
             "You can't attach more than %(limit_value)s file to single post (added %(show_value)s).",
             "You can't attach more than %(limit_value)s file to single post (added %(show_value)s).",
             "You can't attach more than %(limit_value)s flies to single post (added %(show_value)s).",
             "You can't attach more than %(limit_value)s flies to single post (added %(show_value)s).",
-            settings.MISAGO_POST_ATTACHMENTS_LIMIT)
-        raise serializers.ValidationError(message % {
-            'limit_value': settings.MISAGO_POST_ATTACHMENTS_LIMIT,
-            'show_value': total_attachments
-        })
+            settings.MISAGO_POST_ATTACHMENTS_LIMIT,
+        )
+        raise serializers.ValidationError(
+            message % {
+                'limit_value': settings.MISAGO_POST_ATTACHMENTS_LIMIT,
+                'show_value': total_attachments,
+            }
+        )

+ 11 - 10
misago/threads/api/postingendpoint/category.py

@@ -15,9 +15,8 @@ from . import PostingEndpoint, PostingMiddleware
 
 
 
 
 class CategoryMiddleware(PostingMiddleware):
 class CategoryMiddleware(PostingMiddleware):
-    """
-    Middleware that validates category id and sets category on thread and post instances
-    """
+    """middleware that validates category id and sets category on thread and post instances"""
+
     def use_this_middleware(self):
     def use_this_middleware(self):
         if self.mode == PostingEndpoint.START:
         if self.mode == PostingEndpoint.START:
             return self.tree_name == THREADS_ROOT_NAME
             return self.tree_name == THREADS_ROOT_NAME
@@ -41,10 +40,12 @@ class CategoryMiddleware(PostingMiddleware):
 
 
 
 
 class CategorySerializer(serializers.Serializer):
 class CategorySerializer(serializers.Serializer):
-    category = serializers.IntegerField(error_messages={
-        'required': ugettext_lazy("You have to select category to post thread in."),
-        'invalid': ugettext_lazy("Selected category is invalid.")
-    })
+    category = serializers.IntegerField(
+        error_messages={
+            'required': ugettext_lazy("You have to select category to post thread in."),
+            'invalid': ugettext_lazy("Selected category is invalid."),
+        }
+    )
 
 
     def __init__(self, user, *args, **kwargs):
     def __init__(self, user, *args, **kwargs):
         self.user = user
         self.user = user
@@ -55,8 +56,7 @@ class CategorySerializer(serializers.Serializer):
     def validate_category(self, value):
     def validate_category(self, value):
         try:
         try:
             self.category_cache = Category.objects.get(
             self.category_cache = Category.objects.get(
-                pk=value,
-                tree_id=trees_map.get_tree_id_for_root(THREADS_ROOT_NAME)
+                pk=value, tree_id=trees_map.get_tree_id_for_root(THREADS_ROOT_NAME)
             )
             )
 
 
             can_see = can_see_category(self.user, self.category_cache)
             can_see = can_see_category(self.user, self.category_cache)
@@ -69,4 +69,5 @@ class CategorySerializer(serializers.Serializer):
             raise serializers.ValidationError(e.args[0])
             raise serializers.ValidationError(e.args[0])
         except Category.DoesNotExist:
         except Category.DoesNotExist:
             raise serializers.ValidationError(
             raise serializers.ValidationError(
-                _("Selected category doesn't exist or you don't have permission to browse it."))
+                _("Selected category doesn't exist or you don't have permission to browse it.")
+            )

+ 4 - 7
misago/threads/api/postingendpoint/emailnotification.py

@@ -18,7 +18,7 @@ class EmailNotificationMiddleware(PostingMiddleware):
     def post_save(self, serializer):
     def post_save(self, serializer):
         queryset = self.thread.subscription_set.filter(
         queryset = self.thread.subscription_set.filter(
             send_email=True,
             send_email=True,
-            last_read_on__gte=self.previous_last_post_on
+            last_read_on__gte=self.previous_last_post_on,
         ).exclude(user=self.user).select_related('user')
         ).exclude(user=self.user).select_related('user')
 
 
         notifications = []
         notifications = []
@@ -40,10 +40,7 @@ class EmailNotificationMiddleware(PostingMiddleware):
         else:
         else:
             subject = _('%(user)s has replied to thread "%(thread)s" that you are watching')
             subject = _('%(user)s has replied to thread "%(thread)s" that you are watching')
 
 
-        subject_formats = {
-            'user': self.user.username,
-            'thread': self.thread.title
-        }
+        subject_formats = {'user': self.user.username, 'thread': self.thread.title}
 
 
         return build_mail(
         return build_mail(
             self.request,
             self.request,
@@ -52,6 +49,6 @@ class EmailNotificationMiddleware(PostingMiddleware):
             'misago/emails/thread/reply',
             'misago/emails/thread/reply',
             {
             {
                 'thread': self.thread,
                 'thread': self.thread,
-                'post': self.post
-            }
+                'post': self.post,
+            },
         )
         )

+ 2 - 1
misago/threads/api/postingendpoint/floodprotection.py

@@ -13,7 +13,8 @@ MIN_POSTING_PAUSE = 3
 
 
 class FloodProtectionMiddleware(PostingMiddleware):
 class FloodProtectionMiddleware(PostingMiddleware):
     def use_this_middleware(self):
     def use_this_middleware(self):
-        return not self.user.acl_cache['can_omit_flood_protection'] and self.mode != PostingEndpoint.EDIT
+        return not self.user.acl_cache['can_omit_flood_protection'
+                                       ] and self.mode != PostingEndpoint.EDIT
 
 
     def interrupt_posting(self, serializer):
     def interrupt_posting(self, serializer):
         now = timezone.now()
         now = timezone.now()

+ 13 - 15
misago/threads/api/postingendpoint/participants.py

@@ -23,9 +23,7 @@ class ParticipantsMiddleware(PostingMiddleware):
         return False
         return False
 
 
     def get_serializer(self):
     def get_serializer(self):
-        return ParticipantsSerializer(data=self.request.data, context={
-            'user': self.user
-        })
+        return ParticipantsSerializer(data=self.request.data, context={'user': self.user})
 
 
     def save(self, serializer):
     def save(self, serializer):
         set_owner(self.thread, self.user)
         set_owner(self.thread, self.user)
@@ -33,10 +31,7 @@ class ParticipantsMiddleware(PostingMiddleware):
 
 
 
 
 class ParticipantsSerializer(serializers.Serializer):
 class ParticipantsSerializer(serializers.Serializer):
-    to = serializers.ListField(
-       child=serializers.CharField(),
-       required=True
-    )
+    to = serializers.ListField(child=serializers.CharField(), required=True)
 
 
     def validate_to(self, usernames):
     def validate_to(self, usernames):
         clean_usernames = self.clean_usernames(usernames)
         clean_usernames = self.clean_usernames(usernames)
@@ -49,7 +44,8 @@ class ParticipantsSerializer(serializers.Serializer):
 
 
             if clean_name == self.context['user'].slug:
             if clean_name == self.context['user'].slug:
                 raise serializers.ValidationError(
                 raise serializers.ValidationError(
-                    _("You can't include yourself on the list of users to invite to new thread."))
+                    _("You can't include yourself on the list of users to invite to new thread.")
+                )
 
 
             if clean_name and clean_name not in clean_usernames:
             if clean_name and clean_name not in clean_usernames:
                 clean_usernames.append(clean_name)
                 clean_usernames.append(clean_name)
@@ -62,11 +58,14 @@ class ParticipantsSerializer(serializers.Serializer):
             message = ungettext(
             message = ungettext(
                 "You can't add more than %(users)s user to private thread (you've added %(added)s).",
                 "You can't add more than %(users)s user to private thread (you've added %(added)s).",
                 "You can't add more than %(users)s users to private thread (you've added %(added)s).",
                 "You can't add more than %(users)s users to private thread (you've added %(added)s).",
-                max_participants)
-            raise serializers.ValidationError(message % {
-                'users': max_participants,
-                'added': len(clean_usernames)
-            })
+                max_participants,
+            )
+            raise serializers.ValidationError(
+                message % {
+                    'users': max_participants,
+                    'added': len(clean_usernames),
+                }
+            )
 
 
         return list(set(clean_usernames))
         return list(set(clean_usernames))
 
 
@@ -84,7 +83,6 @@ class ParticipantsSerializer(serializers.Serializer):
             sorted_usernames = sorted(invalid_usernames)
             sorted_usernames = sorted(invalid_usernames)
 
 
             message = _("One or more users could not be found: %(usernames)s")
             message = _("One or more users could not be found: %(usernames)s")
-            raise serializers.ValidationError(
-                message % {'usernames': ', '.join(sorted_usernames)})
+            raise serializers.ValidationError(message % {'usernames': ', '.join(sorted_usernames)})
 
 
         return users
         return users

+ 2 - 3
misago/threads/api/postingendpoint/privatethread.py

@@ -6,9 +6,8 @@ from . import PostingEndpoint, PostingMiddleware
 
 
 
 
 class PrivateThreadMiddleware(PostingMiddleware):
 class PrivateThreadMiddleware(PostingMiddleware):
-    """
-    Middleware that sets private threads category for thread and post
-    """
+    """middleware that sets private threads category for thread and post"""
+
     def use_this_middleware(self):
     def use_this_middleware(self):
         if self.mode == PostingEndpoint.START:
         if self.mode == PostingEndpoint.START:
             return self.tree_name == PRIVATE_THREADS_ROOT_NAME
             return self.tree_name == PRIVATE_THREADS_ROOT_NAME

+ 4 - 8
misago/threads/api/postingendpoint/recordedit.py

@@ -21,13 +21,9 @@ class RecordEditMiddleware(PostingMiddleware):
         self.post.last_editor_name = self.user.username
         self.post.last_editor_name = self.user.username
         self.post.last_editor_slug = self.user.slug
         self.post.last_editor_slug = self.user.slug
 
 
-        self.post.update_fields.extend((
-            'updated_on',
-            'edits',
-            'last_editor',
-            'last_editor_name',
-            'last_editor_slug',
-        ))
+        self.post.update_fields.extend(
+            ('updated_on', 'edits', 'last_editor', 'last_editor_name', 'last_editor_slug', )
+        )
 
 
         self.post.edits_record.create(
         self.post.edits_record.create(
             category=self.post.category,
             category=self.post.category,
@@ -38,5 +34,5 @@ class RecordEditMiddleware(PostingMiddleware):
             editor_slug=self.user.slug,
             editor_slug=self.user.slug,
             editor_ip=self.request.user_ip,
             editor_ip=self.request.user_ip,
             edited_from=self.original_post,
             edited_from=self.original_post,
-            edited_to=self.post.original
+            edited_to=self.post.original,
         )
         )

+ 2 - 4
misago/threads/api/postingendpoint/reply.py

@@ -1,9 +1,7 @@
 from rest_framework import serializers
 from rest_framework import serializers
 
 
-from django.db.models import F
 from django.utils.translation import ugettext_lazy
 from django.utils.translation import ugettext_lazy
 
 
-from misago.conf import settings
 from misago.markup import common_flavour
 from misago.markup import common_flavour
 from misago.threads.checksums import update_post_checksum
 from misago.threads.checksums import update_post_checksum
 from misago.threads.validators import validate_post, validate_title
 from misago.threads.validators import validate_post, validate_title
@@ -87,7 +85,7 @@ class ReplySerializer(serializers.Serializer):
     post = serializers.CharField(
     post = serializers.CharField(
         validators=[validate_post],
         validators=[validate_post],
         error_messages={
         error_messages={
-            'required': ugettext_lazy("You have to enter a message.")
+            'required': ugettext_lazy("You have to enter a message."),
         }
         }
     )
     )
 
 
@@ -96,6 +94,6 @@ class ThreadSerializer(ReplySerializer):
     title = serializers.CharField(
     title = serializers.CharField(
         validators=[validate_title],
         validators=[validate_title],
         error_messages={
         error_messages={
-        'required': ugettext_lazy("You have to enter thread title.")
+            'required': ugettext_lazy("You have to enter thread title."),
         }
         }
     )
     )

+ 3 - 4
misago/threads/api/postingendpoint/subscribe.py

@@ -26,7 +26,7 @@ class SubscribeMiddleware(PostingMiddleware):
         self.user.subscription_set.create(
         self.user.subscription_set.create(
             category=self.thread.category,
             category=self.thread.category,
             thread=self.thread,
             thread=self.thread,
-            send_email=self.user.subscribe_to_started_threads == UserModel.SUBSCRIBE_ALL
+            send_email=self.user.subscribe_to_started_threads == UserModel.SUBSCRIBE_ALL,
         )
         )
 
 
     def subscribe_replied_thread(self):
     def subscribe_replied_thread(self):
@@ -37,8 +37,7 @@ class SubscribeMiddleware(PostingMiddleware):
             return
             return
 
 
         try:
         try:
-            subscription = self.user.subscription_set.get(thread=self.thread)
-            return
+            return self.user.subscription_set.get(thread=self.thread)
         except Subscription.DoesNotExist:
         except Subscription.DoesNotExist:
             pass
             pass
 
 
@@ -49,5 +48,5 @@ class SubscribeMiddleware(PostingMiddleware):
         self.user.subscription_set.create(
         self.user.subscription_set.create(
             category=self.thread.category,
             category=self.thread.category,
             thread=self.thread,
             thread=self.thread,
-            send_email=self.user.subscribe_to_replied_threads == UserModel.SUBSCRIBE_ALL
+            send_email=self.user.subscribe_to_replied_threads == UserModel.SUBSCRIBE_ALL,
         )
         )

+ 3 - 4
misago/threads/api/postingendpoint/syncprivatethreads.py

@@ -5,9 +5,8 @@ from . import PostingEndpoint, PostingMiddleware
 
 
 
 
 class SyncPrivateThreadsMiddleware(PostingMiddleware):
 class SyncPrivateThreadsMiddleware(PostingMiddleware):
-    """
-    Middleware that sets private thread participants to sync unread threads
-    """
+    """middleware that sets private thread participants to sync unread threads"""
+
     def use_this_middleware(self):
     def use_this_middleware(self):
         if self.mode == PostingEndpoint.REPLY:
         if self.mode == PostingEndpoint.REPLY:
             return self.thread.thread_type.root_name == PRIVATE_THREADS_ROOT_NAME
             return self.thread.thread_type.root_name == PRIVATE_THREADS_ROOT_NAME
@@ -16,5 +15,5 @@ class SyncPrivateThreadsMiddleware(PostingMiddleware):
     def post_save(self, serializer):
     def post_save(self, serializer):
         set_users_unread_private_threads_sync(
         set_users_unread_private_threads_sync(
             participants=self.thread.participants_list,
             participants=self.thread.participants_list,
-            exclude_user=self.user
+            exclude_user=self.user,
         )
         )

+ 5 - 3
misago/threads/api/threadendpoints/editor.py

@@ -31,7 +31,7 @@ def thread_start_editor(request):
             post = {
             post = {
                 'close': bool(category.acl['can_close_threads']),
                 'close': bool(category.acl['can_close_threads']),
                 'hide': bool(category.acl['can_hide_threads']),
                 'hide': bool(category.acl['can_hide_threads']),
-                'pin': category.acl['can_pin_threads']
+                'pin': category.acl['can_pin_threads'],
             }
             }
 
 
             available.append(category.pk)
             available.append(category.pk)
@@ -43,7 +43,7 @@ def thread_start_editor(request):
             'id': category.pk,
             'id': category.pk,
             'name': category.name,
             'name': category.name,
             'level': category.level - 1,
             'level': category.level - 1,
-            'post': post
+            'post': post,
         })
         })
 
 
     # list only categories that allow new threads, or contains subcategory that allows one
     # list only categories that allow new threads, or contains subcategory that allows one
@@ -53,6 +53,8 @@ def thread_start_editor(request):
             cleaned_categories.append(category)
             cleaned_categories.append(category)
 
 
     if not cleaned_categories:
     if not cleaned_categories:
-        raise PermissionDenied(_("No categories that allow new threads are available to you at the moment."))
+        raise PermissionDenied(
+            _("No categories that allow new threads are available to you at the moment.")
+        )
 
 
     return Response(cleaned_categories)
     return Response(cleaned_categories)

+ 1 - 1
misago/threads/api/threadendpoints/list.py

@@ -11,7 +11,7 @@ class ThreadsList(object):
     def __call__(self, request, **kwargs):
     def __call__(self, request, **kwargs):
         page = get_int_or_404(request.query_params.get('page', 0))
         page = get_int_or_404(request.query_params.get('page', 0))
         if page == 1:
         if page == 1:
-            page = 0 # api allows explicit first page
+            page = 0  # api allows explicit first page
 
 
         list_type = request.query_params.get('list', 'all')
         list_type = request.query_params.get('list', 'all')
 
 

+ 38 - 33
misago/threads/api/threadendpoints/merge.py

@@ -1,6 +1,6 @@
 from rest_framework.response import Response
 from rest_framework.response import Response
 
 
-from django.core.exceptions import PermissionDenied, ValidationError
+from django.core.exceptions import PermissionDenied
 from django.http import Http404
 from django.http import Http404
 from django.utils.translation import gettext as _
 from django.utils.translation import gettext as _
 from django.utils.translation import ungettext
 from django.utils.translation import ungettext
@@ -19,7 +19,7 @@ from misago.threads.utils import add_categories_to_items, get_thread_id_from_url
 from .pollmergehandler import PollMergeHandler
 from .pollmergehandler import PollMergeHandler
 
 
 
 
-MERGE_LIMIT = 20 # no more than 20 threads can be merged in single action
+MERGE_LIMIT = 20  # no more than 20 threads can be merged in single action
 
 
 
 
 class MergeError(Exception):
 class MergeError(Exception):
@@ -42,15 +42,21 @@ def thread_merge_endpoint(request, thread, viewmodel):
         if not can_reply_thread(request.user, other_thread):
         if not can_reply_thread(request.user, other_thread):
             raise PermissionDenied(_("You can't merge this thread into thread you can't reply."))
             raise PermissionDenied(_("You can't merge this thread into thread you can't reply."))
         if not other_thread.acl['can_merge']:
         if not other_thread.acl['can_merge']:
-            raise PermissionDenied(_("You don't have permission to merge this thread with current one."))
+            raise PermissionDenied(
+                _("You don't have permission to merge this thread with current one.")
+            )
     except PermissionDenied as e:
     except PermissionDenied as e:
-        return Response({
-            'detail': e.args[0]
-        }, status=400)
+        return Response({'detail': e.args[0]}, status=400)
     except Http404:
     except Http404:
-        return Response({
-            'detail': _("The thread you have entered link to doesn't exist or you don't have permission to see it.")
-        }, status=400)
+        return Response(
+            {
+                'detail': _(
+                    "The thread you have entered link to doesn't "
+                    "exist or you don't have permission to see it."
+                )
+            },
+            status=400,
+        )
 
 
     polls_handler = PollMergeHandler([thread, other_thread])
     polls_handler = PollMergeHandler([thread, other_thread])
     if len(polls_handler.polls) == 1:
     if len(polls_handler.polls) == 1:
@@ -67,13 +73,9 @@ def thread_merge_endpoint(request, thread, viewmodel):
                 elif not poll:
                 elif not poll:
                     other_thread.poll.delete()
                     other_thread.poll.delete()
             else:
             else:
-                return Response({
-                    'detail': _("Invalid choice.")
-                }, status=400)
+                return Response({'detail': _("Invalid choice.")}, status=400)
         else:
         else:
-            return Response({
-                'polls': polls_handler.get_available_resolutions()
-            }, status=400)
+            return Response({'polls': polls_handler.get_available_resolutions()}, status=400)
 
 
     moderation.merge_thread(request, other_thread, thread)
     moderation.merge_thread(request, other_thread, thread)
 
 
@@ -90,7 +92,7 @@ def thread_merge_endpoint(request, thread, viewmodel):
     return Response({
     return Response({
         'id': other_thread.pk,
         'id': other_thread.pk,
         'title': other_thread.title,
         'title': other_thread.title,
-        'url': other_thread.get_absolute_url()
+        'url': other_thread.get_absolute_url(),
     })
     })
 
 
 
 
@@ -106,9 +108,7 @@ def threads_merge_endpoint(request):
             invalid_threads.append({
             invalid_threads.append({
                 'id': thread.pk,
                 'id': thread.pk,
                 'title': thread.title,
                 'title': thread.title,
-                'errors': [
-                    _("You don't have permission to merge this thread with others.")
-                ]
+                'errors': [_("You don't have permission to merge this thread with others.")]
             })
             })
 
 
     if invalid_threads:
     if invalid_threads:
@@ -125,13 +125,9 @@ def threads_merge_endpoint(request):
                 if polls_handler.is_valid():
                 if polls_handler.is_valid():
                     poll = polls_handler.get_resolution()
                     poll = polls_handler.get_resolution()
                 else:
                 else:
-                    return Response({
-                        'detail': _("Invalid choice.")
-                    }, status=400)
+                    return Response({'detail': _("Invalid choice.")}, status=400)
             else:
             else:
-                return Response({
-                    'polls': polls_handler.get_available_resolutions()
-                }, status=400)
+                return Response({'polls': polls_handler.get_available_resolutions()}, status=400)
         else:
         else:
             poll = None
             poll = None
 
 
@@ -153,7 +149,8 @@ def clean_threads_for_merge(request):
         message = ungettext(
         message = ungettext(
             "No more than %(limit)s thread can be merged at single time.",
             "No more than %(limit)s thread can be merged at single time.",
             "No more than %(limit)s threads can be merged at single time.",
             "No more than %(limit)s threads can be merged at single time.",
-            MERGE_LIMIT)
+            MERGE_LIMIT,
+        )
         raise MergeError(message % {'limit': MERGE_LIMIT})
         raise MergeError(message % {'limit': MERGE_LIMIT})
 
 
     threads_tree_id = trees_map.get_tree_id_for_root(THREADS_ROOT_NAME)
     threads_tree_id = trees_map.get_tree_id_for_root(THREADS_ROOT_NAME)
@@ -179,7 +176,7 @@ def merge_threads(request, validated_data, threads, poll):
     new_thread = Thread(
     new_thread = Thread(
         category=validated_data['category'],
         category=validated_data['category'],
         started_on=threads[0].started_on,
         started_on=threads[0].started_on,
-        last_post_on=threads[0].last_post_on
+        last_post_on=threads[0].last_post_on,
     )
     )
 
 
     new_thread.set_title(validated_data['title'])
     new_thread.set_title(validated_data['title'])
@@ -194,9 +191,15 @@ def merge_threads(request, validated_data, threads, poll):
         new_thread.merge(thread)
         new_thread.merge(thread)
         thread.delete()
         thread.delete()
 
 
-        record_event(request, new_thread, 'merged', {
-            'merged_thread': thread.title,
-        }, commit=False)
+        record_event(
+            request,
+            new_thread,
+            'merged',
+            {
+                'merged_thread': thread.title,
+            },
+            commit=False,
+        )
 
 
     new_thread.synchronize()
     new_thread.synchronize()
     new_thread.save()
     new_thread.save()
@@ -223,9 +226,11 @@ def merge_threads(request, validated_data, threads, poll):
 
 
     # add top category to thread
     # add top category to thread
     if validated_data.get('top_category'):
     if validated_data.get('top_category'):
-        categories = list(Category.objects.all_categories().filter(
-            id__in=request.user.acl_cache['visible_categories']
-        ))
+        categories = list(
+            Category.objects.all_categories().filter(
+                id__in=request.user.acl_cache['visible_categories'],
+            )
+        )
         add_categories_to_items(validated_data['top_category'], categories, [new_thread])
         add_categories_to_items(validated_data['top_category'], categories, [new_thread])
     else:
     else:
         new_thread.top_category = None
         new_thread.top_category = None

+ 42 - 31
misago/threads/api/threadendpoints/patch.py

@@ -10,7 +10,6 @@ from misago.categories.permissions import allow_browse_category, allow_see_categ
 from misago.categories.serializers import CategorySerializer
 from misago.categories.serializers import CategorySerializer
 from misago.core.apipatch import ApiPatch
 from misago.core.apipatch import ApiPatch
 from misago.core.shortcuts import get_int_or_404
 from misago.core.shortcuts import get_int_or_404
-from misago.threads.models import ThreadParticipant
 from misago.threads.moderation import threads as moderation
 from misago.threads.moderation import threads as moderation
 from misago.threads.participants import (
 from misago.threads.participants import (
     add_participant, change_owner, make_participants_aware, remove_participant)
     add_participant, change_owner, make_participants_aware, remove_participant)
@@ -34,6 +33,8 @@ def patch_acl(request, thread, value):
         return {'acl': thread.acl}
         return {'acl': thread.acl}
     else:
     else:
         return {'acl': None}
         return {'acl': None}
+
+
 thread_patch_dispatcher.add('acl', patch_acl)
 thread_patch_dispatcher.add('acl', patch_acl)
 
 
 
 
@@ -52,6 +53,8 @@ def patch_title(request, thread, value):
 
 
     moderation.change_thread_title(request, thread, value_cleaned)
     moderation.change_thread_title(request, thread, value_cleaned)
     return {'title': thread.title}
     return {'title': thread.title}
+
+
 thread_patch_dispatcher.replace('title', patch_title)
 thread_patch_dispatcher.replace('title', patch_title)
 
 
 
 
@@ -73,6 +76,8 @@ def patch_weight(request, thread, value):
         moderation.unpin_thread(request, thread)
         moderation.unpin_thread(request, thread)
 
 
     return {'weight': thread.weight}
     return {'weight': thread.weight}
+
+
 thread_patch_dispatcher.replace('weight', patch_weight)
 thread_patch_dispatcher.replace('weight', patch_weight)
 
 
 
 
@@ -82,8 +87,7 @@ def patch_move(request, thread, value):
 
 
     category_pk = get_int_or_404(value)
     category_pk = get_int_or_404(value)
     new_category = get_object_or_404(
     new_category = get_object_or_404(
-        Category.objects.all_categories().select_related('parent'),
-        pk=category_pk
+        Category.objects.all_categories().select_related('parent'), pk=category_pk
     )
     )
 
 
     add_acl(request.user, new_category)
     add_acl(request.user, new_category)
@@ -98,21 +102,24 @@ def patch_move(request, thread, value):
 
 
     return {'category': CategorySerializer(new_category).data}
     return {'category': CategorySerializer(new_category).data}
 
 
+
 thread_patch_dispatcher.replace('category', patch_move)
 thread_patch_dispatcher.replace('category', patch_move)
 
 
 
 
 def patch_top_category(request, thread, value):
 def patch_top_category(request, thread, value):
     category_pk = get_int_or_404(value)
     category_pk = get_int_or_404(value)
     root_category = get_object_or_404(
     root_category = get_object_or_404(
-        Category.objects.all_categories(include_root=True),
-        pk=category_pk
+        Category.objects.all_categories(include_root=True), pk=category_pk
     )
     )
 
 
-    categories = list(Category.objects.all_categories().filter(
-        id__in=request.user.acl_cache['visible_categories']
-    ))
+    categories = list(
+        Category.objects.all_categories()
+        .filter(id__in=request.user.acl_cache['visible_categories'])
+    )
     add_categories_to_items(root_category, categories, [thread])
     add_categories_to_items(root_category, categories, [thread])
     return {'top_category': CategorySerializer(thread.top_category).data}
     return {'top_category': CategorySerializer(thread.top_category).data}
+
+
 thread_patch_dispatcher.add('top-category', patch_top_category)
 thread_patch_dispatcher.add('top-category', patch_top_category)
 
 
 
 
@@ -122,11 +129,10 @@ def patch_flatten_categories(request, thread, value):
             'category': thread.category_id,
             'category': thread.category_id,
             'top_category': thread.top_category.pk,
             'top_category': thread.top_category.pk,
         }
         }
-    except AttributeError as e:
-        return {
-            'category': thread.category_id,
-            'top_category': None
-        }
+    except AttributeError:
+        return {'category': thread.category_id, 'top_category': None}
+
+
 thread_patch_dispatcher.replace('flatten-categories', patch_flatten_categories)
 thread_patch_dispatcher.replace('flatten-categories', patch_flatten_categories)
 
 
 
 
@@ -143,6 +149,8 @@ def patch_is_unapproved(request, thread, value):
         }
         }
     else:
     else:
         raise PermissionDenied(_("You don't have permission to approve this thread."))
         raise PermissionDenied(_("You don't have permission to approve this thread."))
+
+
 thread_patch_dispatcher.replace('is-unapproved', patch_is_unapproved)
 thread_patch_dispatcher.replace('is-unapproved', patch_is_unapproved)
 
 
 
 
@@ -159,6 +167,8 @@ def patch_is_closed(request, thread, value):
             raise PermissionDenied(_("You don't have permission to close this thread."))
             raise PermissionDenied(_("You don't have permission to close this thread."))
         else:
         else:
             raise PermissionDenied(_("You don't have permission to open this thread."))
             raise PermissionDenied(_("You don't have permission to open this thread."))
+
+
 thread_patch_dispatcher.replace('is-closed', patch_is_closed)
 thread_patch_dispatcher.replace('is-closed', patch_is_closed)
 
 
 
 
@@ -172,6 +182,8 @@ def patch_is_hidden(request, thread, value):
         return {'is_hidden': thread.is_hidden}
         return {'is_hidden': thread.is_hidden}
     else:
     else:
         raise PermissionDenied(_("You don't have permission to hide this thread."))
         raise PermissionDenied(_("You don't have permission to hide this thread."))
+
+
 thread_patch_dispatcher.replace('is-hidden', patch_is_hidden)
 thread_patch_dispatcher.replace('is-hidden', patch_is_hidden)
 
 
 
 
@@ -198,6 +210,8 @@ def patch_subscription(request, thread, value):
         return {'subscription': True}
         return {'subscription': True}
     else:
     else:
         return {'subscription': None}
         return {'subscription': None}
+
+
 thread_patch_dispatcher.replace('subscription', patch_subscription)
 thread_patch_dispatcher.replace('subscription', patch_subscription)
 
 
 
 
@@ -207,8 +221,7 @@ def patch_add_participant(request, thread, value):
     try:
     try:
         username = six.text_type(value).strip().lower()
         username = six.text_type(value).strip().lower()
         if not username:
         if not username:
-            raise PermissionDenied(
-                _("You have to enter new participant's username."))
+            raise PermissionDenied(_("You have to enter new participant's username."))
         participant = UserModel.objects.get(slug=username)
         participant = UserModel.objects.get(slug=username)
     except UserModel.DoesNotExist:
     except UserModel.DoesNotExist:
         raise PermissionDenied(_("No user with such name exists."))
         raise PermissionDenied(_("No user with such name exists."))
@@ -220,12 +233,11 @@ def patch_add_participant(request, thread, value):
     add_participant(request, thread, participant)
     add_participant(request, thread, participant)
 
 
     make_participants_aware(request.user, thread)
     make_participants_aware(request.user, thread)
-    participants = ThreadParticipantSerializer(
-        thread.participants_list, many=True)
+    participants = ThreadParticipantSerializer(thread.participants_list, many=True)
+
+    return {'participants': participants.data}
+
 
 
-    return {
-        'participants': participants.data
-    }
 thread_patch_dispatcher.add('participants', patch_add_participant)
 thread_patch_dispatcher.add('participants', patch_add_participant)
 
 
 
 
@@ -245,18 +257,17 @@ def patch_remove_participant(request, thread, value):
     remove_participant(request, thread, participant.user)
     remove_participant(request, thread, participant.user)
 
 
     if len(thread.participants_list) == 1:
     if len(thread.participants_list) == 1:
-        return {
-            'deleted': True
-        }
+        return {'deleted': True}
     else:
     else:
         make_participants_aware(request.user, thread)
         make_participants_aware(request.user, thread)
-        participants = ThreadParticipantSerializer(
-            thread.participants_list, many=True)
+        participants = ThreadParticipantSerializer(thread.participants_list, many=True)
 
 
         return {
         return {
             'deleted': False,
             'deleted': False,
-            'participants': participants.data
+            'participants': participants.data,
         }
         }
+
+
 thread_patch_dispatcher.remove('participants', patch_remove_participant)
 thread_patch_dispatcher.remove('participants', patch_remove_participant)
 
 
 
 
@@ -280,9 +291,9 @@ def patch_replace_owner(request, thread, value):
 
 
     make_participants_aware(request.user, thread)
     make_participants_aware(request.user, thread)
     participants = ThreadParticipantSerializer(thread.participants_list, many=True)
     participants = ThreadParticipantSerializer(thread.participants_list, many=True)
-    return {
-        'participants': participants.data
-    }
+    return {'participants': participants.data}
+
+
 thread_patch_dispatcher.replace('owner', patch_replace_owner)
 thread_patch_dispatcher.replace('owner', patch_replace_owner)
 
 
 
 
@@ -299,9 +310,9 @@ def thread_patch_endpoint(request, thread):
     unapproved_changed = old_is_unapproved != thread.is_unapproved
     unapproved_changed = old_is_unapproved != thread.is_unapproved
     category_changed = old_category != thread.category
     category_changed = old_category != thread.category
 
 
-    title_changed = old_is_hidden != thread.is_hidden
+    title_changed = old_title != thread.title
     if thread.category.last_thread_id != thread.pk:
     if thread.category.last_thread_id != thread.pk:
-        title_changed = False # don't trigger resync on simple title change
+        title_changed = False  # don't trigger resync on simple title change
 
 
     if hidden_changed or unapproved_changed or category_changed:
     if hidden_changed or unapproved_changed or category_changed:
         thread.category.synchronize()
         thread.category.synchronize()

+ 3 - 4
misago/threads/api/threadendpoints/read.py

@@ -14,7 +14,8 @@ def read_threads(user, pk):
     category_id = get_int_or_404(pk)
     category_id = get_int_or_404(pk)
     threads_tree_id = trees_map.get_tree_id_for_root(THREADS_ROOT_NAME)
     threads_tree_id = trees_map.get_tree_id_for_root(THREADS_ROOT_NAME)
 
 
-    category = get_object_or_404(Category,
+    category = get_object_or_404(
+        Category,
         id=category_id,
         id=category_id,
         tree_id=threads_tree_id,
         tree_id=threads_tree_id,
     )
     )
@@ -32,6 +33,4 @@ def read_private_threads(user):
 
 
     user.sync_unread_private_threads = False
     user.sync_unread_private_threads = False
     user.unread_private_threads = 0
     user.unread_private_threads = 0
-    user.save(update_fields=[
-        'sync_unread_private_threads', 'unread_private_threads'
-    ])
+    user.save(update_fields=['sync_unread_private_threads', 'unread_private_threads'])

+ 3 - 2
misago/threads/api/threadpoll.py

@@ -113,7 +113,7 @@ class ViewSet(viewsets.ViewSet):
         thread.save()
         thread.save()
 
 
         return Response({
         return Response({
-            'can_start_poll': can_start_poll(request.user, thread)
+            'can_start_poll': can_start_poll(request.user, thread),
         })
         })
 
 
     @detail_route(methods=['get', 'post'])
     @detail_route(methods=['get', 'post'])
@@ -152,7 +152,8 @@ class ViewSet(viewsets.ViewSet):
             choices.append(choice)
             choices.append(choice)
 
 
         queryset = thread.poll.pollvote_set.values(
         queryset = thread.poll.pollvote_set.values(
-            'voter_id', 'voter_name', 'voter_slug', 'voted_on', 'choice_hash')
+            'voter_id', 'voter_name', 'voter_slug', 'voted_on', 'choice_hash'
+        )
 
 
         for voter in queryset.order_by('voter_name').iterator():
         for voter in queryset.order_by('voter_name').iterator():
             voters[voter['choice_hash']].append(PollVoteSerializer(voter).data)
             voters[voter['choice_hash']].append(PollVoteSerializer(voter).data)

+ 25 - 16
misago/threads/api/threadposts.py

@@ -32,22 +32,30 @@ class ViewSet(viewsets.ViewSet):
     posts = ThreadPosts
     posts = ThreadPosts
     post_ = ThreadPost
     post_ = ThreadPost
 
 
-    def get_thread(self, request, pk, read_aware=True, subscription_aware=True, select_for_update=False):
+    def get_thread(
+            self,
+            request,
+            pk,
+            read_aware=True,
+            subscription_aware=True,
+            select_for_update=False,
+    ):
         return self.thread(
         return self.thread(
             request,
             request,
             get_int_or_404(pk),
             get_int_or_404(pk),
             None,
             None,
             read_aware,
             read_aware,
             subscription_aware,
             subscription_aware,
-            select_for_update
+            select_for_update,
         )
         )
 
 
     def get_thread_for_update(self, request, pk):
     def get_thread_for_update(self, request, pk):
         return self.get_thread(
         return self.get_thread(
-            request, pk,
+            request,
+            pk,
             read_aware=False,
             read_aware=False,
             subscription_aware=False,
             subscription_aware=False,
-            select_for_update=True
+            select_for_update=True,
         )
         )
 
 
     def get_posts(self, request, thread, page):
     def get_posts(self, request, thread, page):
@@ -62,7 +70,7 @@ class ViewSet(viewsets.ViewSet):
     def list(self, request, thread_pk):
     def list(self, request, thread_pk):
         page = get_int_or_404(request.query_params.get('page', 0))
         page = get_int_or_404(request.query_params.get('page', 0))
         if page == 1:
         if page == 1:
-            page = 0 # api allows explicit first page
+            page = 0  # api allows explicit first page
 
 
         thread = self.get_thread(request, thread_pk)
         thread = self.get_thread(request, thread_pk)
         posts = self.get_posts(request, thread, page)
         posts = self.get_posts(request, thread, page)
@@ -95,14 +103,17 @@ class ViewSet(viewsets.ViewSet):
         thread = self.get_thread_for_update(request, thread_pk).unwrap()
         thread = self.get_thread_for_update(request, thread_pk).unwrap()
         allow_reply_thread(request.user, thread)
         allow_reply_thread(request.user, thread)
 
 
-        post = Post(thread=thread, category=thread.category)
+        post = Post(
+            thread=thread,
+            category=thread.category,
+        )
 
 
         # Put them through posting pipeline
         # Put them through posting pipeline
         posting = PostingEndpoint(
         posting = PostingEndpoint(
             request,
             request,
             PostingEndpoint.REPLY,
             PostingEndpoint.REPLY,
             thread=thread,
             thread=thread,
-            post=post
+            post=post,
         )
         )
 
 
         if posting.is_valid():
         if posting.is_valid():
@@ -132,7 +143,7 @@ class ViewSet(viewsets.ViewSet):
             request,
             request,
             PostingEndpoint.EDIT,
             PostingEndpoint.EDIT,
             thread=thread,
             thread=thread,
-            post=post
+            post=post,
         )
         )
 
 
         if posting.is_valid():
         if posting.is_valid():
@@ -197,7 +208,7 @@ class ViewSet(viewsets.ViewSet):
             request,
             request,
             thread_pk,
             thread_pk,
             read_aware=False,
             read_aware=False,
-            subscription_aware=False
+            subscription_aware=False,
         )
         )
         post = self.get_post(request, thread, pk).unwrap()
         post = self.get_post(request, thread, pk).unwrap()
 
 
@@ -208,7 +219,8 @@ class ViewSet(viewsets.ViewSet):
             add_acl(request.user, attachment)
             add_acl(request.user, attachment)
             attachments.append(attachment)
             attachments.append(attachment)
         attachments_json = AttachmentSerializer(
         attachments_json = AttachmentSerializer(
-            attachments, many=True, context={'user': request.user}).data
+            attachments, many=True, context={'user': request.user}
+        ).data
 
 
         return Response({
         return Response({
             'id': post.pk,
             'id': post.pk,
@@ -217,16 +229,13 @@ class ViewSet(viewsets.ViewSet):
             'attachments': attachments_json,
             'attachments': attachments_json,
             'can_protect': bool(thread.category.acl['can_protect_posts']),
             'can_protect': bool(thread.category.acl['can_protect_posts']),
             'is_protected': post.is_protected,
             'is_protected': post.is_protected,
-            'poster': post.poster_name
+            'poster': post.poster_name,
         })
         })
 
 
     @list_route(methods=['get'], url_path='editor')
     @list_route(methods=['get'], url_path='editor')
     def reply_editor(self, request, thread_pk):
     def reply_editor(self, request, thread_pk):
         thread = self.get_thread(
         thread = self.get_thread(
-            request,
-            thread_pk,
-            read_aware=False,
-            subscription_aware=False
+            request, thread_pk, read_aware=False, subscription_aware=False
         ).unwrap()
         ).unwrap()
         allow_reply_thread(request.user, thread)
         allow_reply_thread(request.user, thread)
 
 
@@ -241,7 +250,7 @@ class ViewSet(viewsets.ViewSet):
             return Response({
             return Response({
                 'id': reply_to.pk,
                 'id': reply_to.pk,
                 'post': reply_to.original,
                 'post': reply_to.original,
-                'poster': reply_to.poster_name
+                'poster': reply_to.poster_name,
             })
             })
         else:
         else:
             return Response({})
             return Response({})

+ 11 - 8
misago/threads/api/threads.py

@@ -24,22 +24,25 @@ from .threadendpoints.read import read_private_threads, read_threads
 class ViewSet(viewsets.ViewSet):
 class ViewSet(viewsets.ViewSet):
     thread = None
     thread = None
 
 
-    def get_thread(self, request, pk, read_aware=True, subscription_aware=True, select_for_update=False):
+    def get_thread(
+            self, request, pk, read_aware=True, subscription_aware=True, select_for_update=False
+    ):
         return self.thread(
         return self.thread(
             request,
             request,
             get_int_or_404(pk),
             get_int_or_404(pk),
             None,
             None,
             read_aware,
             read_aware,
             subscription_aware,
             subscription_aware,
-            select_for_update
+            select_for_update,
         )
         )
 
 
     def get_thread_for_update(self, request, pk):
     def get_thread_for_update(self, request, pk):
         return self.get_thread(
         return self.get_thread(
-            request, pk,
+            request,
+            pk,
             read_aware=False,
             read_aware=False,
             subscription_aware=False,
             subscription_aware=False,
-            select_for_update=True
+            select_for_update=True,
         )
         )
 
 
     def retrieve(self, request, pk):
     def retrieve(self, request, pk):
@@ -80,7 +83,7 @@ class ThreadViewSet(ViewSet):
             PostingEndpoint.START,
             PostingEndpoint.START,
             tree_name=THREADS_ROOT_NAME,
             tree_name=THREADS_ROOT_NAME,
             thread=thread,
             thread=thread,
-            post=post
+            post=post,
         )
         )
 
 
         if posting.is_valid():
         if posting.is_valid():
@@ -89,7 +92,7 @@ class ThreadViewSet(ViewSet):
             return Response({
             return Response({
                 'id': thread.pk,
                 'id': thread.pk,
                 'title': thread.title,
                 'title': thread.title,
-                'url': thread.get_absolute_url()
+                'url': thread.get_absolute_url(),
             })
             })
         else:
         else:
             return Response(posting.errors, status=400)
             return Response(posting.errors, status=400)
@@ -138,7 +141,7 @@ class PrivateThreadViewSet(ViewSet):
             PostingEndpoint.START,
             PostingEndpoint.START,
             tree_name=PRIVATE_THREADS_ROOT_NAME,
             tree_name=PRIVATE_THREADS_ROOT_NAME,
             thread=thread,
             thread=thread,
-            post=post
+            post=post,
         )
         )
 
 
         if posting.is_valid():
         if posting.is_valid():
@@ -147,7 +150,7 @@ class PrivateThreadViewSet(ViewSet):
             return Response({
             return Response({
                 'id': thread.pk,
                 'id': thread.pk,
                 'title': thread.title,
                 'title': thread.title,
-                'url': thread.get_absolute_url()
+                'url': thread.get_absolute_url(),
             })
             })
         else:
         else:
             return Response(posting.errors, status=400)
             return Response(posting.errors, status=400)

+ 1 - 1
misago/threads/apps.py

@@ -7,4 +7,4 @@ class MisagoThreadsConfig(AppConfig):
     verbose_name = "Misago Threads"
     verbose_name = "Misago Threads"
 
 
     def ready(self):
     def ready(self):
-        from . import signals
+        from . import signals as _

+ 0 - 4
misago/threads/context_processors.py

@@ -4,13 +4,9 @@ from django.urls import reverse
 def preload_threads_urls(request):
 def preload_threads_urls(request):
     request.frontend_context.update({
     request.frontend_context.update({
         'ATTACHMENTS_API': reverse('misago:api:attachment-list'),
         'ATTACHMENTS_API': reverse('misago:api:attachment-list'),
-
         'THREAD_EDITOR_API': reverse('misago:api:thread-editor'),
         'THREAD_EDITOR_API': reverse('misago:api:thread-editor'),
         'THREADS_API': reverse('misago:api:thread-list'),
         'THREADS_API': reverse('misago:api:thread-list'),
-
         'PRIVATE_THREADS_API': reverse('misago:api:private-thread-list'),
         'PRIVATE_THREADS_API': reverse('misago:api:private-thread-list'),
-
-
         'PRIVATE_THREADS_URL': reverse('misago:private-threads'),
         'PRIVATE_THREADS_URL': reverse('misago:private-threads'),
     })
     })
 
 

+ 24 - 16
misago/threads/forms.py

@@ -1,7 +1,7 @@
 from django import forms
 from django import forms
 from django.utils.translation import ugettext as _
 from django.utils.translation import ugettext as _
 
 
-from .models import Attachment, AttachmentType
+from .models import AttachmentType
 
 
 
 
 def get_searchable_filetypes():
 def get_searchable_filetypes():
@@ -18,16 +18,16 @@ class SearchAttachmentsForm(forms.Form):
         coerce=int,
         coerce=int,
         choices=get_searchable_filetypes,
         choices=get_searchable_filetypes,
         empty_value=0,
         empty_value=0,
-        required=False
+        required=False,
     )
     )
     is_orphan = forms.ChoiceField(
     is_orphan = forms.ChoiceField(
         label=_("State"),
         label=_("State"),
         required=False,
         required=False,
-        choices=(
+        choices=[
             ('', _("All")),
             ('', _("All")),
             ('yes', _("Only orphaned")),
             ('yes', _("Only orphaned")),
             ('no', _("Not orphaned")),
             ('no', _("Not orphaned")),
-        ),
+        ],
     )
     )
 
 
     def filter_queryset(self, criteria, queryset):
     def filter_queryset(self, criteria, queryset):
@@ -59,18 +59,26 @@ class AttachmentTypeForm(forms.ModelForm):
         }
         }
         help_texts = {
         help_texts = {
             'extensions': _("List of comma separated file extensions associated with this attachment type."),
             'extensions': _("List of comma separated file extensions associated with this attachment type."),
-            'mimetypes': _("Optional list of comma separated mime types associated with this attachment type."),
-            'size_limit': _("Maximum allowed uploaded file size for this type, in kb. "
-                            "May be overriden via user permission."),
+            'mimetypes': _(
+                "Optional list of comma separated mime types associated with this attachment type."
+            ),
+            'size_limit': _(
+                "Maximum allowed uploaded file size for this type, in kb. "
+                "May be overriden via user permission."
+            ),
             'status': _("Controls this attachment type availability on your site."),
             'status': _("Controls this attachment type availability on your site."),
-            'limit_uploads_to': _("If you wish to limit option to upload files of this type to users with specific "
-                                    "roles, select them on this list. Otherwhise don't select any roles to allow all "
-                                    "users with permission to upload attachments to be able to upload attachments of "
-                                    "this type."),
-            'limit_downloads_to': _("If you wish to limit option to download files of this type to users with "
-                                      "specific roles, select them on this list. Otherwhise don't select any roles to "
-                                      "allow all users with permission to download attachments to be able to download "
-                                      " attachments of this type."),
+            'limit_uploads_to': _(
+                "If you wish to limit option to upload files of this type to users with specific "
+                "roles, select them on this list. Otherwhise don't select any roles to allow all "
+                "users with permission to upload attachments to be able to upload attachments of "
+                "this type."
+            ),
+            'limit_downloads_to': _(
+                "If you wish to limit option to download files of this type to users with "
+                "specific roles, select them on this list. Otherwhise don't select any roles to "
+                "allow all users with permission to download attachments to be able to download "
+                "attachments of this type."
+            ),
         }
         }
         widgets = {
         widgets = {
             'limit_uploads_to': forms.CheckboxSelectMultiple,
             'limit_uploads_to': forms.CheckboxSelectMultiple,
@@ -78,7 +86,7 @@ class AttachmentTypeForm(forms.ModelForm):
         }
         }
 
 
     def clean_extensions(self):
     def clean_extensions(self):
-        data =  self.clean_list(self.cleaned_data['extensions'])
+        data = self.clean_list(self.cleaned_data['extensions'])
         if not data:
         if not data:
             raise forms.ValidationError(_("This field is required."))
             raise forms.ValidationError(_("This field is required."))
         return data
         return data

+ 1 - 1
misago/threads/management/commands/clearattachments.py

@@ -17,7 +17,7 @@ class Command(BaseCommand):
         cutoff = timezone.now() - timedelta(minutes=settings.MISAGO_ATTACHMENT_ORPHANED_EXPIRE)
         cutoff = timezone.now() - timedelta(minutes=settings.MISAGO_ATTACHMENT_ORPHANED_EXPIRE)
         queryset = Attachment.objects.filter(
         queryset = Attachment.objects.filter(
             post__isnull=True,
             post__isnull=True,
-            uploaded_on__lt=cutoff
+            uploaded_on__lt=cutoff,
         )
         )
 
 
         attachments_to_sync = queryset.count()
         attachments_to_sync = queryset.count()

+ 7 - 9
misago/threads/middleware.py

@@ -3,7 +3,6 @@ from django.utils.deprecation import MiddlewareMixin
 from misago.categories.models import Category
 from misago.categories.models import Category
 
 
 from .models import Thread
 from .models import Thread
-from .permissions import exclude_invisible_threads
 from .viewmodels import filter_read_threads_queryset
 from .viewmodels import filter_read_threads_queryset
 
 
 
 
@@ -21,10 +20,7 @@ class UnreadThreadsCountMiddleware(MiddlewareMixin):
         participated_threads = request.user.threadparticipant_set.values('thread_id')
         participated_threads = request.user.threadparticipant_set.values('thread_id')
 
 
         category = Category.objects.private_threads()
         category = Category.objects.private_threads()
-        threads = Thread.objects.filter(
-            category=category,
-            id__in=participated_threads
-        )
+        threads = Thread.objects.filter(category=category, id__in=participated_threads)
 
 
         new_threads = filter_read_threads_queryset(request.user, [category], 'new', threads)
         new_threads = filter_read_threads_queryset(request.user, [category], 'new', threads)
         unread_threads = filter_read_threads_queryset(request.user, [category], 'unread', threads)
         unread_threads = filter_read_threads_queryset(request.user, [category], 'unread', threads)
@@ -32,7 +28,9 @@ class UnreadThreadsCountMiddleware(MiddlewareMixin):
         request.user.unread_private_threads = new_threads.count() + unread_threads.count()
         request.user.unread_private_threads = new_threads.count() + unread_threads.count()
         request.user.sync_unread_private_threads = False
         request.user.sync_unread_private_threads = False
 
 
-        request.user.save(update_fields=[
-            'unread_private_threads',
-            'sync_unread_private_threads',
-        ])
+        request.user.save(
+            update_fields=[
+                'unread_private_threads',
+                'sync_unread_private_threads',
+            ]
+        )

+ 280 - 58
misago/threads/migrations/0001_initial.py

@@ -9,7 +9,7 @@ from django.contrib.postgres.search import SearchVectorField
 from django.db import migrations, models
 from django.db import migrations, models
 
 
 import misago.threads.models.attachment
 import misago.threads.models.attachment
-from misago.core.pgutils import CreatePartialCompositeIndex, CreatePartialIndex
+from misago.core.pgutils import CreatePartialIndex
 
 
 
 
 class Migration(migrations.Migration):
 class Migration(migrations.Migration):
@@ -22,7 +22,11 @@ class Migration(migrations.Migration):
         migrations.CreateModel(
         migrations.CreateModel(
             name='Post',
             name='Post',
             fields=[
             fields=[
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+                (
+                    'id', models.AutoField(
+                        verbose_name='ID', serialize=False, auto_created=True, primary_key=True
+                    )
+                ),
                 ('poster_name', models.CharField(max_length=255)),
                 ('poster_name', models.CharField(max_length=255)),
                 ('poster_ip', models.GenericIPAddressField()),
                 ('poster_ip', models.GenericIPAddressField()),
                 ('original', models.TextField()),
                 ('original', models.TextField()),
@@ -34,7 +38,15 @@ class Migration(migrations.Migration):
                 ('edits', models.PositiveIntegerField(default=0)),
                 ('edits', models.PositiveIntegerField(default=0)),
                 ('last_editor_name', models.CharField(max_length=255, null=True, blank=True)),
                 ('last_editor_name', models.CharField(max_length=255, null=True, blank=True)),
                 ('last_editor_slug', models.SlugField(max_length=255, null=True, blank=True)),
                 ('last_editor_slug', models.SlugField(max_length=255, null=True, blank=True)),
-                ('hidden_by', models.ForeignKey(related_name='+', on_delete=django.db.models.deletion.SET_NULL, blank=True, to=settings.AUTH_USER_MODEL, null=True)),
+                (
+                    'hidden_by', models.ForeignKey(
+                        related_name='+',
+                        on_delete=django.db.models.deletion.SET_NULL,
+                        blank=True,
+                        to=settings.AUTH_USER_MODEL,
+                        null=True
+                    )
+                ),
                 ('hidden_by_name', models.CharField(max_length=255, null=True, blank=True)),
                 ('hidden_by_name', models.CharField(max_length=255, null=True, blank=True)),
                 ('hidden_by_slug', models.SlugField(max_length=255, null=True, blank=True)),
                 ('hidden_by_slug', models.SlugField(max_length=255, null=True, blank=True)),
                 ('hidden_on', models.DateTimeField(default=django.utils.timezone.now)),
                 ('hidden_on', models.DateTimeField(default=django.utils.timezone.now)),
@@ -44,9 +56,28 @@ class Migration(migrations.Migration):
                 ('is_hidden', models.BooleanField(default=False)),
                 ('is_hidden', models.BooleanField(default=False)),
                 ('is_protected', models.BooleanField(default=False)),
                 ('is_protected', models.BooleanField(default=False)),
                 ('category', models.ForeignKey(to='misago_categories.Category')),
                 ('category', models.ForeignKey(to='misago_categories.Category')),
-                ('last_editor', models.ForeignKey(related_name='+', on_delete=django.db.models.deletion.SET_NULL, blank=True, to=settings.AUTH_USER_MODEL, null=True)),
-                ('mentions', models.ManyToManyField(related_name='mention_set', to=settings.AUTH_USER_MODEL)),
-                ('poster', models.ForeignKey(on_delete=django.db.models.deletion.SET_NULL, blank=True, to=settings.AUTH_USER_MODEL, null=True)),
+                (
+                    'last_editor', models.ForeignKey(
+                        related_name='+',
+                        on_delete=django.db.models.deletion.SET_NULL,
+                        blank=True,
+                        to=settings.AUTH_USER_MODEL,
+                        null=True
+                    )
+                ),
+                (
+                    'mentions', models.ManyToManyField(
+                        related_name='mention_set', to=settings.AUTH_USER_MODEL
+                    )
+                ),
+                (
+                    'poster', models.ForeignKey(
+                        on_delete=django.db.models.deletion.SET_NULL,
+                        blank=True,
+                        to=settings.AUTH_USER_MODEL,
+                        null=True
+                    )
+                ),
                 ('is_event', models.BooleanField(default=False, db_index=True)),
                 ('is_event', models.BooleanField(default=False, db_index=True)),
                 ('event_type', models.CharField(max_length=255, null=True, blank=True)),
                 ('event_type', models.CharField(max_length=255, null=True, blank=True)),
                 ('event_context', JSONField(null=True, blank=True)),
                 ('event_context', JSONField(null=True, blank=True)),
@@ -55,9 +86,8 @@ class Migration(migrations.Migration):
                 ('search_document', models.TextField(blank=True, null=True)),
                 ('search_document', models.TextField(blank=True, null=True)),
                 ('search_vector', SearchVectorField()),
                 ('search_vector', SearchVectorField()),
             ],
             ],
-            options={
-            },
-            bases=(models.Model,),
+            options={},
+            bases=(models.Model, ),
         ),
         ),
         CreatePartialIndex(
         CreatePartialIndex(
             field='Post.has_open_reports',
             field='Post.has_open_reports',
@@ -72,7 +102,11 @@ class Migration(migrations.Migration):
         migrations.CreateModel(
         migrations.CreateModel(
             name='Thread',
             name='Thread',
             fields=[
             fields=[
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+                (
+                    'id', models.AutoField(
+                        verbose_name='ID', serialize=False, auto_created=True, primary_key=True
+                    )
+                ),
                 ('title', models.CharField(max_length=255)),
                 ('title', models.CharField(max_length=255)),
                 ('slug', models.CharField(max_length=255)),
                 ('slug', models.CharField(max_length=255)),
                 ('replies', models.PositiveIntegerField(default=0, db_index=True)),
                 ('replies', models.PositiveIntegerField(default=0, db_index=True)),
@@ -94,9 +128,8 @@ class Migration(migrations.Migration):
                 ('is_hidden', models.BooleanField(default=False)),
                 ('is_hidden', models.BooleanField(default=False)),
                 ('is_closed', models.BooleanField(default=False)),
                 ('is_closed', models.BooleanField(default=False)),
             ],
             ],
-            options={
-            },
-            bases=(models.Model,),
+            options={},
+            bases=(models.Model, ),
         ),
         ),
         CreatePartialIndex(
         CreatePartialIndex(
             field='Thread.weight',
             field='Thread.weight',
@@ -111,19 +144,26 @@ class Migration(migrations.Migration):
         migrations.CreateModel(
         migrations.CreateModel(
             name='ThreadParticipant',
             name='ThreadParticipant',
             fields=[
             fields=[
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+                (
+                    'id', models.AutoField(
+                        verbose_name='ID', serialize=False, auto_created=True, primary_key=True
+                    )
+                ),
                 ('thread', models.ForeignKey(to='misago_threads.Thread')),
                 ('thread', models.ForeignKey(to='misago_threads.Thread')),
                 ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
                 ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
                 ('is_owner', models.BooleanField(default=False)),
                 ('is_owner', models.BooleanField(default=False)),
             ],
             ],
-            options={
-            },
-            bases=(models.Model,),
+            options={},
+            bases=(models.Model, ),
         ),
         ),
         migrations.AddField(
         migrations.AddField(
             model_name='thread',
             model_name='thread',
             name='participants',
             name='participants',
-            field=models.ManyToManyField(related_name='privatethread_set', through='misago_threads.ThreadParticipant', to=settings.AUTH_USER_MODEL),
+            field=models.ManyToManyField(
+                related_name='privatethread_set',
+                through='misago_threads.ThreadParticipant',
+                to=settings.AUTH_USER_MODEL
+            ),
             preserve_default=True,
             preserve_default=True,
         ),
         ),
         CreatePartialIndex(
         CreatePartialIndex(
@@ -165,7 +205,13 @@ class Migration(migrations.Migration):
         migrations.AddField(
         migrations.AddField(
             model_name='thread',
             model_name='thread',
             name='first_post',
             name='first_post',
-            field=models.ForeignKey(related_name='+', on_delete=django.db.models.deletion.SET_NULL, blank=True, to='misago_threads.Post', null=True),
+            field=models.ForeignKey(
+                related_name='+',
+                on_delete=django.db.models.deletion.SET_NULL,
+                blank=True,
+                to='misago_threads.Post',
+                null=True
+            ),
             preserve_default=True,
             preserve_default=True,
         ),
         ),
         migrations.AddField(
         migrations.AddField(
@@ -177,19 +223,36 @@ class Migration(migrations.Migration):
         migrations.AddField(
         migrations.AddField(
             model_name='thread',
             model_name='thread',
             name='last_post',
             name='last_post',
-            field=models.ForeignKey(related_name='+', on_delete=django.db.models.deletion.SET_NULL, blank=True, to='misago_threads.Post', null=True),
+            field=models.ForeignKey(
+                related_name='+',
+                on_delete=django.db.models.deletion.SET_NULL,
+                blank=True,
+                to='misago_threads.Post',
+                null=True
+            ),
             preserve_default=True,
             preserve_default=True,
         ),
         ),
         migrations.AddField(
         migrations.AddField(
             model_name='thread',
             model_name='thread',
             name='last_poster',
             name='last_poster',
-            field=models.ForeignKey(related_name='last_poster_set', on_delete=django.db.models.deletion.SET_NULL, blank=True, to=settings.AUTH_USER_MODEL, null=True),
+            field=models.ForeignKey(
+                related_name='last_poster_set',
+                on_delete=django.db.models.deletion.SET_NULL,
+                blank=True,
+                to=settings.AUTH_USER_MODEL,
+                null=True
+            ),
             preserve_default=True,
             preserve_default=True,
         ),
         ),
         migrations.AddField(
         migrations.AddField(
             model_name='thread',
             model_name='thread',
             name='starter',
             name='starter',
-            field=models.ForeignKey(on_delete=django.db.models.deletion.SET_NULL, blank=True, to=settings.AUTH_USER_MODEL, null=True),
+            field=models.ForeignKey(
+                on_delete=django.db.models.deletion.SET_NULL,
+                blank=True,
+                to=settings.AUTH_USER_MODEL,
+                null=True
+            ),
             preserve_default=True,
             preserve_default=True,
         ),
         ),
         migrations.AlterIndexTogether(
         migrations.AlterIndexTogether(
@@ -211,16 +274,19 @@ class Migration(migrations.Migration):
         migrations.CreateModel(
         migrations.CreateModel(
             name='Subscription',
             name='Subscription',
             fields=[
             fields=[
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+                (
+                    'id', models.AutoField(
+                        verbose_name='ID', serialize=False, auto_created=True, primary_key=True
+                    )
+                ),
                 ('last_read_on', models.DateTimeField(default=django.utils.timezone.now)),
                 ('last_read_on', models.DateTimeField(default=django.utils.timezone.now)),
                 ('send_email', models.BooleanField(default=False)),
                 ('send_email', models.BooleanField(default=False)),
                 ('category', models.ForeignKey(to='misago_categories.Category')),
                 ('category', models.ForeignKey(to='misago_categories.Category')),
                 ('thread', models.ForeignKey(to='misago_threads.Thread')),
                 ('thread', models.ForeignKey(to='misago_threads.Thread')),
                 ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
                 ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
             ],
             ],
-            options={
-            },
-            bases=(models.Model,),
+            options={},
+            bases=(models.Model, ),
         ),
         ),
         migrations.AlterIndexTogether(
         migrations.AlterIndexTogether(
             name='subscription',
             name='subscription',
@@ -231,17 +297,43 @@ class Migration(migrations.Migration):
         migrations.CreateModel(
         migrations.CreateModel(
             name='PostEdit',
             name='PostEdit',
             fields=[
             fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                (
+                    'id', models.AutoField(
+                        auto_created=True, primary_key=True, serialize=False, verbose_name='ID'
+                    )
+                ),
                 ('edited_on', models.DateTimeField(default=django.utils.timezone.now)),
                 ('edited_on', models.DateTimeField(default=django.utils.timezone.now)),
                 ('editor_name', models.CharField(max_length=255)),
                 ('editor_name', models.CharField(max_length=255)),
                 ('editor_slug', models.CharField(max_length=255)),
                 ('editor_slug', models.CharField(max_length=255)),
                 ('editor_ip', models.GenericIPAddressField()),
                 ('editor_ip', models.GenericIPAddressField()),
                 ('edited_from', models.TextField()),
                 ('edited_from', models.TextField()),
                 ('edited_to', models.TextField()),
                 ('edited_to', models.TextField()),
-                ('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='misago_categories.Category')),
-                ('editor', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
-                ('post', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='edits_record', to='misago_threads.Post')),
-                ('thread', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='misago_threads.Thread')),
+                (
+                    'category', models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        to='misago_categories.Category'
+                    )
+                ),
+                (
+                    'editor', models.ForeignKey(
+                        blank=True,
+                        null=True,
+                        on_delete=django.db.models.deletion.SET_NULL,
+                        to=settings.AUTH_USER_MODEL
+                    )
+                ),
+                (
+                    'post', models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name='edits_record',
+                        to='misago_threads.Post'
+                    )
+                ),
+                (
+                    'thread', models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE, to='misago_threads.Thread'
+                    )
+                ),
             ],
             ],
             options={
             options={
                 'ordering': ['-id'],
                 'ordering': ['-id'],
@@ -250,47 +342,115 @@ class Migration(migrations.Migration):
         migrations.CreateModel(
         migrations.CreateModel(
             name='Attachment',
             name='Attachment',
             fields=[
             fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                (
+                    'id', models.AutoField(
+                        auto_created=True, primary_key=True, serialize=False, verbose_name='ID'
+                    )
+                ),
                 ('secret', models.CharField(max_length=64)),
                 ('secret', models.CharField(max_length=64)),
-                ('uploaded_on', models.DateTimeField(default=django.utils.timezone.now, db_index=True)),
+                (
+                    'uploaded_on',
+                    models.DateTimeField(default=django.utils.timezone.now, db_index=True)
+                ),
                 ('uploader_name', models.CharField(max_length=255)),
                 ('uploader_name', models.CharField(max_length=255)),
                 ('uploader_slug', models.CharField(max_length=255, db_index=True)),
                 ('uploader_slug', models.CharField(max_length=255, db_index=True)),
                 ('uploader_ip', models.GenericIPAddressField()),
                 ('uploader_ip', models.GenericIPAddressField()),
                 ('filename', models.CharField(max_length=255, db_index=True)),
                 ('filename', models.CharField(max_length=255, db_index=True)),
                 ('size', models.PositiveIntegerField(default=0, db_index=True)),
                 ('size', models.PositiveIntegerField(default=0, db_index=True)),
-                ('thumbnail', models.ImageField(max_length=255, blank=True, null=True, upload_to=misago.threads.models.attachment.upload_to)),
-                ('image', models.ImageField(max_length=255, blank=True, null=True, upload_to=misago.threads.models.attachment.upload_to)),
-                ('file', models.FileField(max_length=255, blank=True, null=True, upload_to=misago.threads.models.attachment.upload_to)),
-                ('post', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='misago_threads.Post')),
+                (
+                    'thumbnail', models.ImageField(
+                        max_length=255,
+                        blank=True,
+                        null=True,
+                        upload_to=misago.threads.models.attachment.upload_to
+                    )
+                ),
+                (
+                    'image', models.ImageField(
+                        max_length=255,
+                        blank=True,
+                        null=True,
+                        upload_to=misago.threads.models.attachment.upload_to
+                    )
+                ),
+                (
+                    'file', models.FileField(
+                        max_length=255,
+                        blank=True,
+                        null=True,
+                        upload_to=misago.threads.models.attachment.upload_to
+                    )
+                ),
+                (
+                    'post', models.ForeignKey(
+                        blank=True,
+                        null=True,
+                        on_delete=django.db.models.deletion.SET_NULL,
+                        to='misago_threads.Post'
+                    )
+                ),
             ],
             ],
         ),
         ),
         migrations.CreateModel(
         migrations.CreateModel(
             name='AttachmentType',
             name='AttachmentType',
             fields=[
             fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                (
+                    'id', models.AutoField(
+                        auto_created=True, primary_key=True, serialize=False, verbose_name='ID'
+                    )
+                ),
                 ('name', models.CharField(max_length=255)),
                 ('name', models.CharField(max_length=255)),
                 ('extensions', models.CharField(max_length=255)),
                 ('extensions', models.CharField(max_length=255)),
                 ('mimetypes', models.CharField(blank=True, max_length=255, null=True)),
                 ('mimetypes', models.CharField(blank=True, max_length=255, null=True)),
                 ('size_limit', models.PositiveIntegerField(default=1024)),
                 ('size_limit', models.PositiveIntegerField(default=1024)),
-                ('status', models.PositiveIntegerField(choices=[(0, 'Allow uploads and downloads'), (1, 'Allow downloads only'), (2, 'Disallow both uploading and downloading')], default=0)),
-                ('limit_downloads_to', models.ManyToManyField(blank=True, related_name='_attachmenttype_limit_downloads_to_+', to='misago_acl.Role')),
-                ('limit_uploads_to', models.ManyToManyField(blank=True, related_name='_attachmenttype_limit_uploads_to_+', to='misago_acl.Role')),
+                (
+                    'status', models.PositiveIntegerField(
+                        choices=[(0, 'Allow uploads and downloads'), (1, 'Allow downloads only'),
+                                 (2, 'Disallow both uploading and downloading')],
+                        default=0
+                    )
+                ),
+                (
+                    'limit_downloads_to', models.ManyToManyField(
+                        blank=True,
+                        related_name='_attachmenttype_limit_downloads_to_+',
+                        to='misago_acl.Role'
+                    )
+                ),
+                (
+                    'limit_uploads_to', models.ManyToManyField(
+                        blank=True,
+                        related_name='_attachmenttype_limit_uploads_to_+',
+                        to='misago_acl.Role'
+                    )
+                ),
             ],
             ],
         ),
         ),
         migrations.AddField(
         migrations.AddField(
             model_name='attachment',
             model_name='attachment',
             name='filetype',
             name='filetype',
-            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='misago_threads.AttachmentType'),
+            field=models.ForeignKey(
+                on_delete=django.db.models.deletion.CASCADE, to='misago_threads.AttachmentType'
+            ),
         ),
         ),
         migrations.AddField(
         migrations.AddField(
             model_name='attachment',
             model_name='attachment',
             name='uploader',
             name='uploader',
-            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
+            field=models.ForeignKey(
+                blank=True,
+                null=True,
+                on_delete=django.db.models.deletion.SET_NULL,
+                to=settings.AUTH_USER_MODEL
+            ),
         ),
         ),
         migrations.CreateModel(
         migrations.CreateModel(
             name='Poll',
             name='Poll',
             fields=[
             fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                (
+                    'id', models.AutoField(
+                        auto_created=True, primary_key=True, serialize=False, verbose_name='ID'
+                    )
+                ),
                 ('poster_name', models.CharField(max_length=255)),
                 ('poster_name', models.CharField(max_length=255)),
                 ('poster_slug', models.CharField(max_length=255)),
                 ('poster_slug', models.CharField(max_length=255)),
                 ('poster_ip', models.GenericIPAddressField()),
                 ('poster_ip', models.GenericIPAddressField()),
@@ -302,24 +462,64 @@ class Migration(migrations.Migration):
                 ('allow_revotes', models.BooleanField(default=False)),
                 ('allow_revotes', models.BooleanField(default=False)),
                 ('votes', models.PositiveIntegerField(default=0)),
                 ('votes', models.PositiveIntegerField(default=0)),
                 ('is_public', models.BooleanField(default=False)),
                 ('is_public', models.BooleanField(default=False)),
-                ('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='misago_categories.Category')),
-                ('poster', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
-                ('thread', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='misago_threads.Thread')),
+                (
+                    'category', models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        to='misago_categories.Category'
+                    )
+                ),
+                (
+                    'poster', models.ForeignKey(
+                        blank=True,
+                        null=True,
+                        on_delete=django.db.models.deletion.SET_NULL,
+                        to=settings.AUTH_USER_MODEL
+                    )
+                ),
+                (
+                    'thread', models.OneToOneField(
+                        on_delete=django.db.models.deletion.CASCADE, to='misago_threads.Thread'
+                    )
+                ),
             ],
             ],
         ),
         ),
         migrations.CreateModel(
         migrations.CreateModel(
             name='PollVote',
             name='PollVote',
             fields=[
             fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                (
+                    'id', models.AutoField(
+                        auto_created=True, primary_key=True, serialize=False, verbose_name='ID'
+                    )
+                ),
                 ('voter_name', models.CharField(max_length=255)),
                 ('voter_name', models.CharField(max_length=255)),
                 ('voter_slug', models.CharField(max_length=255)),
                 ('voter_slug', models.CharField(max_length=255)),
                 ('voter_ip', models.GenericIPAddressField()),
                 ('voter_ip', models.GenericIPAddressField()),
                 ('voted_on', models.DateTimeField(default=django.utils.timezone.now)),
                 ('voted_on', models.DateTimeField(default=django.utils.timezone.now)),
                 ('choice_hash', models.CharField(db_index=True, max_length=12)),
                 ('choice_hash', models.CharField(db_index=True, max_length=12)),
-                ('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='misago_categories.Category')),
-                ('poll', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='misago_threads.Poll')),
-                ('thread', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='misago_threads.Thread')),
-                ('voter', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
+                (
+                    'category', models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        to='misago_categories.Category'
+                    )
+                ),
+                (
+                    'poll', models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE, to='misago_threads.Poll'
+                    )
+                ),
+                (
+                    'thread', models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE, to='misago_threads.Thread'
+                    )
+                ),
+                (
+                    'voter', models.ForeignKey(
+                        blank=True,
+                        null=True,
+                        on_delete=django.db.models.deletion.SET_NULL,
+                        to=settings.AUTH_USER_MODEL
+                    )
+                ),
             ],
             ],
         ),
         ),
         migrations.AlterIndexTogether(
         migrations.AlterIndexTogether(
@@ -331,12 +531,21 @@ class Migration(migrations.Migration):
         migrations.CreateModel(
         migrations.CreateModel(
             name='PostLike',
             name='PostLike',
             fields=[
             fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                (
+                    'id', models.AutoField(
+                        auto_created=True, primary_key=True, serialize=False, verbose_name='ID'
+                    )
+                ),
                 ('liker_name', models.CharField(max_length=255, db_index=True)),
                 ('liker_name', models.CharField(max_length=255, db_index=True)),
                 ('liker_slug', models.CharField(max_length=255)),
                 ('liker_slug', models.CharField(max_length=255)),
                 ('liker_ip', models.GenericIPAddressField()),
                 ('liker_ip', models.GenericIPAddressField()),
                 ('liked_on', models.DateTimeField(default=django.utils.timezone.now)),
                 ('liked_on', models.DateTimeField(default=django.utils.timezone.now)),
-                ('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='misago_categories.Category')),
+                (
+                    'category', models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        to='misago_categories.Category'
+                    )
+                ),
             ],
             ],
             options={
             options={
                 'ordering': ['-id'],
                 'ordering': ['-id'],
@@ -345,21 +554,34 @@ class Migration(migrations.Migration):
         migrations.AddField(
         migrations.AddField(
             model_name='postlike',
             model_name='postlike',
             name='post',
             name='post',
-            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='misago_threads.Post'),
+            field=models.ForeignKey(
+                on_delete=django.db.models.deletion.CASCADE, to='misago_threads.Post'
+            ),
         ),
         ),
         migrations.AddField(
         migrations.AddField(
             model_name='postlike',
             model_name='postlike',
             name='thread',
             name='thread',
-            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='misago_threads.Thread'),
+            field=models.ForeignKey(
+                on_delete=django.db.models.deletion.CASCADE, to='misago_threads.Thread'
+            ),
         ),
         ),
         migrations.AddField(
         migrations.AddField(
             model_name='postlike',
             model_name='postlike',
             name='liker',
             name='liker',
-            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
+            field=models.ForeignKey(
+                blank=True,
+                null=True,
+                on_delete=django.db.models.deletion.SET_NULL,
+                to=settings.AUTH_USER_MODEL
+            ),
         ),
         ),
         migrations.AddField(
         migrations.AddField(
             model_name='post',
             model_name='post',
             name='liked_by',
             name='liked_by',
-            field=models.ManyToManyField(related_name='liked_post_set', through='misago_threads.PostLike', to=settings.AUTH_USER_MODEL),
+            field=models.ManyToManyField(
+                related_name='liked_post_set',
+                through='misago_threads.PostLike',
+                to=settings.AUTH_USER_MODEL
+            ),
         ),
         ),
     ]
     ]

+ 57 - 56
misago/threads/migrations/0002_threads_settings.py

@@ -1,8 +1,7 @@
 # -*- coding: utf-8 -*-
 # -*- coding: utf-8 -*-
 from __future__ import unicode_literals
 from __future__ import unicode_literals
 
 
-from django.conf import settings
-from django.db import migrations, models
+from django.db import migrations
 
 
 from misago.conf.migrationutils import migrate_settings_group
 from misago.conf.migrationutils import migrate_settings_group
 
 
@@ -11,64 +10,66 @@ _ = lambda x: x
 
 
 
 
 def create_threads_settings_group(apps, schema_editor):
 def create_threads_settings_group(apps, schema_editor):
-    migrate_settings_group(apps, {
-        'key': 'threads',
-        'name': _("Threads"),
-        'description': _("Those settings control threads and posts."),
-        'settings': (
-            {
-                'setting': 'thread_title_length_min',
-                'name': _("Minimum length"),
-                'description': _("Minimum allowed thread title length."),
-                'legend': _("Thread titles"),
-                'python_type': 'int',
-                'value': 5,
-                'field_extra': {
-                    'min_value': 2,
-                    'max_value': 255,
+    migrate_settings_group(
+        apps, {
+            'key': 'threads',
+            'name': _("Threads"),
+            'description': _("Those settings control threads and posts."),
+            'settings': [
+                {
+                    'setting': 'thread_title_length_min',
+                    'name': _("Minimum length"),
+                    'description': _("Minimum allowed thread title length."),
+                    'legend': _("Thread titles"),
+                    'python_type': 'int',
+                    'value': 5,
+                    'field_extra': {
+                        'min_value': 2,
+                        'max_value': 255,
+                    },
+                    'is_public': True,
                 },
                 },
-                'is_public': True,
-            },
-            {
-                'setting': 'thread_title_length_max',
-                'name': _("Maximum length"),
-                'description': _("Maximum allowed thread length."),
-                'python_type': 'int',
-                'value': 90,
-                'field_extra': {
-                    'min_value': 2,
-                    'max_value': 255,
+                {
+                    'setting': 'thread_title_length_max',
+                    'name': _("Maximum length"),
+                    'description': _("Maximum allowed thread length."),
+                    'python_type': 'int',
+                    'value': 90,
+                    'field_extra': {
+                        'min_value': 2,
+                        'max_value': 255,
+                    },
+                    'is_public': True,
                 },
                 },
-                'is_public': True,
-            },
-            {
-                'setting': 'post_length_min',
-                'name': _("Minimum length"),
-                'description': _("Minimum allowed user post length."),
-                'legend': _("Posts"),
-                'python_type': 'int',
-                'value': 5,
-                'field_extra': {
-                    'min_value': 1,
+                {
+                    'setting': 'post_length_min',
+                    'name': _("Minimum length"),
+                    'description': _("Minimum allowed user post length."),
+                    'legend': _("Posts"),
+                    'python_type': 'int',
+                    'value': 5,
+                    'field_extra': {
+                        'min_value': 1,
+                    },
+                    'is_public': True,
                 },
                 },
-                'is_public': True,
-            },
-            {
-                'setting': 'post_length_max',
-                'name': _("Maximum length"),
-                'description': _(
-                    "Maximum allowed user post length. Enter zero to disable. "
-                    "Longer posts are more costful to parse and index."
-                ),
-                'python_type': 'int',
-                'value': 60000,
-                'field_extra': {
-                    'min_value': 0,
+                {
+                    'setting': 'post_length_max',
+                    'name': _("Maximum length"),
+                    'description': _(
+                        "Maximum allowed user post length. Enter zero to disable. "
+                        "Longer posts are more costful to parse and index."
+                    ),
+                    'python_type': 'int',
+                    'value': 60000,
+                    'field_extra': {
+                        'min_value': 0,
+                    },
+                    'is_public': True,
                 },
                 },
-                'is_public': True,
-            },
-        )
-    })
+            ],
+        }
+    )
 
 
 
 
 class Migration(migrations.Migration):
 class Migration(migrations.Migration):

+ 27 - 31
misago/threads/migrations/0003_attachment_types.py

@@ -2,89 +2,85 @@
 # Generated by Django 1.9.7 on 2016-10-04 21:41
 # Generated by Django 1.9.7 on 2016-10-04 21:41
 from __future__ import unicode_literals
 from __future__ import unicode_literals
 
 
-from django.conf import settings
-from django.db import migrations, models
+from django.db import migrations
 
 
 
 
-ATTACHMENTS = (
+ATTACHMENTS = [
     {
     {
         'name': 'GIF',
         'name': 'GIF',
-        'extensions': ('gif',),
-        'mimetypes': ('image/gif',),
+        'extensions': ('gif', ),
+        'mimetypes': ('image/gif', ),
         'size_limit': 5 * 1024
         'size_limit': 5 * 1024
     },
     },
     {
     {
         'name': 'JPG',
         'name': 'JPG',
-        'extensions': ('jpg', 'jpeg',),
-        'mimetypes': ('image/jpeg',),
+        'extensions': ('jpg', 'jpeg', ),
+        'mimetypes': ('image/jpeg', ),
         'size_limit': 3 * 1024
         'size_limit': 3 * 1024
     },
     },
     {
     {
         'name': 'PNG',
         'name': 'PNG',
-        'extensions': ('png',),
-        'mimetypes': ('image/png',),
+        'extensions': ('png', ),
+        'mimetypes': ('image/png', ),
         'size_limit': 3 * 1024
         'size_limit': 3 * 1024
     },
     },
     {
     {
         'name': 'PDF',
         'name': 'PDF',
-        'extensions': ('pdf',),
+        'extensions': ('pdf', ),
         'mimetypes': (
         'mimetypes': (
-            'application/pdf',
-            'application/x-pdf',
-            'application/x-bzpdf',
-            'application/x-gzpdf'
+            'application/pdf', 'application/x-pdf', 'application/x-bzpdf', 'application/x-gzpdf',
         ),
         ),
         'size_limit': 4 * 1024
         'size_limit': 4 * 1024
     },
     },
     {
     {
         'name': 'Text',
         'name': 'Text',
-        'extensions': ('txt',),
-        'mimetypes': ('text/plain',),
+        'extensions': ('txt', ),
+        'mimetypes': ('text/plain', ),
         'size_limit': 4 * 1024
         'size_limit': 4 * 1024
     },
     },
     {
     {
         'name': 'Markdown',
         'name': 'Markdown',
-        'extensions': ('md',),
-        'mimetypes': ('text/markdown',),
+        'extensions': ('md', ),
+        'mimetypes': ('text/markdown', ),
         'size_limit': 4 * 1024
         'size_limit': 4 * 1024
     },
     },
     {
     {
         'name': 'reStructuredText',
         'name': 'reStructuredText',
-        'extensions': ('rst',),
-        'mimetypes': ('text/x-rst',),
+        'extensions': ('rst', ),
+        'mimetypes': ('text/x-rst', ),
         'size_limit': 4 * 1024
         'size_limit': 4 * 1024
     },
     },
     {
     {
         'name': '7Z',
         'name': '7Z',
-        'extensions': ('7z',),
-        'mimetypes': ('application/x-7z-compressed',),
+        'extensions': ('7z', ),
+        'mimetypes': ('application/x-7z-compressed', ),
         'size_limit': 4 * 1024
         'size_limit': 4 * 1024
     },
     },
     {
     {
         'name': 'RAR',
         'name': 'RAR',
-        'extensions': ('rar',),
-        'mimetypes': ('application/vnd.rar',),
+        'extensions': ('rar', ),
+        'mimetypes': ('application/vnd.rar', ),
         'size_limit': 4 * 1024
         'size_limit': 4 * 1024
     },
     },
     {
     {
         'name': 'TAR',
         'name': 'TAR',
-        'extensions': ('tar',),
-        'mimetypes': ('application/x-tar',),
+        'extensions': ('tar', ),
+        'mimetypes': ('application/x-tar', ),
         'size_limit': 4 * 1024
         'size_limit': 4 * 1024
     },
     },
     {
     {
         'name': 'GZ',
         'name': 'GZ',
-        'extensions': ('gz',),
-        'mimetypes': ('application/gzip',),
+        'extensions': ('gz', ),
+        'mimetypes': ('application/gzip', ),
         'size_limit': 4 * 1024
         'size_limit': 4 * 1024
     },
     },
     {
     {
         'name': 'ZIP',
         'name': 'ZIP',
-        'extensions': ('zip', 'zipx',),
-        'mimetypes': ('application/zip',),
+        'extensions': ('zip', 'zipx', ),
+        'mimetypes': ('application/zip', ),
         'size_limit': 4 * 1024
         'size_limit': 4 * 1024
     },
     },
-)
+]
 
 
 
 
 def create_attachment_types(apps, schema_editor):
 def create_attachment_types(apps, schema_editor):

+ 56 - 54
misago/threads/migrations/0004_update_settings.py

@@ -10,64 +10,66 @@ _ = lambda x: x
 
 
 
 
 def update_threads_settings(apps, schema_editor):
 def update_threads_settings(apps, schema_editor):
-    migrate_settings_group(apps, {
-        'key': 'threads',
-        'name': _("Threads"),
-        'description': _("Those settings control threads and posts."),
-        'settings': (
-            {
-                'setting': 'thread_title_length_min',
-                'name': _("Minimum length"),
-                'description': _("Minimum allowed thread title length."),
-                'legend': _("Thread titles"),
-                'python_type': 'int',
-                'default_value': 5,
-                'field_extra': {
-                    'min_value': 2,
-                    'max_value': 255,
+    migrate_settings_group(
+        apps, {
+            'key': 'threads',
+            'name': _("Threads"),
+            'description': _("Those settings control threads and posts."),
+            'settings': [
+                {
+                    'setting': 'thread_title_length_min',
+                    'name': _("Minimum length"),
+                    'description': _("Minimum allowed thread title length."),
+                    'legend': _("Thread titles"),
+                    'python_type': 'int',
+                    'default_value': 5,
+                    'field_extra': {
+                        'min_value': 2,
+                        'max_value': 255,
+                    },
+                    'is_public': True,
                 },
                 },
-                'is_public': True,
-            },
-            {
-                'setting': 'thread_title_length_max',
-                'name': _("Maximum length"),
-                'description': _("Maximum allowed thread length."),
-                'python_type': 'int',
-                'default_value': 90,
-                'field_extra': {
-                    'min_value': 2,
-                    'max_value': 255,
+                {
+                    'setting': 'thread_title_length_max',
+                    'name': _("Maximum length"),
+                    'description': _("Maximum allowed thread length."),
+                    'python_type': 'int',
+                    'default_value': 90,
+                    'field_extra': {
+                        'min_value': 2,
+                        'max_value': 255,
+                    },
+                    'is_public': True,
                 },
                 },
-                'is_public': True,
-            },
-            {
-                'setting': 'post_length_min',
-                'name': _("Minimum length"),
-                'description': _("Minimum allowed user post length."),
-                'legend': _("Posts"),
-                'python_type': 'int',
-                'default_value': 5,
-                'field_extra': {
-                    'min_value': 1,
+                {
+                    'setting': 'post_length_min',
+                    'name': _("Minimum length"),
+                    'description': _("Minimum allowed user post length."),
+                    'legend': _("Posts"),
+                    'python_type': 'int',
+                    'default_value': 5,
+                    'field_extra': {
+                        'min_value': 1,
+                    },
+                    'is_public': True,
                 },
                 },
-                'is_public': True,
-            },
-            {
-                'setting': 'post_length_max',
-                'name': _("Maximum length"),
-                'description': _(
-                    "Maximum allowed user post length. Enter zero to disable. "
-                    "Longer posts are more costful to parse and index."
-                ),
-                'python_type': 'int',
-                'default_value': 60000,
-                'field_extra': {
-                    'min_value': 0,
+                {
+                    'setting': 'post_length_max',
+                    'name': _("Maximum length"),
+                    'description': _(
+                        "Maximum allowed user post length. Enter zero to disable. "
+                        "Longer posts are more costful to parse and index."
+                    ),
+                    'python_type': 'int',
+                    'default_value': 60000,
+                    'field_extra': {
+                        'min_value': 0,
+                    },
+                    'is_public': True,
                 },
                 },
-                'is_public': True,
-            },
-        )
-    })
+            ],
+        }
+    )
 
 
     delete_settings_cache()
     delete_settings_cache()
 
 

+ 21 - 41
misago/threads/models/attachment.py

@@ -25,25 +25,16 @@ def upload_to(instance, filename):
         if filename_lowered.endswith(extension):
         if filename_lowered.endswith(extension):
             break
             break
 
 
-    filename_clean = '.'.join((
-        slugify(filename[:(len(extension) + 1) * -1])[:16],
-        extension
-    ))
+    filename_clean = '.'.join((slugify(filename[:(len(extension) + 1) * -1])[:16], extension))
 
 
-    return os.path.join(
-        'attachments', spread_path[:2], spread_path[2:4], secret, filename_clean)
+    return os.path.join('attachments', spread_path[:2], spread_path[2:4], secret, filename_clean)
 
 
 
 
 @python_2_unicode_compatible
 @python_2_unicode_compatible
 class Attachment(models.Model):
 class Attachment(models.Model):
     secret = models.CharField(max_length=64)
     secret = models.CharField(max_length=64)
     filetype = models.ForeignKey('AttachmentType')
     filetype = models.ForeignKey('AttachmentType')
-    post = models.ForeignKey(
-        'Post',
-        blank=True,
-        null=True,
-        on_delete=models.SET_NULL
-    )
+    post = models.ForeignKey('Post', blank=True, null=True, on_delete=models.SET_NULL)
 
 
     uploaded_on = models.DateTimeField(default=timezone.now, db_index=True)
     uploaded_on = models.DateTimeField(default=timezone.now, db_index=True)
 
 
@@ -51,7 +42,7 @@ class Attachment(models.Model):
         settings.AUTH_USER_MODEL,
         settings.AUTH_USER_MODEL,
         blank=True,
         blank=True,
         null=True,
         null=True,
-        on_delete=models.SET_NULL
+        on_delete=models.SET_NULL,
     )
     )
     uploader_name = models.CharField(max_length=255)
     uploader_name = models.CharField(max_length=255)
     uploader_slug = models.CharField(max_length=255, db_index=True)
     uploader_slug = models.CharField(max_length=255, db_index=True)
@@ -60,24 +51,9 @@ class Attachment(models.Model):
     filename = models.CharField(max_length=255, db_index=True)
     filename = models.CharField(max_length=255, db_index=True)
     size = models.PositiveIntegerField(default=0, db_index=True)
     size = models.PositiveIntegerField(default=0, db_index=True)
 
 
-    thumbnail = models.ImageField(
-        max_length=255,
-        blank=True,
-        null=True,
-        upload_to=upload_to
-    )
-    image = models.ImageField(
-        max_length=255,
-        blank=True,
-        null=True,
-        upload_to=upload_to
-    )
-    file = models.FileField(
-        max_length=255,
-        blank=True,
-        null=True,
-        upload_to=upload_to
-    )
+    thumbnail = models.ImageField(max_length=255, blank=True, null=True, upload_to=upload_to)
+    image = models.ImageField(max_length=255, blank=True, null=True, upload_to=upload_to)
+    file = models.FileField(max_length=255, blank=True, null=True, upload_to=upload_to)
 
 
     def __str__(self):
     def __str__(self):
         return self.filename
         return self.filename
@@ -107,17 +83,21 @@ class Attachment(models.Model):
         return not self.is_image
         return not self.is_image
 
 
     def get_absolute_url(self):
     def get_absolute_url(self):
-        return reverse('misago:attachment', kwargs={
-            'pk': self.pk,
-            'secret': self.secret,
-        })
+        return reverse(
+            'misago:attachment', kwargs={
+                'pk': self.pk,
+                'secret': self.secret,
+            }
+        )
 
 
     def get_thumbnail_url(self):
     def get_thumbnail_url(self):
         if self.thumbnail:
         if self.thumbnail:
-            return reverse('misago:attachment-thumbnail', kwargs={
-                'pk': self.pk,
-                'secret': self.secret,
-            })
+            return reverse(
+                'misago:attachment-thumbnail', kwargs={
+                    'pk': self.pk,
+                    'secret': self.secret,
+                }
+            )
         else:
         else:
             return None
             return None
 
 
@@ -131,8 +111,8 @@ class Attachment(models.Model):
 
 
         thumbnail = Image.open(upload)
         thumbnail = Image.open(upload)
         downscale_image = (
         downscale_image = (
-            thumbnail.size[0] > settings.MISAGO_ATTACHMENT_IMAGE_SIZE_LIMIT[0] or
-            thumbnail.size[1] > settings.MISAGO_ATTACHMENT_IMAGE_SIZE_LIMIT[1]
+            thumbnail.size[0] > settings.MISAGO_ATTACHMENT_IMAGE_SIZE_LIMIT[0]
+            or thumbnail.size[1] > settings.MISAGO_ATTACHMENT_IMAGE_SIZE_LIMIT[1]
         )
         )
         strip_animation = fileformat == 'gif'
         strip_animation = fileformat == 'gif'
 
 

+ 2 - 2
misago/threads/models/attachmenttype.py

@@ -17,11 +17,11 @@ class AttachmentType(models.Model):
     size_limit = models.PositiveIntegerField(default=1024)
     size_limit = models.PositiveIntegerField(default=1024)
     status = models.PositiveIntegerField(
     status = models.PositiveIntegerField(
         default=ENABLED,
         default=ENABLED,
-        choices=(
+        choices=[
             (ENABLED, _("Allow uploads and downloads")),
             (ENABLED, _("Allow uploads and downloads")),
             (LOCKED, _("Allow downloads only")),
             (LOCKED, _("Allow downloads only")),
             (DISABLED, _("Disallow both uploading and downloading")),
             (DISABLED, _("Disallow both uploading and downloading")),
-        )
+        ],
     )
     )
 
 
     limit_uploads_to = models.ManyToManyField('misago_acl.Role', related_name='+', blank=True)
     limit_uploads_to = models.ManyToManyField('misago_acl.Role', related_name='+', blank=True)

+ 2 - 5
misago/threads/models/poll.py

@@ -37,10 +37,7 @@ class Poll(models.Model):
             self.category_id = thread.category_id
             self.category_id = thread.category_id
             self.save()
             self.save()
 
 
-            self.pollvote_set.update(
-                thread=self.thread,
-                category_id=self.category_id
-            )
+            self.pollvote_set.update(thread=self.thread, category_id=self.category_id)
 
 
     @property
     @property
     def ends_on(self):
     def ends_on(self):
@@ -96,6 +93,6 @@ class Poll(models.Model):
                 'label': choice['label'],
                 'label': choice['label'],
                 'votes': choice['votes'],
                 'votes': choice['votes'],
                 'selected': choice['selected'],
                 'selected': choice['selected'],
-                'proc': proc
+                'proc': proc,
             })
             })
         return view_choices
         return view_choices

+ 5 - 5
misago/threads/models/post.py

@@ -5,15 +5,12 @@ import copy
 from django.contrib.postgres.fields import JSONField
 from django.contrib.postgres.fields import JSONField
 from django.contrib.postgres.search import SearchVector, SearchVectorField
 from django.contrib.postgres.search import SearchVector, SearchVectorField
 from django.db import models
 from django.db import models
-from django.dispatch import receiver
-from django.urls import reverse
 from django.utils import six, timezone
 from django.utils import six, timezone
 from django.utils.encoding import python_2_unicode_compatible
 from django.utils.encoding import python_2_unicode_compatible
 
 
 from misago.conf import settings
 from misago.conf import settings
 from misago.core.utils import parse_iso8601_string
 from misago.core.utils import parse_iso8601_string
 from misago.markup import finalise_markup
 from misago.markup import finalise_markup
-from misago.threads import threadtypes
 from misago.threads.checksums import is_post_valid, update_post_checksum
 from misago.threads.checksums import is_post_valid, update_post_checksum
 
 
 
 
@@ -85,9 +82,9 @@ class Post(models.Model):
 
 
     class Meta:
     class Meta:
         index_together = [
         index_together = [
-            ('thread', 'id'), # speed up threadview for team members
+            ('thread', 'id'),  # speed up threadview for team members
             ('is_event', 'is_hidden'),
             ('is_event', 'is_hidden'),
-            ('poster', 'posted_on')
+            ('poster', 'posted_on'),
         ]
         ]
 
 
     def __str__(self):
     def __str__(self):
@@ -100,6 +97,9 @@ class Post(models.Model):
         super(Post, self).delete(*args, **kwargs)
         super(Post, self).delete(*args, **kwargs)
 
 
     def merge(self, other_post):
     def merge(self, other_post):
+        if not self.poster_id or self.poster_id != other_post.poster_id:
+            raise ValueError("post can't be merged with other user's post")
+
         if self.thread_id != other_post.thread_id:
         if self.thread_id != other_post.thread_id:
             raise ValueError("only posts belonging to same thread can be merged")
             raise ValueError("only posts belonging to same thread can be merged")
 
 

+ 1 - 3
misago/threads/models/subscription.py

@@ -13,6 +13,4 @@ class Subscription(models.Model):
     send_email = models.BooleanField(default=False)
     send_email = models.BooleanField(default=False)
 
 
     class Meta:
     class Meta:
-        index_together = [
-            ['send_email', 'last_read_on']
-        ]
+        index_together = [['send_email', 'last_read_on']]

+ 10 - 13
misago/threads/models/thread.py

@@ -1,5 +1,5 @@
 from django.core.exceptions import ObjectDoesNotExist
 from django.core.exceptions import ObjectDoesNotExist
-from django.db import models, transaction
+from django.db import models
 from django.utils.encoding import python_2_unicode_compatible
 from django.utils.encoding import python_2_unicode_compatible
 from django.utils.translation import ugettext_lazy as _
 from django.utils.translation import ugettext_lazy as _
 
 
@@ -13,11 +13,11 @@ class Thread(models.Model):
     WEIGHT_PINNED = 1
     WEIGHT_PINNED = 1
     WEIGHT_GLOBAL = 2
     WEIGHT_GLOBAL = 2
 
 
-    WEIGHT_CHOICES = (
+    WEIGHT_CHOICES = [
         (WEIGHT_DEFAULT, _("Don't pin thread")),
         (WEIGHT_DEFAULT, _("Don't pin thread")),
         (WEIGHT_PINNED, _("Pin thread within category")),
         (WEIGHT_PINNED, _("Pin thread within category")),
-        (WEIGHT_GLOBAL, _("Pin thread globally"))
-    )
+        (WEIGHT_GLOBAL, _("Pin thread globally")),
+    ]
 
 
     category = models.ForeignKey('misago_categories.Category')
     category = models.ForeignKey('misago_categories.Category')
     title = models.CharField(max_length=255)
     title = models.CharField(max_length=255)
@@ -39,13 +39,13 @@ class Thread(models.Model):
         related_name='+',
         related_name='+',
         null=True,
         null=True,
         blank=True,
         blank=True,
-        on_delete=models.SET_NULL
+        on_delete=models.SET_NULL,
     )
     )
     starter = models.ForeignKey(
     starter = models.ForeignKey(
         settings.AUTH_USER_MODEL,
         settings.AUTH_USER_MODEL,
         null=True,
         null=True,
         blank=True,
         blank=True,
-        on_delete=models.SET_NULL
+        on_delete=models.SET_NULL,
     )
     )
     starter_name = models.CharField(max_length=255)
     starter_name = models.CharField(max_length=255)
     starter_slug = models.CharField(max_length=255)
     starter_slug = models.CharField(max_length=255)
@@ -55,7 +55,7 @@ class Thread(models.Model):
         related_name='+',
         related_name='+',
         null=True,
         null=True,
         blank=True,
         blank=True,
-        on_delete=models.SET_NULL
+        on_delete=models.SET_NULL,
     )
     )
     last_post_is_event = models.BooleanField(default=False)
     last_post_is_event = models.BooleanField(default=False)
     last_poster = models.ForeignKey(
     last_poster = models.ForeignKey(
@@ -63,7 +63,7 @@ class Thread(models.Model):
         related_name='last_poster_set',
         related_name='last_poster_set',
         null=True,
         null=True,
         blank=True,
         blank=True,
-        on_delete=models.SET_NULL
+        on_delete=models.SET_NULL,
     )
     )
     last_poster_name = models.CharField(max_length=255, null=True, blank=True)
     last_poster_name = models.CharField(max_length=255, null=True, blank=True)
     last_poster_slug = models.CharField(max_length=255, null=True, blank=True)
     last_poster_slug = models.CharField(max_length=255, null=True, blank=True)
@@ -78,7 +78,7 @@ class Thread(models.Model):
         settings.AUTH_USER_MODEL,
         settings.AUTH_USER_MODEL,
         related_name='privatethread_set',
         related_name='privatethread_set',
         through='ThreadParticipant',
         through='ThreadParticipant',
-        through_fields=('thread', 'user')
+        through_fields=('thread', 'user'),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -119,10 +119,7 @@ class Thread(models.Model):
         except ObjectDoesNotExist:
         except ObjectDoesNotExist:
             self.has_poll = False
             self.has_poll = False
 
 
-        self.replies = self.post_set.filter(
-            is_event=False,
-            is_unapproved=False
-        ).count()
+        self.replies = self.post_set.filter(is_event=False, is_unapproved=False).count()
 
 
         if self.replies > 0:
         if self.replies > 0:
             self.replies -= 1
             self.replies -= 1

+ 4 - 18
misago/threads/models/threadparticipant.py

@@ -5,35 +5,21 @@ from misago.conf import settings
 
 
 class ThreadParticipantManager(models.Manager):
 class ThreadParticipantManager(models.Manager):
     def set_owner(self, thread, user):
     def set_owner(self, thread, user):
-        ThreadParticipant.objects.filter(
-            thread=thread,
-            is_owner=True
-        ).update(is_owner=False)
+        ThreadParticipant.objects.filter(thread=thread, is_owner=True).update(is_owner=False)
 
 
         self.remove_participant(thread, user)
         self.remove_participant(thread, user)
 
 
-        ThreadParticipant.objects.create(
-            thread=thread,
-            user=user,
-            is_owner=True
-        )
+        ThreadParticipant.objects.create(thread=thread, user=user, is_owner=True)
 
 
     def add_participants(self, thread, users):
     def add_participants(self, thread, users):
         bulk = []
         bulk = []
         for user in users:
         for user in users:
-            bulk.append(ThreadParticipant(
-                thread=thread,
-                user=user,
-                is_owner=False
-            ))
+            bulk.append(ThreadParticipant(thread=thread, user=user, is_owner=False))
 
 
         ThreadParticipant.objects.bulk_create(bulk)
         ThreadParticipant.objects.bulk_create(bulk)
 
 
     def remove_participant(self, thread, user):
     def remove_participant(self, thread, user):
-        ThreadParticipant.objects.filter(
-            thread=thread,
-            user=user
-        ).delete()
+        ThreadParticipant.objects.filter(thread=thread, user=user).delete()
 
 
 
 
 class ThreadParticipant(models.Model):
 class ThreadParticipant(models.Model):

+ 9 - 7
misago/threads/moderation/posts.py

@@ -64,13 +64,15 @@ def hide_post(user, post):
         post.hidden_by_name = user.username
         post.hidden_by_name = user.username
         post.hidden_by_slug = user.slug
         post.hidden_by_slug = user.slug
         post.hidden_on = timezone.now()
         post.hidden_on = timezone.now()
-        post.save(update_fields=[
-            'is_hidden',
-            'hidden_by',
-            'hidden_by_name',
-            'hidden_by_slug',
-            'hidden_on',
-        ])
+        post.save(
+            update_fields=[
+                'is_hidden',
+                'hidden_by',
+                'hidden_by_name',
+                'hidden_by_slug',
+                'hidden_on',
+            ]
+        )
         return True
         return True
     else:
     else:
         return False
         return False

+ 17 - 14
misago/threads/moderation/threads.py

@@ -1,6 +1,5 @@
 from django.db.transaction import atomic
 from django.db.transaction import atomic
 from django.utils import timezone
 from django.utils import timezone
-from django.utils.translation import ugettext as _
 
 
 from misago.threads.events import record_event
 from misago.threads.events import record_event
 
 
@@ -35,7 +34,7 @@ def change_thread_title(request, thread, new_title):
         thread.first_post.save(update_fields=['search_vector'])
         thread.first_post.save(update_fields=['search_vector'])
 
 
         record_event(request, thread, 'changed_title', {
         record_event(request, thread, 'changed_title', {
-            'old_title': old_title
+            'old_title': old_title,
         })
         })
         return True
         return True
     else:
     else:
@@ -78,12 +77,14 @@ def move_thread(request, thread, new_category):
         from_category = thread.category
         from_category = thread.category
         thread.move(new_category)
         thread.move(new_category)
 
 
-        record_event(request, thread, 'moved', {
-            'from_category': {
-                'name': from_category.name,
-                'url': from_category.get_absolute_url(),
+        record_event(
+            request, thread, 'moved', {
+                'from_category': {
+                    'name': from_category.name,
+                    'url': from_category.get_absolute_url(),
+                },
             }
             }
-        })
+        )
         return True
         return True
     else:
     else:
         return False
         return False
@@ -154,13 +155,15 @@ def hide_thread(request, thread):
         thread.first_post.hidden_by_name = request.user.username
         thread.first_post.hidden_by_name = request.user.username
         thread.first_post.hidden_by_slug = request.user.slug
         thread.first_post.hidden_by_slug = request.user.slug
         thread.first_post.hidden_on = timezone.now()
         thread.first_post.hidden_on = timezone.now()
-        thread.first_post.save(update_fields=[
-            'is_hidden',
-            'hidden_by',
-            'hidden_by_name',
-            'hidden_by_slug',
-            'hidden_on',
-        ])
+        thread.first_post.save(
+            update_fields=[
+                'is_hidden',
+                'hidden_by',
+                'hidden_by_name',
+                'hidden_by_slug',
+                'hidden_on',
+            ]
+        )
         thread.is_hidden = True
         thread.is_hidden = True
 
 
         record_event(request, thread, 'hid')
         record_event(request, thread, 'hid')

+ 6 - 14
misago/threads/paginator.py

@@ -1,26 +1,18 @@
-from math import ceil, floor
-
 from django.core.paginator import Paginator
 from django.core.paginator import Paginator
-from django.utils.functional import cached_property
 
 
 
 
 class PostsPaginator(Paginator):
 class PostsPaginator(Paginator):
-    """
-    Paginator that returns that makes last item on page
-    repeat as first item on next page.
-    """
-    def __init__(self, object_list, per_page, orphans=0,
-                 allow_empty_first_page=True):
+    """paginator that returns that makes last item on page repeat as first item on next page."""
+
+    def __init__(self, object_list, per_page, orphans=0, allow_empty_first_page=True):
         per_page = int(per_page) - 1
         per_page = int(per_page) - 1
         if orphans:
         if orphans:
             orphans += 1
             orphans += 1
-        super(PostsPaginator, self).__init__(
-            object_list, per_page, orphans, allow_empty_first_page)
+        super(PostsPaginator,
+              self).__init__(object_list, per_page, orphans, allow_empty_first_page)
 
 
     def page(self, number):
     def page(self, number):
-        """
-        Returns a Page object for the given 1-based page number.
-        """
+        """returns a Page object for the given 1-based page number."""
         number = self.validate_number(number)
         number = self.validate_number(number)
         bottom = (number - 1) * self.per_page
         bottom = (number - 1) * self.per_page
         top = bottom + self.per_page
         top = bottom + self.per_page

+ 45 - 48
misago/threads/participants.py

@@ -1,7 +1,6 @@
 from django.contrib.auth import get_user_model
 from django.contrib.auth import get_user_model
 from django.utils.translation import ugettext as _
 from django.utils.translation import ugettext as _
 
 
-from misago.core import deprecations
 from misago.core.mail import build_mail, send_messages
 from misago.core.mail import build_mail, send_messages
 
 
 from .events import record_event
 from .events import record_event
@@ -30,7 +29,7 @@ def make_threads_participants_aware(user, threads):
 
 
     participants_qs = ThreadParticipant.objects.filter(
     participants_qs = ThreadParticipant.objects.filter(
         user=user,
         user=user,
-        thread_id__in=threads_dict.keys()
+        thread_id__in=threads_dict.keys(),
     )
     )
 
 
     for participant in participants_qs:
     for participant in participants_qs:
@@ -52,8 +51,7 @@ def make_thread_participants_aware(user, thread):
     return thread.participants_list
     return thread.participants_list
 
 
 
 
-def set_users_unread_private_threads_sync(
-        users=None, participants=None, exclude_user=None):
+def set_users_unread_private_threads_sync(users=None, participants=None, exclude_user=None):
     users_ids = []
     users_ids = []
     if users:
     if users:
         users_ids += [u.pk for u in users]
         users_ids += [u.pk for u in users]
@@ -65,60 +63,60 @@ def set_users_unread_private_threads_sync(
     if not users_ids:
     if not users_ids:
         return
         return
 
 
-    UserModel.objects.filter(id__in=set(users_ids)).update(
-        sync_unread_private_threads=True
-    )
+    UserModel.objects.filter(id__in=set(users_ids)).update(sync_unread_private_threads=True)
 
 
 
 
 def set_owner(thread, user):
 def set_owner(thread, user):
-    """
-    Set user as thread's owner
-    """
     ThreadParticipant.objects.set_owner(thread, user)
     ThreadParticipant.objects.set_owner(thread, user)
 
 
 
 
 def change_owner(request, thread, user):
 def change_owner(request, thread, user):
-    """
-    Replace thread's owner with other
-    """
     ThreadParticipant.objects.set_owner(thread, user)
     ThreadParticipant.objects.set_owner(thread, user)
     set_users_unread_private_threads_sync(
     set_users_unread_private_threads_sync(
         participants=thread.participants_list,
         participants=thread.participants_list,
-        exclude_user=request.user
+        exclude_user=request.user,
     )
     )
 
 
     if thread.participant and thread.participant.is_owner:
     if thread.participant and thread.participant.is_owner:
-        record_event(request, thread, 'changed_owner', {
-            'user': {
-                'username': user.username,
-                'url': user.get_absolute_url(),
-            }
-        })
+        record_event(
+            request,
+            thread,
+            'changed_owner',
+            {
+                'user': {
+                    'username': user.username,
+                    'url': user.get_absolute_url(),
+                },
+            },
+        )
     else:
     else:
         record_event(request, thread, 'tookover')
         record_event(request, thread, 'tookover')
 
 
 
 
 def add_participant(request, thread, user):
 def add_participant(request, thread, user):
-    """
-    Adds single participant to thread, registers this on the event
-    """
+    """adds single participant to thread, registers this on the event"""
     add_participants(request, thread, [user])
     add_participants(request, thread, [user])
 
 
     if request.user == user:
     if request.user == user:
         record_event(request, thread, 'entered_thread')
         record_event(request, thread, 'entered_thread')
     else:
     else:
-        record_event(request, thread, 'added_participant', {
-            'user': {
-                'username': user.username,
-                'url': user.get_absolute_url(),
-            }
-        })
+        record_event(
+            request,
+            thread,
+            'added_participant',
+            {
+                'user': {
+                    'username': user.username,
+                    'url': user.get_absolute_url(),
+                },
+            },
+        )
 
 
 
 
 def add_participants(request, thread, users):
 def add_participants(request, thread, users):
     """
     """
     Add multiple participants to thread, set "recound private threads" flag on them
     Add multiple participants to thread, set "recound private threads" flag on them
-    notify them about being added to thread
+    notify them about being added to thread.
     """
     """
     ThreadParticipant.objects.add_participants(thread, users)
     ThreadParticipant.objects.add_participants(thread, users)
 
 
@@ -130,7 +128,7 @@ def add_participants(request, thread, users):
     set_users_unread_private_threads_sync(
     set_users_unread_private_threads_sync(
         users=users,
         users=users,
         participants=thread_participants,
         participants=thread_participants,
-        exclude_user=request.user
+        exclude_user=request.user,
     )
     )
 
 
     emails = []
     emails = []
@@ -145,24 +143,18 @@ def build_noticiation_email(request, thread, user):
     subject = _('%(user)s has invited you to participate in private thread "%(thread)s"')
     subject = _('%(user)s has invited you to participate in private thread "%(thread)s"')
     subject_formats = {
     subject_formats = {
         'thread': thread.title,
         'thread': thread.title,
-        'user': request.user.username
+        'user': request.user.username,
     }
     }
 
 
     return build_mail(
     return build_mail(
-        request,
-        user,
-        subject % subject_formats,
-        'misago/emails/privatethread/added',
-        {
-            'thread': thread
+        request, user, subject % subject_formats, 'misago/emails/privatethread/added', {
+            'thread': thread,
         }
         }
     )
     )
 
 
 
 
 def remove_participant(request, thread, user):
 def remove_participant(request, thread, user):
-    """
-    Remove thread participant, set "recound private threads" flag on user
-    """
+    """remove thread participant, set "recound private threads" flag on user"""
     removed_owner = False
     removed_owner = False
     remaining_participants = []
     remaining_participants = []
 
 
@@ -181,7 +173,7 @@ def remove_participant(request, thread, user):
         thread.subscription_set.filter(user=user).delete()
         thread.subscription_set.filter(user=user).delete()
 
 
         if removed_owner:
         if removed_owner:
-            thread.is_closed = True # flag thread to close
+            thread.is_closed = True  # flag thread to close
 
 
             if request.user == user:
             if request.user == user:
                 event_type = 'owner_left'
                 event_type = 'owner_left'
@@ -193,9 +185,14 @@ def remove_participant(request, thread, user):
             else:
             else:
                 event_type = 'removed_participant'
                 event_type = 'removed_participant'
 
 
-        record_event(request, thread, event_type, {
-            'user': {
-                'username': user.username,
-                'url': user.get_absolute_url(),
-            }
-        })
+        record_event(
+            request,
+            thread,
+            event_type,
+            {
+                'user': {
+                    'username': user.username,
+                    'url': user.get_absolute_url(),
+                },
+            },
+        )

+ 9 - 12
misago/threads/permissions/attachments.py

@@ -7,9 +7,7 @@ from misago.core.forms import YesNoSwitch
 from misago.threads.models import Attachment
 from misago.threads.models import Attachment
 
 
 
 
-"""
-Admin Permissions Form
-"""
+# Admin Permissions Forms
 class PermissionsForm(forms.Form):
 class PermissionsForm(forms.Form):
     legend = _("Attachments")
     legend = _("Attachments")
 
 
@@ -20,7 +18,9 @@ class PermissionsForm(forms.Form):
         min_value=0
         min_value=0
     )
     )
 
 
-    can_download_other_users_attachments = YesNoSwitch(label=_("Can download other users attachments"))
+    can_download_other_users_attachments = YesNoSwitch(
+        label=_("Can download other users attachments")
+    )
     can_delete_other_users_attachments = YesNoSwitch(label=_("Can delete other users attachments"))
     can_delete_other_users_attachments = YesNoSwitch(label=_("Can delete other users attachments"))
 
 
 
 
@@ -40,9 +40,6 @@ def change_permissions_form(role):
         return None
         return None
 
 
 
 
-"""
-ACL Builder
-"""
 def build_acl(acl, roles, key_name):
 def build_acl(acl, roles, key_name):
     new_acl = {
     new_acl = {
         'max_attachment_size': 0,
         'max_attachment_size': 0,
@@ -51,16 +48,16 @@ def build_acl(acl, roles, key_name):
     }
     }
     new_acl.update(acl)
     new_acl.update(acl)
 
 
-    return algebra.sum_acls(new_acl, roles=roles, key=key_name,
+    return algebra.sum_acls(
+        new_acl,
+        roles=roles,
+        key=key_name,
         max_attachment_size=algebra.greater,
         max_attachment_size=algebra.greater,
         can_download_other_users_attachments=algebra.greater,
         can_download_other_users_attachments=algebra.greater,
-        can_delete_other_users_attachments=algebra.greater
+        can_delete_other_users_attachments=algebra.greater,
     )
     )
 
 
 
 
-"""
-ACL's for targets
-"""
 def add_acl_to_attachment(user, attachment):
 def add_acl_to_attachment(user, attachment):
     if user.is_authenticated and user.id == attachment.uploader_id:
     if user.is_authenticated and user.id == attachment.uploader_id:
         attachment.acl.update({
         attachment.acl.update({

+ 51 - 40
misago/threads/permissions/polls.py

@@ -25,9 +25,6 @@ __all__ = [
 ]
 ]
 
 
 
 
-"""
-Admin Permissions Forms
-"""
 class RolePermissionsForm(forms.Form):
 class RolePermissionsForm(forms.Form):
     legend = _("Polls")
     legend = _("Polls")
 
 
@@ -35,41 +32,41 @@ class RolePermissionsForm(forms.Form):
         label=_("Can start polls"),
         label=_("Can start polls"),
         coerce=int,
         coerce=int,
         initial=0,
         initial=0,
-        choices=(
+        choices=[
             (0, _("No")),
             (0, _("No")),
             (1, _("Own threads")),
             (1, _("Own threads")),
-            (2, _("All threads"))
-        )
+            (2, _("All threads")),
+        ],
     )
     )
     can_edit_polls = forms.TypedChoiceField(
     can_edit_polls = forms.TypedChoiceField(
         label=_("Can edit polls"),
         label=_("Can edit polls"),
         coerce=int,
         coerce=int,
         initial=0,
         initial=0,
-        choices=(
+        choices=[
             (0, _("No")),
             (0, _("No")),
             (1, _("Own polls")),
             (1, _("Own polls")),
-            (2, _("All polls"))
-        )
+            (2, _("All polls")),
+        ],
     )
     )
     can_delete_polls = forms.TypedChoiceField(
     can_delete_polls = forms.TypedChoiceField(
         label=_("Can delete polls"),
         label=_("Can delete polls"),
         coerce=int,
         coerce=int,
         initial=0,
         initial=0,
-        choices=(
+        choices=[
             (0, _("No")),
             (0, _("No")),
             (1, _("Own polls")),
             (1, _("Own polls")),
-            (2, _("All polls"))
-        )
+            (2, _("All polls")),
+        ],
     )
     )
     poll_edit_time = forms.IntegerField(
     poll_edit_time = forms.IntegerField(
         label=_("Time limit for own polls edits, in minutes"),
         label=_("Time limit for own polls edits, in minutes"),
         help_text=_("Enter 0 to don't limit time for editing own polls."),
         help_text=_("Enter 0 to don't limit time for editing own polls."),
         initial=0,
         initial=0,
-        min_value=0
+        min_value=0,
     )
     )
     can_always_see_poll_voters = YesNoSwitch(
     can_always_see_poll_voters = YesNoSwitch(
         label=_("Can always see polls voters"),
         label=_("Can always see polls voters"),
-        help_text=_("Allows users to see who voted in poll even if poll votes are secret.")
+        help_text=_("Allows users to see who voted in poll even if poll votes are secret."),
     )
     )
 
 
 
 
@@ -80,19 +77,19 @@ def change_permissions_form(role):
         return None
         return None
 
 
 
 
-"""
-ACL Builder
-"""
 def build_acl(acl, roles, key_name):
 def build_acl(acl, roles, key_name):
     acl.update({
     acl.update({
         'can_start_polls': 0,
         'can_start_polls': 0,
         'can_edit_polls': 0,
         'can_edit_polls': 0,
         'can_delete_polls': 0,
         'can_delete_polls': 0,
         'poll_edit_time': 0,
         'poll_edit_time': 0,
-        'can_always_see_poll_voters': 0
+        'can_always_see_poll_voters': 0,
     })
     })
 
 
-    return algebra.sum_acls(acl, roles=roles, key=key_name,
+    return algebra.sum_acls(
+        acl,
+        roles=roles,
+        key=key_name,
         can_start_polls=algebra.greater,
         can_start_polls=algebra.greater,
         can_edit_polls=algebra.greater,
         can_edit_polls=algebra.greater,
         can_delete_polls=algebra.greater,
         can_delete_polls=algebra.greater,
@@ -101,9 +98,6 @@ def build_acl(acl, roles, key_name):
     )
     )
 
 
 
 
-"""
-ACL's for targets
-"""
 def add_acl_to_poll(user, poll):
 def add_acl_to_poll(user, poll):
     poll.acl.update({
     poll.acl.update({
         'can_vote': can_vote_poll(user, poll),
         'can_vote': can_vote_poll(user, poll),
@@ -115,7 +109,7 @@ def add_acl_to_poll(user, poll):
 
 
 def add_acl_to_thread(user, thread):
 def add_acl_to_thread(user, thread):
     thread.acl.update({
     thread.acl.update({
-        'can_start_poll': can_start_poll(user, thread)
+        'can_start_poll': can_start_poll(user, thread),
     })
     })
 
 
 
 
@@ -124,16 +118,15 @@ def register_with(registry):
     registry.acl_annotator(Thread, add_acl_to_thread)
     registry.acl_annotator(Thread, add_acl_to_thread)
 
 
 
 
-"""
-ACL tests
-"""
 def allow_start_poll(user, target):
 def allow_start_poll(user, target):
     if user.is_anonymous:
     if user.is_anonymous:
         raise PermissionDenied(_("You have to sign in to start polls."))
         raise PermissionDenied(_("You have to sign in to start polls."))
 
 
-    category_acl = user.acl_cache['categories'].get(target.category_id, {
-        'can_close_threads': False,
-    })
+    category_acl = user.acl_cache['categories'].get(
+        target.category_id, {
+            'can_close_threads': False,
+        }
+    )
 
 
     if not user.acl_cache.get('can_start_polls'):
     if not user.acl_cache.get('can_start_polls'):
         raise PermissionDenied(_("You can't start polls."))
         raise PermissionDenied(_("You can't start polls."))
@@ -145,6 +138,8 @@ def allow_start_poll(user, target):
             raise PermissionDenied(_("This category is closed. You can't start polls in it."))
             raise PermissionDenied(_("This category is closed. You can't start polls in it."))
         if target.is_closed:
         if target.is_closed:
             raise PermissionDenied(_("This thread is closed. You can't start polls in it."))
             raise PermissionDenied(_("This thread is closed. You can't start polls in it."))
+
+
 can_start_poll = return_boolean(allow_start_poll)
 can_start_poll = return_boolean(allow_start_poll)
 
 
 
 
@@ -152,9 +147,11 @@ def allow_edit_poll(user, target):
     if user.is_anonymous:
     if user.is_anonymous:
         raise PermissionDenied(_("You have to sign in to edit polls."))
         raise PermissionDenied(_("You have to sign in to edit polls."))
 
 
-    category_acl = user.acl_cache['categories'].get(target.category_id, {
-        'can_close_threads': False,
-    })
+    category_acl = user.acl_cache['categories'].get(
+        target.category_id, {
+            'can_close_threads': False,
+        }
+    )
 
 
     if not user.acl_cache.get('can_edit_polls'):
     if not user.acl_cache.get('can_edit_polls'):
         raise PermissionDenied(_("You can't edit polls."))
         raise PermissionDenied(_("You can't edit polls."))
@@ -166,7 +163,8 @@ def allow_edit_poll(user, target):
             message = ungettext(
             message = ungettext(
                 "You can't edit polls that are older than %(minutes)s minute.",
                 "You can't edit polls that are older than %(minutes)s minute.",
                 "You can't edit polls that are older than %(minutes)s minutes.",
                 "You can't edit polls that are older than %(minutes)s minutes.",
-                user.acl_cache['poll_edit_time'])
+                user.acl_cache['poll_edit_time']
+            )
             raise PermissionDenied(message % {'minutes': user.acl_cache['poll_edit_time']})
             raise PermissionDenied(message % {'minutes': user.acl_cache['poll_edit_time']})
 
 
         if target.is_over:
         if target.is_over:
@@ -177,6 +175,8 @@ def allow_edit_poll(user, target):
             raise PermissionDenied(_("This category is closed. You can't edit polls in it."))
             raise PermissionDenied(_("This category is closed. You can't edit polls in it."))
         if target.thread.is_closed:
         if target.thread.is_closed:
             raise PermissionDenied(_("This thread is closed. You can't edit polls in it."))
             raise PermissionDenied(_("This thread is closed. You can't edit polls in it."))
+
+
 can_edit_poll = return_boolean(allow_edit_poll)
 can_edit_poll = return_boolean(allow_edit_poll)
 
 
 
 
@@ -184,9 +184,11 @@ def allow_delete_poll(user, target):
     if user.is_anonymous:
     if user.is_anonymous:
         raise PermissionDenied(_("You have to sign in to delete polls."))
         raise PermissionDenied(_("You have to sign in to delete polls."))
 
 
-    category_acl = user.acl_cache['categories'].get(target.category_id, {
-        'can_close_threads': False,
-    })
+    category_acl = user.acl_cache['categories'].get(
+        target.category_id, {
+            'can_close_threads': False,
+        }
+    )
 
 
     if not user.acl_cache.get('can_delete_polls'):
     if not user.acl_cache.get('can_delete_polls'):
         raise PermissionDenied(_("You can't delete polls."))
         raise PermissionDenied(_("You can't delete polls."))
@@ -198,7 +200,8 @@ def allow_delete_poll(user, target):
             message = ungettext(
             message = ungettext(
                 "You can't delete polls that are older than %(minutes)s minute.",
                 "You can't delete polls that are older than %(minutes)s minute.",
                 "You can't delete polls that are older than %(minutes)s minutes.",
                 "You can't delete polls that are older than %(minutes)s minutes.",
-                user.acl_cache['poll_edit_time'])
+                user.acl_cache['poll_edit_time']
+            )
             raise PermissionDenied(message % {'minutes': user.acl_cache['poll_edit_time']})
             raise PermissionDenied(message % {'minutes': user.acl_cache['poll_edit_time']})
         if target.is_over:
         if target.is_over:
             raise PermissionDenied(_("This poll is over. You can't delete it."))
             raise PermissionDenied(_("This poll is over. You can't delete it."))
@@ -208,6 +211,8 @@ def allow_delete_poll(user, target):
             raise PermissionDenied(_("This category is closed. You can't delete polls in it."))
             raise PermissionDenied(_("This category is closed. You can't delete polls in it."))
         if target.thread.is_closed:
         if target.thread.is_closed:
             raise PermissionDenied(_("This thread is closed. You can't delete polls in it."))
             raise PermissionDenied(_("This thread is closed. You can't delete polls in it."))
+
+
 can_delete_poll = return_boolean(allow_delete_poll)
 can_delete_poll = return_boolean(allow_delete_poll)
 
 
 
 
@@ -220,21 +225,27 @@ def allow_vote_poll(user, target):
     if target.is_over:
     if target.is_over:
         raise PermissionDenied(_("This poll is over. You can't vote in it."))
         raise PermissionDenied(_("This poll is over. You can't vote in it."))
 
 
-    category_acl = user.acl_cache['categories'].get(target.category_id, {
-        'can_close_threads': False,
-    })
+    category_acl = user.acl_cache['categories'].get(
+        target.category_id, {
+            'can_close_threads': False,
+        }
+    )
 
 
     if not category_acl.get('can_close_threads'):
     if not category_acl.get('can_close_threads'):
         if target.category.is_closed:
         if target.category.is_closed:
             raise PermissionDenied(_("This category is closed. You can't vote in it."))
             raise PermissionDenied(_("This category is closed. You can't vote in it."))
         if target.thread.is_closed:
         if target.thread.is_closed:
             raise PermissionDenied(_("This thread is closed. You can't vote in it."))
             raise PermissionDenied(_("This thread is closed. You can't vote in it."))
+
+
 can_vote_poll = return_boolean(allow_vote_poll)
 can_vote_poll = return_boolean(allow_vote_poll)
 
 
 
 
 def allow_see_poll_votes(user, target):
 def allow_see_poll_votes(user, target):
     if not target.is_public and not user.acl_cache['can_always_see_poll_voters']:
     if not target.is_public and not user.acl_cache['can_always_see_poll_voters']:
         raise PermissionDenied(_("You dont have permission to this poll's voters."))
         raise PermissionDenied(_("You dont have permission to this poll's voters."))
+
+
 can_see_poll_votes = return_boolean(allow_see_poll_votes)
 can_see_poll_votes = return_boolean(allow_see_poll_votes)
 
 
 
 

+ 41 - 36
misago/threads/permissions/privatethreads.py

@@ -1,11 +1,9 @@
 from django import forms
 from django import forms
-from django.contrib.auth import get_user_model
 from django.core.exceptions import PermissionDenied
 from django.core.exceptions import PermissionDenied
-from django.db.models import Q
 from django.http import Http404
 from django.http import Http404
 from django.utils.translation import ugettext_lazy as _
 from django.utils.translation import ugettext_lazy as _
 
 
-from misago.acl import add_acl, algebra
+from misago.acl import algebra
 from misago.acl.decorators import return_boolean
 from misago.acl.decorators import return_boolean
 from misago.acl.models import Role
 from misago.acl.models import Role
 from misago.categories import PRIVATE_THREADS_ROOT_NAME
 from misago.categories import PRIVATE_THREADS_ROOT_NAME
@@ -32,9 +30,6 @@ __all__ = [
 ]
 ]
 
 
 
 
-"""
-Admin Permissions Form
-"""
 class PermissionsForm(forms.Form):
 class PermissionsForm(forms.Form):
     legend = _("Private threads")
     legend = _("Private threads")
 
 
@@ -44,21 +39,25 @@ class PermissionsForm(forms.Form):
         label=_("Max number of users invited to private thread"),
         label=_("Max number of users invited to private thread"),
         help_text=_("Enter 0 to don't limit number of participants."),
         help_text=_("Enter 0 to don't limit number of participants."),
         initial=3,
         initial=3,
-        min_value=0
+        min_value=0,
     )
     )
     can_add_everyone_to_private_threads = YesNoSwitch(
     can_add_everyone_to_private_threads = YesNoSwitch(
         label=_("Can add everyone to threads"),
         label=_("Can add everyone to threads"),
-        help_text=_("Allows user to add users that are blocking him to private threads.")
+        help_text=_("Allows user to add users that are blocking him to private threads."),
     )
     )
     can_report_private_threads = YesNoSwitch(
     can_report_private_threads = YesNoSwitch(
         label=_("Can report private threads"),
         label=_("Can report private threads"),
-        help_text=_("Allows user to report private threads they are "
-                    "participating in, making them accessible to moderators.")
+        help_text=_(
+            "Allows user to report private threads they are "
+            "participating in, making them accessible to moderators."
+        ),
     )
     )
     can_moderate_private_threads = YesNoSwitch(
     can_moderate_private_threads = YesNoSwitch(
         label=_("Can moderate private threads"),
         label=_("Can moderate private threads"),
-        help_text=_("Allows user to read, reply, edit and delete content "
-                    "in reported private threads.")
+        help_text=_(
+            "Allows user to read, reply, edit and delete content "
+            "in reported private threads."
+        ),
     )
     )
 
 
 
 
@@ -69,9 +68,6 @@ def change_permissions_form(role):
         return None
         return None
 
 
 
 
-"""
-ACL Builder
-"""
 def build_acl(acl, roles, key_name):
 def build_acl(acl, roles, key_name):
     new_acl = {
     new_acl = {
         'can_use_private_threads': 0,
         'can_use_private_threads': 0,
@@ -84,7 +80,10 @@ def build_acl(acl, roles, key_name):
 
 
     new_acl.update(acl)
     new_acl.update(acl)
 
 
-    algebra.sum_acls(new_acl, roles=roles, key=key_name,
+    algebra.sum_acls(
+        new_acl,
+        roles=roles,
+        key=key_name,
         can_use_private_threads=algebra.greater,
         can_use_private_threads=algebra.greater,
         can_start_private_threads=algebra.greater,
         can_start_private_threads=algebra.greater,
         max_private_thread_participants=algebra.greater_or_zero,
         max_private_thread_participants=algebra.greater_or_zero,
@@ -171,14 +170,13 @@ def register_with(registry):
     registry.acl_annotator(Thread, add_acl_to_thread)
     registry.acl_annotator(Thread, add_acl_to_thread)
 
 
 
 
-"""
-ACL tests
-"""
 def allow_use_private_threads(user):
 def allow_use_private_threads(user):
     if user.is_anonymous:
     if user.is_anonymous:
         raise PermissionDenied(_("You have to sign in to use private threads."))
         raise PermissionDenied(_("You have to sign in to use private threads."))
     if not user.acl_cache['can_use_private_threads']:
     if not user.acl_cache['can_use_private_threads']:
         raise PermissionDenied(_("You can't use private threads."))
         raise PermissionDenied(_("You can't use private threads."))
+
+
 can_use_private_threads = return_boolean(allow_use_private_threads)
 can_use_private_threads = return_boolean(allow_use_private_threads)
 
 
 
 
@@ -192,6 +190,8 @@ def allow_see_private_thread(user, target):
 
 
     if not (can_see_participating or can_see_reported):
     if not (can_see_participating or can_see_reported):
         raise Http404()
         raise Http404()
+
+
 can_see_private_thread = return_boolean(allow_see_private_thread)
 can_see_private_thread = return_boolean(allow_see_private_thread)
 
 
 
 
@@ -200,12 +200,12 @@ def allow_change_owner(user, target):
     is_owner = target.participant and target.participant.is_owner
     is_owner = target.participant and target.participant.is_owner
 
 
     if not (is_owner or is_moderator):
     if not (is_owner or is_moderator):
-        raise PermissionDenied(
-            _("Only thread owner and moderators can change threads owners."))
+        raise PermissionDenied(_("Only thread owner and moderators can change threads owners."))
 
 
     if not is_moderator and target.is_closed:
     if not is_moderator and target.is_closed:
-        raise PermissionDenied(
-            _("Only moderators can change closed threads owners."))
+        raise PermissionDenied(_("Only moderators can change closed threads owners."))
+
+
 can_change_owner = return_boolean(allow_change_owner)
 can_change_owner = return_boolean(allow_change_owner)
 
 
 
 
@@ -214,19 +214,18 @@ def allow_add_participants(user, target):
 
 
     if not is_moderator:
     if not is_moderator:
         if not target.participant or not target.participant.is_owner:
         if not target.participant or not target.participant.is_owner:
-            raise PermissionDenied(
-                _("You have to be thread owner to add new participants to it."))
+            raise PermissionDenied(_("You have to be thread owner to add new participants to it."))
 
 
         if target.is_closed:
         if target.is_closed:
-            raise PermissionDenied(
-                _("Only moderators can add participants to closed threads."))
+            raise PermissionDenied(_("Only moderators can add participants to closed threads."))
 
 
     max_participants = user.acl_cache['max_private_thread_participants']
     max_participants = user.acl_cache['max_private_thread_participants']
     current_participants = len(target.participants_list) - 1
     current_participants = len(target.participants_list) - 1
 
 
     if current_participants >= max_participants:
     if current_participants >= max_participants:
-        raise PermissionDenied(
-            _("You can't add any more new users to this thread."))
+        raise PermissionDenied(_("You can't add any more new users to this thread."))
+
+
 can_add_participants = return_boolean(allow_add_participants)
 can_add_participants = return_boolean(allow_add_participants)
 
 
 
 
@@ -235,15 +234,15 @@ def allow_remove_participant(user, thread, target):
         return
         return
 
 
     if user == target:
     if user == target:
-        return # we can always remove ourselves
+        return  # we can always remove ourselves
 
 
     if thread.is_closed:
     if thread.is_closed:
-        raise PermissionDenied(
-            _("Only moderators can remove participants from closed threads."))
+        raise PermissionDenied(_("Only moderators can remove participants from closed threads."))
 
 
     if not thread.participant or not thread.participant.is_owner:
     if not thread.participant or not thread.participant.is_owner:
-        raise PermissionDenied(
-            _("You have to be thread owner to remove participants from it."))
+        raise PermissionDenied(_("You have to be thread owner to remove participants from it."))
+
+
 can_remove_participant = return_boolean(allow_remove_participant)
 can_remove_participant = return_boolean(allow_remove_participant)
 
 
 
 
@@ -252,7 +251,8 @@ def allow_add_participant(user, target):
 
 
     if not can_use_private_threads(target):
     if not can_use_private_threads(target):
         raise PermissionDenied(
         raise PermissionDenied(
-            _("%(user)s can't participate in private threads.") % message_format)
+            _("%(user)s can't participate in private threads.") % message_format
+        )
 
 
     if user.acl_cache['can_add_everyone_to_private_threads']:
     if user.acl_cache['can_add_everyone_to_private_threads']:
         return
         return
@@ -262,15 +262,20 @@ def allow_add_participant(user, target):
 
 
     if target.can_be_messaged_by_nobody:
     if target.can_be_messaged_by_nobody:
         raise PermissionDenied(
         raise PermissionDenied(
-            _("%(user)s is not allowing invitations to private threads.") % message_format)
+            _("%(user)s is not allowing invitations to private threads.") % message_format
+        )
 
 
     if target.can_be_messaged_by_followed and not target.is_following(user):
     if target.can_be_messaged_by_followed and not target.is_following(user):
         message = _("%(user)s limits invitations to private threads to followed users.")
         message = _("%(user)s limits invitations to private threads to followed users.")
         raise PermissionDenied(message % message_format)
         raise PermissionDenied(message % message_format)
+
+
 can_add_participant = return_boolean(allow_add_participant)
 can_add_participant = return_boolean(allow_add_participant)
 
 
 
 
 def allow_message_user(user, target):
 def allow_message_user(user, target):
     allow_use_private_threads(user)
     allow_use_private_threads(user)
     allow_add_participant(user, target)
     allow_add_participant(user, target)
+
+
 can_message_user = return_boolean(allow_message_user)
 can_message_user = return_boolean(allow_message_user)

+ 171 - 119
misago/threads/permissions/threads.py

@@ -9,7 +9,7 @@ from django.utils.translation import ungettext
 from misago.acl import add_acl, algebra
 from misago.acl import add_acl, algebra
 from misago.acl.decorators import return_boolean
 from misago.acl.decorators import return_boolean
 from misago.acl.models import Role
 from misago.acl.models import Role
-from misago.categories.models import Category, CategoryRole, RoleCategoryACL
+from misago.categories.models import Category, CategoryRole
 from misago.categories.permissions import get_categories_roles
 from misago.categories.permissions import get_categories_roles
 from misago.core.forms import YesNoSwitch
 from misago.core.forms import YesNoSwitch
 from misago.threads.models import Post, Thread
 from misago.threads.models import Post, Thread
@@ -47,33 +47,34 @@ __all__ = [
 ]
 ]
 
 
 
 
-"""
-Admin Permissions Forms
-"""
 class RolePermissionsForm(forms.Form):
 class RolePermissionsForm(forms.Form):
     legend = _("Threads")
     legend = _("Threads")
 
 
     can_see_unapproved_content_lists = YesNoSwitch(
     can_see_unapproved_content_lists = YesNoSwitch(
         label=_("Can see unapproved content list"),
         label=_("Can see unapproved content list"),
-        help_text=_('Allows access to "unapproved" tab on threads lists for '
-                    "easy listing of threads that are unapproved or contain "
-                    "unapproved posts. Despite the tab being available on all "
-                    "threads lists, it will only display threads belonging to "
-                    "categories in which the user has permission to approve "
-                    "content.")
+        help_text=_(
+            'Allows access to "unapproved" tab on threads lists for '
+            "easy listing of threads that are unapproved or contain "
+            "unapproved posts. Despite the tab being available on all "
+            "threads lists, it will only display threads belonging to "
+            "categories in which the user has permission to approve "
+            "content."
+        ),
     )
     )
     can_see_reported_content_lists = YesNoSwitch(
     can_see_reported_content_lists = YesNoSwitch(
         label=_("Can see reported content list"),
         label=_("Can see reported content list"),
-        help_text=_('Allows access to "reported" tab on threads lists for '
-                    "easy listing of threads that contain reported posts. "
-                    "Despite the tab being available on all categories "
-                    "threads lists, it will only display threads belonging to "
-                    "categories in which the user has permission to see posts "
-                    "reports.")
+        help_text=_(
+            'Allows access to "reported" tab on threads lists for '
+            "easy listing of threads that contain reported posts. "
+            "Despite the tab being available on all categories "
+            "threads lists, it will only display threads belonging to "
+            "categories in which the user has permission to see posts "
+            "reports."
+        ),
     )
     )
     can_omit_flood_protection = YesNoSwitch(
     can_omit_flood_protection = YesNoSwitch(
         label=_("Can omit flood protection"),
         label=_("Can omit flood protection"),
-        help_text=_("Allows posting more frequently than flood protection would.")
+        help_text=_("Allows posting more frequently than flood protection would."),
     )
     )
 
 
 
 
@@ -84,7 +85,10 @@ class CategoryPermissionsForm(forms.Form):
         label=_("Can see threads"),
         label=_("Can see threads"),
         coerce=int,
         coerce=int,
         initial=0,
         initial=0,
-        choices=((0, _("Started threads")), (1, _("All threads")))
+        choices=[
+            (0, _("Started threads")),
+            (1, _("All threads")),
+        ],
     )
     )
 
 
     can_start_threads = YesNoSwitch(label=_("Can start threads"))
     can_start_threads = YesNoSwitch(label=_("Can start threads"))
@@ -94,46 +98,52 @@ class CategoryPermissionsForm(forms.Form):
         label=_("Can edit threads"),
         label=_("Can edit threads"),
         coerce=int,
         coerce=int,
         initial=0,
         initial=0,
-        choices=((0, _("No")), (1, _("Own threads")), (2, _("All threads")))
+        choices=[
+            (0, _("No")),
+            (1, _("Own threads")),
+            (2, _("All threads")),
+        ],
     )
     )
     can_hide_own_threads = forms.TypedChoiceField(
     can_hide_own_threads = forms.TypedChoiceField(
         label=_("Can hide own threads"),
         label=_("Can hide own threads"),
-        help_text=_("Only threads started within time limit and "
-                    "with no replies can be hidden."),
+        help_text=_(
+            "Only threads started within time limit and "
+            "with no replies can be hidden."
+        ),
         coerce=int,
         coerce=int,
         initial=0,
         initial=0,
-        choices=(
+        choices=[
             (0, _("No")),
             (0, _("No")),
             (1, _("Hide threads")),
             (1, _("Hide threads")),
-            (2, _("Delete threads"))
-        )
+            (2, _("Delete threads")),
+        ],
     )
     )
     thread_edit_time = forms.IntegerField(
     thread_edit_time = forms.IntegerField(
         label=_("Time limit for own threads edits, in minutes"),
         label=_("Time limit for own threads edits, in minutes"),
         help_text=_("Enter 0 to don't limit time for editing own threads."),
         help_text=_("Enter 0 to don't limit time for editing own threads."),
         initial=0,
         initial=0,
-        min_value=0
+        min_value=0,
     )
     )
     can_hide_threads = forms.TypedChoiceField(
     can_hide_threads = forms.TypedChoiceField(
         label=_("Can hide all threads"),
         label=_("Can hide all threads"),
         coerce=int,
         coerce=int,
         initial=0,
         initial=0,
-        choices=(
+        choices=[
             (0, _("No")),
             (0, _("No")),
             (1, _("Hide threads")),
             (1, _("Hide threads")),
-            (2, _("Delete threads"))
-        )
+            (2, _("Delete threads")),
+        ],
     )
     )
 
 
     can_pin_threads = forms.TypedChoiceField(
     can_pin_threads = forms.TypedChoiceField(
         label=_("Can pin threads"),
         label=_("Can pin threads"),
         coerce=int,
         coerce=int,
         initial=0,
         initial=0,
-        choices=(
+        choices=[
             (0, _("No")),
             (0, _("No")),
             (1, _("Locally")),
             (1, _("Locally")),
-            (2, _("Globally"))
-        )
+            (2, _("Globally")),
+        ],
     )
     )
     can_close_threads = YesNoSwitch(label=_("Can close threads"))
     can_close_threads = YesNoSwitch(label=_("Can close threads"))
     can_move_threads = YesNoSwitch(label=_("Can move threads"))
     can_move_threads = YesNoSwitch(label=_("Can move threads"))
@@ -143,63 +153,66 @@ class CategoryPermissionsForm(forms.Form):
         label=_("Can edit posts"),
         label=_("Can edit posts"),
         coerce=int,
         coerce=int,
         initial=0,
         initial=0,
-        choices=((0, _("No")), (1, _("Own posts")), (2, _("All posts")))
+        choices=[
+            (0, _("No")),
+            (1, _("Own posts")),
+            (2, _("All posts")),
+        ],
     )
     )
     can_hide_own_posts = forms.TypedChoiceField(
     can_hide_own_posts = forms.TypedChoiceField(
         label=_("Can hide own posts"),
         label=_("Can hide own posts"),
         help_text=_("Only last posts to thread made within edit time limit can be hidden."),
         help_text=_("Only last posts to thread made within edit time limit can be hidden."),
         coerce=int,
         coerce=int,
         initial=0,
         initial=0,
-        choices=(
+        choices=[
             (0, _("No")),
             (0, _("No")),
             (1, _("Hide posts")),
             (1, _("Hide posts")),
-            (2, _("Delete posts"))
-        )
+            (2, _("Delete posts")),
+        ],
     )
     )
     post_edit_time = forms.IntegerField(
     post_edit_time = forms.IntegerField(
         label=_("Time limit for own post edits, in minutes"),
         label=_("Time limit for own post edits, in minutes"),
         help_text=_("Enter 0 to don't limit time for editing own posts."),
         help_text=_("Enter 0 to don't limit time for editing own posts."),
         initial=0,
         initial=0,
-        min_value=0
+        min_value=0,
     )
     )
     can_hide_posts = forms.TypedChoiceField(
     can_hide_posts = forms.TypedChoiceField(
         label=_("Can hide all posts"),
         label=_("Can hide all posts"),
         coerce=int,
         coerce=int,
         initial=0,
         initial=0,
-        choices=(
+        choices=[
             (0, _("No")),
             (0, _("No")),
             (1, _("Hide posts")),
             (1, _("Hide posts")),
-            (2, _("Delete posts"))
-        )
+            (2, _("Delete posts")),
+        ],
     )
     )
 
 
     can_see_posts_likes = forms.TypedChoiceField(
     can_see_posts_likes = forms.TypedChoiceField(
         label=_("Can see posts likes"),
         label=_("Can see posts likes"),
         coerce=int,
         coerce=int,
         initial=0,
         initial=0,
-        choices=(
+        choices=[
             (0, _("No")),
             (0, _("No")),
             (1, _("Number only")),
             (1, _("Number only")),
-            (2, _("Number and list of likers"))
-        )
+            (2, _("Number and list of likers")),
+        ],
     )
     )
     can_like_posts = YesNoSwitch(
     can_like_posts = YesNoSwitch(
         label=_("Can like posts"),
         label=_("Can like posts"),
-        help_text=_("Only users with this permission to see likes can like posts.")
+        help_text=_("Only users with this permission to see likes can like posts."),
     )
     )
 
 
     can_protect_posts = YesNoSwitch(
     can_protect_posts = YesNoSwitch(
         label=_("Can protect posts"),
         label=_("Can protect posts"),
-        help_text=_("Only users with this permission can edit protected posts.")
+        help_text=_("Only users with this permission can edit protected posts."),
     )
     )
     can_move_posts = YesNoSwitch(
     can_move_posts = YesNoSwitch(
-        label=_("Can move posts"),
-        help_text=_("Will be able to move posts to other threads.")
+        label=_("Can move posts"), help_text=_("Will be able to move posts to other threads.")
     )
     )
     can_merge_posts = YesNoSwitch(label=_("Can merge posts"))
     can_merge_posts = YesNoSwitch(label=_("Can merge posts"))
     can_approve_content = YesNoSwitch(
     can_approve_content = YesNoSwitch(
         label=_("Can approve content"),
         label=_("Can approve content"),
-        help_text=_("Will be able to see and approve unapproved content.")
+        help_text=_("Will be able to see and approve unapproved content."),
     )
     )
     can_report_content = YesNoSwitch(label=_("Can report posts"))
     can_report_content = YesNoSwitch(label=_("Can report posts"))
     can_see_reports = YesNoSwitch(label=_("Can see reports"))
     can_see_reports = YesNoSwitch(label=_("Can see reports"))
@@ -208,11 +221,11 @@ class CategoryPermissionsForm(forms.Form):
         label=_("Can hide events"),
         label=_("Can hide events"),
         coerce=int,
         coerce=int,
         initial=0,
         initial=0,
-        choices=(
+        choices=[
             (0, _("No")),
             (0, _("No")),
             (1, _("Hide events")),
             (1, _("Hide events")),
-            (2, _("Delete events"))
-        )
+            (2, _("Delete events")),
+        ],
     )
     )
 
 
 
 
@@ -225,9 +238,6 @@ def change_permissions_form(role):
         return None
         return None
 
 
 
 
-"""
-ACL Builder
-"""
 def build_acl(acl, roles, key_name):
 def build_acl(acl, roles, key_name):
     acl.update({
     acl.update({
         'can_see_unapproved_content_lists': False,
         'can_see_unapproved_content_lists': False,
@@ -237,7 +247,10 @@ def build_acl(acl, roles, key_name):
         'can_see_reports': [],
         'can_see_reports': [],
     })
     })
 
 
-    acl = algebra.sum_acls(acl, roles=roles, key=key_name,
+    acl = algebra.sum_acls(
+        acl,
+        roles=roles,
+        key=key_name,
         can_see_unapproved_content_lists=algebra.greater,
         can_see_unapproved_content_lists=algebra.greater,
         can_see_reported_content_lists=algebra.greater,
         can_see_reported_content_lists=algebra.greater,
         can_omit_flood_protection=algebra.greater
         can_omit_flood_protection=algebra.greater
@@ -250,7 +263,8 @@ def build_acl(acl, roles, key_name):
         category_acl = acl['categories'].get(category.pk, {'can_browse': 0})
         category_acl = acl['categories'].get(category.pk, {'can_browse': 0})
         if category_acl['can_browse']:
         if category_acl['can_browse']:
             category_acl = acl['categories'][category.pk] = build_category_acl(
             category_acl = acl['categories'][category.pk] = build_category_acl(
-                category_acl, category, categories_roles, key_name)
+                category_acl, category, categories_roles, key_name
+            )
 
 
             if category_acl.get('can_approve_content'):
             if category_acl.get('can_approve_content'):
                 acl['can_approve_content'].append(category.pk)
                 acl['can_approve_content'].append(category.pk)
@@ -291,7 +305,10 @@ def build_category_acl(acl, category, categories_roles, key_name):
     }
     }
     final_acl.update(acl)
     final_acl.update(acl)
 
 
-    algebra.sum_acls(final_acl, roles=category_roles, key=key_name,
+    algebra.sum_acls(
+        final_acl,
+        roles=category_roles,
+        key=key_name,
         can_see_all_threads=algebra.greater,
         can_see_all_threads=algebra.greater,
         can_start_threads=algebra.greater,
         can_start_threads=algebra.greater,
         can_reply_threads=algebra.greater,
         can_reply_threads=algebra.greater,
@@ -321,9 +338,6 @@ def build_category_acl(acl, category, categories_roles, key_name):
     return final_acl
     return final_acl
 
 
 
 
-"""
-ACL's for targets
-"""
 def add_acl_to_category(user, category):
 def add_acl_to_category(user, category):
     category_acl = user.acl_cache['categories'].get(category.pk, {})
     category_acl = user.acl_cache['categories'].get(category.pk, {})
 
 
@@ -355,13 +369,17 @@ def add_acl_to_category(user, category):
         'can_hide_events': 0,
         'can_hide_events': 0,
     })
     })
 
 
-    algebra.sum_acls(category.acl, acls=[category_acl],
+    algebra.sum_acls(
+        category.acl,
+        acls=[category_acl],
         can_see_all_threads=algebra.greater,
         can_see_all_threads=algebra.greater,
         can_see_posts_likes=algebra.greater,
         can_see_posts_likes=algebra.greater,
     )
     )
 
 
     if user.is_authenticated:
     if user.is_authenticated:
-        algebra.sum_acls(category.acl, acls=[category_acl],
+        algebra.sum_acls(
+            category.acl,
+            acls=[category_acl],
             can_start_threads=algebra.greater,
             can_start_threads=algebra.greater,
             can_reply_threads=algebra.greater,
             can_reply_threads=algebra.greater,
             can_edit_threads=algebra.greater,
             can_edit_threads=algebra.greater,
@@ -477,14 +495,13 @@ def register_with(registry):
     registry.acl_annotator(Post, add_acl_to_post)
     registry.acl_annotator(Post, add_acl_to_post)
 
 
 
 
-"""
-ACL tests
-"""
 def allow_see_thread(user, target):
 def allow_see_thread(user, target):
-    category_acl = user.acl_cache['categories'].get(target.category_id, {
-        'can_see': False,
-        'can_browse': False
-    })
+    category_acl = user.acl_cache['categories'].get(
+        target.category_id, {
+            'can_see': False,
+            'can_browse': False,
+        }
+    )
 
 
     if not (category_acl['can_see'] and category_acl['can_browse']):
     if not (category_acl['can_see'] and category_acl['can_browse']):
         raise Http404()
         raise Http404()
@@ -498,6 +515,8 @@ def allow_see_thread(user, target):
 
 
         if target.is_unapproved and not category_acl['can_approve_content']:
         if target.is_unapproved and not category_acl['can_approve_content']:
             raise Http404()
             raise Http404()
+
+
 can_see_thread = return_boolean(allow_see_thread)
 can_see_thread = return_boolean(allow_see_thread)
 
 
 
 
@@ -505,16 +524,22 @@ def allow_start_thread(user, target):
     if user.is_anonymous:
     if user.is_anonymous:
         raise PermissionDenied(_("You have to sign in to start threads."))
         raise PermissionDenied(_("You have to sign in to start threads."))
 
 
-    category_acl = user.acl_cache['categories'].get(target.pk, {
-        'can_close_threads': False,
-        'can_start_threads': False
-    })
+    category_acl = user.acl_cache['categories'].get(
+        target.pk, {
+            'can_close_threads': False,
+            'can_start_threads': False,
+        }
+    )
 
 
     if target.is_closed and not category_acl['can_close_threads']:
     if target.is_closed and not category_acl['can_close_threads']:
         raise PermissionDenied(_("This category is closed. You can't start new threads in it."))
         raise PermissionDenied(_("This category is closed. You can't start new threads in it."))
 
 
     if not category_acl['can_start_threads']:
     if not category_acl['can_start_threads']:
-        raise PermissionDenied(_("You don't have permission to start new threads in this category."))
+        raise PermissionDenied(
+            _("You don't have permission to start new threads in this category.")
+        )
+
+
 can_start_thread = return_boolean(allow_start_thread)
 can_start_thread = return_boolean(allow_start_thread)
 
 
 
 
@@ -522,10 +547,12 @@ def allow_reply_thread(user, target):
     if user.is_anonymous:
     if user.is_anonymous:
         raise PermissionDenied(_("You have to sign in to reply threads."))
         raise PermissionDenied(_("You have to sign in to reply threads."))
 
 
-    category_acl = user.acl_cache['categories'].get(target.category_id, {
-        'can_close_threads': False,
-        'can_reply_threads': False
-    })
+    category_acl = user.acl_cache['categories'].get(
+        target.category_id, {
+            'can_close_threads': False,
+            'can_reply_threads': False,
+        }
+    )
 
 
     if not category_acl['can_close_threads']:
     if not category_acl['can_close_threads']:
         if target.category.is_closed:
         if target.category.is_closed:
@@ -535,6 +562,8 @@ def allow_reply_thread(user, target):
 
 
     if not category_acl['can_reply_threads']:
     if not category_acl['can_reply_threads']:
         raise PermissionDenied(_("You can't reply to threads in this category."))
         raise PermissionDenied(_("You can't reply to threads in this category."))
+
+
 can_reply_thread = return_boolean(allow_reply_thread)
 can_reply_thread = return_boolean(allow_reply_thread)
 
 
 
 
@@ -542,9 +571,9 @@ def allow_edit_thread(user, target):
     if user.is_anonymous:
     if user.is_anonymous:
         raise PermissionDenied(_("You have to sign in to edit threads."))
         raise PermissionDenied(_("You have to sign in to edit threads."))
 
 
-    category_acl = user.acl_cache['categories'].get(target.category_id, {
-        'can_edit_threads': False
-    })
+    category_acl = user.acl_cache['categories'].get(
+        target.category_id, {'can_edit_threads': False}
+    )
 
 
     if not category_acl['can_edit_threads']:
     if not category_acl['can_edit_threads']:
         raise PermissionDenied(_("You can't edit threads in this category."))
         raise PermissionDenied(_("You can't edit threads in this category."))
@@ -563,16 +592,21 @@ def allow_edit_thread(user, target):
             message = ungettext(
             message = ungettext(
                 "You can't edit threads that are older than %(minutes)s minute.",
                 "You can't edit threads that are older than %(minutes)s minute.",
                 "You can't edit threads that are older than %(minutes)s minutes.",
                 "You can't edit threads that are older than %(minutes)s minutes.",
-                category_acl['thread_edit_time'])
+                category_acl['thread_edit_time']
+            )
             raise PermissionDenied(message % {'minutes': category_acl['thread_edit_time']})
             raise PermissionDenied(message % {'minutes': category_acl['thread_edit_time']})
+
+
 can_edit_thread = return_boolean(allow_edit_thread)
 can_edit_thread = return_boolean(allow_edit_thread)
 
 
 
 
 def allow_see_post(user, target):
 def allow_see_post(user, target):
-    category_acl = user.acl_cache['categories'].get(target.category_id, {
-        'can_approve_content': False,
-        'can_hide_events': False
-    })
+    category_acl = user.acl_cache['categories'].get(
+        target.category_id, {
+            'can_approve_content': False,
+            'can_hide_events': False,
+        }
+    )
 
 
     if not target.is_event and target.is_unapproved:
     if not target.is_event and target.is_unapproved:
         if user.is_anonymous:
         if user.is_anonymous:
@@ -583,6 +617,8 @@ def allow_see_post(user, target):
 
 
     if target.is_event and target.is_hidden and not category_acl['can_hide_events']:
     if target.is_event and target.is_hidden and not category_acl['can_hide_events']:
         raise Http404()
         raise Http404()
+
+
 can_see_post = return_boolean(allow_see_post)
 can_see_post = return_boolean(allow_see_post)
 
 
 
 
@@ -593,9 +629,7 @@ def allow_edit_post(user, target):
     if target.is_event:
     if target.is_event:
         raise PermissionDenied(_("Events can't be edited."))
         raise PermissionDenied(_("Events can't be edited."))
 
 
-    category_acl = user.acl_cache['categories'].get(target.category_id, {
-        'can_edit_posts': False
-    })
+    category_acl = user.acl_cache['categories'].get(target.category_id, {'can_edit_posts': False})
 
 
     if not category_acl['can_edit_posts']:
     if not category_acl['can_edit_posts']:
         raise PermissionDenied(_("You can't edit posts in this category."))
         raise PermissionDenied(_("You can't edit posts in this category."))
@@ -620,8 +654,11 @@ def allow_edit_post(user, target):
             message = ungettext(
             message = ungettext(
                 "You can't edit posts that are older than %(minutes)s minute.",
                 "You can't edit posts that are older than %(minutes)s minute.",
                 "You can't edit posts that are older than %(minutes)s minutes.",
                 "You can't edit posts that are older than %(minutes)s minutes.",
-                category_acl['post_edit_time'])
+                category_acl['post_edit_time'],
+            )
             raise PermissionDenied(message % {'minutes': category_acl['post_edit_time']})
             raise PermissionDenied(message % {'minutes': category_acl['post_edit_time']})
+
+
 can_edit_post = return_boolean(allow_edit_post)
 can_edit_post = return_boolean(allow_edit_post)
 
 
 
 
@@ -629,10 +666,12 @@ def allow_unhide_post(user, target):
     if user.is_anonymous:
     if user.is_anonymous:
         raise PermissionDenied(_("You have to sign in to reveal posts."))
         raise PermissionDenied(_("You have to sign in to reveal posts."))
 
 
-    category_acl = user.acl_cache['categories'].get(target.category_id, {
-        'can_hide_posts': 0,
-        'can_hide_own_posts': 0
-    })
+    category_acl = user.acl_cache['categories'].get(
+        target.category_id, {
+            'can_hide_posts': 0,
+            'can_hide_own_posts': 0,
+        }
+    )
 
 
     if not category_acl['can_hide_posts']:
     if not category_acl['can_hide_posts']:
         if not category_acl['can_hide_own_posts']:
         if not category_acl['can_hide_own_posts']:
@@ -654,11 +693,14 @@ def allow_unhide_post(user, target):
             message = ungettext(
             message = ungettext(
                 "You can't reveal posts that are older than %(minutes)s minute.",
                 "You can't reveal posts that are older than %(minutes)s minute.",
                 "You can't reveal posts that are older than %(minutes)s minutes.",
                 "You can't reveal posts that are older than %(minutes)s minutes.",
-                category_acl['post_edit_time'])
+                category_acl['post_edit_time'],
+            )
             raise PermissionDenied(message % {'minutes': category_acl['post_edit_time']})
             raise PermissionDenied(message % {'minutes': category_acl['post_edit_time']})
 
 
     if target.is_first_post:
     if target.is_first_post:
         raise PermissionDenied(_("You can't reveal thread's first post."))
         raise PermissionDenied(_("You can't reveal thread's first post."))
+
+
 can_unhide_post = return_boolean(allow_unhide_post)
 can_unhide_post = return_boolean(allow_unhide_post)
 
 
 
 
@@ -666,10 +708,12 @@ def allow_hide_post(user, target):
     if user.is_anonymous:
     if user.is_anonymous:
         raise PermissionDenied(_("You have to sign in to hide posts."))
         raise PermissionDenied(_("You have to sign in to hide posts."))
 
 
-    category_acl = user.acl_cache['categories'].get(target.category_id, {
-        'can_hide_posts': 0,
-        'can_hide_own_posts': 0
-    })
+    category_acl = user.acl_cache['categories'].get(
+        target.category_id, {
+            'can_hide_posts': 0,
+            'can_hide_own_posts': 0,
+        }
+    )
 
 
     if not category_acl['can_hide_posts']:
     if not category_acl['can_hide_posts']:
         if not category_acl['can_hide_own_posts']:
         if not category_acl['can_hide_own_posts']:
@@ -691,11 +735,14 @@ def allow_hide_post(user, target):
             message = ungettext(
             message = ungettext(
                 "You can't hide posts that are older than %(minutes)s minute.",
                 "You can't hide posts that are older than %(minutes)s minute.",
                 "You can't hide posts that are older than %(minutes)s minutes.",
                 "You can't hide posts that are older than %(minutes)s minutes.",
-                category_acl['post_edit_time'])
+                category_acl['post_edit_time'],
+            )
             raise PermissionDenied(message % {'minutes': category_acl['post_edit_time']})
             raise PermissionDenied(message % {'minutes': category_acl['post_edit_time']})
 
 
     if target.is_first_post:
     if target.is_first_post:
         raise PermissionDenied(_("You can't hide thread's first post."))
         raise PermissionDenied(_("You can't hide thread's first post."))
+
+
 can_hide_post = return_boolean(allow_hide_post)
 can_hide_post = return_boolean(allow_hide_post)
 
 
 
 
@@ -703,10 +750,12 @@ def allow_delete_post(user, target):
     if user.is_anonymous:
     if user.is_anonymous:
         raise PermissionDenied(_("You have to sign in to delete posts."))
         raise PermissionDenied(_("You have to sign in to delete posts."))
 
 
-    category_acl = user.acl_cache['categories'].get(target.category_id, {
-        'can_hide_posts': 0,
-        'can_hide_own_posts': 0
-    })
+    category_acl = user.acl_cache['categories'].get(
+        target.category_id, {
+            'can_hide_posts': 0,
+            'can_hide_own_posts': 0,
+        }
+    )
 
 
     if category_acl['can_hide_posts'] != 2:
     if category_acl['can_hide_posts'] != 2:
         if category_acl['can_hide_own_posts'] != 2:
         if category_acl['can_hide_own_posts'] != 2:
@@ -728,11 +777,14 @@ def allow_delete_post(user, target):
             message = ungettext(
             message = ungettext(
                 "You can't delete posts that are older than %(minutes)s minute.",
                 "You can't delete posts that are older than %(minutes)s minute.",
                 "You can't delete posts that are older than %(minutes)s minutes.",
                 "You can't delete posts that are older than %(minutes)s minutes.",
-                category_acl['post_edit_time'])
+                category_acl['post_edit_time'],
+            )
             raise PermissionDenied(message % {'minutes': category_acl['post_edit_time']})
             raise PermissionDenied(message % {'minutes': category_acl['post_edit_time']})
 
 
     if target.is_first_post:
     if target.is_first_post:
         raise PermissionDenied(_("You can't delete thread's first post."))
         raise PermissionDenied(_("You can't delete thread's first post."))
+
+
 can_delete_post = return_boolean(allow_delete_post)
 can_delete_post = return_boolean(allow_delete_post)
 
 
 
 
@@ -740,14 +792,16 @@ def allow_protect_post(user, target):
     if user.is_anonymous:
     if user.is_anonymous:
         raise PermissionDenied(_("You have to sign in to protect posts."))
         raise PermissionDenied(_("You have to sign in to protect posts."))
 
 
-    category_acl = user.acl_cache['categories'].get(target.category_id, {
-        'can_protect_posts': False
-    })
+    category_acl = user.acl_cache['categories'].get(
+        target.category_id, {'can_protect_posts': False}
+    )
 
 
     if not category_acl['can_protect_posts']:
     if not category_acl['can_protect_posts']:
         raise PermissionDenied(_("You can't protect posts in this category."))
         raise PermissionDenied(_("You can't protect posts in this category."))
     if not can_edit_post(user, target):
     if not can_edit_post(user, target):
         raise PermissionDenied(_("You can't protect posts you can't edit."))
         raise PermissionDenied(_("You can't protect posts you can't edit."))
+
+
 can_protect_post = return_boolean(allow_protect_post)
 can_protect_post = return_boolean(allow_protect_post)
 
 
 
 
@@ -755,9 +809,9 @@ def allow_approve_post(user, target):
     if user.is_anonymous:
     if user.is_anonymous:
         raise PermissionDenied(_("You have to sign in to approve posts."))
         raise PermissionDenied(_("You have to sign in to approve posts."))
 
 
-    category_acl = user.acl_cache['categories'].get(target.category_id, {
-        'can_approve_content': False
-    })
+    category_acl = user.acl_cache['categories'].get(
+        target.category_id, {'can_approve_content': False}
+    )
 
 
     if not category_acl['can_approve_content']:
     if not category_acl['can_approve_content']:
         raise PermissionDenied(_("You can't approve posts in this category."))
         raise PermissionDenied(_("You can't approve posts in this category."))
@@ -765,6 +819,8 @@ def allow_approve_post(user, target):
         raise PermissionDenied(_("You can't approve thread's first post."))
         raise PermissionDenied(_("You can't approve thread's first post."))
     if not target.is_first_post and not category_acl['can_hide_posts'] and target.is_hidden:
     if not target.is_first_post and not category_acl['can_hide_posts'] and target.is_hidden:
         raise PermissionDenied(_("You can't approve posts the content you can't see."))
         raise PermissionDenied(_("You can't approve posts the content you can't see."))
+
+
 can_approve_post = return_boolean(allow_approve_post)
 can_approve_post = return_boolean(allow_approve_post)
 
 
 
 
@@ -772,9 +828,7 @@ def allow_move_post(user, target):
     if user.is_anonymous:
     if user.is_anonymous:
         raise PermissionDenied(_("You have to sign in to move posts."))
         raise PermissionDenied(_("You have to sign in to move posts."))
 
 
-    category_acl = user.acl_cache['categories'].get(target.category_id, {
-        'can_move_posts': False
-    })
+    category_acl = user.acl_cache['categories'].get(target.category_id, {'can_move_posts': False})
 
 
     if not category_acl['can_move_posts']:
     if not category_acl['can_move_posts']:
         raise PermissionDenied(_("You can't move posts in this category."))
         raise PermissionDenied(_("You can't move posts in this category."))
@@ -784,6 +838,8 @@ def allow_move_post(user, target):
         raise PermissionDenied(_("You can't move thread's first post."))
         raise PermissionDenied(_("You can't move thread's first post."))
     if not category_acl['can_hide_posts'] and target.is_hidden:
     if not category_acl['can_hide_posts'] and target.is_hidden:
         raise PermissionDenied(_("You can't move posts the content you can't see."))
         raise PermissionDenied(_("You can't move posts the content you can't see."))
+
+
 can_move_post = return_boolean(allow_move_post)
 can_move_post = return_boolean(allow_move_post)
 
 
 
 
@@ -795,12 +851,11 @@ def allow_delete_event(user, target):
 
 
     if not category_acl or category_acl['can_hide_events'] != 2:
     if not category_acl or category_acl['can_hide_events'] != 2:
         raise PermissionDenied(_("You can't delete events in this category."))
         raise PermissionDenied(_("You can't delete events in this category."))
+
+
 can_delete_event = return_boolean(allow_delete_event)
 can_delete_event = return_boolean(allow_delete_event)
 
 
 
 
-"""
-Permission check helpers
-"""
 def can_change_owned_thread(user, target):
 def can_change_owned_thread(user, target):
     if user.is_anonymous or user.pk != target.starter_id:
     if user.is_anonymous or user.pk != target.starter_id:
         return False
         return False
@@ -833,9 +888,6 @@ def has_time_to_edit_post(user, target):
         return True
         return True
 
 
 
 
-"""
-Queryset helpers
-"""
 def exclude_invisible_threads(user, categories, queryset):
 def exclude_invisible_threads(user, categories, queryset):
     show_all = []
     show_all = []
     show_accepted_visible = []
     show_accepted_visible = []

+ 13 - 8
misago/threads/search.py

@@ -22,14 +22,19 @@ class SearchThreads(SearchProvider):
 
 
         if len(query) > 2:
         if len(query) > 2:
             visible_threads = exclude_invisible_threads(
             visible_threads = exclude_invisible_threads(
-                self.request.user, threads_categories, Thread.objects)
+                self.request.user, threads_categories, Thread.objects
+            )
             results = search_threads(self.request, query, visible_threads)
             results = search_threads(self.request, query, visible_threads)
         else:
         else:
             results = []
             results = []
 
 
         list_page = paginate(
         list_page = paginate(
-            results, page, settings.MISAGO_POSTS_PER_PAGE, settings.MISAGO_POSTS_TAIL,
-            allow_explicit_first_page=True)
+            results,
+            page,
+            settings.MISAGO_POSTS_PER_PAGE,
+            settings.MISAGO_POSTS_TAIL,
+            allow_explicit_first_page=True,
+        )
         paginator = pagination_dict(list_page)
         paginator = pagination_dict(list_page)
 
 
         posts = list(list_page.object_list)
         posts = list(list_page.object_list)
@@ -38,12 +43,12 @@ class SearchThreads(SearchProvider):
         for post in posts:
         for post in posts:
             threads.append(post.thread)
             threads.append(post.thread)
 
 
-        add_categories_to_items(
-            root_category.unwrap(), threads_categories, posts + threads)
+        add_categories_to_items(root_category.unwrap(), threads_categories, posts + threads)
 
 
         results = {
         results = {
-            'results': FeedSerializer(
-                posts, many=True, context={'user': self.request.user}).data
+            'results': FeedSerializer(posts, many=True, context={
+            'user': self.request.user,
+        }).data
         }
         }
         results.update(paginator)
         results.update(paginator)
 
 
@@ -59,5 +64,5 @@ def search_threads(request, query, visible_threads):
         is_hidden=False,
         is_hidden=False,
         is_unapproved=False,
         is_unapproved=False,
         thread_id__in=visible_threads.values('id'),
         thread_id__in=visible_threads.values('id'),
-        search_vector=search_query
+        search_vector=search_query,
     ).annotate(rank=SearchRank(search_vector, search_query)).order_by('-rank', '-id')
     ).annotate(rank=SearchRank(search_vector, search_query)).order_by('-rank', '-id')

+ 8 - 9
misago/threads/serializers/attachment.py

@@ -2,7 +2,6 @@ from rest_framework import serializers
 
 
 from django.urls import reverse
 from django.urls import reverse
 
 
-from misago.core.utils import format_plaintext_for_html
 from misago.threads.models import Attachment
 from misago.threads.models import Attachment
 
 
 
 
@@ -21,7 +20,7 @@ class AttachmentSerializer(serializers.ModelSerializer):
 
 
     class Meta:
     class Meta:
         model = Attachment
         model = Attachment
-        fields = (
+        fields = [
             'id',
             'id',
             'filetype',
             'filetype',
             'post',
             'post',
@@ -30,12 +29,10 @@ class AttachmentSerializer(serializers.ModelSerializer):
             'uploader_ip',
             'uploader_ip',
             'filename',
             'filename',
             'size',
             'size',
-
             'acl',
             'acl',
             'is_image',
             'is_image',
-
             'url',
             'url',
-        )
+        ]
 
 
     def get_acl(self, obj):
     def get_acl(self, obj):
         try:
         try:
@@ -64,9 +61,11 @@ class AttachmentSerializer(serializers.ModelSerializer):
 
 
     def get_uploader_url(self, obj):
     def get_uploader_url(self, obj):
         if obj.uploader_id:
         if obj.uploader_id:
-            return reverse('misago:user', kwargs={
-                'slug': obj.uploader_slug,
-                'pk': obj.uploader_id,
-            })
+            return reverse(
+                'misago:user', kwargs={
+                    'slug': obj.uploader_slug,
+                    'pk': obj.uploader_id,
+                }
+            )
         else:
         else:
             return None
             return None

+ 4 - 16
misago/threads/serializers/feed.py

@@ -1,7 +1,5 @@
 from rest_framework import serializers
 from rest_framework import serializers
 
 
-from django.urls import reverse
-
 from misago.categories.serializers import CategorySerializer
 from misago.categories.serializers import CategorySerializer
 from misago.core.serializers import MutableFields
 from misago.core.serializers import MutableFields
 from misago.threads.models import Post
 from misago.threads.models import Post
@@ -14,14 +12,9 @@ __all__ = [
     'FeedSerializer',
     'FeedSerializer',
 ]
 ]
 
 
+FeedUserSerializer = UserSerializer.subset_fields('id', 'username', 'avatars', 'absolute_url')
 
 
-
-FeedUserSerializer = UserSerializer.subset_fields(
-    'id', 'username', 'avatars', 'absolute_url')
-
-
-FeedCategorySerializer = CategorySerializer.subset_fields(
-    'name', 'css_class', 'absolute_url')
+FeedCategorySerializer = CategorySerializer.subset_fields('name', 'css_class', 'absolute_url')
 
 
 
 
 class FeedSerializer(PostSerializer, MutableFields):
 class FeedSerializer(PostSerializer, MutableFields):
@@ -33,17 +26,12 @@ class FeedSerializer(PostSerializer, MutableFields):
 
 
     class Meta:
     class Meta:
         model = Post
         model = Post
-        fields = PostSerializer.Meta.fields + [
-            'category',
-
-            'thread',
-            'top_category'
-        ]
+        fields = PostSerializer.Meta.fields + ['category', 'thread', 'top_category']
 
 
     def get_thread(self, obj):
     def get_thread(self, obj):
         return {
         return {
             'title': obj.thread.title,
             'title': obj.thread.title,
-            'url': obj.thread.get_absolute_url()
+            'url': obj.thread.get_absolute_url(),
         }
         }
 
 
     def get_top_category(self, obj):
     def get_top_category(self, obj):

+ 12 - 6
misago/threads/serializers/moderation.py

@@ -43,20 +43,24 @@ class NewThreadSerializer(serializers.Serializer):
         try:
         try:
             add_acl(self.context, self.category)
             add_acl(self.context, self.category)
         except AttributeError:
         except AttributeError:
-            return weight # don't validate weight further if category failed
+            return weight  # don't validate weight further if category failed
 
 
         if weight > self.category.acl.get('can_pin_threads', 0):
         if weight > self.category.acl.get('can_pin_threads', 0):
             if weight == 2:
             if weight == 2:
-                raise ValidationError(_("You don't have permission to pin threads globally in this category."))
+                raise ValidationError(
+                    _("You don't have permission to pin threads globally in this category.")
+                )
             else:
             else:
-                raise ValidationError(_("You don't have permission to pin threads in this category."))
+                raise ValidationError(
+                    _("You don't have permission to pin threads in this category.")
+                )
         return weight
         return weight
 
 
     def validate_is_hidden(self, is_hidden):
     def validate_is_hidden(self, is_hidden):
         try:
         try:
             add_acl(self.context, self.category)
             add_acl(self.context, self.category)
         except AttributeError:
         except AttributeError:
-            return is_hidden # don't validate hidden further if category failed
+            return is_hidden  # don't validate hidden further if category failed
 
 
         if is_hidden and not self.category.acl.get('can_hide_threads'):
         if is_hidden and not self.category.acl.get('can_hide_threads'):
             raise ValidationError(_("You don't have permission to hide threads in this category."))
             raise ValidationError(_("You don't have permission to hide threads in this category."))
@@ -66,8 +70,10 @@ class NewThreadSerializer(serializers.Serializer):
         try:
         try:
             add_acl(self.context, self.category)
             add_acl(self.context, self.category)
         except AttributeError:
         except AttributeError:
-            return is_closed # don't validate closed further if category failed
+            return is_closed  # don't validate closed further if category failed
 
 
         if is_closed and not self.category.acl.get('can_close_threads'):
         if is_closed and not self.category.acl.get('can_close_threads'):
-            raise ValidationError(_("You don't have permission to close threads in this category."))
+            raise ValidationError(
+                _("You don't have permission to close threads in this category.")
+            )
         return is_closed
         return is_closed

+ 27 - 25
misago/threads/serializers/poll.py

@@ -27,7 +27,7 @@ class PollSerializer(serializers.ModelSerializer):
 
 
     class Meta:
     class Meta:
         model = Poll
         model = Poll
-        fields = (
+        fields = [
             'id',
             'id',
             'poster_name',
             'poster_name',
             'posted_on',
             'posted_on',
@@ -37,13 +37,11 @@ class PollSerializer(serializers.ModelSerializer):
             'allow_revotes',
             'allow_revotes',
             'votes',
             'votes',
             'is_public',
             'is_public',
-
             'acl',
             'acl',
             'choices',
             'choices',
-
             'api',
             'api',
             'url',
             'url',
-        )
+        ]
 
 
     def get_api(self, obj):
     def get_api(self, obj):
         return {
         return {
@@ -58,10 +56,12 @@ class PollSerializer(serializers.ModelSerializer):
 
 
     def get_poster_url(self, obj):
     def get_poster_url(self, obj):
         if obj.poster_id:
         if obj.poster_id:
-            return reverse('misago:user', kwargs={
-                'slug': obj.poster_slug,
-                'pk': obj.poster_id,
-            })
+            return reverse(
+                'misago:user', kwargs={
+                    'slug': obj.poster_slug,
+                    'pk': obj.poster_id,
+                }
+            )
         else:
         else:
             return None
             return None
 
 
@@ -80,19 +80,19 @@ class EditPollSerializer(serializers.ModelSerializer):
     question = serializers.CharField(required=True, max_length=255)
     question = serializers.CharField(required=True, max_length=255)
     allowed_choices = serializers.IntegerField(required=True, min_value=1)
     allowed_choices = serializers.IntegerField(required=True, min_value=1)
     choices = serializers.ListField(
     choices = serializers.ListField(
-       allow_empty=False,
-       child=serializers.DictField(),
+        allow_empty=False,
+        child=serializers.DictField(),
     )
     )
 
 
     class Meta:
     class Meta:
         model = Poll
         model = Poll
-        fields = (
+        fields = [
             'length',
             'length',
             'question',
             'question',
             'allowed_choices',
             'allowed_choices',
             'allow_revotes',
             'allow_revotes',
             'choices',
             'choices',
-        )
+        ]
 
 
     def validate_choices(self, choices):
     def validate_choices(self, choices):
         clean_choices = list(map(self.clean_choice, choices))
         clean_choices = list(map(self.clean_choice, choices))
@@ -105,14 +105,12 @@ class EditPollSerializer(serializers.ModelSerializer):
         final_choices = []
         final_choices = []
         for choice in clean_choices:
         for choice in clean_choices:
             if choice['hash'] in choices_map:
             if choice['hash'] in choices_map:
-                choices_map[choice['hash']].update({
-                    'label': choice['label']
-                })
+                choices_map[choice['hash']].update({'label': choice['label']})
                 final_choices.append(choices_map[choice['hash']])
                 final_choices.append(choices_map[choice['hash']])
             else:
             else:
                 choice.update({
                 choice.update({
                     'hash': get_random_string(12),
                     'hash': get_random_string(12),
-                    'votes': 0
+                    'votes': 0,
                 })
                 })
                 final_choices.append(choice)
                 final_choices.append(choice)
 
 
@@ -142,16 +140,20 @@ class EditPollSerializer(serializers.ModelSerializer):
             message = ungettext(
             message = ungettext(
                 "You can't add more than %(limit_value)s option to a single poll (added %(show_value)s).",
                 "You can't add more than %(limit_value)s option to a single poll (added %(show_value)s).",
                 "You can't add more than %(limit_value)s options to a single poll (added %(show_value)s).",
                 "You can't add more than %(limit_value)s options to a single poll (added %(show_value)s).",
-                MAX_POLL_OPTIONS)
-            raise serializers.ValidationError(message % {
-                'limit_value': MAX_POLL_OPTIONS,
-                'show_value': total_choices
-            })
+                MAX_POLL_OPTIONS,
+            )
+            raise serializers.ValidationError(
+                message % {
+                    'limit_value': MAX_POLL_OPTIONS,
+                    'show_value': total_choices,
+                }
+            )
 
 
     def validate(self, data):
     def validate(self, data):
         if data['allowed_choices'] > len(data['choices']):
         if data['allowed_choices'] > len(data['choices']):
             raise serializers.ValidationError(
             raise serializers.ValidationError(
-                _("Number of allowed choices can't be greater than number of all choices."))
+                _("Number of allowed choices can't be greater than number of all choices.")
+            )
         return data
         return data
 
 
     def update(self, instance, validated_data):
     def update(self, instance, validated_data):
@@ -176,14 +178,14 @@ class EditPollSerializer(serializers.ModelSerializer):
 class NewPollSerializer(EditPollSerializer):
 class NewPollSerializer(EditPollSerializer):
     class Meta:
     class Meta:
         model = Poll
         model = Poll
-        fields = (
+        fields = [
             'length',
             'length',
             'question',
             'question',
             'allowed_choices',
             'allowed_choices',
             'allow_revotes',
             'allow_revotes',
             'is_public',
             'is_public',
             'choices',
             'choices',
-        )
+        ]
 
 
     def validate_choices(self, choices):
     def validate_choices(self, choices):
         clean_choices = list(map(self.clean_choice, choices))
         clean_choices = list(map(self.clean_choice, choices))
@@ -193,7 +195,7 @@ class NewPollSerializer(EditPollSerializer):
         for choice in clean_choices:
         for choice in clean_choices:
             choice.update({
             choice.update({
                 'hash': get_random_string(12),
                 'hash': get_random_string(12),
-                'votes': 0
+                'votes': 0,
             })
             })
 
 
         return clean_choices
         return clean_choices

+ 8 - 8
misago/threads/serializers/pollvote.py

@@ -13,20 +13,20 @@ class PollVoteSerializer(serializers.Serializer):
     url = serializers.SerializerMethodField()
     url = serializers.SerializerMethodField()
 
 
     class Meta:
     class Meta:
-        fields = (
+        fields = [
             'voted_on',
             'voted_on',
-
             'username',
             'username',
-
             'url',
             'url',
-        )
+        ]
 
 
     def get_username(self, obj):
     def get_username(self, obj):
         return obj['voter_name']
         return obj['voter_name']
 
 
     def get_url(self, obj):
     def get_url(self, obj):
         if obj['voter_id']:
         if obj['voter_id']:
-            return reverse('misago:user', kwargs={
-                'pk': obj['voter_id'],
-                'slug': obj['voter_slug'],
-            })
+            return reverse(
+                'misago:user', kwargs={
+                    'pk': obj['voter_id'],
+                    'slug': obj['voter_slug'],
+                }
+            )

+ 14 - 13
misago/threads/serializers/post.py

@@ -2,7 +2,6 @@ from rest_framework import serializers
 
 
 from django.urls import reverse
 from django.urls import reverse
 
 
-from misago.categories.serializers import CategorySerializer
 from misago.core.serializers import MutableFields
 from misago.core.serializers import MutableFields
 from misago.threads.models import Post
 from misago.threads.models import Post
 from misago.users.serializers import UserSerializer as BaseUserSerializer
 from misago.users.serializers import UserSerializer as BaseUserSerializer
@@ -10,9 +9,9 @@ from misago.users.serializers import UserSerializer as BaseUserSerializer
 
 
 __all__ = ['PostSerializer']
 __all__ = ['PostSerializer']
 
 
-
 UserSerializer = BaseUserSerializer.subset_fields(
 UserSerializer = BaseUserSerializer.subset_fields(
-    'id', 'username', 'rank', 'avatars', 'signature', 'title', 'status', 'absolute_url')
+    'id', 'username', 'rank', 'avatars', 'signature', 'title', 'status', 'absolute_url'
+)
 
 
 
 
 class PostSerializer(serializers.ModelSerializer, MutableFields):
 class PostSerializer(serializers.ModelSerializer, MutableFields):
@@ -58,14 +57,12 @@ class PostSerializer(serializers.ModelSerializer, MutableFields):
             'is_event',
             'is_event',
             'event_type',
             'event_type',
             'event_context',
             'event_context',
-
             'acl',
             'acl',
             'is_liked',
             'is_liked',
             'is_new',
             'is_new',
             'is_read',
             'is_read',
             'last_likes',
             'last_likes',
             'likes',
             'likes',
-
             'api',
             'api',
             'url',
             'url',
         ]
         ]
@@ -152,18 +149,22 @@ class PostSerializer(serializers.ModelSerializer, MutableFields):
 
 
     def get_last_editor_url(self, obj):
     def get_last_editor_url(self, obj):
         if obj.last_editor_id:
         if obj.last_editor_id:
-            return reverse('misago:user', kwargs={
-                'pk': obj.last_editor_id,
-                'slug': obj.last_editor_slug
-            })
+            return reverse(
+                'misago:user', kwargs={
+                    'pk': obj.last_editor_id,
+                    'slug': obj.last_editor_slug,
+                }
+            )
         else:
         else:
             return None
             return None
 
 
     def get_hidden_by_url(self, obj):
     def get_hidden_by_url(self, obj):
         if obj.hidden_by_id:
         if obj.hidden_by_id:
-            return reverse('misago:user', kwargs={
-                'pk': obj.hidden_by_id,
-                'slug': obj.hidden_by_slug
-            })
+            return reverse(
+                'misago:user', kwargs={
+                    'pk': obj.hidden_by_id,
+                    'slug': obj.hidden_by_slug,
+                }
+            )
         else:
         else:
             return None
             return None

+ 8 - 8
misago/threads/serializers/postedit.py

@@ -17,16 +17,14 @@ class PostEditSerializer(serializers.ModelSerializer):
 
 
     class Meta:
     class Meta:
         model = PostEdit
         model = PostEdit
-        fields = (
+        fields = [
             'id',
             'id',
             'edited_on',
             'edited_on',
             'editor_name',
             'editor_name',
             'editor_slug',
             'editor_slug',
-
             'diff',
             'diff',
-
             'url',
             'url',
-        )
+        ]
 
 
     def get_diff(self, obj):
     def get_diff(self, obj):
         return obj.get_diff()
         return obj.get_diff()
@@ -38,9 +36,11 @@ class PostEditSerializer(serializers.ModelSerializer):
 
 
     def get_editor_url(self, obj):
     def get_editor_url(self, obj):
         if obj.editor_id:
         if obj.editor_id:
-            return reverse('misago:user', kwargs={
-                'slug': obj.editor_slug,
-                'pk': obj.editor_id,
-            })
+            return reverse(
+                'misago:user', kwargs={
+                    'slug': obj.editor_slug,
+                    'pk': obj.editor_id,
+                }
+            )
         else:
         else:
             return None
             return None

+ 8 - 8
misago/threads/serializers/postlike.py

@@ -18,15 +18,13 @@ class PostLikeSerializer(serializers.ModelSerializer):
 
 
     class Meta:
     class Meta:
         model = PostLike
         model = PostLike
-        fields = (
+        fields = [
             'id',
             'id',
             'liked_on',
             'liked_on',
-
             'liker_id',
             'liker_id',
             'username',
             'username',
-
             'url',
             'url',
-        )
+        ]
 
 
     def get_liker_id(self, obj):
     def get_liker_id(self, obj):
         return obj['liker_id']
         return obj['liker_id']
@@ -36,9 +34,11 @@ class PostLikeSerializer(serializers.ModelSerializer):
 
 
     def get_url(self, obj):
     def get_url(self, obj):
         if obj['liker_id']:
         if obj['liker_id']:
-            return reverse('misago:user', kwargs={
-                'slug': obj['liker_slug'],
-                'pk': obj['liker_id'],
-            })
+            return reverse(
+                'misago:user', kwargs={
+                    'slug': obj['liker_slug'],
+                    'pk': obj['liker_id'],
+                }
+            )
         else:
         else:
             return None
             return None

+ 18 - 18
misago/threads/serializers/thread.py

@@ -16,10 +16,10 @@ __all__ = [
     'ThreadsListSerializer',
     'ThreadsListSerializer',
 ]
 ]
 
 
-
 BasicCategorySerializer = CategorySerializer.subset_fields(
 BasicCategorySerializer = CategorySerializer.subset_fields(
-    'id', 'parent', 'name', 'description', 'is_closed', 'css_class',
-    'absolute_url', 'api_url', 'level', 'lft', 'rght', 'is_read')
+    'id', 'parent', 'name', 'description', 'is_closed', 'css_class', 'absolute_url', 'api_url',
+    'level', 'lft', 'rght', 'is_read'
+)
 
 
 
 
 class ThreadSerializer(serializers.ModelSerializer, MutableFields):
 class ThreadSerializer(serializers.ModelSerializer, MutableFields):
@@ -37,7 +37,7 @@ class ThreadSerializer(serializers.ModelSerializer, MutableFields):
 
 
     class Meta:
     class Meta:
         model = Thread
         model = Thread
-        fields = (
+        fields = [
             'id',
             'id',
             'category',
             'category',
             'title',
             'title',
@@ -52,17 +52,15 @@ class ThreadSerializer(serializers.ModelSerializer, MutableFields):
             'is_hidden',
             'is_hidden',
             'is_closed',
             'is_closed',
             'weight',
             'weight',
-
             'acl',
             'acl',
             'is_new',
             'is_new',
             'is_read',
             'is_read',
             'path',
             'path',
             'poll',
             'poll',
             'subscription',
             'subscription',
-
             'api',
             'api',
             'url',
             'url',
-        )
+        ]
 
 
     def get_acl(self, obj):
     def get_acl(self, obj):
         try:
         try:
@@ -107,8 +105,8 @@ class ThreadSerializer(serializers.ModelSerializer, MutableFields):
                 'index': obj.get_posts_api_url(),
                 'index': obj.get_posts_api_url(),
                 'merge': obj.get_post_merge_api_url(),
                 'merge': obj.get_post_merge_api_url(),
                 'move': obj.get_post_move_api_url(),
                 'move': obj.get_post_move_api_url(),
-                'split': obj.get_post_split_api_url()
-            }
+                'split': obj.get_post_split_api_url(),
+            },
         }
         }
 
 
     def get_url(self, obj):
     def get_url(self, obj):
@@ -122,10 +120,12 @@ class ThreadSerializer(serializers.ModelSerializer, MutableFields):
 
 
     def get_last_poster_url(self, obj):
     def get_last_poster_url(self, obj):
         if obj.last_poster_id:
         if obj.last_poster_id:
-            return reverse('misago:user', kwargs={
-                'slug': obj.last_poster_slug,
-                'pk': obj.last_poster_id,
-            })
+            return reverse(
+                'misago:user', kwargs={
+                    'slug': obj.last_poster_slug,
+                    'pk': obj.last_poster_id,
+                }
+            )
         else:
         else:
             return None
             return None
 
 
@@ -135,9 +135,9 @@ class PrivateThreadSerializer(ThreadSerializer):
 
 
     class Meta:
     class Meta:
         model = Thread
         model = Thread
-        fields = ThreadSerializer.Meta.fields + (
+        fields = ThreadSerializer.Meta.fields + [
             'participants',
             'participants',
-        )
+        ]
 
 
 
 
 class ThreadsListSerializer(ThreadSerializer):
 class ThreadsListSerializer(ThreadSerializer):
@@ -148,7 +148,7 @@ class ThreadsListSerializer(ThreadSerializer):
 
 
     class Meta:
     class Meta:
         model = Thread
         model = Thread
-        fields = ThreadSerializer.Meta.fields + (
-            'has_poll', 'top_category'
-        )
+        fields = ThreadSerializer.Meta.fields + ['has_poll', 'top_category']
+
+
 ThreadsListSerializer = ThreadsListSerializer.exclude_fields('path', 'poll')
 ThreadsListSerializer = ThreadsListSerializer.exclude_fields('path', 'poll')

+ 1 - 7
misago/threads/serializers/threadparticipant.py

@@ -15,13 +15,7 @@ class ThreadParticipantSerializer(serializers.ModelSerializer):
 
 
     class Meta:
     class Meta:
         model = ThreadParticipant
         model = ThreadParticipant
-        fields = (
-            'id',
-            'username',
-            'avatars',
-            'url',
-            'is_owner'
-        )
+        fields = ['id', 'username', 'avatars', 'url', 'is_owner']
 
 
     def get_id(self, obj):
     def get_id(self, obj):
         return obj.user.id
         return obj.user.id

+ 15 - 13
misago/threads/signals.py

@@ -19,13 +19,13 @@ move_post = Signal()
 move_thread = Signal()
 move_thread = Signal()
 
 
 
 
-"""
-Signal handlers
-"""
 @receiver(merge_thread)
 @receiver(merge_thread)
 def merge_threads_posts(sender, **kwargs):
 def merge_threads_posts(sender, **kwargs):
     other_thread = kwargs['other_thread']
     other_thread = kwargs['other_thread']
-    other_thread.post_set.update(category=sender.category, thread=sender)
+    other_thread.post_set.update(
+        category=sender.category,
+        thread=sender,
+    )
 
 
 
 
 @receiver(merge_post)
 @receiver(merge_post)
@@ -102,44 +102,46 @@ def delete_user_threads(sender, **kwargs):
 def update_usernames(sender, **kwargs):
 def update_usernames(sender, **kwargs):
     Thread.objects.filter(starter=sender).update(
     Thread.objects.filter(starter=sender).update(
         starter_name=sender.username,
         starter_name=sender.username,
-        starter_slug=sender.slug
+        starter_slug=sender.slug,
     )
     )
 
 
     Thread.objects.filter(last_poster=sender).update(
     Thread.objects.filter(last_poster=sender).update(
         last_poster_name=sender.username,
         last_poster_name=sender.username,
-        last_poster_slug=sender.slug
+        last_poster_slug=sender.slug,
     )
     )
 
 
-    Post.objects.filter(poster=sender).update(poster_name=sender.username)
+    Post.objects.filter(poster=sender).update(
+        poster_name=sender.username,
+    )
 
 
     Post.objects.filter(last_editor=sender).update(
     Post.objects.filter(last_editor=sender).update(
         last_editor_name=sender.username,
         last_editor_name=sender.username,
-        last_editor_slug=sender.slug
+        last_editor_slug=sender.slug,
     )
     )
 
 
     PostEdit.objects.filter(editor=sender).update(
     PostEdit.objects.filter(editor=sender).update(
         editor_name=sender.username,
         editor_name=sender.username,
-        editor_slug=sender.slug
+        editor_slug=sender.slug,
     )
     )
 
 
     PostLike.objects.filter(liker=sender).update(
     PostLike.objects.filter(liker=sender).update(
         liker_name=sender.username,
         liker_name=sender.username,
-        liker_slug=sender.slug
+        liker_slug=sender.slug,
     )
     )
 
 
     Attachment.objects.filter(uploader=sender).update(
     Attachment.objects.filter(uploader=sender).update(
         uploader_name=sender.username,
         uploader_name=sender.username,
-        uploader_slug=sender.slug
+        uploader_slug=sender.slug,
     )
     )
 
 
     Poll.objects.filter(poster=sender).update(
     Poll.objects.filter(poster=sender).update(
         poster_name=sender.username,
         poster_name=sender.username,
-        poster_slug=sender.slug
+        poster_slug=sender.slug,
     )
     )
 
 
     PollVote.objects.filter(voter=sender).update(
     PollVote.objects.filter(voter=sender).update(
         voter_name=sender.username,
         voter_name=sender.username,
-        voter_slug=sender.slug
+        voter_slug=sender.slug,
     )
     )
 
 
 
 

+ 1 - 1
misago/threads/subscriptions.py

@@ -22,7 +22,7 @@ def make_threads_subscription_aware(user, threads):
             threads_dict[thread.pk] = thread
             threads_dict[thread.pk] = thread
 
 
         subscriptions_queryset = user.subscription_set.filter(
         subscriptions_queryset = user.subscription_set.filter(
-            thread_id__in=threads_dict.keys()
+            thread_id__in=threads_dict.keys(),
         )
         )
 
 
         for subscription in subscriptions_queryset.iterator():
         for subscription in subscriptions_queryset.iterator():

+ 3 - 11
misago/threads/templatetags/misago_poststags.py

@@ -4,8 +4,6 @@ from django import template
 from django.utils.translation import gettext as _
 from django.utils.translation import gettext as _
 from django.utils.translation import ngettext
 from django.utils.translation import ngettext
 
 
-from misago.conf import settings
-
 
 
 register = template.Library()
 register = template.Library()
 
 
@@ -30,22 +28,16 @@ def likes_label(post):
     if not hidden_likes:
     if not hidden_likes:
         return _("%(users)s like this.") % {'users': usernames_string}
         return _("%(users)s like this.") % {'users': usernames_string}
 
 
-    formats = {
-        'users': usernames_string,
-        'likes': hidden_likes
-    }
+    formats = {'users': usernames_string, 'likes': hidden_likes}
 
 
     return ngettext(
     return ngettext(
         "%(users)s and %(likes)s other user like this.",
         "%(users)s and %(likes)s other user like this.",
         "%(users)s and %(likes)s other users like this.",
         "%(users)s and %(likes)s other users like this.",
-        hidden_likes
+        hidden_likes,
     ) % formats
     ) % formats
 
 
 
 
 def humanize_usernames_list(usernames):
 def humanize_usernames_list(usernames):
-    formats = {
-        'users': ', '.join(usernames[:-1]),
-        'last_user': usernames[-1]
-    }
+    formats = {'users': ', '.join(usernames[:-1]), 'last_user': usernames[-1]}
 
 
     return _("%(users)s and %(last_user)s") % formats
     return _("%(users)s and %(last_user)s") % formats

+ 48 - 19
misago/threads/tests/test_attachmentadmin_views.py

@@ -11,9 +11,7 @@ class AttachmentAdminViewsTests(AdminTestCase):
         super(AttachmentAdminViewsTests, self).setUp()
         super(AttachmentAdminViewsTests, self).setUp()
 
 
         self.category = Category.objects.get(slug='first-category')
         self.category = Category.objects.get(slug='first-category')
-        self.post = testutils.post_thread(
-            category=self.category
-        ).first_post
+        self.post = testutils.post_thread(category=self.category).first_post
 
 
         self.filetype = AttachmentType.objects.order_by('id').first()
         self.filetype = AttachmentType.objects.order_by('id').first()
 
 
@@ -32,7 +30,7 @@ class AttachmentAdminViewsTests(AdminTestCase):
             filename='testfile_{}.zip'.format(Attachment.objects.count() + 1),
             filename='testfile_{}.zip'.format(Attachment.objects.count() + 1),
             file=None,
             file=None,
             image=None,
             image=None,
-            thumbnail=None
+            thumbnail=None,
         )
         )
 
 
     def test_link_registered(self):
     def test_link_registered(self):
@@ -50,14 +48,22 @@ class AttachmentAdminViewsTests(AdminTestCase):
         attachments = [
         attachments = [
             self.mock_attachment(self.post, file='somefile.pdf'),
             self.mock_attachment(self.post, file='somefile.pdf'),
             self.mock_attachment(image='someimage.jpg'),
             self.mock_attachment(image='someimage.jpg'),
-            self.mock_attachment(self.post, image='somelargeimage.png', thumbnail='somethumb.png'),
+            self.mock_attachment(
+                self.post,
+                image='somelargeimage.png',
+                thumbnail='somethumb.png',
+            ),
         ]
         ]
 
 
         response = self.client.get(final_link)
         response = self.client.get(final_link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         for attachment in attachments:
         for attachment in attachments:
-            delete_link = reverse('misago:admin:system:attachments:delete', kwargs={'pk': attachment.pk})
+            delete_link = reverse(
+                'misago:admin:system:attachments:delete', kwargs={
+                    'pk': attachment.pk,
+                }
+            )
             self.assertContains(response, attachment.filename)
             self.assertContains(response, attachment.filename)
             self.assertContains(response, delete_link)
             self.assertContains(response, delete_link)
             self.assertContains(response, attachment.get_absolute_url())
             self.assertContains(response, attachment.get_absolute_url())
@@ -72,16 +78,23 @@ class AttachmentAdminViewsTests(AdminTestCase):
         attachments = [
         attachments = [
             self.mock_attachment(self.post, file='somefile.pdf'),
             self.mock_attachment(self.post, file='somefile.pdf'),
             self.mock_attachment(image='someimage.jpg'),
             self.mock_attachment(image='someimage.jpg'),
-            self.mock_attachment(self.post, image='somelargeimage.png', thumbnail='somethumb.png'),
+            self.mock_attachment(
+                self.post,
+                image='somelargeimage.png',
+                thumbnail='somethumb.png',
+            ),
         ]
         ]
 
 
         self.post.attachments_cache = [{'id': attachments[-1].pk}]
         self.post.attachments_cache = [{'id': attachments[-1].pk}]
         self.post.save()
         self.post.save()
 
 
-        response = self.client.post(self.admin_link, data={
-            'action': 'delete',
-            'selected_items': [a.pk for a in attachments]
-        })
+        response = self.client.post(
+            self.admin_link,
+            data={
+                'action': 'delete',
+                'selected_items': [a.pk for a in attachments],
+            }
+        )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
         self.assertEqual(Attachment.objects.count(), 0)
         self.assertEqual(Attachment.objects.count(), 0)
@@ -94,13 +107,23 @@ class AttachmentAdminViewsTests(AdminTestCase):
         """delete attachment view has no showstoppers"""
         """delete attachment view has no showstoppers"""
         attachment = self.mock_attachment(self.post)
         attachment = self.mock_attachment(self.post)
         self.post.attachments_cache = [
         self.post.attachments_cache = [
-            {'id': attachment.pk + 1},
-            {'id': attachment.pk},
-            {'id': attachment.pk + 2}
+            {
+                'id': attachment.pk + 1
+            },
+            {
+                'id': attachment.pk
+            },
+            {
+                'id': attachment.pk + 2
+            },
         ]
         ]
         self.post.save()
         self.post.save()
 
 
-        action_link = reverse('misago:admin:system:attachments:delete', kwargs={'pk': attachment.pk})
+        action_link = reverse(
+            'misago:admin:system:attachments:delete', kwargs={
+                'pk': attachment.pk,
+            }
+        )
 
 
         response = self.client.post(action_link)
         response = self.client.post(action_link)
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
@@ -114,7 +137,13 @@ class AttachmentAdminViewsTests(AdminTestCase):
 
 
         # assert it was removed from post's attachments cache
         # assert it was removed from post's attachments cache
         attachments_cache = self.category.post_set.get(pk=self.post.pk).attachments_cache
         attachments_cache = self.category.post_set.get(pk=self.post.pk).attachments_cache
-        self.assertEqual(attachments_cache, [
-            {'id': attachment.pk + 1},
-            {'id': attachment.pk + 2}
-        ])
+        self.assertEqual(
+            attachments_cache, [
+                {
+                    'id': attachment.pk + 1,
+                },
+                {
+                    'id': attachment.pk + 2,
+                },
+            ]
+        )

+ 90 - 72
misago/threads/tests/test_attachments_api.py

@@ -1,11 +1,9 @@
-import json
 import os
 import os
 
 
 from PIL import Image
 from PIL import Image
 
 
 from django.urls import reverse
 from django.urls import reverse
 from django.utils import six
 from django.utils import six
-from django.utils.encoding import smart_str
 
 
 from misago.acl.models import Role
 from misago.acl.models import Role
 from misago.acl.testutils import override_acl
 from misago.acl.testutils import override_acl
@@ -45,9 +43,7 @@ class AttachmentsApiTestCase(AuthenticatedUserTestCase):
 
 
     def test_no_permission(self):
     def test_no_permission(self):
         """user needs permission to upload files"""
         """user needs permission to upload files"""
-        self.override_acl({
-            'max_attachment_size': 0
-        })
+        self.override_acl({'max_attachment_size': 0})
 
 
         response = self.client.post(self.api_link)
         response = self.client.post(self.api_link)
         self.assertContains(response, "don't have permission to upload new files", status_code=403)
         self.assertContains(response, "don't have permission to upload new files", status_code=403)
@@ -62,13 +58,15 @@ class AttachmentsApiTestCase(AuthenticatedUserTestCase):
         AttachmentType.objects.create(
         AttachmentType.objects.create(
             name="Test extension",
             name="Test extension",
             extensions='jpg,jpeg',
             extensions='jpg,jpeg',
-            mimetypes=None
+            mimetypes=None,
         )
         )
 
 
         with open(TEST_DOCUMENT_PATH, 'rb') as upload:
         with open(TEST_DOCUMENT_PATH, 'rb') as upload:
-            response = self.client.post(self.api_link, data={
-                'upload': upload
-            })
+            response = self.client.post(
+                self.api_link, data={
+                    'upload': upload,
+                }
+            )
         self.assertContains(response, "You can't upload files of this type.", status_code=400)
         self.assertContains(response, "You can't upload files of this type.", status_code=400)
 
 
     def test_invalid_mime(self):
     def test_invalid_mime(self):
@@ -76,13 +74,15 @@ class AttachmentsApiTestCase(AuthenticatedUserTestCase):
         AttachmentType.objects.create(
         AttachmentType.objects.create(
             name="Test extension",
             name="Test extension",
             extensions='png',
             extensions='png',
-            mimetypes='loremipsum'
+            mimetypes='loremipsum',
         )
         )
 
 
         with open(TEST_DOCUMENT_PATH, 'rb') as upload:
         with open(TEST_DOCUMENT_PATH, 'rb') as upload:
-            response = self.client.post(self.api_link, data={
-                'upload': upload
-            })
+            response = self.client.post(
+                self.api_link, data={
+                    'upload': upload,
+                }
+            )
         self.assertContains(response, "You can't upload files of this type.", status_code=400)
         self.assertContains(response, "You can't upload files of this type.", status_code=400)
 
 
     def test_no_perm_to_type(self):
     def test_no_perm_to_type(self):
@@ -90,46 +90,52 @@ class AttachmentsApiTestCase(AuthenticatedUserTestCase):
         attachment_type = AttachmentType.objects.create(
         attachment_type = AttachmentType.objects.create(
             name="Test extension",
             name="Test extension",
             extensions='png',
             extensions='png',
-            mimetypes='application/pdf'
+            mimetypes='application/pdf',
         )
         )
 
 
         user_roles = (r.pk for r in self.user.get_roles())
         user_roles = (r.pk for r in self.user.get_roles())
         attachment_type.limit_uploads_to.set(Role.objects.exclude(id__in=user_roles))
         attachment_type.limit_uploads_to.set(Role.objects.exclude(id__in=user_roles))
 
 
         with open(TEST_DOCUMENT_PATH, 'rb') as upload:
         with open(TEST_DOCUMENT_PATH, 'rb') as upload:
-            response = self.client.post(self.api_link, data={
-                'upload': upload
-            })
+            response = self.client.post(
+                self.api_link, data={
+                    'upload': upload,
+                }
+            )
         self.assertContains(response, "You can't upload files of this type.", status_code=400)
         self.assertContains(response, "You can't upload files of this type.", status_code=400)
 
 
     def test_type_is_locked(self):
     def test_type_is_locked(self):
         """new uploads for this filetype are locked"""
         """new uploads for this filetype are locked"""
-        attachment_type = AttachmentType.objects.create(
+        AttachmentType.objects.create(
             name="Test extension",
             name="Test extension",
             extensions='png',
             extensions='png',
             mimetypes='application/pdf',
             mimetypes='application/pdf',
-            status=AttachmentType.LOCKED
+            status=AttachmentType.LOCKED,
         )
         )
 
 
         with open(TEST_DOCUMENT_PATH, 'rb') as upload:
         with open(TEST_DOCUMENT_PATH, 'rb') as upload:
-            response = self.client.post(self.api_link, data={
-                'upload': upload
-            })
+            response = self.client.post(
+                self.api_link, data={
+                    'upload': upload,
+                }
+            )
         self.assertContains(response, "You can't upload files of this type.", status_code=400)
         self.assertContains(response, "You can't upload files of this type.", status_code=400)
 
 
     def test_type_is_disabled(self):
     def test_type_is_disabled(self):
         """new uploads for this filetype are disabled"""
         """new uploads for this filetype are disabled"""
-        attachment_type = AttachmentType.objects.create(
+        AttachmentType.objects.create(
             name="Test extension",
             name="Test extension",
             extensions='png',
             extensions='png',
             mimetypes='application/pdf',
             mimetypes='application/pdf',
-            status=AttachmentType.DISABLED
+            status=AttachmentType.DISABLED,
         )
         )
 
 
         with open(TEST_DOCUMENT_PATH, 'rb') as upload:
         with open(TEST_DOCUMENT_PATH, 'rb') as upload:
-            response = self.client.post(self.api_link, data={
-                'upload': upload
-            })
+            response = self.client.post(
+                self.api_link, data={
+                    'upload': upload,
+                }
+            )
         self.assertContains(response, "You can't upload files of this type.", status_code=400)
         self.assertContains(response, "You can't upload files of this type.", status_code=400)
 
 
     def test_upload_too_big_for_type(self):
     def test_upload_too_big_for_type(self):
@@ -138,62 +144,70 @@ class AttachmentsApiTestCase(AuthenticatedUserTestCase):
             name="Test extension",
             name="Test extension",
             extensions='png',
             extensions='png',
             mimetypes='image/png',
             mimetypes='image/png',
-            size_limit=100
+            size_limit=100,
         )
         )
 
 
         with open(TEST_LARGEPNG_PATH, 'rb') as upload:
         with open(TEST_LARGEPNG_PATH, 'rb') as upload:
-            response = self.client.post(self.api_link, data={
-                'upload': upload
-            })
-
-        self.assertContains(response, "can't upload files of this type larger than", status_code=400)
+            response = self.client.post(
+                self.api_link, data={
+                    'upload': upload,
+                }
+            )
+
+        self.assertContains(
+            response, "can't upload files of this type larger than", status_code=400
+        )
 
 
     def test_upload_too_big_for_user(self):
     def test_upload_too_big_for_user(self):
         """too big uploads are rejected"""
         """too big uploads are rejected"""
-        self.override_acl({
-            'max_attachment_size': 100
-        })
+        self.override_acl({'max_attachment_size': 100})
 
 
         AttachmentType.objects.create(
         AttachmentType.objects.create(
             name="Test extension",
             name="Test extension",
             extensions='png',
             extensions='png',
-            mimetypes='image/png'
+            mimetypes='image/png',
         )
         )
 
 
         with open(TEST_LARGEPNG_PATH, 'rb') as upload:
         with open(TEST_LARGEPNG_PATH, 'rb') as upload:
-            response = self.client.post(self.api_link, data={
-                'upload': upload
-            })
+            response = self.client.post(
+                self.api_link, data={
+                    'upload': upload,
+                }
+            )
         self.assertContains(response, "can't upload files larger than", status_code=400)
         self.assertContains(response, "can't upload files larger than", status_code=400)
 
 
     def test_corrupted_image_upload(self):
     def test_corrupted_image_upload(self):
         """corrupted image upload is handled"""
         """corrupted image upload is handled"""
-        attachment_type = AttachmentType.objects.create(
+        AttachmentType.objects.create(
             name="Test extension",
             name="Test extension",
-            extensions='gif'
+            extensions='gif',
         )
         )
 
 
         with open(TEST_CORRUPTEDIMG_PATH, 'rb') as upload:
         with open(TEST_CORRUPTEDIMG_PATH, 'rb') as upload:
-            response = self.client.post(self.api_link, data={
-                'upload': upload
-            })
+            response = self.client.post(
+                self.api_link, data={
+                    'upload': upload,
+                }
+            )
         self.assertContains(response, "Uploaded image was corrupted or invalid.", status_code=400)
         self.assertContains(response, "Uploaded image was corrupted or invalid.", status_code=400)
 
 
     def test_document_upload(self):
     def test_document_upload(self):
         """successful upload creates orphan attachment"""
         """successful upload creates orphan attachment"""
-        attachment_type = AttachmentType.objects.create(
+        AttachmentType.objects.create(
             name="Test extension",
             name="Test extension",
             extensions='pdf',
             extensions='pdf',
-            mimetypes='application/pdf'
+            mimetypes='application/pdf',
         )
         )
 
 
         with open(TEST_DOCUMENT_PATH, 'rb') as upload:
         with open(TEST_DOCUMENT_PATH, 'rb') as upload:
-            response = self.client.post(self.api_link, data={
-                'upload': upload
-            })
+            response = self.client.post(
+                self.api_link, data={
+                    'upload': upload,
+                }
+            )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response_json = json.loads(smart_str(response.content))
+        response_json = response.json()
         attachment = Attachment.objects.get(id=response_json['id'])
         attachment = Attachment.objects.get(id=response_json['id'])
 
 
         self.assertEqual(attachment.filename, 'document.pdf')
         self.assertEqual(attachment.filename, 'document.pdf')
@@ -220,19 +234,21 @@ class AttachmentsApiTestCase(AuthenticatedUserTestCase):
 
 
     def test_small_image_upload(self):
     def test_small_image_upload(self):
         """successful small image upload creates orphan attachment without thumbnail"""
         """successful small image upload creates orphan attachment without thumbnail"""
-        attachment_type = AttachmentType.objects.create(
+        AttachmentType.objects.create(
             name="Test extension",
             name="Test extension",
             extensions='jpeg,jpg',
             extensions='jpeg,jpg',
-            mimetypes='image/jpeg'
+            mimetypes='image/jpeg',
         )
         )
 
 
         with open(TEST_SMALLJPG_PATH, 'rb') as upload:
         with open(TEST_SMALLJPG_PATH, 'rb') as upload:
-            response = self.client.post(self.api_link, data={
-                'upload': upload
-            })
+            response = self.client.post(
+                self.api_link, data={
+                    'upload': upload,
+                }
+            )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response_json = json.loads(smart_str(response.content))
+        response_json = response.json()
         attachment = Attachment.objects.get(id=response_json['id'])
         attachment = Attachment.objects.get(id=response_json['id'])
 
 
         self.assertEqual(attachment.filename, 'small.jpg')
         self.assertEqual(attachment.filename, 'small.jpg')
@@ -253,23 +269,23 @@ class AttachmentsApiTestCase(AuthenticatedUserTestCase):
 
 
     def test_large_image_upload(self):
     def test_large_image_upload(self):
         """successful large image upload creates orphan attachment with thumbnail"""
         """successful large image upload creates orphan attachment with thumbnail"""
-        self.override_acl({
-            'max_attachment_size': 10 * 1024
-        })
+        self.override_acl({'max_attachment_size': 10 * 1024})
 
 
-        attachment_type = AttachmentType.objects.create(
+        AttachmentType.objects.create(
             name="Test extension",
             name="Test extension",
             extensions='png',
             extensions='png',
-            mimetypes='image/png'
+            mimetypes='image/png',
         )
         )
 
 
         with open(TEST_LARGEPNG_PATH, 'rb') as upload:
         with open(TEST_LARGEPNG_PATH, 'rb') as upload:
-            response = self.client.post(self.api_link, data={
-                'upload': upload
-            })
+            response = self.client.post(
+                self.api_link, data={
+                    'upload': upload,
+                }
+            )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response_json = json.loads(smart_str(response.content))
+        response_json = response.json()
         attachment = Attachment.objects.get(id=response_json['id'])
         attachment = Attachment.objects.get(id=response_json['id'])
 
 
         self.assertEqual(attachment.filename, 'large.png')
         self.assertEqual(attachment.filename, 'large.png')
@@ -308,19 +324,21 @@ class AttachmentsApiTestCase(AuthenticatedUserTestCase):
 
 
     def test_animated_image_upload(self):
     def test_animated_image_upload(self):
         """successful gif upload creates orphan attachment with thumbnail"""
         """successful gif upload creates orphan attachment with thumbnail"""
-        attachment_type = AttachmentType.objects.create(
+        AttachmentType.objects.create(
             name="Test extension",
             name="Test extension",
             extensions='gif',
             extensions='gif',
-            mimetypes='image/gif'
+            mimetypes='image/gif',
         )
         )
 
 
         with open(TEST_ANIMATEDGIF_PATH, 'rb') as upload:
         with open(TEST_ANIMATEDGIF_PATH, 'rb') as upload:
-            response = self.client.post(self.api_link, data={
-                'upload': upload
-            })
+            response = self.client.post(
+                self.api_link, data={
+                    'upload': upload,
+                }
+            )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response_json = json.loads(smart_str(response.content))
+        response_json = response.json()
         attachment = Attachment.objects.get(id=response_json['id'])
         attachment = Attachment.objects.get(id=response_json['id'])
 
 
         self.assertEqual(attachment.filename, 'animated.gif')
         self.assertEqual(attachment.filename, 'animated.gif')

+ 28 - 34
misago/threads/tests/test_attachments_middleware.py

@@ -30,9 +30,7 @@ class AttachmentsMiddlewareTests(AuthenticatedUserTestCase):
         self.filetype = AttachmentType.objects.order_by('id').last()
         self.filetype = AttachmentType.objects.order_by('id').last()
 
 
     def override_acl(self, new_acl=None):
     def override_acl(self, new_acl=None):
-        override_acl(self.user, new_acl or {
-            'max_attachment_size': 1024
-        })
+        override_acl(self.user, new_acl or {'max_attachment_size': 1024})
 
 
     def mock_attachment(self, user=True, post=None):
     def mock_attachment(self, user=True, post=None):
         return Attachment.objects.create(
         return Attachment.objects.create(
@@ -51,31 +49,24 @@ class AttachmentsMiddlewareTests(AuthenticatedUserTestCase):
         """use_this_middleware returns False if we can't upload attachments"""
         """use_this_middleware returns False if we can't upload attachments"""
         middleware = AttachmentsMiddleware(user=self.user)
         middleware = AttachmentsMiddleware(user=self.user)
 
 
-        self.override_acl({
-            'max_attachment_size': 0
-        })
+        self.override_acl({'max_attachment_size': 0})
 
 
         self.assertFalse(middleware.use_this_middleware())
         self.assertFalse(middleware.use_this_middleware())
 
 
-        self.override_acl({
-            'max_attachment_size': 1024
-        })
+        self.override_acl({'max_attachment_size': 1024})
 
 
         self.assertTrue(middleware.use_this_middleware())
         self.assertTrue(middleware.use_this_middleware())
 
 
     def test_middleware_is_optional(self):
     def test_middleware_is_optional(self):
         """middleware is optional"""
         """middleware is optional"""
-        INPUTS = (
-            {},
-            {'attachments': []}
-        )
+        INPUTS = [{}, {'attachments': []}]
 
 
         for test_input in INPUTS:
         for test_input in INPUTS:
             middleware = AttachmentsMiddleware(
             middleware = AttachmentsMiddleware(
                 request=RequestMock(test_input),
                 request=RequestMock(test_input),
                 mode=PostingEndpoint.START,
                 mode=PostingEndpoint.START,
                 user=self.user,
                 user=self.user,
-                post=self.post
+                post=self.post,
             )
             )
 
 
             serializer = middleware.get_serializer()
             serializer = middleware.get_serializer()
@@ -83,11 +74,7 @@ class AttachmentsMiddlewareTests(AuthenticatedUserTestCase):
 
 
     def test_middleware_validates_ids(self):
     def test_middleware_validates_ids(self):
         """middleware validates attachments ids"""
         """middleware validates attachments ids"""
-        INPUTS = (
-            'none',
-            ['a', 'b', 123],
-            range(settings.MISAGO_POST_ATTACHMENTS_LIMIT + 1)
-        )
+        INPUTS = ['none', ['a', 'b', 123], range(settings.MISAGO_POST_ATTACHMENTS_LIMIT + 1)]
 
 
         for test_input in INPUTS:
         for test_input in INPUTS:
             middleware = AttachmentsMiddleware(
             middleware = AttachmentsMiddleware(
@@ -96,7 +83,7 @@ class AttachmentsMiddlewareTests(AuthenticatedUserTestCase):
                 }),
                 }),
                 mode=PostingEndpoint.START,
                 mode=PostingEndpoint.START,
                 user=self.user,
                 user=self.user,
-                post=self.post
+                post=self.post,
             )
             )
 
 
             serializer = middleware.get_serializer()
             serializer = middleware.get_serializer()
@@ -108,18 +95,20 @@ class AttachmentsMiddlewareTests(AuthenticatedUserTestCase):
             request=RequestMock(),
             request=RequestMock(),
             mode=PostingEndpoint.EDIT,
             mode=PostingEndpoint.EDIT,
             user=self.user,
             user=self.user,
-            post=self.post
+            post=self.post,
         )
         )
 
 
         serializer = middleware.get_serializer()
         serializer = middleware.get_serializer()
 
 
         attachments = serializer.get_initial_attachments(
         attachments = serializer.get_initial_attachments(
-            middleware.mode, middleware.user, middleware.post)
+            middleware.mode, middleware.user, middleware.post
+        )
         self.assertEqual(attachments, [])
         self.assertEqual(attachments, [])
 
 
         attachment = self.mock_attachment(post=self.post)
         attachment = self.mock_attachment(post=self.post)
         attachments = serializer.get_initial_attachments(
         attachments = serializer.get_initial_attachments(
-            middleware.mode, middleware.user, middleware.post)
+            middleware.mode, middleware.user, middleware.post
+        )
         self.assertEqual(attachments, [attachment])
         self.assertEqual(attachments, [attachment])
 
 
     def test_get_new_attachments(self):
     def test_get_new_attachments(self):
@@ -128,7 +117,7 @@ class AttachmentsMiddlewareTests(AuthenticatedUserTestCase):
             request=RequestMock(),
             request=RequestMock(),
             mode=PostingEndpoint.EDIT,
             mode=PostingEndpoint.EDIT,
             user=self.user,
             user=self.user,
-            post=self.post
+            post=self.post,
         )
         )
 
 
         serializer = middleware.get_serializer()
         serializer = middleware.get_serializer()
@@ -149,17 +138,19 @@ class AttachmentsMiddlewareTests(AuthenticatedUserTestCase):
         """middleware validates if we have permission to delete other users attachments"""
         """middleware validates if we have permission to delete other users attachments"""
         self.override_acl({
         self.override_acl({
             'max_attachment_size': 1024,
             'max_attachment_size': 1024,
-            'can_delete_other_users_attachments': False
+            'can_delete_other_users_attachments': False,
         })
         })
 
 
         attachment = self.mock_attachment(user=False, post=self.post)
         attachment = self.mock_attachment(user=False, post=self.post)
         self.assertIsNone(attachment.uploader)
         self.assertIsNone(attachment.uploader)
 
 
         serializer = AttachmentsMiddleware(
         serializer = AttachmentsMiddleware(
-            request=RequestMock({'attachments': []}),
+            request=RequestMock({
+                'attachments': []
+            }),
             mode=PostingEndpoint.EDIT,
             mode=PostingEndpoint.EDIT,
             user=self.user,
             user=self.user,
-            post=self.post
+            post=self.post,
         ).get_serializer()
         ).get_serializer()
 
 
         self.assertFalse(serializer.is_valid())
         self.assertFalse(serializer.is_valid())
@@ -177,7 +168,7 @@ class AttachmentsMiddlewareTests(AuthenticatedUserTestCase):
             }),
             }),
             mode=PostingEndpoint.EDIT,
             mode=PostingEndpoint.EDIT,
             user=self.user,
             user=self.user,
-            post=self.post
+            post=self.post,
         )
         )
 
 
         serializer = middleware.get_serializer()
         serializer = middleware.get_serializer()
@@ -189,7 +180,8 @@ class AttachmentsMiddlewareTests(AuthenticatedUserTestCase):
         self.assertEqual(self.post.attachment_set.count(), 2)
         self.assertEqual(self.post.attachment_set.count(), 2)
 
 
         attachments_filenames = list(reversed([a.filename for a in attachments]))
         attachments_filenames = list(reversed([a.filename for a in attachments]))
-        self.assertEqual([a['filename'] for a in self.post.attachments_cache], attachments_filenames)
+        self.assertEqual([a['filename'] for a in self.post.attachments_cache],
+                         attachments_filenames)
 
 
     def test_remove_attachments(self):
     def test_remove_attachments(self):
         """middleware removes attachment from post and db"""
         """middleware removes attachment from post and db"""
@@ -204,7 +196,7 @@ class AttachmentsMiddlewareTests(AuthenticatedUserTestCase):
             }),
             }),
             mode=PostingEndpoint.EDIT,
             mode=PostingEndpoint.EDIT,
             user=self.user,
             user=self.user,
-            post=self.post
+            post=self.post,
         )
         )
 
 
         serializer = middleware.get_serializer()
         serializer = middleware.get_serializer()
@@ -218,7 +210,8 @@ class AttachmentsMiddlewareTests(AuthenticatedUserTestCase):
         self.assertEqual(Attachment.objects.count(), 1)
         self.assertEqual(Attachment.objects.count(), 1)
 
 
         attachments_filenames = [attachments[0].filename]
         attachments_filenames = [attachments[0].filename]
-        self.assertEqual([a['filename'] for a in self.post.attachments_cache], attachments_filenames)
+        self.assertEqual([a['filename'] for a in self.post.attachments_cache],
+                         attachments_filenames)
 
 
     def test_steal_attachments(self):
     def test_steal_attachments(self):
         """middleware validates if attachments are already assigned to other posts"""
         """middleware validates if attachments are already assigned to other posts"""
@@ -235,7 +228,7 @@ class AttachmentsMiddlewareTests(AuthenticatedUserTestCase):
             }),
             }),
             mode=PostingEndpoint.EDIT,
             mode=PostingEndpoint.EDIT,
             user=self.user,
             user=self.user,
-            post=self.post
+            post=self.post,
         )
         )
 
 
         serializer = middleware.get_serializer()
         serializer = middleware.get_serializer()
@@ -263,7 +256,7 @@ class AttachmentsMiddlewareTests(AuthenticatedUserTestCase):
             }),
             }),
             mode=PostingEndpoint.EDIT,
             mode=PostingEndpoint.EDIT,
             user=self.user,
             user=self.user,
-            post=self.post
+            post=self.post,
         )
         )
 
 
         serializer = middleware.get_serializer()
         serializer = middleware.get_serializer()
@@ -275,7 +268,8 @@ class AttachmentsMiddlewareTests(AuthenticatedUserTestCase):
         self.assertEqual(self.post.attachment_set.count(), 2)
         self.assertEqual(self.post.attachment_set.count(), 2)
 
 
         attachments_filenames = [attachments[2].filename, attachments[0].filename]
         attachments_filenames = [attachments[2].filename, attachments[0].filename]
-        self.assertEqual([a['filename'] for a in self.post.attachments_cache], attachments_filenames)
+        self.assertEqual([a['filename'] for a in self.post.attachments_cache],
+                         attachments_filenames)
 
 
 
 
 class ValidateAttachmentsCountTests(AuthenticatedUserTestCase):
 class ValidateAttachmentsCountTests(AuthenticatedUserTestCase):

+ 86 - 53
misago/threads/tests/test_attachmenttypeadmin_views.py

@@ -37,12 +37,15 @@ class AttachmentTypeAdminViewsTests(AdminTestCase):
         response = self.client.post(form_link, data={})
         response = self.client.post(form_link, data={})
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response = self.client.post(form_link, data={
-            'name': 'Test type',
-            'extensions': '.test',
-            'size_limit': 0,
-            'status': AttachmentType.ENABLED,
-        })
+        response = self.client.post(
+            form_link,
+            data={
+                'name': 'Test type',
+                'extensions': '.test',
+                'size_limit': 0,
+                'status': AttachmentType.ENABLED,
+            }
+        )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
         # clean alert about new item created
         # clean alert about new item created
@@ -55,17 +58,24 @@ class AttachmentTypeAdminViewsTests(AdminTestCase):
 
 
     def test_edit_view(self):
     def test_edit_view(self):
         """edit attachment type view has no showstoppers"""
         """edit attachment type view has no showstoppers"""
-        self.client.post(reverse('misago:admin:system:attachment-types:new'), data={
-            'name': 'Test type',
-            'extensions': '.test',
-            'size_limit': 0,
-            'status': AttachmentType.ENABLED,
-        })
+        self.client.post(
+            reverse('misago:admin:system:attachment-types:new'),
+            data={
+                'name': 'Test type',
+                'extensions': '.test',
+                'size_limit': 0,
+                'status': AttachmentType.ENABLED,
+            }
+        )
 
 
         test_type = AttachmentType.objects.order_by('id').last()
         test_type = AttachmentType.objects.order_by('id').last()
         self.assertEqual(test_type.name, 'Test type')
         self.assertEqual(test_type.name, 'Test type')
 
 
-        form_link = reverse('misago:admin:system:attachment-types:edit', kwargs={'pk': test_type.pk})
+        form_link = reverse(
+            'misago:admin:system:attachment-types:edit', kwargs={
+                'pk': test_type.pk,
+            }
+        )
 
 
         response = self.client.get(form_link)
         response = self.client.get(form_link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
@@ -73,15 +83,18 @@ class AttachmentTypeAdminViewsTests(AdminTestCase):
         response = self.client.post(form_link, data={})
         response = self.client.post(form_link, data={})
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response = self.client.post(form_link, data={
-            'name': 'Test type edited',
-            'extensions': '.test.extension',
-            'mimetypes': 'test/edited-mime',
-            'size_limit': 512,
-            'status': AttachmentType.DISABLED,
-            'limit_uploads_to': [r.pk for r in Role.objects.all()],
-            'limit_downloads_to': [r.pk for r in Role.objects.all()],
-        })
+        response = self.client.post(
+            form_link,
+            data={
+                'name': 'Test type edited',
+                'extensions': '.test.extension',
+                'mimetypes': 'test/edited-mime',
+                'size_limit': 512,
+                'status': AttachmentType.DISABLED,
+                'limit_uploads_to': [r.pk for r in Role.objects.all()],
+                'limit_downloads_to': [r.pk for r in Role.objects.all()],
+            }
+        )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
         test_type = AttachmentType.objects.order_by('id').last()
         test_type = AttachmentType.objects.order_by('id').last()
@@ -100,15 +113,18 @@ class AttachmentTypeAdminViewsTests(AdminTestCase):
         self.assertEqual(test_type.limit_downloads_to.count(), Role.objects.count())
         self.assertEqual(test_type.limit_downloads_to.count(), Role.objects.count())
 
 
         # remove limits from type
         # remove limits from type
-        response = self.client.post(form_link, data={
-            'name': 'Test type edited',
-            'extensions': '.test.extension',
-            'mimetypes': 'test/edited-mime',
-            'size_limit': 512,
-            'status': AttachmentType.DISABLED,
-            'limit_uploads_to': [],
-            'limit_downloads_to': [],
-        })
+        response = self.client.post(
+            form_link,
+            data={
+                'name': 'Test type edited',
+                'extensions': '.test.extension',
+                'mimetypes': 'test/edited-mime',
+                'size_limit': 512,
+                'status': AttachmentType.DISABLED,
+                'limit_uploads_to': [],
+                'limit_downloads_to': [],
+            }
+        )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
         self.assertEqual(test_type.limit_uploads_to.count(), 0)
         self.assertEqual(test_type.limit_uploads_to.count(), 0)
@@ -116,7 +132,7 @@ class AttachmentTypeAdminViewsTests(AdminTestCase):
 
 
     def test_clean_params_view(self):
     def test_clean_params_view(self):
         """admin form nicely cleans lists of extensions/mimetypes"""
         """admin form nicely cleans lists of extensions/mimetypes"""
-        TEST_CASES = (
+        TEST_CASES = [
             ('test', ['test']),
             ('test', ['test']),
             ('.test', ['test']),
             ('.test', ['test']),
             ('.tar.gz', ['tar.gz']),
             ('.tar.gz', ['tar.gz']),
@@ -125,15 +141,18 @@ class AttachmentTypeAdminViewsTests(AdminTestCase):
             ('test, tEst', ['test']),
             ('test, tEst', ['test']),
             ('test, other, tEst', ['test', 'other']),
             ('test, other, tEst', ['test', 'other']),
             ('test, other, tEst,OTher', ['test', 'other']),
             ('test, other, tEst,OTher', ['test', 'other']),
-        )
+        ]
 
 
         for raw, final in TEST_CASES:
         for raw, final in TEST_CASES:
-            response = self.client.post(reverse('misago:admin:system:attachment-types:new'), data={
-                'name': 'Test type',
-                'extensions': raw,
-                'size_limit': 0,
-                'status': AttachmentType.ENABLED,
-            })
+            response = self.client.post(
+                reverse('misago:admin:system:attachment-types:new'),
+                data={
+                    'name': 'Test type',
+                    'extensions': raw,
+                    'size_limit': 0,
+                    'status': AttachmentType.ENABLED,
+                }
+            )
             self.assertEqual(response.status_code, 302)
             self.assertEqual(response.status_code, 302)
 
 
             test_type = AttachmentType.objects.order_by('id').last()
             test_type = AttachmentType.objects.order_by('id').last()
@@ -141,17 +160,24 @@ class AttachmentTypeAdminViewsTests(AdminTestCase):
 
 
     def test_delete_view(self):
     def test_delete_view(self):
         """delete attachment type view has no showstoppers"""
         """delete attachment type view has no showstoppers"""
-        self.client.post(reverse('misago:admin:system:attachment-types:new'), data={
-            'name': 'Test type',
-            'extensions': '.test',
-            'size_limit': 0,
-            'status': AttachmentType.ENABLED,
-        })
+        self.client.post(
+            reverse('misago:admin:system:attachment-types:new'),
+            data={
+                'name': 'Test type',
+                'extensions': '.test',
+                'size_limit': 0,
+                'status': AttachmentType.ENABLED,
+            }
+        )
 
 
         test_type = AttachmentType.objects.order_by('id').last()
         test_type = AttachmentType.objects.order_by('id').last()
         self.assertEqual(test_type.name, 'Test type')
         self.assertEqual(test_type.name, 'Test type')
 
 
-        action_link = reverse('misago:admin:system:attachment-types:delete', kwargs={'pk': test_type.pk})
+        action_link = reverse(
+            'misago:admin:system:attachment-types:delete', kwargs={
+                'pk': test_type.pk,
+            }
+        )
 
 
         response = self.client.post(action_link)
         response = self.client.post(action_link)
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
@@ -165,12 +191,15 @@ class AttachmentTypeAdminViewsTests(AdminTestCase):
 
 
     def test_cant_delete_type_with_attachments_view(self):
     def test_cant_delete_type_with_attachments_view(self):
         """delete attachment type is not allowed if it has attachments associated"""
         """delete attachment type is not allowed if it has attachments associated"""
-        self.client.post(reverse('misago:admin:system:attachment-types:new'), data={
-            'name': 'Test type',
-            'extensions': '.test',
-            'size_limit': 0,
-            'status': AttachmentType.ENABLED,
-        })
+        self.client.post(
+            reverse('misago:admin:system:attachment-types:new'),
+            data={
+                'name': 'Test type',
+                'extensions': '.test',
+                'size_limit': 0,
+                'status': AttachmentType.ENABLED,
+            }
+        )
 
 
         test_type = AttachmentType.objects.order_by('id').last()
         test_type = AttachmentType.objects.order_by('id').last()
         self.assertEqual(test_type.name, 'Test type')
         self.assertEqual(test_type.name, 'Test type')
@@ -185,7 +214,11 @@ class AttachmentTypeAdminViewsTests(AdminTestCase):
             file='sad76asd678as687sa.zip'
             file='sad76asd678as687sa.zip'
         )
         )
 
 
-        action_link = reverse('misago:admin:system:attachment-types:delete', kwargs={'pk': test_type.pk})
+        action_link = reverse(
+            'misago:admin:system:attachment-types:delete', kwargs={
+                'pk': test_type.pk,
+            }
+        )
 
 
         response = self.client.post(action_link)
         response = self.client.post(action_link)
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)

+ 34 - 21
misago/threads/tests/test_attachmentview.py

@@ -29,11 +29,11 @@ class AttachmentViewTestCase(AuthenticatedUserTestCase):
 
 
         self.attachment_type_jpg = AttachmentType.objects.create(
         self.attachment_type_jpg = AttachmentType.objects.create(
             name="JPG",
             name="JPG",
-            extensions='jpeg,jpg'
+            extensions='jpeg,jpg',
         )
         )
         self.attachment_type_pdf = AttachmentType.objects.create(
         self.attachment_type_pdf = AttachmentType.objects.create(
             name="PDF",
             name="PDF",
-            extensions='pdf'
+            extensions='pdf',
         )
         )
 
 
         self.override_acl()
         self.override_acl()
@@ -42,15 +42,17 @@ class AttachmentViewTestCase(AuthenticatedUserTestCase):
         acl = self.user.acl_cache.copy()
         acl = self.user.acl_cache.copy()
         acl.update({
         acl.update({
             'max_attachment_size': 1000,
             'max_attachment_size': 1000,
-            'can_download_other_users_attachments': allow_download
+            'can_download_other_users_attachments': allow_download,
         })
         })
         override_acl(self.user, acl)
         override_acl(self.user, acl)
 
 
     def upload_document(self, is_orphaned=False, by_other_user=False):
     def upload_document(self, is_orphaned=False, by_other_user=False):
         with open(TEST_DOCUMENT_PATH, 'rb') as upload:
         with open(TEST_DOCUMENT_PATH, 'rb') as upload:
-            response = self.client.post(self.api_link, data={
-                'upload': upload
-            })
+            response = self.client.post(
+                self.api_link, data={
+                    'upload': upload,
+                }
+            )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         attachment = Attachment.objects.order_by('id').last()
         attachment = Attachment.objects.order_by('id').last()
@@ -68,9 +70,11 @@ class AttachmentViewTestCase(AuthenticatedUserTestCase):
 
 
     def upload_image(self):
     def upload_image(self):
         with open(TEST_SMALLJPG_PATH, 'rb') as upload:
         with open(TEST_SMALLJPG_PATH, 'rb') as upload:
-            response = self.client.post(self.api_link, data={
-                'upload': upload
-            })
+            response = self.client.post(
+                self.api_link, data={
+                    'upload': upload,
+                }
+            )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         attachment = Attachment.objects.order_by('id').last()
         attachment = Attachment.objects.order_by('id').last()
@@ -94,10 +98,12 @@ class AttachmentViewTestCase(AuthenticatedUserTestCase):
 
 
     def test_nonexistant_file(self):
     def test_nonexistant_file(self):
         """user tries to retrieve nonexistant file"""
         """user tries to retrieve nonexistant file"""
-        response = self.client.get(reverse('misago:attachment', kwargs={
-            'pk': 123,
-            'secret': 'qwertyuiop'
-        }))
+        response = self.client.get(
+            reverse('misago:attachment', kwargs={
+                'pk': 123,
+                'secret': 'qwertyuiop',
+            })
+        )
 
 
         self.assertIs404(response)
         self.assertIs404(response)
 
 
@@ -105,10 +111,12 @@ class AttachmentViewTestCase(AuthenticatedUserTestCase):
         """user tries to retrieve existing file using invalid secret"""
         """user tries to retrieve existing file using invalid secret"""
         attachment = self.upload_document()
         attachment = self.upload_document()
 
 
-        response = self.client.get(reverse('misago:attachment', kwargs={
-            'pk': attachment.pk,
-            'secret': 'qwertyuiop'
-        }))
+        response = self.client.get(
+            reverse('misago:attachment', kwargs={
+                'pk': attachment.pk,
+                'secret': 'qwertyuiop',
+            })
+        )
 
 
         self.assertIs404(response)
         self.assertIs404(response)
 
 
@@ -135,10 +143,15 @@ class AttachmentViewTestCase(AuthenticatedUserTestCase):
         """user tries to retrieve thumbnail from non-image attachment"""
         """user tries to retrieve thumbnail from non-image attachment"""
         attachment = self.upload_document()
         attachment = self.upload_document()
 
 
-        response = self.client.get(reverse('misago:attachment-thumbnail', kwargs={
-            'pk': attachment.pk,
-            'secret': attachment.secret
-        }))
+        response = self.client.get(
+            reverse(
+                'misago:attachment-thumbnail',
+                kwargs={
+                    'pk': attachment.pk,
+                    'secret': attachment.secret,
+                }
+            )
+        )
         self.assertIs404(response)
         self.assertIs404(response)
 
 
     def test_no_role(self):
     def test_no_role(self):

+ 3 - 3
misago/threads/tests/test_clearattachments.py

@@ -31,7 +31,7 @@ class ClearAttachmentsTests(TestCase):
         cutoff = timezone.now() - timedelta(minutes=settings.MISAGO_ATTACHMENT_ORPHANED_EXPIRE)
         cutoff = timezone.now() - timedelta(minutes=settings.MISAGO_ATTACHMENT_ORPHANED_EXPIRE)
         cutoff -= timedelta(minutes=5)
         cutoff -= timedelta(minutes=5)
 
 
-        for i in range(5):
+        for _ in range(5):
             Attachment.objects.create(
             Attachment.objects.create(
                 secret=Attachment.generate_new_secret(),
                 secret=Attachment.generate_new_secret(),
                 filetype=filetype,
                 filetype=filetype,
@@ -47,7 +47,7 @@ class ClearAttachmentsTests(TestCase):
         category = Category.objects.get(slug='first-category')
         category = Category.objects.get(slug='first-category')
         post = testutils.post_thread(category).first_post
         post = testutils.post_thread(category).first_post
 
 
-        for i in range(5):
+        for _ in range(5):
             Attachment.objects.create(
             Attachment.objects.create(
                 secret=Attachment.generate_new_secret(),
                 secret=Attachment.generate_new_secret(),
                 filetype=filetype,
                 filetype=filetype,
@@ -61,7 +61,7 @@ class ClearAttachmentsTests(TestCase):
             )
             )
 
 
         # create 5 fresh orphaned attachments
         # create 5 fresh orphaned attachments
-        for i in range(5):
+        for _ in range(5):
             Attachment.objects.create(
             Attachment.objects.create(
                 secret=Attachment.generate_new_secret(),
                 secret=Attachment.generate_new_secret(),
                 filetype=filetype,
                 filetype=filetype,

+ 51 - 36
misago/threads/tests/test_emailnotification_middleware.py

@@ -26,16 +26,17 @@ class EmailNotificationTests(AuthenticatedUserTestCase):
         self.category = Category.objects.get(slug='first-category')
         self.category = Category.objects.get(slug='first-category')
         self.thread = testutils.post_thread(
         self.thread = testutils.post_thread(
             category=self.category,
             category=self.category,
-            started_on=timezone.now() - timedelta(seconds=5)
+            started_on=timezone.now() - timedelta(seconds=5),
         )
         )
         self.override_acl()
         self.override_acl()
 
 
-        self.api_link = reverse('misago:api:thread-post-list', kwargs={
-            'thread_pk': self.thread.pk
-        })
+        self.api_link = reverse(
+            'misago:api:thread-post-list', kwargs={
+                'thread_pk': self.thread.pk,
+            }
+        )
 
 
-        self.other_user = UserModel.objects.create_user(
-            'Bob', 'bob@boberson.com', 'pass123')
+        self.other_user = UserModel.objects.create_user('Bob', 'bob@boberson.com', 'pass123')
 
 
     def override_acl(self):
     def override_acl(self):
         new_acl = deepcopy(self.user.acl_cache)
         new_acl = deepcopy(self.user.acl_cache)
@@ -44,7 +45,7 @@ class EmailNotificationTests(AuthenticatedUserTestCase):
             'can_browse': 1,
             'can_browse': 1,
             'can_start_threads': 1,
             'can_start_threads': 1,
             'can_reply_threads': 1,
             'can_reply_threads': 1,
-            'can_edit_posts': 1
+            'can_edit_posts': 1,
         })
         })
 
 
         override_acl(self.user, new_acl)
         override_acl(self.user, new_acl)
@@ -56,21 +57,23 @@ class EmailNotificationTests(AuthenticatedUserTestCase):
             'can_browse': 1,
             'can_browse': 1,
             'can_start_threads': 1,
             'can_start_threads': 1,
             'can_reply_threads': 1,
             'can_reply_threads': 1,
-            'can_edit_posts': 1
+            'can_edit_posts': 1,
         })
         })
 
 
         if hide:
         if hide:
             new_acl['categories'][self.category.pk].update({
             new_acl['categories'][self.category.pk].update({
-                'can_browse': False
+                'can_browse': False,
             })
             })
 
 
         override_acl(self.other_user, new_acl)
         override_acl(self.other_user, new_acl)
 
 
     def test_no_subscriptions(self):
     def test_no_subscriptions(self):
         """no emails are sent because noone subscibes to thread"""
         """no emails are sent because noone subscibes to thread"""
-        response = self.client.post(self.api_link, data={
-            'post': 'This is test response!'
-        })
+        response = self.client.post(
+            self.api_link, data={
+                'post': 'This is test response!',
+            }
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         self.assertEqual(len(mail.outbox), 0)
         self.assertEqual(len(mail.outbox), 0)
@@ -81,12 +84,14 @@ class EmailNotificationTests(AuthenticatedUserTestCase):
             thread=self.thread,
             thread=self.thread,
             category=self.category,
             category=self.category,
             last_read_on=timezone.now(),
             last_read_on=timezone.now(),
-            send_email=True
+            send_email=True,
         )
         )
 
 
-        response = self.client.post(self.api_link, data={
-            'post': 'This is test response!'
-        })
+        response = self.client.post(
+            self.api_link, data={
+                'post': 'This is test response!',
+            }
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         self.assertEqual(len(mail.outbox), 0)
         self.assertEqual(len(mail.outbox), 0)
@@ -97,12 +102,14 @@ class EmailNotificationTests(AuthenticatedUserTestCase):
             thread=self.thread,
             thread=self.thread,
             category=self.category,
             category=self.category,
             last_read_on=timezone.now(),
             last_read_on=timezone.now(),
-            send_email=False
+            send_email=False,
         )
         )
 
 
-        response = self.client.post(self.api_link, data={
-            'post': 'This is test response!'
-        })
+        response = self.client.post(
+            self.api_link, data={
+                'post': 'This is test response!',
+            }
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         self.assertEqual(len(mail.outbox), 0)
         self.assertEqual(len(mail.outbox), 0)
@@ -113,13 +120,15 @@ class EmailNotificationTests(AuthenticatedUserTestCase):
             thread=self.thread,
             thread=self.thread,
             category=self.category,
             category=self.category,
             last_read_on=timezone.now(),
             last_read_on=timezone.now(),
-            send_email=True
+            send_email=True,
         )
         )
         self.override_other_user_acl(hide=True)
         self.override_other_user_acl(hide=True)
 
 
-        response = self.client.post(self.api_link, data={
-            'post': 'This is test response!'
-        })
+        response = self.client.post(
+            self.api_link, data={
+                'post': 'This is test response!',
+            }
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         self.assertEqual(len(mail.outbox), 0)
         self.assertEqual(len(mail.outbox), 0)
@@ -130,15 +139,17 @@ class EmailNotificationTests(AuthenticatedUserTestCase):
             thread=self.thread,
             thread=self.thread,
             category=self.category,
             category=self.category,
             last_read_on=timezone.now(),
             last_read_on=timezone.now(),
-            send_email=True
+            send_email=True,
         )
         )
         self.override_other_user_acl()
         self.override_other_user_acl()
 
 
         testutils.reply_thread(self.thread, posted_on=timezone.now())
         testutils.reply_thread(self.thread, posted_on=timezone.now())
 
 
-        response = self.client.post(self.api_link, data={
-            'post': 'This is test response!'
-        })
+        response = self.client.post(
+            self.api_link, data={
+                'post': 'This is test response!',
+            }
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         self.assertEqual(len(mail.outbox), 0)
         self.assertEqual(len(mail.outbox), 0)
@@ -149,13 +160,15 @@ class EmailNotificationTests(AuthenticatedUserTestCase):
             thread=self.thread,
             thread=self.thread,
             category=self.category,
             category=self.category,
             last_read_on=timezone.now(),
             last_read_on=timezone.now(),
-            send_email=True
+            send_email=True,
         )
         )
         self.override_other_user_acl()
         self.override_other_user_acl()
 
 
-        response = self.client.post(self.api_link, data={
-            'post': 'This is test response!'
-        })
+        response = self.client.post(
+            self.api_link, data={
+                'post': 'This is test response!',
+            }
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         self.assertEqual(len(mail.outbox), 1)
         self.assertEqual(len(mail.outbox), 1)
@@ -179,13 +192,15 @@ class EmailNotificationTests(AuthenticatedUserTestCase):
             thread=self.thread,
             thread=self.thread,
             category=self.category,
             category=self.category,
             last_read_on=self.thread.last_post_on,
             last_read_on=self.thread.last_post_on,
-            send_email=True
+            send_email=True,
         )
         )
         self.override_other_user_acl()
         self.override_other_user_acl()
 
 
-        response = self.client.post(self.api_link, data={
-            'post': 'This is test response!'
-        })
+        response = self.client.post(
+            self.api_link, data={
+                'post': 'This is test response!',
+            }
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         self.assertEqual(len(mail.outbox), 1)
         self.assertEqual(len(mail.outbox), 1)

+ 3 - 7
misago/threads/tests/test_events.py

@@ -1,6 +1,4 @@
 #-*- coding: utf-8 -*-
 #-*- coding: utf-8 -*-
-import random
-
 from django.contrib.auth import get_user_model
 from django.contrib.auth import get_user_model
 from django.test import TestCase
 from django.test import TestCase
 from django.utils import timezone
 from django.utils import timezone
@@ -8,8 +6,7 @@ from django.utils import timezone
 from misago.acl import add_acl
 from misago.acl import add_acl
 from misago.categories.models import Category
 from misago.categories.models import Category
 from misago.threads.events import record_event
 from misago.threads.events import record_event
-from misago.threads.models import Post, Thread
-from misago.threads.testutils import reply_thread
+from misago.threads.models import Thread
 
 
 
 
 UserModel = get_user_model()
 UserModel = get_user_model()
@@ -23,8 +20,7 @@ class MockRequest(object):
 
 
 class EventsAPITests(TestCase):
 class EventsAPITests(TestCase):
     def setUp(self):
     def setUp(self):
-        self.user = UserModel.objects.create_user(
-            "Bob", "bob@bob.com", "Pass.123")
+        self.user = UserModel.objects.create_user("Bob", "bob@bob.com", "Pass.123")
 
 
         datetime = timezone.now()
         datetime = timezone.now()
 
 
@@ -36,7 +32,7 @@ class EventsAPITests(TestCase):
             starter_slug='tester',
             starter_slug='tester',
             last_post_on=datetime,
             last_post_on=datetime,
             last_poster_name='Tester',
             last_poster_name='Tester',
-            last_poster_slug='tester'
+            last_poster_slug='tester',
         )
         )
 
 
         self.thread.set_title("Test thread")
         self.thread.set_title("Test thread")

+ 19 - 11
misago/threads/tests/test_floodprotection.py

@@ -17,9 +17,11 @@ class PostMentionsTests(AuthenticatedUserTestCase):
         self.thread = testutils.post_thread(category=self.category)
         self.thread = testutils.post_thread(category=self.category)
         self.override_acl()
         self.override_acl()
 
 
-        self.post_link = reverse('misago:api:thread-post-list', kwargs={
-            'thread_pk': self.thread.pk
-        })
+        self.post_link = reverse(
+            'misago:api:thread-post-list', kwargs={
+                'thread_pk': self.thread.pk,
+            }
+        )
 
 
     def override_acl(self):
     def override_acl(self):
         new_acl = self.user.acl_cache
         new_acl = self.user.acl_cache
@@ -27,19 +29,25 @@ class PostMentionsTests(AuthenticatedUserTestCase):
             'can_see': 1,
             'can_see': 1,
             'can_browse': 1,
             'can_browse': 1,
             'can_start_threads': 1,
             'can_start_threads': 1,
-            'can_reply_threads': 1
+            'can_reply_threads': 1,
         })
         })
 
 
         override_acl(self.user, new_acl)
         override_acl(self.user, new_acl)
 
 
     def test_flood_has_no_showstoppers(self):
     def test_flood_has_no_showstoppers(self):
         """endpoint handles posting interruption"""
         """endpoint handles posting interruption"""
-        response = self.client.post(self.post_link, data={
-            'post': "This is test response!"
-        })
+        response = self.client.post(
+            self.post_link, data={
+                'post': "This is test response!",
+            }
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response = self.client.post(self.post_link, data={
-            'post': "This is test response!"
-        })
-        self.assertContains(response, "You can't post message so quickly after previous one.", status_code=403)
+        response = self.client.post(
+            self.post_link, data={
+                'post': "This is test response!",
+            }
+        )
+        self.assertContains(
+            response, "You can't post message so quickly after previous one.", status_code=403
+        )

+ 2 - 5
misago/threads/tests/test_floodprotection_middleware.py

@@ -4,8 +4,7 @@ from django.utils import timezone
 
 
 from misago.acl.testutils import override_acl
 from misago.acl.testutils import override_acl
 from misago.threads.api.postingendpoint import PostingInterrupt
 from misago.threads.api.postingendpoint import PostingInterrupt
-from misago.threads.api.postingendpoint.floodprotection import (
-    MIN_POSTING_PAUSE, FloodProtectionMiddleware)
+from misago.threads.api.postingendpoint.floodprotection import FloodProtectionMiddleware
 from misago.users.testutils import AuthenticatedUserTestCase
 from misago.users.testutils import AuthenticatedUserTestCase
 
 
 
 
@@ -42,9 +41,7 @@ class FloodProtectionMiddlewareTests(AuthenticatedUserTestCase):
 
 
     def test_flood_permission(self):
     def test_flood_permission(self):
         """middleware is respects permission to flood for team members"""
         """middleware is respects permission to flood for team members"""
-        override_acl(self.user, {
-            'can_omit_flood_protection': True
-        })
+        override_acl(self.user, {'can_omit_flood_protection': True})
 
 
         middleware = FloodProtectionMiddleware(user=self.user)
         middleware = FloodProtectionMiddleware(user=self.user)
         self.assertFalse(middleware.use_this_middleware())
         self.assertFalse(middleware.use_this_middleware())

+ 58 - 26
misago/threads/tests/test_gotoviews.py

@@ -25,31 +25,38 @@ class GotoPostTests(GotoViewTestCase):
         """first post redirect url is valid"""
         """first post redirect url is valid"""
         response = self.client.get(self.thread.first_post.get_absolute_url())
         response = self.client.get(self.thread.first_post.get_absolute_url())
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
-        self.assertEqual(response['location'], GOTO_URL % (self.thread.get_absolute_url(), self.thread.first_post_id))
+        self.assertEqual(
+            response['location'],
+            GOTO_URL % (self.thread.get_absolute_url(), self.thread.first_post_id)
+        )
 
 
         response = self.client.get(response['location'])
         response = self.client.get(response['location'])
         self.assertContains(response, self.thread.first_post.get_absolute_url())
         self.assertContains(response, self.thread.first_post.get_absolute_url())
 
 
     def test_goto_last_post_on_page(self):
     def test_goto_last_post_on_page(self):
         """last post on page redirect url is valid"""
         """last post on page redirect url is valid"""
-        for i in range(settings.MISAGO_POSTS_PER_PAGE + settings.MISAGO_POSTS_TAIL - 1):
+        for _ in range(settings.MISAGO_POSTS_PER_PAGE + settings.MISAGO_POSTS_TAIL - 1):
             post = testutils.reply_thread(self.thread)
             post = testutils.reply_thread(self.thread)
 
 
         response = self.client.get(post.get_absolute_url())
         response = self.client.get(post.get_absolute_url())
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
-        self.assertEqual(response['location'], GOTO_URL % (self.thread.get_absolute_url(), post.pk))
+        self.assertEqual(
+            response['location'], GOTO_URL % (self.thread.get_absolute_url(), post.pk)
+        )
 
 
         response = self.client.get(response['location'])
         response = self.client.get(response['location'])
         self.assertContains(response, post.get_absolute_url())
         self.assertContains(response, post.get_absolute_url())
 
 
     def test_goto_first_post_on_next_page(self):
     def test_goto_first_post_on_next_page(self):
         """first post on next page redirect url is valid"""
         """first post on next page redirect url is valid"""
-        for i in range(settings.MISAGO_POSTS_PER_PAGE + settings.MISAGO_POSTS_TAIL):
+        for _ in range(settings.MISAGO_POSTS_PER_PAGE + settings.MISAGO_POSTS_TAIL):
             post = testutils.reply_thread(self.thread)
             post = testutils.reply_thread(self.thread)
 
 
         response = self.client.get(post.get_absolute_url())
         response = self.client.get(post.get_absolute_url())
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
-        self.assertEqual(response['location'], GOTO_PAGE_URL % (self.thread.get_absolute_url(), 2, post.pk))
+        self.assertEqual(
+            response['location'], GOTO_PAGE_URL % (self.thread.get_absolute_url(), 2, post.pk)
+        )
 
 
         response = self.client.get(response['location'])
         response = self.client.get(response['location'])
         self.assertContains(response, post.get_absolute_url())
         self.assertContains(response, post.get_absolute_url())
@@ -57,7 +64,7 @@ class GotoPostTests(GotoViewTestCase):
     def test_goto_first_post_on_page_three_out_of_five(self):
     def test_goto_first_post_on_page_three_out_of_five(self):
         """first post on next page redirect url is valid"""
         """first post on next page redirect url is valid"""
         posts = []
         posts = []
-        for i in range(settings.MISAGO_POSTS_PER_PAGE * 4 - 1):
+        for _ in range(settings.MISAGO_POSTS_PER_PAGE * 4 - 1):
             post = testutils.reply_thread(self.thread)
             post = testutils.reply_thread(self.thread)
             posts.append(post)
             posts.append(post)
 
 
@@ -65,7 +72,9 @@ class GotoPostTests(GotoViewTestCase):
 
 
         response = self.client.get(post.get_absolute_url())
         response = self.client.get(post.get_absolute_url())
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
-        self.assertEqual(response['location'], GOTO_PAGE_URL % (self.thread.get_absolute_url(), 3, post.pk))
+        self.assertEqual(
+            response['location'], GOTO_PAGE_URL % (self.thread.get_absolute_url(), 3, post.pk)
+        )
 
 
         response = self.client.get(response['location'])
         response = self.client.get(response['location'])
         self.assertContains(response, post.get_absolute_url())
         self.assertContains(response, post.get_absolute_url())
@@ -73,7 +82,7 @@ class GotoPostTests(GotoViewTestCase):
     def test_goto_first_event_on_page_three_out_of_five(self):
     def test_goto_first_event_on_page_three_out_of_five(self):
         """event redirect url is valid"""
         """event redirect url is valid"""
         posts = []
         posts = []
-        for i in range(settings.MISAGO_POSTS_PER_PAGE * 4 - 1):
+        for _ in range(settings.MISAGO_POSTS_PER_PAGE * 4 - 1):
             post = testutils.reply_thread(self.thread)
             post = testutils.reply_thread(self.thread)
             posts.append(post)
             posts.append(post)
 
 
@@ -87,7 +96,9 @@ class GotoPostTests(GotoViewTestCase):
 
 
         response = self.client.get(post.get_absolute_url())
         response = self.client.get(post.get_absolute_url())
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
-        self.assertEqual(response['location'], GOTO_PAGE_URL % (self.thread.get_absolute_url(), 3, post.pk))
+        self.assertEqual(
+            response['location'], GOTO_PAGE_URL % (self.thread.get_absolute_url(), 3, post.pk)
+        )
 
 
         response = self.client.get(response['location'])
         response = self.client.get(response['location'])
         self.assertContains(response, post.get_absolute_url())
         self.assertContains(response, post.get_absolute_url())
@@ -98,19 +109,24 @@ class GotoLastTests(GotoViewTestCase):
         """first post redirect url is valid"""
         """first post redirect url is valid"""
         response = self.client.get(self.thread.get_last_post_url())
         response = self.client.get(self.thread.get_last_post_url())
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
-        self.assertEqual(response['location'], GOTO_URL % (self.thread.get_absolute_url(), self.thread.first_post_id))
+        self.assertEqual(
+            response['location'],
+            GOTO_URL % (self.thread.get_absolute_url(), self.thread.first_post_id)
+        )
 
 
         response = self.client.get(response['location'])
         response = self.client.get(response['location'])
         self.assertContains(response, self.thread.last_post.get_absolute_url())
         self.assertContains(response, self.thread.last_post.get_absolute_url())
 
 
     def test_goto_last_post_on_page(self):
     def test_goto_last_post_on_page(self):
         """last post on page redirect url is valid"""
         """last post on page redirect url is valid"""
-        for i in range(settings.MISAGO_POSTS_PER_PAGE + settings.MISAGO_POSTS_TAIL - 1):
+        for _ in range(settings.MISAGO_POSTS_PER_PAGE + settings.MISAGO_POSTS_TAIL - 1):
             post = testutils.reply_thread(self.thread)
             post = testutils.reply_thread(self.thread)
 
 
         response = self.client.get(self.thread.get_last_post_url())
         response = self.client.get(self.thread.get_last_post_url())
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
-        self.assertEqual(response['location'], GOTO_URL % (self.thread.get_absolute_url(), post.pk))
+        self.assertEqual(
+            response['location'], GOTO_URL % (self.thread.get_absolute_url(), post.pk)
+        )
 
 
         response = self.client.get(response['location'])
         response = self.client.get(response['location'])
         self.assertContains(response, post.get_absolute_url())
         self.assertContains(response, post.get_absolute_url())
@@ -121,7 +137,10 @@ class GotoNewTests(GotoViewTestCase):
         """first unread post redirect url is valid"""
         """first unread post redirect url is valid"""
         response = self.client.get(self.thread.get_new_post_url())
         response = self.client.get(self.thread.get_new_post_url())
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
-        self.assertEqual(response['location'], GOTO_URL % (self.thread.get_absolute_url(), self.thread.first_post_id))
+        self.assertEqual(
+            response['location'],
+            GOTO_URL % (self.thread.get_absolute_url(), self.thread.first_post_id)
+        )
 
 
     def test_goto_first_new_post(self):
     def test_goto_first_new_post(self):
         """first unread post redirect url in already read thread is valid"""
         """first unread post redirect url in already read thread is valid"""
@@ -129,32 +148,36 @@ class GotoNewTests(GotoViewTestCase):
         read_thread(self.user, self.thread, self.thread.last_post)
         read_thread(self.user, self.thread, self.thread.last_post)
 
 
         post = testutils.reply_thread(self.thread, posted_on=timezone.now())
         post = testutils.reply_thread(self.thread, posted_on=timezone.now())
-        for i in range(settings.MISAGO_POSTS_PER_PAGE + settings.MISAGO_POSTS_TAIL - 1):
+        for _ in range(settings.MISAGO_POSTS_PER_PAGE + settings.MISAGO_POSTS_TAIL - 1):
             testutils.reply_thread(self.thread, posted_on=timezone.now())
             testutils.reply_thread(self.thread, posted_on=timezone.now())
 
 
         response = self.client.get(self.thread.get_new_post_url())
         response = self.client.get(self.thread.get_new_post_url())
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
-        self.assertEqual(response['location'], GOTO_URL % (self.thread.get_absolute_url(), post.pk))
+        self.assertEqual(
+            response['location'], GOTO_URL % (self.thread.get_absolute_url(), post.pk)
+        )
 
 
     def test_goto_first_new_post_on_next_page(self):
     def test_goto_first_new_post_on_next_page(self):
         """first unread post redirect url in already read multipage thread is valid"""
         """first unread post redirect url in already read multipage thread is valid"""
-        for i in range(settings.MISAGO_POSTS_PER_PAGE + settings.MISAGO_POSTS_TAIL):
+        for _ in range(settings.MISAGO_POSTS_PER_PAGE + settings.MISAGO_POSTS_TAIL):
             testutils.reply_thread(self.thread, posted_on=timezone.now())
             testutils.reply_thread(self.thread, posted_on=timezone.now())
 
 
         make_thread_read_aware(self.user, self.thread)
         make_thread_read_aware(self.user, self.thread)
         read_thread(self.user, self.thread, self.thread.last_post)
         read_thread(self.user, self.thread, self.thread.last_post)
 
 
         post = testutils.reply_thread(self.thread, posted_on=timezone.now())
         post = testutils.reply_thread(self.thread, posted_on=timezone.now())
-        for i in range(settings.MISAGO_POSTS_PER_PAGE + settings.MISAGO_POSTS_TAIL - 1):
+        for _ in range(settings.MISAGO_POSTS_PER_PAGE + settings.MISAGO_POSTS_TAIL - 1):
             testutils.reply_thread(self.thread, posted_on=timezone.now())
             testutils.reply_thread(self.thread, posted_on=timezone.now())
 
 
         response = self.client.get(self.thread.get_new_post_url())
         response = self.client.get(self.thread.get_new_post_url())
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
-        self.assertEqual(response['location'], GOTO_PAGE_URL % (self.thread.get_absolute_url(), 2, post.pk))
+        self.assertEqual(
+            response['location'], GOTO_PAGE_URL % (self.thread.get_absolute_url(), 2, post.pk)
+        )
 
 
     def test_goto_first_new_post_in_read_thread(self):
     def test_goto_first_new_post_in_read_thread(self):
         """goto new in read thread points to last post"""
         """goto new in read thread points to last post"""
-        for i in range(settings.MISAGO_POSTS_PER_PAGE + settings.MISAGO_POSTS_TAIL):
+        for _ in range(settings.MISAGO_POSTS_PER_PAGE + settings.MISAGO_POSTS_TAIL):
             post = testutils.reply_thread(self.thread, posted_on=timezone.now())
             post = testutils.reply_thread(self.thread, posted_on=timezone.now())
 
 
         make_thread_read_aware(self.user, self.thread)
         make_thread_read_aware(self.user, self.thread)
@@ -162,18 +185,22 @@ class GotoNewTests(GotoViewTestCase):
 
 
         response = self.client.get(self.thread.get_new_post_url())
         response = self.client.get(self.thread.get_new_post_url())
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
-        self.assertEqual(response['location'], GOTO_PAGE_URL % (self.thread.get_absolute_url(), 2, post.pk))
+        self.assertEqual(
+            response['location'], GOTO_PAGE_URL % (self.thread.get_absolute_url(), 2, post.pk)
+        )
 
 
     def test_guest_goto_first_new_post_in_thread(self):
     def test_guest_goto_first_new_post_in_thread(self):
         """guest goto new in read thread points to last post"""
         """guest goto new in read thread points to last post"""
-        for i in range(settings.MISAGO_POSTS_PER_PAGE + settings.MISAGO_POSTS_TAIL):
+        for _ in range(settings.MISAGO_POSTS_PER_PAGE + settings.MISAGO_POSTS_TAIL):
             post = testutils.reply_thread(self.thread, posted_on=timezone.now())
             post = testutils.reply_thread(self.thread, posted_on=timezone.now())
 
 
         self.logout_user()
         self.logout_user()
 
 
         response = self.client.get(self.thread.get_new_post_url())
         response = self.client.get(self.thread.get_new_post_url())
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
-        self.assertEqual(response['location'], GOTO_PAGE_URL % (self.thread.get_absolute_url(), 2, post.pk))
+        self.assertEqual(
+            response['location'], GOTO_PAGE_URL % (self.thread.get_absolute_url(), 2, post.pk)
+        )
 
 
 
 
 class GotoUnapprovedTests(GotoViewTestCase):
 class GotoUnapprovedTests(GotoViewTestCase):
@@ -197,22 +224,27 @@ class GotoUnapprovedTests(GotoViewTestCase):
 
 
         response = self.client.get(self.thread.get_unapproved_post_url())
         response = self.client.get(self.thread.get_unapproved_post_url())
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
-        self.assertEqual(response['location'], GOTO_URL % (self.thread.get_absolute_url(), self.thread.first_post_id))
+        self.assertEqual(
+            response['location'],
+            GOTO_URL % (self.thread.get_absolute_url(), self.thread.first_post_id)
+        )
 
 
     def test_vie_handles_unapproved_posts(self):
     def test_vie_handles_unapproved_posts(self):
         """if thread has unapproved posts, redirect to first of them"""
         """if thread has unapproved posts, redirect to first of them"""
-        for i in range(settings.MISAGO_POSTS_PER_PAGE + settings.MISAGO_POSTS_TAIL):
+        for _ in range(settings.MISAGO_POSTS_PER_PAGE + settings.MISAGO_POSTS_TAIL):
             testutils.reply_thread(self.thread, posted_on=timezone.now())
             testutils.reply_thread(self.thread, posted_on=timezone.now())
 
 
         make_thread_read_aware(self.user, self.thread)
         make_thread_read_aware(self.user, self.thread)
         read_thread(self.user, self.thread, self.thread.last_post)
         read_thread(self.user, self.thread, self.thread.last_post)
 
 
         post = testutils.reply_thread(self.thread, is_unapproved=True, posted_on=timezone.now())
         post = testutils.reply_thread(self.thread, is_unapproved=True, posted_on=timezone.now())
-        for i in range(settings.MISAGO_POSTS_PER_PAGE + settings.MISAGO_POSTS_TAIL - 1):
+        for _ in range(settings.MISAGO_POSTS_PER_PAGE + settings.MISAGO_POSTS_TAIL - 1):
             testutils.reply_thread(self.thread, posted_on=timezone.now())
             testutils.reply_thread(self.thread, posted_on=timezone.now())
 
 
         self.grant_permission()
         self.grant_permission()
 
 
         response = self.client.get(self.thread.get_new_post_url())
         response = self.client.get(self.thread.get_new_post_url())
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
-        self.assertEqual(response['location'], GOTO_PAGE_URL % (self.thread.get_absolute_url(), 2, post.pk))
+        self.assertEqual(
+            response['location'], GOTO_PAGE_URL % (self.thread.get_absolute_url(), 2, post.pk)
+        )

+ 57 - 42
misago/threads/tests/test_paginator.py

@@ -11,61 +11,75 @@ class PostsPaginatorTests(TestCase):
         items = [i + 1 for i in range(30)]
         items = [i + 1 for i in range(30)]
 
 
         paginator = PostsPaginator(items, 5)
         paginator = PostsPaginator(items, 5)
-        self.assertEqual(self.get_paginator_items_list(paginator), [
-            [1, 2, 3, 4, 5],
-            [5, 6, 7, 8, 9],
-            [9, 10, 11, 12, 13],
-            [13, 14, 15, 16, 17],
-            [17, 18, 19, 20, 21],
-            [21, 22, 23, 24, 25],
-            [25, 26, 27, 28, 29],
-            [29, 30],
-        ])
+        self.assertEqual(
+            self.get_paginator_items_list(paginator), [
+                [1, 2, 3, 4, 5],
+                [5, 6, 7, 8, 9],
+                [9, 10, 11, 12, 13],
+                [13, 14, 15, 16, 17],
+                [17, 18, 19, 20, 21],
+                [21, 22, 23, 24, 25],
+                [25, 26, 27, 28, 29],
+                [29, 30],
+            ]
+        )
 
 
     def test_paginator_orphans(self):
     def test_paginator_orphans(self):
         """paginator handles orphans"""
         """paginator handles orphans"""
         items = [i + 1 for i in range(16)]
         items = [i + 1 for i in range(16)]
 
 
         paginator = PostsPaginator(items, 8, 6)
         paginator = PostsPaginator(items, 8, 6)
-        self.assertEqual(self.get_paginator_items_list(paginator), [
-            [1, 2, 3, 4, 5, 6, 7, 8],
-            [8, 9, 10, 11, 12, 13, 14, 15, 16],
-        ])
+        self.assertEqual(
+            self.get_paginator_items_list(paginator), [
+                [1, 2, 3, 4, 5, 6, 7, 8],
+                [8, 9, 10, 11, 12, 13, 14, 15, 16],
+            ]
+        )
 
 
         paginator = PostsPaginator(items, 4, 4)
         paginator = PostsPaginator(items, 4, 4)
-        self.assertEqual(self.get_paginator_items_list(paginator), [
-            [1, 2, 3, 4],
-            [4, 5, 6, 7],
-            [7, 8, 9, 10],
-            [10, 11, 12, 13, 14, 15, 16],
-        ])
+        self.assertEqual(
+            self.get_paginator_items_list(paginator), [
+                [1, 2, 3, 4],
+                [4, 5, 6, 7],
+                [7, 8, 9, 10],
+                [10, 11, 12, 13, 14, 15, 16],
+            ]
+        )
 
 
         paginator = PostsPaginator(items, 5, 3)
         paginator = PostsPaginator(items, 5, 3)
-        self.assertEqual(self.get_paginator_items_list(paginator), [
-            [1, 2, 3, 4, 5],
-            [5, 6, 7, 8, 9],
-            [9, 10, 11, 12, 13, 14, 15, 16],
-        ])
+        self.assertEqual(
+            self.get_paginator_items_list(paginator), [
+                [1, 2, 3, 4, 5],
+                [5, 6, 7, 8, 9],
+                [9, 10, 11, 12, 13, 14, 15, 16],
+            ]
+        )
 
 
         paginator = PostsPaginator(items, 6, 2)
         paginator = PostsPaginator(items, 6, 2)
-        self.assertEqual(self.get_paginator_items_list(paginator), [
-            [1, 2, 3, 4, 5, 6],
-            [6, 7, 8, 9, 10, 11],
-            [11, 12, 13, 14, 15, 16],
-        ])
+        self.assertEqual(
+            self.get_paginator_items_list(paginator), [
+                [1, 2, 3, 4, 5, 6],
+                [6, 7, 8, 9, 10, 11],
+                [11, 12, 13, 14, 15, 16],
+            ]
+        )
 
 
         paginator = PostsPaginator(items, 7, 1)
         paginator = PostsPaginator(items, 7, 1)
-        self.assertEqual(self.get_paginator_items_list(paginator), [
-            [1, 2, 3, 4, 5, 6, 7],
-            [7, 8, 9, 10, 11, 12, 13],
-            [13, 14, 15, 16],
-        ])
+        self.assertEqual(
+            self.get_paginator_items_list(paginator), [
+                [1, 2, 3, 4, 5, 6, 7],
+                [7, 8, 9, 10, 11, 12, 13],
+                [13, 14, 15, 16],
+            ]
+        )
 
 
         paginator = PostsPaginator(items, 7, 3)
         paginator = PostsPaginator(items, 7, 3)
-        self.assertEqual(self.get_paginator_items_list(paginator), [
-            [1, 2, 3, 4, 5, 6, 7],
-            [7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
-        ])
+        self.assertEqual(
+            self.get_paginator_items_list(paginator), [
+                [1, 2, 3, 4, 5, 6, 7],
+                [7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
+            ]
+        )
 
 
         paginator = PostsPaginator(items, 10, 6)
         paginator = PostsPaginator(items, 10, 6)
         self.assertEqual(self.get_paginator_items_list(paginator), [items])
         self.assertEqual(self.get_paginator_items_list(paginator), [items])
@@ -84,9 +98,10 @@ class PostsPaginatorTests(TestCase):
                         continue
                         continue
 
 
                     common_part = set(page) & set(compared)
                     common_part = set(page) & set(compared)
-                    self.assertTrue(len(common_part) < 2, "invalid page %s: %s" % (
-                        max(p, c) + 1, sorted(list(common_part))
-                    ))
+                    self.assertTrue(
+                        len(common_part) < 2,
+                        "invalid page %s: %s" % (max(p, c) + 1, sorted(list(common_part)))
+                    )
 
 
     def get_paginator_items_list(self, paginator):
     def get_paginator_items_list(self, paginator):
         items_list = []
         items_list = []

+ 26 - 8
misago/threads/tests/test_participants.py

@@ -23,7 +23,7 @@ class ParticipantsTests(TestCase):
             starter_slug='tester',
             starter_slug='tester',
             last_post_on=datetime,
             last_post_on=datetime,
             last_poster_name='Tester',
             last_poster_name='Tester',
-            last_poster_slug='tester'
+            last_poster_slug='tester',
         )
         )
 
 
         self.thread.set_title("Test thread")
         self.thread.set_title("Test thread")
@@ -38,7 +38,7 @@ class ParticipantsTests(TestCase):
             parsed="<p>Hello! I am test message!</p>",
             parsed="<p>Hello! I am test message!</p>",
             checksum="nope",
             checksum="nope",
             posted_on=datetime,
             posted_on=datetime,
-            updated_on=datetime
+            updated_on=datetime,
         )
         )
 
 
         self.thread.first_post = post
         self.thread.first_post = post
@@ -137,7 +137,10 @@ class ParticipantsTests(TestCase):
 
 
         set_users_unread_private_threads_sync(users=users)
         set_users_unread_private_threads_sync(users=users)
         for user in users:
         for user in users:
-            UserModel.objects.get(pk=user.pk, sync_unread_private_threads=True)
+            UserModel.objects.get(
+                pk=user.pk,
+                sync_unread_private_threads=True,
+            )
 
 
     def test_set_participants_unread_private_threads_sync(self):
     def test_set_participants_unread_private_threads_sync(self):
         """
         """
@@ -153,7 +156,10 @@ class ParticipantsTests(TestCase):
 
 
         set_users_unread_private_threads_sync(participants=participants)
         set_users_unread_private_threads_sync(participants=participants)
         for user in users:
         for user in users:
-            UserModel.objects.get(pk=user.pk, sync_unread_private_threads=True)
+            UserModel.objects.get(
+                pk=user.pk,
+                sync_unread_private_threads=True,
+            )
 
 
     def test_set_participants_users_unread_private_threads_sync(self):
     def test_set_participants_users_unread_private_threads_sync(self):
         """
         """
@@ -168,9 +174,15 @@ class ParticipantsTests(TestCase):
 
 
         users.append(UserModel.objects.create_user("Bob2", "bob2@boberson.com", "Pass.123"))
         users.append(UserModel.objects.create_user("Bob2", "bob2@boberson.com", "Pass.123"))
 
 
-        set_users_unread_private_threads_sync(users=users, participants=participants)
+        set_users_unread_private_threads_sync(
+            users=users,
+            participants=participants,
+        )
         for user in users:
         for user in users:
-            UserModel.objects.get(pk=user.pk, sync_unread_private_threads=True)
+            UserModel.objects.get(
+                pk=user.pk,
+                sync_unread_private_threads=True,
+            )
 
 
     def test_set_users_unread_private_threads_sync_exclude_user(self):
     def test_set_users_unread_private_threads_sync_exclude_user(self):
         """exclude_user kwarg works"""
         """exclude_user kwarg works"""
@@ -179,7 +191,10 @@ class ParticipantsTests(TestCase):
             UserModel.objects.create_user("Bob2", "bob2@boberson.com", "Pass.123")
             UserModel.objects.create_user("Bob2", "bob2@boberson.com", "Pass.123")
         ]
         ]
 
 
-        set_users_unread_private_threads_sync(users=users, exclude_user=users[0])
+        set_users_unread_private_threads_sync(
+            users=users,
+            exclude_user=users[0],
+        )
 
 
         self.assertFalse(UserModel.objects.get(pk=users[0].pk).sync_unread_private_threads)
         self.assertFalse(UserModel.objects.get(pk=users[0].pk).sync_unread_private_threads)
         self.assertTrue(UserModel.objects.get(pk=users[1].pk).sync_unread_private_threads)
         self.assertTrue(UserModel.objects.get(pk=users[1].pk).sync_unread_private_threads)
@@ -189,6 +204,9 @@ class ParticipantsTests(TestCase):
         user = UserModel.objects.create_user("Bob1", "bob1@boberson.com", "Pass.123")
         user = UserModel.objects.create_user("Bob1", "bob1@boberson.com", "Pass.123")
 
 
         with self.assertNumQueries(0):
         with self.assertNumQueries(0):
-            set_users_unread_private_threads_sync(users=[user], exclude_user=user)
+            set_users_unread_private_threads_sync(
+                users=[user],
+                exclude_user=user,
+            )
 
 
         self.assertFalse(UserModel.objects.get(pk=user.pk).sync_unread_private_threads)
         self.assertFalse(UserModel.objects.get(pk=user.pk).sync_unread_private_threads)

+ 74 - 44
misago/threads/tests/test_post_mentions.py

@@ -23,9 +23,11 @@ class PostMentionsTests(AuthenticatedUserTestCase):
         self.thread = testutils.post_thread(category=self.category)
         self.thread = testutils.post_thread(category=self.category)
         self.override_acl()
         self.override_acl()
 
 
-        self.post_link = reverse('misago:api:thread-post-list', kwargs={
-            'thread_pk': self.thread.pk
-        })
+        self.post_link = reverse(
+            'misago:api:thread-post-list', kwargs={
+                'thread_pk': self.thread.pk,
+            }
+        )
 
 
     def override_acl(self):
     def override_acl(self):
         new_acl = self.user.acl_cache
         new_acl = self.user.acl_cache
@@ -34,20 +36,26 @@ class PostMentionsTests(AuthenticatedUserTestCase):
             'can_browse': 1,
             'can_browse': 1,
             'can_start_threads': 1,
             'can_start_threads': 1,
             'can_reply_threads': 1,
             'can_reply_threads': 1,
-            'can_edit_posts': 1
+            'can_edit_posts': 1,
         })
         })
 
 
         override_acl(self.user, new_acl)
         override_acl(self.user, new_acl)
 
 
-    def put(self, url, data=None):
+    def put(
+            self,
+            url,
+            data=None,
+    ):
         content = encode_multipart(BOUNDARY, data or {})
         content = encode_multipart(BOUNDARY, data or {})
         return self.client.put(url, content, content_type=MULTIPART_CONTENT)
         return self.client.put(url, content, content_type=MULTIPART_CONTENT)
 
 
     def test_mention_noone(self):
     def test_mention_noone(self):
         """endpoint handles no mentions in post"""
         """endpoint handles no mentions in post"""
-        response = self.client.post(self.post_link, data={
-            'post': "This is test response!"
-        })
+        response = self.client.post(
+            self.post_link, data={
+                'post': "This is test response!",
+            }
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         post = self.user.post_set.order_by('id').last()
         post = self.user.post_set.order_by('id').last()
@@ -55,9 +63,11 @@ class PostMentionsTests(AuthenticatedUserTestCase):
 
 
     def test_mention_nonexistant(self):
     def test_mention_nonexistant(self):
         """endpoint handles nonexistant mention"""
         """endpoint handles nonexistant mention"""
-        response = self.client.post(self.post_link, data={
-            'post': "This is test response, @InvalidUser!"
-        })
+        response = self.client.post(
+            self.post_link, data={
+                'post': "This is test response, @InvalidUser!",
+            }
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         post = self.user.post_set.order_by('id').last()
         post = self.user.post_set.order_by('id').last()
@@ -65,9 +75,11 @@ class PostMentionsTests(AuthenticatedUserTestCase):
 
 
     def test_mention_self(self):
     def test_mention_self(self):
         """endpoint mentions author"""
         """endpoint mentions author"""
-        response = self.client.post(self.post_link, data={
-            'post': "This is test response, @{}!".format(self.user)
-        })
+        response = self.client.post(
+            self.post_link, data={
+                'post': "This is test response, @{}!".format(self.user),
+            }
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         post = self.user.post_set.order_by('id').last()
         post = self.user.post_set.order_by('id').last()
@@ -80,16 +92,18 @@ class PostMentionsTests(AuthenticatedUserTestCase):
         users = []
         users = []
 
 
         for i in range(MENTIONS_LIMIT + 5):
         for i in range(MENTIONS_LIMIT + 5):
-            users.append(UserModel.objects.create_user(
-                'Mention{}'.format(i),
-                'mention{}@bob.com'.format(i),
-                'pass123'
-            ))
+            users.append(
+                UserModel.objects.
+                create_user('Mention{}'.format(i), 'mention{}@bob.com'.format(i), 'pass123')
+            )
 
 
         mentions = ['@{}'.format(u) for u in users]
         mentions = ['@{}'.format(u) for u in users]
-        response = self.client.post(self.post_link, data={
-            'post': "This is test response, {}!".format(', '.join(mentions))
-        })
+        response = self.client.post(
+            self.post_link,
+            data={
+                'post': "This is test response, {}!".format(', '.join(mentions)),
+            }
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         post = self.user.post_set.order_by('id').last()
         post = self.user.post_set.order_by('id').last()
@@ -102,9 +116,11 @@ class PostMentionsTests(AuthenticatedUserTestCase):
         user_a = UserModel.objects.create_user('Mention', 'mention@test.com', 'pass123')
         user_a = UserModel.objects.create_user('Mention', 'mention@test.com', 'pass123')
         user_b = UserModel.objects.create_user('MentionB', 'mentionb@test.com', 'pass123')
         user_b = UserModel.objects.create_user('MentionB', 'mentionb@test.com', 'pass123')
 
 
-        response = self.client.post(self.post_link, data={
-            'post': "This is test response, @{}!".format(user_a)
-        })
+        response = self.client.post(
+            self.post_link, data={
+                'post': "This is test response, @{}!".format(user_a),
+            }
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         post = self.user.post_set.order_by('id').last()
         post = self.user.post_set.order_by('id').last()
@@ -113,15 +129,20 @@ class PostMentionsTests(AuthenticatedUserTestCase):
         self.assertEqual(post.mentions.order_by('id')[0], user_a)
         self.assertEqual(post.mentions.order_by('id')[0], user_a)
 
 
         # add mention to post
         # add mention to post
-        edit_link = reverse('misago:api:thread-post-detail', kwargs={
-            'thread_pk': self.thread.pk,
-            'pk': post.pk
-        })
+        edit_link = reverse(
+            'misago:api:thread-post-detail', kwargs={
+                'thread_pk': self.thread.pk,
+                'pk': post.pk,
+            }
+        )
 
 
         self.override_acl()
         self.override_acl()
-        response = self.put(edit_link, data={
-            'post': "This is test response, @{} and @{}!".format(user_a, user_b)
-        })
+        response = self.put(
+            edit_link,
+            data={
+                'post': "This is test response, @{} and @{}!".format(user_a, user_b),
+            }
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         self.assertEqual(post.mentions.count(), 2)
         self.assertEqual(post.mentions.count(), 2)
@@ -129,9 +150,11 @@ class PostMentionsTests(AuthenticatedUserTestCase):
 
 
         # remove first mention from post - should preserve mentions
         # remove first mention from post - should preserve mentions
         self.override_acl()
         self.override_acl()
-        response = self.put(edit_link, data={
-            'post': "This is test response, @{}!".format(user_b)
-        })
+        response = self.put(
+            edit_link, data={
+                'post': "This is test response, @{}!".format(user_b),
+            }
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         self.assertEqual(post.mentions.count(), 2)
         self.assertEqual(post.mentions.count(), 2)
@@ -139,9 +162,11 @@ class PostMentionsTests(AuthenticatedUserTestCase):
 
 
         # remove mentions from post - should preserve mentions
         # remove mentions from post - should preserve mentions
         self.override_acl()
         self.override_acl()
-        response = self.put(edit_link, data={
-            'post': "This is test response!"
-        })
+        response = self.put(
+            edit_link, data={
+                'post': "This is test response!",
+            }
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         self.assertEqual(post.mentions.count(), 2)
         self.assertEqual(post.mentions.count(), 2)
@@ -152,9 +177,11 @@ class PostMentionsTests(AuthenticatedUserTestCase):
         user_a = UserModel.objects.create_user('Mention', 'mention@test.com', 'pass123')
         user_a = UserModel.objects.create_user('Mention', 'mention@test.com', 'pass123')
         user_b = UserModel.objects.create_user('MentionB', 'mentionb@test.com', 'pass123')
         user_b = UserModel.objects.create_user('MentionB', 'mentionb@test.com', 'pass123')
 
 
-        response = self.client.post(self.post_link, data={
-            'post': "This is test response, @{}!".format(user_a)
-        })
+        response = self.client.post(
+            self.post_link, data={
+                'post': "This is test response, @{}!".format(user_a),
+            }
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         post_a = self.user.post_set.order_by('id').last()
         post_a = self.user.post_set.order_by('id').last()
@@ -166,9 +193,12 @@ class PostMentionsTests(AuthenticatedUserTestCase):
         self.user.last_post_on = None
         self.user.last_post_on = None
         self.user.save()
         self.user.save()
 
 
-        response = self.client.post(self.post_link, data={
-            'post': "This is test response, @{} and @{}!".format(user_a, user_b)
-        })
+        response = self.client.post(
+            self.post_link,
+            data={
+                'post': "This is test response, @{} and @{}!".format(user_a, user_b),
+            }
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         post_b = self.user.post_set.order_by('id').last()
         post_b = self.user.post_set.order_by('id').last()

+ 51 - 30
misago/threads/tests/test_post_model.py

@@ -26,7 +26,7 @@ class PostModelTests(TestCase):
             starter_slug='tester',
             starter_slug='tester',
             last_post_on=datetime,
             last_post_on=datetime,
             last_poster_name='Tester',
             last_poster_name='Tester',
-            last_poster_slug='tester'
+            last_poster_slug='tester',
         )
         )
 
 
         self.thread.set_title("Test thread")
         self.thread.set_title("Test thread")
@@ -42,7 +42,7 @@ class PostModelTests(TestCase):
             parsed="<p>Hello! I am test message!</p>",
             parsed="<p>Hello! I am test message!</p>",
             checksum="nope",
             checksum="nope",
             posted_on=datetime,
             posted_on=datetime,
-            updated_on=datetime
+            updated_on=datetime,
         )
         )
 
 
         update_post_checksum(self.post)
         update_post_checksum(self.post)
@@ -67,39 +67,60 @@ class PostModelTests(TestCase):
             starter_slug='tester',
             starter_slug='tester',
             last_post_on=timezone.now(),
             last_post_on=timezone.now(),
             last_poster_name='Tester',
             last_poster_name='Tester',
-            last_poster_slug='tester'
+            last_poster_slug='tester',
         )
         )
 
 
+        # can't merge with other users posts
+        with self.assertRaises(ValueError):
+            self.post.merge(
+                Post.objects.create(
+                    category=self.category,
+                    thread=self.thread,
+                    poster=other_user,
+                    poster_name=other_user.username,
+                    poster_ip='127.0.0.1',
+                    original="Hello! I am test message!",
+                    parsed="<p>Hello! I am test message!</p>",
+                    checksum="nope",
+                    posted_on=timezone.now() + timedelta(minutes=5),
+                    updated_on=timezone.now() + timedelta(minutes=5),
+                )
+            )
+
         # can't merge across threads
         # can't merge across threads
         with self.assertRaises(ValueError):
         with self.assertRaises(ValueError):
-            self.post.merge(Post.objects.create(
-                category=self.category,
-                thread=other_thread,
-                poster=self.user,
-                poster_name=self.user.username,
-                poster_ip='127.0.0.1',
-                original="Hello! I am test message!",
-                parsed="<p>Hello! I am test message!</p>",
-                checksum="nope",
-                posted_on=timezone.now() + timedelta(minutes=5),
-                updated_on=timezone.now() + timedelta(minutes=5)
-            ))
+            self.post.merge(
+                Post.objects.create(
+                    category=self.category,
+                    thread=other_thread,
+                    poster=self.user,
+                    poster_name=self.user.username,
+                    poster_ip='127.0.0.1',
+                    original="Hello! I am test message!",
+                    parsed="<p>Hello! I am test message!</p>",
+                    checksum="nope",
+                    posted_on=timezone.now() + timedelta(minutes=5),
+                    updated_on=timezone.now() + timedelta(minutes=5),
+                )
+            )
 
 
         # can't merge with events
         # can't merge with events
         with self.assertRaises(ValueError):
         with self.assertRaises(ValueError):
-            self.post.merge(Post.objects.create(
-                category=self.category,
-                thread=self.thread,
-                poster=self.user,
-                poster_name=self.user.username,
-                poster_ip='127.0.0.1',
-                original="Hello! I am test message!",
-                parsed="<p>Hello! I am test message!</p>",
-                checksum="nope",
-                posted_on=timezone.now() + timedelta(minutes=5),
-                updated_on=timezone.now() + timedelta(minutes=5),
-                is_event=True
-            ))
+            self.post.merge(
+                Post.objects.create(
+                    category=self.category,
+                    thread=self.thread,
+                    poster=self.user,
+                    poster_name=self.user.username,
+                    poster_ip='127.0.0.1',
+                    original="Hello! I am test message!",
+                    parsed="<p>Hello! I am test message!</p>",
+                    checksum="nope",
+                    posted_on=timezone.now() + timedelta(minutes=5),
+                    updated_on=timezone.now() + timedelta(minutes=5),
+                    is_event=True,
+                )
+            )
 
 
     def test_merge(self):
     def test_merge(self):
         """merge method merges two posts into one"""
         """merge method merges two posts into one"""
@@ -113,7 +134,7 @@ class PostModelTests(TestCase):
             parsed="<p>I am other message!</p>",
             parsed="<p>I am other message!</p>",
             checksum="nope",
             checksum="nope",
             posted_on=timezone.now() + timedelta(minutes=5),
             posted_on=timezone.now() + timedelta(minutes=5),
-            updated_on=timezone.now() + timedelta(minutes=5)
+            updated_on=timezone.now() + timedelta(minutes=5),
         )
         )
 
 
         other_post.merge(self.post)
         other_post.merge(self.post)
@@ -131,7 +152,7 @@ class PostModelTests(TestCase):
             starter_slug='tester',
             starter_slug='tester',
             last_post_on=timezone.now(),
             last_post_on=timezone.now(),
             last_poster_name='Tester',
             last_poster_name='Tester',
-            last_poster_slug='tester'
+            last_poster_slug='tester',
         )
         )
 
 
         self.post.move(new_thread)
         self.post.move(new_thread)

+ 319 - 135
misago/threads/tests/test_privatethread_patch_api.py

@@ -21,11 +21,11 @@ class PrivateThreadPatchApiTestCase(PrivateThreadsTestCase):
         self.api_link = self.thread.get_api_url()
         self.api_link = self.thread.get_api_url()
 
 
         self.other_user = UserModel.objects.create_user(
         self.other_user = UserModel.objects.create_user(
-            'BobBoberson', 'bob@boberson.com', 'pass123')
+            'BobBoberson', 'bob@boberson.com', 'pass123'
+        )
 
 
     def patch(self, api_link, ops):
     def patch(self, api_link, ops):
-        return self.client.patch(
-            api_link, json.dumps(ops), content_type="application/json")
+        return self.client.patch(api_link, json.dumps(ops), content_type="application/json")
 
 
 
 
 class PrivateThreadAddParticipantApiTests(PrivateThreadPatchApiTestCase):
 class PrivateThreadAddParticipantApiTests(PrivateThreadPatchApiTestCase):
@@ -33,31 +33,51 @@ class PrivateThreadAddParticipantApiTests(PrivateThreadPatchApiTestCase):
         """non-owner can't add participant"""
         """non-owner can't add participant"""
         ThreadParticipant.objects.add_participants(self.thread, [self.user])
         ThreadParticipant.objects.add_participants(self.thread, [self.user])
 
 
-        response = self.patch(self.api_link, [
-            {'op': 'add', 'path': 'participants', 'value': self.user.username}
-        ])
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'add',
+                    'path': 'participants',
+                    'value': self.user.username,
+                },
+            ]
+        )
 
 
         self.assertContains(
         self.assertContains(
-            response, "be thread owner to add new participants to it", status_code=400)
+            response, "be thread owner to add new participants to it", status_code=400
+        )
 
 
     def test_add_empty_username(self):
     def test_add_empty_username(self):
         """path validates username"""
         """path validates username"""
         ThreadParticipant.objects.set_owner(self.thread, self.user)
         ThreadParticipant.objects.set_owner(self.thread, self.user)
 
 
-        response = self.patch(self.api_link, [
-            {'op': 'add', 'path': 'participants', 'value': ''}
-        ])
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'add',
+                    'path': 'participants',
+                    'value': '',
+                },
+            ]
+        )
 
 
         self.assertContains(
         self.assertContains(
-            response, "You have to enter new participant's username.", status_code=400)
+            response, "You have to enter new participant's username.", status_code=400
+        )
 
 
     def test_add_nonexistant_user(self):
     def test_add_nonexistant_user(self):
         """can't user two times"""
         """can't user two times"""
         ThreadParticipant.objects.set_owner(self.thread, self.user)
         ThreadParticipant.objects.set_owner(self.thread, self.user)
 
 
-        response = self.patch(self.api_link, [
-            {'op': 'add', 'path': 'participants', 'value': 'InvalidUser'}
-        ])
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'add',
+                    'path': 'participants',
+                    'value': 'InvalidUser',
+                },
+            ]
+        )
 
 
         self.assertContains(response, "No user with such name exists.", status_code=400)
         self.assertContains(response, "No user with such name exists.", status_code=400)
 
 
@@ -65,21 +85,32 @@ class PrivateThreadAddParticipantApiTests(PrivateThreadPatchApiTestCase):
         """can't add user that is already participant"""
         """can't add user that is already participant"""
         ThreadParticipant.objects.set_owner(self.thread, self.user)
         ThreadParticipant.objects.set_owner(self.thread, self.user)
 
 
-        response = self.patch(self.api_link, [
-            {'op': 'add', 'path': 'participants', 'value': self.user.username}
-        ])
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'add',
+                    'path': 'participants',
+                    'value': self.user.username,
+                },
+            ]
+        )
 
 
-        self.assertContains(
-            response, "This user is already thread participant", status_code=400)
+        self.assertContains(response, "This user is already thread participant", status_code=400)
 
 
     def test_add_blocking_user(self):
     def test_add_blocking_user(self):
         """can't add user that is already participant"""
         """can't add user that is already participant"""
         ThreadParticipant.objects.set_owner(self.thread, self.user)
         ThreadParticipant.objects.set_owner(self.thread, self.user)
         self.other_user.blocks.add(self.user)
         self.other_user.blocks.add(self.user)
 
 
-        response = self.patch(self.api_link, [
-            {'op': 'add', 'path': 'participants', 'value': self.other_user.username}
-        ])
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'add',
+                    'path': 'participants',
+                    'value': self.other_user.username,
+                },
+            ]
+        )
 
 
         self.assertContains(response, "BobBoberson is blocking you.", status_code=400)
         self.assertContains(response, "BobBoberson is blocking you.", status_code=400)
 
 
@@ -87,13 +118,17 @@ class PrivateThreadAddParticipantApiTests(PrivateThreadPatchApiTestCase):
         """can't add user that has no permission to use private threads"""
         """can't add user that has no permission to use private threads"""
         ThreadParticipant.objects.set_owner(self.thread, self.user)
         ThreadParticipant.objects.set_owner(self.thread, self.user)
 
 
-        override_acl(self.other_user, {
-            'can_use_private_threads': 0
-        })
+        override_acl(self.other_user, {'can_use_private_threads': 0})
 
 
-        response = self.patch(self.api_link, [
-            {'op': 'add', 'path': 'participants', 'value': self.other_user.username}
-        ])
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'add',
+                    'path': 'participants',
+                    'value': self.other_user.username,
+                },
+            ]
+        )
 
 
         self.assertContains(response, "BobBoberson can't participate", status_code=400)
         self.assertContains(response, "BobBoberson can't participate", status_code=400)
 
 
@@ -103,15 +138,23 @@ class PrivateThreadAddParticipantApiTests(PrivateThreadPatchApiTestCase):
 
 
         for i in range(self.user.acl_cache['max_private_thread_participants']):
         for i in range(self.user.acl_cache['max_private_thread_participants']):
             user = UserModel.objects.create_user(
             user = UserModel.objects.create_user(
-                'User{}'.format(i), 'user{}@example.com'.format(i), 'Pass.123')
+                'User{}'.format(i), 'user{}@example.com'.format(i), 'Pass.123'
+            )
             ThreadParticipant.objects.add_participants(self.thread, [user])
             ThreadParticipant.objects.add_participants(self.thread, [user])
 
 
-        response = self.patch(self.api_link, [
-            {'op': 'add', 'path': 'participants', 'value': self.other_user.username}
-        ])
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'add',
+                    'path': 'participants',
+                    'value': self.other_user.username,
+                },
+            ]
+        )
 
 
         self.assertContains(
         self.assertContains(
-            response, "You can't add any more new users to this thread.", status_code=400)
+            response, "You can't add any more new users to this thread.", status_code=400
+        )
 
 
     def test_add_user_closed_thread(self):
     def test_add_user_closed_thread(self):
         """adding user to closed thread fails for non-moderator"""
         """adding user to closed thread fails for non-moderator"""
@@ -120,20 +163,33 @@ class PrivateThreadAddParticipantApiTests(PrivateThreadPatchApiTestCase):
         self.thread.is_closed = True
         self.thread.is_closed = True
         self.thread.save()
         self.thread.save()
 
 
-        response = self.patch(self.api_link, [
-            {'op': 'add', 'path': 'participants', 'value': self.other_user.username}
-        ])
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'add',
+                    'path': 'participants',
+                    'value': self.other_user.username,
+                },
+            ]
+        )
 
 
         self.assertContains(
         self.assertContains(
-            response, "Only moderators can add participants to closed threads.", status_code=400)
+            response, "Only moderators can add participants to closed threads.", status_code=400
+        )
 
 
     def test_add_user(self):
     def test_add_user(self):
         """adding user to thread add user to thread as participant, sets event and emails him"""
         """adding user to thread add user to thread as participant, sets event and emails him"""
         ThreadParticipant.objects.set_owner(self.thread, self.user)
         ThreadParticipant.objects.set_owner(self.thread, self.user)
 
 
-        response = self.patch(self.api_link, [
-            {'op': 'add', 'path': 'participants', 'value': self.other_user.username}
-        ])
+        self.patch(
+            self.api_link, [
+                {
+                    'op': 'add',
+                    'path': 'participants',
+                    'value': self.other_user.username,
+                },
+            ]
+        )
 
 
         # event was set on thread
         # event was set on thread
         event = self.thread.post_set.order_by('id').last()
         event = self.thread.post_set.order_by('id').last()
@@ -154,13 +210,17 @@ class PrivateThreadAddParticipantApiTests(PrivateThreadPatchApiTestCase):
         self.thread.has_reported_posts = True
         self.thread.has_reported_posts = True
         self.thread.save()
         self.thread.save()
 
 
-        override_acl(self.user, {
-            'can_moderate_private_threads': 1
-        })
+        override_acl(self.user, {'can_moderate_private_threads': 1})
 
 
-        response = self.patch(self.api_link, [
-            {'op': 'add', 'path': 'participants', 'value': self.user.username}
-        ])
+        self.patch(
+            self.api_link, [
+                {
+                    'op': 'add',
+                    'path': 'participants',
+                    'value': self.user.username,
+                },
+            ]
+        )
 
 
         # event was set on thread
         # event was set on thread
         event = self.thread.post_set.order_by('id').last()
         event = self.thread.post_set.order_by('id').last()
@@ -177,13 +237,17 @@ class PrivateThreadAddParticipantApiTests(PrivateThreadPatchApiTestCase):
         self.thread.is_closed = True
         self.thread.is_closed = True
         self.thread.save()
         self.thread.save()
 
 
-        override_acl(self.user, {
-            'can_moderate_private_threads': 1
-        })
+        override_acl(self.user, {'can_moderate_private_threads': 1})
 
 
-        response = self.patch(self.api_link, [
-            {'op': 'add', 'path': 'participants', 'value': self.other_user.username}
-        ])
+        self.patch(
+            self.api_link, [
+                {
+                    'op': 'add',
+                    'path': 'participants',
+                    'value': self.other_user.username,
+                },
+            ]
+        )
 
 
         # event was set on thread
         # event was set on thread
         event = self.thread.post_set.order_by('id').last()
         event = self.thread.post_set.order_by('id').last()
@@ -203,9 +267,15 @@ class PrivateThreadRemoveParticipantApiTests(PrivateThreadPatchApiTestCase):
         """api handles empty user id"""
         """api handles empty user id"""
         ThreadParticipant.objects.set_owner(self.thread, self.user)
         ThreadParticipant.objects.set_owner(self.thread, self.user)
 
 
-        response = self.patch(self.api_link, [
-            {'op': 'remove', 'path': 'participants', 'value': 'string'}
-        ])
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'remove',
+                    'path': 'participants',
+                    'value': 'string',
+                },
+            ]
+        )
 
 
         self.assertContains(response, "Participant doesn't exist.", status_code=400)
         self.assertContains(response, "Participant doesn't exist.", status_code=400)
 
 
@@ -213,9 +283,15 @@ class PrivateThreadRemoveParticipantApiTests(PrivateThreadPatchApiTestCase):
         """api validates user id type"""
         """api validates user id type"""
         ThreadParticipant.objects.set_owner(self.thread, self.user)
         ThreadParticipant.objects.set_owner(self.thread, self.user)
 
 
-        response = self.patch(self.api_link, [
-            {'op': 'remove', 'path': 'participants', 'value': 'string'}
-        ])
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'remove',
+                    'path': 'participants',
+                    'value': 'string',
+                },
+            ]
+        )
 
 
         self.assertContains(response, "Participant doesn't exist.", status_code=400)
         self.assertContains(response, "Participant doesn't exist.", status_code=400)
 
 
@@ -223,9 +299,15 @@ class PrivateThreadRemoveParticipantApiTests(PrivateThreadPatchApiTestCase):
         """removed user has to be participant"""
         """removed user has to be participant"""
         ThreadParticipant.objects.set_owner(self.thread, self.user)
         ThreadParticipant.objects.set_owner(self.thread, self.user)
 
 
-        response = self.patch(self.api_link, [
-            {'op': 'remove', 'path': 'participants', 'value': self.other_user.pk}
-        ])
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'remove',
+                    'path': 'participants',
+                    'value': self.other_user.pk,
+                },
+            ]
+        )
 
 
         self.assertContains(response, "Participant doesn't exist.", status_code=400)
         self.assertContains(response, "Participant doesn't exist.", status_code=400)
 
 
@@ -234,12 +316,19 @@ class PrivateThreadRemoveParticipantApiTests(PrivateThreadPatchApiTestCase):
         ThreadParticipant.objects.set_owner(self.thread, self.other_user)
         ThreadParticipant.objects.set_owner(self.thread, self.other_user)
         ThreadParticipant.objects.add_participants(self.thread, [self.user])
         ThreadParticipant.objects.add_participants(self.thread, [self.user])
 
 
-        response = self.patch(self.api_link, [
-            {'op': 'remove', 'path': 'participants', 'value': self.other_user.pk}
-        ])
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'remove',
+                    'path': 'participants',
+                    'value': self.other_user.pk,
+                },
+            ]
+        )
 
 
         self.assertContains(
         self.assertContains(
-            response, "be thread owner to remove participants from it", status_code=400)
+            response, "be thread owner to remove participants from it", status_code=400
+        )
 
 
     def test_owner_remove_user_closed_thread(self):
     def test_owner_remove_user_closed_thread(self):
         """api disallows owner to remove other user from closed thread"""
         """api disallows owner to remove other user from closed thread"""
@@ -249,12 +338,19 @@ class PrivateThreadRemoveParticipantApiTests(PrivateThreadPatchApiTestCase):
         self.thread.is_closed = True
         self.thread.is_closed = True
         self.thread.save()
         self.thread.save()
 
 
-        response = self.patch(self.api_link, [
-            {'op': 'remove', 'path': 'participants', 'value': self.other_user.pk}
-        ])
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'remove',
+                    'path': 'participants',
+                    'value': self.other_user.pk,
+                },
+            ]
+        )
 
 
         self.assertContains(
         self.assertContains(
-            response, "moderators can remove participants from closed threads", status_code=400)
+            response, "moderators can remove participants from closed threads", status_code=400
+        )
 
 
     def test_user_leave_thread(self):
     def test_user_leave_thread(self):
         """api allows user to remove himself from thread"""
         """api allows user to remove himself from thread"""
@@ -266,9 +362,15 @@ class PrivateThreadRemoveParticipantApiTests(PrivateThreadPatchApiTestCase):
             thread=self.thread,
             thread=self.thread,
         )
         )
 
 
-        response = self.patch(self.api_link, [
-            {'op': 'remove', 'path': 'participants', 'value': self.user.pk}
-        ])
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'remove',
+                    'path': 'participants',
+                    'value': self.user.pk,
+                },
+            ]
+        )
 
 
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
         self.assertFalse(response.json()['deleted'])
         self.assertFalse(response.json()['deleted'])
@@ -300,9 +402,15 @@ class PrivateThreadRemoveParticipantApiTests(PrivateThreadPatchApiTestCase):
         self.thread.is_closed = True
         self.thread.is_closed = True
         self.thread.save()
         self.thread.save()
 
 
-        response = self.patch(self.api_link, [
-            {'op': 'remove', 'path': 'participants', 'value': self.user.pk}
-        ])
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'remove',
+                    'path': 'participants',
+                    'value': self.user.pk,
+                },
+            ]
+        )
 
 
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
         self.assertFalse(response.json()['deleted'])
         self.assertFalse(response.json()['deleted'])
@@ -325,19 +433,22 @@ class PrivateThreadRemoveParticipantApiTests(PrivateThreadPatchApiTestCase):
 
 
     def test_moderator_remove_user(self):
     def test_moderator_remove_user(self):
         """api allows moderator to remove other user"""
         """api allows moderator to remove other user"""
-        removed_user = UserModel.objects.create_user(
-            'Vigilante', 'test@test.com', 'pass123')
+        removed_user = UserModel.objects.create_user('Vigilante', 'test@test.com', 'pass123')
 
 
         ThreadParticipant.objects.set_owner(self.thread, self.other_user)
         ThreadParticipant.objects.set_owner(self.thread, self.other_user)
         ThreadParticipant.objects.add_participants(self.thread, [self.user, removed_user])
         ThreadParticipant.objects.add_participants(self.thread, [self.user, removed_user])
 
 
-        override_acl(self.user, {
-            'can_moderate_private_threads': True
-        })
+        override_acl(self.user, {'can_moderate_private_threads': True})
 
 
-        response = self.patch(self.api_link, [
-            {'op': 'remove', 'path': 'participants', 'value': removed_user.pk}
-        ])
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'remove',
+                    'path': 'participants',
+                    'value': removed_user.pk,
+                },
+            ]
+        )
 
 
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
         self.assertFalse(response.json()['deleted'])
         self.assertFalse(response.json()['deleted'])
@@ -364,9 +475,15 @@ class PrivateThreadRemoveParticipantApiTests(PrivateThreadPatchApiTestCase):
         ThreadParticipant.objects.set_owner(self.thread, self.user)
         ThreadParticipant.objects.set_owner(self.thread, self.user)
         ThreadParticipant.objects.add_participants(self.thread, [self.other_user])
         ThreadParticipant.objects.add_participants(self.thread, [self.other_user])
 
 
-        response = self.patch(self.api_link, [
-            {'op': 'remove', 'path': 'participants', 'value': self.other_user.pk}
-        ])
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'remove',
+                    'path': 'participants',
+                    'value': self.other_user.pk,
+                },
+            ]
+        )
 
 
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
         self.assertFalse(response.json()['deleted'])
         self.assertFalse(response.json()['deleted'])
@@ -392,9 +509,15 @@ class PrivateThreadRemoveParticipantApiTests(PrivateThreadPatchApiTestCase):
         ThreadParticipant.objects.set_owner(self.thread, self.user)
         ThreadParticipant.objects.set_owner(self.thread, self.user)
         ThreadParticipant.objects.add_participants(self.thread, [self.other_user])
         ThreadParticipant.objects.add_participants(self.thread, [self.other_user])
 
 
-        response = self.patch(self.api_link, [
-            {'op': 'remove', 'path': 'participants', 'value': self.user.pk}
-        ])
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'remove',
+                    'path': 'participants',
+                    'value': self.user.pk,
+                },
+            ]
+        )
 
 
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
         self.assertFalse(response.json()['deleted'])
         self.assertFalse(response.json()['deleted'])
@@ -419,9 +542,15 @@ class PrivateThreadRemoveParticipantApiTests(PrivateThreadPatchApiTestCase):
         """api allows last user leave thread, causing thread to delete"""
         """api allows last user leave thread, causing thread to delete"""
         ThreadParticipant.objects.set_owner(self.thread, self.user)
         ThreadParticipant.objects.set_owner(self.thread, self.user)
 
 
-        response = self.patch(self.api_link, [
-            {'op': 'remove', 'path': 'participants', 'value': self.user.pk}
-        ])
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'remove',
+                    'path': 'participants',
+                    'value': self.user.pk,
+                },
+            ]
+        )
 
 
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
         self.assertTrue(response.json()['deleted'])
         self.assertTrue(response.json()['deleted'])
@@ -439,9 +568,15 @@ class PrivateThreadTakeOverApiTests(PrivateThreadPatchApiTestCase):
         """api handles empty user id"""
         """api handles empty user id"""
         ThreadParticipant.objects.set_owner(self.thread, self.user)
         ThreadParticipant.objects.set_owner(self.thread, self.user)
 
 
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'owner', 'value': ''}
-        ])
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'owner',
+                    'value': '',
+                },
+            ]
+        )
 
 
         self.assertContains(response, "Participant doesn't exist.", status_code=400)
         self.assertContains(response, "Participant doesn't exist.", status_code=400)
 
 
@@ -449,9 +584,15 @@ class PrivateThreadTakeOverApiTests(PrivateThreadPatchApiTestCase):
         """api handles invalid user id"""
         """api handles invalid user id"""
         ThreadParticipant.objects.set_owner(self.thread, self.user)
         ThreadParticipant.objects.set_owner(self.thread, self.user)
 
 
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'owner', 'value': 'dsadsa'}
-        ])
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'owner',
+                    'value': 'dsadsa',
+                },
+            ]
+        )
 
 
         self.assertContains(response, "Participant doesn't exist.", status_code=400)
         self.assertContains(response, "Participant doesn't exist.", status_code=400)
 
 
@@ -459,9 +600,15 @@ class PrivateThreadTakeOverApiTests(PrivateThreadPatchApiTestCase):
         """api handles nonexistant user id"""
         """api handles nonexistant user id"""
         ThreadParticipant.objects.set_owner(self.thread, self.user)
         ThreadParticipant.objects.set_owner(self.thread, self.user)
 
 
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'owner', 'value': self.other_user.pk}
-        ])
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'owner',
+                    'value': self.other_user.pk,
+                },
+            ]
+        )
 
 
         self.assertContains(response, "Participant doesn't exist.", status_code=400)
         self.assertContains(response, "Participant doesn't exist.", status_code=400)
 
 
@@ -470,21 +617,34 @@ class PrivateThreadTakeOverApiTests(PrivateThreadPatchApiTestCase):
         ThreadParticipant.objects.set_owner(self.thread, self.other_user)
         ThreadParticipant.objects.set_owner(self.thread, self.other_user)
         ThreadParticipant.objects.add_participants(self.thread, [self.user])
         ThreadParticipant.objects.add_participants(self.thread, [self.user])
 
 
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'owner', 'value': self.user.pk}
-        ])
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'owner',
+                    'value': self.user.pk,
+                },
+            ]
+        )
 
 
         self.assertContains(
         self.assertContains(
-            response, "thread owner and moderators can change threads owners", status_code=400)
+            response, "thread owner and moderators can change threads owners", status_code=400
+        )
 
 
     def test_no_change(self):
     def test_no_change(self):
         """api validates that new owner id is same as current owner"""
         """api validates that new owner id is same as current owner"""
         ThreadParticipant.objects.set_owner(self.thread, self.user)
         ThreadParticipant.objects.set_owner(self.thread, self.user)
         ThreadParticipant.objects.add_participants(self.thread, [self.other_user])
         ThreadParticipant.objects.add_participants(self.thread, [self.other_user])
 
 
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'owner', 'value': self.user.pk}
-        ])
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'owner',
+                    'value': self.user.pk,
+                },
+            ]
+        )
 
 
         self.assertContains(response, "This user already is thread owner.", status_code=400)
         self.assertContains(response, "This user already is thread owner.", status_code=400)
 
 
@@ -496,21 +656,34 @@ class PrivateThreadTakeOverApiTests(PrivateThreadPatchApiTestCase):
         self.thread.is_closed = True
         self.thread.is_closed = True
         self.thread.save()
         self.thread.save()
 
 
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'owner', 'value': self.other_user.pk}
-        ])
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'owner',
+                    'value': self.other_user.pk,
+                },
+            ]
+        )
 
 
         self.assertContains(
         self.assertContains(
-            response, "Only moderators can change closed threads owners.", status_code=400)
+            response, "Only moderators can change closed threads owners.", status_code=400
+        )
 
 
     def test_owner_change_thread_owner(self):
     def test_owner_change_thread_owner(self):
         """owner can pass thread ownership to other participant"""
         """owner can pass thread ownership to other participant"""
         ThreadParticipant.objects.set_owner(self.thread, self.user)
         ThreadParticipant.objects.set_owner(self.thread, self.user)
         ThreadParticipant.objects.add_participants(self.thread, [self.other_user])
         ThreadParticipant.objects.add_participants(self.thread, [self.other_user])
 
 
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'owner', 'value': self.other_user.pk}
-        ])
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'owner',
+                    'value': self.other_user.pk,
+                },
+            ]
+        )
 
 
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
@@ -530,19 +703,22 @@ class PrivateThreadTakeOverApiTests(PrivateThreadPatchApiTestCase):
 
 
     def test_moderator_change_owner(self):
     def test_moderator_change_owner(self):
         """moderator can change thread owner to other user"""
         """moderator can change thread owner to other user"""
-        new_owner = UserModel.objects.create_user(
-            'NewOwner', 'new@owner.com', 'pass123')
+        new_owner = UserModel.objects.create_user('NewOwner', 'new@owner.com', 'pass123')
 
 
         ThreadParticipant.objects.set_owner(self.thread, self.other_user)
         ThreadParticipant.objects.set_owner(self.thread, self.other_user)
         ThreadParticipant.objects.add_participants(self.thread, [self.user, new_owner])
         ThreadParticipant.objects.add_participants(self.thread, [self.user, new_owner])
 
 
-        override_acl(self.user, {
-            'can_moderate_private_threads': 1
-        })
+        override_acl(self.user, {'can_moderate_private_threads': 1})
 
 
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'owner', 'value': new_owner.pk}
-        ])
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'owner',
+                    'value': new_owner.pk,
+                },
+            ]
+        )
 
 
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
@@ -567,13 +743,17 @@ class PrivateThreadTakeOverApiTests(PrivateThreadPatchApiTestCase):
         ThreadParticipant.objects.set_owner(self.thread, self.other_user)
         ThreadParticipant.objects.set_owner(self.thread, self.other_user)
         ThreadParticipant.objects.add_participants(self.thread, [self.user])
         ThreadParticipant.objects.add_participants(self.thread, [self.user])
 
 
-        override_acl(self.user, {
-            'can_moderate_private_threads': 1
-        })
+        override_acl(self.user, {'can_moderate_private_threads': 1})
 
 
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'owner', 'value': self.user.pk}
-        ])
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'owner',
+                    'value': self.user.pk,
+                },
+            ]
+        )
 
 
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
@@ -599,13 +779,17 @@ class PrivateThreadTakeOverApiTests(PrivateThreadPatchApiTestCase):
         self.thread.is_closed = True
         self.thread.is_closed = True
         self.thread.save()
         self.thread.save()
 
 
-        override_acl(self.user, {
-            'can_moderate_private_threads': 1
-        })
+        override_acl(self.user, {'can_moderate_private_threads': 1})
 
 
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'owner', 'value': self.user.pk}
-        ])
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'owner',
+                    'value': self.user.pk,
+                },
+            ]
+        )
 
 
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 

+ 7 - 5
misago/threads/tests/test_privatethread_reply_api.py

@@ -1,6 +1,5 @@
 from django.contrib.auth import get_user_model
 from django.contrib.auth import get_user_model
 
 
-from misago.acl.testutils import override_acl
 from misago.threads import testutils
 from misago.threads import testutils
 from misago.threads.models import ThreadParticipant
 from misago.threads.models import ThreadParticipant
 
 
@@ -18,16 +17,19 @@ class PrivateThreadReplyApiTestCase(PrivateThreadsTestCase):
         self.api_link = self.thread.get_posts_api_url()
         self.api_link = self.thread.get_posts_api_url()
 
 
         self.other_user = UserModel.objects.create_user(
         self.other_user = UserModel.objects.create_user(
-            'BobBoberson', 'bob@boberson.com', 'pass123')
+            'BobBoberson', 'bob@boberson.com', 'pass123'
+        )
 
 
     def test_reply_private_thread(self):
     def test_reply_private_thread(self):
         """api sets other private thread participants sync thread flag"""
         """api sets other private thread participants sync thread flag"""
         ThreadParticipant.objects.set_owner(self.thread, self.user)
         ThreadParticipant.objects.set_owner(self.thread, self.user)
         ThreadParticipant.objects.add_participants(self.thread, [self.other_user])
         ThreadParticipant.objects.add_participants(self.thread, [self.other_user])
 
 
-        response = self.client.post(self.api_link, data={
-            'post': "This is test response!"
-        })
+        response = self.client.post(
+            self.api_link, data={
+                'post': "This is test response!",
+            }
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         # don't count private thread replies
         # don't count private thread replies

+ 195 - 167
misago/threads/tests/test_privatethread_start_api.py

@@ -1,8 +1,6 @@
 # -*- coding: utf-8 -*-
 # -*- coding: utf-8 -*-
 from __future__ import unicode_literals
 from __future__ import unicode_literals
 
 
-import json
-
 from django.contrib.auth import get_user_model
 from django.contrib.auth import get_user_model
 from django.core import mail
 from django.core import mail
 from django.urls import reverse
 from django.urls import reverse
@@ -10,7 +8,7 @@ from django.utils.encoding import smart_str
 
 
 from misago.acl.testutils import override_acl
 from misago.acl.testutils import override_acl
 from misago.categories.models import Category
 from misago.categories.models import Category
-from misago.threads.models import Thread, ThreadParticipant
+from misago.threads.models import ThreadParticipant
 from misago.users.testutils import AuthenticatedUserTestCase
 from misago.users.testutils import AuthenticatedUserTestCase
 
 
 
 
@@ -25,7 +23,8 @@ class StartPrivateThreadTests(AuthenticatedUserTestCase):
         self.api_link = reverse('misago:api:private-thread-list')
         self.api_link = reverse('misago:api:private-thread-list')
 
 
         self.other_user = UserModel.objects.create_user(
         self.other_user = UserModel.objects.create_user(
-            'BobBoberson', 'bob@boberson.com', 'pass123')
+            'BobBoberson', 'bob@boberson.com', 'pass123'
+        )
 
 
     def test_cant_start_thread_as_guest(self):
     def test_cant_start_thread_as_guest(self):
         """user has to be authenticated to be able to post private thread"""
         """user has to be authenticated to be able to post private thread"""
@@ -53,144 +52,160 @@ class StartPrivateThreadTests(AuthenticatedUserTestCase):
         response = self.client.post(self.api_link, data={})
         response = self.client.post(self.api_link, data={})
 
 
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'to': [
-                "You have to enter user names."
-            ],
-            'title':[
-                "You have to enter thread title."
-            ],
-            'post': [
-                "You have to enter a message."
-            ]
-        })
+        self.assertEqual(
+            response.json(), {
+                'to': ["You have to enter user names."],
+                'title': ["You have to enter thread title."],
+                'post': ["You have to enter a message."],
+            }
+        )
 
 
     def test_title_is_validated(self):
     def test_title_is_validated(self):
         """title is validated"""
         """title is validated"""
-        response = self.client.post(self.api_link, data={
-            'to': [self.other_user.username],
-            'title': "------",
-            'post': "Lorem ipsum dolor met, sit amet elit!",
-        })
+        response = self.client.post(
+            self.api_link,
+            data={
+                'to': [self.other_user.username],
+                'title': "------",
+                'post': "Lorem ipsum dolor met, sit amet elit!",
+            }
+        )
 
 
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'title': [
-                "Thread title should contain alpha-numeric characters."
-            ]
-        })
+        self.assertEqual(
+            response.json(), {
+                'title': ["Thread title should contain alpha-numeric characters."],
+            }
+        )
 
 
     def test_post_is_validated(self):
     def test_post_is_validated(self):
         """post is validated"""
         """post is validated"""
-        response = self.client.post(self.api_link, data={
-            'to': [self.other_user.username],
-            'title': "Lorem ipsum dolor met",
-            'post': "a",
-        })
+        response = self.client.post(
+            self.api_link,
+            data={
+                'to': [self.other_user.username],
+                'title': "Lorem ipsum dolor met",
+                'post': "a",
+            }
+        )
 
 
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'post': [
-                "Posted message should be at least 5 characters long (it has 1)."
-            ]
-        })
+        self.assertEqual(
+            response.json(), {
+                'post': ["Posted message should be at least 5 characters long (it has 1)."],
+            }
+        )
 
 
     def test_cant_invite_self(self):
     def test_cant_invite_self(self):
         """api validates that you cant invite yourself to private thread"""
         """api validates that you cant invite yourself to private thread"""
-        response = self.client.post(self.api_link, data={
-            'to': [self.user.username],
-            'title': "Lorem ipsum dolor met",
-            'post': "Lorem ipsum dolor.",
-        })
+        response = self.client.post(
+            self.api_link,
+            data={
+                'to': [self.user.username],
+                'title': "Lorem ipsum dolor met",
+                'post': "Lorem ipsum dolor.",
+            }
+        )
 
 
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'to': [
-                "You can't include yourself on the list of users to invite to new thread."
-            ]
-        })
+        self.assertEqual(
+            response.json(), {
+                'to': ["You can't include yourself on the list of users to invite to new thread."],
+            }
+        )
 
 
     def test_cant_invite_nonexisting(self):
     def test_cant_invite_nonexisting(self):
         """api validates that you cant invite nonexisting user to thread"""
         """api validates that you cant invite nonexisting user to thread"""
-        response = self.client.post(self.api_link, data={
-            'to': ['Ab', 'Cd'],
-            'title': "Lorem ipsum dolor met",
-            'post': "Lorem ipsum dolor.",
-        })
+        response = self.client.post(
+            self.api_link,
+            data={
+                'to': ['Ab', 'Cd'],
+                'title': "Lorem ipsum dolor met",
+                'post': "Lorem ipsum dolor.",
+            }
+        )
 
 
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'to': [
-                "One or more users could not be found: ab, cd"
-            ]
-        })
+        self.assertEqual(
+            response.json(), {
+                'to': ["One or more users could not be found: ab, cd"],
+            }
+        )
 
 
     def test_cant_invite_too_many(self):
     def test_cant_invite_too_many(self):
         """api validates that you cant invite too many users to thread"""
         """api validates that you cant invite too many users to thread"""
-        response = self.client.post(self.api_link, data={
-            'to': ['Username{}'.format(i) for i in range(50)],
-            'title': "Lorem ipsum dolor met",
-            'post': "Lorem ipsum dolor.",
-        })
+        response = self.client.post(
+            self.api_link,
+            data={
+                'to': ['Username{}'.format(i) for i in range(50)],
+                'title': "Lorem ipsum dolor met",
+                'post': "Lorem ipsum dolor.",
+            }
+        )
 
 
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'to': [
-                "You can't add more than 3 users to private thread (you've added 50)."
-            ]
-        })
+        self.assertEqual(
+            response.json(), {
+                'to': ["You can't add more than 3 users to private thread (you've added 50)."],
+            }
+        )
 
 
     def test_cant_invite_no_permission(self):
     def test_cant_invite_no_permission(self):
         """api validates invited user permission to private thread"""
         """api validates invited user permission to private thread"""
-        override_acl(self.other_user, {
-            'can_use_private_threads': 0
-        })
-
-        response = self.client.post(self.api_link, data={
-            'to': [self.other_user.username],
-            'title': "Lorem ipsum dolor met",
-            'post': "Lorem ipsum dolor.",
-        })
+        override_acl(self.other_user, {'can_use_private_threads': 0})
+
+        response = self.client.post(
+            self.api_link,
+            data={
+                'to': [self.other_user.username],
+                'title': "Lorem ipsum dolor met",
+                'post': "Lorem ipsum dolor.",
+            }
+        )
 
 
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'to': [
-                "BobBoberson can't participate in private threads."
-            ]
-        })
+        self.assertEqual(
+            response.json(), {
+                'to': ["BobBoberson can't participate in private threads."],
+            }
+        )
 
 
     def test_cant_invite_blocking(self):
     def test_cant_invite_blocking(self):
         """api validates that you cant invite blocking user to thread"""
         """api validates that you cant invite blocking user to thread"""
         self.other_user.blocks.add(self.user)
         self.other_user.blocks.add(self.user)
 
 
-        response = self.client.post(self.api_link, data={
-            'to': [self.other_user.username],
-            'title': "Lorem ipsum dolor met",
-            'post': "Lorem ipsum dolor.",
-        })
+        response = self.client.post(
+            self.api_link,
+            data={
+                'to': [self.other_user.username],
+                'title': "Lorem ipsum dolor met",
+                'post': "Lorem ipsum dolor.",
+            }
+        )
 
 
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.json(), {
         self.assertEqual(response.json(), {
-            'to': [
-                "BobBoberson is blocking you."
-            ]
+            'to': ["BobBoberson is blocking you."],
         })
         })
 
 
         # allow us to bypass blocked check
         # allow us to bypass blocked check
         override_acl(self.user, {'can_add_everyone_to_private_threads': 1})
         override_acl(self.user, {'can_add_everyone_to_private_threads': 1})
 
 
-        response = self.client.post(self.api_link, data={
-            'to': [self.other_user.username],
-            'title': "-----",
-            'post': "Lorem ipsum dolor.",
-        })
+        response = self.client.post(
+            self.api_link,
+            data={
+                'to': [self.other_user.username],
+                'title': "-----",
+                'post': "Lorem ipsum dolor.",
+            }
+        )
 
 
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'title': [
-                "Thread title should contain alpha-numeric characters."
-            ]
-        })
+        self.assertEqual(
+            response.json(), {
+                'title': ["Thread title should contain alpha-numeric characters."],
+            }
+        )
 
 
     def test_cant_invite_followers_only(self):
     def test_cant_invite_followers_only(self):
         """api validates that you cant invite followers-only user to thread"""
         """api validates that you cant invite followers-only user to thread"""
@@ -198,51 +213,60 @@ class StartPrivateThreadTests(AuthenticatedUserTestCase):
         self.other_user.limits_private_thread_invites_to = user_constant
         self.other_user.limits_private_thread_invites_to = user_constant
         self.other_user.save()
         self.other_user.save()
 
 
-        response = self.client.post(self.api_link, data={
-            'to': [self.other_user.username],
-            'title': "Lorem ipsum dolor met",
-            'post': "Lorem ipsum dolor.",
-        })
+        response = self.client.post(
+            self.api_link,
+            data={
+                'to': [self.other_user.username],
+                'title': "Lorem ipsum dolor met",
+                'post': "Lorem ipsum dolor.",
+            }
+        )
 
 
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'to': [
-                "BobBoberson limits invitations to private threads to followed users."
-            ]
-        })
+        self.assertEqual(
+            response.json(), {
+                'to': ["BobBoberson limits invitations to private threads to followed users."],
+            }
+        )
 
 
         # allow us to bypass following check
         # allow us to bypass following check
         override_acl(self.user, {'can_add_everyone_to_private_threads': 1})
         override_acl(self.user, {'can_add_everyone_to_private_threads': 1})
 
 
-        response = self.client.post(self.api_link, data={
-            'to': [self.other_user.username],
-            'title': "-----",
-            'post': "Lorem ipsum dolor.",
-        })
+        response = self.client.post(
+            self.api_link,
+            data={
+                'to': [self.other_user.username],
+                'title': "-----",
+                'post': "Lorem ipsum dolor.",
+            }
+        )
 
 
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'title': [
-                "Thread title should contain alpha-numeric characters."
-            ]
-        })
+        self.assertEqual(
+            response.json(), {
+                'title': ["Thread title should contain alpha-numeric characters."],
+            }
+        )
 
 
         # make user follow us
         # make user follow us
         override_acl(self.user, {'can_add_everyone_to_private_threads': 0})
         override_acl(self.user, {'can_add_everyone_to_private_threads': 0})
         self.other_user.follows.add(self.user)
         self.other_user.follows.add(self.user)
 
 
-        response = self.client.post(self.api_link, data={
-            'to': [self.other_user.username],
-            'title': "-----",
-            'post': "Lorem ipsum dolor.",
-        })
+        response = self.client.post(
+            self.api_link,
+            data={
+                'to': [self.other_user.username],
+                'title': "-----",
+                'post': "Lorem ipsum dolor.",
+            }
+        )
 
 
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'title': [
-                "Thread title should contain alpha-numeric characters."
-            ]
-        })
+        self.assertEqual(
+            response.json(), {
+                'title': ["Thread title should contain alpha-numeric characters."],
+            }
+        )
 
 
     def test_cant_invite_anyone(self):
     def test_cant_invite_anyone(self):
         """api validates that you cant invite nobody user to thread"""
         """api validates that you cant invite nobody user to thread"""
@@ -250,42 +274,51 @@ class StartPrivateThreadTests(AuthenticatedUserTestCase):
         self.other_user.limits_private_thread_invites_to = user_constant
         self.other_user.limits_private_thread_invites_to = user_constant
         self.other_user.save()
         self.other_user.save()
 
 
-        response = self.client.post(self.api_link, data={
-            'to': [self.other_user.username],
-            'title': "Lorem ipsum dolor met",
-            'post': "Lorem ipsum dolor.",
-        })
+        response = self.client.post(
+            self.api_link,
+            data={
+                'to': [self.other_user.username],
+                'title': "Lorem ipsum dolor met",
+                'post': "Lorem ipsum dolor.",
+            }
+        )
 
 
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'to': [
-                "BobBoberson is not allowing invitations to private threads."
-            ]
-        })
+        self.assertEqual(
+            response.json(), {
+                'to': ["BobBoberson is not allowing invitations to private threads."],
+            }
+        )
 
 
         # allow us to bypass user preference check
         # allow us to bypass user preference check
         override_acl(self.user, {'can_add_everyone_to_private_threads': 1})
         override_acl(self.user, {'can_add_everyone_to_private_threads': 1})
 
 
-        response = self.client.post(self.api_link, data={
-            'to': [self.other_user.username],
-            'title': "-----",
-            'post': "Lorem ipsum dolor.",
-        })
+        response = self.client.post(
+            self.api_link,
+            data={
+                'to': [self.other_user.username],
+                'title': "-----",
+                'post': "Lorem ipsum dolor.",
+            }
+        )
 
 
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'title': [
-                "Thread title should contain alpha-numeric characters."
-            ]
-        })
+        self.assertEqual(
+            response.json(), {
+                'title': ["Thread title should contain alpha-numeric characters."],
+            }
+        )
 
 
     def test_can_start_thread(self):
     def test_can_start_thread(self):
         """endpoint creates new thread"""
         """endpoint creates new thread"""
-        response = self.client.post(self.api_link, data={
-            'to': [self.other_user.username],
-            'title': "Hello, I am test thread!",
-            'post': "Lorem ipsum dolor met!"
-        })
+        response = self.client.post(
+            self.api_link,
+            data={
+                'to': [self.other_user.username],
+                'title': "Hello, I am test thread!",
+                'post': "Lorem ipsum dolor met!",
+            }
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         thread = self.user.thread_set.all()[:1][0]
         thread = self.user.thread_set.all()[:1][0]
@@ -322,18 +355,10 @@ class StartPrivateThreadTests(AuthenticatedUserTestCase):
         self.assertEqual(thread.participants.count(), 2)
         self.assertEqual(thread.participants.count(), 2)
 
 
         # we are thread owner
         # we are thread owner
-        ThreadParticipant.objects.get(
-            thread=thread,
-            user=self.user,
-            is_owner=True
-        )
+        ThreadParticipant.objects.get(thread=thread, user=self.user, is_owner=True)
 
 
         # other user was added to thread
         # other user was added to thread
-        ThreadParticipant.objects.get(
-            thread=thread,
-            user=self.other_user,
-            is_owner=False
-        )
+        ThreadParticipant.objects.get(thread=thread, user=self.other_user, is_owner=False)
 
 
         # other user has sync_unread_private_threads flag
         # other user has sync_unread_private_threads flag
         user_to_sync = UserModel.objects.get(sync_unread_private_threads=True)
         user_to_sync = UserModel.objects.get(sync_unread_private_threads=True)
@@ -354,9 +379,12 @@ class StartPrivateThreadTests(AuthenticatedUserTestCase):
 
 
     def test_post_unicode(self):
     def test_post_unicode(self):
         """unicode characters can be posted"""
         """unicode characters can be posted"""
-        response = self.client.post(self.api_link, data={
-            'to': [self.other_user.username],
-            'title': "Brzęczyżczykiewicz",
-            'post': "Chrzążczyżewoszyce, powiat Łękółody."
-        })
+        response = self.client.post(
+            self.api_link,
+            data={
+                'to': [self.other_user.username],
+                'title': "Brzęczyżczykiewicz",
+                'post': "Chrzążczyżewoszyce, powiat Łękółody.",
+            }
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)

+ 3 - 9
misago/threads/tests/test_privatethread_view.py

@@ -21,9 +21,7 @@ class PrivateThreadViewTests(PrivateThreadsTestCase):
 
 
     def test_no_permission(self):
     def test_no_permission(self):
         """user needs to have permission to see private thread"""
         """user needs to have permission to see private thread"""
-        override_acl(self.user, {
-            'can_use_private_threads': 0
-        })
+        override_acl(self.user, {'can_use_private_threads': 0})
 
 
         response = self.client.get(self.test_link)
         response = self.client.get(self.test_link)
         self.assertContains(response, "t use private threads", status_code=403)
         self.assertContains(response, "t use private threads", status_code=403)
@@ -35,9 +33,7 @@ class PrivateThreadViewTests(PrivateThreadsTestCase):
 
 
     def test_mod_not_reported(self):
     def test_mod_not_reported(self):
         """moderator can't see private thread that has no reports"""
         """moderator can't see private thread that has no reports"""
-        override_acl(self.user, {
-            'can_moderate_private_threads': 1
-        })
+        override_acl(self.user, {'can_moderate_private_threads': 1})
 
 
         response = self.client.get(self.test_link)
         response = self.client.get(self.test_link)
         self.assertEqual(response.status_code, 404)
         self.assertEqual(response.status_code, 404)
@@ -66,9 +62,7 @@ class PrivateThreadViewTests(PrivateThreadsTestCase):
 
 
     def test_mod_can_see_reported(self):
     def test_mod_can_see_reported(self):
         """moderator can see private thread that has reports"""
         """moderator can see private thread that has reports"""
-        override_acl(self.user, {
-            'can_moderate_private_threads': 1
-        })
+        override_acl(self.user, {'can_moderate_private_threads': 1})
 
 
         self.thread.has_reported_posts = True
         self.thread.has_reported_posts = True
         self.thread.save()
         self.thread.save()

+ 5 - 4
misago/threads/tests/test_privatethreads.py

@@ -10,8 +10,9 @@ class PrivateThreadsTestCase(AuthenticatedUserTestCase):
 
 
         override_acl(self.user, {
         override_acl(self.user, {
             'can_use_private_threads': 1,
             'can_use_private_threads': 1,
-            'can_start_private_threads': 1
+            'can_start_private_threads': 1,
         })
         })
+
         self.override_acl()
         self.override_acl()
 
 
     def override_acl(self, acl=None):
     def override_acl(self, acl=None):
@@ -26,7 +27,7 @@ class PrivateThreadsTestCase(AuthenticatedUserTestCase):
             'can_edit_posts': 0,
             'can_edit_posts': 0,
             'can_hide_posts': 0,
             'can_hide_posts': 0,
             'can_hide_own_posts': 0,
             'can_hide_own_posts': 0,
-            'can_merge_threads': 0
+            'can_merge_threads': 0,
         })
         })
 
 
         if acl:
         if acl:
@@ -34,6 +35,6 @@ class PrivateThreadsTestCase(AuthenticatedUserTestCase):
 
 
         override_acl(self.user, {
         override_acl(self.user, {
             'categories': {
             'categories': {
-                self.category.pk: final_acl
-            }
+                self.category.pk: final_acl,
+            },
         })
         })

+ 37 - 48
misago/threads/tests/test_privatethreads_api.py

@@ -22,9 +22,7 @@ class PrivateThreadsListApiTests(PrivateThreadsTestCase):
 
 
     def test_no_permission(self):
     def test_no_permission(self):
         """api requires user to have permission to be able to access it"""
         """api requires user to have permission to be able to access it"""
-        override_acl(self.user, {
-            'can_use_private_threads': 0
-        })
+        override_acl(self.user, {'can_use_private_threads': 0})
 
 
         response = self.client.get(self.api_link)
         response = self.client.get(self.api_link)
         self.assertContains(response, "can't use private threads", status_code=403)
         self.assertContains(response, "can't use private threads", status_code=403)
@@ -40,9 +38,11 @@ class PrivateThreadsListApiTests(PrivateThreadsTestCase):
     def test_thread_visibility(self):
     def test_thread_visibility(self):
         """only participated threads are returned by private threads api"""
         """only participated threads are returned by private threads api"""
         visible = testutils.post_thread(category=self.category, poster=self.user)
         visible = testutils.post_thread(category=self.category, poster=self.user)
-        hidden = testutils.post_thread(category=self.category, poster=self.user)
         reported = testutils.post_thread(category=self.category, poster=self.user)
         reported = testutils.post_thread(category=self.category, poster=self.user)
 
 
+        # hidden thread
+        testutils.post_thread(category=self.category, poster=self.user)
+
         ThreadParticipant.objects.add_participants(visible, [self.user])
         ThreadParticipant.objects.add_participants(visible, [self.user])
 
 
         reported.has_reported_posts = True
         reported.has_reported_posts = True
@@ -56,9 +56,7 @@ class PrivateThreadsListApiTests(PrivateThreadsTestCase):
         self.assertEqual(response_json['results'][0]['id'], visible.id)
         self.assertEqual(response_json['results'][0]['id'], visible.id)
 
 
         # threads with reported posts will also show to moderators
         # threads with reported posts will also show to moderators
-        override_acl(self.user, {
-            'can_moderate_private_threads': 1
-        })
+        override_acl(self.user, {'can_moderate_private_threads': 1})
 
 
         response = self.client.get(self.api_link)
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
@@ -85,9 +83,7 @@ class PrivateThreadRetrieveApiTests(PrivateThreadsTestCase):
 
 
     def test_no_permission(self):
     def test_no_permission(self):
         """user needs to have permission to see private thread"""
         """user needs to have permission to see private thread"""
-        override_acl(self.user, {
-            'can_use_private_threads': 0
-        })
+        override_acl(self.user, {'can_use_private_threads': 0})
 
 
         response = self.client.get(self.api_link)
         response = self.client.get(self.api_link)
         self.assertContains(response, "t use private threads", status_code=403)
         self.assertContains(response, "t use private threads", status_code=403)
@@ -99,9 +95,7 @@ class PrivateThreadRetrieveApiTests(PrivateThreadsTestCase):
 
 
     def test_mod_not_reported(self):
     def test_mod_not_reported(self):
         """moderator can't see private thread that has no reports"""
         """moderator can't see private thread that has no reports"""
-        override_acl(self.user, {
-            'can_moderate_private_threads': 1
-        })
+        override_acl(self.user, {'can_moderate_private_threads': 1})
 
 
         response = self.client.get(self.api_link)
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 404)
         self.assertEqual(response.status_code, 404)
@@ -123,15 +117,17 @@ class PrivateThreadRetrieveApiTests(PrivateThreadsTestCase):
 
 
         response_json = response.json()
         response_json = response.json()
         self.assertEqual(response_json['title'], self.thread.title)
         self.assertEqual(response_json['title'], self.thread.title)
-        self.assertEqual(response_json['participants'], [
-            {
-                'id': self.user.id,
-                'username': self.user.username,
-                'avatars': self.user.avatars,
-                'url': self.user.get_absolute_url(),
-                'is_owner': True
-            }
-        ])
+        self.assertEqual(
+            response_json['participants'], [
+                {
+                    'id': self.user.id,
+                    'username': self.user.username,
+                    'avatars': self.user.avatars,
+                    'url': self.user.get_absolute_url(),
+                    'is_owner': True,
+                },
+            ]
+        )
 
 
     def test_can_see_participant(self):
     def test_can_see_participant(self):
         """user can see thread he is participant of"""
         """user can see thread he is participant of"""
@@ -142,21 +138,21 @@ class PrivateThreadRetrieveApiTests(PrivateThreadsTestCase):
 
 
         response_json = response.json()
         response_json = response.json()
         self.assertEqual(response_json['title'], self.thread.title)
         self.assertEqual(response_json['title'], self.thread.title)
-        self.assertEqual(response_json['participants'], [
-            {
-                'id': self.user.id,
-                'username': self.user.username,
-                'avatars': self.user.avatars,
-                'url': self.user.get_absolute_url(),
-                'is_owner': False
-            }
-        ])
+        self.assertEqual(
+            response_json['participants'], [
+                {
+                    'id': self.user.id,
+                    'username': self.user.username,
+                    'avatars': self.user.avatars,
+                    'url': self.user.get_absolute_url(),
+                    'is_owner': False,
+                },
+            ]
+        )
 
 
     def test_mod_can_see_reported(self):
     def test_mod_can_see_reported(self):
         """moderator can see private thread that has reports"""
         """moderator can see private thread that has reports"""
-        override_acl(self.user, {
-            'can_moderate_private_threads': 1
-        })
+        override_acl(self.user, {'can_moderate_private_threads': 1})
 
 
         self.thread.has_reported_posts = True
         self.thread.has_reported_posts = True
         self.thread.save()
         self.thread.save()
@@ -176,9 +172,7 @@ class PrivateThreadsReadApiTests(PrivateThreadsTestCase):
 
 
     def test_read_threads_no_permission(self):
     def test_read_threads_no_permission(self):
         """api validates permission to use private threads"""
         """api validates permission to use private threads"""
-        override_acl(self.user, {
-            'can_use_private_threads': 0
-        })
+        override_acl(self.user, {'can_use_private_threads': 0})
 
 
         response = self.client.post(self.api_link)
         response = self.client.post(self.api_link)
         self.assertEqual(response.status_code, 403)
         self.assertEqual(response.status_code, 403)
@@ -210,29 +204,24 @@ class PrivateThreadDeleteApiTests(PrivateThreadsTestCase):
 
 
     def test_delete_thread_no_permission(self):
     def test_delete_thread_no_permission(self):
         """DELETE to API link with no permission to delete fails"""
         """DELETE to API link with no permission to delete fails"""
-        self.override_acl({
-            'can_hide_threads': 1
-        })
+        self.override_acl({'can_hide_threads': 1})
 
 
         response = self.client.delete(self.api_link)
         response = self.client.delete(self.api_link)
         self.assertEqual(response.status_code, 403)
         self.assertEqual(response.status_code, 403)
 
 
-        self.override_acl({
-            'can_hide_threads': 0
-        })
+        self.override_acl({'can_hide_threads': 0})
 
 
         response_json = response.json()
         response_json = response.json()
-        self.assertEqual(response_json['detail'],
-            "You don't have permission to delete this thread.")
+        self.assertEqual(
+            response_json['detail'], "You don't have permission to delete this thread."
+        )
 
 
         response = self.client.delete(self.api_link)
         response = self.client.delete(self.api_link)
         self.assertEqual(response.status_code, 403)
         self.assertEqual(response.status_code, 403)
 
 
     def test_delete_thread(self):
     def test_delete_thread(self):
         """DELETE to API link with permission deletes thread"""
         """DELETE to API link with permission deletes thread"""
-        self.override_acl({
-            'can_hide_threads': 2
-        })
+        self.override_acl({'can_hide_threads': 2})
 
 
         response = self.client.delete(self.api_link)
         response = self.client.delete(self.api_link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)

+ 5 - 7
misago/threads/tests/test_privatethreads_lists.py

@@ -22,9 +22,7 @@ class PrivateThreadsListTests(PrivateThreadsTestCase):
 
 
     def test_no_permission(self):
     def test_no_permission(self):
         """view requires user to have permission to be able to access it"""
         """view requires user to have permission to be able to access it"""
-        override_acl(self.user, {
-            'can_use_private_threads': 0
-        })
+        override_acl(self.user, {'can_use_private_threads': 0})
 
 
         response = self.client.get(self.test_link)
         response = self.client.get(self.test_link)
         self.assertContains(response, "use private threads", status_code=403)
         self.assertContains(response, "use private threads", status_code=403)
@@ -38,9 +36,11 @@ class PrivateThreadsListTests(PrivateThreadsTestCase):
     def test_thread_visibility(self):
     def test_thread_visibility(self):
         """only participated threads are returned by private threads view"""
         """only participated threads are returned by private threads view"""
         visible = testutils.post_thread(category=self.category, poster=self.user)
         visible = testutils.post_thread(category=self.category, poster=self.user)
-        hidden = testutils.post_thread(category=self.category, poster=self.user)
         reported = testutils.post_thread(category=self.category, poster=self.user)
         reported = testutils.post_thread(category=self.category, poster=self.user)
 
 
+        # post hidden thread
+        testutils.post_thread(category=self.category, poster=self.user)
+
         ThreadParticipant.objects.add_participants(visible, [self.user])
         ThreadParticipant.objects.add_participants(visible, [self.user])
 
 
         reported.has_reported_posts = True
         reported.has_reported_posts = True
@@ -51,9 +51,7 @@ class PrivateThreadsListTests(PrivateThreadsTestCase):
         self.assertContains(response, visible.get_absolute_url())
         self.assertContains(response, visible.get_absolute_url())
 
 
         # threads with reported posts will also show to moderators
         # threads with reported posts will also show to moderators
-        override_acl(self.user, {
-            'can_moderate_private_threads': 1
-        })
+        override_acl(self.user, {'can_moderate_private_threads': 1})
 
 
         response = self.client.get(self.test_link)
         response = self.client.get(self.test_link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)

+ 13 - 5
misago/threads/tests/test_search.py

@@ -83,7 +83,10 @@ class SearchApiTests(AuthenticatedUserTestCase):
         """hidden posts are extempt from search"""
         """hidden posts are extempt from search"""
         thread = testutils.post_thread(self.category)
         thread = testutils.post_thread(self.category)
         post = testutils.reply_thread(
         post = testutils.reply_thread(
-            thread, message="Lorem ipsum dolor.", is_hidden=True)
+            thread,
+            message="Lorem ipsum dolor.",
+            is_hidden=True,
+        )
         self.index_post(post)
         self.index_post(post)
 
 
         response = self.client.get('%s?q=ipsum' % self.api_link)
         response = self.client.get('%s?q=ipsum' % self.api_link)
@@ -100,7 +103,10 @@ class SearchApiTests(AuthenticatedUserTestCase):
         """unapproves posts are extempt from search"""
         """unapproves posts are extempt from search"""
         thread = testutils.post_thread(self.category)
         thread = testutils.post_thread(self.category)
         post = testutils.reply_thread(
         post = testutils.reply_thread(
-            thread, message="Lorem ipsum dolor.", is_unapproved=True)
+            thread,
+            message="Lorem ipsum dolor.",
+            is_unapproved=True,
+        )
         self.index_post(post)
         self.index_post(post)
 
 
         response = self.client.get('%s?q=ipsum' % self.api_link)
         response = self.client.get('%s?q=ipsum' % self.api_link)
@@ -174,6 +180,8 @@ class SearchProviderApiTests(SearchApiTests):
     def setUp(self):
     def setUp(self):
         super(SearchProviderApiTests, self).setUp()
         super(SearchProviderApiTests, self).setUp()
 
 
-        self.api_link = reverse('misago:api:search', kwargs={
-            'search_provider': 'threads'
-        })
+        self.api_link = reverse(
+            'misago:api:search', kwargs={
+                'search_provider': 'threads',
+            }
+        )

+ 55 - 34
misago/threads/tests/test_subscription_middleware.py

@@ -26,7 +26,7 @@ class SubscriptionMiddlewareTestCase(AuthenticatedUserTestCase):
             'can_see': 1,
             'can_see': 1,
             'can_browse': 1,
             'can_browse': 1,
             'can_start_threads': 1,
             'can_start_threads': 1,
-            'can_reply_threads': 1
+            'can_reply_threads': 1,
         })
         })
 
 
         override_acl(self.user, new_acl)
         override_acl(self.user, new_acl)
@@ -43,11 +43,14 @@ class SubscribeStartedThreadTests(SubscriptionMiddlewareTestCase):
         self.user.subscribe_to_replied_threads = UserModel.SUBSCRIBE_NOTIFY
         self.user.subscribe_to_replied_threads = UserModel.SUBSCRIBE_NOTIFY
         self.user.save()
         self.user.save()
 
 
-        response = self.client.post(self.api_link, data={
-            'category': self.category.id,
-            'title': "This is an test thread!",
-            'post': "This is test response!"
-        })
+        response = self.client.post(
+            self.api_link,
+            data={
+                'category': self.category.id,
+                'title': "This is an test thread!",
+                'post': "This is test response!",
+            }
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         # user has no subscriptions
         # user has no subscriptions
@@ -58,11 +61,14 @@ class SubscribeStartedThreadTests(SubscriptionMiddlewareTestCase):
         self.user.subscribe_to_started_threads = UserModel.SUBSCRIBE_NOTIFY
         self.user.subscribe_to_started_threads = UserModel.SUBSCRIBE_NOTIFY
         self.user.save()
         self.user.save()
 
 
-        response = self.client.post(self.api_link, data={
-            'category': self.category.id,
-            'title': "This is an test thread!",
-            'post': "This is test response!"
-        })
+        response = self.client.post(
+            self.api_link,
+            data={
+                'category': self.category.id,
+                'title': "This is an test thread!",
+                'post': "This is test response!",
+            }
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         # user has subscribed to thread
         # user has subscribed to thread
@@ -77,11 +83,14 @@ class SubscribeStartedThreadTests(SubscriptionMiddlewareTestCase):
         self.user.subscribe_to_started_threads = UserModel.SUBSCRIBE_ALL
         self.user.subscribe_to_started_threads = UserModel.SUBSCRIBE_ALL
         self.user.save()
         self.user.save()
 
 
-        response = self.client.post(self.api_link, data={
-            'category': self.category.id,
-            'title': "This is an test thread!",
-            'post': "This is test response!"
-        })
+        response = self.client.post(
+            self.api_link,
+            data={
+                'category': self.category.id,
+                'title': "This is an test thread!",
+                'post': "This is test response!",
+            }
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         # user has subscribed to thread
         # user has subscribed to thread
@@ -96,9 +105,11 @@ class SubscribeRepliedThreadTests(SubscriptionMiddlewareTestCase):
     def setUp(self):
     def setUp(self):
         super(SubscribeRepliedThreadTests, self).setUp()
         super(SubscribeRepliedThreadTests, self).setUp()
         self.thread = testutils.post_thread(self.category)
         self.thread = testutils.post_thread(self.category)
-        self.api_link = reverse('misago:api:thread-post-list', kwargs={
-            'thread_pk': self.thread.pk
-        })
+        self.api_link = reverse(
+            'misago:api:thread-post-list', kwargs={
+                'thread_pk': self.thread.pk,
+            }
+        )
 
 
     def test_dont_subscribe(self):
     def test_dont_subscribe(self):
         """middleware makes no subscription to thread"""
         """middleware makes no subscription to thread"""
@@ -106,9 +117,11 @@ class SubscribeRepliedThreadTests(SubscriptionMiddlewareTestCase):
         self.user.subscribe_to_replied_threads = UserModel.SUBSCRIBE_NONE
         self.user.subscribe_to_replied_threads = UserModel.SUBSCRIBE_NONE
         self.user.save()
         self.user.save()
 
 
-        response = self.client.post(self.api_link, data={
-            'post': "This is test response!"
-        })
+        response = self.client.post(
+            self.api_link, data={
+                'post': "This is test response!",
+            }
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         # user has no subscriptions
         # user has no subscriptions
@@ -119,9 +132,11 @@ class SubscribeRepliedThreadTests(SubscriptionMiddlewareTestCase):
         self.user.subscribe_to_replied_threads = UserModel.SUBSCRIBE_NOTIFY
         self.user.subscribe_to_replied_threads = UserModel.SUBSCRIBE_NOTIFY
         self.user.save()
         self.user.save()
 
 
-        response = self.client.post(self.api_link, data={
-            'post': "This is test response!"
-        })
+        response = self.client.post(
+            self.api_link, data={
+                'post': "This is test response!",
+            }
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         # user has subscribed to thread
         # user has subscribed to thread
@@ -135,9 +150,11 @@ class SubscribeRepliedThreadTests(SubscriptionMiddlewareTestCase):
         self.user.subscribe_to_replied_threads = UserModel.SUBSCRIBE_ALL
         self.user.subscribe_to_replied_threads = UserModel.SUBSCRIBE_ALL
         self.user.save()
         self.user.save()
 
 
-        response = self.client.post(self.api_link, data={
-            'post': "This is test response!"
-        })
+        response = self.client.post(
+            self.api_link, data={
+                'post': "This is test response!",
+            }
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         # user has subscribed to thread
         # user has subscribed to thread
@@ -151,17 +168,21 @@ class SubscribeRepliedThreadTests(SubscriptionMiddlewareTestCase):
         self.user.subscribe_to_replied_threads = UserModel.SUBSCRIBE_ALL
         self.user.subscribe_to_replied_threads = UserModel.SUBSCRIBE_ALL
         self.user.save()
         self.user.save()
 
 
-        response = self.client.post(self.api_link, data={
-            'post': "This is test response!"
-        })
+        response = self.client.post(
+            self.api_link, data={
+                'post': "This is test response!",
+            }
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         # clear subscription
         # clear subscription
         self.user.subscription_set.all().delete()
         self.user.subscription_set.all().delete()
         # reply again
         # reply again
-        response = self.client.post(self.api_link, data={
-            'post': "This is test response!"
-        })
+        response = self.client.post(
+            self.api_link, data={
+                'post': "This is test response!",
+            }
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         # user has no subscriptions
         # user has no subscriptions

+ 5 - 24
misago/threads/tests/test_subscriptions.py

@@ -3,7 +3,6 @@ from datetime import timedelta
 from django.contrib.auth import get_user_model
 from django.contrib.auth import get_user_model
 from django.test import TestCase
 from django.test import TestCase
 from django.utils import timezone
 from django.utils import timezone
-from django.utils.six.moves import range
 
 
 from misago.categories.models import Category
 from misago.categories.models import Category
 from misago.threads import testutils
 from misago.threads import testutils
@@ -19,14 +18,13 @@ class SubscriptionsTests(TestCase):
         self.category = list(Category.objects.all_categories()[:1])[0]
         self.category = list(Category.objects.all_categories()[:1])[0]
         self.thread = self.post_thread(timezone.now() - timedelta(days=10))
         self.thread = self.post_thread(timezone.now() - timedelta(days=10))
 
 
-        self.user = UserModel.objects.create_user(
-            "Bob", "bob@test.com", "Pass.123")
+        self.user = UserModel.objects.create_user("Bob", "bob@test.com", "Pass.123")
         self.anon = AnonymousUser()
         self.anon = AnonymousUser()
 
 
     def post_thread(self, datetime):
     def post_thread(self, datetime):
         return testutils.post_thread(
         return testutils.post_thread(
             category=self.category,
             category=self.category,
-            started_on=datetime
+            started_on=datetime,
         )
         )
 
 
     def test_anon_subscription(self):
     def test_anon_subscription(self):
@@ -37,9 +35,8 @@ class SubscriptionsTests(TestCase):
     def test_anon_threads_subscription(self):
     def test_anon_threads_subscription(self):
         """make multiple threads list sub aware for anon"""
         """make multiple threads list sub aware for anon"""
         threads = []
         threads = []
-        for i in range(10):
-            threads.append(
-                self.post_thread(timezone.now() - timedelta(days=10)))
+        for _ in range(10):
+            threads.append(self.post_thread(timezone.now() - timedelta(days=10)))
 
 
         make_subscription_aware(self.anon, threads)
         make_subscription_aware(self.anon, threads)
 
 
@@ -51,24 +48,11 @@ class SubscriptionsTests(TestCase):
         make_subscription_aware(self.user, self.thread)
         make_subscription_aware(self.user, self.thread)
         self.assertIsNone(self.thread.subscription)
         self.assertIsNone(self.thread.subscription)
 
 
-    def test_threads_no_subscription(self):
-        """make mulitple threads sub aware for authenticated"""
-        threads = []
-        for i in range(10):
-            threads.append(
-                self.post_thread(timezone.now() - timedelta(days=10)))
-
-        make_subscription_aware(self.user, threads)
-
-        for thread in threads:
-            self.assertIsNone(thread.subscription)
-
     def test_subscribed_thread(self):
     def test_subscribed_thread(self):
         """make thread sub aware for authenticated"""
         """make thread sub aware for authenticated"""
         self.user.subscription_set.create(
         self.user.subscription_set.create(
             thread=self.thread,
             thread=self.thread,
             category=self.category,
             category=self.category,
-
             last_read_on=timezone.now(),
             last_read_on=timezone.now(),
             send_email=True,
             send_email=True,
         )
         )
@@ -80,14 +64,12 @@ class SubscriptionsTests(TestCase):
         """make mulitple threads sub aware for authenticated"""
         """make mulitple threads sub aware for authenticated"""
         threads = []
         threads = []
         for i in range(10):
         for i in range(10):
-            threads.append(
-                self.post_thread(timezone.now() - timedelta(days=10)))
+            threads.append(self.post_thread(timezone.now() - timedelta(days=10)))
 
 
             if i % 3 == 0:
             if i % 3 == 0:
                 self.user.subscription_set.create(
                 self.user.subscription_set.create(
                     thread=threads[-1],
                     thread=threads[-1],
                     category=self.category,
                     category=self.category,
-
                     last_read_on=timezone.now(),
                     last_read_on=timezone.now(),
                     send_email=False,
                     send_email=False,
                 )
                 )
@@ -95,7 +77,6 @@ class SubscriptionsTests(TestCase):
                 self.user.subscription_set.create(
                 self.user.subscription_set.create(
                     thread=threads[-1],
                     thread=threads[-1],
                     category=self.category,
                     category=self.category,
-
                     last_read_on=timezone.now(),
                     last_read_on=timezone.now(),
                     send_email=True,
                     send_email=True,
                 )
                 )

+ 2 - 1
misago/threads/tests/test_sync_unread_private_threads.py

@@ -14,7 +14,8 @@ class SyncUnreadPrivateThreadsTestCase(PrivateThreadsTestCase):
         super(SyncUnreadPrivateThreadsTestCase, self).setUp()
         super(SyncUnreadPrivateThreadsTestCase, self).setUp()
 
 
         self.other_user = UserModel.objects.create_user(
         self.other_user = UserModel.objects.create_user(
-            'BobBoberson', 'bob@boberson.com', 'pass123')
+            'BobBoberson', 'bob@boberson.com', 'pass123'
+        )
 
 
         self.thread = testutils.post_thread(self.category, poster=self.user)
         self.thread = testutils.post_thread(self.category, poster=self.user)
 
 

+ 2 - 3
misago/threads/tests/test_synchronizethreads.py

@@ -1,7 +1,6 @@
 from django.core.management import call_command
 from django.core.management import call_command
 from django.test import TestCase
 from django.test import TestCase
 from django.utils.six import StringIO
 from django.utils.six import StringIO
-from django.utils.six.moves import range
 
 
 from misago.categories.models import Category
 from misago.categories.models import Category
 from misago.threads import testutils
 from misago.threads import testutils
@@ -23,9 +22,9 @@ class SynchronizeThreadsTests(TestCase):
         """command synchronizes threads"""
         """command synchronizes threads"""
         category = Category.objects.all_categories()[:1][0]
         category = Category.objects.all_categories()[:1][0]
 
 
-        threads = [testutils.post_thread(category) for t in range(10)]
+        threads = [testutils.post_thread(category) for _ in range(10)]
         for i, thread in enumerate(threads):
         for i, thread in enumerate(threads):
-            [testutils.reply_thread(thread) for r in range(i)]
+            [testutils.reply_thread(thread) for _ in range(i)]
             thread.replies = 0
             thread.replies = 0
             thread.save()
             thread.save()
 
 

+ 72 - 89
misago/threads/tests/test_thread_editreply_api.py

@@ -1,16 +1,12 @@
 # -*- coding: utf-8 -*-
 # -*- coding: utf-8 -*-
 from __future__ import unicode_literals
 from __future__ import unicode_literals
 
 
-import json
-
 from django.test.client import BOUNDARY, MULTIPART_CONTENT, encode_multipart
 from django.test.client import BOUNDARY, MULTIPART_CONTENT, encode_multipart
 from django.urls import reverse
 from django.urls import reverse
-from django.utils.encoding import smart_str
 
 
 from misago.acl.testutils import override_acl
 from misago.acl.testutils import override_acl
 from misago.categories.models import Category
 from misago.categories.models import Category
 from misago.threads import testutils
 from misago.threads import testutils
-from misago.threads.models import Thread
 from misago.users.testutils import AuthenticatedUserTestCase
 from misago.users.testutils import AuthenticatedUserTestCase
 
 
 
 
@@ -22,10 +18,13 @@ class EditReplyTests(AuthenticatedUserTestCase):
         self.thread = testutils.post_thread(category=self.category)
         self.thread = testutils.post_thread(category=self.category)
         self.post = testutils.reply_thread(self.thread, poster=self.user)
         self.post = testutils.reply_thread(self.thread, poster=self.user)
 
 
-        self.api_link = reverse('misago:api:thread-post-detail', kwargs={
-            'thread_pk': self.thread.pk,
-            'pk': self.post.pk
-        })
+        self.api_link = reverse(
+            'misago:api:thread-post-detail',
+            kwargs={
+                'thread_pk': self.thread.pk,
+                'pk': self.post.pk,
+            }
+        )
 
 
     def override_acl(self, extra_acl=None):
     def override_acl(self, extra_acl=None):
         new_acl = self.user.acl_cache
         new_acl = self.user.acl_cache
@@ -34,7 +33,7 @@ class EditReplyTests(AuthenticatedUserTestCase):
             'can_browse': 1,
             'can_browse': 1,
             'can_start_threads': 0,
             'can_start_threads': 0,
             'can_reply_threads': 0,
             'can_reply_threads': 0,
-            'can_edit_posts': 1
+            'can_edit_posts': 1,
         })
         })
 
 
         if extra_acl:
         if extra_acl:
@@ -69,81 +68,73 @@ class EditReplyTests(AuthenticatedUserTestCase):
 
 
     def test_cant_edit_reply(self):
     def test_cant_edit_reply(self):
         """permission to edit reply is validated"""
         """permission to edit reply is validated"""
-        self.override_acl({
-            'can_edit_posts': 0
-        })
+        self.override_acl({'can_edit_posts': 0})
 
 
         response = self.put(self.api_link)
         response = self.put(self.api_link)
         self.assertContains(response, "You can't edit posts in this category.", status_code=403)
         self.assertContains(response, "You can't edit posts in this category.", status_code=403)
 
 
     def test_cant_edit_other_user_reply(self):
     def test_cant_edit_other_user_reply(self):
         """permission to edit reply by other users is validated"""
         """permission to edit reply by other users is validated"""
-        self.override_acl({
-            'can_edit_posts': 1
-        })
+        self.override_acl({'can_edit_posts': 1})
 
 
         self.post.poster = None
         self.post.poster = None
         self.post.save()
         self.post.save()
 
 
         response = self.put(self.api_link)
         response = self.put(self.api_link)
-        self.assertContains(response, "You can't edit other users posts in this category.", status_code=403)
+        self.assertContains(
+            response, "You can't edit other users posts in this category.", status_code=403
+        )
 
 
     def test_closed_category(self):
     def test_closed_category(self):
         """permssion to edit reply in closed category is validated"""
         """permssion to edit reply in closed category is validated"""
-        self.override_acl({
-            'can_close_threads': 0
-        })
+        self.override_acl({'can_close_threads': 0})
 
 
         self.category.is_closed = True
         self.category.is_closed = True
         self.category.save()
         self.category.save()
 
 
         response = self.put(self.api_link)
         response = self.put(self.api_link)
-        self.assertContains(response, "This category is closed. You can't edit posts in it.", status_code=403)
+        self.assertContains(
+            response, "This category is closed. You can't edit posts in it.", status_code=403
+        )
 
 
         # allow to post in closed category
         # allow to post in closed category
-        self.override_acl({
-            'can_close_threads': 1
-        })
+        self.override_acl({'can_close_threads': 1})
 
 
         response = self.put(self.api_link)
         response = self.put(self.api_link)
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
     def test_closed_thread(self):
     def test_closed_thread(self):
         """permssion to edit reply in closed thread is validated"""
         """permssion to edit reply in closed thread is validated"""
-        self.override_acl({
-            'can_close_threads': 0
-        })
+        self.override_acl({'can_close_threads': 0})
 
 
         self.thread.is_closed = True
         self.thread.is_closed = True
         self.thread.save()
         self.thread.save()
 
 
         response = self.put(self.api_link)
         response = self.put(self.api_link)
-        self.assertContains(response, "This thread is closed. You can't edit posts in it.", status_code=403)
+        self.assertContains(
+            response, "This thread is closed. You can't edit posts in it.", status_code=403
+        )
 
 
         # allow to post in closed thread
         # allow to post in closed thread
-        self.override_acl({
-            'can_close_threads': 1
-        })
+        self.override_acl({'can_close_threads': 1})
 
 
         response = self.put(self.api_link)
         response = self.put(self.api_link)
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
     def test_protected_post(self):
     def test_protected_post(self):
         """permssion to edit protected post is validated"""
         """permssion to edit protected post is validated"""
-        self.override_acl({
-            'can_protect_posts': 0
-        })
+        self.override_acl({'can_protect_posts': 0})
 
 
         self.post.is_protected = True
         self.post.is_protected = True
         self.post.save()
         self.post.save()
 
 
         response = self.put(self.api_link)
         response = self.put(self.api_link)
-        self.assertContains(response, "This post is protected. You can't edit it.", status_code=403)
+        self.assertContains(
+            response, "This post is protected. You can't edit it.", status_code=403
+        )
 
 
         # allow to post in closed thread
         # allow to post in closed thread
-        self.override_acl({
-            'can_protect_posts': 1
-        })
+        self.override_acl({'can_protect_posts': 1})
 
 
         response = self.put(self.api_link)
         response = self.put(self.api_link)
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
@@ -155,10 +146,8 @@ class EditReplyTests(AuthenticatedUserTestCase):
         response = self.put(self.api_link, data={})
         response = self.put(self.api_link, data={})
 
 
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
-        self.assertEqual(json.loads(smart_str(response.content)), {
-            'post': [
-                "You have to enter a message."
-            ]
+        self.assertEqual(response.json(), {
+            'post': ["You have to enter a message."],
         })
         })
 
 
     def test_edit_event(self):
     def test_edit_event(self):
@@ -176,29 +165,27 @@ class EditReplyTests(AuthenticatedUserTestCase):
         """post is validated"""
         """post is validated"""
         self.override_acl()
         self.override_acl()
 
 
-        response = self.put(self.api_link, data={
-            'post': "a",
-        })
+        response = self.put(
+            self.api_link, data={
+                'post': "a",
+            }
+        )
 
 
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
-        self.assertEqual(json.loads(smart_str(response.content)), {
-            'post': [
-                "Posted message should be at least 5 characters long (it has 1)."
-            ]
-        })
+        self.assertEqual(
+            response.json(), {
+                'post': ["Posted message should be at least 5 characters long (it has 1)."],
+            }
+        )
 
 
     def test_edit_reply_no_change(self):
     def test_edit_reply_no_change(self):
         """endpoint isn't bumping edits count if no change was made to post's body"""
         """endpoint isn't bumping edits count if no change was made to post's body"""
         self.override_acl()
         self.override_acl()
         self.assertEqual(self.post.edits_record.count(), 0)
         self.assertEqual(self.post.edits_record.count(), 0)
 
 
-        response = self.put(self.api_link, data={
-            'post': self.post.original
-        })
+        response = self.put(self.api_link, data={'post': self.post.original})
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        thread = Thread.objects.get(pk=self.thread.pk)
-
         self.override_acl()
         self.override_acl()
         response = self.client.get(self.thread.get_absolute_url())
         response = self.client.get(self.thread.get_absolute_url())
         self.assertContains(response, self.post.parsed)
         self.assertContains(response, self.post.parsed)
@@ -217,13 +204,9 @@ class EditReplyTests(AuthenticatedUserTestCase):
         self.override_acl()
         self.override_acl()
         self.assertEqual(self.post.edits_record.count(), 0)
         self.assertEqual(self.post.edits_record.count(), 0)
 
 
-        response = self.put(self.api_link, data={
-            'post': "This is test edit!"
-        })
+        response = self.put(self.api_link, data={'post': "This is test edit!"})
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        thread = Thread.objects.get(pk=self.thread.pk)
-
         self.override_acl()
         self.override_acl()
         response = self.client.get(self.thread.get_absolute_url())
         response = self.client.get(self.thread.get_absolute_url())
         self.assertContains(response, "<p>This is test edit!</p>")
         self.assertContains(response, "<p>This is test edit!</p>")
@@ -247,36 +230,34 @@ class EditReplyTests(AuthenticatedUserTestCase):
 
 
     def test_edit_first_post_hidden(self):
     def test_edit_first_post_hidden(self):
         """endpoint updates hidden thread's first post"""
         """endpoint updates hidden thread's first post"""
-        self.override_acl({
-            'can_hide_threads': 1,
-            'can_edit_posts': 2
-        })
+        self.override_acl({'can_hide_threads': 1, 'can_edit_posts': 2})
 
 
         self.thread.is_hidden = True
         self.thread.is_hidden = True
         self.thread.save()
         self.thread.save()
         self.thread.first_post.is_hidden = True
         self.thread.first_post.is_hidden = True
         self.thread.first_post.save()
         self.thread.first_post.save()
 
 
-        api_link = reverse('misago:api:thread-post-detail', kwargs={
-            'thread_pk': self.thread.pk,
-            'pk': self.thread.first_post.pk
-        })
+        api_link = reverse(
+            'misago:api:thread-post-detail',
+            kwargs={
+                'thread_pk': self.thread.pk,
+                'pk': self.thread.first_post.pk,
+            }
+        )
 
 
-        response = self.put(self.api_link, data={
-            'post': "This is test edit!"
-        })
+        response = self.put(api_link, data={'post': "This is test edit!"})
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
     def test_protect_post(self):
     def test_protect_post(self):
         """can protect post"""
         """can protect post"""
-        self.override_acl({
-            'can_protect_posts': 1
-        })
-
-        response = self.put(self.api_link, data={
-            'post': "Lorem ipsum dolor met!",
-            'protect': 1
-        })
+        self.override_acl({'can_protect_posts': 1})
+
+        response = self.put(
+            self.api_link, data={
+                'post': "Lorem ipsum dolor met!",
+                'protect': 1,
+            }
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         post = self.user.post_set.order_by('id').last()
         post = self.user.post_set.order_by('id').last()
@@ -284,14 +265,14 @@ class EditReplyTests(AuthenticatedUserTestCase):
 
 
     def test_protect_post_no_permission(self):
     def test_protect_post_no_permission(self):
         """cant protect post without permission"""
         """cant protect post without permission"""
-        self.override_acl({
-            'can_protect_posts': 0
-        })
-
-        response = self.put(self.api_link, data={
-            'post': "Lorem ipsum dolor met!",
-            'protect': 1
-        })
+        self.override_acl({'can_protect_posts': 0})
+
+        response = self.put(
+            self.api_link, data={
+                'post': "Lorem ipsum dolor met!",
+                'protect': 1,
+            }
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         post = self.user.post_set.order_by('id').last()
         post = self.user.post_set.order_by('id').last()
@@ -301,7 +282,9 @@ class EditReplyTests(AuthenticatedUserTestCase):
         """unicode characters can be posted"""
         """unicode characters can be posted"""
         self.override_acl()
         self.override_acl()
 
 
-        response = self.put(self.api_link, data={
-            'post': "Chrzążczyżewoszyce, powiat Łękółody."
-        })
+        response = self.put(
+            self.api_link, data={
+                'post': "Chrzążczyżewoszyce, powiat Łękółody.",
+            }
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)

+ 143 - 153
misago/threads/tests/test_thread_merge_api.py

@@ -1,7 +1,4 @@
-import json
-
 from django.urls import reverse
 from django.urls import reverse
-from django.utils.encoding import smart_str
 
 
 from misago.acl.testutils import override_acl
 from misago.acl.testutils import override_acl
 from misago.categories.models import Category
 from misago.categories.models import Category
@@ -18,10 +15,18 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
         Category(
         Category(
             name='Category B',
             name='Category B',
             slug='category-b',
             slug='category-b',
-        ).insert_at(self.category, position='last-child', save=True)
+        ).insert_at(
+            self.category,
+            position='last-child',
+            save=True,
+        )
         self.category_b = Category.objects.get(slug='category-b')
         self.category_b = Category.objects.get(slug='category-b')
 
 
-        self.api_link = reverse('misago:api:thread-merge', kwargs={'pk': self.thread.pk})
+        self.api_link = reverse(
+            'misago:api:thread-merge', kwargs={
+                'pk': self.thread.pk,
+            }
+        )
 
 
     def override_other_acl(self, acl=None):
     def override_other_acl(self, acl=None):
         other_category_acl = self.user.acl_cache['categories'][self.category.pk].copy()
         other_category_acl = self.user.acl_cache['categories'][self.category.pk].copy()
@@ -35,7 +40,7 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
             'can_edit_posts': 0,
             'can_edit_posts': 0,
             'can_hide_posts': 0,
             'can_hide_posts': 0,
             'can_hide_own_posts': 0,
             'can_hide_own_posts': 0,
-            'can_merge_threads': 0
+            'can_merge_threads': 0,
         })
         })
 
 
         if acl:
         if acl:
@@ -48,56 +53,54 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
         if other_category_acl['can_see']:
         if other_category_acl['can_see']:
             visible_categories.append(self.category_b.pk)
             visible_categories.append(self.category_b.pk)
 
 
-        override_acl(self.user, {
-            'visible_categories': visible_categories,
-            'categories': categories_acl,
-        })
+        override_acl(
+            self.user, {
+                'visible_categories': visible_categories,
+                'categories': categories_acl,
+            }
+        )
 
 
     def test_merge_no_permission(self):
     def test_merge_no_permission(self):
         """api validates if thread can be merged with other one"""
         """api validates if thread can be merged with other one"""
-        self.override_acl({
-            'can_merge_threads': 0
-        })
+        self.override_acl({'can_merge_threads': 0})
 
 
         response = self.client.post(self.api_link)
         response = self.client.post(self.api_link)
-        self.assertContains(response, "You don't have permission to merge this thread with others.", status_code=403)
+        self.assertContains(
+            response,
+            "You don't have permission to merge this thread with others.",
+            status_code=403
+        )
 
 
     def test_merge_no_url(self):
     def test_merge_no_url(self):
         """api validates if thread url was given"""
         """api validates if thread url was given"""
-        self.override_acl({
-            'can_merge_threads': 1
-        })
+        self.override_acl({'can_merge_threads': 1})
 
 
         response = self.client.post(self.api_link)
         response = self.client.post(self.api_link)
         self.assertContains(response, "This is not a valid thread link.", status_code=400)
         self.assertContains(response, "This is not a valid thread link.", status_code=400)
 
 
     def test_invalid_url(self):
     def test_invalid_url(self):
         """api validates thread url"""
         """api validates thread url"""
-        self.override_acl({
-            'can_merge_threads': 1
-        })
+        self.override_acl({'can_merge_threads': 1})
 
 
         response = self.client.post(self.api_link, {
         response = self.client.post(self.api_link, {
-            'thread_url': self.user.get_absolute_url()
+            'thread_url': self.user.get_absolute_url(),
         })
         })
         self.assertContains(response, "This is not a valid thread link.", status_code=400)
         self.assertContains(response, "This is not a valid thread link.", status_code=400)
 
 
     def test_current_thread_url(self):
     def test_current_thread_url(self):
         """api validates if thread url given is to current thread"""
         """api validates if thread url given is to current thread"""
-        self.override_acl({
-            'can_merge_threads': 1
-        })
+        self.override_acl({'can_merge_threads': 1})
 
 
-        response = self.client.post(self.api_link, {
-            'thread_url': self.thread.get_absolute_url()
-        })
+        response = self.client.post(
+            self.api_link, {
+                'thread_url': self.thread.get_absolute_url(),
+            }
+        )
         self.assertContains(response, "You can't merge thread with itself.", status_code=400)
         self.assertContains(response, "You can't merge thread with itself.", status_code=400)
 
 
     def test_other_thread_exists(self):
     def test_other_thread_exists(self):
         """api validates if other thread exists"""
         """api validates if other thread exists"""
-        self.override_acl({
-            'can_merge_threads': 1
-        })
+        self.override_acl({'can_merge_threads': 1})
 
 
         self.override_other_acl()
         self.override_other_acl()
 
 
@@ -106,76 +109,76 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
         other_thread.delete()
         other_thread.delete()
 
 
         response = self.client.post(self.api_link, {
         response = self.client.post(self.api_link, {
-            'thread_url': other_thread_url
+            'thread_url': other_thread_url,
         })
         })
-        self.assertContains(response, "The thread you have entered link to doesn't exist", status_code=400)
+        self.assertContains(
+            response, "The thread you have entered link to doesn't exist", status_code=400
+        )
 
 
     def test_other_thread_is_invisible(self):
     def test_other_thread_is_invisible(self):
         """api validates if other thread is visible"""
         """api validates if other thread is visible"""
-        self.override_acl({
-            'can_merge_threads': 1
-        })
+        self.override_acl({'can_merge_threads': 1})
 
 
-        self.override_other_acl({
-            'can_see': 0
-        })
+        self.override_other_acl({'can_see': 0})
 
 
         other_thread = testutils.post_thread(self.category_b)
         other_thread = testutils.post_thread(self.category_b)
 
 
-        response = self.client.post(self.api_link, {
-            'thread_url': other_thread.get_absolute_url()
-        })
-        self.assertContains(response, "The thread you have entered link to doesn't exist", status_code=400)
+        response = self.client.post(
+            self.api_link, {
+                'thread_url': other_thread.get_absolute_url(),
+            }
+        )
+        self.assertContains(
+            response, "The thread you have entered link to doesn't exist", status_code=400
+        )
 
 
     def test_other_thread_isnt_mergeable(self):
     def test_other_thread_isnt_mergeable(self):
         """api validates if other thread can be merged"""
         """api validates if other thread can be merged"""
-        self.override_acl({
-            'can_merge_threads': 1
-        })
+        self.override_acl({'can_merge_threads': 1})
 
 
-        self.override_other_acl({
-            'can_merge_threads': 0
-        })
+        self.override_other_acl({'can_merge_threads': 0})
 
 
         other_thread = testutils.post_thread(self.category_b)
         other_thread = testutils.post_thread(self.category_b)
 
 
-        response = self.client.post(self.api_link, {
-            'thread_url': other_thread.get_absolute_url()
-        })
-        self.assertContains(response, "You don't have permission to merge this thread", status_code=400)
+        response = self.client.post(
+            self.api_link, {
+                'thread_url': other_thread.get_absolute_url(),
+            }
+        )
+        self.assertContains(
+            response, "You don't have permission to merge this thread", status_code=400
+        )
 
 
     def test_other_thread_isnt_replyable(self):
     def test_other_thread_isnt_replyable(self):
         """api validates if other thread can be replied, which is condition for merg"""
         """api validates if other thread can be replied, which is condition for merg"""
-        self.override_acl({
-            'can_merge_threads': 1
-        })
+        self.override_acl({'can_merge_threads': 1})
 
 
-        self.override_other_acl({
-            'can_reply_threads': 0
-        })
+        self.override_other_acl({'can_reply_threads': 0})
 
 
         other_thread = testutils.post_thread(self.category_b)
         other_thread = testutils.post_thread(self.category_b)
 
 
-        response = self.client.post(self.api_link, {
-            'thread_url': other_thread.get_absolute_url()
-        })
-        self.assertContains(response, "You can't merge this thread into thread you can't reply.", status_code=400)
+        response = self.client.post(
+            self.api_link, {
+                'thread_url': other_thread.get_absolute_url(),
+            }
+        )
+        self.assertContains(
+            response, "You can't merge this thread into thread you can't reply.", status_code=400
+        )
 
 
     def test_merge_threads(self):
     def test_merge_threads(self):
         """api merges two threads successfully"""
         """api merges two threads successfully"""
-        self.override_acl({
-            'can_merge_threads': 1
-        })
+        self.override_acl({'can_merge_threads': 1})
 
 
-        self.override_other_acl({
-            'can_merge_threads': 1
-        })
+        self.override_other_acl({'can_merge_threads': 1})
 
 
         other_thread = testutils.post_thread(self.category_b)
         other_thread = testutils.post_thread(self.category_b)
 
 
-        response = self.client.post(self.api_link, {
-            'thread_url': other_thread.get_absolute_url()
-        })
+        response = self.client.post(
+            self.api_link, {
+                'thread_url': other_thread.get_absolute_url(),
+            }
+        )
         self.assertContains(response, other_thread.get_absolute_url(), status_code=200)
         self.assertContains(response, other_thread.get_absolute_url(), status_code=200)
 
 
         # other thread has two posts now
         # other thread has two posts now
@@ -187,20 +190,18 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
 
 
     def test_merge_threads_kept_poll(self):
     def test_merge_threads_kept_poll(self):
         """api merges two threads successfully, keeping poll from old thread"""
         """api merges two threads successfully, keeping poll from old thread"""
-        self.override_acl({
-            'can_merge_threads': 1
-        })
+        self.override_acl({'can_merge_threads': 1})
 
 
-        self.override_other_acl({
-            'can_merge_threads': 1
-        })
+        self.override_other_acl({'can_merge_threads': 1})
 
 
         other_thread = testutils.post_thread(self.category_b)
         other_thread = testutils.post_thread(self.category_b)
         poll = testutils.post_poll(other_thread, self.user)
         poll = testutils.post_poll(other_thread, self.user)
 
 
-        response = self.client.post(self.api_link, {
-            'thread_url': other_thread.get_absolute_url()
-        })
+        response = self.client.post(
+            self.api_link, {
+                'thread_url': other_thread.get_absolute_url(),
+            }
+        )
         self.assertContains(response, other_thread.get_absolute_url(), status_code=200)
         self.assertContains(response, other_thread.get_absolute_url(), status_code=200)
 
 
         # other thread has two posts now
         # other thread has two posts now
@@ -216,20 +217,18 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
 
 
     def test_merge_threads_moved_poll(self):
     def test_merge_threads_moved_poll(self):
         """api merges two threads successfully, moving poll from other thread"""
         """api merges two threads successfully, moving poll from other thread"""
-        self.override_acl({
-            'can_merge_threads': 1
-        })
+        self.override_acl({'can_merge_threads': 1})
 
 
-        self.override_other_acl({
-            'can_merge_threads': 1
-        })
+        self.override_other_acl({'can_merge_threads': 1})
 
 
         other_thread = testutils.post_thread(self.category_b)
         other_thread = testutils.post_thread(self.category_b)
         poll = testutils.post_poll(self.thread, self.user)
         poll = testutils.post_poll(self.thread, self.user)
 
 
-        response = self.client.post(self.api_link, {
-            'thread_url': other_thread.get_absolute_url()
-        })
+        response = self.client.post(
+            self.api_link, {
+                'thread_url': other_thread.get_absolute_url(),
+            }
+        )
         self.assertContains(response, other_thread.get_absolute_url(), status_code=200)
         self.assertContains(response, other_thread.get_absolute_url(), status_code=200)
 
 
         # other thread has two posts now
         # other thread has two posts now
@@ -245,29 +244,29 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
 
 
     def test_threads_merge_conflict(self):
     def test_threads_merge_conflict(self):
         """api errors on merge conflict, returning list of available polls"""
         """api errors on merge conflict, returning list of available polls"""
-        self.override_acl({
-            'can_merge_threads': 1
-        })
+        self.override_acl({'can_merge_threads': 1})
 
 
-        self.override_other_acl({
-            'can_merge_threads': 1
-        })
+        self.override_other_acl({'can_merge_threads': 1})
 
 
         other_thread = testutils.post_thread(self.category_b)
         other_thread = testutils.post_thread(self.category_b)
         poll = testutils.post_poll(self.thread, self.user)
         poll = testutils.post_poll(self.thread, self.user)
         other_poll = testutils.post_poll(other_thread, self.user)
         other_poll = testutils.post_poll(other_thread, self.user)
 
 
-        response = self.client.post(self.api_link, {
-            'thread_url': other_thread.get_absolute_url()
-        })
+        response = self.client.post(
+            self.api_link, {
+                'thread_url': other_thread.get_absolute_url(),
+            }
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'polls': [
-                [0, "Delete all polls"],
-                [poll.pk, poll.question],
-                [other_poll.pk, other_poll.question]
-            ]
-        })
+        self.assertEqual(
+            response.json(), {
+                'polls': [
+                    [0, "Delete all polls"],
+                    [poll.pk, poll.question],
+                    [other_poll.pk, other_poll.question],
+                ]
+            }
+        )
 
 
         # polls and votes were untouched
         # polls and votes were untouched
         self.assertEqual(Poll.objects.count(), 2)
         self.assertEqual(Poll.objects.count(), 2)
@@ -275,26 +274,23 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
 
 
     def test_threads_merge_conflict_invalid_resolution(self):
     def test_threads_merge_conflict_invalid_resolution(self):
         """api errors on invalid merge conflict resolution"""
         """api errors on invalid merge conflict resolution"""
-        self.override_acl({
-            'can_merge_threads': 1
-        })
+        self.override_acl({'can_merge_threads': 1})
 
 
-        self.override_other_acl({
-            'can_merge_threads': 1
-        })
+        self.override_other_acl({'can_merge_threads': 1})
 
 
         other_thread = testutils.post_thread(self.category_b)
         other_thread = testutils.post_thread(self.category_b)
-        poll = testutils.post_poll(self.thread, self.user)
-        other_poll = testutils.post_poll(other_thread, self.user)
 
 
-        response = self.client.post(self.api_link, {
-            'thread_url': other_thread.get_absolute_url(),
-            'poll': 'jhdkajshdsak'
-        })
+        testutils.post_poll(self.thread, self.user)
+        testutils.post_poll(other_thread, self.user)
+
+        response = self.client.post(
+            self.api_link, {
+                'thread_url': other_thread.get_absolute_url(),
+                'poll': 'jhdkajshdsak',
+            }
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'detail': "Invalid choice."
-        })
+        self.assertEqual(response.json(), {'detail': "Invalid choice."})
 
 
         # polls and votes were untouched
         # polls and votes were untouched
         self.assertEqual(Poll.objects.count(), 2)
         self.assertEqual(Poll.objects.count(), 2)
@@ -302,22 +298,20 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
 
 
     def test_threads_merge_conflict_delete_all(self):
     def test_threads_merge_conflict_delete_all(self):
         """api deletes all polls when delete all choice is selected"""
         """api deletes all polls when delete all choice is selected"""
-        self.override_acl({
-            'can_merge_threads': 1
-        })
+        self.override_acl({'can_merge_threads': 1})
 
 
-        self.override_other_acl({
-            'can_merge_threads': 1
-        })
+        self.override_other_acl({'can_merge_threads': 1})
 
 
         other_thread = testutils.post_thread(self.category_b)
         other_thread = testutils.post_thread(self.category_b)
-        poll = testutils.post_poll(self.thread, self.user)
-        other_poll = testutils.post_poll(other_thread, self.user)
-
-        response = self.client.post(self.api_link, {
-            'thread_url': other_thread.get_absolute_url(),
-            'poll': 0
-        })
+        testutils.post_poll(self.thread, self.user)
+        testutils.post_poll(other_thread, self.user)
+
+        response = self.client.post(
+            self.api_link, {
+                'thread_url': other_thread.get_absolute_url(),
+                'poll': 0,
+            }
+        )
         self.assertContains(response, other_thread.get_absolute_url(), status_code=200)
         self.assertContains(response, other_thread.get_absolute_url(), status_code=200)
 
 
         # other thread has two posts now
         # other thread has two posts now
@@ -333,22 +327,20 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
 
 
     def test_threads_merge_conflict_keep_first_poll(self):
     def test_threads_merge_conflict_keep_first_poll(self):
         """api deletes other poll on merge"""
         """api deletes other poll on merge"""
-        self.override_acl({
-            'can_merge_threads': 1
-        })
+        self.override_acl({'can_merge_threads': 1})
 
 
-        self.override_other_acl({
-            'can_merge_threads': 1
-        })
+        self.override_other_acl({'can_merge_threads': 1})
 
 
         other_thread = testutils.post_thread(self.category_b)
         other_thread = testutils.post_thread(self.category_b)
         poll = testutils.post_poll(self.thread, self.user)
         poll = testutils.post_poll(self.thread, self.user)
         other_poll = testutils.post_poll(other_thread, self.user)
         other_poll = testutils.post_poll(other_thread, self.user)
 
 
-        response = self.client.post(self.api_link, {
-            'thread_url': other_thread.get_absolute_url(),
-            'poll': poll.pk
-        })
+        response = self.client.post(
+            self.api_link, {
+                'thread_url': other_thread.get_absolute_url(),
+                'poll': poll.pk,
+            }
+        )
         self.assertContains(response, other_thread.get_absolute_url(), status_code=200)
         self.assertContains(response, other_thread.get_absolute_url(), status_code=200)
 
 
         # other thread has two posts now
         # other thread has two posts now
@@ -371,22 +363,20 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
 
 
     def test_threads_merge_conflict_keep_other_poll(self):
     def test_threads_merge_conflict_keep_other_poll(self):
         """api deletes first poll on merge"""
         """api deletes first poll on merge"""
-        self.override_acl({
-            'can_merge_threads': 1
-        })
+        self.override_acl({'can_merge_threads': 1})
 
 
-        self.override_other_acl({
-            'can_merge_threads': 1
-        })
+        self.override_other_acl({'can_merge_threads': 1})
 
 
         other_thread = testutils.post_thread(self.category_b)
         other_thread = testutils.post_thread(self.category_b)
         poll = testutils.post_poll(self.thread, self.user)
         poll = testutils.post_poll(self.thread, self.user)
         other_poll = testutils.post_poll(other_thread, self.user)
         other_poll = testutils.post_poll(other_thread, self.user)
 
 
-        response = self.client.post(self.api_link, {
-            'thread_url': other_thread.get_absolute_url(),
-            'poll': other_poll.pk
-        })
+        response = self.client.post(
+            self.api_link, {
+                'thread_url': other_thread.get_absolute_url(),
+                'poll': other_poll.pk,
+            }
+        )
         self.assertContains(response, other_thread.get_absolute_url(), status_code=200)
         self.assertContains(response, other_thread.get_absolute_url(), status_code=200)
 
 
         # other thread has two posts now
         # other thread has two posts now

+ 23 - 24
misago/threads/tests/test_thread_model.py

@@ -23,7 +23,7 @@ class ThreadModelTests(TestCase):
             starter_slug='tester',
             starter_slug='tester',
             last_post_on=datetime,
             last_post_on=datetime,
             last_poster_name='Tester',
             last_poster_name='Tester',
-            last_poster_slug='tester'
+            last_poster_slug='tester',
         )
         )
 
 
         self.thread.set_title("Test thread")
         self.thread.set_title("Test thread")
@@ -38,7 +38,7 @@ class ThreadModelTests(TestCase):
             parsed="<p>Hello! I am test message!</p>",
             parsed="<p>Hello! I am test message!</p>",
             checksum="nope",
             checksum="nope",
             posted_on=datetime,
             posted_on=datetime,
-            updated_on=datetime
+            updated_on=datetime,
         )
         )
 
 
         self.thread.first_post = post
         self.thread.first_post = post
@@ -47,8 +47,7 @@ class ThreadModelTests(TestCase):
 
 
     def test_synchronize(self):
     def test_synchronize(self):
         """synchronize method updates thread data to reflect its contents"""
         """synchronize method updates thread data to reflect its contents"""
-        user = UserModel.objects.create_user(
-            "Bob", "bob@boberson.com", "Pass.123")
+        user = UserModel.objects.create_user("Bob", "bob@boberson.com", "Pass.123")
 
 
         self.assertEqual(self.thread.replies, 0)
         self.assertEqual(self.thread.replies, 0)
 
 
@@ -63,7 +62,7 @@ class ThreadModelTests(TestCase):
             parsed="<p>Hello! I am test message!</p>",
             parsed="<p>Hello! I am test message!</p>",
             checksum="nope",
             checksum="nope",
             posted_on=datetime,
             posted_on=datetime,
-            updated_on=datetime
+            updated_on=datetime,
         )
         )
 
 
         # first sync call, updates last thread
         # first sync call, updates last thread
@@ -91,7 +90,7 @@ class ThreadModelTests(TestCase):
             checksum="nope",
             checksum="nope",
             posted_on=datetime + timedelta(5),
             posted_on=datetime + timedelta(5),
             updated_on=datetime + timedelta(5),
             updated_on=datetime + timedelta(5),
-            is_unapproved=True
+            is_unapproved=True,
         )
         )
 
 
         self.thread.synchronize()
         self.thread.synchronize()
@@ -117,7 +116,7 @@ class ThreadModelTests(TestCase):
             checksum="nope",
             checksum="nope",
             posted_on=datetime + timedelta(10),
             posted_on=datetime + timedelta(10),
             updated_on=datetime + timedelta(10),
             updated_on=datetime + timedelta(10),
-            is_hidden=True
+            is_hidden=True,
         )
         )
 
 
         self.thread.synchronize()
         self.thread.synchronize()
@@ -163,7 +162,7 @@ class ThreadModelTests(TestCase):
         self.assertFalse(self.thread.has_hidden_posts)
         self.assertFalse(self.thread.has_hidden_posts)
         self.assertEqual(self.thread.replies, 3)
         self.assertEqual(self.thread.replies, 3)
 
 
-         # add event post
+        # add event post
         event = Post.objects.create(
         event = Post.objects.create(
             category=self.category,
             category=self.category,
             thread=self.thread,
             thread=self.thread,
@@ -175,7 +174,7 @@ class ThreadModelTests(TestCase):
             checksum="nope",
             checksum="nope",
             posted_on=datetime + timedelta(10),
             posted_on=datetime + timedelta(10),
             updated_on=datetime + timedelta(10),
             updated_on=datetime + timedelta(10),
-            is_event=True
+            is_event=True,
         )
         )
 
 
         self.thread.synchronize()
         self.thread.synchronize()
@@ -203,7 +202,7 @@ class ThreadModelTests(TestCase):
             parsed="<p>Hello! I am test message!</p>",
             parsed="<p>Hello! I am test message!</p>",
             checksum="nope",
             checksum="nope",
             posted_on=datetime,
             posted_on=datetime,
-            updated_on=datetime
+            updated_on=datetime,
         )
         )
 
 
         self.thread.synchronize()
         self.thread.synchronize()
@@ -226,7 +225,7 @@ class ThreadModelTests(TestCase):
             poster_name='test',
             poster_name='test',
             poster_slug='test',
             poster_slug='test',
             poster_ip='127.0.0.1',
             poster_ip='127.0.0.1',
-            choices=[]
+            choices=[],
         )
         )
 
 
         self.thread.synchronize()
         self.thread.synchronize()
@@ -234,8 +233,7 @@ class ThreadModelTests(TestCase):
 
 
     def test_set_first_post(self):
     def test_set_first_post(self):
         """set_first_post sets first post and poster data on thread"""
         """set_first_post sets first post and poster data on thread"""
-        user = UserModel.objects.create_user(
-            "Bob", "bob@boberson.com", "Pass.123")
+        user = UserModel.objects.create_user("Bob", "bob@boberson.com", "Pass.123")
 
 
         datetime = timezone.now() + timedelta(5)
         datetime = timezone.now() + timedelta(5)
 
 
@@ -249,7 +247,7 @@ class ThreadModelTests(TestCase):
             parsed="<p>Hello! I am test message!</p>",
             parsed="<p>Hello! I am test message!</p>",
             checksum="nope",
             checksum="nope",
             posted_on=datetime,
             posted_on=datetime,
-            updated_on=datetime
+            updated_on=datetime,
         )
         )
 
 
         self.thread.set_first_post(post)
         self.thread.set_first_post(post)
@@ -261,8 +259,7 @@ class ThreadModelTests(TestCase):
 
 
     def test_set_last_post(self):
     def test_set_last_post(self):
         """set_last_post sets first post and poster data on thread"""
         """set_last_post sets first post and poster data on thread"""
-        user = UserModel.objects.create_user(
-            "Bob", "bob@boberson.com", "Pass.123")
+        user = UserModel.objects.create_user("Bob", "bob@boberson.com", "Pass.123")
 
 
         datetime = timezone.now() + timedelta(5)
         datetime = timezone.now() + timedelta(5)
 
 
@@ -276,7 +273,7 @@ class ThreadModelTests(TestCase):
             parsed="<p>Hello! I am test message!</p>",
             parsed="<p>Hello! I am test message!</p>",
             checksum="nope",
             checksum="nope",
             posted_on=datetime,
             posted_on=datetime,
-            updated_on=datetime
+            updated_on=datetime,
         )
         )
 
 
         self.thread.set_last_post(post)
         self.thread.set_last_post(post)
@@ -293,7 +290,11 @@ class ThreadModelTests(TestCase):
         Category(
         Category(
             name='New Category',
             name='New Category',
             slug='new-category',
             slug='new-category',
-        ).insert_at(root_category, position='last-child', save=True)
+        ).insert_at(
+            root_category,
+            position='last-child',
+            save=True,
+        )
         new_category = Category.objects.get(slug='new-category')
         new_category = Category.objects.get(slug='new-category')
 
 
         self.thread.move(new_category)
         self.thread.move(new_category)
@@ -316,7 +317,7 @@ class ThreadModelTests(TestCase):
             starter_slug='tester',
             starter_slug='tester',
             last_post_on=datetime,
             last_post_on=datetime,
             last_poster_name='Tester',
             last_poster_name='Tester',
-            last_poster_slug='tester'
+            last_poster_slug='tester',
         )
         )
 
 
         other_thread.set_title("Other thread")
         other_thread.set_title("Other thread")
@@ -331,7 +332,7 @@ class ThreadModelTests(TestCase):
             parsed="<p>Hello! I am other message!</p>",
             parsed="<p>Hello! I am other message!</p>",
             checksum="nope",
             checksum="nope",
             posted_on=datetime,
             posted_on=datetime,
-            updated_on=datetime
+            updated_on=datetime,
         )
         )
 
 
         other_thread.first_post = post
         other_thread.first_post = post
@@ -352,10 +353,8 @@ class ThreadModelTests(TestCase):
         private thread gets deleted automatically
         private thread gets deleted automatically
         when there are no participants left in it
         when there are no participants left in it
         """
         """
-        user_a = UserModel.objects.create_user(
-            "Bob", "bob@boberson.com", "Pass.123")
-        user_b = UserModel.objects.create_user(
-            "Weebl", "weebl@weeblson.com", "Pass.123")
+        user_a = UserModel.objects.create_user("Bob", "bob@boberson.com", "Pass.123")
+        user_b = UserModel.objects.create_user("Weebl", "weebl@weeblson.com", "Pass.123")
 
 
         ThreadParticipant.objects.add_participants(self.thread, [user_a, user_b])
         ThreadParticipant.objects.add_participants(self.thread, [user_a, user_b])
         self.assertEqual(self.thread.participants.count(), 2)
         self.assertEqual(self.thread.participants.count(), 2)

+ 468 - 295
misago/threads/tests/test_thread_patch_api.py

@@ -11,15 +11,18 @@ from .test_threads_api import ThreadsApiTestCase
 
 
 class ThreadPatchApiTestCase(ThreadsApiTestCase):
 class ThreadPatchApiTestCase(ThreadsApiTestCase):
     def patch(self, api_link, ops):
     def patch(self, api_link, ops):
-        return self.client.patch(
-            api_link, json.dumps(ops), content_type="application/json")
+        return self.client.patch(api_link, json.dumps(ops), content_type="application/json")
 
 
 
 
 class ThreadAddAclApiTests(ThreadPatchApiTestCase):
 class ThreadAddAclApiTests(ThreadPatchApiTestCase):
     def test_add_acl_true(self):
     def test_add_acl_true(self):
         """api adds current thread's acl to response"""
         """api adds current thread's acl to response"""
         response = self.patch(self.api_link, [
         response = self.patch(self.api_link, [
-            {'op': 'add', 'path': 'acl', 'value': True}
+            {
+                'op': 'add',
+                'path': 'acl',
+                'value': True,
+            },
         ])
         ])
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
@@ -29,7 +32,11 @@ class ThreadAddAclApiTests(ThreadPatchApiTestCase):
     def test_add_acl_false(self):
     def test_add_acl_false(self):
         """if value is false, api won't add acl to the response, but will set empty key"""
         """if value is false, api won't add acl to the response, but will set empty key"""
         response = self.patch(self.api_link, [
         response = self.patch(self.api_link, [
-            {'op': 'add', 'path': 'acl', 'value': False}
+            {
+                'op': 'add',
+                'path': 'acl',
+                'value': False,
+            },
         ])
         ])
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
@@ -40,13 +47,17 @@ class ThreadAddAclApiTests(ThreadPatchApiTestCase):
 class ThreadChangeTitleApiTests(ThreadPatchApiTestCase):
 class ThreadChangeTitleApiTests(ThreadPatchApiTestCase):
     def test_change_thread_title(self):
     def test_change_thread_title(self):
         """api makes it possible to change thread title"""
         """api makes it possible to change thread title"""
-        self.override_acl({
-            'can_edit_threads': 2
-        })
-
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'title', 'value': "Lorem ipsum change!"}
-        ])
+        self.override_acl({'can_edit_threads': 2})
+
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'title',
+                    'value': "Lorem ipsum change!",
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         thread_json = self.get_thread_json()
         thread_json = self.get_thread_json()
@@ -54,65 +65,82 @@ class ThreadChangeTitleApiTests(ThreadPatchApiTestCase):
 
 
     def test_change_thread_title_no_permission(self):
     def test_change_thread_title_no_permission(self):
         """api validates permission to change title"""
         """api validates permission to change title"""
-        self.override_acl({
-            'can_edit_threads': 0
-        })
-
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'title', 'value': "Lorem ipsum change!"}
-        ])
+        self.override_acl({'can_edit_threads': 0})
+
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'title',
+                    'value': "Lorem ipsum change!",
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         response_json = response.json()
         response_json = response.json()
-        self.assertEqual(response_json['detail'][0],
-            "You can't edit threads in this category.")
+        self.assertEqual(response_json['detail'][0], "You can't edit threads in this category.")
 
 
     def test_change_thread_title_after_edit_time(self):
     def test_change_thread_title_after_edit_time(self):
         """api cleans, validates and rejects too short title"""
         """api cleans, validates and rejects too short title"""
-        self.override_acl({
-            'thread_edit_time': 1,
-            'can_edit_threads': 1
-        })
+        self.override_acl({'thread_edit_time': 1, 'can_edit_threads': 1})
 
 
         self.thread.starter = self.user
         self.thread.starter = self.user
         self.thread.started_on = timezone.now() - timedelta(minutes=10)
         self.thread.started_on = timezone.now() - timedelta(minutes=10)
         self.thread.save()
         self.thread.save()
 
 
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'title', 'value': "Lorem ipsum change!"}
-        ])
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'title',
+                    'value': "Lorem ipsum change!",
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         response_json = response.json()
         response_json = response.json()
-        self.assertEqual(response_json['detail'][0],
-            "You can't edit threads that are older than 1 minute.")
+        self.assertEqual(
+            response_json['detail'][0], "You can't edit threads that are older than 1 minute."
+        )
 
 
     def test_change_thread_title_invalid(self):
     def test_change_thread_title_invalid(self):
         """api cleans, validates and rejects too short title"""
         """api cleans, validates and rejects too short title"""
-        self.override_acl({
-            'can_edit_threads': 2
-        })
-
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'title', 'value': 12}
-        ])
+        self.override_acl({'can_edit_threads': 2})
+
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'title',
+                    'value': 12,
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         response_json = response.json()
         response_json = response.json()
-        self.assertEqual(response_json['detail'][0],
-            "Thread title should be at least 5 characters long (it has 2).")
+        self.assertEqual(
+            response_json['detail'][0],
+            "Thread title should be at least 5 characters long (it has 2)."
+        )
 
 
 
 
 class ThreadPinGloballyApiTests(ThreadPatchApiTestCase):
 class ThreadPinGloballyApiTests(ThreadPatchApiTestCase):
     def test_pin_thread(self):
     def test_pin_thread(self):
         """api makes it possible to pin globally thread"""
         """api makes it possible to pin globally thread"""
-        self.override_acl({
-            'can_pin_threads': 2
-        })
-
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'weight', 'value': 2}
-        ])
+        self.override_acl({'can_pin_threads': 2})
+
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'weight',
+                    'value': 2,
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         thread_json = self.get_thread_json()
         thread_json = self.get_thread_json()
@@ -126,13 +154,17 @@ class ThreadPinGloballyApiTests(ThreadPatchApiTestCase):
         thread_json = self.get_thread_json()
         thread_json = self.get_thread_json()
         self.assertEqual(thread_json['weight'], 2)
         self.assertEqual(thread_json['weight'], 2)
 
 
-        self.override_acl({
-            'can_pin_threads': 2
-        })
-
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'weight', 'value': 0}
-        ])
+        self.override_acl({'can_pin_threads': 2})
+
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'weight',
+                    'value': 0,
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         thread_json = self.get_thread_json()
         thread_json = self.get_thread_json()
@@ -140,18 +172,23 @@ class ThreadPinGloballyApiTests(ThreadPatchApiTestCase):
 
 
     def test_pin_thread_no_permission(self):
     def test_pin_thread_no_permission(self):
         """api pin thread globally with no permission fails"""
         """api pin thread globally with no permission fails"""
-        self.override_acl({
-            'can_pin_threads': 1
-        })
-
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'weight', 'value': 2}
-        ])
+        self.override_acl({'can_pin_threads': 1})
+
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'weight',
+                    'value': 2,
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         response_json = response.json()
         response_json = response.json()
-        self.assertEqual(response_json['detail'][0],
-            "You don't have permission to pin this thread globally.")
+        self.assertEqual(
+            response_json['detail'][0], "You don't have permission to pin this thread globally."
+        )
 
 
         thread_json = self.get_thread_json()
         thread_json = self.get_thread_json()
         self.assertEqual(thread_json['weight'], 0)
         self.assertEqual(thread_json['weight'], 0)
@@ -164,18 +201,23 @@ class ThreadPinGloballyApiTests(ThreadPatchApiTestCase):
         thread_json = self.get_thread_json()
         thread_json = self.get_thread_json()
         self.assertEqual(thread_json['weight'], 2)
         self.assertEqual(thread_json['weight'], 2)
 
 
-        self.override_acl({
-            'can_pin_threads': 1
-        })
-
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'weight', 'value': 1}
-        ])
+        self.override_acl({'can_pin_threads': 1})
+
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'weight',
+                    'value': 1,
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         response_json = response.json()
         response_json = response.json()
-        self.assertEqual(response_json['detail'][0],
-            "You don't have permission to change this thread's weight.")
+        self.assertEqual(
+            response_json['detail'][0], "You don't have permission to change this thread's weight."
+        )
 
 
         thread_json = self.get_thread_json()
         thread_json = self.get_thread_json()
         self.assertEqual(thread_json['weight'], 2)
         self.assertEqual(thread_json['weight'], 2)
@@ -184,13 +226,17 @@ class ThreadPinGloballyApiTests(ThreadPatchApiTestCase):
 class ThreadPinLocallyApiTests(ThreadPatchApiTestCase):
 class ThreadPinLocallyApiTests(ThreadPatchApiTestCase):
     def test_pin_thread(self):
     def test_pin_thread(self):
         """api makes it possible to pin locally thread"""
         """api makes it possible to pin locally thread"""
-        self.override_acl({
-            'can_pin_threads': 1
-        })
-
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'weight', 'value': 1}
-        ])
+        self.override_acl({'can_pin_threads': 1})
+
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'weight',
+                    'value': 1,
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         thread_json = self.get_thread_json()
         thread_json = self.get_thread_json()
@@ -204,13 +250,17 @@ class ThreadPinLocallyApiTests(ThreadPatchApiTestCase):
         thread_json = self.get_thread_json()
         thread_json = self.get_thread_json()
         self.assertEqual(thread_json['weight'], 1)
         self.assertEqual(thread_json['weight'], 1)
 
 
-        self.override_acl({
-            'can_pin_threads': 1
-        })
-
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'weight', 'value': 0}
-        ])
+        self.override_acl({'can_pin_threads': 1})
+
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'weight',
+                    'value': 0,
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         thread_json = self.get_thread_json()
         thread_json = self.get_thread_json()
@@ -218,18 +268,23 @@ class ThreadPinLocallyApiTests(ThreadPatchApiTestCase):
 
 
     def test_pin_thread_no_permission(self):
     def test_pin_thread_no_permission(self):
         """api pin thread locally with no permission fails"""
         """api pin thread locally with no permission fails"""
-        self.override_acl({
-            'can_pin_threads': 0
-        })
-
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'weight', 'value': 1}
-        ])
+        self.override_acl({'can_pin_threads': 0})
+
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'weight',
+                    'value': 1,
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         response_json = response.json()
         response_json = response.json()
-        self.assertEqual(response_json['detail'][0],
-            "You don't have permission to change this thread's weight.")
+        self.assertEqual(
+            response_json['detail'][0], "You don't have permission to change this thread's weight."
+        )
 
 
         thread_json = self.get_thread_json()
         thread_json = self.get_thread_json()
         self.assertEqual(thread_json['weight'], 0)
         self.assertEqual(thread_json['weight'], 0)
@@ -242,18 +297,23 @@ class ThreadPinLocallyApiTests(ThreadPatchApiTestCase):
         thread_json = self.get_thread_json()
         thread_json = self.get_thread_json()
         self.assertEqual(thread_json['weight'], 1)
         self.assertEqual(thread_json['weight'], 1)
 
 
-        self.override_acl({
-            'can_pin_threads': 0
-        })
-
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'weight', 'value': 0}
-        ])
+        self.override_acl({'can_pin_threads': 0})
+
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'weight',
+                    'value': 0,
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         response_json = response.json()
         response_json = response.json()
-        self.assertEqual(response_json['detail'][0],
-            "You don't have permission to change this thread's weight.")
+        self.assertEqual(
+            response_json['detail'][0], "You don't have permission to change this thread's weight."
+        )
 
 
         thread_json = self.get_thread_json()
         thread_json = self.get_thread_json()
         self.assertEqual(thread_json['weight'], 1)
         self.assertEqual(thread_json['weight'], 1)
@@ -266,7 +326,11 @@ class ThreadMoveApiTests(ThreadPatchApiTestCase):
         Category(
         Category(
             name='Category B',
             name='Category B',
             slug='category-b',
             slug='category-b',
-        ).insert_at(self.category, position='last-child', save=True)
+        ).insert_at(
+            self.category,
+            position='last-child',
+            save=True,
+        )
         self.category_b = Category.objects.get(slug='category-b')
         self.category_b = Category.objects.get(slug='category-b')
 
 
     def override_other_acl(self, acl):
     def override_other_acl(self, acl):
@@ -288,25 +352,37 @@ class ThreadMoveApiTests(ThreadPatchApiTestCase):
         if other_category_acl['can_see']:
         if other_category_acl['can_see']:
             visible_categories.append(self.category_b.pk)
             visible_categories.append(self.category_b.pk)
 
 
-        override_acl(self.user, {
-            'visible_categories': visible_categories,
-            'categories': categories_acl,
-        })
+        override_acl(
+            self.user, {
+                'visible_categories': visible_categories,
+                'categories': categories_acl,
+            }
+        )
 
 
     def test_move_thread_no_top(self):
     def test_move_thread_no_top(self):
         """api moves thread to other category, sets no top category"""
         """api moves thread to other category, sets no top category"""
-        self.override_acl({
-            'can_move_threads': True
-        })
-        self.override_other_acl({
-            'can_start_threads': 2
-        })
-
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'category', 'value': self.category_b.pk},
-            {'op': 'add', 'path': 'top-category', 'value': self.category_b.pk},
-            {'op': 'replace', 'path': 'flatten-categories', 'value': None},
-        ])
+        self.override_acl({'can_move_threads': True})
+        self.override_other_acl({'can_start_threads': 2})
+
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'category',
+                    'value': self.category_b.pk,
+                },
+                {
+                    'op': 'add',
+                    'path': 'top-category',
+                    'value': self.category_b.pk,
+                },
+                {
+                    'op': 'replace',
+                    'path': 'flatten-categories',
+                    'value': None,
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         self.override_other_acl({})
         self.override_other_acl({})
@@ -320,22 +396,28 @@ class ThreadMoveApiTests(ThreadPatchApiTestCase):
 
 
     def test_move_thread_with_top(self):
     def test_move_thread_with_top(self):
         """api moves thread to other category, sets top"""
         """api moves thread to other category, sets top"""
-        self.override_acl({
-            'can_move_threads': True
-        })
-        self.override_other_acl({
-            'can_start_threads': 2
-        })
-
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'category', 'value': self.category_b.pk},
-            {
-                'op': 'add',
-                'path': 'top-category',
-                'value': Category.objects.root_category().pk,
-            },
-            {'op': 'replace', 'path': 'flatten-categories', 'value': None},
-        ])
+        self.override_acl({'can_move_threads': True})
+        self.override_other_acl({'can_start_threads': 2})
+
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'category',
+                    'value': self.category_b.pk,
+                },
+                {
+                    'op': 'add',
+                    'path': 'top-category',
+                    'value': Category.objects.root_category().pk,
+                },
+                {
+                    'op': 'replace',
+                    'path': 'flatten-categories',
+                    'value': None,
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         self.override_other_acl({})
         self.override_other_acl({})
@@ -349,19 +431,24 @@ class ThreadMoveApiTests(ThreadPatchApiTestCase):
 
 
     def test_move_thread_no_permission(self):
     def test_move_thread_no_permission(self):
         """api move thread to other category with no permission fails"""
         """api move thread to other category with no permission fails"""
-        self.override_acl({
-            'can_move_threads': False
-        })
+        self.override_acl({'can_move_threads': False})
         self.override_other_acl({})
         self.override_other_acl({})
 
 
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'category', 'value': self.category_b.pk}
-        ])
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'category',
+                    'value': self.category_b.pk,
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         response_json = response.json()
         response_json = response.json()
-        self.assertEqual(response_json['detail'][0],
-            "You don't have permission to move this thread.")
+        self.assertEqual(
+            response_json['detail'][0], "You don't have permission to move this thread."
+        )
 
 
         self.override_other_acl({})
         self.override_other_acl({})
 
 
@@ -370,16 +457,18 @@ class ThreadMoveApiTests(ThreadPatchApiTestCase):
 
 
     def test_move_thread_no_category_access(self):
     def test_move_thread_no_category_access(self):
         """api move thread to category with no access fails"""
         """api move thread to category with no access fails"""
-        self.override_acl({
-            'can_move_threads': True
-        })
-        self.override_other_acl({
-            'can_see': False
-        })
-
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'category', 'value': self.category_b.pk}
-        ])
+        self.override_acl({'can_move_threads': True})
+        self.override_other_acl({'can_see': False})
+
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'category',
+                    'value': self.category_b.pk,
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         response_json = response.json()
         response_json = response.json()
@@ -392,21 +481,25 @@ class ThreadMoveApiTests(ThreadPatchApiTestCase):
 
 
     def test_move_thread_no_category_browse(self):
     def test_move_thread_no_category_browse(self):
         """api move thread to category with no browsing access fails"""
         """api move thread to category with no browsing access fails"""
-        self.override_acl({
-            'can_move_threads': True
-        })
-        self.override_other_acl({
-            'can_browse': False
-        })
-
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'category', 'value': self.category_b.pk}
-        ])
+        self.override_acl({'can_move_threads': True})
+        self.override_other_acl({'can_browse': False})
+
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'category',
+                    'value': self.category_b.pk,
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         response_json = response.json()
         response_json = response.json()
-        self.assertEqual(response_json['detail'][0],
-            'You don\'t have permission to browse "Category B" contents.')
+        self.assertEqual(
+            response_json['detail'][0],
+            'You don\'t have permission to browse "Category B" contents.'
+        )
 
 
         self.override_other_acl({})
         self.override_other_acl({})
 
 
@@ -415,21 +508,24 @@ class ThreadMoveApiTests(ThreadPatchApiTestCase):
 
 
     def test_move_thread_same_category(self):
     def test_move_thread_same_category(self):
         """api move thread to category it's already in fails"""
         """api move thread to category it's already in fails"""
-        self.override_acl({
-            'can_move_threads': True
-        })
-        self.override_other_acl({
-            'can_start_threads': 2
-        })
-
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'category', 'value': self.thread.category_id}
-        ])
+        self.override_acl({'can_move_threads': True})
+        self.override_other_acl({'can_start_threads': 2})
+
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'category',
+                    'value': self.thread.category_id,
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         response_json = response.json()
         response_json = response.json()
-        self.assertEqual(response_json['detail'][0],
-            "You can't move thread to the category it's already in.")
+        self.assertEqual(
+            response_json['detail'][0], "You can't move thread to the category it's already in."
+        )
 
 
         self.override_other_acl({})
         self.override_other_acl({})
 
 
@@ -438,9 +534,15 @@ class ThreadMoveApiTests(ThreadPatchApiTestCase):
 
 
     def test_thread_flatten_categories(self):
     def test_thread_flatten_categories(self):
         """api flatten thread categories"""
         """api flatten thread categories"""
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'flatten-categories', 'value': None}
-        ])
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'flatten-categories',
+                    'value': None,
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         response_json = response.json()
         response_json = response.json()
@@ -453,14 +555,20 @@ class ThreadMoveApiTests(ThreadPatchApiTestCase):
 
 
         self.override_other_acl({})
         self.override_other_acl({})
 
 
-        response = self.patch(self.api_link, [
-            {
-                'op': 'add',
-                'path': 'top-category',
-                'value': Category.objects.root_category().pk,
-            },
-            {'op': 'replace', 'path': 'flatten-categories', 'value': None},
-        ])
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'add',
+                    'path': 'top-category',
+                    'value': Category.objects.root_category().pk,
+                },
+                {
+                    'op': 'replace',
+                    'path': 'flatten-categories',
+                    'value': None,
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         response_json = response.json()
         response_json = response.json()
@@ -471,13 +579,17 @@ class ThreadMoveApiTests(ThreadPatchApiTestCase):
 class ThreadCloseApiTests(ThreadPatchApiTestCase):
 class ThreadCloseApiTests(ThreadPatchApiTestCase):
     def test_close_thread(self):
     def test_close_thread(self):
         """api makes it possible to close thread"""
         """api makes it possible to close thread"""
-        self.override_acl({
-            'can_close_threads': True
-        })
-
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'is-closed', 'value': True}
-        ])
+        self.override_acl({'can_close_threads': True})
+
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-closed',
+                    'value': True,
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         thread_json = self.get_thread_json()
         thread_json = self.get_thread_json()
@@ -491,13 +603,17 @@ class ThreadCloseApiTests(ThreadPatchApiTestCase):
         thread_json = self.get_thread_json()
         thread_json = self.get_thread_json()
         self.assertTrue(thread_json['is_closed'])
         self.assertTrue(thread_json['is_closed'])
 
 
-        self.override_acl({
-            'can_close_threads': True
-        })
-
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'is-closed', 'value': False}
-        ])
+        self.override_acl({'can_close_threads': True})
+
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-closed',
+                    'value': False,
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         thread_json = self.get_thread_json()
         thread_json = self.get_thread_json()
@@ -505,18 +621,23 @@ class ThreadCloseApiTests(ThreadPatchApiTestCase):
 
 
     def test_close_thread_no_permission(self):
     def test_close_thread_no_permission(self):
         """api close thread with no permission fails"""
         """api close thread with no permission fails"""
-        self.override_acl({
-            'can_close_threads': False
-        })
-
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'is-closed', 'value': True}
-        ])
+        self.override_acl({'can_close_threads': False})
+
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-closed',
+                    'value': True,
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         response_json = response.json()
         response_json = response.json()
-        self.assertEqual(response_json['detail'][0],
-            "You don't have permission to close this thread.")
+        self.assertEqual(
+            response_json['detail'][0], "You don't have permission to close this thread."
+        )
 
 
         thread_json = self.get_thread_json()
         thread_json = self.get_thread_json()
         self.assertFalse(thread_json['is_closed'])
         self.assertFalse(thread_json['is_closed'])
@@ -529,18 +650,23 @@ class ThreadCloseApiTests(ThreadPatchApiTestCase):
         thread_json = self.get_thread_json()
         thread_json = self.get_thread_json()
         self.assertTrue(thread_json['is_closed'])
         self.assertTrue(thread_json['is_closed'])
 
 
-        self.override_acl({
-            'can_close_threads': False
-        })
-
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'is-closed', 'value': False}
-        ])
+        self.override_acl({'can_close_threads': False})
+
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-closed',
+                    'value': False,
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         response_json = response.json()
         response_json = response.json()
-        self.assertEqual(response_json['detail'][0],
-            "You don't have permission to open this thread.")
+        self.assertEqual(
+            response_json['detail'][0], "You don't have permission to open this thread."
+        )
 
 
         thread_json = self.get_thread_json()
         thread_json = self.get_thread_json()
         self.assertTrue(thread_json['is_closed'])
         self.assertTrue(thread_json['is_closed'])
@@ -552,13 +678,17 @@ class ThreadApproveApiTests(ThreadPatchApiTestCase):
         self.thread.is_unapproved = True
         self.thread.is_unapproved = True
         self.thread.save()
         self.thread.save()
 
 
-        self.override_acl({
-            'can_approve_content': 1
-        })
-
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'is-unapproved', 'value': False}
-        ])
+        self.override_acl({'can_approve_content': 1})
+
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-unapproved',
+                    'value': False,
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         thread_json = self.get_thread_json()
         thread_json = self.get_thread_json()
@@ -566,35 +696,40 @@ class ThreadApproveApiTests(ThreadPatchApiTestCase):
 
 
     def test_unapprove_thread(self):
     def test_unapprove_thread(self):
         """api returns permission error on approval removal"""
         """api returns permission error on approval removal"""
-        self.override_acl({
-            'can_approve_content': 1
-        })
-
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'is-unapproved', 'value': True}
-        ])
+        self.override_acl({'can_approve_content': 1})
+
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-unapproved',
+                    'value': True,
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         response_json = response.json()
         response_json = response.json()
-        self.assertEqual(response_json['detail'][0],
-            "Content approval can't be reversed.")
+        self.assertEqual(response_json['detail'][0], "Content approval can't be reversed.")
 
 
 
 
 class ThreadHideApiTests(ThreadPatchApiTestCase):
 class ThreadHideApiTests(ThreadPatchApiTestCase):
     def test_hide_thread(self):
     def test_hide_thread(self):
         """api makes it possible to hide thread"""
         """api makes it possible to hide thread"""
-        self.override_acl({
-            'can_hide_threads': 1
-        })
-
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'is-hidden', 'value': True}
-        ])
+        self.override_acl({'can_hide_threads': 1})
+
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-hidden',
+                    'value': True,
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        self.override_acl({
-            'can_hide_threads': 1
-        })
+        self.override_acl({'can_hide_threads': 1})
 
 
         thread_json = self.get_thread_json()
         thread_json = self.get_thread_json()
         self.assertTrue(thread_json['is_hidden'])
         self.assertTrue(thread_json['is_hidden'])
@@ -604,43 +739,48 @@ class ThreadHideApiTests(ThreadPatchApiTestCase):
         self.thread.is_hidden = True
         self.thread.is_hidden = True
         self.thread.save()
         self.thread.save()
 
 
-        self.override_acl({
-            'can_hide_threads': 1
-        })
+        self.override_acl({'can_hide_threads': 1})
 
 
         thread_json = self.get_thread_json()
         thread_json = self.get_thread_json()
         self.assertTrue(thread_json['is_hidden'])
         self.assertTrue(thread_json['is_hidden'])
 
 
-        self.override_acl({
-            'can_hide_threads': 1
-        })
-
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'is-hidden', 'value': False}
-        ])
+        self.override_acl({'can_hide_threads': 1})
+
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-hidden',
+                    'value': False,
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        self.override_acl({
-            'can_hide_threads': 1
-        })
+        self.override_acl({'can_hide_threads': 1})
 
 
         thread_json = self.get_thread_json()
         thread_json = self.get_thread_json()
         self.assertFalse(thread_json['is_hidden'])
         self.assertFalse(thread_json['is_hidden'])
 
 
     def test_hide_thread_no_permission(self):
     def test_hide_thread_no_permission(self):
         """api hide thread with no permission fails"""
         """api hide thread with no permission fails"""
-        self.override_acl({
-            'can_hide_threads': 0
-        })
-
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'is-hidden', 'value': True}
-        ])
+        self.override_acl({'can_hide_threads': 0})
+
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-hidden',
+                    'value': True,
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         response_json = response.json()
         response_json = response.json()
-        self.assertEqual(response_json['detail'][0],
-            "You don't have permission to hide this thread.")
+        self.assertEqual(
+            response_json['detail'][0], "You don't have permission to hide this thread."
+        )
 
 
         thread_json = self.get_thread_json()
         thread_json = self.get_thread_json()
         self.assertFalse(thread_json['is_hidden'])
         self.assertFalse(thread_json['is_hidden'])
@@ -650,29 +790,37 @@ class ThreadHideApiTests(ThreadPatchApiTestCase):
         self.thread.is_hidden = True
         self.thread.is_hidden = True
         self.thread.save()
         self.thread.save()
 
 
-        self.override_acl({
-            'can_hide_threads': 1
-        })
+        self.override_acl({'can_hide_threads': 1})
 
 
         thread_json = self.get_thread_json()
         thread_json = self.get_thread_json()
         self.assertTrue(thread_json['is_hidden'])
         self.assertTrue(thread_json['is_hidden'])
 
 
-        self.override_acl({
-            'can_hide_threads': 0
-        })
-
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'is-hidden', 'value': False}
-        ])
+        self.override_acl({'can_hide_threads': 0})
+
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-hidden',
+                    'value': False,
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 404)
         self.assertEqual(response.status_code, 404)
 
 
 
 
 class ThreadSubscribeApiTests(ThreadPatchApiTestCase):
 class ThreadSubscribeApiTests(ThreadPatchApiTestCase):
     def test_subscribe_thread(self):
     def test_subscribe_thread(self):
         """api makes it possible to subscribe thread"""
         """api makes it possible to subscribe thread"""
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'subscription', 'value': 'notify'}
-        ])
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'subscription',
+                    'value': 'notify',
+                },
+            ]
+        )
 
 
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
@@ -684,9 +832,15 @@ class ThreadSubscribeApiTests(ThreadPatchApiTestCase):
 
 
     def test_subscribe_thread_with_email(self):
     def test_subscribe_thread_with_email(self):
         """api makes it possible to subscribe thread with emails"""
         """api makes it possible to subscribe thread with emails"""
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'subscription', 'value': 'email'}
-        ])
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'subscription',
+                    'value': 'email',
+                },
+            ]
+        )
 
 
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
@@ -698,9 +852,15 @@ class ThreadSubscribeApiTests(ThreadPatchApiTestCase):
 
 
     def test_unsubscribe_thread(self):
     def test_unsubscribe_thread(self):
         """api makes it possible to unsubscribe thread"""
         """api makes it possible to unsubscribe thread"""
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'subscription', 'value': 'remove'}
-        ])
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'subscription',
+                    'value': 'remove',
+                },
+            ]
+        )
 
 
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
@@ -713,19 +873,32 @@ class ThreadSubscribeApiTests(ThreadPatchApiTestCase):
         """api makes it impossible to subscribe thread"""
         """api makes it impossible to subscribe thread"""
         self.logout_user()
         self.logout_user()
 
 
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'subscription', 'value': 'email'}
-        ])
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'subscription',
+                    'value': 'email',
+                },
+            ]
+        )
 
 
         self.assertEqual(response.status_code, 403)
         self.assertEqual(response.status_code, 403)
 
 
     def test_subscribe_nonexistant_thread(self):
     def test_subscribe_nonexistant_thread(self):
         """api makes it impossible to subscribe nonexistant thread"""
         """api makes it impossible to subscribe nonexistant thread"""
         bad_api_link = self.api_link.replace(
         bad_api_link = self.api_link.replace(
-            six.text_type(self.thread.pk), six.text_type(self.thread.pk + 9))
-
-        response = self.patch(bad_api_link, [
-            {'op': 'replace', 'path': 'subscription', 'value': 'email'}
-        ])
+            six.text_type(self.thread.pk), six.text_type(self.thread.pk + 9)
+        )
+
+        response = self.patch(
+            bad_api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'subscription',
+                    'value': 'email',
+                },
+            ]
+        )
 
 
         self.assertEqual(response.status_code, 404)
         self.assertEqual(response.status_code, 404)

+ 13 - 8
misago/threads/tests/test_thread_poll_api.py

@@ -16,9 +16,11 @@ class ThreadPollApiTestCase(AuthenticatedUserTestCase):
         self.thread = testutils.post_thread(self.category, poster=self.user)
         self.thread = testutils.post_thread(self.category, poster=self.user)
         self.override_acl()
         self.override_acl()
 
 
-        self.api_link = reverse('misago:api:thread-poll-list', kwargs={
-            'thread_pk': self.thread.pk
-        })
+        self.api_link = reverse(
+            'misago:api:thread-poll-list', kwargs={
+                'thread_pk': self.thread.pk,
+            }
+        )
 
 
     def post(self, url, data=None):
     def post(self, url, data=None):
         return self.client.post(url, json.dumps(data or {}), content_type='application/json')
         return self.client.post(url, json.dumps(data or {}), content_type='application/json')
@@ -39,7 +41,7 @@ class ThreadPollApiTestCase(AuthenticatedUserTestCase):
             'can_edit_polls': 1,
             'can_edit_polls': 1,
             'can_delete_polls': 1,
             'can_delete_polls': 1,
             'poll_edit_time': 0,
             'poll_edit_time': 0,
-            'can_always_see_poll_voters': 0
+            'can_always_see_poll_voters': 0,
         })
         })
 
 
         if user:
         if user:
@@ -52,7 +54,10 @@ class ThreadPollApiTestCase(AuthenticatedUserTestCase):
     def mock_poll(self):
     def mock_poll(self):
         self.poll = self.thread.poll = testutils.post_poll(self.thread, self.user)
         self.poll = self.thread.poll = testutils.post_poll(self.thread, self.user)
 
 
-        self.api_link = reverse('misago:api:thread-poll-detail', kwargs={
-            'thread_pk': self.thread.pk,
-            'pk': self.poll.pk
-        })
+        self.api_link = reverse(
+            'misago:api:thread-poll-detail',
+            kwargs={
+                'thread_pk': self.thread.pk,
+                'pk': self.poll.pk,
+            }
+        )

+ 134 - 131
misago/threads/tests/test_thread_pollcreate_api.py

@@ -16,36 +16,36 @@ class ThreadPollCreateTests(ThreadPollApiTestCase):
 
 
     def test_invalid_thread_id(self):
     def test_invalid_thread_id(self):
         """api validates that thread id is integer"""
         """api validates that thread id is integer"""
-        api_link = reverse('misago:api:thread-poll-list', kwargs={
-            'thread_pk': 'kjha6dsa687sa'
-        })
+        api_link = reverse(
+            'misago:api:thread-poll-list', kwargs={
+                'thread_pk': 'kjha6dsa687sa',
+            }
+        )
 
 
         response = self.post(api_link)
         response = self.post(api_link)
         self.assertEqual(response.status_code, 404)
         self.assertEqual(response.status_code, 404)
 
 
     def test_nonexistant_thread_id(self):
     def test_nonexistant_thread_id(self):
         """api validates that thread exists"""
         """api validates that thread exists"""
-        api_link = reverse('misago:api:thread-poll-list', kwargs={
-            'thread_pk': self.thread.pk + 1
-        })
+        api_link = reverse(
+            'misago:api:thread-poll-list', kwargs={
+                'thread_pk': self.thread.pk + 1,
+            }
+        )
 
 
         response = self.post(api_link)
         response = self.post(api_link)
         self.assertEqual(response.status_code, 404)
         self.assertEqual(response.status_code, 404)
 
 
     def test_no_permission(self):
     def test_no_permission(self):
         """api validates that user has permission to start poll in thread"""
         """api validates that user has permission to start poll in thread"""
-        self.override_acl({
-            'can_start_polls': 0
-        })
+        self.override_acl({'can_start_polls': 0})
 
 
         response = self.post(self.api_link)
         response = self.post(self.api_link)
         self.assertContains(response, "can't start polls", status_code=403)
         self.assertContains(response, "can't start polls", status_code=403)
 
 
     def test_no_permission_closed_thread(self):
     def test_no_permission_closed_thread(self):
         """api validates that user has permission to start poll in closed thread"""
         """api validates that user has permission to start poll in closed thread"""
-        self.override_acl(category={
-            'can_close_threads': 0
-        })
+        self.override_acl(category={'can_close_threads': 0})
 
 
         self.thread.is_closed = True
         self.thread.is_closed = True
         self.thread.save()
         self.thread.save()
@@ -53,18 +53,14 @@ class ThreadPollCreateTests(ThreadPollApiTestCase):
         response = self.post(self.api_link)
         response = self.post(self.api_link)
         self.assertContains(response, "thread is closed", status_code=403)
         self.assertContains(response, "thread is closed", status_code=403)
 
 
-        self.override_acl(category={
-            'can_close_threads': 1
-        })
+        self.override_acl(category={'can_close_threads': 1})
 
 
         response = self.post(self.api_link)
         response = self.post(self.api_link)
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
     def test_no_permission_closed_category(self):
     def test_no_permission_closed_category(self):
         """api validates that user has permission to start poll in closed category"""
         """api validates that user has permission to start poll in closed category"""
-        self.override_acl(category={
-            'can_close_threads': 0
-        })
+        self.override_acl(category={'can_close_threads': 0})
 
 
         self.category.is_closed = True
         self.category.is_closed = True
         self.category.save()
         self.category.save()
@@ -72,18 +68,14 @@ class ThreadPollCreateTests(ThreadPollApiTestCase):
         response = self.post(self.api_link)
         response = self.post(self.api_link)
         self.assertContains(response, "category is closed", status_code=403)
         self.assertContains(response, "category is closed", status_code=403)
 
 
-        self.override_acl(category={
-            'can_close_threads': 1
-        })
+        self.override_acl(category={'can_close_threads': 1})
 
 
         response = self.post(self.api_link)
         response = self.post(self.api_link)
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
     def test_no_permission_other_user_thread(self):
     def test_no_permission_other_user_thread(self):
         """api validates that user has permission to start poll in other user's thread"""
         """api validates that user has permission to start poll in other user's thread"""
-        self.override_acl({
-            'can_start_polls': 1
-        })
+        self.override_acl({'can_start_polls': 1})
 
 
         self.thread.starter = None
         self.thread.starter = None
         self.thread.save()
         self.thread.save()
@@ -91,9 +83,7 @@ class ThreadPollCreateTests(ThreadPollApiTestCase):
         response = self.post(self.api_link)
         response = self.post(self.api_link)
         self.assertContains(response, "can't start polls in other users threads", status_code=403)
         self.assertContains(response, "can't start polls in other users threads", status_code=403)
 
 
-        self.override_acl({
-            'can_start_polls': 2
-        })
+        self.override_acl({'can_start_polls': 2})
 
 
         response = self.post(self.api_link)
         response = self.post(self.api_link)
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
@@ -108,8 +98,12 @@ class ThreadPollCreateTests(ThreadPollApiTestCase):
             poster_ip='127.0.0.1',
             poster_ip='127.0.0.1',
             length=30,
             length=30,
             question='Test',
             question='Test',
-            choices=[{'hash': 't3st'}],
-            allowed_choices=1
+            choices=[
+                {
+                    'hash': 't3st'
+                },
+            ],
+            allowed_choices=1,
         )
         )
 
 
         response = self.post(self.api_link)
         response = self.post(self.api_link)
@@ -125,156 +119,165 @@ class ThreadPollCreateTests(ThreadPollApiTestCase):
 
 
     def test_length_validation(self):
     def test_length_validation(self):
         """api validates poll's length"""
         """api validates poll's length"""
-        response = self.post(self.api_link, data={
-            'length': -1
-        })
+        response = self.post(
+            self.api_link, data={
+                'length': -1,
+            }
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         response_json = response.json()
         response_json = response.json()
-        self.assertEqual(response_json['length'], [
-            "Ensure this value is greater than or equal to 0."
-        ])
+        self.assertEqual(
+            response_json['length'], ["Ensure this value is greater than or equal to 0."]
+        )
 
 
-        response = self.post(self.api_link, data={
-            'length': 200
-        })
+        response = self.post(
+            self.api_link, data={
+                'length': 200,
+            }
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         response_json = response.json()
         response_json = response.json()
-        self.assertEqual(response_json['length'], [
-            "Ensure this value is less than or equal to 180."
-        ])
+        self.assertEqual(
+            response_json['length'], ["Ensure this value is less than or equal to 180."]
+        )
 
 
     def test_question_validation(self):
     def test_question_validation(self):
         """api validates question length"""
         """api validates question length"""
-        response = self.post(self.api_link, data={
-            'question': 'abcd' * 255
-        })
+        response = self.post(
+            self.api_link, data={
+                'question': 'abcd' * 255,
+            }
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         response_json = response.json()
         response_json = response.json()
-        self.assertEqual(response_json['question'], [
-            "Ensure this field has no more than 255 characters."
-        ])
+        self.assertEqual(
+            response_json['question'], ["Ensure this field has no more than 255 characters."]
+        )
 
 
     def test_validate_choice_length(self):
     def test_validate_choice_length(self):
         """api validates single choice length"""
         """api validates single choice length"""
-        response = self.post(self.api_link, data={
-            'choices': [
-                {
-                    'hash': 'qwertyuiopas',
-                    'label': ''
-                }
-            ]
-        })
+        response = self.post(
+            self.api_link, data={
+                'choices': [
+                    {
+                        'hash': 'qwertyuiopas',
+                        'label': '',
+                    },
+                ],
+            }
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         response_json = response.json()
         response_json = response.json()
-        self.assertEqual(response_json['choices'], [
-            "One or more poll choices are invalid."
-        ])
-
-        response = self.post(self.api_link, data={
-            'choices': [
-                {
-                    'hash': 'qwertyuiopas',
-                    'label': 'abcd' * 255
-                }
-            ]
-        })
+        self.assertEqual(response_json['choices'], ["One or more poll choices are invalid."])
+
+        response = self.post(
+            self.api_link,
+            data={
+                'choices': [
+                    {
+                        'hash': 'qwertyuiopas',
+                        'label': 'abcd' * 255,
+                    },
+                ],
+            }
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         response_json = response.json()
         response_json = response.json()
-        self.assertEqual(response_json['choices'], [
-            "One or more poll choices are invalid."
-        ])
+        self.assertEqual(response_json['choices'], ["One or more poll choices are invalid."])
 
 
     def test_validate_two_choices(self):
     def test_validate_two_choices(self):
         """api validates that there are at least two choices in poll"""
         """api validates that there are at least two choices in poll"""
-        response = self.post(self.api_link, data={
-            'choices': [
-                {
-                    'label': 'Choice'
-                }
-            ]
-        })
+        response = self.post(self.api_link, data={'choices': [{'label': 'Choice'}]})
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         response_json = response.json()
         response_json = response.json()
-        self.assertEqual(response_json['choices'], [
-            "You need to add at least two choices to a poll."
-        ])
+        self.assertEqual(
+            response_json['choices'], ["You need to add at least two choices to a poll."]
+        )
 
 
     def test_validate_max_choices(self):
     def test_validate_max_choices(self):
         """api validates that there are no more choices in poll than allowed number"""
         """api validates that there are no more choices in poll than allowed number"""
-        response = self.post(self.api_link, data={
-            'choices': [
-                {
-                    'label': 'Choice'
-                }
-            ] * (MAX_POLL_OPTIONS + 1)
-        })
+        response = self.post(
+            self.api_link, data={
+                'choices': [
+                    {
+                        'label': 'Choice',
+                    },
+                ] * (MAX_POLL_OPTIONS + 1),
+            }
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         error_formats = (MAX_POLL_OPTIONS, MAX_POLL_OPTIONS + 1)
         error_formats = (MAX_POLL_OPTIONS, MAX_POLL_OPTIONS + 1)
 
 
         response_json = response.json()
         response_json = response.json()
-        self.assertEqual(response_json['choices'], [
-            "You can't add more than %s options to a single poll (added %s)." % error_formats
-        ])
+        self.assertEqual(
+            response_json['choices'],
+            ["You can't add more than %s options to a single poll (added %s)." % error_formats]
+        )
 
 
     def test_allowed_choices_validation(self):
     def test_allowed_choices_validation(self):
         """api validates allowed choices number"""
         """api validates allowed choices number"""
-        response = self.post(self.api_link, data={
-            'allowed_choices': 0
-        })
+        response = self.post(self.api_link, data={'allowed_choices': 0})
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         response_json = response.json()
         response_json = response.json()
-        self.assertEqual(response_json['allowed_choices'], [
-            "Ensure this value is greater than or equal to 1."
-        ])
-
-        response = self.post(self.api_link, data={
-            'length': 0,
-            'question': "Lorem ipsum",
-            'allowed_choices': 3,
-            'choices': [
-                {
-                    'label': 'Choice'
-                },
-                {
-                    'label': 'Choice'
-                }
-            ]
-        })
+        self.assertEqual(
+            response_json['allowed_choices'], ["Ensure this value is greater than or equal to 1."]
+        )
+
+        response = self.post(
+            self.api_link,
+            data={
+                'length': 0,
+                'question': "Lorem ipsum",
+                'allowed_choices': 3,
+                'choices': [
+                    {
+                        'label': 'Choice',
+                    },
+                    {
+                        'label': 'Choice',
+                    },
+                ],
+            }
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         response_json = response.json()
         response_json = response.json()
-        self.assertEqual(response_json['non_field_errors'], [
-            "Number of allowed choices can't be greater than number of all choices."
-        ])
+        self.assertEqual(
+            response_json['non_field_errors'],
+            ["Number of allowed choices can't be greater than number of all choices."]
+        )
 
 
     def test_poll_created(self):
     def test_poll_created(self):
         """api creates public poll if provided with valid data"""
         """api creates public poll if provided with valid data"""
-        response = self.post(self.api_link, data={
-            'length': 40,
-            'question': "Select two best colors",
-            'allowed_choices': 2,
-            'allow_revotes': True,
-            'is_public': True,
-            'choices': [
-                {
-                    'label': '\nRed '
-                },
-                {
-                    'label': 'Green'
-                },
-                {
-                    'label': 'Blue'
-                }
-            ]
-        })
+        response = self.post(
+            self.api_link,
+            data={
+                'length': 40,
+                'question': "Select two best colors",
+                'allowed_choices': 2,
+                'allow_revotes': True,
+                'is_public': True,
+                'choices': [
+                    {
+                        'label': '\nRed ',
+                    },
+                    {
+                        'label': 'Green',
+                    },
+                    {
+                        'label': 'Blue',
+                    },
+                ],
+            }
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         response_json = response.json()
         response_json = response.json()

+ 40 - 46
misago/threads/tests/test_thread_polldelete_api.py

@@ -23,71 +23,78 @@ class ThreadPollDeleteTests(ThreadPollApiTestCase):
 
 
     def test_invalid_thread_id(self):
     def test_invalid_thread_id(self):
         """api validates that thread id is integer"""
         """api validates that thread id is integer"""
-        api_link = reverse('misago:api:thread-poll-detail', kwargs={
-            'thread_pk': 'kjha6dsa687sa',
-            'pk': self.poll.pk
-        })
+        api_link = reverse(
+            'misago:api:thread-poll-detail',
+            kwargs={
+                'thread_pk': 'kjha6dsa687sa',
+                'pk': self.poll.pk,
+            }
+        )
 
 
         response = self.client.delete(api_link)
         response = self.client.delete(api_link)
         self.assertEqual(response.status_code, 404)
         self.assertEqual(response.status_code, 404)
 
 
     def test_nonexistant_thread_id(self):
     def test_nonexistant_thread_id(self):
         """api validates that thread exists"""
         """api validates that thread exists"""
-        api_link = reverse('misago:api:thread-poll-detail', kwargs={
-            'thread_pk': self.thread.pk + 1,
-            'pk': self.poll.pk
-        })
+        api_link = reverse(
+            'misago:api:thread-poll-detail',
+            kwargs={
+                'thread_pk': self.thread.pk + 1,
+                'pk': self.poll.pk,
+            }
+        )
 
 
         response = self.client.delete(api_link)
         response = self.client.delete(api_link)
         self.assertEqual(response.status_code, 404)
         self.assertEqual(response.status_code, 404)
 
 
     def test_invalid_poll_id(self):
     def test_invalid_poll_id(self):
         """api validates that poll id is integer"""
         """api validates that poll id is integer"""
-        api_link = reverse('misago:api:thread-poll-detail', kwargs={
-            'thread_pk': self.thread.pk,
-            'pk': 'sad98as7d97sa98'
-        })
+        api_link = reverse(
+            'misago:api:thread-poll-detail',
+            kwargs={
+                'thread_pk': self.thread.pk,
+                'pk': 'sad98as7d97sa98',
+            }
+        )
 
 
         response = self.client.delete(api_link)
         response = self.client.delete(api_link)
         self.assertEqual(response.status_code, 404)
         self.assertEqual(response.status_code, 404)
 
 
     def test_nonexistant_poll_id(self):
     def test_nonexistant_poll_id(self):
         """api validates that poll exists"""
         """api validates that poll exists"""
-        api_link = reverse('misago:api:thread-poll-detail', kwargs={
-            'thread_pk': self.thread.pk,
-            'pk': self.poll.pk + 123
-        })
+        api_link = reverse(
+            'misago:api:thread-poll-detail',
+            kwargs={
+                'thread_pk': self.thread.pk,
+                'pk': self.poll.pk + 123,
+            }
+        )
 
 
         response = self.client.delete(api_link)
         response = self.client.delete(api_link)
         self.assertEqual(response.status_code, 404)
         self.assertEqual(response.status_code, 404)
 
 
     def test_no_permission(self):
     def test_no_permission(self):
         """api validates that user has permission to delete poll in thread"""
         """api validates that user has permission to delete poll in thread"""
-        self.override_acl({
-            'can_delete_polls': 0
-        })
+        self.override_acl({'can_delete_polls': 0})
 
 
         response = self.client.delete(self.api_link)
         response = self.client.delete(self.api_link)
         self.assertContains(response, "can't delete polls", status_code=403)
         self.assertContains(response, "can't delete polls", status_code=403)
 
 
     def test_no_permission_timeout(self):
     def test_no_permission_timeout(self):
         """api validates that user's window to delete poll in thread has closed"""
         """api validates that user's window to delete poll in thread has closed"""
-        self.override_acl({
-            'can_delete_polls': 1,
-            'poll_edit_time': 5
-        })
+        self.override_acl({'can_delete_polls': 1, 'poll_edit_time': 5})
 
 
         self.poll.posted_on = timezone.now() - timedelta(minutes=15)
         self.poll.posted_on = timezone.now() - timedelta(minutes=15)
         self.poll.save()
         self.poll.save()
 
 
         response = self.client.delete(self.api_link)
         response = self.client.delete(self.api_link)
-        self.assertContains(response, "can't delete polls that are older than 5 minutes", status_code=403)
+        self.assertContains(
+            response, "can't delete polls that are older than 5 minutes", status_code=403
+        )
 
 
     def test_no_permission_poll_closed(self):
     def test_no_permission_poll_closed(self):
         """api validates that user's window to delete poll in thread has closed"""
         """api validates that user's window to delete poll in thread has closed"""
-        self.override_acl({
-            'can_delete_polls': 1
-        })
+        self.override_acl({'can_delete_polls': 1})
 
 
         self.poll.posted_on = timezone.now() - timedelta(days=15)
         self.poll.posted_on = timezone.now() - timedelta(days=15)
         self.poll.length = 5
         self.poll.length = 5
@@ -98,9 +105,7 @@ class ThreadPollDeleteTests(ThreadPollApiTestCase):
 
 
     def test_no_permission_other_user_poll(self):
     def test_no_permission_other_user_poll(self):
         """api validates that user has permission to delete other user poll in thread"""
         """api validates that user has permission to delete other user poll in thread"""
-        self.override_acl({
-            'can_delete_polls': 1
-        })
+        self.override_acl({'can_delete_polls': 1})
 
 
         self.poll.poster = None
         self.poll.poster = None
         self.poll.save()
         self.poll.save()
@@ -110,9 +115,7 @@ class ThreadPollDeleteTests(ThreadPollApiTestCase):
 
 
     def test_no_permission_closed_thread(self):
     def test_no_permission_closed_thread(self):
         """api validates that user has permission to delete poll in closed thread"""
         """api validates that user has permission to delete poll in closed thread"""
-        self.override_acl(category={
-            'can_close_threads': 0
-        })
+        self.override_acl(category={'can_close_threads': 0})
 
 
         self.thread.is_closed = True
         self.thread.is_closed = True
         self.thread.save()
         self.thread.save()
@@ -120,18 +123,14 @@ class ThreadPollDeleteTests(ThreadPollApiTestCase):
         response = self.client.delete(self.api_link)
         response = self.client.delete(self.api_link)
         self.assertContains(response, "thread is closed", status_code=403)
         self.assertContains(response, "thread is closed", status_code=403)
 
 
-        self.override_acl(category={
-            'can_close_threads': 1
-        })
+        self.override_acl(category={'can_close_threads': 1})
 
 
         response = self.client.delete(self.api_link)
         response = self.client.delete(self.api_link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
     def test_no_permission_closed_category(self):
     def test_no_permission_closed_category(self):
         """api validates that user has permission to delete poll in closed category"""
         """api validates that user has permission to delete poll in closed category"""
-        self.override_acl(category={
-            'can_close_threads': 0
-        })
+        self.override_acl(category={'can_close_threads': 0})
 
 
         self.category.is_closed = True
         self.category.is_closed = True
         self.category.save()
         self.category.save()
@@ -139,9 +138,7 @@ class ThreadPollDeleteTests(ThreadPollApiTestCase):
         response = self.client.delete(self.api_link)
         response = self.client.delete(self.api_link)
         self.assertContains(response, "category is closed", status_code=403)
         self.assertContains(response, "category is closed", status_code=403)
 
 
-        self.override_acl(category={
-            'can_close_threads': 1
-        })
+        self.override_acl(category={'can_close_threads': 1})
 
 
         response = self.client.delete(self.api_link)
         response = self.client.delete(self.api_link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
@@ -162,10 +159,7 @@ class ThreadPollDeleteTests(ThreadPollApiTestCase):
 
 
     def test_other_user_poll_delete(self):
     def test_other_user_poll_delete(self):
         """api deletes other user's poll and associated votes, even if its over"""
         """api deletes other user's poll and associated votes, even if its over"""
-        self.override_acl({
-            'can_delete_polls': 2,
-            'poll_edit_time': 5
-        })
+        self.override_acl({'can_delete_polls': 2, 'poll_edit_time': 5})
 
 
         self.poll.poster = None
         self.poll.poster = None
         self.poll.posted_on = timezone.now() - timedelta(days=15)
         self.poll.posted_on = timezone.now() - timedelta(days=15)

+ 282 - 264
misago/threads/tests/test_thread_polledit_api.py

@@ -23,71 +23,78 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
 
 
     def test_invalid_thread_id(self):
     def test_invalid_thread_id(self):
         """api validates that thread id is integer"""
         """api validates that thread id is integer"""
-        api_link = reverse('misago:api:thread-poll-detail', kwargs={
-            'thread_pk': 'kjha6dsa687sa',
-            'pk': self.poll.pk
-        })
+        api_link = reverse(
+            'misago:api:thread-poll-detail',
+            kwargs={
+                'thread_pk': 'kjha6dsa687sa',
+                'pk': self.poll.pk,
+            }
+        )
 
 
         response = self.put(api_link)
         response = self.put(api_link)
         self.assertEqual(response.status_code, 404)
         self.assertEqual(response.status_code, 404)
 
 
     def test_nonexistant_thread_id(self):
     def test_nonexistant_thread_id(self):
         """api validates that thread exists"""
         """api validates that thread exists"""
-        api_link = reverse('misago:api:thread-poll-detail', kwargs={
-            'thread_pk': self.thread.pk + 1,
-            'pk': self.poll.pk
-        })
+        api_link = reverse(
+            'misago:api:thread-poll-detail',
+            kwargs={
+                'thread_pk': self.thread.pk + 1,
+                'pk': self.poll.pk,
+            }
+        )
 
 
         response = self.put(api_link)
         response = self.put(api_link)
         self.assertEqual(response.status_code, 404)
         self.assertEqual(response.status_code, 404)
 
 
     def test_invalid_poll_id(self):
     def test_invalid_poll_id(self):
         """api validates that poll id is integer"""
         """api validates that poll id is integer"""
-        api_link = reverse('misago:api:thread-poll-detail', kwargs={
-            'thread_pk': self.thread.pk,
-            'pk': 'sad98as7d97sa98'
-        })
+        api_link = reverse(
+            'misago:api:thread-poll-detail',
+            kwargs={
+                'thread_pk': self.thread.pk,
+                'pk': 'sad98as7d97sa98',
+            }
+        )
 
 
         response = self.put(api_link)
         response = self.put(api_link)
         self.assertEqual(response.status_code, 404)
         self.assertEqual(response.status_code, 404)
 
 
     def test_nonexistant_poll_id(self):
     def test_nonexistant_poll_id(self):
         """api validates that poll exists"""
         """api validates that poll exists"""
-        api_link = reverse('misago:api:thread-poll-detail', kwargs={
-            'thread_pk': self.thread.pk,
-            'pk': self.poll.pk + 123
-        })
+        api_link = reverse(
+            'misago:api:thread-poll-detail',
+            kwargs={
+                'thread_pk': self.thread.pk,
+                'pk': self.poll.pk + 123,
+            }
+        )
 
 
         response = self.put(api_link)
         response = self.put(api_link)
         self.assertEqual(response.status_code, 404)
         self.assertEqual(response.status_code, 404)
 
 
     def test_no_permission(self):
     def test_no_permission(self):
         """api validates that user has permission to edit poll in thread"""
         """api validates that user has permission to edit poll in thread"""
-        self.override_acl({
-            'can_edit_polls': 0
-        })
+        self.override_acl({'can_edit_polls': 0})
 
 
         response = self.put(self.api_link)
         response = self.put(self.api_link)
         self.assertContains(response, "can't edit polls", status_code=403)
         self.assertContains(response, "can't edit polls", status_code=403)
 
 
     def test_no_permission_timeout(self):
     def test_no_permission_timeout(self):
         """api validates that user's window to edit poll in thread has closed"""
         """api validates that user's window to edit poll in thread has closed"""
-        self.override_acl({
-            'can_edit_polls': 1,
-            'poll_edit_time': 5
-        })
+        self.override_acl({'can_edit_polls': 1, 'poll_edit_time': 5})
 
 
         self.poll.posted_on = timezone.now() - timedelta(minutes=15)
         self.poll.posted_on = timezone.now() - timedelta(minutes=15)
         self.poll.save()
         self.poll.save()
 
 
         response = self.put(self.api_link)
         response = self.put(self.api_link)
-        self.assertContains(response, "can't edit polls that are older than 5 minutes", status_code=403)
+        self.assertContains(
+            response, "can't edit polls that are older than 5 minutes", status_code=403
+        )
 
 
     def test_no_permission_poll_closed(self):
     def test_no_permission_poll_closed(self):
         """api validates that user's window to edit poll in thread has closed"""
         """api validates that user's window to edit poll in thread has closed"""
-        self.override_acl({
-            'can_edit_polls': 1
-        })
+        self.override_acl({'can_edit_polls': 1})
 
 
         self.poll.posted_on = timezone.now() - timedelta(days=15)
         self.poll.posted_on = timezone.now() - timedelta(days=15)
         self.poll.length = 5
         self.poll.length = 5
@@ -98,9 +105,7 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
 
 
     def test_no_permission_other_user_poll(self):
     def test_no_permission_other_user_poll(self):
         """api validates that user has permission to edit other user poll in thread"""
         """api validates that user has permission to edit other user poll in thread"""
-        self.override_acl({
-            'can_edit_polls': 1
-        })
+        self.override_acl({'can_edit_polls': 1})
 
 
         self.poll.poster = None
         self.poll.poster = None
         self.poll.save()
         self.poll.save()
@@ -110,9 +115,7 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
 
 
     def test_no_permission_closed_thread(self):
     def test_no_permission_closed_thread(self):
         """api validates that user has permission to edit poll in closed thread"""
         """api validates that user has permission to edit poll in closed thread"""
-        self.override_acl(category={
-            'can_close_threads': 0
-        })
+        self.override_acl(category={'can_close_threads': 0})
 
 
         self.thread.is_closed = True
         self.thread.is_closed = True
         self.thread.save()
         self.thread.save()
@@ -120,18 +123,14 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
         response = self.put(self.api_link)
         response = self.put(self.api_link)
         self.assertContains(response, "thread is closed", status_code=403)
         self.assertContains(response, "thread is closed", status_code=403)
 
 
-        self.override_acl(category={
-            'can_close_threads': 1
-        })
+        self.override_acl(category={'can_close_threads': 1})
 
 
         response = self.put(self.api_link)
         response = self.put(self.api_link)
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
     def test_no_permission_closed_category(self):
     def test_no_permission_closed_category(self):
         """api validates that user has permission to edit poll in closed category"""
         """api validates that user has permission to edit poll in closed category"""
-        self.override_acl(category={
-            'can_close_threads': 0
-        })
+        self.override_acl(category={'can_close_threads': 0})
 
 
         self.category.is_closed = True
         self.category.is_closed = True
         self.category.save()
         self.category.save()
@@ -139,9 +138,7 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
         response = self.put(self.api_link)
         response = self.put(self.api_link)
         self.assertContains(response, "category is closed", status_code=403)
         self.assertContains(response, "category is closed", status_code=403)
 
 
-        self.override_acl(category={
-            'can_close_threads': 1
-        })
+        self.override_acl(category={'can_close_threads': 1})
 
 
         response = self.put(self.api_link)
         response = self.put(self.api_link)
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
@@ -156,156 +153,165 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
 
 
     def test_length_validation(self):
     def test_length_validation(self):
         """api validates poll's length"""
         """api validates poll's length"""
-        response = self.put(self.api_link, data={
-            'length': -1
-        })
+        response = self.put(
+            self.api_link, data={
+                'length': -1,
+            }
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         response_json = response.json()
         response_json = response.json()
-        self.assertEqual(response_json['length'], [
-            "Ensure this value is greater than or equal to 0."
-        ])
+        self.assertEqual(
+            response_json['length'], ["Ensure this value is greater than or equal to 0."]
+        )
 
 
-        response = self.put(self.api_link, data={
-            'length': 200
-        })
+        response = self.put(self.api_link, data={'length': 200})
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         response_json = response.json()
         response_json = response.json()
-        self.assertEqual(response_json['length'], [
-            "Ensure this value is less than or equal to 180."
-        ])
+        self.assertEqual(
+            response_json['length'], ["Ensure this value is less than or equal to 180."]
+        )
 
 
     def test_question_validation(self):
     def test_question_validation(self):
         """api validates question length"""
         """api validates question length"""
-        response = self.put(self.api_link, data={
-            'question': 'abcd' * 255
-        })
+        response = self.put(self.api_link, data={'question': 'abcd' * 255})
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         response_json = response.json()
         response_json = response.json()
-        self.assertEqual(response_json['question'], [
-            "Ensure this field has no more than 255 characters."
-        ])
+        self.assertEqual(
+            response_json['question'], ["Ensure this field has no more than 255 characters."]
+        )
 
 
     def test_validate_choice_length(self):
     def test_validate_choice_length(self):
         """api validates single choice length"""
         """api validates single choice length"""
-        response = self.put(self.api_link, data={
-            'choices': [
-                {
-                    'hash': 'qwertyuiopas',
-                    'label': ''
-                }
-            ]
-        })
+        response = self.put(
+            self.api_link, data={
+                'choices': [
+                    {
+                        'hash': 'qwertyuiopas',
+                        'label': '',
+                    },
+                ],
+            }
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         response_json = response.json()
         response_json = response.json()
-        self.assertEqual(response_json['choices'], [
-            "One or more poll choices are invalid."
-        ])
-
-        response = self.put(self.api_link, data={
-            'choices': [
-                {
-                    'hash': 'qwertyuiopas',
-                    'label': 'abcd' * 255
-                }
-            ]
-        })
+        self.assertEqual(response_json['choices'], ["One or more poll choices are invalid."])
+
+        response = self.put(
+            self.api_link,
+            data={
+                'choices': [
+                    {
+                        'hash': 'qwertyuiopas',
+                        'label': 'abcd' * 255,
+                    },
+                ],
+            }
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         response_json = response.json()
         response_json = response.json()
-        self.assertEqual(response_json['choices'], [
-            "One or more poll choices are invalid."
-        ])
+        self.assertEqual(response_json['choices'], ["One or more poll choices are invalid."])
 
 
     def test_validate_two_choices(self):
     def test_validate_two_choices(self):
         """api validates that there are at least two choices in poll"""
         """api validates that there are at least two choices in poll"""
-        response = self.put(self.api_link, data={
-            'choices': [
-                {
-                    'label': 'Choice'
-                }
-            ]
-        })
+        response = self.put(
+            self.api_link, data={
+                'choices': [
+                    {
+                        'label': 'Choice',
+                    },
+                ],
+            }
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         response_json = response.json()
         response_json = response.json()
-        self.assertEqual(response_json['choices'], [
-            "You need to add at least two choices to a poll."
-        ])
+        self.assertEqual(
+            response_json['choices'], ["You need to add at least two choices to a poll."]
+        )
 
 
     def test_validate_max_choices(self):
     def test_validate_max_choices(self):
         """api validates that there are no more choices in poll than allowed number"""
         """api validates that there are no more choices in poll than allowed number"""
-        response = self.put(self.api_link, data={
-            'choices': [
-                {
-                    'label': 'Choice'
-                }
-            ] * (MAX_POLL_OPTIONS + 1)
-        })
+        response = self.put(
+            self.api_link, data={
+                'choices': [
+                    {
+                        'label': 'Choice',
+                    },
+                ] * (MAX_POLL_OPTIONS + 1),
+            }
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         error_formats = (MAX_POLL_OPTIONS, MAX_POLL_OPTIONS + 1)
         error_formats = (MAX_POLL_OPTIONS, MAX_POLL_OPTIONS + 1)
 
 
         response_json = response.json()
         response_json = response.json()
-        self.assertEqual(response_json['choices'], [
-            "You can't add more than %s options to a single poll (added %s)." % error_formats
-        ])
+        self.assertEqual(
+            response_json['choices'],
+            ["You can't add more than %s options to a single poll (added %s)." % error_formats]
+        )
 
 
     def test_allowed_choices_validation(self):
     def test_allowed_choices_validation(self):
         """api validates allowed choices number"""
         """api validates allowed choices number"""
-        response = self.put(self.api_link, data={
-            'allowed_choices': 0
-        })
+        response = self.put(self.api_link, data={'allowed_choices': 0})
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         response_json = response.json()
         response_json = response.json()
-        self.assertEqual(response_json['allowed_choices'], [
-            "Ensure this value is greater than or equal to 1."
-        ])
-
-        response = self.put(self.api_link, data={
-            'length': 0,
-            'question': "Lorem ipsum",
-            'allowed_choices': 3,
-            'choices': [
-                {
-                    'label': 'Choice'
-                },
-                {
-                    'label': 'Choice'
-                }
-            ]
-        })
+        self.assertEqual(
+            response_json['allowed_choices'], ["Ensure this value is greater than or equal to 1."]
+        )
+
+        response = self.put(
+            self.api_link,
+            data={
+                'length': 0,
+                'question': "Lorem ipsum",
+                'allowed_choices': 3,
+                'choices': [
+                    {
+                        'label': 'Choice',
+                    },
+                    {
+                        'label': 'Choice',
+                    },
+                ],
+            }
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         response_json = response.json()
         response_json = response.json()
-        self.assertEqual(response_json['non_field_errors'], [
-            "Number of allowed choices can't be greater than number of all choices."
-        ])
+        self.assertEqual(
+            response_json['non_field_errors'],
+            ["Number of allowed choices can't be greater than number of all choices."]
+        )
 
 
     def test_poll_all_choices_replaced(self):
     def test_poll_all_choices_replaced(self):
         """api edits all poll choices out"""
         """api edits all poll choices out"""
-        response = self.put(self.api_link, data={
-            'length': 40,
-            'question': "Select two best colors",
-            'allowed_choices': 2,
-            'allow_revotes': True,
-            'is_public': True,
-            'choices': [
-                {
-                    'label': '\nRed  '
-                },
-                {
-                    'label': 'Green'
-                },
-                {
-                    'label': 'Blue'
-                }
-            ]
-        })
+        response = self.put(
+            self.api_link,
+            data={
+                'length': 40,
+                'question': "Select two best colors",
+                'allowed_choices': 2,
+                'allow_revotes': True,
+                'is_public': True,
+                'choices': [
+                    {
+                        'label': '\nRed  ',
+                    },
+                    {
+                        'label': 'Green',
+                    },
+                    {
+                        'label': 'Blue',
+                    },
+                ],
+            }
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         response_json = response.json()
         response_json = response.json()
@@ -332,35 +338,38 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
 
 
     def test_poll_current_choices_edited(self):
     def test_poll_current_choices_edited(self):
         """api edits current poll choices"""
         """api edits current poll choices"""
-        response = self.put(self.api_link, data={
-            'length': 40,
-            'question': "Select two best colors",
-            'allowed_choices': 2,
-            'allow_revotes': True,
-            'is_public': True,
-            'choices': [
-                {
-                    'hash': 'aaaaaaaaaaaa',
-                    'label': '\nFirst  ',
-                    'votes': 5555
-                },
-                {
-                    'hash': 'bbbbbbbbbbbb',
-                    'label': 'Second',
-                    'votes': 5555
-                },
-                {
-                    'hash': 'gggggggggggg',
-                    'label': 'Third',
-                    'votes': 5555
-                },
-                {
-                    'hash': 'dddddddddddd',
-                    'label': 'Fourth',
-                    'votes': 5555
-                }
-            ]
-        })
+        response = self.put(
+            self.api_link,
+            data={
+                'length': 40,
+                'question': "Select two best colors",
+                'allowed_choices': 2,
+                'allow_revotes': True,
+                'is_public': True,
+                'choices': [
+                    {
+                        'hash': 'aaaaaaaaaaaa',
+                        'label': '\nFirst  ',
+                        'votes': 5555,
+                    },
+                    {
+                        'hash': 'bbbbbbbbbbbb',
+                        'label': 'Second',
+                        'votes': 5555,
+                    },
+                    {
+                        'hash': 'gggggggggggg',
+                        'label': 'Third',
+                        'votes': 5555,
+                    },
+                    {
+                        'hash': 'dddddddddddd',
+                        'label': 'Fourth',
+                        'votes': 5555,
+                    },
+                ],
+            }
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         response_json = response.json()
         response_json = response.json()
@@ -376,63 +385,69 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
 
 
         # choices were updated
         # choices were updated
         self.assertEqual(len(response_json['choices']), 4)
         self.assertEqual(len(response_json['choices']), 4)
-        self.assertEqual(response_json['choices'], [
-            {
-                'hash': 'aaaaaaaaaaaa',
-                'label': 'First',
-                'votes': 1,
-                'selected': False
-            },
-            {
-                'hash': 'bbbbbbbbbbbb',
-                'label': 'Second',
-                'votes': 0,
-                'selected': False
-            },
-            {
-                'hash': 'gggggggggggg',
-                'label': 'Third',
-                'votes': 2,
-                'selected': True
-            },
-            {
-                'hash': 'dddddddddddd',
-                'label': 'Fourth',
-                'votes': 1,
-                'selected': True
-            }
-        ])
-
-        # no votes were removed
-        self.assertEqual(response_json['votes'], 4)
-        self.assertEqual(self.poll.pollvote_set.count(), 4)
-
-    def test_poll_some_choices_edited(self):
-        """api edits some poll choices"""
-        response = self.put(self.api_link, data={
-            'length': 40,
-            'question': "Select two best colors",
-            'allowed_choices': 2,
-            'allow_revotes': True,
-            'is_public': True,
-            'choices': [
+        self.assertEqual(
+            response_json['choices'],
+            [
                 {
                 {
                     'hash': 'aaaaaaaaaaaa',
                     'hash': 'aaaaaaaaaaaa',
-                    'label': '\nFirst ',
-                    'votes': 5555
+                    'label': 'First',
+                    'votes': 1,
+                    'selected': False,
                 },
                 },
                 {
                 {
                     'hash': 'bbbbbbbbbbbb',
                     'hash': 'bbbbbbbbbbbb',
                     'label': 'Second',
                     'label': 'Second',
-                    'votes': 5555
+                    'votes': 0,
+                    'selected': False,
                 },
                 },
                 {
                 {
-                    'hash': 'dsadsadsa788',
-                    'label': 'New Option',
-                    'votes': 5555
-                }
-            ]
-        })
+                    'hash': 'gggggggggggg',
+                    'label': 'Third',
+                    'votes': 2,
+                    'selected': True,
+                },
+                {
+                    'hash': 'dddddddddddd',
+                    'label': 'Fourth',
+                    'votes': 1,
+                    'selected': True,
+                },
+            ],
+        )
+
+        # no votes were removed
+        self.assertEqual(response_json['votes'], 4)
+        self.assertEqual(self.poll.pollvote_set.count(), 4)
+
+    def test_poll_some_choices_edited(self):
+        """api edits some poll choices"""
+        response = self.put(
+            self.api_link,
+            data={
+                'length': 40,
+                'question': "Select two best colors",
+                'allowed_choices': 2,
+                'allow_revotes': True,
+                'is_public': True,
+                'choices': [
+                    {
+                        'hash': 'aaaaaaaaaaaa',
+                        'label': '\nFirst ',
+                        'votes': 5555,
+                    },
+                    {
+                        'hash': 'bbbbbbbbbbbb',
+                        'label': 'Second',
+                        'votes': 5555,
+                    },
+                    {
+                        'hash': 'dsadsadsa788',
+                        'label': 'New Option',
+                        'votes': 5555,
+                    },
+                ],
+            }
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         response_json = response.json()
         response_json = response.json()
@@ -448,26 +463,29 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
 
 
         # choices were updated
         # choices were updated
         self.assertEqual(len(response_json['choices']), 3)
         self.assertEqual(len(response_json['choices']), 3)
-        self.assertEqual(response_json['choices'], [
-            {
-                'hash': 'aaaaaaaaaaaa',
-                'label': 'First',
-                'votes': 1,
-                'selected': False
-            },
-            {
-                'hash': 'bbbbbbbbbbbb',
-                'label': 'Second',
-                'votes': 0,
-                'selected': False
-            },
-            {
-                'hash': response_json['choices'][2]['hash'],
-                'label': 'New Option',
-                'votes': 0,
-                'selected': False
-            }
-        ])
+        self.assertEqual(
+            response_json['choices'],
+            [
+                {
+                    'hash': 'aaaaaaaaaaaa',
+                    'label': 'First',
+                    'votes': 1,
+                    'selected': False,
+                },
+                {
+                    'hash': 'bbbbbbbbbbbb',
+                    'label': 'Second',
+                    'votes': 0,
+                    'selected': False,
+                },
+                {
+                    'hash': response_json['choices'][2]['hash'],
+                    'label': 'New Option',
+                    'votes': 0,
+                    'selected': False,
+                },
+            ],
+        )
 
 
         # no votes were removed
         # no votes were removed
         self.assertEqual(response_json['votes'], 1)
         self.assertEqual(response_json['votes'], 1)
@@ -475,34 +493,34 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
 
 
     def test_moderate_user_poll(self):
     def test_moderate_user_poll(self):
         """api edits all poll choices out in other users poll, even if its over"""
         """api edits all poll choices out in other users poll, even if its over"""
-        self.override_acl({
-            'can_edit_polls': 2,
-            'poll_edit_time': 5
-        })
+        self.override_acl({'can_edit_polls': 2, 'poll_edit_time': 5})
 
 
         self.poll.poster = None
         self.poll.poster = None
         self.poll.posted_on = timezone.now() - timedelta(days=15)
         self.poll.posted_on = timezone.now() - timedelta(days=15)
         self.poll.length = 5
         self.poll.length = 5
         self.poll.save()
         self.poll.save()
 
 
-        response = self.put(self.api_link, data={
-            'length': 40,
-            'question': "Select two best colors",
-            'allowed_choices': 2,
-            'allow_revotes': True,
-            'is_public': True,
-            'choices': [
-                {
-                    'label': '\nRed  '
-                },
-                {
-                    'label': 'Green'
-                },
-                {
-                    'label': 'Blue'
-                }
-            ]
-        })
+        response = self.put(
+            self.api_link,
+            data={
+                'length': 40,
+                'question': "Select two best colors",
+                'allowed_choices': 2,
+                'allow_revotes': True,
+                'is_public': True,
+                'choices': [
+                    {
+                        'label': '\nRed  ',
+                    },
+                    {
+                        'label': 'Green',
+                    },
+                    {
+                        'label': 'Blue',
+                    },
+                ],
+            }
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         response_json = response.json()
         response_json = response.json()

+ 63 - 51
misago/threads/tests/test_thread_pollvotes_api.py

@@ -21,10 +21,13 @@ class ThreadGetVotesTests(ThreadPollApiTestCase):
         self.poll.is_public = True
         self.poll.is_public = True
         self.poll.save()
         self.poll.save()
 
 
-        self.api_link = reverse('misago:api:thread-poll-votes', kwargs={
-            'thread_pk': self.thread.pk,
-            'pk': self.poll.pk
-        })
+        self.api_link = reverse(
+            'misago:api:thread-poll-votes',
+            kwargs={
+                'thread_pk': self.thread.pk,
+                'pk': self.poll.pk,
+            }
+        )
 
 
     def test_anonymous(self):
     def test_anonymous(self):
         """api allows guests to get poll votes"""
         """api allows guests to get poll votes"""
@@ -35,49 +38,59 @@ class ThreadGetVotesTests(ThreadPollApiTestCase):
 
 
     def test_invalid_thread_id(self):
     def test_invalid_thread_id(self):
         """api validates that thread id is integer"""
         """api validates that thread id is integer"""
-        api_link = reverse('misago:api:thread-poll-votes', kwargs={
-            'thread_pk': 'kjha6dsa687sa',
-            'pk': self.poll.pk
-        })
+        api_link = reverse(
+            'misago:api:thread-poll-votes',
+            kwargs={
+                'thread_pk': 'kjha6dsa687sa',
+                'pk': self.poll.pk,
+            }
+        )
 
 
         response = self.client.get(api_link)
         response = self.client.get(api_link)
         self.assertEqual(response.status_code, 404)
         self.assertEqual(response.status_code, 404)
 
 
     def test_nonexistant_thread_id(self):
     def test_nonexistant_thread_id(self):
         """api validates that thread exists"""
         """api validates that thread exists"""
-        api_link = reverse('misago:api:thread-poll-votes', kwargs={
-            'thread_pk': self.thread.pk + 1,
-            'pk': self.poll.pk
-        })
+        api_link = reverse(
+            'misago:api:thread-poll-votes',
+            kwargs={
+                'thread_pk': self.thread.pk + 1,
+                'pk': self.poll.pk,
+            }
+        )
 
 
         response = self.client.get(api_link)
         response = self.client.get(api_link)
         self.assertEqual(response.status_code, 404)
         self.assertEqual(response.status_code, 404)
 
 
     def test_invalid_poll_id(self):
     def test_invalid_poll_id(self):
         """api validates that poll id is integer"""
         """api validates that poll id is integer"""
-        api_link = reverse('misago:api:thread-poll-votes', kwargs={
-            'thread_pk': self.thread.pk,
-            'pk': 'sad98as7d97sa98'
-        })
+        api_link = reverse(
+            'misago:api:thread-poll-votes',
+            kwargs={
+                'thread_pk': self.thread.pk,
+                'pk': 'sad98as7d97sa98',
+            }
+        )
 
 
         response = self.client.get(api_link)
         response = self.client.get(api_link)
         self.assertEqual(response.status_code, 404)
         self.assertEqual(response.status_code, 404)
 
 
     def test_nonexistant_poll_id(self):
     def test_nonexistant_poll_id(self):
         """api validates that poll exists"""
         """api validates that poll exists"""
-        api_link = reverse('misago:api:thread-poll-votes', kwargs={
-            'thread_pk': self.thread.pk,
-            'pk': self.poll.pk + 123
-        })
+        api_link = reverse(
+            'misago:api:thread-poll-votes',
+            kwargs={
+                'thread_pk': self.thread.pk,
+                'pk': self.poll.pk + 123,
+            }
+        )
 
 
         response = self.client.get(api_link)
         response = self.client.get(api_link)
         self.assertEqual(response.status_code, 404)
         self.assertEqual(response.status_code, 404)
 
 
     def test_no_permission(self):
     def test_no_permission(self):
         """api chcecks permission to see poll voters"""
         """api chcecks permission to see poll voters"""
-        self.override_acl({
-            'can_always_see_poll_voters': False
-        })
+        self.override_acl({'can_always_see_poll_voters': False})
 
 
         self.poll.is_public = False
         self.poll.is_public = False
         self.poll.save()
         self.poll.save()
@@ -107,18 +120,17 @@ class ThreadGetVotesTests(ThreadPollApiTestCase):
         self.assertEqual([c['votes'] for c in response_json], [1, 0, 2, 1])
         self.assertEqual([c['votes'] for c in response_json], [1, 0, 2, 1])
         self.assertEqual([len(c['voters']) for c in response_json], [1, 0, 2, 1])
         self.assertEqual([len(c['voters']) for c in response_json], [1, 0, 2, 1])
 
 
-        self.assertEqual([[v['username'] for v in c['voters']] for c in response_json][0][0], 'bob')
+        self.assertEqual([[v['username'] for v in c['voters']] for c in response_json][0][0],
+                         'bob')
 
 
         user = UserModel.objects.get(slug='bob')
         user = UserModel.objects.get(slug='bob')
 
 
-        self.assertEqual(
-            [[v['url'] for v in c['voters']] for c in response_json][0][0], user.get_absolute_url())
+        self.assertEqual([[v['url'] for v in c['voters']] for c in response_json][0][0],
+                         user.get_absolute_url())
 
 
     def test_get_votes_private_poll(self):
     def test_get_votes_private_poll(self):
         """api returns list of voters on private poll for user with permission"""
         """api returns list of voters on private poll for user with permission"""
-        self.override_acl({
-            'can_always_see_poll_voters': True
-        })
+        self.override_acl({'can_always_see_poll_voters': True})
 
 
         self.poll.is_public = False
         self.poll.is_public = False
         self.poll.save()
         self.poll.save()
@@ -133,12 +145,13 @@ class ThreadGetVotesTests(ThreadPollApiTestCase):
         self.assertEqual([c['votes'] for c in response_json], [1, 0, 2, 1])
         self.assertEqual([c['votes'] for c in response_json], [1, 0, 2, 1])
         self.assertEqual([len(c['voters']) for c in response_json], [1, 0, 2, 1])
         self.assertEqual([len(c['voters']) for c in response_json], [1, 0, 2, 1])
 
 
-        self.assertEqual([[v['username'] for v in c['voters']] for c in response_json][0][0], 'bob')
+        self.assertEqual([[v['username'] for v in c['voters']] for c in response_json][0][0],
+                         'bob')
 
 
         user = UserModel.objects.get(slug='bob')
         user = UserModel.objects.get(slug='bob')
 
 
-        self.assertEqual(
-            [[v['url'] for v in c['voters']] for c in response_json][0][0], user.get_absolute_url())
+        self.assertEqual([[v['url'] for v in c['voters']] for c in response_json][0][0],
+                         user.get_absolute_url())
 
 
 
 
 class ThreadPostVotesTests(ThreadPollApiTestCase):
 class ThreadPostVotesTests(ThreadPollApiTestCase):
@@ -147,10 +160,13 @@ class ThreadPostVotesTests(ThreadPollApiTestCase):
 
 
         self.mock_poll()
         self.mock_poll()
 
 
-        self.api_link = reverse('misago:api:thread-poll-votes', kwargs={
-            'thread_pk': self.thread.pk,
-            'pk': self.poll.pk
-        })
+        self.api_link = reverse(
+            'misago:api:thread-poll-votes',
+            kwargs={
+                'thread_pk': self.thread.pk,
+                'pk': self.poll.pk,
+            }
+        )
 
 
     def delete_user_votes(self):
     def delete_user_votes(self):
         self.poll.choices[2]['votes'] = 1
         self.poll.choices[2]['votes'] = 1
@@ -195,7 +211,9 @@ class ThreadPostVotesTests(ThreadPollApiTestCase):
         self.poll.save()
         self.poll.save()
 
 
         response = self.post(self.api_link, data=['aaaaaaaaaaaa', 'bbbbbbbbbbbb'])
         response = self.post(self.api_link, data=['aaaaaaaaaaaa', 'bbbbbbbbbbbb'])
-        self.assertContains(response, "This poll disallows voting for more than 1 choice.", status_code=400)
+        self.assertContains(
+            response, "This poll disallows voting for more than 1 choice.", status_code=400
+        )
 
 
     def test_revote(self):
     def test_revote(self):
         """api validates if user is trying to change vote in poll that disallows revoting"""
         """api validates if user is trying to change vote in poll that disallows revoting"""
@@ -209,9 +227,7 @@ class ThreadPostVotesTests(ThreadPollApiTestCase):
 
 
     def test_vote_in_closed_thread(self):
     def test_vote_in_closed_thread(self):
         """api validates is user has permission to vote poll in closed thread"""
         """api validates is user has permission to vote poll in closed thread"""
-        self.override_acl(category={
-            'can_close_threads': 0
-        })
+        self.override_acl(category={'can_close_threads': 0})
 
 
         self.thread.is_closed = True
         self.thread.is_closed = True
         self.thread.save()
         self.thread.save()
@@ -221,18 +237,14 @@ class ThreadPostVotesTests(ThreadPollApiTestCase):
         response = self.post(self.api_link)
         response = self.post(self.api_link)
         self.assertContains(response, "thread is closed", status_code=403)
         self.assertContains(response, "thread is closed", status_code=403)
 
 
-        self.override_acl(category={
-            'can_close_threads': 1
-        })
+        self.override_acl(category={'can_close_threads': 1})
 
 
         response = self.post(self.api_link)
         response = self.post(self.api_link)
         self.assertContains(response, "You have to make a choice.", status_code=400)
         self.assertContains(response, "You have to make a choice.", status_code=400)
 
 
     def test_vote_in_closed_category(self):
     def test_vote_in_closed_category(self):
         """api validates is user has permission to vote poll in closed category"""
         """api validates is user has permission to vote poll in closed category"""
-        self.override_acl(category={
-            'can_close_threads': 0
-        })
+        self.override_acl(category={'can_close_threads': 0})
 
 
         self.category.is_closed = True
         self.category.is_closed = True
         self.category.save()
         self.category.save()
@@ -242,9 +254,7 @@ class ThreadPostVotesTests(ThreadPollApiTestCase):
         response = self.post(self.api_link)
         response = self.post(self.api_link)
         self.assertContains(response, "category is closed", status_code=403)
         self.assertContains(response, "category is closed", status_code=403)
 
 
-        self.override_acl(category={
-            'can_close_threads': 1
-        })
+        self.override_acl(category={'can_close_threads': 1})
 
 
         response = self.post(self.api_link)
         response = self.post(self.api_link)
         self.assertContains(response, "You have to make a choice.", status_code=400)
         self.assertContains(response, "You have to make a choice.", status_code=400)
@@ -287,7 +297,8 @@ class ThreadPostVotesTests(ThreadPollApiTestCase):
         response_json = response.json()
         response_json = response.json()
         self.assertEqual(response_json['votes'], 4)
         self.assertEqual(response_json['votes'], 4)
         self.assertEqual([c['votes'] for c in response_json['choices']], [2, 1, 1, 0])
         self.assertEqual([c['votes'] for c in response_json['choices']], [2, 1, 1, 0])
-        self.assertEqual([c['selected'] for c in response_json['choices']], [True, True, False, False])
+        self.assertEqual([c['selected'] for c in response_json['choices']],
+                         [True, True, False, False])
 
 
         self.assertFalse(response_json['acl']['can_vote'])
         self.assertFalse(response_json['acl']['can_vote'])
 
 
@@ -313,6 +324,7 @@ class ThreadPostVotesTests(ThreadPollApiTestCase):
         response_json = response.json()
         response_json = response.json()
         self.assertEqual(response_json['votes'], 4)
         self.assertEqual(response_json['votes'], 4)
         self.assertEqual([c['votes'] for c in response_json['choices']], [2, 1, 1, 0])
         self.assertEqual([c['votes'] for c in response_json['choices']], [2, 1, 1, 0])
-        self.assertEqual([c['selected'] for c in response_json['choices']], [True, True, False, False])
+        self.assertEqual([c['selected'] for c in response_json['choices']],
+                         [True, True, False, False])
 
 
         self.assertTrue(response_json['acl']['can_vote'])
         self.assertTrue(response_json['acl']['can_vote'])

+ 39 - 37
misago/threads/tests/test_thread_postdelete_api.py

@@ -15,10 +15,13 @@ class PostDeleteApiTests(ThreadsApiTestCase):
 
 
         self.post = testutils.reply_thread(self.thread, poster=self.user)
         self.post = testutils.reply_thread(self.thread, poster=self.user)
 
 
-        self.api_link = reverse('misago:api:thread-post-detail', kwargs={
-            'thread_pk': self.thread.pk,
-            'pk': self.post.pk
-        })
+        self.api_link = reverse(
+            'misago:api:thread-post-detail',
+            kwargs={
+                'thread_pk': self.thread.pk,
+                'pk': self.post.pk,
+            }
+        )
 
 
     def test_delete_anonymous(self):
     def test_delete_anonymous(self):
         """api validates if deleting user is authenticated"""
         """api validates if deleting user is authenticated"""
@@ -29,10 +32,7 @@ class PostDeleteApiTests(ThreadsApiTestCase):
 
 
     def test_no_permission(self):
     def test_no_permission(self):
         """api validates permission to delete post"""
         """api validates permission to delete post"""
-        self.override_acl({
-            'can_hide_own_posts': 1,
-            'can_hide_posts': 1
-        })
+        self.override_acl({'can_hide_own_posts': 1, 'can_hide_posts': 1})
 
 
         response = self.client.delete(self.api_link)
         response = self.client.delete(self.api_link)
         self.assertContains(response, "You can't delete posts in this category.", status_code=403)
         self.assertContains(response, "You can't delete posts in this category.", status_code=403)
@@ -42,7 +42,7 @@ class PostDeleteApiTests(ThreadsApiTestCase):
         self.override_acl({
         self.override_acl({
             'post_edit_time': 0,
             'post_edit_time': 0,
             'can_hide_own_posts': 2,
             'can_hide_own_posts': 2,
-            'can_hide_posts': 0
+            'can_hide_posts': 0,
         })
         })
 
 
         self.post.poster = None
         self.post.poster = None
@@ -50,7 +50,8 @@ class PostDeleteApiTests(ThreadsApiTestCase):
 
 
         response = self.client.delete(self.api_link)
         response = self.client.delete(self.api_link)
         self.assertContains(
         self.assertContains(
-            response, "You can't delete other users posts in this category", status_code=403)
+            response, "You can't delete other users posts in this category", status_code=403
+        )
 
 
     def test_delete_protected_post_no_permission(self):
     def test_delete_protected_post_no_permission(self):
         """api validates if user can delete protected post"""
         """api validates if user can delete protected post"""
@@ -65,7 +66,8 @@ class PostDeleteApiTests(ThreadsApiTestCase):
 
 
         response = self.client.delete(self.api_link)
         response = self.client.delete(self.api_link)
         self.assertContains(
         self.assertContains(
-            response, "This post is protected. You can't delete it.", status_code=403)
+            response, "This post is protected. You can't delete it.", status_code=403
+        )
 
 
     def test_delete_protected_post_after_edit_time(self):
     def test_delete_protected_post_after_edit_time(self):
         """api validates if user can delete delete post after edit time"""
         """api validates if user can delete delete post after edit time"""
@@ -80,7 +82,8 @@ class PostDeleteApiTests(ThreadsApiTestCase):
 
 
         response = self.client.delete(self.api_link)
         response = self.client.delete(self.api_link)
         self.assertContains(
         self.assertContains(
-            response, "You can't delete posts that are older than 1 minute.", status_code=403)
+            response, "You can't delete posts that are older than 1 minute.", status_code=403
+        )
 
 
     def test_delete_post_closed_thread_no_permission(self):
     def test_delete_post_closed_thread_no_permission(self):
         """api valdiates if user can delete posts in closed threads"""
         """api valdiates if user can delete posts in closed threads"""
@@ -95,7 +98,8 @@ class PostDeleteApiTests(ThreadsApiTestCase):
 
 
         response = self.client.delete(self.api_link)
         response = self.client.delete(self.api_link)
         self.assertContains(
         self.assertContains(
-            response, "This thread is closed. You can't delete posts in it.", status_code=403)
+            response, "This thread is closed. You can't delete posts in it.", status_code=403
+        )
 
 
     def test_delete_post_closed_category_no_permission(self):
     def test_delete_post_closed_category_no_permission(self):
         """api valdiates if user can delete posts in closed categories"""
         """api valdiates if user can delete posts in closed categories"""
@@ -110,19 +114,20 @@ class PostDeleteApiTests(ThreadsApiTestCase):
 
 
         response = self.client.delete(self.api_link)
         response = self.client.delete(self.api_link)
         self.assertContains(
         self.assertContains(
-            response, "This category is closed. You can't delete posts in it.", status_code=403)
+            response, "This category is closed. You can't delete posts in it.", status_code=403
+        )
 
 
     def test_delete_first_post(self):
     def test_delete_first_post(self):
         """api disallows first post's deletion"""
         """api disallows first post's deletion"""
-        self.override_acl({
-            'can_hide_own_posts': 2,
-            'can_hide_posts': 2
-        })
+        self.override_acl({'can_hide_own_posts': 2, 'can_hide_posts': 2})
 
 
-        api_link = reverse('misago:api:thread-post-detail', kwargs={
-            'thread_pk': self.thread.pk,
-            'pk': self.thread.first_post_id
-        })
+        api_link = reverse(
+            'misago:api:thread-post-detail',
+            kwargs={
+                'thread_pk': self.thread.pk,
+                'pk': self.thread.first_post_id,
+            }
+        )
 
 
         response = self.client.delete(api_link)
         response = self.client.delete(api_link)
         self.assertContains(response, "You can't delete thread's first post.", status_code=403)
         self.assertContains(response, "You can't delete thread's first post.", status_code=403)
@@ -132,8 +137,7 @@ class PostDeleteApiTests(ThreadsApiTestCase):
         self.override_acl({
         self.override_acl({
             'can_hide_own_posts': 2,
             'can_hide_own_posts': 2,
             'can_hide_posts': 2,
             'can_hide_posts': 2,
-
-            'can_hide_events': 0
+            'can_hide_events': 0,
         })
         })
 
 
         self.post.is_event = True
         self.post.is_event = True
@@ -147,7 +151,7 @@ class PostDeleteApiTests(ThreadsApiTestCase):
         self.override_acl({
         self.override_acl({
             'post_edit_time': 0,
             'post_edit_time': 0,
             'can_hide_own_posts': 2,
             'can_hide_own_posts': 2,
-            'can_hide_posts': 0
+            'can_hide_posts': 0,
         })
         })
 
 
         response = self.client.delete(self.api_link)
         response = self.client.delete(self.api_link)
@@ -161,10 +165,7 @@ class PostDeleteApiTests(ThreadsApiTestCase):
 
 
     def test_delete_post(self):
     def test_delete_post(self):
         """api deletes thread post"""
         """api deletes thread post"""
-        self.override_acl({
-            'can_hide_own_posts': 0,
-            'can_hide_posts': 2
-        })
+        self.override_acl({'can_hide_own_posts': 0, 'can_hide_posts': 2})
 
 
         response = self.client.delete(self.api_link)
         response = self.client.delete(self.api_link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
@@ -182,10 +183,13 @@ class EventDeleteApiTests(ThreadsApiTestCase):
 
 
         self.event = testutils.reply_thread(self.thread, poster=self.user, is_event=True)
         self.event = testutils.reply_thread(self.thread, poster=self.user, is_event=True)
 
 
-        self.api_link = reverse('misago:api:thread-post-detail', kwargs={
-            'thread_pk': self.thread.pk,
-            'pk': self.event.pk
-        })
+        self.api_link = reverse(
+            'misago:api:thread-post-detail',
+            kwargs={
+                'thread_pk': self.thread.pk,
+                'pk': self.event.pk,
+            }
+        )
 
 
     def test_delete_anonymous(self):
     def test_delete_anonymous(self):
         """api validates if deleting user is authenticated"""
         """api validates if deleting user is authenticated"""
@@ -199,8 +203,7 @@ class EventDeleteApiTests(ThreadsApiTestCase):
         self.override_acl({
         self.override_acl({
             'can_hide_own_posts': 2,
             'can_hide_own_posts': 2,
             'can_hide_posts': 2,
             'can_hide_posts': 2,
-
-            'can_hide_events': 0
+            'can_hide_events': 0,
         })
         })
 
 
         response = self.client.delete(self.api_link)
         response = self.client.delete(self.api_link)
@@ -211,8 +214,7 @@ class EventDeleteApiTests(ThreadsApiTestCase):
         self.override_acl({
         self.override_acl({
             'can_hide_own_posts': 0,
             'can_hide_own_posts': 0,
             'can_hide_posts': 0,
             'can_hide_posts': 0,
-
-            'can_hide_events': 2
+            'can_hide_events': 2,
         })
         })
 
 
         response = self.client.delete(self.api_link)
         response = self.client.delete(self.api_link)

+ 13 - 20
misago/threads/tests/test_thread_postedits_api.py

@@ -1,15 +1,9 @@
 # -*- coding: utf-8 -*-
 # -*- coding: utf-8 -*-
 from __future__ import unicode_literals
 from __future__ import unicode_literals
 
 
-import json
-from datetime import timedelta
-
 from django.urls import reverse
 from django.urls import reverse
 
 
-from misago.acl.testutils import override_acl
-from misago.categories.models import Category
 from misago.threads import testutils
 from misago.threads import testutils
-from misago.threads.models import Post
 
 
 from .test_threads_api import ThreadsApiTestCase
 from .test_threads_api import ThreadsApiTestCase
 
 
@@ -20,10 +14,13 @@ class ThreadPostEditsApiTestCase(ThreadsApiTestCase):
 
 
         self.post = testutils.reply_thread(self.thread, poster=self.user)
         self.post = testutils.reply_thread(self.thread, poster=self.user)
 
 
-        self.api_link = reverse('misago:api:thread-post-edits', kwargs={
-            'thread_pk': self.thread.pk,
-            'pk': self.post.pk
-        })
+        self.api_link = reverse(
+            'misago:api:thread-post-edits',
+            kwargs={
+                'thread_pk': self.thread.pk,
+                'pk': self.post.pk,
+            }
+        )
 
 
         self.override_acl()
         self.override_acl()
 
 
@@ -37,7 +34,7 @@ class ThreadPostEditsApiTestCase(ThreadsApiTestCase):
                 editor_slug=self.user.slug,
                 editor_slug=self.user.slug,
                 editor_ip='127.0.0.1',
                 editor_ip='127.0.0.1',
                 edited_from="Original body",
                 edited_from="Original body",
-                edited_to="First Edit"
+                edited_to="First Edit",
             ),
             ),
             self.post.edits_record.create(
             self.post.edits_record.create(
                 category=self.category,
                 category=self.category,
@@ -46,7 +43,7 @@ class ThreadPostEditsApiTestCase(ThreadsApiTestCase):
                 editor_slug='deleted',
                 editor_slug='deleted',
                 editor_ip='127.0.0.1',
                 editor_ip='127.0.0.1',
                 edited_from="First Edit",
                 edited_from="First Edit",
-                edited_to="Second Edit"
+                edited_to="Second Edit",
             ),
             ),
             self.post.edits_record.create(
             self.post.edits_record.create(
                 category=self.category,
                 category=self.category,
@@ -56,8 +53,8 @@ class ThreadPostEditsApiTestCase(ThreadsApiTestCase):
                 editor_slug=self.user.slug,
                 editor_slug=self.user.slug,
                 editor_ip='127.0.0.1',
                 editor_ip='127.0.0.1',
                 edited_from="Second Edit",
                 edited_from="Second Edit",
-                edited_to="Last Edit"
-            )
+                edited_to="Last Edit",
+            ),
         ]
         ]
 
 
         self.post.original = 'Last Edit'
         self.post.original = 'Last Edit'
@@ -138,9 +135,7 @@ class ThreadPostPostEditTests(ThreadPostEditsApiTestCase):
         super(ThreadPostPostEditTests, self).setUp()
         super(ThreadPostPostEditTests, self).setUp()
         self.edits = self.mock_edit_record()
         self.edits = self.mock_edit_record()
 
 
-        self.override_acl({
-            'can_edit_posts': 2
-        })
+        self.override_acl({'can_edit_posts': 2})
 
 
     def test_empty_edit_id(self):
     def test_empty_edit_id(self):
         """api handles empty edit in querystring"""
         """api handles empty edit in querystring"""
@@ -166,9 +161,7 @@ class ThreadPostPostEditTests(ThreadPostEditsApiTestCase):
 
 
     def test_no_permission(self):
     def test_no_permission(self):
         """api validates permission to revert post"""
         """api validates permission to revert post"""
-        self.override_acl({
-            'can_edit_posts': 0
-        })
+        self.override_acl({'can_edit_posts': 0})
 
 
         response = self.client.post('{}?edit=1321'.format(self.api_link))
         response = self.client.post('{}?edit=1321'.format(self.api_link))
         self.assertEqual(response.status_code, 403)
         self.assertEqual(response.status_code, 403)

+ 15 - 14
misago/threads/tests/test_thread_postlikes_api.py

@@ -12,25 +12,24 @@ class ThreadPostLikesApiTestCase(ThreadsApiTestCase):
 
 
         self.post = testutils.reply_thread(self.thread, poster=self.user)
         self.post = testutils.reply_thread(self.thread, poster=self.user)
 
 
-        self.api_link = reverse('misago:api:thread-post-likes', kwargs={
-            'thread_pk': self.thread.pk,
-            'pk': self.post.pk
-        })
+        self.api_link = reverse(
+            'misago:api:thread-post-likes',
+            kwargs={
+                'thread_pk': self.thread.pk,
+                'pk': self.post.pk,
+            }
+        )
 
 
     def test_no_permission(self):
     def test_no_permission(self):
         """api errors if user has no permission to see likes"""
         """api errors if user has no permission to see likes"""
-        self.override_acl({
-            'can_see_posts_likes': 0
-        })
+        self.override_acl({'can_see_posts_likes': 0})
 
 
         response = self.client.get(self.api_link)
         response = self.client.get(self.api_link)
         self.assertContains(response, "You can't see who liked this post.", status_code=403)
         self.assertContains(response, "You can't see who liked this post.", status_code=403)
 
 
     def test_no_permission_to_list(self):
     def test_no_permission_to_list(self):
         """api errors if user has no permission to see likes, but can see likes count"""
         """api errors if user has no permission to see likes, but can see likes count"""
-        self.override_acl({
-            'can_see_posts_likes': 1
-        })
+        self.override_acl({'can_see_posts_likes': 1})
 
 
         response = self.client.get(self.api_link)
         response = self.client.get(self.api_link)
         self.assertContains(response, "You can't see who liked this post.", status_code=403)
         self.assertContains(response, "You can't see who liked this post.", status_code=403)
@@ -51,7 +50,9 @@ class ThreadPostLikesApiTestCase(ThreadsApiTestCase):
 
 
         response = self.client.get(self.api_link)
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
-        self.assertEqual(response.json(), [
-            PostLikeSerializer(other_like.__dict__).data,
-            PostLikeSerializer(like.__dict__).data,
-        ])
+        self.assertEqual(
+            response.json(), [
+                PostLikeSerializer(other_like.__dict__).data,
+                PostLikeSerializer(like.__dict__).data,
+            ]
+        )

+ 234 - 126
misago/threads/tests/test_thread_postmerge_api.py

@@ -2,12 +2,8 @@
 from __future__ import unicode_literals
 from __future__ import unicode_literals
 
 
 import json
 import json
-from datetime import timedelta
 
 
 from django.urls import reverse
 from django.urls import reverse
-from django.utils import timezone
-from django.utils.encoding import smart_str
-from django.utils.six.moves import range
 
 
 from misago.acl.testutils import override_acl
 from misago.acl.testutils import override_acl
 from misago.categories.models import Category
 from misago.categories.models import Category
@@ -25,9 +21,11 @@ class ThreadPostMergeApiTestCase(AuthenticatedUserTestCase):
         self.thread = testutils.post_thread(category=self.category)
         self.thread = testutils.post_thread(category=self.category)
         self.post = testutils.reply_thread(self.thread, poster=self.user)
         self.post = testutils.reply_thread(self.thread, poster=self.user)
 
 
-        self.api_link = reverse('misago:api:thread-post-merge', kwargs={
-            'thread_pk': self.thread.pk
-        })
+        self.api_link = reverse(
+            'misago:api:thread-post-merge', kwargs={
+                'thread_pk': self.thread.pk,
+            }
+        )
 
 
         self.override_acl()
         self.override_acl()
 
 
@@ -43,7 +41,7 @@ class ThreadPostMergeApiTestCase(AuthenticatedUserTestCase):
             'can_reply_threads': 0,
             'can_reply_threads': 0,
             'can_edit_posts': 1,
             'can_edit_posts': 1,
             'can_approve_content': 0,
             'can_approve_content': 0,
-            'can_merge_posts': 1
+            'can_merge_posts': 1,
         })
         })
 
 
         if extra_acl:
         if extra_acl:
@@ -55,16 +53,22 @@ class ThreadPostMergeApiTestCase(AuthenticatedUserTestCase):
         """you need to authenticate to merge posts"""
         """you need to authenticate to merge posts"""
         self.logout_user()
         self.logout_user()
 
 
-        response = self.client.post(self.api_link, json.dumps({}), content_type="application/json")
+        response = self.client.post(
+            self.api_link,
+            json.dumps({}),
+            content_type="application/json",
+        )
         self.assertEqual(response.status_code, 403)
         self.assertEqual(response.status_code, 403)
 
 
     def test_no_permission(self):
     def test_no_permission(self):
         """api validates permission to merge"""
         """api validates permission to merge"""
-        self.override_acl({
-            'can_merge_posts': 0
-        })
+        self.override_acl({'can_merge_posts': 0})
 
 
-        response = self.client.post(self.api_link, json.dumps({}), content_type="application/json")
+        response = self.client.post(
+            self.api_link,
+            json.dumps({}),
+            content_type="application/json",
+        )
         self.assertContains(response, "You can't merge posts in this thread.", status_code=403)
         self.assertContains(response, "You can't merge posts in this thread.", status_code=403)
 
 
     def test_closed_thread(self):
     def test_closed_thread(self):
@@ -72,165 +76,263 @@ class ThreadPostMergeApiTestCase(AuthenticatedUserTestCase):
         self.thread.is_closed = True
         self.thread.is_closed = True
         self.thread.save()
         self.thread.save()
 
 
-        response = self.client.post(self.api_link, json.dumps({}), content_type="application/json")
+        response = self.client.post(
+            self.api_link,
+            json.dumps({}),
+            content_type="application/json",
+        )
         self.assertContains(response, "You can't merge posts in this thread.", status_code=403)
         self.assertContains(response, "You can't merge posts in this thread.", status_code=403)
 
 
         # allow closing threads
         # allow closing threads
-        self.override_acl({
-            'can_close_threads': 1
-        })
+        self.override_acl({'can_close_threads': 1})
 
 
-        response = self.client.post(self.api_link, json.dumps({}), content_type="application/json")
-        self.assertContains(response, "You have to select at least two posts to merge.", status_code=400)
+        response = self.client.post(
+            self.api_link,
+            json.dumps({}),
+            content_type="application/json",
+        )
+        self.assertContains(
+            response, "You have to select at least two posts to merge.", status_code=400
+        )
 
 
     def test_closed_category(self):
     def test_closed_category(self):
         """api validates permission to merge in closed category"""
         """api validates permission to merge in closed category"""
         self.category.is_closed = True
         self.category.is_closed = True
         self.category.save()
         self.category.save()
 
 
-        response = self.client.post(self.api_link, json.dumps({}), content_type="application/json")
+        response = self.client.post(
+            self.api_link,
+            json.dumps({}),
+            content_type="application/json",
+        )
         self.assertContains(response, "You can't merge posts in this thread.", status_code=403)
         self.assertContains(response, "You can't merge posts in this thread.", status_code=403)
 
 
         # allow closing threads
         # allow closing threads
-        self.override_acl({
-            'can_close_threads': 1
-        })
+        self.override_acl({'can_close_threads': 1})
 
 
-        response = self.client.post(self.api_link, json.dumps({}), content_type="application/json")
-        self.assertContains(response, "You have to select at least two posts to merge.", status_code=400)
+        response = self.client.post(
+            self.api_link,
+            json.dumps({}),
+            content_type="application/json",
+        )
+        self.assertContains(
+            response, "You have to select at least two posts to merge.", status_code=400
+        )
 
 
     def test_empty_data(self):
     def test_empty_data(self):
         """api handles empty data"""
         """api handles empty data"""
-        response = self.client.post(self.api_link, json.dumps({}), content_type="application/json")
-        self.assertContains(response, "You have to select at least two posts to merge.", status_code=400)
+        response = self.client.post(
+            self.api_link,
+            json.dumps({}),
+            content_type="application/json",
+        )
+        self.assertContains(
+            response, "You have to select at least two posts to merge.", status_code=400
+        )
 
 
     def test_no_posts_ids(self):
     def test_no_posts_ids(self):
         """api rejects no posts ids"""
         """api rejects no posts ids"""
-        response = self.client.post(self.api_link, json.dumps({
-            'posts': []
-        }), content_type="application/json")
-        self.assertContains(response, "You have to select at least two posts to merge.", status_code=400)
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'posts': []
+            }),
+            content_type="application/json",
+        )
+        self.assertContains(
+            response, "You have to select at least two posts to merge.", status_code=400
+        )
 
 
     def test_invalid_posts_data(self):
     def test_invalid_posts_data(self):
         """api handles invalid data"""
         """api handles invalid data"""
-        response = self.client.post(self.api_link, json.dumps({
-            'posts': 'string'
-        }), content_type="application/json")
-        self.assertContains(response, "One or more post ids received were invalid.", status_code=400)
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'posts': 'string'
+            }),
+            content_type="application/json",
+        )
+        self.assertContains(
+            response, "One or more post ids received were invalid.", status_code=400
+        )
 
 
     def test_invalid_posts_ids(self):
     def test_invalid_posts_ids(self):
         """api handles invalid post id"""
         """api handles invalid post id"""
-        response = self.client.post(self.api_link, json.dumps({
-            'posts': [1, 2, 'string']
-        }), content_type="application/json")
-        self.assertContains(response, "One or more post ids received were invalid.", status_code=400)
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'posts': [1, 2, 'string']
+            }),
+            content_type="application/json",
+        )
+        self.assertContains(
+            response, "One or more post ids received were invalid.", status_code=400
+        )
 
 
     def test_one_post_id(self):
     def test_one_post_id(self):
         """api rejects one post id"""
         """api rejects one post id"""
-        response = self.client.post(self.api_link, json.dumps({
-            'posts': [1]
-        }), content_type="application/json")
-        self.assertContains(response, "You have to select at least two posts to merge.", status_code=400)
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'posts': [1]
+            }),
+            content_type="application/json",
+        )
+        self.assertContains(
+            response, "You have to select at least two posts to merge.", status_code=400
+        )
 
 
     def test_merge_limit(self):
     def test_merge_limit(self):
         """api rejects more posts than merge limit"""
         """api rejects more posts than merge limit"""
-        response = self.client.post(self.api_link, json.dumps({
-            'posts': list(range(MERGE_LIMIT + 1))
-        }), content_type="application/json")
-        self.assertContains(response, "No more than {} posts can be merged".format(MERGE_LIMIT), status_code=400)
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'posts': list(range(MERGE_LIMIT + 1))
+            }),
+            content_type="application/json",
+        )
+        self.assertContains(
+            response, "No more than {} posts can be merged".format(MERGE_LIMIT), status_code=400
+        )
 
 
     def test_merge_event(self):
     def test_merge_event(self):
         """api recjects events"""
         """api recjects events"""
         event = testutils.reply_thread(self.thread, is_event=True, poster=self.user)
         event = testutils.reply_thread(self.thread, is_event=True, poster=self.user)
 
 
-        response = self.client.post(self.api_link, json.dumps({
-            'posts': [self.post.pk, event.pk]
-        }), content_type="application/json")
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'posts': [self.post.pk, event.pk]
+            }),
+            content_type="application/json",
+        )
         self.assertContains(response, "Events can't be merged.", status_code=400)
         self.assertContains(response, "Events can't be merged.", status_code=400)
 
 
     def test_merge_notfound_pk(self):
     def test_merge_notfound_pk(self):
         """api recjects nonexistant pk's"""
         """api recjects nonexistant pk's"""
-        response = self.client.post(self.api_link, json.dumps({
-            'posts': [self.post.pk, self.post.pk * 1000]
-        }), content_type="application/json")
-        self.assertContains(response, "One or more posts to merge could not be found.", status_code=400)
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'posts': [self.post.pk, self.post.pk * 1000]
+            }),
+            content_type="application/json",
+        )
+        self.assertContains(
+            response, "One or more posts to merge could not be found.", status_code=400
+        )
 
 
     def test_merge_cross_threads(self):
     def test_merge_cross_threads(self):
         """api recjects attempt to merge with post made in other thread"""
         """api recjects attempt to merge with post made in other thread"""
         other_thread = testutils.post_thread(category=self.category)
         other_thread = testutils.post_thread(category=self.category)
         other_post = testutils.reply_thread(other_thread, poster=self.user)
         other_post = testutils.reply_thread(other_thread, poster=self.user)
 
 
-        response = self.client.post(self.api_link, json.dumps({
-            'posts': [self.post.pk, other_post.pk]
-        }), content_type="application/json")
-        self.assertContains(response, "One or more posts to merge could not be found.", status_code=400)
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'posts': [self.post.pk, other_post.pk]
+            }),
+            content_type="application/json",
+        )
+        self.assertContains(
+            response, "One or more posts to merge could not be found.", status_code=400
+        )
 
 
     def test_merge_authenticated_with_guest_post(self):
     def test_merge_authenticated_with_guest_post(self):
         """api recjects attempt to merge with post made by deleted user"""
         """api recjects attempt to merge with post made by deleted user"""
         other_post = testutils.reply_thread(self.thread)
         other_post = testutils.reply_thread(self.thread)
 
 
-        response = self.client.post(self.api_link, json.dumps({
-            'posts': [self.post.pk, other_post.pk]
-        }), content_type="application/json")
-        self.assertContains(response, "Posts made by different users can't be merged.", status_code=400)
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'posts': [self.post.pk, other_post.pk]
+            }),
+            content_type="application/json",
+        )
+        self.assertContains(
+            response, "Posts made by different users can't be merged.", status_code=400
+        )
 
 
     def test_merge_guest_with_authenticated_post(self):
     def test_merge_guest_with_authenticated_post(self):
         """api recjects attempt to merge with post made by deleted user"""
         """api recjects attempt to merge with post made by deleted user"""
         other_post = testutils.reply_thread(self.thread)
         other_post = testutils.reply_thread(self.thread)
 
 
-        response = self.client.post(self.api_link, json.dumps({
-            'posts': [other_post.pk, self.post.pk]
-        }), content_type="application/json")
-        self.assertContains(response, "Posts made by different users can't be merged.", status_code=400)
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'posts': [other_post.pk, self.post.pk]
+            }),
+            content_type="application/json",
+        )
+        self.assertContains(
+            response, "Posts made by different users can't be merged.", status_code=400
+        )
 
 
     def test_merge_guest_posts_different_usernames(self):
     def test_merge_guest_posts_different_usernames(self):
         """api recjects attempt to merge posts made by different guests"""
         """api recjects attempt to merge posts made by different guests"""
-        response = self.client.post(self.api_link, json.dumps({
-            'posts': [
-                testutils.reply_thread(self.thread, poster="Bob").pk,
-                testutils.reply_thread(self.thread, poster="Miku").pk
-            ]
-        }), content_type="application/json")
-        self.assertContains(response, "Posts made by different users can't be merged.", status_code=400)
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'posts': [
+                    testutils.reply_thread(self.thread, poster="Bob").pk,
+                    testutils.reply_thread(self.thread, poster="Miku").pk,
+                ]
+            }),
+            content_type="application/json",
+        )
+        self.assertContains(
+            response, "Posts made by different users can't be merged.", status_code=400
+        )
 
 
     def test_merge_different_visibility(self):
     def test_merge_different_visibility(self):
         """api recjects attempt to merge posts with different visibility"""
         """api recjects attempt to merge posts with different visibility"""
-        self.override_acl({
-            'can_hide_posts': 1
-        })
-
-        response = self.client.post(self.api_link, json.dumps({
-            'posts': [
-                testutils.reply_thread(self.thread, poster="Bob", is_hidden=True).pk,
-                testutils.reply_thread(self.thread, poster="Bob", is_hidden=False).pk
-            ]
-        }), content_type="application/json")
-        self.assertContains(response, "Posts with different visibility can't be merged.", status_code=400)
+        self.override_acl({'can_hide_posts': 1})
+
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'posts': [
+                    testutils.reply_thread(self.thread, poster="Bob", is_hidden=True).pk,
+                    testutils.reply_thread(self.thread, poster="Bob", is_hidden=False).pk,
+                ]
+            }),
+            content_type="application/json",
+        )
+        self.assertContains(
+            response, "Posts with different visibility can't be merged.", status_code=400
+        )
 
 
     def test_merge_different_approval(self):
     def test_merge_different_approval(self):
         """api recjects attempt to merge posts with different approval"""
         """api recjects attempt to merge posts with different approval"""
-        self.override_acl({
-            'can_approve_content': 1
-        })
-
-        response = self.client.post(self.api_link, json.dumps({
-            'posts': [
-                testutils.reply_thread(self.thread, poster="Bob", is_unapproved=True).pk,
-                testutils.reply_thread(self.thread, poster="Bob", is_unapproved=False).pk
-            ]
-        }), content_type="application/json")
-        self.assertContains(response, "Posts with different visibility can't be merged.", status_code=400)
+        self.override_acl({'can_approve_content': 1})
+
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'posts': [
+                    testutils.reply_thread(self.thread, poster="Bob", is_unapproved=True).pk,
+                    testutils.reply_thread(self.thread, poster="Bob", is_unapproved=False).pk,
+                ]
+            }),
+            content_type="application/json",
+        )
+        self.assertContains(
+            response, "Posts with different visibility can't be merged.", status_code=400
+        )
 
 
     def test_merge_posts(self):
     def test_merge_posts(self):
         """api merges two posts"""
         """api merges two posts"""
-        post_a = testutils.reply_thread(self.thread, poster="Bob", message="Battęry")
-        post_b = testutils.reply_thread(self.thread, poster="Bob", message="Hórse")
+        post_a = testutils.reply_thread(self.thread, poster=self.user, message="Battęry")
+        post_b = testutils.reply_thread(self.thread, poster=self.user, message="Hórse")
 
 
         thread_replies = self.thread.replies
         thread_replies = self.thread.replies
 
 
-        response = self.client.post(self.api_link, json.dumps({
-            'posts': [post_a.pk, post_b.pk]
-        }), content_type="application/json")
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'posts': [post_a.pk, post_b.pk]
+            }),
+            content_type="application/json",
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         self.refresh_thread()
         self.refresh_thread()
@@ -244,30 +346,34 @@ class ThreadPostMergeApiTestCase(AuthenticatedUserTestCase):
 
 
     def test_merge_hidden_posts(self):
     def test_merge_hidden_posts(self):
         """api merges two hidden posts"""
         """api merges two hidden posts"""
-        self.override_acl({
-            'can_hide_posts': 1
-        })
-
-        response = self.client.post(self.api_link, json.dumps({
-            'posts': [
-                testutils.reply_thread(self.thread, poster="Bob", is_hidden=True).pk,
-                testutils.reply_thread(self.thread, poster="Bob", is_hidden=True).pk
-            ]
-        }), content_type="application/json")
+        self.override_acl({'can_hide_posts': 1})
+
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'posts': [
+                    testutils.reply_thread(self.thread, poster=self.user, is_hidden=True).pk,
+                    testutils.reply_thread(self.thread, poster=self.user, is_hidden=True).pk,
+                ]
+            }),
+            content_type="application/json",
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
     def test_merge_unapproved_posts(self):
     def test_merge_unapproved_posts(self):
         """api merges two unapproved posts"""
         """api merges two unapproved posts"""
-        self.override_acl({
-            'can_approve_content': 1
-        })
-
-        response = self.client.post(self.api_link, json.dumps({
-            'posts': [
-                testutils.reply_thread(self.thread, poster="Bob", is_unapproved=True).pk,
-                testutils.reply_thread(self.thread, poster="Bob", is_unapproved=True).pk
-            ]
-        }), content_type="application/json")
+        self.override_acl({'can_approve_content': 1})
+
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'posts': [
+                    testutils.reply_thread(self.thread, poster=self.user, is_unapproved=True).pk,
+                    testutils.reply_thread(self.thread, poster=self.user, is_unapproved=True).pk,
+                ]
+            }),
+            content_type="application/json",
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
     def test_merge_with_hidden_thread(self):
     def test_merge_with_hidden_thread(self):
@@ -278,11 +384,13 @@ class ThreadPostMergeApiTestCase(AuthenticatedUserTestCase):
 
 
         post_visible = testutils.reply_thread(self.thread, poster=self.user, is_hidden=False)
         post_visible = testutils.reply_thread(self.thread, poster=self.user, is_hidden=False)
 
 
-        self.override_acl({
-            'can_hide_threads': 1
-        })
+        self.override_acl({'can_hide_threads': 1})
 
 
-        response = self.client.post(self.api_link, json.dumps({
-            'posts': [self.thread.first_post.pk, post_visible.pk]
-        }), content_type="application/json")
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'posts': [self.thread.first_post.pk, post_visible.pk]
+            }),
+            content_type="application/json",
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)

+ 162 - 103
misago/threads/tests/test_thread_postmove_api.py

@@ -4,7 +4,6 @@ from __future__ import unicode_literals
 import json
 import json
 
 
 from django.urls import reverse
 from django.urls import reverse
-from django.utils.six.moves import range
 
 
 from misago.acl.testutils import override_acl
 from misago.acl.testutils import override_acl
 from misago.categories.models import Category
 from misago.categories.models import Category
@@ -21,14 +20,20 @@ class ThreadPostMoveApiTestCase(AuthenticatedUserTestCase):
         self.category = Category.objects.get(slug='first-category')
         self.category = Category.objects.get(slug='first-category')
         self.thread = testutils.post_thread(category=self.category)
         self.thread = testutils.post_thread(category=self.category)
 
 
-        self.api_link = reverse('misago:api:thread-post-move', kwargs={
-            'thread_pk': self.thread.pk
-        })
+        self.api_link = reverse(
+            'misago:api:thread-post-move', kwargs={
+                'thread_pk': self.thread.pk,
+            }
+        )
 
 
         Category(
         Category(
             name='Category B',
             name='Category B',
             slug='category-b',
             slug='category-b',
-        ).insert_at(self.category, position='last-child', save=True)
+        ).insert_at(
+            self.category,
+            position='last-child',
+            save=True,
+        )
         self.category_b = Category.objects.get(slug='category-b')
         self.category_b = Category.objects.get(slug='category-b')
 
 
         self.override_acl()
         self.override_acl()
@@ -46,7 +51,7 @@ class ThreadPostMoveApiTestCase(AuthenticatedUserTestCase):
             'can_reply_threads': 1,
             'can_reply_threads': 1,
             'can_edit_posts': 1,
             'can_edit_posts': 1,
             'can_approve_content': 0,
             'can_approve_content': 0,
-            'can_move_posts': 1
+            'can_move_posts': 1,
         })
         })
 
 
         if extra_acl:
         if extra_acl:
@@ -63,7 +68,7 @@ class ThreadPostMoveApiTestCase(AuthenticatedUserTestCase):
             'can_reply_threads': 0,
             'can_reply_threads': 0,
             'can_edit_posts': 1,
             'can_edit_posts': 1,
             'can_approve_content': 0,
             'can_approve_content': 0,
-            'can_move_posts': 1
+            'can_move_posts': 1,
         })
         })
 
 
         if acl:
         if acl:
@@ -76,10 +81,12 @@ class ThreadPostMoveApiTestCase(AuthenticatedUserTestCase):
         if other_category_acl['can_see']:
         if other_category_acl['can_see']:
             visible_categories.append(self.category_b.pk)
             visible_categories.append(self.category_b.pk)
 
 
-        override_acl(self.user, {
-            'visible_categories': visible_categories,
-            'categories': categories_acl,
-        })
+        override_acl(
+            self.user, {
+                'visible_categories': visible_categories,
+                'categories': categories_acl,
+            }
+        )
 
 
     def test_anonymous_user(self):
     def test_anonymous_user(self):
         """you need to authenticate to move posts"""
         """you need to authenticate to move posts"""
@@ -90,9 +97,7 @@ class ThreadPostMoveApiTestCase(AuthenticatedUserTestCase):
 
 
     def test_no_permission(self):
     def test_no_permission(self):
         """api validates permission to move"""
         """api validates permission to move"""
-        self.override_acl({
-            'can_move_posts': 0
-        })
+        self.override_acl({'can_move_posts': 0})
 
 
         response = self.client.post(self.api_link, json.dumps({}), content_type="application/json")
         response = self.client.post(self.api_link, json.dumps({}), content_type="application/json")
         self.assertContains(response, "You can't move posts in this thread.", status_code=403)
         self.assertContains(response, "You can't move posts in this thread.", status_code=403)
@@ -105,16 +110,20 @@ class ThreadPostMoveApiTestCase(AuthenticatedUserTestCase):
     def test_invalid_url(self):
     def test_invalid_url(self):
         """api validates thread url"""
         """api validates thread url"""
         response = self.client.post(self.api_link, {
         response = self.client.post(self.api_link, {
-            'thread_url': self.user.get_absolute_url()
+            'thread_url': self.user.get_absolute_url(),
         })
         })
         self.assertContains(response, "This is not a valid thread link.", status_code=400)
         self.assertContains(response, "This is not a valid thread link.", status_code=400)
 
 
     def test_current_thread_url(self):
     def test_current_thread_url(self):
         """api validates if thread url given is to current thread"""
         """api validates if thread url given is to current thread"""
-        response = self.client.post(self.api_link, {
-            'thread_url': self.thread.get_absolute_url()
-        })
-        self.assertContains(response, "Thread to move posts to is same as current one.", status_code=400)
+        response = self.client.post(
+            self.api_link, {
+                'thread_url': self.thread.get_absolute_url(),
+            }
+        )
+        self.assertContains(
+            response, "Thread to move posts to is same as current one.", status_code=400
+        )
 
 
     def test_other_thread_exists(self):
     def test_other_thread_exists(self):
         """api validates if other thread exists"""
         """api validates if other thread exists"""
@@ -125,167 +134,217 @@ class ThreadPostMoveApiTestCase(AuthenticatedUserTestCase):
         other_thread.delete()
         other_thread.delete()
 
 
         response = self.client.post(self.api_link, {
         response = self.client.post(self.api_link, {
-            'thread_url': other_thread_url
+            'thread_url': other_thread_url,
         })
         })
-        self.assertContains(response, "The thread you have entered link to doesn't exist", status_code=400)
+        self.assertContains(
+            response, "The thread you have entered link to doesn't exist", status_code=400
+        )
 
 
     def test_other_thread_is_invisible(self):
     def test_other_thread_is_invisible(self):
         """api validates if other thread is visible"""
         """api validates if other thread is visible"""
-        self.override_other_acl({
-            'can_see': 0
-        })
+        self.override_other_acl({'can_see': 0})
 
 
         other_thread = testutils.post_thread(self.category_b)
         other_thread = testutils.post_thread(self.category_b)
 
 
-        response = self.client.post(self.api_link, {
-            'thread_url': other_thread.get_absolute_url()
-        })
-        self.assertContains(response, "The thread you have entered link to doesn't exist", status_code=400)
+        response = self.client.post(
+            self.api_link, {
+                'thread_url': other_thread.get_absolute_url(),
+            }
+        )
+        self.assertContains(
+            response, "The thread you have entered link to doesn't exist", status_code=400
+        )
 
 
     def test_other_thread_isnt_replyable(self):
     def test_other_thread_isnt_replyable(self):
         """api validates if other thread can be replied"""
         """api validates if other thread can be replied"""
-        self.override_other_acl({
-            'can_reply_threads': 0
-        })
+        self.override_other_acl({'can_reply_threads': 0})
 
 
         other_thread = testutils.post_thread(self.category_b)
         other_thread = testutils.post_thread(self.category_b)
 
 
-        response = self.client.post(self.api_link, {
-            'thread_url': other_thread.get_absolute_url()
-        })
-        self.assertContains(response, "You can't move posts to threads you can't reply.", status_code=400)
+        response = self.client.post(
+            self.api_link, {
+                'thread_url': other_thread.get_absolute_url(),
+            }
+        )
+        self.assertContains(
+            response, "You can't move posts to threads you can't reply.", status_code=400
+        )
 
 
     def test_empty_data(self):
     def test_empty_data(self):
         """api handles empty data"""
         """api handles empty data"""
         other_thread = testutils.post_thread(self.category)
         other_thread = testutils.post_thread(self.category)
 
 
-        response = self.client.post(self.api_link, {
-            'thread_url': other_thread.get_absolute_url()
-        })
-        self.assertContains(response, "You have to specify at least one post to move.", status_code=400)
+        response = self.client.post(
+            self.api_link, {
+                'thread_url': other_thread.get_absolute_url(),
+            }
+        )
+        self.assertContains(
+            response, "You have to specify at least one post to move.", status_code=400
+        )
 
 
     def test_no_posts_ids(self):
     def test_no_posts_ids(self):
         """api rejects no posts ids"""
         """api rejects no posts ids"""
         other_thread = testutils.post_thread(self.category)
         other_thread = testutils.post_thread(self.category)
 
 
-        response = self.client.post(self.api_link, json.dumps({
-            'thread_url': other_thread.get_absolute_url(),
-            'posts': []
-        }), content_type="application/json")
-        self.assertContains(response, "You have to specify at least one post to move.", status_code=400)
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'thread_url': other_thread.get_absolute_url(),
+                'posts': [],
+            }),
+            content_type="application/json",
+        )
+        self.assertContains(
+            response, "You have to specify at least one post to move.", status_code=400
+        )
 
 
     def test_invalid_posts_data(self):
     def test_invalid_posts_data(self):
         """api handles invalid data"""
         """api handles invalid data"""
         other_thread = testutils.post_thread(self.category)
         other_thread = testutils.post_thread(self.category)
 
 
-        response = self.client.post(self.api_link, json.dumps({
-            'thread_url': other_thread.get_absolute_url(),
-            'posts': 'string'
-        }), content_type="application/json")
-        self.assertContains(response, "One or more post ids received were invalid.", status_code=400)
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'thread_url': other_thread.get_absolute_url(),
+                'posts': 'string',
+            }),
+            content_type="application/json",
+        )
+        self.assertContains(
+            response, "One or more post ids received were invalid.", status_code=400
+        )
 
 
     def test_invalid_posts_ids(self):
     def test_invalid_posts_ids(self):
         """api handles invalid post id"""
         """api handles invalid post id"""
         other_thread = testutils.post_thread(self.category)
         other_thread = testutils.post_thread(self.category)
 
 
-        response = self.client.post(self.api_link, json.dumps({
-            'thread_url': other_thread.get_absolute_url(),
-            'posts': [1, 2, 'string']
-        }), content_type="application/json")
-        self.assertContains(response, "One or more post ids received were invalid.", status_code=400)
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'thread_url': other_thread.get_absolute_url(),
+                'posts': [1, 2, 'string'],
+            }),
+            content_type="application/json",
+        )
+        self.assertContains(
+            response, "One or more post ids received were invalid.", status_code=400
+        )
 
 
     def test_move_limit(self):
     def test_move_limit(self):
         """api rejects more posts than move limit"""
         """api rejects more posts than move limit"""
         other_thread = testutils.post_thread(self.category)
         other_thread = testutils.post_thread(self.category)
 
 
-        response = self.client.post(self.api_link, json.dumps({
-            'thread_url': other_thread.get_absolute_url(),
-            'posts': list(range(MOVE_LIMIT + 1))
-        }), content_type="application/json")
-        self.assertContains(response, "No more than {} posts can be moved".format(MOVE_LIMIT), status_code=400)
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'thread_url': other_thread.get_absolute_url(),
+                'posts': list(range(MOVE_LIMIT + 1)),
+            }),
+            content_type="application/json",
+        )
+        self.assertContains(
+            response, "No more than {} posts can be moved".format(MOVE_LIMIT), status_code=400
+        )
 
 
     def test_move_invisible(self):
     def test_move_invisible(self):
         """api validates posts visibility"""
         """api validates posts visibility"""
         other_thread = testutils.post_thread(self.category)
         other_thread = testutils.post_thread(self.category)
 
 
-        response = self.client.post(self.api_link, json.dumps({
-            'thread_url': other_thread.get_absolute_url(),
-            'posts': [
-                testutils.reply_thread(self.thread, is_unapproved=True).pk
-            ]
-        }), content_type="application/json")
-        self.assertContains(response, "One or more posts to move could not be found.", status_code=400)
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'thread_url': other_thread.get_absolute_url(),
+                'posts': [testutils.reply_thread(self.thread, is_unapproved=True).pk],
+            }),
+            content_type="application/json",
+        )
+        self.assertContains(
+            response, "One or more posts to move could not be found.", status_code=400
+        )
 
 
     def test_move_other_thread_posts(self):
     def test_move_other_thread_posts(self):
         """api recjects attempt to move other thread's post"""
         """api recjects attempt to move other thread's post"""
         other_thread = testutils.post_thread(self.category)
         other_thread = testutils.post_thread(self.category)
 
 
-        response = self.client.post(self.api_link, json.dumps({
-            'thread_url': other_thread.get_absolute_url(),
-            'posts': [
-                testutils.reply_thread(other_thread, is_hidden=True).pk
-            ]
-        }), content_type="application/json")
-        self.assertContains(response, "One or more posts to move could not be found.", status_code=400)
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'thread_url': other_thread.get_absolute_url(),
+                'posts': [testutils.reply_thread(other_thread, is_hidden=True).pk],
+            }),
+            content_type="application/json",
+        )
+        self.assertContains(
+            response, "One or more posts to move could not be found.", status_code=400
+        )
 
 
     def test_move_event(self):
     def test_move_event(self):
         """api rejects events move"""
         """api rejects events move"""
         other_thread = testutils.post_thread(self.category)
         other_thread = testutils.post_thread(self.category)
 
 
-        response = self.client.post(self.api_link, json.dumps({
-            'thread_url': other_thread.get_absolute_url(),
-            'posts': [
-                testutils.reply_thread(self.thread, is_event=True).pk
-            ]
-        }), content_type="application/json")
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'thread_url': other_thread.get_absolute_url(),
+                'posts': [testutils.reply_thread(self.thread, is_event=True).pk],
+            }),
+            content_type="application/json",
+        )
         self.assertContains(response, "Events can't be moved.", status_code=400)
         self.assertContains(response, "Events can't be moved.", status_code=400)
 
 
     def test_move_first_post(self):
     def test_move_first_post(self):
         """api rejects first post move"""
         """api rejects first post move"""
         other_thread = testutils.post_thread(self.category)
         other_thread = testutils.post_thread(self.category)
 
 
-        response = self.client.post(self.api_link, json.dumps({
-            'thread_url': other_thread.get_absolute_url(),
-            'posts': [
-                self.thread.first_post_id
-            ]
-        }), content_type="application/json")
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'thread_url': other_thread.get_absolute_url(),
+                'posts': [self.thread.first_post_id],
+            }),
+            content_type="application/json",
+        )
         self.assertContains(response, "You can't move thread's first post.", status_code=400)
         self.assertContains(response, "You can't move thread's first post.", status_code=400)
 
 
     def test_move_hidden_posts(self):
     def test_move_hidden_posts(self):
         """api recjects attempt to move urneadable hidden post"""
         """api recjects attempt to move urneadable hidden post"""
         other_thread = testutils.post_thread(self.category)
         other_thread = testutils.post_thread(self.category)
 
 
-        response = self.client.post(self.api_link, json.dumps({
-            'thread_url': other_thread.get_absolute_url(),
-            'posts': [
-                testutils.reply_thread(self.thread, is_hidden=True).pk
-            ]
-        }), content_type="application/json")
-        self.assertContains(response, "You can't move posts the content you can't see.", status_code=400)
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'thread_url': other_thread.get_absolute_url(),
+                'posts': [testutils.reply_thread(self.thread, is_hidden=True).pk],
+            }),
+            content_type="application/json",
+        )
+        self.assertContains(
+            response, "You can't move posts the content you can't see.", status_code=400
+        )
 
 
     def test_move_posts(self):
     def test_move_posts(self):
         """api moves posts to other thread"""
         """api moves posts to other thread"""
-        self.override_other_acl({
-            'can_reply_threads': 1
-        })
+        self.override_other_acl({'can_reply_threads': 1})
 
 
         other_thread = testutils.post_thread(self.category_b)
         other_thread = testutils.post_thread(self.category_b)
 
 
         posts = (
         posts = (
-            testutils.reply_thread(self.thread).pk,
-            testutils.reply_thread(self.thread).pk,
-            testutils.reply_thread(self.thread).pk,
-            testutils.reply_thread(self.thread).pk,
+            testutils.reply_thread(self.thread).pk, testutils.reply_thread(self.thread).pk,
+            testutils.reply_thread(self.thread).pk, testutils.reply_thread(self.thread).pk,
         )
         )
 
 
         self.refresh_thread()
         self.refresh_thread()
         self.assertEqual(self.thread.replies, 4)
         self.assertEqual(self.thread.replies, 4)
 
 
-        response = self.client.post(self.api_link, json.dumps({
-            'thread_url': other_thread.get_absolute_url(),
-            'posts': posts
-        }), content_type="application/json")
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'thread_url': other_thread.get_absolute_url(),
+                'posts': posts,
+            }),
+            content_type="application/json",
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         # replies were moved
         # replies were moved

+ 538 - 315
misago/threads/tests/test_thread_postpatch_api.py

@@ -10,7 +10,7 @@ from django.utils import timezone
 from misago.acl.testutils import override_acl
 from misago.acl.testutils import override_acl
 from misago.categories.models import Category
 from misago.categories.models import Category
 from misago.threads import testutils
 from misago.threads import testutils
-from misago.threads.models import Post, Thread
+from misago.threads.models import Post
 from misago.users.testutils import AuthenticatedUserTestCase
 from misago.users.testutils import AuthenticatedUserTestCase
 
 
 
 
@@ -22,10 +22,13 @@ class ThreadPostPatchApiTestCase(AuthenticatedUserTestCase):
         self.thread = testutils.post_thread(category=self.category)
         self.thread = testutils.post_thread(category=self.category)
         self.post = testutils.reply_thread(self.thread, poster=self.user)
         self.post = testutils.reply_thread(self.thread, poster=self.user)
 
 
-        self.api_link = reverse('misago:api:thread-post-detail', kwargs={
-            'thread_pk': self.thread.pk,
-            'pk': self.post.pk
-        })
+        self.api_link = reverse(
+            'misago:api:thread-post-detail',
+            kwargs={
+                'thread_pk': self.thread.pk,
+                'pk': self.post.pk,
+            }
+        )
 
 
     def patch(self, api_link, ops):
     def patch(self, api_link, ops):
         return self.client.patch(api_link, json.dumps(ops), content_type="application/json")
         return self.client.patch(api_link, json.dumps(ops), content_type="application/json")
@@ -40,7 +43,7 @@ class ThreadPostPatchApiTestCase(AuthenticatedUserTestCase):
             'can_browse': 1,
             'can_browse': 1,
             'can_start_threads': 0,
             'can_start_threads': 0,
             'can_reply_threads': 0,
             'can_reply_threads': 0,
-            'can_edit_posts': 1
+            'can_edit_posts': 1,
         })
         })
 
 
         if extra_acl:
         if extra_acl:
@@ -53,7 +56,11 @@ class PostAddAclApiTests(ThreadPostPatchApiTestCase):
     def test_add_acl_true(self):
     def test_add_acl_true(self):
         """api adds current event's acl to response"""
         """api adds current event's acl to response"""
         response = self.patch(self.api_link, [
         response = self.patch(self.api_link, [
-            {'op': 'add', 'path': 'acl', 'value': True}
+            {
+                'op': 'add',
+                'path': 'acl',
+                'value': True,
+            },
         ])
         ])
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
@@ -63,7 +70,11 @@ class PostAddAclApiTests(ThreadPostPatchApiTestCase):
     def test_add_acl_false(self):
     def test_add_acl_false(self):
         """if value is false, api won't add acl to the response, but will set empty key"""
         """if value is false, api won't add acl to the response, but will set empty key"""
         response = self.patch(self.api_link, [
         response = self.patch(self.api_link, [
-            {'op': 'add', 'path': 'acl', 'value': False}
+            {
+                'op': 'add',
+                'path': 'acl',
+                'value': False,
+            },
         ])
         ])
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
@@ -71,7 +82,11 @@ class PostAddAclApiTests(ThreadPostPatchApiTestCase):
         self.assertIsNone(response_json['acl'])
         self.assertIsNone(response_json['acl'])
 
 
         response = self.patch(self.api_link, [
         response = self.patch(self.api_link, [
-            {'op': 'add', 'path': 'acl', 'value': True}
+            {
+                'op': 'add',
+                'path': 'acl',
+                'value': True,
+            },
         ])
         ])
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
@@ -79,13 +94,17 @@ class PostAddAclApiTests(ThreadPostPatchApiTestCase):
 class PostProtectApiTests(ThreadPostPatchApiTestCase):
 class PostProtectApiTests(ThreadPostPatchApiTestCase):
     def test_protect_post(self):
     def test_protect_post(self):
         """api makes it possible to protect post"""
         """api makes it possible to protect post"""
-        self.override_acl({
-            'can_protect_posts': 1
-        })
-
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'is-protected', 'value': True}
-        ])
+        self.override_acl({'can_protect_posts': 1})
+
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-protected',
+                    'value': True,
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         self.refresh_post()
         self.refresh_post()
@@ -96,13 +115,17 @@ class PostProtectApiTests(ThreadPostPatchApiTestCase):
         self.post.is_protected = True
         self.post.is_protected = True
         self.post.save()
         self.post.save()
 
 
-        self.override_acl({
-            'can_protect_posts': 1
-        })
-
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'is-protected', 'value': False}
-        ])
+        self.override_acl({'can_protect_posts': 1})
+
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-protected',
+                    'value': False,
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         self.refresh_post()
         self.refresh_post()
@@ -110,13 +133,17 @@ class PostProtectApiTests(ThreadPostPatchApiTestCase):
 
 
     def test_protect_post_no_permission(self):
     def test_protect_post_no_permission(self):
         """api validates permission to protect post"""
         """api validates permission to protect post"""
-        self.override_acl({
-            'can_protect_posts': 0
-        })
-
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'is-protected', 'value': True}
-        ])
+        self.override_acl({'can_protect_posts': 0})
+
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-protected',
+                    'value': True,
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         response_json = response.json()
         response_json = response.json()
@@ -130,13 +157,17 @@ class PostProtectApiTests(ThreadPostPatchApiTestCase):
         self.post.is_protected = True
         self.post.is_protected = True
         self.post.save()
         self.post.save()
 
 
-        self.override_acl({
-            'can_protect_posts': 0
-        })
-
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'is-protected', 'value': False}
-        ])
+        self.override_acl({'can_protect_posts': 0})
+
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-protected',
+                    'value': False,
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         response_json = response.json()
         response_json = response.json()
@@ -147,14 +178,17 @@ class PostProtectApiTests(ThreadPostPatchApiTestCase):
 
 
     def test_unprotect_post_not_editable(self):
     def test_unprotect_post_not_editable(self):
         """api validates if we can edit post we want to protect"""
         """api validates if we can edit post we want to protect"""
-        self.override_acl({
-            'can_edit_posts': 0,
-            'can_protect_posts': 1
-        })
-
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'is-protected', 'value': True}
-        ])
+        self.override_acl({'can_edit_posts': 0, 'can_protect_posts': 1})
+
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-protected',
+                    'value': True,
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         response_json = response.json()
         response_json = response.json()
@@ -170,13 +204,17 @@ class PostApproveApiTests(ThreadPostPatchApiTestCase):
         self.post.is_unapproved = True
         self.post.is_unapproved = True
         self.post.save()
         self.post.save()
 
 
-        self.override_acl({
-            'can_approve_content': 1
-        })
-
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'is-unapproved', 'value': False}
-        ])
+        self.override_acl({'can_approve_content': 1})
+
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-unapproved',
+                    'value': False,
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         self.refresh_post()
         self.refresh_post()
@@ -184,13 +222,17 @@ class PostApproveApiTests(ThreadPostPatchApiTestCase):
 
 
     def test_unapprove_post(self):
     def test_unapprove_post(self):
         """unapproving posts is not supported by api"""
         """unapproving posts is not supported by api"""
-        self.override_acl({
-            'can_approve_content': 1
-        })
-
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'is-unapproved', 'value': True}
-        ])
+        self.override_acl({'can_approve_content': 1})
+
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-unapproved',
+                    'value': True,
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         self.refresh_post()
         self.refresh_post()
@@ -201,13 +243,17 @@ class PostApproveApiTests(ThreadPostPatchApiTestCase):
         self.post.is_unapproved = True
         self.post.is_unapproved = True
         self.post.save()
         self.post.save()
 
 
-        self.override_acl({
-            'can_approve_content': 0
-        })
-
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'is-unapproved', 'value': False}
-        ])
+        self.override_acl({'can_approve_content': 0})
+
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-unapproved',
+                    'value': False,
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         response_json = response.json()
         response_json = response.json()
@@ -224,13 +270,17 @@ class PostApproveApiTests(ThreadPostPatchApiTestCase):
         self.thread.set_first_post(self.post)
         self.thread.set_first_post(self.post)
         self.thread.save()
         self.thread.save()
 
 
-        self.override_acl({
-            'can_approve_content': 1
-        })
-
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'is-unapproved', 'value': False}
-        ])
+        self.override_acl({'can_approve_content': 1})
+
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-unapproved',
+                    'value': False,
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         response_json = response.json()
         response_json = response.json()
@@ -245,17 +295,23 @@ class PostApproveApiTests(ThreadPostPatchApiTestCase):
         self.post.is_hidden = True
         self.post.is_hidden = True
         self.post.save()
         self.post.save()
 
 
-        self.override_acl({
-            'can_approve_content': 1
-        })
-
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'is-unapproved', 'value': False}
-        ])
+        self.override_acl({'can_approve_content': 1})
+
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-unapproved',
+                    'value': False,
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         response_json = response.json()
         response_json = response.json()
-        self.assertEqual(response_json['detail'][0], "You can't approve posts the content you can't see.")
+        self.assertEqual(
+            response_json['detail'][0], "You can't approve posts the content you can't see."
+        )
 
 
         self.refresh_post()
         self.refresh_post()
         self.assertTrue(self.post.is_unapproved)
         self.assertTrue(self.post.is_unapproved)
@@ -264,13 +320,17 @@ class PostApproveApiTests(ThreadPostPatchApiTestCase):
 class PostHideApiTests(ThreadPostPatchApiTestCase):
 class PostHideApiTests(ThreadPostPatchApiTestCase):
     def test_hide_post(self):
     def test_hide_post(self):
         """api makes it possible to hide post"""
         """api makes it possible to hide post"""
-        self.override_acl({
-            'can_hide_posts': 1
-        })
-
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'is-hidden', 'value': True}
-        ])
+        self.override_acl({'can_hide_posts': 1})
+
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-hidden',
+                    'value': True,
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         self.refresh_post()
         self.refresh_post()
@@ -284,13 +344,17 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
         self.refresh_post()
         self.refresh_post()
         self.assertTrue(self.post.is_hidden)
         self.assertTrue(self.post.is_hidden)
 
 
-        self.override_acl({
-            'can_hide_posts': 1
-        })
-
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'is-hidden', 'value': False}
-        ])
+        self.override_acl({'can_hide_posts': 1})
+
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-hidden',
+                    'value': False,
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         self.refresh_post()
         self.refresh_post()
@@ -298,13 +362,17 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
 
 
     def test_hide_own_post(self):
     def test_hide_own_post(self):
         """api makes it possible to hide owned post"""
         """api makes it possible to hide owned post"""
-        self.override_acl({
-            'can_hide_own_posts': 1
-        })
-
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'is-hidden', 'value': True}
-        ])
+        self.override_acl({'can_hide_own_posts': 1})
+
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-hidden',
+                    'value': True,
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         self.refresh_post()
         self.refresh_post()
@@ -318,13 +386,17 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
         self.refresh_post()
         self.refresh_post()
         self.assertTrue(self.post.is_hidden)
         self.assertTrue(self.post.is_hidden)
 
 
-        self.override_acl({
-            'can_hide_own_posts': 1
-        })
-
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'is-hidden', 'value': False}
-        ])
+        self.override_acl({'can_hide_own_posts': 1})
+
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-hidden',
+                    'value': False,
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         self.refresh_post()
         self.refresh_post()
@@ -332,13 +404,17 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
 
 
     def test_hide_post_no_permission(self):
     def test_hide_post_no_permission(self):
         """api hide post with no permission fails"""
         """api hide post with no permission fails"""
-        self.override_acl({
-            'can_hide_posts': 0
-        })
-
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'is-hidden', 'value': True}
-        ])
+        self.override_acl({'can_hide_posts': 0})
+
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-hidden',
+                    'value': True,
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         response_json = response.json()
         response_json = response.json()
@@ -355,13 +431,17 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
         self.refresh_post()
         self.refresh_post()
         self.assertTrue(self.post.is_hidden)
         self.assertTrue(self.post.is_hidden)
 
 
-        self.override_acl({
-            'can_hide_posts': 0
-        })
-
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'is-hidden', 'value': False}
-        ])
+        self.override_acl({'can_hide_posts': 0})
+
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-hidden',
+                    'value': False,
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         response_json = response.json()
         response_json = response.json()
@@ -375,14 +455,17 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
         self.post.is_protected = True
         self.post.is_protected = True
         self.post.save()
         self.post.save()
 
 
-        self.override_acl({
-            'can_protect_posts': 0,
-            'can_hide_own_posts': 1
-        })
-
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'is-hidden', 'value': True}
-        ])
+        self.override_acl({'can_protect_posts': 0, 'can_hide_own_posts': 1})
+
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-hidden',
+                    'value': True,
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         response_json = response.json()
         response_json = response.json()
@@ -396,21 +479,26 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
         self.post.is_hidden = True
         self.post.is_hidden = True
         self.post.save()
         self.post.save()
 
 
-        self.override_acl({
-            'can_protect_posts': 0,
-            'can_hide_own_posts': 1
-        })
+        self.override_acl({'can_protect_posts': 0, 'can_hide_own_posts': 1})
 
 
         self.post.is_protected = True
         self.post.is_protected = True
         self.post.save()
         self.post.save()
 
 
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'is-hidden', 'value': False}
-        ])
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-hidden',
+                    'value': False,
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         response_json = response.json()
         response_json = response.json()
-        self.assertEqual(response_json['detail'][0], "This post is protected. You can't reveal it.")
+        self.assertEqual(
+            response_json['detail'][0], "This post is protected. You can't reveal it."
+        )
 
 
         self.refresh_post()
         self.refresh_post()
         self.assertTrue(self.post.is_hidden)
         self.assertTrue(self.post.is_hidden)
@@ -420,17 +508,23 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
         self.post.poster = None
         self.post.poster = None
         self.post.save()
         self.post.save()
 
 
-        self.override_acl({
-            'can_hide_own_posts': 1
-        })
-
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'is-hidden', 'value': True}
-        ])
+        self.override_acl({'can_hide_own_posts': 1})
+
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-hidden',
+                    'value': True,
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         response_json = response.json()
         response_json = response.json()
-        self.assertEqual(response_json['detail'][0], "You can't hide other users posts in this category.")
+        self.assertEqual(
+            response_json['detail'][0], "You can't hide other users posts in this category."
+        )
 
 
         self.refresh_post()
         self.refresh_post()
         self.assertFalse(self.post.is_hidden)
         self.assertFalse(self.post.is_hidden)
@@ -441,17 +535,23 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
         self.post.poster = None
         self.post.poster = None
         self.post.save()
         self.post.save()
 
 
-        self.override_acl({
-            'can_hide_own_posts': 1
-        })
-
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'is-hidden', 'value': False}
-        ])
+        self.override_acl({'can_hide_own_posts': 1})
+
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-hidden',
+                    'value': False,
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         response_json = response.json()
         response_json = response.json()
-        self.assertEqual(response_json['detail'][0], "You can't reveal other users posts in this category.")
+        self.assertEqual(
+            response_json['detail'][0], "You can't reveal other users posts in this category."
+        )
 
 
         self.refresh_post()
         self.refresh_post()
         self.assertTrue(self.post.is_hidden)
         self.assertTrue(self.post.is_hidden)
@@ -461,18 +561,23 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
         self.post.posted_on = timezone.now() - timedelta(minutes=10)
         self.post.posted_on = timezone.now() - timedelta(minutes=10)
         self.post.save()
         self.post.save()
 
 
-        self.override_acl({
-            'post_edit_time': 1,
-            'can_hide_own_posts': 1
-        })
-
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'is-hidden', 'value': True}
-        ])
+        self.override_acl({'post_edit_time': 1, 'can_hide_own_posts': 1})
+
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-hidden',
+                    'value': True,
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         response_json = response.json()
         response_json = response.json()
-        self.assertEqual(response_json['detail'][0], "You can't hide posts that are older than 1 minute.")
+        self.assertEqual(
+            response_json['detail'][0], "You can't hide posts that are older than 1 minute."
+        )
 
 
         self.refresh_post()
         self.refresh_post()
         self.assertFalse(self.post.is_hidden)
         self.assertFalse(self.post.is_hidden)
@@ -483,18 +588,23 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
         self.post.posted_on = timezone.now() - timedelta(minutes=10)
         self.post.posted_on = timezone.now() - timedelta(minutes=10)
         self.post.save()
         self.post.save()
 
 
-        self.override_acl({
-            'post_edit_time': 1,
-            'can_hide_own_posts': 1
-        })
-
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'is-hidden', 'value': False}
-        ])
+        self.override_acl({'post_edit_time': 1, 'can_hide_own_posts': 1})
+
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-hidden',
+                    'value': False,
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         response_json = response.json()
         response_json = response.json()
-        self.assertEqual(response_json['detail'][0], "You can't reveal posts that are older than 1 minute.")
+        self.assertEqual(
+            response_json['detail'][0], "You can't reveal posts that are older than 1 minute."
+        )
 
 
         self.refresh_post()
         self.refresh_post()
         self.assertTrue(self.post.is_hidden)
         self.assertTrue(self.post.is_hidden)
@@ -504,17 +614,23 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
         self.thread.is_closed = True
         self.thread.is_closed = True
         self.thread.save()
         self.thread.save()
 
 
-        self.override_acl({
-            'can_hide_own_posts': 1
-        })
-
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'is-hidden', 'value': True}
-        ])
+        self.override_acl({'can_hide_own_posts': 1})
+
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-hidden',
+                    'value': True,
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         response_json = response.json()
         response_json = response.json()
-        self.assertEqual(response_json['detail'][0], "This thread is closed. You can't hide posts in it.")
+        self.assertEqual(
+            response_json['detail'][0], "This thread is closed. You can't hide posts in it."
+        )
 
 
         self.refresh_post()
         self.refresh_post()
         self.assertFalse(self.post.is_hidden)
         self.assertFalse(self.post.is_hidden)
@@ -527,17 +643,23 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
         self.post.is_hidden = True
         self.post.is_hidden = True
         self.post.save()
         self.post.save()
 
 
-        self.override_acl({
-            'can_hide_own_posts': 1
-        })
-
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'is-hidden', 'value': False}
-        ])
+        self.override_acl({'can_hide_own_posts': 1})
+
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-hidden',
+                    'value': False,
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         response_json = response.json()
         response_json = response.json()
-        self.assertEqual(response_json['detail'][0], "This thread is closed. You can't reveal posts in it.")
+        self.assertEqual(
+            response_json['detail'][0], "This thread is closed. You can't reveal posts in it."
+        )
 
 
         self.refresh_post()
         self.refresh_post()
         self.assertTrue(self.post.is_hidden)
         self.assertTrue(self.post.is_hidden)
@@ -547,17 +669,23 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
         self.category.is_closed = True
         self.category.is_closed = True
         self.category.save()
         self.category.save()
 
 
-        self.override_acl({
-            'can_hide_own_posts': 1
-        })
-
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'is-hidden', 'value': True}
-        ])
+        self.override_acl({'can_hide_own_posts': 1})
+
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-hidden',
+                    'value': True,
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         response_json = response.json()
         response_json = response.json()
-        self.assertEqual(response_json['detail'][0], "This category is closed. You can't hide posts in it.")
+        self.assertEqual(
+            response_json['detail'][0], "This category is closed. You can't hide posts in it."
+        )
 
 
         self.refresh_post()
         self.refresh_post()
         self.assertFalse(self.post.is_hidden)
         self.assertFalse(self.post.is_hidden)
@@ -570,17 +698,23 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
         self.post.is_hidden = True
         self.post.is_hidden = True
         self.post.save()
         self.post.save()
 
 
-        self.override_acl({
-            'can_hide_own_posts': 1
-        })
-
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'is-hidden', 'value': False}
-        ])
+        self.override_acl({'can_hide_own_posts': 1})
+
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-hidden',
+                    'value': False,
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         response_json = response.json()
         response_json = response.json()
-        self.assertEqual(response_json['detail'][0], "This category is closed. You can't reveal posts in it.")
+        self.assertEqual(
+            response_json['detail'][0], "This category is closed. You can't reveal posts in it."
+        )
 
 
         self.refresh_post()
         self.refresh_post()
         self.assertTrue(self.post.is_hidden)
         self.assertTrue(self.post.is_hidden)
@@ -590,13 +724,17 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
         self.thread.set_first_post(self.post)
         self.thread.set_first_post(self.post)
         self.thread.save()
         self.thread.save()
 
 
-        self.override_acl({
-            'can_hide_posts': 1
-        })
-
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'is-hidden', 'value': True}
-        ])
+        self.override_acl({'can_hide_posts': 1})
+
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-hidden',
+                    'value': True,
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         response_json = response.json()
         response_json = response.json()
@@ -607,13 +745,17 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
         self.thread.set_first_post(self.post)
         self.thread.set_first_post(self.post)
         self.thread.save()
         self.thread.save()
 
 
-        self.override_acl({
-            'can_hide_posts': 1
-        })
-
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'is-hidden', 'value': False}
-        ])
+        self.override_acl({'can_hide_posts': 1})
+
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-hidden',
+                    'value': False,
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         response_json = response.json()
         response_json = response.json()
@@ -623,42 +765,58 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
 class PostLikeApiTests(ThreadPostPatchApiTestCase):
 class PostLikeApiTests(ThreadPostPatchApiTestCase):
     def test_like_no_see_permission(self):
     def test_like_no_see_permission(self):
         """api validates user's permission to see posts likes"""
         """api validates user's permission to see posts likes"""
-        self.override_acl({
-            'can_see_posts_likes': 0
-        })
-
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'is-liked', 'value': True}
-        ])
+        self.override_acl({'can_see_posts_likes': 0})
+
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-liked',
+                    'value': True,
+                },
+            ]
+        )
         self.assertContains(response, "You can't like posts in this category.", status_code=400)
         self.assertContains(response, "You can't like posts in this category.", status_code=400)
 
 
     def test_like_no_like_permission(self):
     def test_like_no_like_permission(self):
         """api validates user's permission to see posts likes"""
         """api validates user's permission to see posts likes"""
-        self.override_acl({
-            'can_like_posts': False
-        })
-
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'is-liked', 'value': True}
-        ])
+        self.override_acl({'can_like_posts': False})
+
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-liked',
+                    'value': True,
+                },
+            ]
+        )
         self.assertContains(response, "You can't like posts in this category.", status_code=400)
         self.assertContains(response, "You can't like posts in this category.", status_code=400)
 
 
     def test_like_post(self):
     def test_like_post(self):
         """api adds user like to post"""
         """api adds user like to post"""
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'is-liked', 'value': True}
-        ])
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-liked',
+                    'value': True,
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         response_json = response.json()
         response_json = response.json()
         self.assertEqual(response_json['likes'], 1)
         self.assertEqual(response_json['likes'], 1)
         self.assertEqual(response_json['is_liked'], True)
         self.assertEqual(response_json['is_liked'], True)
-        self.assertEqual(response_json['last_likes'], [
-            {
-                'id': self.user.id,
-                'username': self.user.username
-            }
-        ])
+        self.assertEqual(
+            response_json['last_likes'], [
+                {
+                    'id': self.user.id,
+                    'username': self.user.username,
+                },
+            ]
+        )
 
 
         post = Post.objects.get(pk=self.post.pk)
         post = Post.objects.get(pk=self.post.pk)
         self.assertEqual(post.likes, response_json['likes'])
         self.assertEqual(post.likes, response_json['likes'])
@@ -671,32 +829,40 @@ class PostLikeApiTests(ThreadPostPatchApiTestCase):
         testutils.like_post(self.post, username='Bob')
         testutils.like_post(self.post, username='Bob')
         testutils.like_post(self.post, username='Miku')
         testutils.like_post(self.post, username='Miku')
 
 
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'is-liked', 'value': True}
-        ])
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-liked',
+                    'value': True,
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         response_json = response.json()
         response_json = response.json()
         self.assertEqual(response_json['likes'], 5)
         self.assertEqual(response_json['likes'], 5)
         self.assertEqual(response_json['is_liked'], True)
         self.assertEqual(response_json['is_liked'], True)
-        self.assertEqual(response_json['last_likes'], [
-            {
-                'id': self.user.id,
-                'username': self.user.username
-            },
-            {
-                'id': None,
-                'username': 'Miku'
-            },
-            {
-                'id': None,
-                'username': 'Bob'
-            },
-            {
-                'id': None,
-                'username': 'Mugi'
-            }
-        ])
+        self.assertEqual(
+            response_json['last_likes'], [
+                {
+                    'id': self.user.id,
+                    'username': self.user.username
+                },
+                {
+                    'id': None,
+                    'username': 'Miku',
+                },
+                {
+                    'id': None,
+                    'username': 'Bob',
+                },
+                {
+                    'id': None,
+                    'username': 'Mugi',
+                },
+            ]
+        )
 
 
         post = Post.objects.get(pk=self.post.pk)
         post = Post.objects.get(pk=self.post.pk)
         self.assertEqual(post.likes, response_json['likes'])
         self.assertEqual(post.likes, response_json['likes'])
@@ -706,9 +872,15 @@ class PostLikeApiTests(ThreadPostPatchApiTestCase):
         """api removes user like from post"""
         """api removes user like from post"""
         testutils.like_post(self.post, self.user)
         testutils.like_post(self.post, self.user)
 
 
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'is-liked', 'value': False}
-        ])
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-liked',
+                    'value': False,
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         response_json = response.json()
         response_json = response.json()
@@ -724,20 +896,28 @@ class PostLikeApiTests(ThreadPostPatchApiTestCase):
         """api does no state change if we are linking liked post"""
         """api does no state change if we are linking liked post"""
         testutils.like_post(self.post, self.user)
         testutils.like_post(self.post, self.user)
 
 
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'is-liked', 'value': True}
-        ])
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-liked',
+                    'value': True,
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         response_json = response.json()
         response_json = response.json()
         self.assertEqual(response_json['likes'], 1)
         self.assertEqual(response_json['likes'], 1)
         self.assertEqual(response_json['is_liked'], True)
         self.assertEqual(response_json['is_liked'], True)
-        self.assertEqual(response_json['last_likes'], [
-            {
-                'id': self.user.id,
-                'username': self.user.username
-            }
-        ])
+        self.assertEqual(
+            response_json['last_likes'], [
+                {
+                    'id': self.user.id,
+                    'username': self.user.username,
+                },
+            ]
+        )
 
 
         post = Post.objects.get(pk=self.post.pk)
         post = Post.objects.get(pk=self.post.pk)
         self.assertEqual(post.likes, response_json['likes'])
         self.assertEqual(post.likes, response_json['likes'])
@@ -745,9 +925,15 @@ class PostLikeApiTests(ThreadPostPatchApiTestCase):
 
 
     def test_unlike_post_no_change(self):
     def test_unlike_post_no_change(self):
         """api does no state change if we are unlinking unliked post"""
         """api does no state change if we are unlinking unliked post"""
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'is-liked', 'value': False}
-        ])
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-liked',
+                    'value': False,
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         response_json = response.json()
         response_json = response.json()
@@ -762,10 +948,13 @@ class ThreadEventPatchApiTestCase(ThreadPostPatchApiTestCase):
 
 
         self.event = testutils.reply_thread(self.thread, poster=self.user, is_event=True)
         self.event = testutils.reply_thread(self.thread, poster=self.user, is_event=True)
 
 
-        self.api_link = reverse('misago:api:thread-post-detail', kwargs={
-            'thread_pk': self.thread.pk,
-            'pk': self.event.pk
-        })
+        self.api_link = reverse(
+            'misago:api:thread-post-detail',
+            kwargs={
+                'thread_pk': self.thread.pk,
+                'pk': self.event.pk,
+            }
+        )
 
 
     def refresh_event(self):
     def refresh_event(self):
         self.event = self.thread.post_set.get(pk=self.event.pk)
         self.event = self.thread.post_set.get(pk=self.event.pk)
@@ -777,7 +966,11 @@ class EventAnonPatchApiTests(ThreadEventPatchApiTestCase):
         self.logout_user()
         self.logout_user()
 
 
         response = self.patch(self.api_link, [
         response = self.patch(self.api_link, [
-            {'op': 'add', 'path': 'acl', 'value': True}
+            {
+                'op': 'add',
+                'path': 'acl',
+                'value': True,
+            },
         ])
         ])
         self.assertEqual(response.status_code, 403)
         self.assertEqual(response.status_code, 403)
 
 
@@ -786,7 +979,11 @@ class EventAddAclApiTests(ThreadEventPatchApiTestCase):
     def test_add_acl_true(self):
     def test_add_acl_true(self):
         """api adds current event's acl to response"""
         """api adds current event's acl to response"""
         response = self.patch(self.api_link, [
         response = self.patch(self.api_link, [
-            {'op': 'add', 'path': 'acl', 'value': True}
+            {
+                'op': 'add',
+                'path': 'acl',
+                'value': True,
+            },
         ])
         ])
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
@@ -796,7 +993,11 @@ class EventAddAclApiTests(ThreadEventPatchApiTestCase):
     def test_add_acl_false(self):
     def test_add_acl_false(self):
         """if value is false, api won't add acl to the response, but will set empty key"""
         """if value is false, api won't add acl to the response, but will set empty key"""
         response = self.patch(self.api_link, [
         response = self.patch(self.api_link, [
-            {'op': 'add', 'path': 'acl', 'value': False}
+            {
+                'op': 'add',
+                'path': 'acl',
+                'value': False,
+            },
         ])
         ])
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
@@ -804,7 +1005,11 @@ class EventAddAclApiTests(ThreadEventPatchApiTestCase):
         self.assertIsNone(response_json['acl'])
         self.assertIsNone(response_json['acl'])
 
 
         response = self.patch(self.api_link, [
         response = self.patch(self.api_link, [
-            {'op': 'add', 'path': 'acl', 'value': True}
+            {
+                'op': 'add',
+                'path': 'acl',
+                'value': True,
+            },
         ])
         ])
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
@@ -812,13 +1017,17 @@ class EventAddAclApiTests(ThreadEventPatchApiTestCase):
 class EventHideApiTests(ThreadEventPatchApiTestCase):
 class EventHideApiTests(ThreadEventPatchApiTestCase):
     def test_hide_event(self):
     def test_hide_event(self):
         """api makes it possible to hide event"""
         """api makes it possible to hide event"""
-        self.override_acl({
-            'can_hide_events': 1
-        })
-
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'is-hidden', 'value': True}
-        ])
+        self.override_acl({'can_hide_events': 1})
+
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-hidden',
+                    'value': True,
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         self.refresh_event()
         self.refresh_event()
@@ -832,13 +1041,17 @@ class EventHideApiTests(ThreadEventPatchApiTestCase):
         self.refresh_event()
         self.refresh_event()
         self.assertTrue(self.event.is_hidden)
         self.assertTrue(self.event.is_hidden)
 
 
-        self.override_acl({
-            'can_hide_events': 1
-        })
-
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'is-hidden', 'value': False}
-        ])
+        self.override_acl({'can_hide_events': 1})
+
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-hidden',
+                    'value': False,
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         self.refresh_event()
         self.refresh_event()
@@ -846,17 +1059,23 @@ class EventHideApiTests(ThreadEventPatchApiTestCase):
 
 
     def test_hide_event_no_permission(self):
     def test_hide_event_no_permission(self):
         """api hide event with no permission fails"""
         """api hide event with no permission fails"""
-        self.override_acl({
-            'can_hide_events': 0
-        })
-
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'is-hidden', 'value': True}
-        ])
+        self.override_acl({'can_hide_events': 0})
+
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-hidden',
+                    'value': True,
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         response_json = response.json()
         response_json = response.json()
-        self.assertEqual(response_json['detail'][0], "You don't have permission to hide this event.")
+        self.assertEqual(
+            response_json['detail'][0], "You don't have permission to hide this event."
+        )
 
 
         self.refresh_event()
         self.refresh_event()
         self.assertFalse(self.event.is_hidden)
         self.assertFalse(self.event.is_hidden)
@@ -869,11 +1088,15 @@ class EventHideApiTests(ThreadEventPatchApiTestCase):
         self.refresh_event()
         self.refresh_event()
         self.assertTrue(self.event.is_hidden)
         self.assertTrue(self.event.is_hidden)
 
 
-        self.override_acl({
-            'can_hide_events': 0
-        })
-
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'is-hidden', 'value': False}
-        ])
+        self.override_acl({'can_hide_events': 0})
+
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-hidden',
+                    'value': False,
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 404)
         self.assertEqual(response.status_code, 404)

+ 9 - 7
misago/threads/tests/test_thread_postread_api.py

@@ -2,7 +2,6 @@ from django.urls import reverse
 from django.utils import timezone
 from django.utils import timezone
 
 
 from misago.threads import testutils
 from misago.threads import testutils
-from misago.threads.models import Post, Thread
 
 
 from .test_threads_api import ThreadsApiTestCase
 from .test_threads_api import ThreadsApiTestCase
 
 
@@ -14,13 +13,16 @@ class PostReadApiTests(ThreadsApiTestCase):
         self.post = testutils.reply_thread(
         self.post = testutils.reply_thread(
             self.thread,
             self.thread,
             poster=self.user,
             poster=self.user,
-            posted_on=timezone.now()
+            posted_on=timezone.now(),
         )
         )
 
 
-        self.api_link = reverse('misago:api:thread-post-read', kwargs={
-            'thread_pk': self.thread.pk,
-            'pk': self.post.pk
-        })
+        self.api_link = reverse(
+            'misago:api:thread-post-read',
+            kwargs={
+                'thread_pk': self.thread.pk,
+                'pk': self.post.pk,
+            }
+        )
 
 
     def test_read_anonymous(self):
     def test_read_anonymous(self):
         """api validates if reading user is authenticated"""
         """api validates if reading user is authenticated"""
@@ -47,7 +49,7 @@ class PostReadApiTests(ThreadsApiTestCase):
             user=self.user,
             user=self.user,
             thread=self.thread,
             thread=self.thread,
             category=self.thread.category,
             category=self.thread.category,
-            last_read_on=self.thread.post_set.order_by('id').first().posted_on
+            last_read_on=self.thread.post_set.order_by('id').first().posted_on,
         )
         )
 
 
         response = self.client.post(self.api_link)
         response = self.client.post(self.api_link)

+ 346 - 237
misago/threads/tests/test_thread_postsplit_api.py

@@ -4,8 +4,6 @@ from __future__ import unicode_literals
 import json
 import json
 
 
 from django.urls import reverse
 from django.urls import reverse
-from django.utils.encoding import smart_str
-from django.utils.six.moves import range
 
 
 from misago.acl.testutils import override_acl
 from misago.acl.testutils import override_acl
 from misago.categories.models import Category
 from misago.categories.models import Category
@@ -22,18 +20,23 @@ class ThreadPostSplitApiTestCase(AuthenticatedUserTestCase):
         self.category = Category.objects.get(slug='first-category')
         self.category = Category.objects.get(slug='first-category')
         self.thread = testutils.post_thread(category=self.category)
         self.thread = testutils.post_thread(category=self.category)
         self.posts = [
         self.posts = [
-            testutils.reply_thread(self.thread).pk,
-            testutils.reply_thread(self.thread).pk
+            testutils.reply_thread(self.thread).pk, testutils.reply_thread(self.thread).pk
         ]
         ]
 
 
-        self.api_link = reverse('misago:api:thread-post-split', kwargs={
-            'thread_pk': self.thread.pk
-        })
+        self.api_link = reverse(
+            'misago:api:thread-post-split', kwargs={
+                'thread_pk': self.thread.pk,
+            }
+        )
 
 
         Category(
         Category(
             name='Category B',
             name='Category B',
             slug='category-b',
             slug='category-b',
-        ).insert_at(self.category, position='last-child', save=True)
+        ).insert_at(
+            self.category,
+            position='last-child',
+            save=True,
+        )
         self.category_b = Category.objects.get(slug='category-b')
         self.category_b = Category.objects.get(slug='category-b')
 
 
         self.override_acl()
         self.override_acl()
@@ -51,7 +54,7 @@ class ThreadPostSplitApiTestCase(AuthenticatedUserTestCase):
             'can_reply_threads': 1,
             'can_reply_threads': 1,
             'can_edit_posts': 1,
             'can_edit_posts': 1,
             'can_approve_content': 0,
             'can_approve_content': 0,
-            'can_move_posts': 1
+            'can_move_posts': 1,
         })
         })
 
 
         if extra_acl:
         if extra_acl:
@@ -68,7 +71,7 @@ class ThreadPostSplitApiTestCase(AuthenticatedUserTestCase):
             'can_reply_threads': 0,
             'can_reply_threads': 0,
             'can_edit_posts': 1,
             'can_edit_posts': 1,
             'can_approve_content': 0,
             'can_approve_content': 0,
-            'can_move_posts': 1
+            'can_move_posts': 1,
         })
         })
 
 
         if acl:
         if acl:
@@ -81,10 +84,12 @@ class ThreadPostSplitApiTestCase(AuthenticatedUserTestCase):
         if other_category_acl['can_see']:
         if other_category_acl['can_see']:
             visible_categories.append(self.category_b.pk)
             visible_categories.append(self.category_b.pk)
 
 
-        override_acl(self.user, {
-            'visible_categories': visible_categories,
-            'categories': categories_acl,
-        })
+        override_acl(
+            self.user, {
+                'visible_categories': visible_categories,
+                'categories': categories_acl,
+            }
+        )
 
 
     def test_anonymous_user(self):
     def test_anonymous_user(self):
         """you need to authenticate to split posts"""
         """you need to authenticate to split posts"""
@@ -95,9 +100,7 @@ class ThreadPostSplitApiTestCase(AuthenticatedUserTestCase):
 
 
     def test_no_permission(self):
     def test_no_permission(self):
         """api validates permission to split"""
         """api validates permission to split"""
-        self.override_acl({
-            'can_move_posts': 0
-        })
+        self.override_acl({'can_move_posts': 0})
 
 
         response = self.client.post(self.api_link, json.dumps({}), content_type="application/json")
         response = self.client.post(self.api_link, json.dumps({}), content_type="application/json")
         self.assertContains(response, "You can't split posts from this thread.", status_code=403)
         self.assertContains(response, "You can't split posts from this thread.", status_code=403)
@@ -105,319 +108,421 @@ class ThreadPostSplitApiTestCase(AuthenticatedUserTestCase):
     def test_empty_data(self):
     def test_empty_data(self):
         """api handles empty data"""
         """api handles empty data"""
         response = self.client.post(self.api_link)
         response = self.client.post(self.api_link)
-        self.assertContains(response, "You have to specify at least one post to split.", status_code=400)
+        self.assertContains(
+            response, "You have to specify at least one post to split.", status_code=400
+        )
 
 
     def test_no_posts_ids(self):
     def test_no_posts_ids(self):
         """api rejects no posts ids"""
         """api rejects no posts ids"""
-        response = self.client.post(self.api_link, json.dumps({
-            'posts': []
-        }), content_type="application/json")
-        self.assertContains(response, "You have to specify at least one post to split.", status_code=400)
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'posts': [],
+            }),
+            content_type="application/json",
+        )
+        self.assertContains(
+            response, "You have to specify at least one post to split.", status_code=400
+        )
 
 
     def test_invalid_posts_data(self):
     def test_invalid_posts_data(self):
         """api handles invalid data"""
         """api handles invalid data"""
-        response = self.client.post(self.api_link, json.dumps({
-            'posts': 'string'
-        }), content_type="application/json")
-        self.assertContains(response, "One or more post ids received were invalid.", status_code=400)
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'posts': 'string',
+            }),
+            content_type="application/json",
+        )
+        self.assertContains(
+            response, "One or more post ids received were invalid.", status_code=400
+        )
 
 
     def test_invalid_posts_ids(self):
     def test_invalid_posts_ids(self):
         """api handles invalid post id"""
         """api handles invalid post id"""
-        response = self.client.post(self.api_link, json.dumps({
-            'posts': [1, 2, 'string']
-        }), content_type="application/json")
-        self.assertContains(response, "One or more post ids received were invalid.", status_code=400)
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'posts': [1, 2, 'string'],
+            }),
+            content_type="application/json",
+        )
+        self.assertContains(
+            response, "One or more post ids received were invalid.", status_code=400
+        )
 
 
     def test_split_limit(self):
     def test_split_limit(self):
         """api rejects more posts than split limit"""
         """api rejects more posts than split limit"""
-        response = self.client.post(self.api_link, json.dumps({
-            'posts': list(range(SPLIT_LIMIT + 1))
-        }), content_type="application/json")
-        self.assertContains(response, "No more than {} posts can be split".format(SPLIT_LIMIT), status_code=400)
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'posts': list(range(SPLIT_LIMIT + 1)),
+            }),
+            content_type="application/json",
+        )
+        self.assertContains(
+            response, "No more than {} posts can be split".format(SPLIT_LIMIT), status_code=400
+        )
 
 
     def test_split_invisible(self):
     def test_split_invisible(self):
         """api validates posts visibility"""
         """api validates posts visibility"""
-        response = self.client.post(self.api_link, json.dumps({
-            'posts': [
-                testutils.reply_thread(self.thread, is_unapproved=True).pk
-            ]
-        }), content_type="application/json")
-        self.assertContains(response, "One or more posts to split could not be found.", status_code=400)
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'posts': [testutils.reply_thread(self.thread, is_unapproved=True).pk],
+            }),
+            content_type="application/json",
+        )
+        self.assertContains(
+            response, "One or more posts to split could not be found.", status_code=400
+        )
 
 
     def test_split_event(self):
     def test_split_event(self):
         """api rejects events split"""
         """api rejects events split"""
-        response = self.client.post(self.api_link, json.dumps({
-            'posts': [
-                testutils.reply_thread(self.thread, is_event=True).pk
-            ]
-        }), content_type="application/json")
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'posts': [testutils.reply_thread(self.thread, is_event=True).pk],
+            }),
+            content_type="application/json",
+        )
         self.assertContains(response, "Events can't be split.", status_code=400)
         self.assertContains(response, "Events can't be split.", status_code=400)
 
 
     def test_split_first_post(self):
     def test_split_first_post(self):
         """api rejects first post split"""
         """api rejects first post split"""
-        response = self.client.post(self.api_link, json.dumps({
-            'posts': [
-                self.thread.first_post_id
-            ]
-        }), content_type="application/json")
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'posts': [self.thread.first_post_id],
+            }),
+            content_type="application/json",
+        )
         self.assertContains(response, "You can't split thread's first post.", status_code=400)
         self.assertContains(response, "You can't split thread's first post.", status_code=400)
 
 
     def test_split_hidden_posts(self):
     def test_split_hidden_posts(self):
         """api recjects attempt to split urneadable hidden post"""
         """api recjects attempt to split urneadable hidden post"""
-        response = self.client.post(self.api_link, json.dumps({
-            'posts': [
-                testutils.reply_thread(self.thread, is_hidden=True).pk
-            ]
-        }), content_type="application/json")
-        self.assertContains(response, "You can't split posts the content you can't see.", status_code=400)
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'posts': [testutils.reply_thread(self.thread, is_hidden=True).pk],
+            }),
+            content_type="application/json",
+        )
+        self.assertContains(
+            response, "You can't split posts the content you can't see.", status_code=400
+        )
 
 
     def test_split_other_thread_posts(self):
     def test_split_other_thread_posts(self):
         """api recjects attempt to split other thread's post"""
         """api recjects attempt to split other thread's post"""
         other_thread = testutils.post_thread(self.category)
         other_thread = testutils.post_thread(self.category)
 
 
-        response = self.client.post(self.api_link, json.dumps({
-            'posts': [
-                testutils.reply_thread(other_thread, is_hidden=True).pk
-            ]
-        }), content_type="application/json")
-        self.assertContains(response, "One or more posts to split could not be found.", status_code=400)
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'posts': [testutils.reply_thread(other_thread, is_hidden=True).pk],
+            }),
+            content_type="application/json",
+        )
+        self.assertContains(
+            response, "One or more posts to split could not be found.", status_code=400
+        )
 
 
     def test_split_empty_new_thread_data(self):
     def test_split_empty_new_thread_data(self):
         """api handles empty form data"""
         """api handles empty form data"""
-        response = self.client.post(self.api_link, json.dumps({
-            'posts': self.posts
-        }), content_type="application/json")
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'posts': self.posts,
+            }),
+            content_type="application/json",
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
-        response_json = json.loads(smart_str(response.content))
-        self.assertEqual(response_json, {
-            'title': ['This field is required.'],
-            'category': ['This field is required.'],
-        })
+        response_json = response.json()
+        self.assertEqual(
+            response_json, {
+                'title': ['This field is required.'],
+                'category': ['This field is required.'],
+            }
+        )
 
 
     def test_split_invalid_final_title(self):
     def test_split_invalid_final_title(self):
         """api rejects split because final thread title was invalid"""
         """api rejects split because final thread title was invalid"""
-        response = self.client.post(self.api_link, json.dumps({
-            'posts': self.posts,
-            'title': '$$$',
-            'category': self.category.id
-        }), content_type="application/json")
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'posts': self.posts,
+                'title': '$$$',
+                'category': self.category.id,
+            }),
+            content_type="application/json",
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
-        response_json = json.loads(smart_str(response.content))
-        self.assertEqual(response_json, {
-            'title': ["Thread title should be at least 5 characters long (it has 3)."]
-        })
+        response_json = response.json()
+        self.assertEqual(
+            response_json, {
+                'title': ["Thread title should be at least 5 characters long (it has 3)."],
+            }
+        )
 
 
     def test_split_invalid_category(self):
     def test_split_invalid_category(self):
         """api rejects split because final category was invalid"""
         """api rejects split because final category was invalid"""
-        self.override_other_acl({
-            'can_see': 0
-        })
-
-        response = self.client.post(self.api_link, json.dumps({
-            'posts': self.posts,
-            'title': 'Valid thread title',
-            'category': self.category_b.id
-        }), content_type="application/json")
+        self.override_other_acl({'can_see': 0})
+
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'posts': self.posts,
+                'title': 'Valid thread title',
+                'category': self.category_b.id,
+            }),
+            content_type="application/json",
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
-        response_json = json.loads(smart_str(response.content))
-        self.assertEqual(response_json, {
-            'category': ["Requested category could not be found."]
-        })
+        response_json = response.json()
+        self.assertEqual(
+            response_json, {
+                'category': ["Requested category could not be found."],
+            }
+        )
 
 
     def test_split_unallowed_start_thread(self):
     def test_split_unallowed_start_thread(self):
         """api rejects split because category isn't allowing starting threads"""
         """api rejects split because category isn't allowing starting threads"""
-        self.override_acl({
-            'can_start_threads': 0
-        })
-
-        response = self.client.post(self.api_link, json.dumps({
-            'posts': self.posts,
-            'title': 'Valid thread title',
-            'category': self.category.id
-        }), content_type="application/json")
+        self.override_acl({'can_start_threads': 0})
+
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'posts': self.posts,
+                'title': 'Valid thread title',
+                'category': self.category.id,
+            }),
+            content_type="application/json",
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
-        response_json = json.loads(smart_str(response.content))
-        self.assertEqual(response_json, {
-            'category': [
-                "You can't create new threads in selected category."
-            ]
-        })
+        response_json = response.json()
+        self.assertEqual(
+            response_json, {
+                'category': ["You can't create new threads in selected category."],
+            }
+        )
 
 
     def test_split_invalid_weight(self):
     def test_split_invalid_weight(self):
         """api rejects split because final weight was invalid"""
         """api rejects split because final weight was invalid"""
-        response = self.client.post(self.api_link, json.dumps({
-            'posts': self.posts,
-            'title': 'Valid thread title',
-            'category': self.category.id,
-            'weight': 4,
-        }), content_type="application/json")
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'posts': self.posts,
+                'title': 'Valid thread title',
+                'category': self.category.id,
+                'weight': 4,
+            }),
+            content_type="application/json",
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
-        response_json = json.loads(smart_str(response.content))
-        self.assertEqual(response_json, {
-            'weight': ["Ensure this value is less than or equal to 2."]
-        })
+        response_json = response.json()
+        self.assertEqual(
+            response_json, {
+                'weight': ["Ensure this value is less than or equal to 2."],
+            }
+        )
 
 
     def test_split_unallowed_global_weight(self):
     def test_split_unallowed_global_weight(self):
         """api rejects split because global weight was unallowed"""
         """api rejects split because global weight was unallowed"""
-        response = self.client.post(self.api_link, json.dumps({
-            'posts': self.posts,
-            'title': 'Valid thread title',
-            'category': self.category.id,
-            'weight': 2,
-        }), content_type="application/json")
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'posts': self.posts,
+                'title': 'Valid thread title',
+                'category': self.category.id,
+                'weight': 2,
+            }),
+            content_type="application/json",
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
-        response_json = json.loads(smart_str(response.content))
-        self.assertEqual(response_json, {
-            'weight': [
-                "You don't have permission to pin threads globally in this category."
-            ]
-        })
+        response_json = response.json()
+        self.assertEqual(
+            response_json, {
+                'weight': ["You don't have permission to pin threads globally in this category."],
+            }
+        )
 
 
     def test_split_unallowed_local_weight(self):
     def test_split_unallowed_local_weight(self):
         """api rejects split because local weight was unallowed"""
         """api rejects split because local weight was unallowed"""
-        response = self.client.post(self.api_link, json.dumps({
-            'posts': self.posts,
-            'title': 'Valid thread title',
-            'category': self.category.id,
-            'weight': 1,
-        }), content_type="application/json")
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'posts': self.posts,
+                'title': 'Valid thread title',
+                'category': self.category.id,
+                'weight': 1,
+            }),
+            content_type="application/json"
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
-        response_json = json.loads(smart_str(response.content))
-        self.assertEqual(response_json, {
-            'weight': [
-                "You don't have permission to pin threads in this category."
-            ]
-        })
+        response_json = response.json()
+        self.assertEqual(
+            response_json, {
+                'weight': ["You don't have permission to pin threads in this category."],
+            }
+        )
 
 
     def test_split_allowed_local_weight(self):
     def test_split_allowed_local_weight(self):
         """api allows local weight"""
         """api allows local weight"""
-        self.override_acl({
-            'can_pin_threads': 1
-        })
-
-        response = self.client.post(self.api_link, json.dumps({
-            'posts': self.posts,
-            'title': '$$$',
-            'category': self.category.id,
-            'weight': 1,
-        }), content_type="application/json")
+        self.override_acl({'can_pin_threads': 1})
+
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'posts': self.posts,
+                'title': '$$$',
+                'category': self.category.id,
+                'weight': 1,
+            }),
+            content_type="application/json",
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
-        response_json = json.loads(smart_str(response.content))
-        self.assertEqual(response_json, {
-            'title': ["Thread title should be at least 5 characters long (it has 3)."]
-        })
+        response_json = response.json()
+        self.assertEqual(
+            response_json, {
+                'title': ["Thread title should be at least 5 characters long (it has 3)."],
+            }
+        )
 
 
     def test_split_allowed_global_weight(self):
     def test_split_allowed_global_weight(self):
         """api allows global weight"""
         """api allows global weight"""
-        self.override_acl({
-            'can_pin_threads': 2
-        })
-
-        response = self.client.post(self.api_link, json.dumps({
-            'posts': self.posts,
-            'title': '$$$',
-            'category': self.category.id,
-            'weight': 2,
-        }), content_type="application/json")
+        self.override_acl({'can_pin_threads': 2})
+
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'posts': self.posts,
+                'title': '$$$',
+                'category': self.category.id,
+                'weight': 2,
+            }),
+            content_type="application/json",
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
-        response_json = json.loads(smart_str(response.content))
-        self.assertEqual(response_json, {
-            'title': ["Thread title should be at least 5 characters long (it has 3)."]
-        })
+        response_json = response.json()
+        self.assertEqual(
+            response_json, {
+                'title': ["Thread title should be at least 5 characters long (it has 3)."],
+            }
+        )
 
 
     def test_split_unallowed_close(self):
     def test_split_unallowed_close(self):
         """api rejects split because closing thread was unallowed"""
         """api rejects split because closing thread was unallowed"""
-        response = self.client.post(self.api_link, json.dumps({
-            'posts': self.posts,
-            'title': 'Valid thread title',
-            'category': self.category.id,
-            'is_closed': True,
-        }), content_type="application/json")
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'posts': self.posts,
+                'title': 'Valid thread title',
+                'category': self.category.id,
+                'is_closed': True,
+            }),
+            content_type="application/json",
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
-        response_json = json.loads(smart_str(response.content))
-        self.assertEqual(response_json, {
-            'is_closed': [
-                "You don't have permission to close threads in this category."
-            ]
-        })
+        response_json = response.json()
+        self.assertEqual(
+            response_json, {
+                'is_closed': ["You don't have permission to close threads in this category."],
+            }
+        )
 
 
     def test_split_with_close(self):
     def test_split_with_close(self):
         """api allows for closing thread"""
         """api allows for closing thread"""
-        self.override_acl({
-            'can_close_threads': True
-        })
-
-        response = self.client.post(self.api_link, json.dumps({
-            'posts': self.posts,
-            'title': '$$$',
-            'category': self.category.id,
-            'weight': 0,
-            'is_closed': True,
-        }), content_type="application/json")
+        self.override_acl({'can_close_threads': True})
+
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'posts': self.posts,
+                'title': '$$$',
+                'category': self.category.id,
+                'weight': 0,
+                'is_closed': True,
+            }),
+            content_type="application/json",
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
-        response_json = json.loads(smart_str(response.content))
-        self.assertEqual(response_json, {
-            'title': ["Thread title should be at least 5 characters long (it has 3)."]
-        })
+        response_json = response.json()
+        self.assertEqual(
+            response_json, {
+                'title': ["Thread title should be at least 5 characters long (it has 3)."],
+            }
+        )
 
 
     def test_split_unallowed_hidden(self):
     def test_split_unallowed_hidden(self):
         """api rejects split because hidden thread was unallowed"""
         """api rejects split because hidden thread was unallowed"""
-        response = self.client.post(self.api_link, json.dumps({
-            'posts': self.posts,
-            'title': 'Valid thread title',
-            'category': self.category.id,
-            'is_hidden': True,
-        }), content_type="application/json")
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'posts': self.posts,
+                'title': 'Valid thread title',
+                'category': self.category.id,
+                'is_hidden': True,
+            }),
+            content_type="application/json",
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
-        response_json = json.loads(smart_str(response.content))
-        self.assertEqual(response_json, {
-            'is_hidden': [
-                "You don't have permission to hide threads in this category."
-            ]
-        })
+        response_json = response.json()
+        self.assertEqual(
+            response_json, {
+                'is_hidden': ["You don't have permission to hide threads in this category."],
+            }
+        )
 
 
     def test_split_with_hide(self):
     def test_split_with_hide(self):
         """api allows for hiding thread"""
         """api allows for hiding thread"""
-        self.override_acl({
-            'can_hide_threads': True
-        })
-
-        response = self.client.post(self.api_link, json.dumps({
-            'posts': self.posts,
-            'title': '$$$',
-            'category': self.category.id,
-            'weight': 0,
-            'is_hidden': True,
-        }), content_type="application/json")
+        self.override_acl({'can_hide_threads': True})
+
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'posts': self.posts,
+                'title': '$$$',
+                'category': self.category.id,
+                'weight': 0,
+                'is_hidden': True,
+            }),
+            content_type="application/json",
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
-        response_json = json.loads(smart_str(response.content))
-        self.assertEqual(response_json, {
-            'title': ["Thread title should be at least 5 characters long (it has 3)."]
-        })
+        response_json = response.json()
+        self.assertEqual(
+            response_json, {
+                'title': ["Thread title should be at least 5 characters long (it has 3)."],
+            }
+        )
 
 
     def test_split(self):
     def test_split(self):
         """api splits posts to new thread"""
         """api splits posts to new thread"""
         self.refresh_thread()
         self.refresh_thread()
         self.assertEqual(self.thread.replies, 2)
         self.assertEqual(self.thread.replies, 2)
 
 
-        response = self.client.post(self.api_link, json.dumps({
-            'posts': self.posts,
-            'title': 'Split thread.',
-            'category': self.category.id
-        }), content_type="application/json")
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'posts': self.posts,
+                'title': 'Split thread.',
+                'category': self.category.id,
+            }),
+            content_type="application/json",
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         # thread was created
         # thread was created
@@ -440,17 +545,21 @@ class ThreadPostSplitApiTestCase(AuthenticatedUserTestCase):
             'can_start_threads': 2,
             'can_start_threads': 2,
             'can_close_threads': True,
             'can_close_threads': True,
             'can_hide_threads': True,
             'can_hide_threads': True,
-            'can_pin_threads': 2
+            'can_pin_threads': 2,
         })
         })
 
 
-        response = self.client.post(self.api_link, json.dumps({
-            'posts': self.posts,
-            'title': 'Split thread',
-            'category': self.category_b.id,
-            'weight': 2,
-            'is_closed': 1,
-            'is_hidden': 1
-        }), content_type="application/json")
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'posts': self.posts,
+                'title': 'Split thread',
+                'category': self.category_b.id,
+                'weight': 2,
+                'is_closed': 1,
+                'is_hidden': 1,
+            }),
+            content_type="application/json",
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         # thread was created
         # thread was created

+ 44 - 43
misago/threads/tests/test_thread_reply_api.py

@@ -1,10 +1,7 @@
 # -*- coding: utf-8 -*-
 # -*- coding: utf-8 -*-
 from __future__ import unicode_literals
 from __future__ import unicode_literals
 
 
-import json
-
 from django.urls import reverse
 from django.urls import reverse
-from django.utils.encoding import smart_str
 
 
 from misago.acl.testutils import override_acl
 from misago.acl.testutils import override_acl
 from misago.categories.models import Category
 from misago.categories.models import Category
@@ -20,9 +17,11 @@ class ReplyThreadTests(AuthenticatedUserTestCase):
         self.category = Category.objects.get(slug='first-category')
         self.category = Category.objects.get(slug='first-category')
         self.thread = testutils.post_thread(category=self.category)
         self.thread = testutils.post_thread(category=self.category)
 
 
-        self.api_link = reverse('misago:api:thread-post-list', kwargs={
-            'thread_pk': self.thread.pk
-        })
+        self.api_link = reverse(
+            'misago:api:thread-post-list', kwargs={
+                'thread_pk': self.thread.pk,
+            }
+        )
 
 
     def override_acl(self, extra_acl=None):
     def override_acl(self, extra_acl=None):
         new_acl = self.user.acl_cache
         new_acl = self.user.acl_cache
@@ -30,7 +29,7 @@ class ReplyThreadTests(AuthenticatedUserTestCase):
             'can_see': 1,
             'can_see': 1,
             'can_browse': 1,
             'can_browse': 1,
             'can_start_threads': 0,
             'can_start_threads': 0,
-            'can_reply_threads': 1
+            'can_reply_threads': 1,
         })
         })
 
 
         if extra_acl:
         if extra_acl:
@@ -61,49 +60,47 @@ class ReplyThreadTests(AuthenticatedUserTestCase):
 
 
     def test_cant_reply_thread(self):
     def test_cant_reply_thread(self):
         """permission to reply thread is validated"""
         """permission to reply thread is validated"""
-        self.override_acl({
-            'can_reply_threads': 0
-        })
+        self.override_acl({'can_reply_threads': 0})
 
 
         response = self.client.post(self.api_link)
         response = self.client.post(self.api_link)
-        self.assertContains(response, "You can't reply to threads in this category.", status_code=403)
+        self.assertContains(
+            response, "You can't reply to threads in this category.", status_code=403
+        )
 
 
     def test_closed_category(self):
     def test_closed_category(self):
         """permssion to reply in closed category is validated"""
         """permssion to reply in closed category is validated"""
-        self.override_acl({
-            'can_close_threads': 0
-        })
+        self.override_acl({'can_close_threads': 0})
 
 
         self.category.is_closed = True
         self.category.is_closed = True
         self.category.save()
         self.category.save()
 
 
         response = self.client.post(self.api_link)
         response = self.client.post(self.api_link)
-        self.assertContains(response, "This category is closed. You can't reply to threads in it.", status_code=403)
+        self.assertContains(
+            response,
+            "This category is closed. You can't reply to threads in it.",
+            status_code=403
+        )
 
 
         # allow to post in closed category
         # allow to post in closed category
-        self.override_acl({
-            'can_close_threads': 1
-        })
+        self.override_acl({'can_close_threads': 1})
 
 
         response = self.client.post(self.api_link)
         response = self.client.post(self.api_link)
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
     def test_closed_thread(self):
     def test_closed_thread(self):
         """permssion to reply in closed thread is validated"""
         """permssion to reply in closed thread is validated"""
-        self.override_acl({
-            'can_close_threads': 0
-        })
+        self.override_acl({'can_close_threads': 0})
 
 
         self.thread.is_closed = True
         self.thread.is_closed = True
         self.thread.save()
         self.thread.save()
 
 
         response = self.client.post(self.api_link)
         response = self.client.post(self.api_link)
-        self.assertContains(response, "You can't reply to closed threads in this category.", status_code=403)
+        self.assertContains(
+            response, "You can't reply to closed threads in this category.", status_code=403
+        )
 
 
         # allow to post in closed thread
         # allow to post in closed thread
-        self.override_acl({
-            'can_close_threads': 1
-        })
+        self.override_acl({'can_close_threads': 1})
 
 
         response = self.client.post(self.api_link)
         response = self.client.post(self.api_link)
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
@@ -114,33 +111,35 @@ class ReplyThreadTests(AuthenticatedUserTestCase):
 
 
         response = self.client.post(self.api_link, data={})
         response = self.client.post(self.api_link, data={})
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
-        self.assertEqual(json.loads(smart_str(response.content)), {
-            'post': [
-                "You have to enter a message."
-            ]
+        self.assertEqual(response.json(), {
+            'post': ["You have to enter a message."],
         })
         })
 
 
     def test_post_is_validated(self):
     def test_post_is_validated(self):
         """post is validated"""
         """post is validated"""
         self.override_acl()
         self.override_acl()
 
 
-        response = self.client.post(self.api_link, data={
-            'post': "a",
-        })
+        response = self.client.post(
+            self.api_link, data={
+                'post': "a",
+            }
+        )
 
 
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
-        self.assertEqual(json.loads(smart_str(response.content)), {
-            'post': [
-                "Posted message should be at least 5 characters long (it has 1)."
-            ]
-        })
+        self.assertEqual(
+            response.json(), {
+                'post': ["Posted message should be at least 5 characters long (it has 1)."],
+            }
+        )
 
 
     def test_can_reply_thread(self):
     def test_can_reply_thread(self):
         """endpoint creates new reply"""
         """endpoint creates new reply"""
         self.override_acl()
         self.override_acl()
-        response = self.client.post(self.api_link, data={
-            'post': "This is test response!"
-        })
+        response = self.client.post(
+            self.api_link, data={
+                'post': "This is test response!",
+            }
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         thread = Thread.objects.get(pk=self.thread.pk)
         thread = Thread.objects.get(pk=self.thread.pk)
@@ -178,7 +177,9 @@ class ReplyThreadTests(AuthenticatedUserTestCase):
         """unicode characters can be posted"""
         """unicode characters can be posted"""
         self.override_acl()
         self.override_acl()
 
 
-        response = self.client.post(self.api_link, data={
-            'post': "Chrzążczyżewoszyce, powiat Łękółody."
-        })
+        response = self.client.post(
+            self.api_link, data={
+                'post': "Chrzążczyżewoszyce, powiat Łękółody.",
+            }
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)

+ 139 - 109
misago/threads/tests/test_thread_start_api.py

@@ -4,10 +4,7 @@ from __future__ import unicode_literals
 from django.urls import reverse
 from django.urls import reverse
 
 
 from misago.acl.testutils import override_acl
 from misago.acl.testutils import override_acl
-from misago.categories import THREADS_ROOT_NAME
 from misago.categories.models import Category
 from misago.categories.models import Category
-from misago.threads.models import Thread
-from misago.threads.threadtypes import trees_map
 from misago.users.testutils import AuthenticatedUserTestCase
 from misago.users.testutils import AuthenticatedUserTestCase
 
 
 
 
@@ -15,8 +12,6 @@ class StartThreadTests(AuthenticatedUserTestCase):
     def setUp(self):
     def setUp(self):
         super(StartThreadTests, self).setUp()
         super(StartThreadTests, self).setUp()
 
 
-        threads_tree_id = trees_map.get_tree_id_for_root(THREADS_ROOT_NAME)
-
         self.category = Category.objects.get(slug='first-category')
         self.category = Category.objects.get(slug='first-category')
         self.api_link = reverse('misago:api:thread-list')
         self.api_link = reverse('misago:api:thread-list')
 
 
@@ -29,7 +24,7 @@ class StartThreadTests(AuthenticatedUserTestCase):
             'can_pin_threads': 0,
             'can_pin_threads': 0,
             'can_close_threads': 0,
             'can_close_threads': 0,
             'can_hide_threads': 0,
             'can_hide_threads': 0,
-            'can_hide_own_threads': 0
+            'can_hide_own_threads': 0,
         })
         })
 
 
         if extra_acl:
         if extra_acl:
@@ -56,7 +51,7 @@ class StartThreadTests(AuthenticatedUserTestCase):
         self.override_acl({'can_see': 0})
         self.override_acl({'can_see': 0})
 
 
         response = self.client.post(self.api_link, {
         response = self.client.post(self.api_link, {
-            'category': self.category.pk
+            'category': self.category.pk,
         })
         })
 
 
         self.assertContains(response, "Selected category is invalid.", status_code=400)
         self.assertContains(response, "Selected category is invalid.", status_code=400)
@@ -66,7 +61,7 @@ class StartThreadTests(AuthenticatedUserTestCase):
         self.override_acl({'can_browse': 0})
         self.override_acl({'can_browse': 0})
 
 
         response = self.client.post(self.api_link, {
         response = self.client.post(self.api_link, {
-            'category': self.category.pk
+            'category': self.category.pk,
         })
         })
 
 
         self.assertContains(response, "Selected category is invalid.", status_code=400)
         self.assertContains(response, "Selected category is invalid.", status_code=400)
@@ -76,10 +71,12 @@ class StartThreadTests(AuthenticatedUserTestCase):
         self.override_acl({'can_start_threads': 0})
         self.override_acl({'can_start_threads': 0})
 
 
         response = self.client.post(self.api_link, {
         response = self.client.post(self.api_link, {
-            'category': self.category.pk
+            'category': self.category.pk,
         })
         })
 
 
-        self.assertContains(response, "You don't have permission to start new threads", status_code=400)
+        self.assertContains(
+            response, "You don't have permission to start new threads", status_code=400
+        )
 
 
     def test_cant_start_thread_in_locked_category(self):
     def test_cant_start_thread_in_locked_category(self):
         """can't post in closed category"""
         """can't post in closed category"""
@@ -89,7 +86,7 @@ class StartThreadTests(AuthenticatedUserTestCase):
         self.override_acl({'can_close_threads': 0})
         self.override_acl({'can_close_threads': 0})
 
 
         response = self.client.post(self.api_link, {
         response = self.client.post(self.api_link, {
-            'category': self.category.pk
+            'category': self.category.pk,
         })
         })
 
 
         self.assertContains(response, "This category is closed.", status_code=400)
         self.assertContains(response, "This category is closed.", status_code=400)
@@ -101,9 +98,7 @@ class StartThreadTests(AuthenticatedUserTestCase):
 
 
         self.override_acl({'can_close_threads': 0})
         self.override_acl({'can_close_threads': 0})
 
 
-        response = self.client.post(self.api_link, {
-            'category': self.category.pk * 100000
-        })
+        response = self.client.post(self.api_link, {'category': self.category.pk * 100000})
 
 
         self.assertContains(response, "Selected category doesn't exist", status_code=400)
         self.assertContains(response, "Selected category doesn't exist", status_code=400)
 
 
@@ -113,60 +108,65 @@ class StartThreadTests(AuthenticatedUserTestCase):
 
 
         response = self.client.post(self.api_link, data={})
         response = self.client.post(self.api_link, data={})
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'category': [
-                "You have to select category to post thread in."
-            ],
-            'title':[
-                "You have to enter thread title."
-            ],
-            'post': [
-                "You have to enter a message."
-            ]
-        })
+        self.assertEqual(
+            response.json(), {
+                'category': ["You have to select category to post thread in."],
+                'title': ["You have to enter thread title."],
+                'post': ["You have to enter a message."],
+            }
+        )
 
 
     def test_title_is_validated(self):
     def test_title_is_validated(self):
         """title is validated"""
         """title is validated"""
         self.override_acl()
         self.override_acl()
 
 
-        response = self.client.post(self.api_link, data={
-            'category': self.category.pk,
-            'title': "------",
-            'post': "Lorem ipsum dolor met, sit amet elit!",
-        })
+        response = self.client.post(
+            self.api_link,
+            data={
+                'category': self.category.pk,
+                'title': "------",
+                'post': "Lorem ipsum dolor met, sit amet elit!",
+            }
+        )
 
 
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'title': [
-                "Thread title should contain alpha-numeric characters."
-            ]
-        })
+        self.assertEqual(
+            response.json(), {
+                'title': ["Thread title should contain alpha-numeric characters."],
+            }
+        )
 
 
     def test_post_is_validated(self):
     def test_post_is_validated(self):
         """post is validated"""
         """post is validated"""
         self.override_acl()
         self.override_acl()
 
 
-        response = self.client.post(self.api_link, data={
-            'category': self.category.pk,
-            'title': "Lorem ipsum dolor met",
-            'post': "a",
-        })
+        response = self.client.post(
+            self.api_link,
+            data={
+                'category': self.category.pk,
+                'title': "Lorem ipsum dolor met",
+                'post': "a",
+            }
+        )
 
 
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'post': [
-                "Posted message should be at least 5 characters long (it has 1)."
-            ]
-        })
+        self.assertEqual(
+            response.json(), {
+                'post': ["Posted message should be at least 5 characters long (it has 1)."],
+            }
+        )
 
 
     def test_can_start_thread(self):
     def test_can_start_thread(self):
         """endpoint creates new thread"""
         """endpoint creates new thread"""
         self.override_acl()
         self.override_acl()
-        response = self.client.post(self.api_link, data={
-            'category': self.category.pk,
-            'title': "Hello, I am test thread!",
-            'post': "Lorem ipsum dolor met!"
-        })
+        response = self.client.post(
+            self.api_link,
+            data={
+                'category': self.category.pk,
+                'title': "Hello, I am test thread!",
+                'post': "Lorem ipsum dolor met!",
+            }
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         thread = self.user.thread_set.all()[:1][0]
         thread = self.user.thread_set.all()[:1][0]
@@ -215,12 +215,15 @@ class StartThreadTests(AuthenticatedUserTestCase):
         """permission is checked before thread is closed"""
         """permission is checked before thread is closed"""
         self.override_acl({'can_close_threads': 0})
         self.override_acl({'can_close_threads': 0})
 
 
-        response = self.client.post(self.api_link, data={
-            'category': self.category.pk,
-            'title': "Hello, I am test thread!",
-            'post': "Lorem ipsum dolor met!",
-            'close': True
-        })
+        response = self.client.post(
+            self.api_link,
+            data={
+                'category': self.category.pk,
+                'title': "Hello, I am test thread!",
+                'post': "Lorem ipsum dolor met!",
+                'close': True,
+            }
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         thread = self.user.thread_set.all()[:1][0]
         thread = self.user.thread_set.all()[:1][0]
@@ -230,12 +233,15 @@ class StartThreadTests(AuthenticatedUserTestCase):
         """can post closed thread"""
         """can post closed thread"""
         self.override_acl({'can_close_threads': 1})
         self.override_acl({'can_close_threads': 1})
 
 
-        response = self.client.post(self.api_link, data={
-            'category': self.category.pk,
-            'title': "Hello, I am test thread!",
-            'post': "Lorem ipsum dolor met!",
-            'close': True
-        })
+        response = self.client.post(
+            self.api_link,
+            data={
+                'category': self.category.pk,
+                'title': "Hello, I am test thread!",
+                'post': "Lorem ipsum dolor met!",
+                'close': True,
+            }
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         thread = self.user.thread_set.all()[:1][0]
         thread = self.user.thread_set.all()[:1][0]
@@ -245,12 +251,15 @@ class StartThreadTests(AuthenticatedUserTestCase):
         """can post unpinned thread"""
         """can post unpinned thread"""
         self.override_acl({'can_pin_threads': 1})
         self.override_acl({'can_pin_threads': 1})
 
 
-        response = self.client.post(self.api_link, data={
-            'category': self.category.pk,
-            'title': "Hello, I am test thread!",
-            'post': "Lorem ipsum dolor met!",
-            'pin': 0
-        })
+        response = self.client.post(
+            self.api_link,
+            data={
+                'category': self.category.pk,
+                'title': "Hello, I am test thread!",
+                'post': "Lorem ipsum dolor met!",
+                'pin': 0,
+            }
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         thread = self.user.thread_set.all()[:1][0]
         thread = self.user.thread_set.all()[:1][0]
@@ -260,12 +269,15 @@ class StartThreadTests(AuthenticatedUserTestCase):
         """can post locally pinned thread"""
         """can post locally pinned thread"""
         self.override_acl({'can_pin_threads': 1})
         self.override_acl({'can_pin_threads': 1})
 
 
-        response = self.client.post(self.api_link, data={
-            'category': self.category.pk,
-            'title': "Hello, I am test thread!",
-            'post': "Lorem ipsum dolor met!",
-            'pin': 1
-        })
+        response = self.client.post(
+            self.api_link,
+            data={
+                'category': self.category.pk,
+                'title': "Hello, I am test thread!",
+                'post': "Lorem ipsum dolor met!",
+                'pin': 1,
+            }
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         thread = self.user.thread_set.all()[:1][0]
         thread = self.user.thread_set.all()[:1][0]
@@ -275,12 +287,15 @@ class StartThreadTests(AuthenticatedUserTestCase):
         """can post globally pinned thread"""
         """can post globally pinned thread"""
         self.override_acl({'can_pin_threads': 2})
         self.override_acl({'can_pin_threads': 2})
 
 
-        response = self.client.post(self.api_link, data={
-            'category': self.category.pk,
-            'title': "Hello, I am test thread!",
-            'post': "Lorem ipsum dolor met!",
-            'pin': 2
-        })
+        response = self.client.post(
+            self.api_link,
+            data={
+                'category': self.category.pk,
+                'title': "Hello, I am test thread!",
+                'post': "Lorem ipsum dolor met!",
+                'pin': 2,
+            }
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         thread = self.user.thread_set.all()[:1][0]
         thread = self.user.thread_set.all()[:1][0]
@@ -290,12 +305,15 @@ class StartThreadTests(AuthenticatedUserTestCase):
         """cant post globally pinned thread without permission"""
         """cant post globally pinned thread without permission"""
         self.override_acl({'can_pin_threads': 1})
         self.override_acl({'can_pin_threads': 1})
 
 
-        response = self.client.post(self.api_link, data={
-            'category': self.category.pk,
-            'title': "Hello, I am test thread!",
-            'post': "Lorem ipsum dolor met!",
-            'pin': 2
-        })
+        response = self.client.post(
+            self.api_link,
+            data={
+                'category': self.category.pk,
+                'title': "Hello, I am test thread!",
+                'post': "Lorem ipsum dolor met!",
+                'pin': 2,
+            }
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         thread = self.user.thread_set.all()[:1][0]
         thread = self.user.thread_set.all()[:1][0]
@@ -305,12 +323,15 @@ class StartThreadTests(AuthenticatedUserTestCase):
         """cant post locally pinned thread without permission"""
         """cant post locally pinned thread without permission"""
         self.override_acl({'can_pin_threads': 0})
         self.override_acl({'can_pin_threads': 0})
 
 
-        response = self.client.post(self.api_link, data={
-            'category': self.category.pk,
-            'title': "Hello, I am test thread!",
-            'post': "Lorem ipsum dolor met!",
-            'pin': 1
-        })
+        response = self.client.post(
+            self.api_link,
+            data={
+                'category': self.category.pk,
+                'title': "Hello, I am test thread!",
+                'post': "Lorem ipsum dolor met!",
+                'pin': 1,
+            }
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         thread = self.user.thread_set.all()[:1][0]
         thread = self.user.thread_set.all()[:1][0]
@@ -320,12 +341,15 @@ class StartThreadTests(AuthenticatedUserTestCase):
         """can post hidden thread"""
         """can post hidden thread"""
         self.override_acl({'can_hide_threads': 1})
         self.override_acl({'can_hide_threads': 1})
 
 
-        response = self.client.post(self.api_link, data={
-            'category': self.category.pk,
-            'title': "Hello, I am test thread!",
-            'post': "Lorem ipsum dolor met!",
-            'hide': 1
-        })
+        response = self.client.post(
+            self.api_link,
+            data={
+                'category': self.category.pk,
+                'title': "Hello, I am test thread!",
+                'post': "Lorem ipsum dolor met!",
+                'hide': 1,
+            }
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         thread = self.user.thread_set.all()[:1][0]
         thread = self.user.thread_set.all()[:1][0]
@@ -338,12 +362,15 @@ class StartThreadTests(AuthenticatedUserTestCase):
         """cant post hidden thread without permission"""
         """cant post hidden thread without permission"""
         self.override_acl({'can_hide_threads': 0})
         self.override_acl({'can_hide_threads': 0})
 
 
-        response = self.client.post(self.api_link, data={
-            'category': self.category.pk,
-            'title': "Hello, I am test thread!",
-            'post': "Lorem ipsum dolor met!",
-            'hide': 1
-        })
+        response = self.client.post(
+            self.api_link,
+            data={
+                'category': self.category.pk,
+                'title': "Hello, I am test thread!",
+                'post': "Lorem ipsum dolor met!",
+                'hide': 1,
+            }
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         thread = self.user.thread_set.all()[:1][0]
         thread = self.user.thread_set.all()[:1][0]
@@ -353,9 +380,12 @@ class StartThreadTests(AuthenticatedUserTestCase):
         """unicode characters can be posted"""
         """unicode characters can be posted"""
         self.override_acl()
         self.override_acl()
 
 
-        response = self.client.post(self.api_link, data={
-            'category': self.category.pk,
-            'title': "Brzęczyżczykiewicz",
-            'post': "Chrzążczyżewoszyce, powiat Łękółody."
-        })
+        response = self.client.post(
+            self.api_link,
+            data={
+                'category': self.category.pk,
+                'title': "Brzęczyżczykiewicz",
+                'post': "Chrzążczyżewoszyce, powiat Łękółody.",
+            }
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)

+ 3 - 4
misago/threads/tests/test_threadparticipant_model.py

@@ -21,7 +21,7 @@ class ThreadParticipantTests(TestCase):
             starter_slug='tester',
             starter_slug='tester',
             last_post_on=datetime,
             last_post_on=datetime,
             last_poster_name='Tester',
             last_poster_name='Tester',
-            last_poster_slug='tester'
+            last_poster_slug='tester',
         )
         )
 
 
         self.thread.set_title("Test thread")
         self.thread.set_title("Test thread")
@@ -36,7 +36,7 @@ class ThreadParticipantTests(TestCase):
             parsed="<p>Hello! I am test message!</p>",
             parsed="<p>Hello! I am test message!</p>",
             checksum="nope",
             checksum="nope",
             posted_on=datetime,
             posted_on=datetime,
-            updated_on=datetime
+            updated_on=datetime,
         )
         )
 
 
         self.thread.first_post = post
         self.thread.first_post = post
@@ -91,5 +91,4 @@ class ThreadParticipantTests(TestCase):
         self.assertEqual(self.thread.participants.count(), 1)
         self.assertEqual(self.thread.participants.count(), 1)
 
 
         with self.assertRaises(ThreadParticipant.DoesNotExist):
         with self.assertRaises(ThreadParticipant.DoesNotExist):
-            participant = ThreadParticipant.objects.get(
-                thread=self.thread, user=user)
+            ThreadParticipant.objects.get(thread=self.thread, user=user)

+ 38 - 52
misago/threads/tests/test_threads_api.py

@@ -33,7 +33,7 @@ class ThreadsApiTestCase(AuthenticatedUserTestCase):
             'can_edit_posts': 0,
             'can_edit_posts': 0,
             'can_hide_posts': 0,
             'can_hide_posts': 0,
             'can_hide_own_posts': 0,
             'can_hide_own_posts': 0,
-            'can_merge_threads': 0
+            'can_merge_threads': 0,
         })
         })
 
 
         if acl:
         if acl:
@@ -49,13 +49,15 @@ class ThreadsApiTestCase(AuthenticatedUserTestCase):
         if not final_acl['can_browse'] and self.category.pk in browseable_categories:
         if not final_acl['can_browse'] and self.category.pk in browseable_categories:
             browseable_categories.remove(self.category.pk)
             browseable_categories.remove(self.category.pk)
 
 
-        override_acl(self.user, {
-            'visible_categories': visible_categories,
-            'browseable_categories': browseable_categories,
-            'categories': {
-                self.category.pk: final_acl
+        override_acl(
+            self.user, {
+                'visible_categories': visible_categories,
+                'browseable_categories': browseable_categories,
+                'categories': {
+                    self.category.pk: final_acl,
+                },
             }
             }
-        })
+        )
 
 
     def get_thread_json(self):
     def get_thread_json(self):
         response = self.client.get(self.thread.get_api_url())
         response = self.client.get(self.thread.get_api_url())
@@ -92,9 +94,7 @@ class ThreadRetrieveApiTests(ThreadsApiTestCase):
     def test_api_shows_owned_thread(self):
     def test_api_shows_owned_thread(self):
         """api handles "owned threads only"""
         """api handles "owned threads only"""
         for link in self.tested_links:
         for link in self.tested_links:
-            self.override_acl({
-                'can_see_all_threads': 0
-            })
+            self.override_acl({'can_see_all_threads': 0})
 
 
             response = self.client.get(link)
             response = self.client.get(link)
             self.assertEqual(response.status_code, 404)
             self.assertEqual(response.status_code, 404)
@@ -103,9 +103,7 @@ class ThreadRetrieveApiTests(ThreadsApiTestCase):
         self.thread.save()
         self.thread.save()
 
 
         for link in self.tested_links:
         for link in self.tested_links:
-            self.override_acl({
-                'can_see_all_threads': 0
-            })
+            self.override_acl({'can_see_all_threads': 0})
 
 
             response = self.client.get(link)
             response = self.client.get(link)
             self.assertEqual(response.status_code, 200)
             self.assertEqual(response.status_code, 200)
@@ -113,9 +111,7 @@ class ThreadRetrieveApiTests(ThreadsApiTestCase):
     def test_api_validates_category_see_permission(self):
     def test_api_validates_category_see_permission(self):
         """api validates category visiblity"""
         """api validates category visiblity"""
         for link in self.tested_links:
         for link in self.tested_links:
-            self.override_acl({
-                'can_see': 0
-            })
+            self.override_acl({'can_see': 0})
 
 
             response = self.client.get(link)
             response = self.client.get(link)
             self.assertEqual(response.status_code, 404)
             self.assertEqual(response.status_code, 404)
@@ -123,46 +119,45 @@ class ThreadRetrieveApiTests(ThreadsApiTestCase):
     def test_api_validates_category_browse_permission(self):
     def test_api_validates_category_browse_permission(self):
         """api validates category browsability"""
         """api validates category browsability"""
         for link in self.tested_links:
         for link in self.tested_links:
-            self.override_acl({
-                'can_browse': 0
-            })
+            self.override_acl({'can_browse': 0})
 
 
             response = self.client.get(link)
             response = self.client.get(link)
             self.assertEqual(response.status_code, 404)
             self.assertEqual(response.status_code, 404)
 
 
     def test_api_validates_posts_visibility(self):
     def test_api_validates_posts_visibility(self):
         """api validates posts visiblity"""
         """api validates posts visiblity"""
-        self.override_acl({
-            'can_hide_posts': 0
-        })
+        self.override_acl({'can_hide_posts': 0})
 
 
-        hidden_post = testutils.reply_thread(self.thread, is_hidden=True, message="I'am hidden test message!")
+        hidden_post = testutils.reply_thread(
+            self.thread,
+            is_hidden=True,
+            message="I'am hidden test message!",
+        )
 
 
         response = self.client.get(self.tested_links[1])
         response = self.client.get(self.tested_links[1])
-        self.assertNotContains(response, hidden_post.parsed) # post's body is hidden
+        self.assertNotContains(response, hidden_post.parsed)  # post's body is hidden
 
 
         # add permission to see hidden posts
         # add permission to see hidden posts
-        self.override_acl({
-            'can_hide_posts': 1
-        })
+        self.override_acl({'can_hide_posts': 1})
 
 
         response = self.client.get(self.tested_links[1])
         response = self.client.get(self.tested_links[1])
-        self.assertContains(response, hidden_post.parsed) # hidden post's body is visible with permission
+        self.assertContains(
+            response, hidden_post.parsed
+        )  # hidden post's body is visible with permission
 
 
-        self.override_acl({
-            'can_approve_content': 0
-        })
+        self.override_acl({'can_approve_content': 0})
 
 
         # unapproved posts shouldn't show at all
         # unapproved posts shouldn't show at all
-        unapproved_post = testutils.reply_thread(self.thread, is_unapproved=True)
+        unapproved_post = testutils.reply_thread(
+            self.thread,
+            is_unapproved=True,
+        )
 
 
         response = self.client.get(self.tested_links[1])
         response = self.client.get(self.tested_links[1])
         self.assertNotContains(response, unapproved_post.get_absolute_url())
         self.assertNotContains(response, unapproved_post.get_absolute_url())
 
 
         # add permission to see unapproved posts
         # add permission to see unapproved posts
-        self.override_acl({
-            'can_approve_content': 1
-        })
+        self.override_acl({'can_approve_content': 1})
 
 
         response = self.client.get(self.tested_links[1])
         response = self.client.get(self.tested_links[1])
         self.assertContains(response, unapproved_post.get_absolute_url())
         self.assertContains(response, unapproved_post.get_absolute_url())
@@ -189,18 +184,14 @@ class ThreadsReadApiTests(ThreadsApiTestCase):
 
 
     def test_read_category_no_see(self):
     def test_read_category_no_see(self):
         """api validates permission to see category"""
         """api validates permission to see category"""
-        self.override_acl({
-            'can_see': 0
-        })
+        self.override_acl({'can_see': 0})
 
 
         response = self.client.post(self.api_link)
         response = self.client.post(self.api_link)
         self.assertEqual(response.status_code, 404)
         self.assertEqual(response.status_code, 404)
 
 
     def test_read_category_no_browse(self):
     def test_read_category_no_browse(self):
         """api validates permission to browse category"""
         """api validates permission to browse category"""
-        self.override_acl({
-            'can_browse': 0
-        })
+        self.override_acl({'can_browse': 0})
 
 
         response = self.client.post(self.api_link)
         response = self.client.post(self.api_link)
         self.assertEqual(response.status_code, 403)
         self.assertEqual(response.status_code, 403)
@@ -229,29 +220,24 @@ class ThreadsReadApiTests(ThreadsApiTestCase):
 class ThreadDeleteApiTests(ThreadsApiTestCase):
 class ThreadDeleteApiTests(ThreadsApiTestCase):
     def test_delete_thread_no_permission(self):
     def test_delete_thread_no_permission(self):
         """DELETE to API link with no permission to delete fails"""
         """DELETE to API link with no permission to delete fails"""
-        self.override_acl({
-            'can_hide_threads': 1
-        })
+        self.override_acl({'can_hide_threads': 1})
 
 
         response = self.client.delete(self.api_link)
         response = self.client.delete(self.api_link)
         self.assertEqual(response.status_code, 403)
         self.assertEqual(response.status_code, 403)
 
 
-        self.override_acl({
-            'can_hide_threads': 0
-        })
+        self.override_acl({'can_hide_threads': 0})
 
 
         response_json = response.json()
         response_json = response.json()
-        self.assertEqual(response_json['detail'],
-            "You don't have permission to delete this thread.")
+        self.assertEqual(
+            response_json['detail'], "You don't have permission to delete this thread."
+        )
 
 
         response = self.client.delete(self.api_link)
         response = self.client.delete(self.api_link)
         self.assertEqual(response.status_code, 403)
         self.assertEqual(response.status_code, 403)
 
 
     def test_delete_thread(self):
     def test_delete_thread(self):
         """DELETE to API link with permission deletes thread"""
         """DELETE to API link with permission deletes thread"""
-        self.override_acl({
-            'can_hide_threads': 2
-        })
+        self.override_acl({'can_hide_threads': 2})
 
 
         response = self.client.delete(self.api_link)
         response = self.client.delete(self.api_link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)

+ 204 - 248
misago/threads/tests/test_threads_editor_api.py

@@ -1,8 +1,6 @@
-import json
 import os
 import os
 
 
 from django.urls import reverse
 from django.urls import reverse
-from django.utils.encoding import smart_str
 
 
 from misago.acl import add_acl
 from misago.acl import add_acl
 from misago.acl.testutils import override_acl
 from misago.acl.testutils import override_acl
@@ -61,12 +59,14 @@ class EditorApiTestCase(AuthenticatedUserTestCase):
         if final_acl['can_browse']:
         if final_acl['can_browse']:
             browseable_categories.append(self.category.pk)
             browseable_categories.append(self.category.pk)
 
 
-        override_acl(self.user, {
-            'browseable_categories': browseable_categories,
-            'categories': {
-                self.category.pk: final_acl
+        override_acl(
+            self.user, {
+                'browseable_categories': browseable_categories,
+                'categories': {
+                    self.category.pk: final_acl,
+                },
             }
             }
-        })
+        )
 
 
 
 
 class ThreadPostEditorApiTests(EditorApiTestCase):
 class ThreadPostEditorApiTests(EditorApiTestCase):
@@ -91,19 +91,14 @@ class ThreadPostEditorApiTests(EditorApiTestCase):
 
 
     def test_category_disallowing_new_threads(self):
     def test_category_disallowing_new_threads(self):
         """endpoint omits category disallowing starting threads"""
         """endpoint omits category disallowing starting threads"""
-        self.override_acl({
-            'can_start_threads': 0,
-        })
+        self.override_acl({'can_start_threads': 0})
 
 
         response = self.client.get(self.api_link)
         response = self.client.get(self.api_link)
         self.assertContains(response, "No categories that allow new threads", status_code=403)
         self.assertContains(response, "No categories that allow new threads", status_code=403)
 
 
     def test_category_closed_disallowing_new_threads(self):
     def test_category_closed_disallowing_new_threads(self):
         """endpoint omits closed category"""
         """endpoint omits closed category"""
-        self.override_acl({
-            'can_start_threads': 2,
-            'can_close_threads': 0,
-        })
+        self.override_acl({'can_start_threads': 2, 'can_close_threads': 0})
 
 
         self.category.is_closed = True
         self.category.is_closed = True
         self.category.save()
         self.category.save()
@@ -113,10 +108,7 @@ class ThreadPostEditorApiTests(EditorApiTestCase):
 
 
     def test_category_closed_allowing_new_threads(self):
     def test_category_closed_allowing_new_threads(self):
         """endpoint adds closed category that allows new threads"""
         """endpoint adds closed category that allows new threads"""
-        self.override_acl({
-            'can_start_threads': 2,
-            'can_close_threads': 1,
-        })
+        self.override_acl({'can_start_threads': 2, 'can_close_threads': 1})
 
 
         self.category.is_closed = True
         self.category.is_closed = True
         self.category.save()
         self.category.save()
@@ -124,146 +116,143 @@ class ThreadPostEditorApiTests(EditorApiTestCase):
         response = self.client.get(self.api_link)
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response_json = json.loads(smart_str(response.content))
-        self.assertEqual(response_json[0], {
-            'id': self.category.pk,
-            'name': self.category.name,
-            'level': 0,
-            'post': {
-                'close': True,
-                'hide': False,
-                'pin': 0
+        response_json = response.json()
+        self.assertEqual(
+            response_json[0], {
+                'id': self.category.pk,
+                'name': self.category.name,
+                'level': 0,
+                'post': {
+                    'close': True,
+                    'hide': False,
+                    'pin': 0,
+                },
             }
             }
-        })
+        )
 
 
     def test_category_allowing_new_threads(self):
     def test_category_allowing_new_threads(self):
         """endpoint adds category that allows new threads"""
         """endpoint adds category that allows new threads"""
-        self.override_acl({
-            'can_start_threads': 2,
-        })
+        self.override_acl({'can_start_threads': 2})
 
 
         response = self.client.get(self.api_link)
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response_json = json.loads(smart_str(response.content))
-        self.assertEqual(response_json[0], {
-            'id': self.category.pk,
-            'name': self.category.name,
-            'level': 0,
-            'post': {
-                'close': False,
-                'hide': False,
-                'pin': 0
+        response_json = response.json()
+        self.assertEqual(
+            response_json[0], {
+                'id': self.category.pk,
+                'name': self.category.name,
+                'level': 0,
+                'post': {
+                    'close': False,
+                    'hide': False,
+                    'pin': 0,
+                },
             }
             }
-        })
+        )
 
 
     def test_category_allowing_closing_threads(self):
     def test_category_allowing_closing_threads(self):
         """endpoint adds category that allows new closed threads"""
         """endpoint adds category that allows new closed threads"""
-        self.override_acl({
-            'can_start_threads': 2,
-            'can_close_threads': 1,
-        })
+        self.override_acl({'can_start_threads': 2, 'can_close_threads': 1})
 
 
         response = self.client.get(self.api_link)
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response_json = json.loads(smart_str(response.content))
-        self.assertEqual(response_json[0], {
-            'id': self.category.pk,
-            'name': self.category.name,
-            'level': 0,
-            'post': {
-                'close': True,
-                'hide': False,
-                'pin': 0
+        response_json = response.json()
+        self.assertEqual(
+            response_json[0], {
+                'id': self.category.pk,
+                'name': self.category.name,
+                'level': 0,
+                'post': {
+                    'close': True,
+                    'hide': False,
+                    'pin': 0,
+                },
             }
             }
-        })
+        )
 
 
     def test_category_allowing_locally_pinned_threads(self):
     def test_category_allowing_locally_pinned_threads(self):
         """endpoint adds category that allows locally pinned threads"""
         """endpoint adds category that allows locally pinned threads"""
-        self.override_acl({
-            'can_start_threads': 2,
-            'can_pin_threads': 1,
-        })
+        self.override_acl({'can_start_threads': 2, 'can_pin_threads': 1})
 
 
         response = self.client.get(self.api_link)
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response_json = json.loads(smart_str(response.content))
-        self.assertEqual(response_json[0], {
-            'id': self.category.pk,
-            'name': self.category.name,
-            'level': 0,
-            'post': {
-                'close': False,
-                'hide': False,
-                'pin': 1
+        response_json = response.json()
+        self.assertEqual(
+            response_json[0], {
+                'id': self.category.pk,
+                'name': self.category.name,
+                'level': 0,
+                'post': {
+                    'close': False,
+                    'hide': False,
+                    'pin': 1,
+                },
             }
             }
-        })
+        )
 
 
     def test_category_allowing_globally_pinned_threads(self):
     def test_category_allowing_globally_pinned_threads(self):
         """endpoint adds category that allows globally pinned threads"""
         """endpoint adds category that allows globally pinned threads"""
-        self.override_acl({
-            'can_start_threads': 2,
-            'can_pin_threads': 2,
-        })
+        self.override_acl({'can_start_threads': 2, 'can_pin_threads': 2})
 
 
         response = self.client.get(self.api_link)
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response_json = json.loads(smart_str(response.content))
-        self.assertEqual(response_json[0], {
-            'id': self.category.pk,
-            'name': self.category.name,
-            'level': 0,
-            'post': {
-                'close': False,
-                'hide': False,
-                'pin': 2
+        response_json = response.json()
+        self.assertEqual(
+            response_json[0], {
+                'id': self.category.pk,
+                'name': self.category.name,
+                'level': 0,
+                'post': {
+                    'close': False,
+                    'hide': False,
+                    'pin': 2,
+                },
             }
             }
-        })
+        )
 
 
     def test_category_allowing_hidden_threads(self):
     def test_category_allowing_hidden_threads(self):
         """endpoint adds category that allows globally pinned threads"""
         """endpoint adds category that allows globally pinned threads"""
-        self.override_acl({
-            'can_start_threads': 2,
-            'can_hide_threads': 1,
-        })
+        self.override_acl({'can_start_threads': 2, 'can_hide_threads': 1})
 
 
         response = self.client.get(self.api_link)
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response_json = json.loads(smart_str(response.content))
-        self.assertEqual(response_json[0], {
-            'id': self.category.pk,
-            'name': self.category.name,
-            'level': 0,
-            'post': {
-                'close': 0,
-                'hide': 1,
-                'pin': 0
+        response_json = response.json()
+        self.assertEqual(
+            response_json[0], {
+                'id': self.category.pk,
+                'name': self.category.name,
+                'level': 0,
+                'post': {
+                    'close': 0,
+                    'hide': 1,
+                    'pin': 0,
+                },
             }
             }
-        })
+        )
 
 
-        self.override_acl({
-            'can_start_threads': 2,
-            'can_hide_threads': 2,
-        })
+        self.override_acl({'can_start_threads': 2, 'can_hide_threads': 2})
 
 
         response = self.client.get(self.api_link)
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response_json = json.loads(smart_str(response.content))
-        self.assertEqual(response_json[0], {
-            'id': self.category.pk,
-            'name': self.category.name,
-            'level': 0,
-            'post': {
-                'close': False,
-                'hide': True,
-                'pin': 0
+        response_json = response.json()
+        self.assertEqual(
+            response_json[0], {
+                'id': self.category.pk,
+                'name': self.category.name,
+                'level': 0,
+                'post': {
+                    'close': False,
+                    'hide': True,
+                    'pin': 0,
+                },
             }
             }
-        })
+        )
 
 
 
 
 class ThreadReplyEditorApiTests(EditorApiTestCase):
 class ThreadReplyEditorApiTests(EditorApiTestCase):
@@ -271,9 +260,11 @@ class ThreadReplyEditorApiTests(EditorApiTestCase):
         super(ThreadReplyEditorApiTests, self).setUp()
         super(ThreadReplyEditorApiTests, self).setUp()
 
 
         self.thread = testutils.post_thread(category=self.category)
         self.thread = testutils.post_thread(category=self.category)
-        self.api_link = reverse('misago:api:thread-post-editor', kwargs={
-            'thread_pk': self.thread.pk
-        })
+        self.api_link = reverse(
+            'misago:api:thread-post-editor', kwargs={
+                'thread_pk': self.thread.pk,
+            }
+        )
 
 
     def test_anonymous_user_request(self):
     def test_anonymous_user_request(self):
         """endpoint validates if user is authenticated"""
         """endpoint validates if user is authenticated"""
@@ -298,82 +289,73 @@ class ThreadReplyEditorApiTests(EditorApiTestCase):
 
 
     def test_no_reply_permission(self):
     def test_no_reply_permission(self):
         """permssion to reply is validated"""
         """permssion to reply is validated"""
-        self.override_acl({
-            'can_reply_threads': 0
-        })
+        self.override_acl({'can_reply_threads': 0})
 
 
         response = self.client.get(self.api_link)
         response = self.client.get(self.api_link)
-        self.assertContains(response, "You can't reply to threads in this category.", status_code=403)
+        self.assertContains(
+            response, "You can't reply to threads in this category.", status_code=403
+        )
 
 
     def test_closed_category(self):
     def test_closed_category(self):
         """permssion to reply in closed category is validated"""
         """permssion to reply in closed category is validated"""
-        self.override_acl({
-            'can_reply_threads': 1,
-            'can_close_threads': 0
-        })
+        self.override_acl({'can_reply_threads': 1, 'can_close_threads': 0})
 
 
         self.category.is_closed = True
         self.category.is_closed = True
         self.category.save()
         self.category.save()
 
 
         response = self.client.get(self.api_link)
         response = self.client.get(self.api_link)
-        self.assertContains(response, "This category is closed. You can't reply to threads in it.", status_code=403)
+        self.assertContains(
+            response,
+            "This category is closed. You can't reply to threads in it.",
+            status_code=403
+        )
 
 
         # allow to post in closed category
         # allow to post in closed category
-        self.override_acl({
-            'can_reply_threads': 1,
-            'can_close_threads': 1
-        })
+        self.override_acl({'can_reply_threads': 1, 'can_close_threads': 1})
 
 
         response = self.client.get(self.api_link)
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
     def test_closed_thread(self):
     def test_closed_thread(self):
         """permssion to reply in closed thread is validated"""
         """permssion to reply in closed thread is validated"""
-        self.override_acl({
-            'can_reply_threads': 1,
-            'can_close_threads': 0
-        })
+        self.override_acl({'can_reply_threads': 1, 'can_close_threads': 0})
 
 
         self.thread.is_closed = True
         self.thread.is_closed = True
         self.thread.save()
         self.thread.save()
 
 
         response = self.client.get(self.api_link)
         response = self.client.get(self.api_link)
-        self.assertContains(response, "You can't reply to closed threads in this category.", status_code=403)
+        self.assertContains(
+            response, "You can't reply to closed threads in this category.", status_code=403
+        )
 
 
         # allow to post in closed thread
         # allow to post in closed thread
-        self.override_acl({
-            'can_reply_threads': 1,
-            'can_close_threads': 1
-        })
+        self.override_acl({'can_reply_threads': 1, 'can_close_threads': 1})
 
 
         response = self.client.get(self.api_link)
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
     def test_allow_reply_thread(self):
     def test_allow_reply_thread(self):
         """api returns 200 code if thread reply is allowed"""
         """api returns 200 code if thread reply is allowed"""
-        self.override_acl({
-            'can_reply_threads': 1
-        })
+        self.override_acl({'can_reply_threads': 1})
 
 
         response = self.client.get(self.api_link)
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
     def test_reply_to_visibility(self):
     def test_reply_to_visibility(self):
         """api validates replied post visibility"""
         """api validates replied post visibility"""
-        self.override_acl({
-            'can_reply_threads': 1
-        })
+        self.override_acl({'can_reply_threads': 1})
 
 
         # unapproved reply can't be replied to
         # unapproved reply can't be replied to
-        unapproved_reply = testutils.reply_thread(self.thread, is_unapproved=True)
+        unapproved_reply = testutils.reply_thread(
+            self.thread,
+            is_unapproved=True,
+        )
 
 
         response = self.client.get('{}?reply={}'.format(self.api_link, unapproved_reply.pk))
         response = self.client.get('{}?reply={}'.format(self.api_link, unapproved_reply.pk))
         self.assertEqual(response.status_code, 404)
         self.assertEqual(response.status_code, 404)
 
 
         # hidden reply can't be replied to
         # hidden reply can't be replied to
-        self.override_acl({
-            'can_reply_threads': 1
-        })
+        self.override_acl({'can_reply_threads': 1})
 
 
         hidden_reply = testutils.reply_thread(self.thread, is_hidden=True)
         hidden_reply = testutils.reply_thread(self.thread, is_hidden=True)
 
 
@@ -390,9 +372,7 @@ class ThreadReplyEditorApiTests(EditorApiTestCase):
 
 
     def test_reply_to_event(self):
     def test_reply_to_event(self):
         """events can't be edited"""
         """events can't be edited"""
-        self.override_acl({
-            'can_reply_threads': 1
-        })
+        self.override_acl({'can_reply_threads': 1})
 
 
         reply_to = testutils.reply_thread(self.thread, is_event=True)
         reply_to = testutils.reply_thread(self.thread, is_event=True)
 
 
@@ -402,20 +382,20 @@ class ThreadReplyEditorApiTests(EditorApiTestCase):
 
 
     def test_reply_to(self):
     def test_reply_to(self):
         """api includes replied to post details in response"""
         """api includes replied to post details in response"""
-        self.override_acl({
-            'can_reply_threads': 1
-        })
+        self.override_acl({'can_reply_threads': 1})
 
 
         reply_to = testutils.reply_thread(self.thread)
         reply_to = testutils.reply_thread(self.thread)
 
 
         response = self.client.get('{}?reply={}'.format(self.api_link, reply_to.pk))
         response = self.client.get('{}?reply={}'.format(self.api_link, reply_to.pk))
 
 
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
-        self.assertEqual(json.loads(smart_str(response.content)), {
-            'id': reply_to.pk,
-            'post': reply_to.original,
-            'poster': reply_to.poster_name
-        })
+        self.assertEqual(
+            response.json(), {
+                'id': reply_to.pk,
+                'post': reply_to.original,
+                'poster': reply_to.poster_name,
+            }
+        )
 
 
 
 
 class EditReplyEditorApiTests(EditorApiTestCase):
 class EditReplyEditorApiTests(EditorApiTestCase):
@@ -425,10 +405,13 @@ class EditReplyEditorApiTests(EditorApiTestCase):
         self.thread = testutils.post_thread(category=self.category)
         self.thread = testutils.post_thread(category=self.category)
         self.post = testutils.reply_thread(self.thread, poster=self.user)
         self.post = testutils.reply_thread(self.thread, poster=self.user)
 
 
-        self.api_link = reverse('misago:api:thread-post-editor', kwargs={
-            'thread_pk': self.thread.pk,
-            'pk': self.post.pk
-        })
+        self.api_link = reverse(
+            'misago:api:thread-post-editor',
+            kwargs={
+                'thread_pk': self.thread.pk,
+                'pk': self.post.pk,
+            }
+        )
 
 
     def test_anonymous_user_request(self):
     def test_anonymous_user_request(self):
         """endpoint validates if user is authenticated"""
         """endpoint validates if user is authenticated"""
@@ -453,121 +436,96 @@ class EditReplyEditorApiTests(EditorApiTestCase):
 
 
     def test_no_edit_permission(self):
     def test_no_edit_permission(self):
         """permssion to edit is validated"""
         """permssion to edit is validated"""
-        self.override_acl({
-            'can_edit_posts': 0
-        })
+        self.override_acl({'can_edit_posts': 0})
 
 
         response = self.client.get(self.api_link)
         response = self.client.get(self.api_link)
         self.assertContains(response, "You can't edit posts in this category.", status_code=403)
         self.assertContains(response, "You can't edit posts in this category.", status_code=403)
 
 
     def test_closed_category(self):
     def test_closed_category(self):
         """permssion to edit in closed category is validated"""
         """permssion to edit in closed category is validated"""
-        self.override_acl({
-            'can_edit_posts': 1,
-            'can_close_threads': 0
-        })
+        self.override_acl({'can_edit_posts': 1, 'can_close_threads': 0})
 
 
         self.category.is_closed = True
         self.category.is_closed = True
         self.category.save()
         self.category.save()
 
 
         response = self.client.get(self.api_link)
         response = self.client.get(self.api_link)
-        self.assertContains(response, "This category is closed. You can't edit posts in it.", status_code=403)
+        self.assertContains(
+            response, "This category is closed. You can't edit posts in it.", status_code=403
+        )
 
 
         # allow to edit in closed category
         # allow to edit in closed category
-        self.override_acl({
-            'can_edit_posts': 1,
-            'can_close_threads': 1
-        })
+        self.override_acl({'can_edit_posts': 1, 'can_close_threads': 1})
 
 
         response = self.client.get(self.api_link)
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
     def test_closed_thread(self):
     def test_closed_thread(self):
         """permssion to edit in closed thread is validated"""
         """permssion to edit in closed thread is validated"""
-        self.override_acl({
-            'can_edit_posts': 1,
-            'can_close_threads': 0
-        })
+        self.override_acl({'can_edit_posts': 1, 'can_close_threads': 0})
 
 
         self.thread.is_closed = True
         self.thread.is_closed = True
         self.thread.save()
         self.thread.save()
 
 
         response = self.client.get(self.api_link)
         response = self.client.get(self.api_link)
-        self.assertContains(response, "This thread is closed. You can't edit posts in it.", status_code=403)
+        self.assertContains(
+            response, "This thread is closed. You can't edit posts in it.", status_code=403
+        )
 
 
         # allow to edit in closed thread
         # allow to edit in closed thread
-        self.override_acl({
-            'can_edit_posts': 1,
-            'can_close_threads': 1
-        })
+        self.override_acl({'can_edit_posts': 1, 'can_close_threads': 1})
 
 
         response = self.client.get(self.api_link)
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
     def test_protected_post(self):
     def test_protected_post(self):
         """permssion to edit protected post is validated"""
         """permssion to edit protected post is validated"""
-        self.override_acl({
-            'can_edit_posts': 1,
-            'can_protect_posts': 0
-        })
+        self.override_acl({'can_edit_posts': 1, 'can_protect_posts': 0})
 
 
         self.post.is_protected = True
         self.post.is_protected = True
         self.post.save()
         self.post.save()
 
 
         response = self.client.get(self.api_link)
         response = self.client.get(self.api_link)
-        self.assertContains(response, "This post is protected. You can't edit it.", status_code=403)
+        self.assertContains(
+            response, "This post is protected. You can't edit it.", status_code=403
+        )
 
 
         # allow to post in closed thread
         # allow to post in closed thread
-        self.override_acl({
-            'can_edit_posts': 1,
-            'can_protect_posts': 1
-        })
+        self.override_acl({'can_edit_posts': 1, 'can_protect_posts': 1})
 
 
         response = self.client.get(self.api_link)
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
     def test_post_visibility(self):
     def test_post_visibility(self):
         """edited posts visibility is validated"""
         """edited posts visibility is validated"""
-        self.override_acl({
-            'can_edit_posts': 1
-        })
+        self.override_acl({'can_edit_posts': 1})
 
 
-        self.post.is_hidden = True;
+        self.post.is_hidden = True
         self.post.save()
         self.post.save()
 
 
         response = self.client.get(self.api_link)
         response = self.client.get(self.api_link)
         self.assertContains(response, "This post is hidden, you can't edit it.", status_code=403)
         self.assertContains(response, "This post is hidden, you can't edit it.", status_code=403)
 
 
         # allow hidden edition
         # allow hidden edition
-        self.override_acl({
-            'can_edit_posts': 1,
-            'can_hide_posts': 1
-        })
+        self.override_acl({'can_edit_posts': 1, 'can_hide_posts': 1})
 
 
         response = self.client.get(self.api_link)
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         # test unapproved post
         # test unapproved post
-        self.post.is_hidden = False;
-        self.post.poster = None;
+        self.post.is_hidden = False
+        self.post.poster = None
         self.post.save()
         self.post.save()
 
 
-        self.override_acl({
-            'can_edit_posts': 2,
-            'can_approve_content': 0
-        })
+        self.override_acl({'can_edit_posts': 2, 'can_approve_content': 0})
 
 
-        self.post.is_unapproved = True;
+        self.post.is_unapproved = True
         self.post.save()
         self.post.save()
 
 
         response = self.client.get(self.api_link)
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 404)
         self.assertEqual(response.status_code, 404)
 
 
         # allow unapproved edition
         # allow unapproved edition
-        self.override_acl({
-            'can_edit_posts': 2,
-            'can_approve_content': 1
-        })
+        self.override_acl({'can_edit_posts': 2, 'can_approve_content': 1})
 
 
         response = self.client.get(self.api_link)
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
@@ -585,55 +543,53 @@ class EditReplyEditorApiTests(EditorApiTestCase):
 
 
     def test_other_user_post(self):
     def test_other_user_post(self):
         """api validates if other user's post can be edited"""
         """api validates if other user's post can be edited"""
-        self.override_acl({
-            'can_edit_posts': 1,
-        })
+        self.override_acl({'can_edit_posts': 1})
 
 
-        self.post.poster = None;
+        self.post.poster = None
         self.post.save()
         self.post.save()
 
 
         response = self.client.get(self.api_link)
         response = self.client.get(self.api_link)
-        self.assertContains(response, "You can't edit other users posts in this category.", status_code=403)
+        self.assertContains(
+            response, "You can't edit other users posts in this category.", status_code=403
+        )
 
 
         # allow other users post edition
         # allow other users post edition
-        self.override_acl({
-            'can_edit_posts': 2,
-        })
+        self.override_acl({'can_edit_posts': 2})
 
 
         response = self.client.get(self.api_link)
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
     def test_edit_first_post_hidden(self):
     def test_edit_first_post_hidden(self):
         """endpoint returns valid configuration for editor of hidden thread's first post"""
         """endpoint returns valid configuration for editor of hidden thread's first post"""
-        self.override_acl({
-            'can_hide_threads': 1,
-            'can_edit_posts': 2
-        })
+        self.override_acl({'can_hide_threads': 1, 'can_edit_posts': 2})
 
 
         self.thread.is_hidden = True
         self.thread.is_hidden = True
         self.thread.save()
         self.thread.save()
         self.thread.first_post.is_hidden = True
         self.thread.first_post.is_hidden = True
         self.thread.first_post.save()
         self.thread.first_post.save()
 
 
-        api_link = reverse('misago:api:thread-post-editor', kwargs={
-            'thread_pk': self.thread.pk,
-            'pk': self.thread.first_post.pk
-        })
+        api_link = reverse(
+            'misago:api:thread-post-editor',
+            kwargs={
+                'thread_pk': self.thread.pk,
+                'pk': self.thread.first_post.pk,
+            }
+        )
 
 
         response = self.client.get(api_link)
         response = self.client.get(api_link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
     def test_edit(self):
     def test_edit(self):
         """endpoint returns valid configuration for editor"""
         """endpoint returns valid configuration for editor"""
-        for i in range(3):
-            self.override_acl({
-                'max_attachment_size': 1000,
-            })
+        for _ in range(3):
+            self.override_acl({'max_attachment_size': 1000})
 
 
             with open(TEST_DOCUMENT_PATH, 'rb') as upload:
             with open(TEST_DOCUMENT_PATH, 'rb') as upload:
-                response = self.client.post(reverse('misago:api:attachment-list'), data={
-                    'upload': upload
-                })
+                response = self.client.post(
+                    reverse('misago:api:attachment-list'), data={
+                        'upload': upload,
+                    }
+                )
             self.assertEqual(response.status_code, 200)
             self.assertEqual(response.status_code, 200)
 
 
         attachments = list(Attachment.objects.order_by('id'))
         attachments = list(Attachment.objects.order_by('id'))
@@ -645,24 +601,24 @@ class EditReplyEditorApiTests(EditorApiTestCase):
             attachment.post = self.post
             attachment.post = self.post
             attachment.save()
             attachment.save()
 
 
-        self.override_acl({
-            'can_edit_posts': 1,
-        })
+        self.override_acl({'can_edit_posts': 1})
         response = self.client.get(self.api_link)
         response = self.client.get(self.api_link)
 
 
         for attachment in attachments:
         for attachment in attachments:
             add_acl(self.user, attachment)
             add_acl(self.user, attachment)
 
 
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
-        self.assertEqual(json.loads(smart_str(response.content)), {
-            'id': self.post.pk,
-            'api': self.post.get_api_url(),
-            'post': self.post.original,
-            'can_protect': False,
-            'is_protected': self.post.is_protected,
-            'poster': self.post.poster_name,
-            'attachments': [
-                AttachmentSerializer(attachments[1], context={'user': self.user}).data,
-                AttachmentSerializer(attachments[0], context={'user': self.user}).data,
-            ]
-        })
+        self.assertEqual(
+            response.json(), {
+                'id': self.post.pk,
+                'api': self.post.get_api_url(),
+                'post': self.post.original,
+                'can_protect': False,
+                'is_protected': self.post.is_protected,
+                'poster': self.post.poster_name,
+                'attachments': [
+                    AttachmentSerializer(attachments[1], context={'user': self.user}).data,
+                    AttachmentSerializer(attachments[0], context={'user': self.user}).data,
+                ],
+            }
+        )

+ 434 - 288
misago/threads/tests/test_threads_merge_api.py

@@ -1,10 +1,8 @@
 import json
 import json
 
 
 from django.urls import reverse
 from django.urls import reverse
-from django.utils.six.moves import range
 
 
 from misago.acl import add_acl
 from misago.acl import add_acl
-from misago.acl.testutils import override_acl
 from misago.categories.models import Category
 from misago.categories.models import Category
 from misago.threads import testutils
 from misago.threads import testutils
 from misago.threads.api.threadendpoints.merge import MERGE_LIMIT
 from misago.threads.api.threadendpoints.merge import MERGE_LIMIT
@@ -22,7 +20,11 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         Category(
         Category(
             name='Category B',
             name='Category B',
             slug='category-b',
             slug='category-b',
-        ).insert_at(self.category, position='last-child', save=True)
+        ).insert_at(
+            self.category,
+            position='last-child',
+            save=True,
+        )
         self.category_b = Category.objects.get(slug='category-b')
         self.category_b = Category.objects.get(slug='category-b')
 
 
     def test_merge_no_threads(self):
     def test_merge_no_threads(self):
@@ -31,115 +33,155 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         self.assertEqual(response.status_code, 403)
         self.assertEqual(response.status_code, 403)
 
 
         response_json = response.json()
         response_json = response.json()
-        self.assertEqual(response_json, {
-            'detail': "You have to select at least two threads to merge."
-        })
+        self.assertEqual(
+            response_json, {
+                'detail': "You have to select at least two threads to merge.",
+            }
+        )
 
 
     def test_merge_empty_threads(self):
     def test_merge_empty_threads(self):
         """api validates if we are trying to empty threads list"""
         """api validates if we are trying to empty threads list"""
-        response = self.client.post(self.api_link, json.dumps({
-            'threads': []
-        }), content_type="application/json")
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'threads': [],
+            }),
+            content_type="application/json",
+        )
         self.assertEqual(response.status_code, 403)
         self.assertEqual(response.status_code, 403)
 
 
         response_json = response.json()
         response_json = response.json()
-        self.assertEqual(response_json, {
-            'detail': "You have to select at least two threads to merge."
-        })
+        self.assertEqual(
+            response_json, {
+                'detail': "You have to select at least two threads to merge.",
+            }
+        )
 
 
     def test_merge_invalid_threads(self):
     def test_merge_invalid_threads(self):
         """api validates if we are trying to merge invalid thread ids"""
         """api validates if we are trying to merge invalid thread ids"""
-        response = self.client.post(self.api_link, json.dumps({
-            'threads': 'abcd'
-        }), content_type="application/json")
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'threads': 'abcd',
+            }),
+            content_type="application/json",
+        )
         self.assertEqual(response.status_code, 403)
         self.assertEqual(response.status_code, 403)
 
 
         response_json = response.json()
         response_json = response.json()
-        self.assertEqual(response_json, {
-            'detail': "One or more thread ids received were invalid."
-        })
-
-        response = self.client.post(self.api_link, json.dumps({
-            'threads': ['a', '-', 'c']
-        }), content_type="application/json")
+        self.assertEqual(
+            response_json, {
+                'detail': "One or more thread ids received were invalid.",
+            }
+        )
+
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'threads': ['a', '-', 'c'],
+            }),
+            content_type="application/json",
+        )
         self.assertEqual(response.status_code, 403)
         self.assertEqual(response.status_code, 403)
 
 
         response_json = response.json()
         response_json = response.json()
-        self.assertEqual(response_json, {
-            'detail': "One or more thread ids received were invalid."
-        })
+        self.assertEqual(
+            response_json, {
+                'detail': "One or more thread ids received were invalid.",
+            }
+        )
 
 
     def test_merge_single_thread(self):
     def test_merge_single_thread(self):
         """api validates if we are trying to merge single thread"""
         """api validates if we are trying to merge single thread"""
-        response = self.client.post(self.api_link, json.dumps({
-            'threads': [self.thread.id]
-        }), content_type="application/json")
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'threads': [self.thread.id],
+            }),
+            content_type="application/json",
+        )
         self.assertEqual(response.status_code, 403)
         self.assertEqual(response.status_code, 403)
 
 
         response_json = response.json()
         response_json = response.json()
-        self.assertEqual(response_json, {
-            'detail': "You have to select at least two threads to merge."
-        })
+        self.assertEqual(
+            response_json, {
+                'detail': "You have to select at least two threads to merge.",
+            }
+        )
 
 
     def test_merge_with_nonexisting_thread(self):
     def test_merge_with_nonexisting_thread(self):
         """api validates if we are trying to merge with invalid thread"""
         """api validates if we are trying to merge with invalid thread"""
-        unaccesible_thread = testutils.post_thread(category=self.category_b)
-
-        response = self.client.post(self.api_link, json.dumps({
-            'threads': [self.thread.id, self.thread.id + 1000]
-        }), content_type="application/json")
+        testutils.post_thread(category=self.category_b)
+
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'threads': [self.thread.id, self.thread.id + 1000],
+            }),
+            content_type="application/json",
+        )
         self.assertEqual(response.status_code, 403)
         self.assertEqual(response.status_code, 403)
 
 
         response_json = response.json()
         response_json = response.json()
-        self.assertEqual(response_json, {
-            'detail': "One or more threads to merge could not be found."
-        })
+        self.assertEqual(
+            response_json, {
+                'detail': "One or more threads to merge could not be found.",
+            }
+        )
 
 
     def test_merge_with_invisible_thread(self):
     def test_merge_with_invisible_thread(self):
         """api validates if we are trying to merge with inaccesible thread"""
         """api validates if we are trying to merge with inaccesible thread"""
         unaccesible_thread = testutils.post_thread(category=self.category_b)
         unaccesible_thread = testutils.post_thread(category=self.category_b)
 
 
-        response = self.client.post(self.api_link, json.dumps({
-            'threads': [self.thread.id, unaccesible_thread.id]
-        }), content_type="application/json")
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'threads': [self.thread.id, unaccesible_thread.id],
+            }),
+            content_type="application/json",
+        )
         self.assertEqual(response.status_code, 403)
         self.assertEqual(response.status_code, 403)
 
 
         response_json = response.json()
         response_json = response.json()
-        self.assertEqual(response_json, {
-            'detail': "One or more threads to merge could not be found."
-        })
+        self.assertEqual(
+            response_json, {
+                'detail': "One or more threads to merge could not be found.",
+            }
+        )
 
 
     def test_merge_no_permission(self):
     def test_merge_no_permission(self):
         """api validates permission to merge threads"""
         """api validates permission to merge threads"""
         thread = testutils.post_thread(category=self.category)
         thread = testutils.post_thread(category=self.category)
 
 
-        response = self.client.post(self.api_link, json.dumps({
-            'threads': [self.thread.id, thread.id]
-        }), content_type="application/json")
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'threads': [self.thread.id, thread.id],
+            }),
+            content_type="application/json",
+        )
         self.assertEqual(response.status_code, 403)
         self.assertEqual(response.status_code, 403)
 
 
         response_json = response.json()
         response_json = response.json()
-        self.assertEqual(response_json, [
-            {
-                'id': thread.pk,
-                'title': thread.title,
-                'errors': [
-                    "You don't have permission to merge this thread with others."
-                ]
-            },
-            {
-                'id': self.thread.pk,
-                'title': self.thread.title,
-                'errors': [
-                    "You don't have permission to merge this thread with others."
-                ]
-            },
-        ])
+        self.assertEqual(
+            response_json, [
+                {
+                    'id': thread.pk,
+                    'title': thread.title,
+                    'errors': ["You don't have permission to merge this thread with others."],
+                },
+                {
+                    'id': self.thread.pk,
+                    'title': self.thread.title,
+                    'errors': ["You don't have permission to merge this thread with others."],
+                },
+            ]
+        )
 
 
     def test_merge_too_many_threads(self):
     def test_merge_too_many_threads(self):
         """api rejects too many threads to merge"""
         """api rejects too many threads to merge"""
         threads = []
         threads = []
-        for i in range(MERGE_LIMIT + 1):
+        for _ in range(MERGE_LIMIT + 1):
             threads.append(testutils.post_thread(category=self.category).pk)
             threads.append(testutils.post_thread(category=self.category).pk)
 
 
         self.override_acl({
         self.override_acl({
@@ -149,15 +191,21 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
             'can_reply_threads': False,
             'can_reply_threads': False,
         })
         })
 
 
-        response = self.client.post(self.api_link, json.dumps({
-            'threads': threads
-        }), content_type="application/json")
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'threads': threads,
+            }),
+            content_type="application/json",
+        )
         self.assertEqual(response.status_code, 403)
         self.assertEqual(response.status_code, 403)
 
 
         response_json = response.json()
         response_json = response.json()
-        self.assertEqual(response_json, {
-            'detail': "No more than %s threads can be merged at single time." % MERGE_LIMIT
-        })
+        self.assertEqual(
+            response_json, {
+                'detail': "No more than %s threads can be merged at single time." % MERGE_LIMIT,
+            }
+        )
 
 
     def test_merge_no_final_thread(self):
     def test_merge_no_final_thread(self):
         """api rejects merge because no data to merge threads was specified"""
         """api rejects merge because no data to merge threads was specified"""
@@ -170,16 +218,22 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
 
 
         thread = testutils.post_thread(category=self.category)
         thread = testutils.post_thread(category=self.category)
 
 
-        response = self.client.post(self.api_link, json.dumps({
-            'threads': [self.thread.id, thread.id]
-        }), content_type="application/json")
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'threads': [self.thread.id, thread.id],
+            }),
+            content_type="application/json",
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         response_json = response.json()
         response_json = response.json()
-        self.assertEqual(response_json, {
-            'title': ['This field is required.'],
-            'category': ['This field is required.'],
-        })
+        self.assertEqual(
+            response_json, {
+                'title': ['This field is required.'],
+                'category': ['This field is required.'],
+            }
+        )
 
 
     def test_merge_invalid_final_title(self):
     def test_merge_invalid_final_title(self):
         """api rejects merge because final thread title was invalid"""
         """api rejects merge because final thread title was invalid"""
@@ -192,17 +246,23 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
 
 
         thread = testutils.post_thread(category=self.category)
         thread = testutils.post_thread(category=self.category)
 
 
-        response = self.client.post(self.api_link, json.dumps({
-            'threads': [self.thread.id, thread.id],
-            'title': '$$$',
-            'category': self.category.id,
-        }), content_type="application/json")
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'threads': [self.thread.id, thread.id],
+                'title': '$$$',
+                'category': self.category.id,
+            }),
+            content_type="application/json",
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         response_json = response.json()
         response_json = response.json()
-        self.assertEqual(response_json, {
-            'title': ["Thread title should be at least 5 characters long (it has 3)."]
-        })
+        self.assertEqual(
+            response_json, {
+                'title': ["Thread title should be at least 5 characters long (it has 3)."],
+            }
+        )
 
 
     def test_merge_invalid_category(self):
     def test_merge_invalid_category(self):
         """api rejects merge because final category was invalid"""
         """api rejects merge because final category was invalid"""
@@ -215,17 +275,23 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
 
 
         thread = testutils.post_thread(category=self.category)
         thread = testutils.post_thread(category=self.category)
 
 
-        response = self.client.post(self.api_link, json.dumps({
-            'threads': [self.thread.id, thread.id],
-            'title': 'Valid thread title',
-            'category': self.category_b.id,
-        }), content_type="application/json")
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'threads': [self.thread.id, thread.id],
+                'title': 'Valid thread title',
+                'category': self.category_b.id,
+            }),
+            content_type="application/json",
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         response_json = response.json()
         response_json = response.json()
-        self.assertEqual(response_json, {
-            'category': ["Requested category could not be found."]
-        })
+        self.assertEqual(
+            response_json, {
+                'category': ["Requested category could not be found."],
+            }
+        )
 
 
     def test_merge_unallowed_start_thread(self):
     def test_merge_unallowed_start_thread(self):
         """api rejects merge because category isn't allowing starting threads"""
         """api rejects merge because category isn't allowing starting threads"""
@@ -234,24 +300,28 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
             'can_close_threads': False,
             'can_close_threads': False,
             'can_edit_threads': False,
             'can_edit_threads': False,
             'can_reply_threads': False,
             'can_reply_threads': False,
-            'can_start_threads': 0
+            'can_start_threads': 0,
         })
         })
 
 
         thread = testutils.post_thread(category=self.category)
         thread = testutils.post_thread(category=self.category)
 
 
-        response = self.client.post(self.api_link, json.dumps({
-            'threads': [self.thread.id, thread.id],
-            'title': 'Valid thread title',
-            'category': self.category.id
-        }), content_type="application/json")
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'threads': [self.thread.id, thread.id],
+                'title': 'Valid thread title',
+                'category': self.category.id,
+            }),
+            content_type="application/json",
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         response_json = response.json()
         response_json = response.json()
-        self.assertEqual(response_json, {
-            'category': [
-                "You can't create new threads in selected category."
-            ]
-        })
+        self.assertEqual(
+            response_json, {
+                'category': ["You can't create new threads in selected category."],
+            }
+        )
 
 
     def test_merge_invalid_weight(self):
     def test_merge_invalid_weight(self):
         """api rejects merge because final weight was invalid"""
         """api rejects merge because final weight was invalid"""
@@ -264,18 +334,24 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
 
 
         thread = testutils.post_thread(category=self.category)
         thread = testutils.post_thread(category=self.category)
 
 
-        response = self.client.post(self.api_link, json.dumps({
-            'threads': [self.thread.id, thread.id],
-            'title': 'Valid thread title',
-            'category': self.category.id,
-            'weight': 4,
-        }), content_type="application/json")
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'threads': [self.thread.id, thread.id],
+                'title': 'Valid thread title',
+                'category': self.category.id,
+                'weight': 4,
+            }),
+            content_type="application/json",
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         response_json = response.json()
         response_json = response.json()
-        self.assertEqual(response_json, {
-            'weight': ["Ensure this value is less than or equal to 2."]
-        })
+        self.assertEqual(
+            response_json, {
+                'weight': ["Ensure this value is less than or equal to 2."],
+            }
+        )
 
 
     def test_merge_unallowed_global_weight(self):
     def test_merge_unallowed_global_weight(self):
         """api rejects merge because global weight was unallowed"""
         """api rejects merge because global weight was unallowed"""
@@ -288,20 +364,24 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
 
 
         thread = testutils.post_thread(category=self.category)
         thread = testutils.post_thread(category=self.category)
 
 
-        response = self.client.post(self.api_link, json.dumps({
-            'threads': [self.thread.id, thread.id],
-            'title': 'Valid thread title',
-            'category': self.category.id,
-            'weight': 2,
-        }), content_type="application/json")
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'threads': [self.thread.id, thread.id],
+                'title': 'Valid thread title',
+                'category': self.category.id,
+                'weight': 2,
+            }),
+            content_type="application/json",
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         response_json = response.json()
         response_json = response.json()
-        self.assertEqual(response_json, {
-            'weight': [
-                "You don't have permission to pin threads globally in this category."
-            ]
-        })
+        self.assertEqual(
+            response_json, {
+                'weight': ["You don't have permission to pin threads globally in this category."],
+            }
+        )
 
 
     def test_merge_unallowed_local_weight(self):
     def test_merge_unallowed_local_weight(self):
         """api rejects merge because local weight was unallowed"""
         """api rejects merge because local weight was unallowed"""
@@ -314,20 +394,24 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
 
 
         thread = testutils.post_thread(category=self.category)
         thread = testutils.post_thread(category=self.category)
 
 
-        response = self.client.post(self.api_link, json.dumps({
-            'threads': [self.thread.id, thread.id],
-            'title': 'Valid thread title',
-            'category': self.category.id,
-            'weight': 1,
-        }), content_type="application/json")
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'threads': [self.thread.id, thread.id],
+                'title': 'Valid thread title',
+                'category': self.category.id,
+                'weight': 1,
+            }),
+            content_type="application/json",
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         response_json = response.json()
         response_json = response.json()
-        self.assertEqual(response_json, {
-            'weight': [
-                "You don't have permission to pin threads in this category."
-            ]
-        })
+        self.assertEqual(
+            response_json, {
+                'weight': ["You don't have permission to pin threads in this category."],
+            }
+        )
 
 
     def test_merge_allowed_local_weight(self):
     def test_merge_allowed_local_weight(self):
         """api allows local weight"""
         """api allows local weight"""
@@ -341,18 +425,24 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
 
 
         thread = testutils.post_thread(category=self.category)
         thread = testutils.post_thread(category=self.category)
 
 
-        response = self.client.post(self.api_link, json.dumps({
-            'threads': [self.thread.id, thread.id],
-            'title': '$$$',
-            'category': self.category.id,
-            'weight': 1,
-        }), content_type="application/json")
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'threads': [self.thread.id, thread.id],
+                'title': '$$$',
+                'category': self.category.id,
+                'weight': 1,
+            }),
+            content_type="application/json",
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         response_json = response.json()
         response_json = response.json()
-        self.assertEqual(response_json, {
-            'title': ["Thread title should be at least 5 characters long (it has 3)."]
-        })
+        self.assertEqual(
+            response_json, {
+                'title': ["Thread title should be at least 5 characters long (it has 3)."],
+            }
+        )
 
 
     def test_merge_allowed_global_weight(self):
     def test_merge_allowed_global_weight(self):
         """api allows global weight"""
         """api allows global weight"""
@@ -366,18 +456,24 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
 
 
         thread = testutils.post_thread(category=self.category)
         thread = testutils.post_thread(category=self.category)
 
 
-        response = self.client.post(self.api_link, json.dumps({
-            'threads': [self.thread.id, thread.id],
-            'title': '$$$',
-            'category': self.category.id,
-            'weight': 2,
-        }), content_type="application/json")
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'threads': [self.thread.id, thread.id],
+                'title': '$$$',
+                'category': self.category.id,
+                'weight': 2,
+            }),
+            content_type="application/json",
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         response_json = response.json()
         response_json = response.json()
-        self.assertEqual(response_json, {
-            'title': ["Thread title should be at least 5 characters long (it has 3)."]
-        })
+        self.assertEqual(
+            response_json, {
+                'title': ["Thread title should be at least 5 characters long (it has 3)."],
+            }
+        )
 
 
     def test_merge_unallowed_close(self):
     def test_merge_unallowed_close(self):
         """api rejects merge because closing thread was unallowed"""
         """api rejects merge because closing thread was unallowed"""
@@ -390,20 +486,24 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
 
 
         thread = testutils.post_thread(category=self.category)
         thread = testutils.post_thread(category=self.category)
 
 
-        response = self.client.post(self.api_link, json.dumps({
-            'threads': [self.thread.id, thread.id],
-            'title': 'Valid thread title',
-            'category': self.category.id,
-            'is_closed': True,
-        }), content_type="application/json")
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'threads': [self.thread.id, thread.id],
+                'title': 'Valid thread title',
+                'category': self.category.id,
+                'is_closed': True,
+            }),
+            content_type="application/json",
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         response_json = response.json()
         response_json = response.json()
-        self.assertEqual(response_json, {
-            'is_closed': [
-                "You don't have permission to close threads in this category."
-            ]
-        })
+        self.assertEqual(
+            response_json, {
+                'is_closed': ["You don't have permission to close threads in this category."],
+            }
+        )
 
 
     def test_merge_with_close(self):
     def test_merge_with_close(self):
         """api allows for closing thread"""
         """api allows for closing thread"""
@@ -416,19 +516,25 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
 
 
         thread = testutils.post_thread(category=self.category)
         thread = testutils.post_thread(category=self.category)
 
 
-        response = self.client.post(self.api_link, json.dumps({
-            'threads': [self.thread.id, thread.id],
-            'title': '$$$',
-            'category': self.category.id,
-            'weight': 0,
-            'is_closed': True,
-        }), content_type="application/json")
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'threads': [self.thread.id, thread.id],
+                'title': '$$$',
+                'category': self.category.id,
+                'weight': 0,
+                'is_closed': True,
+            }),
+            content_type="application/json",
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         response_json = response.json()
         response_json = response.json()
-        self.assertEqual(response_json, {
-            'title': ["Thread title should be at least 5 characters long (it has 3)."]
-        })
+        self.assertEqual(
+            response_json, {
+                'title': ["Thread title should be at least 5 characters long (it has 3)."],
+            }
+        )
 
 
     def test_merge_unallowed_hidden(self):
     def test_merge_unallowed_hidden(self):
         """api rejects merge because hidden thread was unallowed"""
         """api rejects merge because hidden thread was unallowed"""
@@ -442,20 +548,24 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
 
 
         thread = testutils.post_thread(category=self.category)
         thread = testutils.post_thread(category=self.category)
 
 
-        response = self.client.post(self.api_link, json.dumps({
-            'threads': [self.thread.id, thread.id],
-            'title': 'Valid thread title',
-            'category': self.category.id,
-            'is_hidden': True,
-        }), content_type="application/json")
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'threads': [self.thread.id, thread.id],
+                'title': 'Valid thread title',
+                'category': self.category.id,
+                'is_hidden': True,
+            }),
+            content_type="application/json",
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         response_json = response.json()
         response_json = response.json()
-        self.assertEqual(response_json, {
-            'is_hidden': [
-                "You don't have permission to hide threads in this category."
-            ]
-        })
+        self.assertEqual(
+            response_json, {
+                'is_hidden': ["You don't have permission to hide threads in this category."],
+            }
+        )
 
 
     def test_merge_with_hide(self):
     def test_merge_with_hide(self):
         """api allows for hiding thread"""
         """api allows for hiding thread"""
@@ -469,19 +579,25 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
 
 
         thread = testutils.post_thread(category=self.category)
         thread = testutils.post_thread(category=self.category)
 
 
-        response = self.client.post(self.api_link, json.dumps({
-            'threads': [self.thread.id, thread.id],
-            'title': '$$$',
-            'category': self.category.id,
-            'weight': 0,
-            'is_hidden': True,
-        }), content_type="application/json")
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'threads': [self.thread.id, thread.id],
+                'title': '$$$',
+                'category': self.category.id,
+                'weight': 0,
+                'is_hidden': True,
+            }),
+            content_type="application/json",
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         response_json = response.json()
         response_json = response.json()
-        self.assertEqual(response_json, {
-            'title': ["Thread title should be at least 5 characters long (it has 3)."]
-        })
+        self.assertEqual(
+            response_json, {
+                'title': ["Thread title should be at least 5 characters long (it has 3)."],
+            }
+        )
 
 
     def test_merge(self):
     def test_merge(self):
         """api performs basic merge"""
         """api performs basic merge"""
@@ -496,11 +612,15 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
 
 
         thread = testutils.post_thread(category=self.category)
         thread = testutils.post_thread(category=self.category)
 
 
-        response = self.client.post(self.api_link, json.dumps({
-            'threads': [self.thread.id, thread.id],
-            'title': 'Merged thread!',
-            'category': self.category.id,
-        }), content_type="application/json")
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'threads': [self.thread.id, thread.id],
+                'title': 'Merged thread!',
+                'category': self.category.id,
+            }),
+            content_type="application/json",
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         # is response json with new thread?
         # is response json with new thread?
@@ -531,19 +651,23 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
             'can_merge_threads': True,
             'can_merge_threads': True,
             'can_close_threads': True,
             'can_close_threads': True,
             'can_hide_threads': 1,
             'can_hide_threads': 1,
-            'can_pin_threads': 2
+            'can_pin_threads': 2,
         })
         })
 
 
         thread = testutils.post_thread(category=self.category)
         thread = testutils.post_thread(category=self.category)
 
 
-        response = self.client.post(self.api_link, json.dumps({
-            'threads': [self.thread.id, thread.id],
-            'title': 'Merged thread!',
-            'category': self.category.id,
-            'is_closed': 1,
-            'is_hidden': 1,
-            'weight': 2
-        }), content_type="application/json")
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'threads': [self.thread.id, thread.id],
+                'title': 'Merged thread!',
+                'category': self.category.id,
+                'is_closed': 1,
+                'is_hidden': 1,
+                'weight': 2,
+            }),
+            content_type="application/json",
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         # is response json with new thread?
         # is response json with new thread?
@@ -583,12 +707,16 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
 
 
         thread = testutils.post_thread(category=self.category)
         thread = testutils.post_thread(category=self.category)
 
 
-        response = self.client.post(self.api_link, json.dumps({
-            'top_category': self.root.id,
-            'threads': [self.thread.id, thread.id],
-            'title': 'Merged thread!',
-            'category': self.category.id,
-        }), content_type="application/json")
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'top_category': self.root.id,
+                'threads': [self.thread.id, thread.id],
+                'title': 'Merged thread!',
+                'category': self.category.id,
+            }),
+            content_type="application/json",
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         # is response json with new thread?
         # is response json with new thread?
@@ -613,18 +741,20 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
 
 
     def test_merge_threads_kept_poll(self):
     def test_merge_threads_kept_poll(self):
         """api merges two threads successfully, keeping poll from old thread"""
         """api merges two threads successfully, keeping poll from old thread"""
-        self.override_acl({
-            'can_merge_threads': True,
-        })
+        self.override_acl({'can_merge_threads': True})
 
 
         other_thread = testutils.post_thread(self.category)
         other_thread = testutils.post_thread(self.category)
         poll = testutils.post_poll(other_thread, self.user)
         poll = testutils.post_poll(other_thread, self.user)
 
 
-        response = self.client.post(self.api_link, json.dumps({
-            'threads': [self.thread.id, other_thread.id],
-            'title': 'Merged thread!',
-            'category': self.category.id,
-        }), content_type="application/json")
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'threads': [self.thread.id, other_thread.id],
+                'title': 'Merged thread!',
+                'category': self.category.id,
+            }),
+            content_type="application/json",
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         response_json = response.json()
         response_json = response.json()
@@ -639,18 +769,20 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
 
 
     def test_merge_threads_moved_poll(self):
     def test_merge_threads_moved_poll(self):
         """api merges two threads successfully, moving poll from other thread"""
         """api merges two threads successfully, moving poll from other thread"""
-        self.override_acl({
-            'can_merge_threads': True,
-        })
+        self.override_acl({'can_merge_threads': True})
 
 
         other_thread = testutils.post_thread(self.category)
         other_thread = testutils.post_thread(self.category)
         poll = testutils.post_poll(self.thread, self.user)
         poll = testutils.post_poll(self.thread, self.user)
 
 
-        response = self.client.post(self.api_link, json.dumps({
-            'threads': [self.thread.id, other_thread.id],
-            'title': 'Merged thread!',
-            'category': self.category.id,
-        }), content_type="application/json")
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'threads': [self.thread.id, other_thread.id],
+                'title': 'Merged thread!',
+                'category': self.category.id,
+            }),
+            content_type="application/json",
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         response_json = response.json()
         response_json = response.json()
@@ -665,28 +797,32 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
 
 
     def test_threads_merge_conflict(self):
     def test_threads_merge_conflict(self):
         """api errors on merge conflict, returning list of available polls"""
         """api errors on merge conflict, returning list of available polls"""
-        self.override_acl({
-            'can_merge_threads': True,
-        })
+        self.override_acl({'can_merge_threads': True})
 
 
         other_thread = testutils.post_thread(self.category)
         other_thread = testutils.post_thread(self.category)
         poll = testutils.post_poll(self.thread, self.user)
         poll = testutils.post_poll(self.thread, self.user)
         other_poll = testutils.post_poll(other_thread, self.user)
         other_poll = testutils.post_poll(other_thread, self.user)
 
 
-        response = self.client.post(self.api_link, json.dumps({
-            'threads': [self.thread.id, other_thread.id],
-            'title': 'Merged thread!',
-            'category': self.category.id,
-        }), content_type="application/json")
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'threads': [self.thread.id, other_thread.id],
+                'title': 'Merged thread!',
+                'category': self.category.id,
+            }),
+            content_type="application/json",
+        )
 
 
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'polls': [
-                [0, "Delete all polls"],
-                [poll.pk, poll.question],
-                [other_poll.pk, other_poll.question]
-            ]
-        })
+        self.assertEqual(
+            response.json(), {
+                'polls': [
+                    [0, "Delete all polls"],
+                    [poll.pk, poll.question],
+                    [other_poll.pk, other_poll.question],
+                ],
+            }
+        )
 
 
         # polls and votes were untouched
         # polls and votes were untouched
         self.assertEqual(Poll.objects.count(), 2)
         self.assertEqual(Poll.objects.count(), 2)
@@ -694,24 +830,27 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
 
 
     def test_threads_merge_conflict_invalid_resolution(self):
     def test_threads_merge_conflict_invalid_resolution(self):
         """api errors on invalid merge conflict resolution"""
         """api errors on invalid merge conflict resolution"""
-        self.override_acl({
-            'can_merge_threads': True,
-        })
+        self.override_acl({'can_merge_threads': True})
 
 
         other_thread = testutils.post_thread(self.category)
         other_thread = testutils.post_thread(self.category)
-        poll = testutils.post_poll(self.thread, self.user)
-        other_poll = testutils.post_poll(other_thread, self.user)
 
 
-        response = self.client.post(self.api_link, json.dumps({
-            'threads': [self.thread.id, other_thread.id],
-            'title': 'Merged thread!',
-            'category': self.category.id,
-            'poll': 'dsa7dsadsa9789'
-        }), content_type="application/json")
+        testutils.post_poll(self.thread, self.user)
+        testutils.post_poll(other_thread, self.user)
+
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'threads': [self.thread.id, other_thread.id],
+                'title': 'Merged thread!',
+                'category': self.category.id,
+                'poll': 'dsa7dsadsa9789',
+            }),
+            content_type="application/json",
+        )
 
 
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.json(), {
         self.assertEqual(response.json(), {
-            'detail': "Invalid choice."
+            'detail': "Invalid choice.",
         })
         })
 
 
         # polls and votes were untouched
         # polls and votes were untouched
@@ -720,20 +859,23 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
 
 
     def test_threads_merge_conflict_delete_all(self):
     def test_threads_merge_conflict_delete_all(self):
         """api deletes all polls when delete all choice is selected"""
         """api deletes all polls when delete all choice is selected"""
-        self.override_acl({
-            'can_merge_threads': True,
-        })
+        self.override_acl({'can_merge_threads': True})
 
 
         other_thread = testutils.post_thread(self.category)
         other_thread = testutils.post_thread(self.category)
-        poll = testutils.post_poll(self.thread, self.user)
-        other_poll = testutils.post_poll(other_thread, self.user)
 
 
-        response = self.client.post(self.api_link, json.dumps({
-            'threads': [self.thread.id, other_thread.id],
-            'title': 'Merged thread!',
-            'category': self.category.id,
-            'poll': 0
-        }), content_type="application/json")
+        testutils.post_poll(self.thread, self.user)
+        testutils.post_poll(other_thread, self.user)
+
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'threads': [self.thread.id, other_thread.id],
+                'title': 'Merged thread!',
+                'category': self.category.id,
+                'poll': 0,
+            }),
+            content_type="application/json",
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         # polls and votes are gone
         # polls and votes are gone
@@ -742,20 +884,22 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
 
 
     def test_threads_merge_conflict_keep_first_poll(self):
     def test_threads_merge_conflict_keep_first_poll(self):
         """api deletes other poll on merge"""
         """api deletes other poll on merge"""
-        self.override_acl({
-            'can_merge_threads': True,
-        })
+        self.override_acl({'can_merge_threads': True})
 
 
         other_thread = testutils.post_thread(self.category)
         other_thread = testutils.post_thread(self.category)
         poll = testutils.post_poll(self.thread, self.user)
         poll = testutils.post_poll(self.thread, self.user)
         other_poll = testutils.post_poll(other_thread, self.user)
         other_poll = testutils.post_poll(other_thread, self.user)
 
 
-        response = self.client.post(self.api_link, json.dumps({
-            'threads': [self.thread.id, other_thread.id],
-            'title': 'Merged thread!',
-            'category': self.category.id,
-            'poll': poll.pk
-        }), content_type="application/json")
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'threads': [self.thread.id, other_thread.id],
+                'title': 'Merged thread!',
+                'category': self.category.id,
+                'poll': poll.pk,
+            }),
+            content_type="application/json",
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         # other poll and its votes are gone
         # other poll and its votes are gone
@@ -768,20 +912,22 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
 
 
     def test_threads_merge_conflict_keep_other_poll(self):
     def test_threads_merge_conflict_keep_other_poll(self):
         """api deletes first poll on merge"""
         """api deletes first poll on merge"""
-        self.override_acl({
-            'can_merge_threads': True,
-        })
+        self.override_acl({'can_merge_threads': True})
 
 
         other_thread = testutils.post_thread(self.category)
         other_thread = testutils.post_thread(self.category)
         poll = testutils.post_poll(self.thread, self.user)
         poll = testutils.post_poll(self.thread, self.user)
         other_poll = testutils.post_poll(other_thread, self.user)
         other_poll = testutils.post_poll(other_thread, self.user)
 
 
-        response = self.client.post(self.api_link, json.dumps({
-            'threads': [self.thread.id, other_thread.id],
-            'title': 'Merged thread!',
-            'category': self.category.id,
-            'poll': other_poll.pk
-        }), content_type="application/json")
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'threads': [self.thread.id, other_thread.id],
+                'title': 'Merged thread!',
+                'category': self.category.id,
+                'poll': other_poll.pk,
+            }),
+            content_type="application/json",
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         # other poll and its votes are gone
         # other poll and its votes are gone

+ 12 - 10
misago/threads/tests/test_threads_moderation.py

@@ -1,6 +1,6 @@
 from misago.categories.models import Category
 from misago.categories.models import Category
 from misago.threads import moderation, testutils
 from misago.threads import moderation, testutils
-from misago.threads.models import Post, Thread
+from misago.threads.models import Thread
 from misago.users.testutils import AuthenticatedUserTestCase
 from misago.users.testutils import AuthenticatedUserTestCase
 
 
 
 
@@ -26,7 +26,9 @@ class ThreadsModerationTests(AuthenticatedUserTestCase):
 
 
     def test_change_thread_title(self):
     def test_change_thread_title(self):
         """change_thread_title changes thread's title and slug"""
         """change_thread_title changes thread's title and slug"""
-        self.assertTrue(moderation.change_thread_title(self.request, self.thread, "New title is here!"))
+        self.assertTrue(
+            moderation.change_thread_title(self.request, self.thread, "New title is here!")
+        )
 
 
         self.reload_thread()
         self.reload_thread()
         self.assertEqual(self.thread.title, "New title is here!")
         self.assertEqual(self.thread.title, "New title is here!")
@@ -73,9 +75,7 @@ class ThreadsModerationTests(AuthenticatedUserTestCase):
         self.assertEqual(event.event_type, 'pinned_locally')
         self.assertEqual(event.event_type, 'pinned_locally')
 
 
     def test_pin_invalid_thread(self):
     def test_pin_invalid_thread(self):
-        """
-        pin_thread_locally returns false for already locally pinned thread
-        """
+        """pin_thread_locally returns false for already locally pinned thread"""
         self.thread.weight = 1
         self.thread.weight = 1
 
 
         self.assertFalse(moderation.pin_thread_locally(self.request, self.thread))
         self.assertFalse(moderation.pin_thread_locally(self.request, self.thread))
@@ -139,12 +139,15 @@ class ThreadsModerationTests(AuthenticatedUserTestCase):
         Category(
         Category(
             name='New Category',
             name='New Category',
             slug='new-category',
             slug='new-category',
-        ).insert_at(root_category, position='last-child', save=True)
+        ).insert_at(
+            root_category,
+            position='last-child',
+            save=True,
+        )
         new_category = Category.objects.get(slug='new-category')
         new_category = Category.objects.get(slug='new-category')
 
 
         self.assertEqual(self.thread.category, self.category)
         self.assertEqual(self.thread.category, self.category)
-        self.assertTrue(
-            moderation.move_thread(self.request, self.thread, new_category))
+        self.assertTrue(moderation.move_thread(self.request, self.thread, new_category))
 
 
         self.reload_thread()
         self.reload_thread()
         self.assertEqual(self.thread.category, new_category)
         self.assertEqual(self.thread.category, new_category)
@@ -157,8 +160,7 @@ class ThreadsModerationTests(AuthenticatedUserTestCase):
     def test_move_thread_to_same_category(self):
     def test_move_thread_to_same_category(self):
         """moves_thread does not move thread to same category it is in"""
         """moves_thread does not move thread to same category it is in"""
         self.assertEqual(self.thread.category, self.category)
         self.assertEqual(self.thread.category, self.category)
-        self.assertFalse(
-            moderation.move_thread(self.request, self.thread, self.category))
+        self.assertFalse(moderation.move_thread(self.request, self.thread, self.category))
 
 
         self.reload_thread()
         self.reload_thread()
         self.assertEqual(self.thread.category, self.category)
         self.assertEqual(self.thread.category, self.category)

+ 257 - 274
misago/threads/tests/test_threadslists.py

@@ -1,40 +1,23 @@
 from datetime import timedelta
 from datetime import timedelta
-from json import loads as json_loads
 
 
 from django.urls import reverse
 from django.urls import reverse
 from django.utils import timezone
 from django.utils import timezone
 from django.utils.encoding import smart_str
 from django.utils.encoding import smart_str
-from django.utils.six.moves import range
 
 
 from misago.acl.testutils import override_acl
 from misago.acl.testutils import override_acl
 from misago.categories.models import Category
 from misago.categories.models import Category
 from misago.conf import settings
 from misago.conf import settings
-from misago.core import threadstore
-from misago.core.cache import cache
-from misago.readtracker import categoriestracker, threadstracker
+from misago.readtracker import threadstracker
 from misago.threads import testutils
 from misago.threads import testutils
 from misago.users.models import AnonymousUser
 from misago.users.models import AnonymousUser
 from misago.users.testutils import AuthenticatedUserTestCase
 from misago.users.testutils import AuthenticatedUserTestCase
 
 
 
 
-LISTS_URLS = (
-    '',
-    'my/',
-    'new/',
-    'unread/',
-    'subscribed/',
-)
+LISTS_URLS = ('', 'my/', 'new/', 'unread/', 'subscribed/', )
 
 
 
 
 class ThreadsListTestCase(AuthenticatedUserTestCase):
 class ThreadsListTestCase(AuthenticatedUserTestCase):
     def setUp(self):
     def setUp(self):
-        super(ThreadsListTestCase, self).setUp()
-
-        self.api_link = reverse('misago:api:thread-list')
-
-        self.root = Category.objects.root_category()
-        self.first_category = Category.objects.get(slug='first-category')
-
         """
         """
         Create categories tree for test cases:
         Create categories tree for test cases:
 
 
@@ -48,16 +31,31 @@ class ThreadsListTestCase(AuthenticatedUserTestCase):
         Category E
         Category E
           + Subcategory F
           + Subcategory F
         """
         """
+        super(ThreadsListTestCase, self).setUp()
+
+        self.api_link = reverse('misago:api:thread-list')
+
+        self.root = Category.objects.root_category()
+        self.first_category = Category.objects.get(slug='first-category')
+
         Category(
         Category(
             name='Category A',
             name='Category A',
             slug='category-a',
             slug='category-a',
             css_class='showing-category-a',
             css_class='showing-category-a',
-        ).insert_at(self.root, position='last-child', save=True)
+        ).insert_at(
+            self.root,
+            position='last-child',
+            save=True,
+        )
         Category(
         Category(
             name='Category E',
             name='Category E',
             slug='category-e',
             slug='category-e',
             css_class='showing-category-e',
             css_class='showing-category-e',
-        ).insert_at(self.root, position='last-child', save=True)
+        ).insert_at(
+            self.root,
+            position='last-child',
+            save=True,
+        )
 
 
         self.root = Category.objects.root_category()
         self.root = Category.objects.root_category()
 
 
@@ -67,7 +65,11 @@ class ThreadsListTestCase(AuthenticatedUserTestCase):
             name='Category B',
             name='Category B',
             slug='category-b',
             slug='category-b',
             css_class='showing-category-b',
             css_class='showing-category-b',
-        ).insert_at(self.category_a, position='last-child', save=True)
+        ).insert_at(
+            self.category_a,
+            position='last-child',
+            save=True,
+        )
 
 
         self.category_b = Category.objects.get(slug='category-b')
         self.category_b = Category.objects.get(slug='category-b')
 
 
@@ -75,12 +77,20 @@ class ThreadsListTestCase(AuthenticatedUserTestCase):
             name='Category C',
             name='Category C',
             slug='category-c',
             slug='category-c',
             css_class='showing-category-c',
             css_class='showing-category-c',
-        ).insert_at(self.category_b, position='last-child', save=True)
+        ).insert_at(
+            self.category_b,
+            position='last-child',
+            save=True,
+        )
         Category(
         Category(
             name='Category D',
             name='Category D',
             slug='category-d',
             slug='category-d',
             css_class='showing-category-d',
             css_class='showing-category-d',
-        ).insert_at(self.category_b, position='last-child', save=True)
+        ).insert_at(
+            self.category_b,
+            position='last-child',
+            save=True,
+        )
 
 
         self.category_c = Category.objects.get(slug='category-c')
         self.category_c = Category.objects.get(slug='category-c')
         self.category_d = Category.objects.get(slug='category-d')
         self.category_d = Category.objects.get(slug='category-d')
@@ -90,7 +100,11 @@ class ThreadsListTestCase(AuthenticatedUserTestCase):
             name='Category F',
             name='Category F',
             slug='category-f',
             slug='category-f',
             css_class='showing-category-f',
             css_class='showing-category-f',
-        ).insert_at(self.category_e, position='last-child', save=True)
+        ).insert_at(
+            self.category_e,
+            position='last-child',
+            save=True,
+        )
 
 
         self.category_f = Category.objects.get(slug='category-f')
         self.category_f = Category.objects.get(slug='category-f')
 
 
@@ -115,7 +129,7 @@ class ThreadsListTestCase(AuthenticatedUserTestCase):
             'categories': {},
             'categories': {},
             'visible_categories': [],
             'visible_categories': [],
             'browseable_categories': [],
             'browseable_categories': [],
-            'can_approve_content': []
+            'can_approve_content': [],
         }
         }
 
 
         # copy first category's acl to other categories to make base for overrides
         # copy first category's acl to other categories to make base for overrides
@@ -135,7 +149,7 @@ class ThreadsListTestCase(AuthenticatedUserTestCase):
                 'can_see_all_threads': 1,
                 'can_see_all_threads': 1,
                 'can_see_own_threads': 0,
                 'can_see_own_threads': 0,
                 'can_hide_threads': 0,
                 'can_hide_threads': 0,
-                'can_approve_content': 0
+                'can_approve_content': 0,
             })
             })
 
 
             if category_acl:
             if category_acl:
@@ -150,26 +164,17 @@ class ThreadsListTestCase(AuthenticatedUserTestCase):
 class ApiTests(ThreadsListTestCase):
 class ApiTests(ThreadsListTestCase):
     def test_root_category(self):
     def test_root_category(self):
         """its possible to access threads endpoint with category=ROOT_ID"""
         """its possible to access threads endpoint with category=ROOT_ID"""
-        response = self.client.get('%s?category=%s' % (
-            self.api_link,
-            self.root.pk,
-        ))
+        response = self.client.get('%s?category=%s' % (self.api_link, self.root.pk, ))
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
     def test_explicit_first_page(self):
     def test_explicit_first_page(self):
         """its possible to access threads endpoint with explicit first page"""
         """its possible to access threads endpoint with explicit first page"""
-        response = self.client.get('%s?category=%s&page=1' % (
-            self.api_link,
-            self.root.pk,
-        ))
+        response = self.client.get('%s?category=%s&page=1' % (self.api_link, self.root.pk, ))
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
     def test_invalid_list_type(self):
     def test_invalid_list_type(self):
         """api returns 404 for invalid list type"""
         """api returns 404 for invalid list type"""
-        response = self.client.get('%s?category=%s&list=nope' % (
-            self.api_link,
-            self.root.pk,
-        ))
+        response = self.client.get('%s?category=%s&list=nope' % (self.api_link, self.root.pk, ))
         self.assertEqual(response.status_code, 404)
         self.assertEqual(response.status_code, 404)
 
 
 
 
@@ -195,7 +200,7 @@ class AllThreadsListTests(ThreadsListTestCase):
             response = self.client.get('%s?list=%s' % (self.api_link, url.strip('/') or 'all'))
             response = self.client.get('%s?list=%s' % (self.api_link, url.strip('/') or 'all'))
             self.assertEqual(response.status_code, 200)
             self.assertEqual(response.status_code, 200)
 
 
-            response_json = json_loads(smart_str(response.content))
+            response_json = response.json()
             self.assertEqual(len(response_json['results']), 0)
             self.assertEqual(len(response_json['results']), 0)
 
 
     def test_list_authenticated_only_views(self):
     def test_list_authenticated_only_views(self):
@@ -215,11 +220,10 @@ class AllThreadsListTests(ThreadsListTestCase):
             self.access_all_categories()
             self.access_all_categories()
 
 
             self.access_all_categories()
             self.access_all_categories()
-            response = self.client.get('%s?category=%s&list=%s' % (
-                self.api_link,
-                self.category_b.pk,
-                url.strip('/') or 'all',
-            ))
+            response = self.client.get(
+                '%s?category=%s&list=%s' %
+                (self.api_link, self.category_b.pk, url.strip('/') or 'all', )
+            )
             self.assertEqual(response.status_code, 200)
             self.assertEqual(response.status_code, 200)
 
 
         self.logout_user()
         self.logout_user()
@@ -235,11 +239,10 @@ class AllThreadsListTests(ThreadsListTestCase):
             self.assertEqual(response.status_code, 403)
             self.assertEqual(response.status_code, 403)
 
 
             self.access_all_categories()
             self.access_all_categories()
-            response = self.client.get('%s?category=%s&list=%s' % (
-                self.api_link,
-                self.category_b.pk,
-                url.strip('/') or 'all',
-            ))
+            response = self.client.get(
+                '%s?category=%s&list=%s' %
+                (self.api_link, self.category_b.pk, url.strip('/') or 'all', )
+            )
             self.assertEqual(response.status_code, 403)
             self.assertEqual(response.status_code, 403)
 
 
     def test_list_renders_categories_picker(self):
     def test_list_renders_categories_picker(self):
@@ -247,7 +250,11 @@ class AllThreadsListTests(ThreadsListTestCase):
         Category(
         Category(
             name='Hidden Category',
             name='Hidden Category',
             slug='hidden-category',
             slug='hidden-category',
-        ).insert_at(self.root, position='last-child', save=True)
+        ).insert_at(
+            self.root,
+            position='last-child',
+            save=True,
+        )
         test_category = Category.objects.get(slug='hidden-category')
         test_category = Category.objects.get(slug='hidden-category')
 
 
         testutils.post_thread(
         testutils.post_thread(
@@ -257,28 +264,22 @@ class AllThreadsListTests(ThreadsListTestCase):
         response = self.client.get('/')
         response = self.client.get('/')
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        self.assertContains(response,
-            'subcategory-%s' % self.category_a.css_class)
+        self.assertContains(response, 'subcategory-%s' % self.category_a.css_class)
 
 
         # readable categories, but non-accessible directly
         # readable categories, but non-accessible directly
-        self.assertNotContains(response,
-            'subcategory-%s' % self.category_b.css_class)
-        self.assertNotContains(response,
-            'subcategory-%s' % self.category_c.css_class)
-        self.assertNotContains(response,
-            'subcategory-%s' % self.category_d.css_class)
-        self.assertNotContains(response,
-            'subcategory-%s' % self.category_f.css_class)
+        self.assertNotContains(response, 'subcategory-%s' % self.category_b.css_class)
+        self.assertNotContains(response, 'subcategory-%s' % self.category_c.css_class)
+        self.assertNotContains(response, 'subcategory-%s' % self.category_d.css_class)
+        self.assertNotContains(response, 'subcategory-%s' % self.category_f.css_class)
 
 
         # hidden category
         # hidden category
-        self.assertNotContains(response,
-            'subcategory-%s' % test_category.css_class)
+        self.assertNotContains(response, 'subcategory-%s' % test_category.css_class)
 
 
         self.access_all_categories()
         self.access_all_categories()
         response = self.client.get(self.api_link)
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response_json = json_loads(smart_str(response.content))
+        response_json = response.json()
         self.assertIn(self.category_a.pk, response_json['subcategories'])
         self.assertIn(self.category_a.pk, response_json['subcategories'])
         self.assertNotIn(self.category_b.pk, response_json['subcategories'])
         self.assertNotIn(self.category_b.pk, response_json['subcategories'])
 
 
@@ -288,24 +289,19 @@ class AllThreadsListTests(ThreadsListTestCase):
         response = self.client.get(self.category_a.get_absolute_url())
         response = self.client.get(self.category_a.get_absolute_url())
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        self.assertContains(response,
-            'subcategory-%s' % self.category_b.css_class)
+        self.assertContains(response, 'subcategory-%s' % self.category_b.css_class)
 
 
         # readable categories, but non-accessible directly
         # readable categories, but non-accessible directly
-        self.assertNotContains(response,
-            'subcategory-%s' % self.category_c.css_class)
-        self.assertNotContains(response,
-            'subcategory-%s' % self.category_d.css_class)
-        self.assertNotContains(response,
-            'subcategory-%s' % self.category_f.css_class)
+        self.assertNotContains(response, 'subcategory-%s' % self.category_c.css_class)
+        self.assertNotContains(response, 'subcategory-%s' % self.category_d.css_class)
+        self.assertNotContains(response, 'subcategory-%s' % self.category_f.css_class)
 
 
         self.access_all_categories()
         self.access_all_categories()
         response = self.client.get('%s?category=%s' % (self.api_link, self.category_a.pk))
         response = self.client.get('%s?category=%s' % (self.api_link, self.category_a.pk))
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response_json = json_loads(smart_str(response.content))
-        self.assertEqual(
-            response_json['subcategories'][0], self.category_b.pk)
+        response_json = response.json()
+        self.assertEqual(response_json['subcategories'][0], self.category_b.pk)
 
 
     def test_display_pinned_threads(self):
     def test_display_pinned_threads(self):
         """
         """
@@ -322,9 +318,7 @@ class AllThreadsListTests(ThreadsListTestCase):
             is_pinned=True,
             is_pinned=True,
         )
         )
 
 
-        standard = testutils.post_thread(
-            category=self.first_category
-        )
+        standard = testutils.post_thread(category=self.first_category)
 
 
         response = self.client.get('/')
         response = self.client.get('/')
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
@@ -373,21 +367,21 @@ class AllThreadsListTests(ThreadsListTestCase):
 
 
     def test_noscript_pagination(self):
     def test_noscript_pagination(self):
         """threads list is paginated for users with js disabled"""
         """threads list is paginated for users with js disabled"""
+        threads_per_page = settings.MISAGO_THREADS_PER_PAGE
+
         threads = []
         threads = []
-        for i in range(settings.MISAGO_THREADS_PER_PAGE * 3):
-            threads.append(testutils.post_thread(
-                category=self.first_category
-            ))
+        for _ in range(settings.MISAGO_THREADS_PER_PAGE * 3):
+            threads.append(testutils.post_thread(category=self.first_category))
 
 
         # secondary page renders
         # secondary page renders
         response = self.client.get('/?page=2')
         response = self.client.get('/?page=2')
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        for thread in threads[:settings.MISAGO_THREADS_PER_PAGE]:
+        for thread in threads[:threads_per_page]:
             self.assertNotContains(response, thread.get_absolute_url())
             self.assertNotContains(response, thread.get_absolute_url())
-        for thread in threads[settings.MISAGO_THREADS_PER_PAGE:settings.MISAGO_THREADS_PER_PAGE * 2]:
+        for thread in threads[threads_per_page:threads_per_page * 2]:
             self.assertContains(response, thread.get_absolute_url())
             self.assertContains(response, thread.get_absolute_url())
-        for thread in threads[settings.MISAGO_THREADS_PER_PAGE * 2:]:
+        for thread in threads[threads_per_page * 2:]:
             self.assertNotContains(response, thread.get_absolute_url())
             self.assertNotContains(response, thread.get_absolute_url())
 
 
         self.assertNotContains(response, '/?page=1')
         self.assertNotContains(response, '/?page=1')
@@ -397,9 +391,9 @@ class AllThreadsListTests(ThreadsListTestCase):
         response = self.client.get('/?page=3')
         response = self.client.get('/?page=3')
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        for thread in threads[settings.MISAGO_THREADS_PER_PAGE:]:
+        for thread in threads[threads_per_page:]:
             self.assertNotContains(response, thread.get_absolute_url())
             self.assertNotContains(response, thread.get_absolute_url())
-        for thread in threads[:settings.MISAGO_THREADS_PER_PAGE]:
+        for thread in threads[:threads_per_page]:
             self.assertContains(response, thread.get_absolute_url())
             self.assertContains(response, thread.get_absolute_url())
 
 
         self.assertContains(response, '/?page=2')
         self.assertContains(response, '/?page=2')
@@ -416,7 +410,11 @@ class CategoryThreadsListTests(ThreadsListTestCase):
         Category(
         Category(
             name='Hidden Category',
             name='Hidden Category',
             slug='hidden-category',
             slug='hidden-category',
-        ).insert_at(self.root, position='last-child', save=True)
+        ).insert_at(
+            self.root,
+            position='last-child',
+            save=True,
+        )
         test_category = Category.objects.get(slug='hidden-category')
         test_category = Category.objects.get(slug='hidden-category')
 
 
         for url in LISTS_URLS:
         for url in LISTS_URLS:
@@ -431,40 +429,46 @@ class CategoryThreadsListTests(ThreadsListTestCase):
         Category(
         Category(
             name='Hidden Category',
             name='Hidden Category',
             slug='hidden-category',
             slug='hidden-category',
-        ).insert_at(self.root, position='last-child', save=True)
+        ).insert_at(
+            self.root,
+            position='last-child',
+            save=True,
+        )
         test_category = Category.objects.get(slug='hidden-category')
         test_category = Category.objects.get(slug='hidden-category')
 
 
         for url in LISTS_URLS:
         for url in LISTS_URLS:
-            override_acl(self.user, {
-                'visible_categories': [test_category.pk],
-                'browseable_categories': [test_category.pk],
-                'categories': {
-                    test_category.pk: {
-                        'can_see': 1,
-                        'can_browse': 0,
-                    }
+            override_acl(
+                self.user, {
+                    'visible_categories': [test_category.pk],
+                    'browseable_categories': [test_category.pk],
+                    'categories': {
+                        test_category.pk: {
+                            'can_see': 1,
+                            'can_browse': 0,
+                        },
+                    },
                 }
                 }
-            });
+            )
 
 
             response = self.client.get(test_category.get_absolute_url() + url)
             response = self.client.get(test_category.get_absolute_url() + url)
             self.assertEqual(response.status_code, 403)
             self.assertEqual(response.status_code, 403)
 
 
-            override_acl(self.user, {
-                'visible_categories': [test_category.pk],
-                'browseable_categories': [test_category.pk],
-                'categories': {
-                    test_category.pk: {
-                        'can_see': 1,
-                        'can_browse': 0,
-                    }
+            override_acl(
+                self.user, {
+                    'visible_categories': [test_category.pk],
+                    'browseable_categories': [test_category.pk],
+                    'categories': {
+                        test_category.pk: {
+                            'can_see': 1,
+                            'can_browse': 0,
+                        },
+                    },
                 }
                 }
-            });
+            )
 
 
-            response = self.client.get('%s?category=%s&list=%s' % (
-                self.api_link,
-                test_category.pk,
-                url.strip('/'),
-            ))
+            response = self.client.get(
+                '%s?category=%s&list=%s' % (self.api_link, test_category.pk, url.strip('/'), )
+            )
             self.assertEqual(response.status_code, 403)
             self.assertEqual(response.status_code, 403)
 
 
     def test_display_pinned_threads(self):
     def test_display_pinned_threads(self):
@@ -482,9 +486,7 @@ class CategoryThreadsListTests(ThreadsListTestCase):
             is_pinned=True,
             is_pinned=True,
         )
         )
 
 
-        standard = testutils.post_thread(
-            category=self.first_category
-        )
+        standard = testutils.post_thread(category=self.first_category)
 
 
         response = self.client.get(self.first_category.get_absolute_url())
         response = self.client.get(self.first_category.get_absolute_url())
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
@@ -544,21 +546,17 @@ class ThreadsVisibilityTests(ThreadsListTestCase):
 
 
         self.assertContains(response, test_thread.get_absolute_url())
         self.assertContains(response, test_thread.get_absolute_url())
 
 
-        self.assertContains(response,
-            'subcategory-%s' % self.category_a.css_class)
-        self.assertContains(response,
-            'subcategory-%s' % self.category_e.css_class)
-        self.assertContains(response,
-            'thread-category-%s' % self.category_a.css_class)
-        self.assertContains(response,
-            'thread-category-%s' % self.category_c.css_class)
+        self.assertContains(response, 'subcategory-%s' % self.category_a.css_class)
+        self.assertContains(response, 'subcategory-%s' % self.category_e.css_class)
+        self.assertContains(response, 'thread-category-%s' % self.category_a.css_class)
+        self.assertContains(response, 'thread-category-%s' % self.category_c.css_class)
 
 
         # api displays same data
         # api displays same data
         self.access_all_categories()
         self.access_all_categories()
         response = self.client.get(self.api_link)
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response_json = json_loads(smart_str(response.content))
+        response_json = response.json()
         self.assertEqual(response_json['results'][0]['id'], test_thread.pk)
         self.assertEqual(response_json['results'][0]['id'], test_thread.pk)
         self.assertEqual(len(response_json['subcategories']), 3)
         self.assertEqual(len(response_json['subcategories']), 3)
         self.assertIn(self.category_a.pk, response_json['subcategories'])
         self.assertIn(self.category_a.pk, response_json['subcategories'])
@@ -571,17 +569,15 @@ class ThreadsVisibilityTests(ThreadsListTestCase):
         # thread displays
         # thread displays
         self.assertContains(response, test_thread.get_absolute_url())
         self.assertContains(response, test_thread.get_absolute_url())
 
 
-        self.assertNotContains(response,
-            'thread-category-%s' % self.category_b.css_class)
-        self.assertContains(response,
-            'thread-category-%s' % self.category_c.css_class)
+        self.assertNotContains(response, 'thread-category-%s' % self.category_b.css_class)
+        self.assertContains(response, 'thread-category-%s' % self.category_c.css_class)
 
 
         # api displays same data
         # api displays same data
         self.access_all_categories()
         self.access_all_categories()
         response = self.client.get('%s?category=%s' % (self.api_link, self.category_b.pk))
         response = self.client.get('%s?category=%s' % (self.api_link, self.category_b.pk))
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response_json = json_loads(smart_str(response.content))
+        response_json = response.json()
         self.assertEqual(response_json['results'][0]['id'], test_thread.pk)
         self.assertEqual(response_json['results'][0]['id'], test_thread.pk)
         self.assertEqual(len(response_json['subcategories']), 2)
         self.assertEqual(len(response_json['subcategories']), 2)
         self.assertEqual(response_json['subcategories'][0], self.category_c.pk)
         self.assertEqual(response_json['subcategories'][0], self.category_c.pk)
@@ -591,33 +587,41 @@ class ThreadsVisibilityTests(ThreadsListTestCase):
         Category(
         Category(
             name='Hidden Category',
             name='Hidden Category',
             slug='hidden-category',
             slug='hidden-category',
-        ).insert_at(self.root, position='last-child', save=True)
-        test_category = Category.objects.get(slug='hidden-category')
-
-        test_thread = testutils.post_thread(
-            category=test_category
+        ).insert_at(
+            self.root,
+            position='last-child',
+            save=True,
         )
         )
 
 
+        test_category = Category.objects.get(slug='hidden-category')
+        test_thread = testutils.post_thread(category=test_category)
+
         response = self.client.get('/')
         response = self.client.get('/')
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
         self.assertContains(response, "empty-message")
         self.assertContains(response, "empty-message")
+        self.assertNotContains(response, test_thread.get_absolute_url())
 
 
     def test_api_hides_hidden_thread(self):
     def test_api_hides_hidden_thread(self):
         """api returns empty due to no permission to see thread"""
         """api returns empty due to no permission to see thread"""
         Category(
         Category(
             name='Hidden Category',
             name='Hidden Category',
             slug='hidden-category',
             slug='hidden-category',
-        ).insert_at(self.root, position='last-child', save=True)
+        ).insert_at(
+            self.root,
+            position='last-child',
+            save=True,
+        )
+
         test_category = Category.objects.get(slug='hidden-category')
         test_category = Category.objects.get(slug='hidden-category')
 
 
-        test_thread = testutils.post_thread(
+        testutils.post_thread(
             category=test_category,
             category=test_category,
         )
         )
 
 
         response = self.client.get(self.api_link)
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response_json = json_loads(smart_str(response.content))
+        response_json = response.json()
         self.assertEqual(len(response_json['results']), 0)
         self.assertEqual(len(response_json['results']), 0)
 
 
     def test_list_user_see_own_unapproved_thread(self):
     def test_list_user_see_own_unapproved_thread(self):
@@ -637,7 +641,7 @@ class ThreadsVisibilityTests(ThreadsListTestCase):
         response = self.client.get(self.api_link)
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response_json = json_loads(smart_str(response.content))
+        response_json = response.json()
         self.assertEqual(response_json['results'][0]['id'], test_thread.pk)
         self.assertEqual(response_json['results'][0]['id'], test_thread.pk)
 
 
     def test_list_user_cant_see_unapproved_thread(self):
     def test_list_user_cant_see_unapproved_thread(self):
@@ -656,7 +660,7 @@ class ThreadsVisibilityTests(ThreadsListTestCase):
         response = self.client.get(self.api_link)
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response_json = json_loads(smart_str(response.content))
+        response_json = response.json()
         self.assertEqual(len(response_json['results']), 0)
         self.assertEqual(len(response_json['results']), 0)
 
 
     def test_list_user_cant_see_hidden_thread(self):
     def test_list_user_cant_see_hidden_thread(self):
@@ -675,7 +679,7 @@ class ThreadsVisibilityTests(ThreadsListTestCase):
         response = self.client.get(self.api_link)
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response_json = json_loads(smart_str(response.content))
+        response_json = response.json()
         self.assertEqual(len(response_json['results']), 0)
         self.assertEqual(len(response_json['results']), 0)
 
 
     def test_list_user_cant_see_own_hidden_thread(self):
     def test_list_user_cant_see_own_hidden_thread(self):
@@ -695,7 +699,7 @@ class ThreadsVisibilityTests(ThreadsListTestCase):
         response = self.client.get(self.api_link)
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response_json = json_loads(smart_str(response.content))
+        response_json = response.json()
         self.assertEqual(len(response_json['results']), 0)
         self.assertEqual(len(response_json['results']), 0)
 
 
     def test_list_user_can_see_own_hidden_thread(self):
     def test_list_user_can_see_own_hidden_thread(self):
@@ -706,79 +710,63 @@ class ThreadsVisibilityTests(ThreadsListTestCase):
             is_hidden=True,
             is_hidden=True,
         )
         )
 
 
-        self.access_all_categories({
-            'can_hide_threads': 1
-        })
+        self.access_all_categories({'can_hide_threads': 1})
 
 
         response = self.client.get('/')
         response = self.client.get('/')
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
         self.assertContains(response, test_thread.get_absolute_url())
         self.assertContains(response, test_thread.get_absolute_url())
 
 
         # test api
         # test api
-        self.access_all_categories({
-            'can_hide_threads': 1
-        })
+        self.access_all_categories({'can_hide_threads': 1})
 
 
         response = self.client.get(self.api_link)
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response_json = json_loads(smart_str(response.content))
+        response_json = response.json()
         self.assertEqual(response_json['results'][0]['id'], test_thread.pk)
         self.assertEqual(response_json['results'][0]['id'], test_thread.pk)
 
 
     def test_list_user_can_see_hidden_thread(self):
     def test_list_user_can_see_hidden_thread(self):
-        """
-        list shows hidden thread that belongs to other user due to permission
-        """
+        """list shows hidden thread that belongs to other user due to permission"""
         test_thread = testutils.post_thread(
         test_thread = testutils.post_thread(
             category=self.category_a,
             category=self.category_a,
             is_hidden=True,
             is_hidden=True,
         )
         )
 
 
-        self.access_all_categories({
-            'can_hide_threads': 1
-        })
+        self.access_all_categories({'can_hide_threads': 1})
 
 
         response = self.client.get('/')
         response = self.client.get('/')
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
         self.assertContains(response, test_thread.get_absolute_url())
         self.assertContains(response, test_thread.get_absolute_url())
 
 
         # test api
         # test api
-        self.access_all_categories({
-            'can_hide_threads': 1
-        })
+        self.access_all_categories({'can_hide_threads': 1})
 
 
         response = self.client.get(self.api_link)
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response_json = json_loads(smart_str(response.content))
+        response_json = response.json()
         self.assertEqual(response_json['results'][0]['id'], test_thread.pk)
         self.assertEqual(response_json['results'][0]['id'], test_thread.pk)
 
 
     def test_list_user_can_see_unapproved_thread(self):
     def test_list_user_can_see_unapproved_thread(self):
-        """
-        list shows hidden thread that belongs to other user due to permission
-        """
+        """list shows hidden thread that belongs to other user due to permission"""
         test_thread = testutils.post_thread(
         test_thread = testutils.post_thread(
             category=self.category_a,
             category=self.category_a,
             is_unapproved=True,
             is_unapproved=True,
         )
         )
 
 
-        self.access_all_categories({
-            'can_approve_content': 1
-        })
+        self.access_all_categories({'can_approve_content': 1})
 
 
         response = self.client.get('/')
         response = self.client.get('/')
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
         self.assertContains(response, test_thread.get_absolute_url())
         self.assertContains(response, test_thread.get_absolute_url())
 
 
         # test api
         # test api
-        self.access_all_categories({
-            'can_approve_content': 1
-        })
+        self.access_all_categories({'can_approve_content': 1})
 
 
         response = self.client.get(self.api_link)
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response_json = json_loads(smart_str(response.content))
+        response_json = response.json()
         self.assertEqual(response_json['results'][0]['id'], test_thread.pk)
         self.assertEqual(response_json['results'][0]['id'], test_thread.pk)
 
 
 
 
@@ -802,13 +790,13 @@ class MyThreadsListTests(ThreadsListTestCase):
         response = self.client.get('%s?list=my' % self.api_link)
         response = self.client.get('%s?list=my' % self.api_link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response_json = json_loads(smart_str(response.content))
+        response_json = response.json()
         self.assertEqual(len(response_json['results']), 0)
         self.assertEqual(len(response_json['results']), 0)
 
 
         self.access_all_categories()
         self.access_all_categories()
         response = self.client.get('%s?list=my&category=%s' % (self.api_link, self.category_a.pk))
         response = self.client.get('%s?list=my&category=%s' % (self.api_link, self.category_a.pk))
 
 
-        response_json = json_loads(smart_str(response.content))
+        response_json = response.json()
         self.assertEqual(len(response_json['results']), 0)
         self.assertEqual(len(response_json['results']), 0)
 
 
     def test_list_renders_test_thread(self):
     def test_list_renders_test_thread(self):
@@ -818,9 +806,7 @@ class MyThreadsListTests(ThreadsListTestCase):
             poster=self.user,
             poster=self.user,
         )
         )
 
 
-        other_thread = testutils.post_thread(
-            category=self.category_a,
-        )
+        other_thread = testutils.post_thread(category=self.category_a)
 
 
         self.access_all_categories()
         self.access_all_categories()
 
 
@@ -841,7 +827,7 @@ class MyThreadsListTests(ThreadsListTestCase):
         response = self.client.get('%s?list=my' % self.api_link)
         response = self.client.get('%s?list=my' % self.api_link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response_json = json_loads(smart_str(response.content))
+        response_json = response.json()
         self.assertEqual(len(response_json['results']), 1)
         self.assertEqual(len(response_json['results']), 1)
         self.assertEqual(response_json['results'][0]['id'], test_thread.pk)
         self.assertEqual(response_json['results'][0]['id'], test_thread.pk)
 
 
@@ -849,7 +835,7 @@ class MyThreadsListTests(ThreadsListTestCase):
         response = self.client.get('%s?list=my&category=%s' % (self.api_link, self.category_a.pk))
         response = self.client.get('%s?list=my&category=%s' % (self.api_link, self.category_a.pk))
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response_json = json_loads(smart_str(response.content))
+        response_json = response.json()
         self.assertEqual(len(response_json['results']), 1)
         self.assertEqual(len(response_json['results']), 1)
         self.assertEqual(response_json['results'][0]['id'], test_thread.pk)
         self.assertEqual(response_json['results'][0]['id'], test_thread.pk)
 
 
@@ -874,20 +860,18 @@ class NewThreadsListTests(ThreadsListTestCase):
         response = self.client.get('%s?list=new' % self.api_link)
         response = self.client.get('%s?list=new' % self.api_link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response_json = json_loads(smart_str(response.content))
+        response_json = response.json()
         self.assertEqual(len(response_json['results']), 0)
         self.assertEqual(len(response_json['results']), 0)
 
 
         self.access_all_categories()
         self.access_all_categories()
         response = self.client.get('%s?list=new&category=%s' % (self.api_link, self.category_a.pk))
         response = self.client.get('%s?list=new&category=%s' % (self.api_link, self.category_a.pk))
 
 
-        response_json = json_loads(smart_str(response.content))
+        response_json = response.json()
         self.assertEqual(len(response_json['results']), 0)
         self.assertEqual(len(response_json['results']), 0)
 
 
     def test_list_renders_new_thread(self):
     def test_list_renders_new_thread(self):
         """list renders new thread"""
         """list renders new thread"""
-        test_thread = testutils.post_thread(
-            category=self.category_a,
-        )
+        test_thread = testutils.post_thread(category=self.category_a)
 
 
         self.access_all_categories()
         self.access_all_categories()
 
 
@@ -906,7 +890,7 @@ class NewThreadsListTests(ThreadsListTestCase):
         response = self.client.get('%s?list=new' % self.api_link)
         response = self.client.get('%s?list=new' % self.api_link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response_json = json_loads(smart_str(response.content))
+        response_json = response.json()
         self.assertEqual(len(response_json['results']), 1)
         self.assertEqual(len(response_json['results']), 1)
         self.assertEqual(response_json['results'][0]['id'], test_thread.pk)
         self.assertEqual(response_json['results'][0]['id'], test_thread.pk)
 
 
@@ -914,7 +898,7 @@ class NewThreadsListTests(ThreadsListTestCase):
         response = self.client.get('%s?list=new&category=%s' % (self.api_link, self.category_a.pk))
         response = self.client.get('%s?list=new&category=%s' % (self.api_link, self.category_a.pk))
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response_json = json_loads(smart_str(response.content))
+        response_json = response.json()
         self.assertEqual(len(response_json['results']), 1)
         self.assertEqual(len(response_json['results']), 1)
         self.assertEqual(response_json['results'][0]['id'], test_thread.pk)
         self.assertEqual(response_json['results'][0]['id'], test_thread.pk)
 
 
@@ -925,11 +909,12 @@ class NewThreadsListTests(ThreadsListTestCase):
 
 
         test_thread = testutils.post_thread(
         test_thread = testutils.post_thread(
             category=self.category_a,
             category=self.category_a,
-            started_on=self.user.joined_on - timedelta(days=2)
+            started_on=self.user.joined_on - timedelta(days=2),
         )
         )
 
 
-        testutils.reply_thread(test_thread,
-            posted_on=self.user.joined_on + timedelta(days=4)
+        testutils.reply_thread(
+            test_thread,
+            posted_on=self.user.joined_on + timedelta(days=4),
         )
         )
 
 
         self.access_all_categories()
         self.access_all_categories()
@@ -949,7 +934,7 @@ class NewThreadsListTests(ThreadsListTestCase):
         response = self.client.get('%s?list=new' % self.api_link)
         response = self.client.get('%s?list=new' % self.api_link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response_json = json_loads(smart_str(response.content))
+        response_json = response.json()
         self.assertEqual(len(response_json['results']), 1)
         self.assertEqual(len(response_json['results']), 1)
         self.assertEqual(response_json['results'][0]['id'], test_thread.pk)
         self.assertEqual(response_json['results'][0]['id'], test_thread.pk)
 
 
@@ -957,7 +942,7 @@ class NewThreadsListTests(ThreadsListTestCase):
         response = self.client.get('%s?list=new&category=%s' % (self.api_link, self.category_a.pk))
         response = self.client.get('%s?list=new&category=%s' % (self.api_link, self.category_a.pk))
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response_json = json_loads(smart_str(response.content))
+        response_json = response.json()
         self.assertEqual(len(response_json['results']), 1)
         self.assertEqual(len(response_json['results']), 1)
         self.assertEqual(response_json['results'][0]['id'], test_thread.pk)
         self.assertEqual(response_json['results'][0]['id'], test_thread.pk)
 
 
@@ -968,9 +953,7 @@ class NewThreadsListTests(ThreadsListTestCase):
 
 
         test_thread = testutils.post_thread(
         test_thread = testutils.post_thread(
             category=self.category_a,
             category=self.category_a,
-            started_on=timezone.now() - timedelta(
-                days=settings.MISAGO_READTRACKER_CUTOFF + 1
-            )
+            started_on=timezone.now() - timedelta(days=settings.MISAGO_READTRACKER_CUTOFF + 1),
         )
         )
 
 
         self.access_all_categories()
         self.access_all_categories()
@@ -990,14 +973,14 @@ class NewThreadsListTests(ThreadsListTestCase):
         response = self.client.get('%s?list=new' % self.api_link)
         response = self.client.get('%s?list=new' % self.api_link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response_json = json_loads(smart_str(response.content))
+        response_json = response.json()
         self.assertEqual(len(response_json['results']), 0)
         self.assertEqual(len(response_json['results']), 0)
 
 
         self.access_all_categories()
         self.access_all_categories()
         response = self.client.get('%s?list=new&category=%s' % (self.api_link, self.category_a.pk))
         response = self.client.get('%s?list=new&category=%s' % (self.api_link, self.category_a.pk))
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response_json = json_loads(smart_str(response.content))
+        response_json = response.json()
         self.assertEqual(len(response_json['results']), 0)
         self.assertEqual(len(response_json['results']), 0)
 
 
     def test_list_hides_user_cutoff_thread(self):
     def test_list_hides_user_cutoff_thread(self):
@@ -1007,7 +990,7 @@ class NewThreadsListTests(ThreadsListTestCase):
 
 
         test_thread = testutils.post_thread(
         test_thread = testutils.post_thread(
             category=self.category_a,
             category=self.category_a,
-            started_on=self.user.joined_on - timedelta(minutes=1)
+            started_on=self.user.joined_on - timedelta(minutes=1),
         )
         )
 
 
         self.access_all_categories()
         self.access_all_categories()
@@ -1027,14 +1010,14 @@ class NewThreadsListTests(ThreadsListTestCase):
         response = self.client.get('%s?list=new' % self.api_link)
         response = self.client.get('%s?list=new' % self.api_link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response_json = json_loads(smart_str(response.content))
+        response_json = response.json()
         self.assertEqual(len(response_json['results']), 0)
         self.assertEqual(len(response_json['results']), 0)
 
 
         self.access_all_categories()
         self.access_all_categories()
         response = self.client.get('%s?list=new&category=%s' % (self.api_link, self.category_a.pk))
         response = self.client.get('%s?list=new&category=%s' % (self.api_link, self.category_a.pk))
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response_json = json_loads(smart_str(response.content))
+        response_json = response.json()
         self.assertEqual(len(response_json['results']), 0)
         self.assertEqual(len(response_json['results']), 0)
 
 
     def test_list_hides_user_read_thread(self):
     def test_list_hides_user_read_thread(self):
@@ -1042,9 +1025,7 @@ class NewThreadsListTests(ThreadsListTestCase):
         self.user.joined_on = timezone.now() - timedelta(days=5)
         self.user.joined_on = timezone.now() - timedelta(days=5)
         self.user.save()
         self.user.save()
 
 
-        test_thread = testutils.post_thread(
-            category=self.category_a
-        )
+        test_thread = testutils.post_thread(category=self.category_a)
 
 
         threadstracker.make_thread_read_aware(self.user, test_thread)
         threadstracker.make_thread_read_aware(self.user, test_thread)
         threadstracker.read_thread(self.user, test_thread, test_thread.last_post)
         threadstracker.read_thread(self.user, test_thread, test_thread.last_post)
@@ -1066,14 +1047,14 @@ class NewThreadsListTests(ThreadsListTestCase):
         response = self.client.get('%s?list=new' % self.api_link)
         response = self.client.get('%s?list=new' % self.api_link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response_json = json_loads(smart_str(response.content))
+        response_json = response.json()
         self.assertEqual(len(response_json['results']), 0)
         self.assertEqual(len(response_json['results']), 0)
 
 
         self.access_all_categories()
         self.access_all_categories()
         response = self.client.get('%s?list=new&category=%s' % (self.api_link, self.category_a.pk))
         response = self.client.get('%s?list=new&category=%s' % (self.api_link, self.category_a.pk))
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response_json = json_loads(smart_str(response.content))
+        response_json = response.json()
         self.assertEqual(len(response_json['results']), 0)
         self.assertEqual(len(response_json['results']), 0)
 
 
     def test_list_hides_category_read_thread(self):
     def test_list_hides_category_read_thread(self):
@@ -1081,9 +1062,7 @@ class NewThreadsListTests(ThreadsListTestCase):
         self.user.joined_on = timezone.now() - timedelta(days=5)
         self.user.joined_on = timezone.now() - timedelta(days=5)
         self.user.save()
         self.user.save()
 
 
-        test_thread = testutils.post_thread(
-            category=self.category_a
-        )
+        test_thread = testutils.post_thread(category=self.category_a)
 
 
         self.user.categoryread_set.create(
         self.user.categoryread_set.create(
             category=self.category_a,
             category=self.category_a,
@@ -1107,14 +1086,14 @@ class NewThreadsListTests(ThreadsListTestCase):
         response = self.client.get('%s?list=new' % self.api_link)
         response = self.client.get('%s?list=new' % self.api_link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response_json = json_loads(smart_str(response.content))
+        response_json = response.json()
         self.assertEqual(len(response_json['results']), 0)
         self.assertEqual(len(response_json['results']), 0)
 
 
         self.access_all_categories()
         self.access_all_categories()
         response = self.client.get('%s?list=new&category=%s' % (self.api_link, self.category_a.pk))
         response = self.client.get('%s?list=new&category=%s' % (self.api_link, self.category_a.pk))
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response_json = json_loads(smart_str(response.content))
+        response_json = response.json()
         self.assertEqual(len(response_json['results']), 0)
         self.assertEqual(len(response_json['results']), 0)
 
 
 
 
@@ -1138,14 +1117,16 @@ class UnreadThreadsListTests(ThreadsListTestCase):
         response = self.client.get('%s?list=unread' % self.api_link)
         response = self.client.get('%s?list=unread' % self.api_link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response_json = json_loads(smart_str(response.content))
+        response_json = response.json()
         self.assertEqual(len(response_json['results']), 0)
         self.assertEqual(len(response_json['results']), 0)
 
 
         self.access_all_categories()
         self.access_all_categories()
-        response = self.client.get('%s?list=unread&category=%s' % (self.api_link, self.category_a.pk))
+        response = self.client.get(
+            '%s?list=unread&category=%s' % (self.api_link, self.category_a.pk)
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response_json = json_loads(smart_str(response.content))
+        response_json = response.json()
         self.assertEqual(len(response_json['results']), 0)
         self.assertEqual(len(response_json['results']), 0)
 
 
     def test_list_renders_unread_thread(self):
     def test_list_renders_unread_thread(self):
@@ -1153,9 +1134,7 @@ class UnreadThreadsListTests(ThreadsListTestCase):
         self.user.joined_on = timezone.now() - timedelta(days=5)
         self.user.joined_on = timezone.now() - timedelta(days=5)
         self.user.save()
         self.user.save()
 
 
-        test_thread = testutils.post_thread(
-            category=self.category_a
-        )
+        test_thread = testutils.post_thread(category=self.category_a)
 
 
         threadstracker.make_thread_read_aware(self.user, test_thread)
         threadstracker.make_thread_read_aware(self.user, test_thread)
         threadstracker.read_thread(self.user, test_thread, test_thread.last_post)
         threadstracker.read_thread(self.user, test_thread, test_thread.last_post)
@@ -1179,15 +1158,17 @@ class UnreadThreadsListTests(ThreadsListTestCase):
         response = self.client.get('%s?list=unread' % self.api_link)
         response = self.client.get('%s?list=unread' % self.api_link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response_json = json_loads(smart_str(response.content))
+        response_json = response.json()
         self.assertEqual(len(response_json['results']), 1)
         self.assertEqual(len(response_json['results']), 1)
         self.assertEqual(response_json['results'][0]['id'], test_thread.pk)
         self.assertEqual(response_json['results'][0]['id'], test_thread.pk)
 
 
         self.access_all_categories()
         self.access_all_categories()
-        response = self.client.get('%s?list=unread&category=%s' % (self.api_link, self.category_a.pk))
+        response = self.client.get(
+            '%s?list=unread&category=%s' % (self.api_link, self.category_a.pk)
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response_json = json_loads(smart_str(response.content))
+        response_json = response.json()
         self.assertEqual(len(response_json['results']), 1)
         self.assertEqual(len(response_json['results']), 1)
         self.assertEqual(response_json['results'][0]['id'], test_thread.pk)
         self.assertEqual(response_json['results'][0]['id'], test_thread.pk)
 
 
@@ -1196,9 +1177,7 @@ class UnreadThreadsListTests(ThreadsListTestCase):
         self.user.joined_on = timezone.now() - timedelta(days=5)
         self.user.joined_on = timezone.now() - timedelta(days=5)
         self.user.save()
         self.user.save()
 
 
-        test_thread = testutils.post_thread(
-            category=self.category_a
-        )
+        test_thread = testutils.post_thread(category=self.category_a)
 
 
         self.access_all_categories()
         self.access_all_categories()
 
 
@@ -1217,14 +1196,16 @@ class UnreadThreadsListTests(ThreadsListTestCase):
         response = self.client.get('%s?list=unread' % self.api_link)
         response = self.client.get('%s?list=unread' % self.api_link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response_json = json_loads(smart_str(response.content))
+        response_json = response.json()
         self.assertEqual(len(response_json['results']), 0)
         self.assertEqual(len(response_json['results']), 0)
 
 
         self.access_all_categories()
         self.access_all_categories()
-        response = self.client.get('%s?list=unread&category=%s' % (self.api_link, self.category_a.pk))
+        response = self.client.get(
+            '%s?list=unread&category=%s' % (self.api_link, self.category_a.pk)
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response_json = json_loads(smart_str(response.content))
+        response_json = response.json()
         self.assertEqual(len(response_json['results']), 0)
         self.assertEqual(len(response_json['results']), 0)
 
 
     def test_list_hides_read_thread(self):
     def test_list_hides_read_thread(self):
@@ -1232,9 +1213,7 @@ class UnreadThreadsListTests(ThreadsListTestCase):
         self.user.joined_on = timezone.now() - timedelta(days=5)
         self.user.joined_on = timezone.now() - timedelta(days=5)
         self.user.save()
         self.user.save()
 
 
-        test_thread = testutils.post_thread(
-            category=self.category_a
-        )
+        test_thread = testutils.post_thread(category=self.category_a)
 
 
         threadstracker.make_thread_read_aware(self.user, test_thread)
         threadstracker.make_thread_read_aware(self.user, test_thread)
         threadstracker.read_thread(self.user, test_thread, test_thread.last_post)
         threadstracker.read_thread(self.user, test_thread, test_thread.last_post)
@@ -1256,14 +1235,16 @@ class UnreadThreadsListTests(ThreadsListTestCase):
         response = self.client.get('%s?list=unread' % self.api_link)
         response = self.client.get('%s?list=unread' % self.api_link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response_json = json_loads(smart_str(response.content))
+        response_json = response.json()
         self.assertEqual(len(response_json['results']), 0)
         self.assertEqual(len(response_json['results']), 0)
 
 
         self.access_all_categories()
         self.access_all_categories()
-        response = self.client.get('%s?list=unread&category=%s' % (self.api_link, self.category_a.pk))
+        response = self.client.get(
+            '%s?list=unread&category=%s' % (self.api_link, self.category_a.pk)
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response_json = json_loads(smart_str(response.content))
+        response_json = response.json()
         self.assertEqual(len(response_json['results']), 0)
         self.assertEqual(len(response_json['results']), 0)
 
 
     def test_list_hides_global_cutoff_thread(self):
     def test_list_hides_global_cutoff_thread(self):
@@ -1273,9 +1254,7 @@ class UnreadThreadsListTests(ThreadsListTestCase):
 
 
         test_thread = testutils.post_thread(
         test_thread = testutils.post_thread(
             category=self.category_a,
             category=self.category_a,
-            started_on=timezone.now() - timedelta(
-                days=settings.MISAGO_READTRACKER_CUTOFF + 5
-            )
+            started_on=timezone.now() - timedelta(days=settings.MISAGO_READTRACKER_CUTOFF + 5),
         )
         )
 
 
         threadstracker.make_thread_read_aware(self.user, test_thread)
         threadstracker.make_thread_read_aware(self.user, test_thread)
@@ -1300,14 +1279,16 @@ class UnreadThreadsListTests(ThreadsListTestCase):
         response = self.client.get('%s?list=unread' % self.api_link)
         response = self.client.get('%s?list=unread' % self.api_link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response_json = json_loads(smart_str(response.content))
+        response_json = response.json()
         self.assertEqual(len(response_json['results']), 0)
         self.assertEqual(len(response_json['results']), 0)
 
 
         self.access_all_categories()
         self.access_all_categories()
-        response = self.client.get('%s?list=unread&category=%s' % (self.api_link, self.category_a.pk))
+        response = self.client.get(
+            '%s?list=unread&category=%s' % (self.api_link, self.category_a.pk)
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response_json = json_loads(smart_str(response.content))
+        response_json = response.json()
         self.assertEqual(len(response_json['results']), 0)
         self.assertEqual(len(response_json['results']), 0)
 
 
     def test_list_hides_user_cutoff_thread(self):
     def test_list_hides_user_cutoff_thread(self):
@@ -1317,13 +1298,16 @@ class UnreadThreadsListTests(ThreadsListTestCase):
 
 
         test_thread = testutils.post_thread(
         test_thread = testutils.post_thread(
             category=self.category_a,
             category=self.category_a,
-            started_on=self.user.joined_on - timedelta(days=2)
+            started_on=self.user.joined_on - timedelta(days=2),
         )
         )
 
 
         threadstracker.make_thread_read_aware(self.user, test_thread)
         threadstracker.make_thread_read_aware(self.user, test_thread)
         threadstracker.read_thread(self.user, test_thread, test_thread.last_post)
         threadstracker.read_thread(self.user, test_thread, test_thread.last_post)
 
 
-        testutils.reply_thread(test_thread, posted_on=test_thread.started_on + timedelta(days=1))
+        testutils.reply_thread(
+            test_thread,
+            posted_on=test_thread.started_on + timedelta(days=1),
+        )
 
 
         self.access_all_categories()
         self.access_all_categories()
 
 
@@ -1342,14 +1326,16 @@ class UnreadThreadsListTests(ThreadsListTestCase):
         response = self.client.get('%s?list=unread' % self.api_link)
         response = self.client.get('%s?list=unread' % self.api_link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response_json = json_loads(smart_str(response.content))
+        response_json = response.json()
         self.assertEqual(len(response_json['results']), 0)
         self.assertEqual(len(response_json['results']), 0)
 
 
         self.access_all_categories()
         self.access_all_categories()
-        response = self.client.get('%s?list=unread&category=%s' % (self.api_link, self.category_a.pk))
+        response = self.client.get(
+            '%s?list=unread&category=%s' % (self.api_link, self.category_a.pk)
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response_json = json_loads(smart_str(response.content))
+        response_json = response.json()
         self.assertEqual(len(response_json['results']), 0)
         self.assertEqual(len(response_json['results']), 0)
 
 
     def test_list_hides_category_cutoff_thread(self):
     def test_list_hides_category_cutoff_thread(self):
@@ -1359,12 +1345,11 @@ class UnreadThreadsListTests(ThreadsListTestCase):
 
 
         test_thread = testutils.post_thread(
         test_thread = testutils.post_thread(
             category=self.category_a,
             category=self.category_a,
-            started_on=self.user.joined_on - timedelta(days=2)
+            started_on=self.user.joined_on - timedelta(days=2),
         )
         )
 
 
         threadstracker.make_thread_read_aware(self.user, test_thread)
         threadstracker.make_thread_read_aware(self.user, test_thread)
-        threadstracker.read_thread(
-            self.user, test_thread, test_thread.last_post)
+        threadstracker.read_thread(self.user, test_thread, test_thread.last_post)
 
 
         testutils.reply_thread(test_thread)
         testutils.reply_thread(test_thread)
 
 
@@ -1390,23 +1375,23 @@ class UnreadThreadsListTests(ThreadsListTestCase):
         response = self.client.get('%s?list=unread' % self.api_link)
         response = self.client.get('%s?list=unread' % self.api_link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response_json = json_loads(smart_str(response.content))
+        response_json = response.json()
         self.assertEqual(len(response_json['results']), 0)
         self.assertEqual(len(response_json['results']), 0)
 
 
         self.access_all_categories()
         self.access_all_categories()
-        response = self.client.get('%s?list=unread&category=%s' % (self.api_link, self.category_a.pk))
+        response = self.client.get(
+            '%s?list=unread&category=%s' % (self.api_link, self.category_a.pk)
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response_json = json_loads(smart_str(response.content))
+        response_json = response.json()
         self.assertEqual(len(response_json['results']), 0)
         self.assertEqual(len(response_json['results']), 0)
 
 
 
 
 class SubscribedThreadsListTests(ThreadsListTestCase):
 class SubscribedThreadsListTests(ThreadsListTestCase):
     def test_list_shows_subscribed_thread(self):
     def test_list_shows_subscribed_thread(self):
         """list shows subscribed thread"""
         """list shows subscribed thread"""
-        test_thread = testutils.post_thread(
-            category=self.category_a
-        )
+        test_thread = testutils.post_thread(category=self.category_a)
         self.user.subscription_set.create(
         self.user.subscription_set.create(
             thread=test_thread,
             thread=test_thread,
             category=self.category_a,
             category=self.category_a,
@@ -1430,23 +1415,23 @@ class SubscribedThreadsListTests(ThreadsListTestCase):
         response = self.client.get('%s?list=subscribed' % self.api_link)
         response = self.client.get('%s?list=subscribed' % self.api_link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response_json = json_loads(smart_str(response.content))
+        response_json = response.json()
         self.assertEqual(len(response_json['results']), 1)
         self.assertEqual(len(response_json['results']), 1)
         self.assertContains(response, test_thread.get_absolute_url())
         self.assertContains(response, test_thread.get_absolute_url())
 
 
         self.access_all_categories()
         self.access_all_categories()
-        response = self.client.get('%s?list=subscribed&category=%s' % (self.api_link, self.category_a.pk))
+        response = self.client.get(
+            '%s?list=subscribed&category=%s' % (self.api_link, self.category_a.pk)
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response_json = json_loads(smart_str(response.content))
+        response_json = response.json()
         self.assertEqual(len(response_json['results']), 1)
         self.assertEqual(len(response_json['results']), 1)
         self.assertContains(response, test_thread.get_absolute_url())
         self.assertContains(response, test_thread.get_absolute_url())
 
 
     def test_list_hides_unsubscribed_thread(self):
     def test_list_hides_unsubscribed_thread(self):
         """list shows subscribed thread"""
         """list shows subscribed thread"""
-        test_thread = testutils.post_thread(
-            category=self.category_a
-        )
+        test_thread = testutils.post_thread(category=self.category_a)
 
 
         self.access_all_categories()
         self.access_all_categories()
 
 
@@ -1465,15 +1450,17 @@ class SubscribedThreadsListTests(ThreadsListTestCase):
         response = self.client.get('%s?list=subscribed' % self.api_link)
         response = self.client.get('%s?list=subscribed' % self.api_link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response_json = json_loads(smart_str(response.content))
+        response_json = response.json()
         self.assertEqual(len(response_json['results']), 0)
         self.assertEqual(len(response_json['results']), 0)
         self.assertNotContains(response, test_thread.get_absolute_url())
         self.assertNotContains(response, test_thread.get_absolute_url())
 
 
         self.access_all_categories()
         self.access_all_categories()
-        response = self.client.get('%s?list=subscribed&category=%s' % (self.api_link, self.category_a.pk))
+        response = self.client.get(
+            '%s?list=subscribed&category=%s' % (self.api_link, self.category_a.pk)
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response_json = json_loads(smart_str(response.content))
+        response_json = response.json()
         self.assertEqual(len(response_json['results']), 0)
         self.assertEqual(len(response_json['results']), 0)
         self.assertNotContains(response, test_thread.get_absolute_url())
         self.assertNotContains(response, test_thread.get_absolute_url())
 
 
@@ -1482,8 +1469,7 @@ class UnapprovedListTests(ThreadsListTestCase):
     def test_list_errors_without_permission(self):
     def test_list_errors_without_permission(self):
         """list errors if user has no permission to access it"""
         """list errors if user has no permission to access it"""
         TEST_URLS = (
         TEST_URLS = (
-            '/unapproved/',
-            self.category_a.get_absolute_url() + 'unapproved/',
+            '/unapproved/', self.category_a.get_absolute_url() + 'unapproved/',
             '%s?list=unapproved' % self.api_link,
             '%s?list=unapproved' % self.api_link,
         )
         )
 
 
@@ -1494,9 +1480,7 @@ class UnapprovedListTests(ThreadsListTestCase):
 
 
         # approval perm has no influence on visibility
         # approval perm has no influence on visibility
         for test_url in TEST_URLS:
         for test_url in TEST_URLS:
-            self.access_all_categories({
-                'can_approve_content': True
-            })
+            self.access_all_categories({'can_approve_content': True})
 
 
             self.access_all_categories()
             self.access_all_categories()
             response = self.client.get(test_url)
             response = self.client.get(test_url)
@@ -1505,7 +1489,7 @@ class UnapprovedListTests(ThreadsListTestCase):
         # approval perm has no influence on visibility
         # approval perm has no influence on visibility
         for test_url in TEST_URLS:
         for test_url in TEST_URLS:
             self.access_all_categories(base_acl={
             self.access_all_categories(base_acl={
-                'can_see_unapproved_content_lists': True
+                'can_see_unapproved_content_lists': True,
             })
             })
 
 
             self.access_all_categories()
             self.access_all_categories()
@@ -1525,10 +1509,11 @@ class UnapprovedListTests(ThreadsListTestCase):
         )
         )
 
 
         self.access_all_categories({
         self.access_all_categories({
-            'can_approve_content': True
+            'can_approve_content': True,
         }, {
         }, {
-            'can_see_unapproved_content_lists': True
+            'can_see_unapproved_content_lists': True,
         })
         })
+
         response = self.client.get('/unapproved/')
         response = self.client.get('/unapproved/')
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
         self.assertContains(response, visible_thread.get_absolute_url())
         self.assertContains(response, visible_thread.get_absolute_url())
@@ -1537,8 +1522,9 @@ class UnapprovedListTests(ThreadsListTestCase):
         self.access_all_categories({
         self.access_all_categories({
             'can_approve_content': True
             'can_approve_content': True
         }, {
         }, {
-            'can_see_unapproved_content_lists': True
+            'can_see_unapproved_content_lists': True,
         })
         })
+
         response = self.client.get(self.category_a.get_absolute_url() + 'unapproved/')
         response = self.client.get(self.category_a.get_absolute_url() + 'unapproved/')
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
         self.assertContains(response, visible_thread.get_absolute_url())
         self.assertContains(response, visible_thread.get_absolute_url())
@@ -1548,17 +1534,16 @@ class UnapprovedListTests(ThreadsListTestCase):
         self.access_all_categories({
         self.access_all_categories({
             'can_approve_content': True
             'can_approve_content': True
         }, {
         }, {
-            'can_see_unapproved_content_lists': True
+            'can_see_unapproved_content_lists': True,
         })
         })
+
         response = self.client.get('%s?list=unapproved' % self.api_link)
         response = self.client.get('%s?list=unapproved' % self.api_link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
         self.assertContains(response, visible_thread.get_absolute_url())
         self.assertContains(response, visible_thread.get_absolute_url())
         self.assertNotContains(response, hidden_thread.get_absolute_url())
         self.assertNotContains(response, hidden_thread.get_absolute_url())
 
 
     def test_list_shows_owned_threads_for_unapproving_user(self):
     def test_list_shows_owned_threads_for_unapproving_user(self):
-        """
-        list shows owned threads with unapproved posts for user without perm
-        """
+        """list shows owned threads with unapproved posts for user without perm"""
         visible_thread = testutils.post_thread(
         visible_thread = testutils.post_thread(
             poster=self.user,
             poster=self.user,
             category=self.category_b,
             category=self.category_b,
@@ -1571,7 +1556,7 @@ class UnapprovedListTests(ThreadsListTestCase):
         )
         )
 
 
         self.access_all_categories(base_acl={
         self.access_all_categories(base_acl={
-            'can_see_unapproved_content_lists': True
+            'can_see_unapproved_content_lists': True,
         })
         })
         response = self.client.get('/unapproved/')
         response = self.client.get('/unapproved/')
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
@@ -1579,7 +1564,7 @@ class UnapprovedListTests(ThreadsListTestCase):
         self.assertNotContains(response, hidden_thread.get_absolute_url())
         self.assertNotContains(response, hidden_thread.get_absolute_url())
 
 
         self.access_all_categories(base_acl={
         self.access_all_categories(base_acl={
-            'can_see_unapproved_content_lists': True
+            'can_see_unapproved_content_lists': True,
         })
         })
         response = self.client.get(self.category_a.get_absolute_url() + 'unapproved/')
         response = self.client.get(self.category_a.get_absolute_url() + 'unapproved/')
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
@@ -1588,7 +1573,7 @@ class UnapprovedListTests(ThreadsListTestCase):
 
 
         # test api
         # test api
         self.access_all_categories(base_acl={
         self.access_all_categories(base_acl={
-            'can_see_unapproved_content_lists': True
+            'can_see_unapproved_content_lists': True,
         })
         })
         response = self.client.get('%s?list=unapproved' % self.api_link)
         response = self.client.get('%s?list=unapproved' % self.api_link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
@@ -1604,9 +1589,7 @@ class OwnerOnlyThreadsVisibilityTests(AuthenticatedUserTestCase):
 
 
     def override_acl(self, user):
     def override_acl(self, user):
         category_acl = user.acl_cache['categories'][self.category.pk].copy()
         category_acl = user.acl_cache['categories'][self.category.pk].copy()
-        category_acl.update({
-            'can_see_all_threads': 0
-        })
+        category_acl.update({'can_see_all_threads': 0})
         user.acl_cache['categories'][self.category.pk] = category_acl
         user.acl_cache['categories'][self.category.pk] = category_acl
 
 
         override_acl(user, user.acl_cache)
         override_acl(user, user.acl_cache)

+ 39 - 70
misago/threads/tests/test_threadview.py

@@ -3,7 +3,6 @@ from misago.categories.models import Category
 from misago.conf import settings
 from misago.conf import settings
 from misago.threads import testutils
 from misago.threads import testutils
 from misago.threads.events import record_event
 from misago.threads.events import record_event
-from misago.threads.models import Post, Thread
 from misago.threads.moderation import threads as threads_moderation
 from misago.threads.moderation import threads as threads_moderation
 from misago.threads.moderation import hide_post
 from misago.threads.moderation import hide_post
 from misago.users.testutils import AuthenticatedUserTestCase
 from misago.users.testutils import AuthenticatedUserTestCase
@@ -44,8 +43,8 @@ class ThreadViewTestCase(AuthenticatedUserTestCase):
 
 
         override_acl(self.user, {
         override_acl(self.user, {
             'categories': {
             'categories': {
-                self.category.pk: category_acl
-            }
+                self.category.pk: category_acl,
+            },
         })
         })
 
 
 
 
@@ -56,10 +55,8 @@ class ThreadVisibilityTests(ThreadViewTestCase):
         self.assertContains(response, self.thread.title)
         self.assertContains(response, self.thread.title)
 
 
     def test_view_shows_owner_thread(self):
     def test_view_shows_owner_thread(self):
-        """view handles "owned threads only" """
-        self.override_acl({
-            'can_see_all_threads': 0
-        })
+        """view handles "owned threads" only"""
+        self.override_acl({'can_see_all_threads': 0})
 
 
         response = self.client.get(self.thread.get_absolute_url())
         response = self.client.get(self.thread.get_absolute_url())
         self.assertEqual(response.status_code, 404)
         self.assertEqual(response.status_code, 404)
@@ -67,34 +64,26 @@ class ThreadVisibilityTests(ThreadViewTestCase):
         self.thread.starter = self.user
         self.thread.starter = self.user
         self.thread.save()
         self.thread.save()
 
 
-        self.override_acl({
-            'can_see_all_threads': 0
-        })
+        self.override_acl({'can_see_all_threads': 0})
 
 
         response = self.client.get(self.thread.get_absolute_url())
         response = self.client.get(self.thread.get_absolute_url())
         self.assertContains(response, self.thread.title)
         self.assertContains(response, self.thread.title)
 
 
     def test_view_validates_category_permissions(self):
     def test_view_validates_category_permissions(self):
         """view validates category visiblity"""
         """view validates category visiblity"""
-        self.override_acl({
-            'can_see': 0
-        })
+        self.override_acl({'can_see': 0})
 
 
         response = self.client.get(self.thread.get_absolute_url())
         response = self.client.get(self.thread.get_absolute_url())
         self.assertEqual(response.status_code, 404)
         self.assertEqual(response.status_code, 404)
 
 
-        self.override_acl({
-            'can_browse': 0
-        })
+        self.override_acl({'can_browse': 0})
 
 
         response = self.client.get(self.thread.get_absolute_url())
         response = self.client.get(self.thread.get_absolute_url())
         self.assertEqual(response.status_code, 404)
         self.assertEqual(response.status_code, 404)
 
 
     def test_view_shows_unapproved_thread(self):
     def test_view_shows_unapproved_thread(self):
         """view handles unapproved thread"""
         """view handles unapproved thread"""
-        self.override_acl({
-            'can_approve_content': 0
-        })
+        self.override_acl({'can_approve_content': 0})
 
 
         self.thread.is_unapproved = True
         self.thread.is_unapproved = True
         self.thread.save()
         self.thread.save()
@@ -103,9 +92,7 @@ class ThreadVisibilityTests(ThreadViewTestCase):
         self.assertEqual(response.status_code, 404)
         self.assertEqual(response.status_code, 404)
 
 
         # grant permission to see unapproved content
         # grant permission to see unapproved content
-        self.override_acl({
-            'can_approve_content': 1
-        })
+        self.override_acl({'can_approve_content': 1})
 
 
         response = self.client.get(self.thread.get_absolute_url())
         response = self.client.get(self.thread.get_absolute_url())
         self.assertContains(response, self.thread.title)
         self.assertContains(response, self.thread.title)
@@ -115,18 +102,14 @@ class ThreadVisibilityTests(ThreadViewTestCase):
         self.thread.starter = self.user
         self.thread.starter = self.user
         self.thread.save()
         self.thread.save()
 
 
-        self.override_acl({
-            'can_approve_content': 0
-        })
+        self.override_acl({'can_approve_content': 0})
 
 
         response = self.client.get(self.thread.get_absolute_url())
         response = self.client.get(self.thread.get_absolute_url())
         self.assertContains(response, self.thread.title)
         self.assertContains(response, self.thread.title)
 
 
     def test_view_shows_hidden_thread(self):
     def test_view_shows_hidden_thread(self):
         """view handles hidden thread"""
         """view handles hidden thread"""
-        self.override_acl({
-            'can_hide_threads': 0
-        })
+        self.override_acl({'can_hide_threads': 0})
 
 
         self.thread.is_hidden = True
         self.thread.is_hidden = True
         self.thread.save()
         self.thread.save()
@@ -142,9 +125,7 @@ class ThreadVisibilityTests(ThreadViewTestCase):
         self.assertEqual(response.status_code, 404)
         self.assertEqual(response.status_code, 404)
 
 
         # grant permission to see hidden content
         # grant permission to see hidden content
-        self.override_acl({
-            'can_hide_threads': 1
-        })
+        self.override_acl({'can_hide_threads': 1})
 
 
         response = self.client.get(self.thread.get_absolute_url())
         response = self.client.get(self.thread.get_absolute_url())
         self.assertContains(response, self.thread.title)
         self.assertContains(response, self.thread.title)
@@ -190,9 +171,7 @@ class ThreadPostsVisibilityTests(ThreadViewTestCase):
         self.assertNotContains(response, post.parsed)
         self.assertNotContains(response, post.parsed)
 
 
         # permission to hide own posts isn't enought to see post content
         # permission to hide own posts isn't enought to see post content
-        self.override_acl({
-            'can_hide_own_posts': 1
-        })
+        self.override_acl({'can_hide_own_posts': 1})
 
 
         response = self.client.get(self.thread.get_absolute_url())
         response = self.client.get(self.thread.get_absolute_url())
         self.assertContains(response, post.get_absolute_url())
         self.assertContains(response, post.get_absolute_url())
@@ -200,13 +179,13 @@ class ThreadPostsVisibilityTests(ThreadViewTestCase):
         self.assertNotContains(response, post.parsed)
         self.assertNotContains(response, post.parsed)
 
 
         # post's content is displayed after permission to see posts is granted
         # post's content is displayed after permission to see posts is granted
-        self.override_acl({
-            'can_hide_posts': 1
-        })
+        self.override_acl({'can_hide_posts': 1})
 
 
         response = self.client.get(self.thread.get_absolute_url())
         response = self.client.get(self.thread.get_absolute_url())
         self.assertContains(response, post.get_absolute_url())
         self.assertContains(response, post.get_absolute_url())
-        self.assertContains(response, "This post is hidden. Only users with permission may see its contents.")
+        self.assertContains(
+            response, "This post is hidden. Only users with permission may see its contents."
+        )
         self.assertNotContains(response, "This post is hidden. You cannot not see its contents.")
         self.assertNotContains(response, "This post is hidden. You cannot not see its contents.")
         self.assertContains(response, post.parsed)
         self.assertContains(response, post.parsed)
 
 
@@ -219,9 +198,7 @@ class ThreadPostsVisibilityTests(ThreadViewTestCase):
         self.assertNotContains(response, post.get_absolute_url())
         self.assertNotContains(response, post.get_absolute_url())
 
 
         # post displays because we have permission to approve unapproved content
         # post displays because we have permission to approve unapproved content
-        self.override_acl({
-            'can_approve_content': 1
-        })
+        self.override_acl({'can_approve_content': 1})
 
 
         response = self.client.get(self.thread.get_absolute_url())
         response = self.client.get(self.thread.get_absolute_url())
         self.assertContains(response, post.get_absolute_url())
         self.assertContains(response, post.get_absolute_url())
@@ -232,9 +209,7 @@ class ThreadPostsVisibilityTests(ThreadViewTestCase):
         post.poster = self.user
         post.poster = self.user
         post.save()
         post.save()
 
 
-        self.override_acl({
-            'can_approve_content': 0
-        })
+        self.override_acl({'can_approve_content': 0})
 
 
         response = self.client.get(self.thread.get_absolute_url())
         response = self.client.get(self.thread.get_absolute_url())
         self.assertContains(response, post.get_absolute_url())
         self.assertContains(response, post.get_absolute_url())
@@ -245,7 +220,7 @@ class ThreadPostsVisibilityTests(ThreadViewTestCase):
 class ThreadEventVisibilityTests(ThreadViewTestCase):
 class ThreadEventVisibilityTests(ThreadViewTestCase):
     def test_thread_events_render(self):
     def test_thread_events_render(self):
         """different thread events render"""
         """different thread events render"""
-        TEST_ACTIONS = (
+        TEST_ACTIONS = [
             (threads_moderation.pin_thread_globally, "Thread has been pinned globally."),
             (threads_moderation.pin_thread_globally, "Thread has been pinned globally."),
             (threads_moderation.pin_thread_locally, "Thread has been pinned locally."),
             (threads_moderation.pin_thread_locally, "Thread has been pinned locally."),
             (threads_moderation.unpin_thread, "Thread has been unpinned."),
             (threads_moderation.unpin_thread, "Thread has been unpinned."),
@@ -254,16 +229,13 @@ class ThreadEventVisibilityTests(ThreadViewTestCase):
             (threads_moderation.open_thread, "Thread has been opened."),
             (threads_moderation.open_thread, "Thread has been opened."),
             (threads_moderation.hide_thread, "Thread has been made hidden."),
             (threads_moderation.hide_thread, "Thread has been made hidden."),
             (threads_moderation.unhide_thread, "Thread has been revealed."),
             (threads_moderation.unhide_thread, "Thread has been revealed."),
-        )
+        ]
 
 
         self.thread.is_unapproved = True
         self.thread.is_unapproved = True
         self.thread.save()
         self.thread.save()
 
 
         for action, message in TEST_ACTIONS:
         for action, message in TEST_ACTIONS:
-            self.override_acl({
-                'can_approve_content': 1,
-                'can_hide_threads': 1,
-            })
+            self.override_acl({'can_approve_content': 1, 'can_hide_threads': 1})
 
 
             self.thread.post_set.filter(is_event=True).delete()
             self.thread.post_set.filter(is_event=True).delete()
             action(MockRequest(self.user), self.thread)
             action(MockRequest(self.user), self.thread)
@@ -277,10 +249,7 @@ class ThreadEventVisibilityTests(ThreadViewTestCase):
 
 
             # hidden events don't render without permission
             # hidden events don't render without permission
             hide_post(self.user, event)
             hide_post(self.user, event)
-            self.override_acl({
-                'can_approve_content': 1,
-                'can_hide_threads': 1,
-            })
+            self.override_acl({'can_approve_content': 1, 'can_hide_threads': 1})
 
 
             response = self.client.get(self.thread.get_absolute_url())
             response = self.client.get(self.thread.get_absolute_url())
             self.assertNotContains(response, event.get_absolute_url())
             self.assertNotContains(response, event.get_absolute_url())
@@ -317,7 +286,7 @@ class ThreadEventVisibilityTests(ThreadViewTestCase):
         events_limit = settings.MISAGO_EVENTS_PER_PAGE
         events_limit = settings.MISAGO_EVENTS_PER_PAGE
         events = []
         events = []
 
 
-        for i in range(events_limit + 5):
+        for _ in range(events_limit + 5):
             event = record_event(MockRequest(self.user), self.thread, 'closed')
             event = record_event(MockRequest(self.user), self.thread, 'closed')
             events.append(event)
             events.append(event)
 
 
@@ -335,12 +304,12 @@ class ThreadEventVisibilityTests(ThreadViewTestCase):
         events_limit = settings.MISAGO_EVENTS_PER_PAGE
         events_limit = settings.MISAGO_EVENTS_PER_PAGE
         events = []
         events = []
 
 
-        for i in range(events_limit + 5):
+        for _ in range(events_limit + 5):
             event = record_event(MockRequest(self.user), self.thread, 'closed')
             event = record_event(MockRequest(self.user), self.thread, 'closed')
             events.append(event)
             events.append(event)
 
 
         posts = []
         posts = []
-        for i in range(posts_limit - 1):
+        for _ in range(posts_limit - 1):
             post = testutils.reply_thread(self.thread)
             post = testutils.reply_thread(self.thread)
             posts.append(post)
             posts.append(post)
 
 
@@ -353,9 +322,9 @@ class ThreadEventVisibilityTests(ThreadViewTestCase):
             self.assertContains(response, post.get_absolute_url())
             self.assertContains(response, post.get_absolute_url())
 
 
         # add second page to thread with more events
         # add second page to thread with more events
-        for i in range(posts_limit):
+        for _ in range(posts_limit):
             post = testutils.reply_thread(self.thread)
             post = testutils.reply_thread(self.thread)
-        for i in range(events_limit):
+        for _ in range(events_limit):
             event = record_event(MockRequest(self.user), self.thread, 'closed')
             event = record_event(MockRequest(self.user), self.thread, 'closed')
             events.append(event)
             events.append(event)
 
 
@@ -376,7 +345,9 @@ class ThreadEventVisibilityTests(ThreadViewTestCase):
 
 
     def test_changed_thread_title_event_renders(self):
     def test_changed_thread_title_event_renders(self):
         """changed thread title event renders"""
         """changed thread title event renders"""
-        threads_moderation.change_thread_title(MockRequest(self.user), self.thread, "Lorem renamed ipsum!")
+        threads_moderation.change_thread_title(
+            MockRequest(self.user), self.thread, "Lorem renamed ipsum!"
+        )
 
 
         event = self.thread.post_set.filter(is_event=True)[0]
         event = self.thread.post_set.filter(is_event=True)[0]
         self.assertEqual(event.event_type, 'changed_title')
         self.assertEqual(event.event_type, 'changed_title')
@@ -425,7 +396,7 @@ class ThreadAttachmentsViewTests(ThreadViewTestCase):
             'filetype': 'ZIP',
             'filetype': 'ZIP',
             'is_image': False,
             'is_image': False,
             'uploaded_on': '2016-10-22T21:17:40.408710Z',
             'uploaded_on': '2016-10-22T21:17:40.408710Z',
-            'uploader_name': 'BobBoberson'
+            'uploader_name': 'BobBoberson',
         }
         }
 
 
         json.update(data)
         json.update(data)
@@ -440,7 +411,7 @@ class ThreadAttachmentsViewTests(ThreadViewTestCase):
                 'url': {
                 'url': {
                     'index': '/attachment/loremipsum-123/',
                     'index': '/attachment/loremipsum-123/',
                     'thumb': None,
                     'thumb': None,
-                    'uploader': '/user/bobboberson-123/'
+                    'uploader': '/user/bobboberson-123/',
                 },
                 },
                 'filename': 'Archiwum-1.zip',
                 'filename': 'Archiwum-1.zip',
             }),
             }),
@@ -448,19 +419,19 @@ class ThreadAttachmentsViewTests(ThreadViewTestCase):
                 'url': {
                 'url': {
                     'index': '/attachment/loremipsum-223/',
                     'index': '/attachment/loremipsum-223/',
                     'thumb': '/attachment/thumb/loremipsum-223/',
                     'thumb': '/attachment/thumb/loremipsum-223/',
-                    'uploader': '/user/bobboberson-223/'
+                    'uploader': '/user/bobboberson-223/',
                 },
                 },
                 'is_image': True,
                 'is_image': True,
-                'filename': 'Archiwum-2.zip'
+                'filename': 'Archiwum-2.zip',
             }),
             }),
             self.mock_attachment_cache({
             self.mock_attachment_cache({
                 'url': {
                 'url': {
                     'index': '/attachment/loremipsum-323/',
                     'index': '/attachment/loremipsum-323/',
                     'thumb': None,
                     'thumb': None,
-                    'uploader': '/user/bobboberson-323/'
+                    'uploader': '/user/bobboberson-323/',
                 },
                 },
-                'filename': 'Archiwum-3.zip'
-            })
+                'filename': 'Archiwum-3.zip',
+            }),
         ]
         ]
         post.save()
         post.save()
 
 
@@ -522,9 +493,7 @@ class ThreadLikedPostsViewTests(ThreadViewTestCase):
         """
         """
         testutils.like_post(self.thread.first_post, self.user)
         testutils.like_post(self.thread.first_post, self.user)
 
 
-        self.override_acl({
-            'can_see_posts_likes': 0
-        })
+        self.override_acl({'can_see_posts_likes': 0})
 
 
         response = self.client.get(self.thread.get_absolute_url())
         response = self.client.get(self.thread.get_absolute_url())
         self.assertNotContains(response, '"is_liked": true')
         self.assertNotContains(response, '"is_liked": true')

+ 15 - 4
misago/threads/tests/test_treesmap.py

@@ -58,7 +58,9 @@ class TreesMapTests(TestCase):
         tree_id = Category.objects.get(special_role='root_category').tree_id
         tree_id = Category.objects.get(special_role='root_category').tree_id
 
 
         self.assertIn('root_category', trees_map.types, "invalid thread type was loaded")
         self.assertIn('root_category', trees_map.types, "invalid thread type was loaded")
-        self.assertEqual(trees_map.trees[tree_id].root_name, 'root_category', "invalid tree was loaded")
+        self.assertEqual(
+            trees_map.trees[tree_id].root_name, 'root_category', "invalid tree was loaded"
+        )
         self.assertIn('root_category', trees_map.roots, "invalid root was loaded")
         self.assertIn('root_category', trees_map.roots, "invalid root was loaded")
 
 
     def test_get_type_for_tree_id(self):
     def test_get_type_for_tree_id(self):
@@ -69,13 +71,19 @@ class TreesMapTests(TestCase):
         tree_id = Category.objects.get(special_role='root_category').tree_id
         tree_id = Category.objects.get(special_role='root_category').tree_id
         thread_type = trees_map.get_type_for_tree_id(tree_id)
         thread_type = trees_map.get_type_for_tree_id(tree_id)
 
 
-        self.assertEqual(thread_type.root_name, 'root_category', "returned invalid thread type for given tree id")
+        self.assertEqual(
+            thread_type.root_name, 'root_category',
+            "returned invalid thread type for given tree id"
+        )
 
 
         try:
         try:
             trees_map.get_type_for_tree_id(tree_id + 1000)
             trees_map.get_type_for_tree_id(tree_id + 1000)
             self.fail("invalid tree id should cause KeyError being raised")
             self.fail("invalid tree id should cause KeyError being raised")
         except KeyError as e:
         except KeyError as e:
-            self.assertIn("tree id has no type defined", six.text_type(e), "invalid exception message as given")
+            self.assertIn(
+                "tree id has no type defined",
+                six.text_type(e), "invalid exception message as given"
+            )
 
 
     def test_get_tree_id_for_root(self):
     def test_get_tree_id_for_root(self):
         """TreesMap().get_tree_id_for_root() returns tree id for valid type name"""
         """TreesMap().get_tree_id_for_root() returns tree id for valid type name"""
@@ -91,4 +99,7 @@ class TreesMapTests(TestCase):
             trees_map.get_tree_id_for_root('hurr_durr')
             trees_map.get_tree_id_for_root('hurr_durr')
             self.fail("invalid root name should cause KeyError being raised")
             self.fail("invalid root name should cause KeyError being raised")
         except KeyError as e:
         except KeyError as e:
-            self.assertIn('"hurr_durr" root has no tree defined', six.text_type(e), "invalid exception message as given")
+            self.assertIn(
+                '"hurr_durr" root has no tree defined',
+                six.text_type(e), "invalid exception message as given"
+            )

+ 58 - 32
misago/threads/tests/test_utils.py

@@ -6,10 +6,6 @@ from misago.threads.utils import add_categories_to_items, get_thread_id_from_url
 
 
 class AddCategoriesToItemsTests(MisagoTestCase):
 class AddCategoriesToItemsTests(MisagoTestCase):
     def setUp(self):
     def setUp(self):
-        super(AddCategoriesToItemsTests, self).setUp()
-
-        self.root = Category.objects.root_category()
-
         """
         """
         Create categories tree for test cases:
         Create categories tree for test cases:
 
 
@@ -23,16 +19,29 @@ class AddCategoriesToItemsTests(MisagoTestCase):
         Category E
         Category E
           + Subcategory F
           + Subcategory F
         """
         """
+
+        super(AddCategoriesToItemsTests, self).setUp()
+
+        self.root = Category.objects.root_category()
+
         Category(
         Category(
             name='Category A',
             name='Category A',
             slug='category-a',
             slug='category-a',
             css_class='showing-category-a',
             css_class='showing-category-a',
-        ).insert_at(self.root, position='last-child', save=True)
+        ).insert_at(
+            self.root,
+            position='last-child',
+            save=True,
+        )
         Category(
         Category(
             name='Category E',
             name='Category E',
             slug='category-e',
             slug='category-e',
             css_class='showing-category-e',
             css_class='showing-category-e',
-        ).insert_at(self.root, position='last-child', save=True)
+        ).insert_at(
+            self.root,
+            position='last-child',
+            save=True,
+        )
 
 
         self.root = Category.objects.root_category()
         self.root = Category.objects.root_category()
 
 
@@ -41,19 +50,31 @@ class AddCategoriesToItemsTests(MisagoTestCase):
             name='Category B',
             name='Category B',
             slug='category-b',
             slug='category-b',
             css_class='showing-category-b',
             css_class='showing-category-b',
-        ).insert_at(self.category_a, position='last-child', save=True)
+        ).insert_at(
+            self.category_a,
+            position='last-child',
+            save=True,
+        )
 
 
         self.category_b = Category.objects.get(slug='category-b')
         self.category_b = Category.objects.get(slug='category-b')
         Category(
         Category(
             name='Category C',
             name='Category C',
             slug='category-c',
             slug='category-c',
             css_class='showing-category-c',
             css_class='showing-category-c',
-        ).insert_at(self.category_b, position='last-child', save=True)
+        ).insert_at(
+            self.category_b,
+            position='last-child',
+            save=True,
+        )
         Category(
         Category(
             name='Category D',
             name='Category D',
             slug='category-d',
             slug='category-d',
             css_class='showing-category-d',
             css_class='showing-category-d',
-        ).insert_at(self.category_b, position='last-child', save=True)
+        ).insert_at(
+            self.category_b,
+            position='last-child',
+            save=True,
+        )
 
 
         self.category_c = Category.objects.get(slug='category-c')
         self.category_c = Category.objects.get(slug='category-c')
         self.category_d = Category.objects.get(slug='category-d')
         self.category_d = Category.objects.get(slug='category-d')
@@ -63,7 +84,11 @@ class AddCategoriesToItemsTests(MisagoTestCase):
             name='Category F',
             name='Category F',
             slug='category-f',
             slug='category-f',
             css_class='showing-category-f',
             css_class='showing-category-f',
-        ).insert_at(self.category_e, position='last-child', save=True)
+        ).insert_at(
+            self.category_e,
+            position='last-child',
+            save=True,
+        )
 
 
         self.clear_state()
         self.clear_state()
 
 
@@ -77,8 +102,7 @@ class AddCategoriesToItemsTests(MisagoTestCase):
         self.category_e = Category.objects.get(slug='category-e')
         self.category_e = Category.objects.get(slug='category-e')
         self.category_f = Category.objects.get(slug='category-f')
         self.category_f = Category.objects.get(slug='category-f')
 
 
-        self.categories = list(Category.objects.all_categories(
-            include_root=True))
+        self.categories = list(Category.objects.all_categories(include_root=True))
 
 
     def test_root_thread_from_root(self):
     def test_root_thread_from_root(self):
         """thread in root category is handled"""
         """thread in root category is handled"""
@@ -163,101 +187,103 @@ class MockRequest(object):
 class GetThreadIdFromUrlTests(MisagoTestCase):
 class GetThreadIdFromUrlTests(MisagoTestCase):
     def test_get_thread_id_from_valid_urls(self):
     def test_get_thread_id_from_valid_urls(self):
         """get_thread_id_from_url extracts thread pk from valid urls"""
         """get_thread_id_from_url extracts thread pk from valid urls"""
-        TEST_CASES = (
+        TEST_CASES = [
             {
             {
                 # perfect match
                 # perfect match
                 'request': MockRequest('https', 'testforum.com', '/discuss/'),
                 'request': MockRequest('https', 'testforum.com', '/discuss/'),
                 'url': 'https://testforum.com/discuss/t/test-thread/123/',
                 'url': 'https://testforum.com/discuss/t/test-thread/123/',
-                'pk': 123
+                'pk': 123,
             },
             },
             {
             {
                 # we don't validate scheme in case site recently moved to https
                 # we don't validate scheme in case site recently moved to https
                 # but user still has old url's saved somewhere
                 # but user still has old url's saved somewhere
                 'request': MockRequest('http', 'testforum.com', '/discuss/'),
                 'request': MockRequest('http', 'testforum.com', '/discuss/'),
                 'url': 'http://testforum.com/discuss/t/test-thread/432/post/12321/',
                 'url': 'http://testforum.com/discuss/t/test-thread/432/post/12321/',
-                'pk': 432
+                'pk': 432,
             },
             },
             {
             {
                 # extract thread id from other thread urls
                 # extract thread id from other thread urls
                 'request': MockRequest('https', 'testforum.com', '/discuss/'),
                 'request': MockRequest('https', 'testforum.com', '/discuss/'),
                 'url': 'http://testforum.com/discuss/t/test-thread/432/post/12321/',
                 'url': 'http://testforum.com/discuss/t/test-thread/432/post/12321/',
-                'pk': 432
+                'pk': 432,
             },
             },
             {
             {
                 # extract thread id from thread page url
                 # extract thread id from thread page url
                 'request': MockRequest('http', 'testforum.com', '/discuss/'),
                 'request': MockRequest('http', 'testforum.com', '/discuss/'),
                 'url': 'http://testforum.com/discuss/t/test-thread/432/123/',
                 'url': 'http://testforum.com/discuss/t/test-thread/432/123/',
-                'pk': 432
+                'pk': 432,
             },
             },
             {
             {
                 # extract thread id from thread last post url with relative schema
                 # extract thread id from thread last post url with relative schema
                 'request': MockRequest('http', 'testforum.com', '/discuss/'),
                 'request': MockRequest('http', 'testforum.com', '/discuss/'),
                 'url': '//testforum.com/discuss/t/test-thread/18/last/',
                 'url': '//testforum.com/discuss/t/test-thread/18/last/',
-                'pk': 18
+                'pk': 18,
             },
             },
             {
             {
                 # extract thread id from url that lacks scheme
                 # extract thread id from url that lacks scheme
                 'request': MockRequest('http', 'testforum.com', ''),
                 'request': MockRequest('http', 'testforum.com', ''),
                 'url': 'testforum.com/t/test-thread/12/last/',
                 'url': 'testforum.com/t/test-thread/12/last/',
-                'pk': 12
+                'pk': 12,
             },
             },
             {
             {
                 # extract thread id from schemaless thread last post url
                 # extract thread id from schemaless thread last post url
                 'request': MockRequest('http', 'testforum.com', '/discuss/'),
                 'request': MockRequest('http', 'testforum.com', '/discuss/'),
                 'url': 'testforum.com/discuss/t/test-thread/18/last/',
                 'url': 'testforum.com/discuss/t/test-thread/18/last/',
-                'pk': 18
+                'pk': 18,
             },
             },
             {
             {
                 # extract thread id from url that lacks scheme and hostname
                 # extract thread id from url that lacks scheme and hostname
                 'request': MockRequest('http', 'testforum.com', ''),
                 'request': MockRequest('http', 'testforum.com', ''),
                 'url': '/t/test-thread/13/',
                 'url': '/t/test-thread/13/',
-                'pk': 13
+                'pk': 13,
             },
             },
             {
             {
                 # extract thread id from url that has port name
                 # extract thread id from url that has port name
                 'request': MockRequest('http', '127.0.0.1:8000', ''),
                 'request': MockRequest('http', '127.0.0.1:8000', ''),
                 'url': 'https://127.0.0.1:8000/t/test-thread/13/',
                 'url': 'https://127.0.0.1:8000/t/test-thread/13/',
-                'pk': 13
+                'pk': 13,
             },
             },
             {
             {
                 # extract thread id from url that isn't trimmed
                 # extract thread id from url that isn't trimmed
                 'request': MockRequest('http', '127.0.0.1:8000', ''),
                 'request': MockRequest('http', '127.0.0.1:8000', ''),
                 'url': '   /t/test-thread/13/   ',
                 'url': '   /t/test-thread/13/   ',
-                'pk': 13
+                'pk': 13,
             }
             }
-        )
+        ]
 
 
         for case in TEST_CASES:
         for case in TEST_CASES:
             pk = get_thread_id_from_url(case['request'], case['url'])
             pk = get_thread_id_from_url(case['request'], case['url'])
             self.assertEqual(
             self.assertEqual(
-                pk, case['pk'], 'get_thread_id_from_url for {} should return {}'.format(case['url'], case['pk']))
+                pk, case['pk'],
+                'get_thread_id_from_url for {} should return {}'.format(case['url'], case['pk'])
+            )
 
 
     def test_get_thread_id_from_invalid_urls(self):
     def test_get_thread_id_from_invalid_urls(self):
-        TEST_CASES = (
+        TEST_CASES = [
             {
             {
                 # invalid wsgi alias
                 # invalid wsgi alias
                 'request': MockRequest('https', 'testforum.com'),
                 'request': MockRequest('https', 'testforum.com'),
-                'url': 'http://testforum.com/discuss/t/test-thread-123/'
+                'url': 'http://testforum.com/discuss/t/test-thread-123/',
             },
             },
             {
             {
                 # invalid hostname
                 # invalid hostname
                 'request': MockRequest('http', 'misago-project.org', '/discuss/'),
                 'request': MockRequest('http', 'misago-project.org', '/discuss/'),
-                'url': 'https://testforum.com/discuss/t/test-thread-432/post/12321/'
+                'url': 'https://testforum.com/discuss/t/test-thread-432/post/12321/',
             },
             },
             {
             {
                 # old thread url
                 # old thread url
                 'request': MockRequest('http', 'testforum.com'),
                 'request': MockRequest('http', 'testforum.com'),
-                'url': 'https://testforum.com/thread/bobboberson-123/'
+                'url': 'https://testforum.com/thread/bobboberson-123/',
             },
             },
             {
             {
                 # dashed thread url
                 # dashed thread url
                 'request': MockRequest('http', 'testforum.com'),
                 'request': MockRequest('http', 'testforum.com'),
-                'url': 'https://testforum.com/t/bobboberson-123/'
+                'url': 'https://testforum.com/t/bobboberson-123/',
             },
             },
             {
             {
                 # non-thread url
                 # non-thread url
                 'request': MockRequest('http', 'testforum.com'),
                 'request': MockRequest('http', 'testforum.com'),
-                'url': 'https://testforum.com/user/bobboberson-123/'
+                'url': 'https://testforum.com/user/bobboberson-123/',
             },
             },
             {
             {
                 # rubbish url
                 # rubbish url
@@ -274,7 +300,7 @@ class GetThreadIdFromUrlTests(MisagoTestCase):
                 'request': MockRequest('http', 'testforum.com'),
                 'request': MockRequest('http', 'testforum.com'),
                 'url': ''
                 'url': ''
             }
             }
-        )
+        ]
 
 
         for case in TEST_CASES:
         for case in TEST_CASES:
             pk = get_thread_id_from_url(case['request'], case['url'])
             pk = get_thread_id_from_url(case['request'], case['url'])

+ 3 - 3
misago/threads/tests/test_validators.py

@@ -10,7 +10,7 @@ class ValidatePostTests(TestCase):
         """valid post passes validation"""
         """valid post passes validation"""
         validate_post("Lorem ipsum dolor met sit amet elit.")
         validate_post("Lorem ipsum dolor met sit amet elit.")
 
 
-    def test_too_short_post(self):
+    def test_empty_post(self):
         """empty post is rejected"""
         """empty post is rejected"""
         with self.assertRaises(ValidationError):
         with self.assertRaises(ValidationError):
             validate_post("")
             validate_post("")
@@ -31,11 +31,11 @@ class ValidatePostTests(TestCase):
 class ValidateTitleTests(TestCase):
 class ValidateTitleTests(TestCase):
     def test_valid_titles(self):
     def test_valid_titles(self):
         """validate_title is ok with valid titles"""
         """validate_title is ok with valid titles"""
-        VALID_TITLES = (
+        VALID_TITLES = [
             'Lorem ipsum dolor met',
             'Lorem ipsum dolor met',
             '123 456 789 112'
             '123 456 789 112'
             'Ugabugagagagagaga',
             'Ugabugagagagagaga',
-        )
+        ]
 
 
         for title in VALID_TITLES:
         for title in VALID_TITLES:
             validate_title(title)
             validate_title(title)

+ 45 - 30
misago/threads/testutils.py

@@ -12,9 +12,17 @@ from .models import Poll, Post, Thread
 UserModel = get_user_model()
 UserModel = get_user_model()
 
 
 
 
-def post_thread(category, title='Test thread', poster='Tester',
-                is_global=False, is_pinned=False, is_unapproved=False,
-                is_hidden=False, is_closed=False, started_on=None):
+def post_thread(
+        category,
+        title='Test thread',
+        poster='Tester',
+        is_global=False,
+        is_pinned=False,
+        is_unapproved=False,
+        is_hidden=False,
+        is_closed=False,
+        started_on=None
+):
     started_on = started_on or timezone.now()
     started_on = started_on or timezone.now()
 
 
     kwargs = {
     kwargs = {
@@ -25,7 +33,7 @@ def post_thread(category, title='Test thread', poster='Tester',
         'last_post_on': started_on,
         'last_post_on': started_on,
         'is_unapproved': is_unapproved,
         'is_unapproved': is_unapproved,
         'is_hidden': is_hidden,
         'is_hidden': is_hidden,
-        'is_closed': is_closed
+        'is_closed': is_closed,
     }
     }
 
 
     if is_global:
     if is_global:
@@ -41,7 +49,7 @@ def post_thread(category, title='Test thread', poster='Tester',
             'last_poster': poster,
             'last_poster': poster,
             'last_poster_name': poster.username,
             'last_poster_name': poster.username,
             'last_poster_slug': poster.slug,
             'last_poster_slug': poster.slug,
-            })
+        })
     except AttributeError:
     except AttributeError:
         kwargs.update({
         kwargs.update({
             'starter_name': poster,
             'starter_name': poster,
@@ -62,9 +70,18 @@ def post_thread(category, title='Test thread', poster='Tester',
     return thread
     return thread
 
 
 
 
-def reply_thread(thread, poster="Tester", message="I am test message",
-                 is_unapproved=False, is_hidden=False, is_event=False,
-                 has_reports=False, has_open_reports=False, posted_on=None, poster_ip='127.0.0.1'):
+def reply_thread(
+        thread,
+        poster="Tester",
+        message="I am test message",
+        is_unapproved=False,
+        is_hidden=False,
+        is_event=False,
+        has_reports=False,
+        has_open_reports=False,
+        posted_on=None,
+        poster_ip='127.0.0.1'
+):
     posted_on = posted_on or thread.last_post_on + timedelta(minutes=5)
     posted_on = posted_on or thread.last_post_on + timedelta(minutes=5)
 
 
     kwargs = {
     kwargs = {
@@ -84,7 +101,10 @@ def reply_thread(thread, poster="Tester", message="I am test message",
     }
     }
 
 
     try:
     try:
-        kwargs.update({'poster': poster, 'poster_name': poster.username})
+        kwargs.update({
+            'poster': poster,
+            'poster_name': poster.username,
+        })
     except AttributeError:
     except AttributeError:
         kwargs.update({'poster_name': poster})
         kwargs.update({'poster_name': poster})
 
 
@@ -130,7 +150,7 @@ def post_poll(thread, poster):
                 'hash': 'dddddddddddd',
                 'hash': 'dddddddddddd',
                 'label': 'Delta',
                 'label': 'Delta',
                 'votes': 1
                 'votes': 1
-            }
+            },
         ],
         ],
         allowed_choices=2,
         allowed_choices=2,
         votes=4
         votes=4
@@ -140,8 +160,7 @@ def post_poll(thread, poster):
     try:
     try:
         user = UserModel.objects.get(slug='bob')
         user = UserModel.objects.get(slug='bob')
     except UserModel.DoesNotExist:
     except UserModel.DoesNotExist:
-        user = UserModel.objects.create_user(
-            'bob', 'bob@test.com', 'Pass.123')
+        user = UserModel.objects.create_user('bob', 'bob@test.com', 'Pass.123')
 
 
     poll.pollvote_set.create(
     poll.pollvote_set.create(
         category=thread.category,
         category=thread.category,
@@ -150,7 +169,7 @@ def post_poll(thread, poster):
         voter_name=user.username,
         voter_name=user.username,
         voter_slug=user.slug,
         voter_slug=user.slug,
         voter_ip='127.0.0.1',
         voter_ip='127.0.0.1',
-        choice_hash='aaaaaaaaaaaa'
+        choice_hash='aaaaaaaaaaaa',
     )
     )
 
 
     # test user voted on third and last choices
     # test user voted on third and last choices
@@ -161,7 +180,7 @@ def post_poll(thread, poster):
         voter_name=poster.username,
         voter_name=poster.username,
         voter_slug=poster.slug,
         voter_slug=poster.slug,
         voter_ip='127.0.0.1',
         voter_ip='127.0.0.1',
-        choice_hash='gggggggggggg'
+        choice_hash='gggggggggggg',
     )
     )
     poll.pollvote_set.create(
     poll.pollvote_set.create(
         category=thread.category,
         category=thread.category,
@@ -170,7 +189,7 @@ def post_poll(thread, poster):
         voter_name=poster.username,
         voter_name=poster.username,
         voter_slug=poster.slug,
         voter_slug=poster.slug,
         voter_ip='127.0.0.1',
         voter_ip='127.0.0.1',
-        choice_hash='dddddddddddd'
+        choice_hash='dddddddddddd',
     )
     )
 
 
     # somebody else voted on third option before being deleted
     # somebody else voted on third option before being deleted
@@ -180,7 +199,7 @@ def post_poll(thread, poster):
         voter_name='deleted',
         voter_name='deleted',
         voter_slug='deleted',
         voter_slug='deleted',
         voter_ip='127.0.0.1',
         voter_ip='127.0.0.1',
-        choice_hash='gggggggggggg'
+        choice_hash='gggggggggggg',
     )
     )
 
 
     return poll
     return poll
@@ -197,30 +216,26 @@ def like_post(post, liker=None, username=None):
             liker=liker,
             liker=liker,
             liker_name=liker.username,
             liker_name=liker.username,
             liker_slug=liker.slug,
             liker_slug=liker.slug,
-            liker_ip='127.0.0.1'
+            liker_ip='127.0.0.1',
         )
         )
 
 
-        post.last_likes = [
-            {
-                'id': liker.id,
-                'username': liker.username
-            }
-        ] + post.last_likes
+        post.last_likes = [{
+            'id': liker.id,
+            'username': liker.username,
+        }] + post.last_likes
     else:
     else:
         like = post.postlike_set.create(
         like = post.postlike_set.create(
             category=post.category,
             category=post.category,
             thread=post.thread,
             thread=post.thread,
             liker_name=username,
             liker_name=username,
             liker_slug=slugify(username),
             liker_slug=slugify(username),
-            liker_ip='127.0.0.1'
+            liker_ip='127.0.0.1',
         )
         )
 
 
-        post.last_likes = [
-            {
-                'id': None,
-                'username': username
-            }
-        ] + post.last_likes
+        post.last_likes = [{
+            'id': None,
+            'username': username,
+        }] + post.last_likes
 
 
     post.likes += 1
     post.likes += 1
     post.save()
     post.save()

+ 103 - 62
misago/threads/threadtypes/privatethread.py

@@ -16,98 +16,139 @@ class PrivateThread(ThreadType):
         return reverse('misago:private-threads')
         return reverse('misago:private-threads')
 
 
     def get_category_last_thread_url(self, category):
     def get_category_last_thread_url(self, category):
-        return reverse('misago:private-thread', kwargs={
-            'slug': category.last_thread_slug,
-            'pk': category.last_thread_id
-        })
+        return reverse(
+            'misago:private-thread',
+            kwargs={
+                'slug': category.last_thread_slug,
+                'pk': category.last_thread_id,
+            }
+        )
 
 
     def get_category_last_post_url(self, category):
     def get_category_last_post_url(self, category):
-        return reverse('misago:private-thread-last', kwargs={
-            'slug': category.last_thread_slug,
-            'pk': category.last_thread_id
-        })
+        return reverse(
+            'misago:private-thread-last',
+            kwargs={
+                'slug': category.last_thread_slug,
+                'pk': category.last_thread_id,
+            }
+        )
 
 
     def get_category_read_api_url(self, category):
     def get_category_read_api_url(self, category):
         return reverse('misago:api:private-thread-read')
         return reverse('misago:api:private-thread-read')
 
 
     def get_thread_absolute_url(self, thread, page=1):
     def get_thread_absolute_url(self, thread, page=1):
         if page > 1:
         if page > 1:
-            return reverse('misago:private-thread', kwargs={
-                'slug': thread.slug,
-                'pk': thread.pk,
-                'page': page
-            })
+            return reverse(
+                'misago:private-thread',
+                kwargs={
+                    'slug': thread.slug,
+                    'pk': thread.pk,
+                    'page': page,
+                }
+            )
         else:
         else:
-            return reverse('misago:private-thread', kwargs={
-                'slug': thread.slug,
-                'pk': thread.pk
-            })
+            return reverse(
+                'misago:private-thread', kwargs={
+                    'slug': thread.slug,
+                    'pk': thread.pk,
+                }
+            )
 
 
     def get_thread_last_post_url(self, thread):
     def get_thread_last_post_url(self, thread):
-        return reverse('misago:private-thread-last', kwargs={
-            'slug': thread.slug,
-            'pk': thread.pk
-        })
+        return reverse(
+            'misago:private-thread-last', kwargs={
+                'slug': thread.slug,
+                'pk': thread.pk,
+            }
+        )
 
 
     def get_thread_new_post_url(self, thread):
     def get_thread_new_post_url(self, thread):
-        return reverse('misago:private-thread-new', kwargs={
-            'slug': thread.slug,
-            'pk': thread.pk
-        })
+        return reverse(
+            'misago:private-thread-new', kwargs={
+                'slug': thread.slug,
+                'pk': thread.pk,
+            }
+        )
 
 
     def get_thread_api_url(self, thread):
     def get_thread_api_url(self, thread):
-        return reverse('misago:api:private-thread-detail', kwargs={
-            'pk': thread.pk
-        })
+        return reverse(
+            'misago:api:private-thread-detail', kwargs={
+                'pk': thread.pk,
+            }
+        )
 
 
     def get_thread_editor_api_url(self, thread):
     def get_thread_editor_api_url(self, thread):
-        return reverse('misago:api:private-thread-post-editor', kwargs={
-            'thread_pk': thread.pk
-        })
+        return reverse(
+            'misago:api:private-thread-post-editor', kwargs={
+                'thread_pk': thread.pk,
+            }
+        )
 
 
     def get_thread_posts_api_url(self, thread):
     def get_thread_posts_api_url(self, thread):
-        return reverse('misago:api:private-thread-post-list', kwargs={
-            'thread_pk': thread.pk
-        })
+        return reverse(
+            'misago:api:private-thread-post-list', kwargs={
+                'thread_pk': thread.pk,
+            }
+        )
 
 
     def get_post_merge_api_url(self, thread):
     def get_post_merge_api_url(self, thread):
-        return reverse('misago:api:private-thread-post-merge', kwargs={
-            'thread_pk': thread.pk
-        })
+        return reverse(
+            'misago:api:private-thread-post-merge', kwargs={
+                'thread_pk': thread.pk,
+            }
+        )
 
 
     def get_post_absolute_url(self, post):
     def get_post_absolute_url(self, post):
-        return reverse('misago:private-thread-post', kwargs={
-            'slug': post.thread.slug,
-            'pk': post.thread.pk,
-            'post': post.pk
-        })
+        return reverse(
+            'misago:private-thread-post',
+            kwargs={
+                'slug': post.thread.slug,
+                'pk': post.thread.pk,
+                'post': post.pk,
+            }
+        )
 
 
     def get_post_api_url(self, post):
     def get_post_api_url(self, post):
-        return reverse('misago:api:private-thread-post-detail', kwargs={
-            'thread_pk': post.thread_id,
-            'pk': post.pk
-        })
+        return reverse(
+            'misago:api:private-thread-post-detail',
+            kwargs={
+                'thread_pk': post.thread_id,
+                'pk': post.pk,
+            }
+        )
 
 
     def get_post_likes_api_url(self, post):
     def get_post_likes_api_url(self, post):
-        return reverse('misago:api:private-thread-post-likes', kwargs={
-            'thread_pk': post.thread_id,
-            'pk': post.pk
-        })
+        return reverse(
+            'misago:api:private-thread-post-likes',
+            kwargs={
+                'thread_pk': post.thread_id,
+                'pk': post.pk,
+            }
+        )
 
 
     def get_post_editor_api_url(self, post):
     def get_post_editor_api_url(self, post):
-        return reverse('misago:api:private-thread-post-editor', kwargs={
-            'thread_pk': post.thread_id,
-            'pk': post.pk
-        })
+        return reverse(
+            'misago:api:private-thread-post-editor',
+            kwargs={
+                'thread_pk': post.thread_id,
+                'pk': post.pk,
+            }
+        )
 
 
     def get_post_edits_api_url(self, post):
     def get_post_edits_api_url(self, post):
-        return reverse('misago:api:private-thread-post-edits', kwargs={
-            'thread_pk': post.thread_id,
-            'pk': post.pk
-        })
+        return reverse(
+            'misago:api:private-thread-post-edits',
+            kwargs={
+                'thread_pk': post.thread_id,
+                'pk': post.pk,
+            }
+        )
 
 
     def get_post_read_api_url(self, post):
     def get_post_read_api_url(self, post):
-        return reverse('misago:api:private-thread-post-read', kwargs={
-            'thread_pk': post.thread_id,
-            'pk': post.pk
-        })
+        return reverse(
+            'misago:api:private-thread-post-read',
+            kwargs={
+                'thread_pk': post.thread_id,
+                'pk': post.pk,
+            }
+        )

+ 142 - 92
misago/threads/threadtypes/thread.py

@@ -17,145 +17,195 @@ class Thread(ThreadType):
 
 
     def get_category_absolute_url(self, category):
     def get_category_absolute_url(self, category):
         if category.level:
         if category.level:
-            return reverse('misago:category', kwargs={
-                'pk': category.pk,
-                'slug': category.slug,
-            })
+            return reverse(
+                'misago:category', kwargs={
+                    'pk': category.pk,
+                    'slug': category.slug,
+                }
+            )
         else:
         else:
             return reverse('misago:threads')
             return reverse('misago:threads')
 
 
     def get_category_last_thread_url(self, category):
     def get_category_last_thread_url(self, category):
-        return reverse('misago:thread', kwargs={
-            'slug': category.last_thread_slug,
-            'pk': category.last_thread_id
-        })
+        return reverse(
+            'misago:thread',
+            kwargs={
+                'slug': category.last_thread_slug,
+                'pk': category.last_thread_id,
+            }
+        )
 
 
     def get_category_last_post_url(self, category):
     def get_category_last_post_url(self, category):
-        return reverse('misago:thread-last', kwargs={
-            'slug': category.last_thread_slug,
-            'pk': category.last_thread_id
-        })
+        return reverse(
+            'misago:thread-last',
+            kwargs={
+                'slug': category.last_thread_slug,
+                'pk': category.last_thread_id,
+            }
+        )
 
 
     def get_category_read_api_url(self, category):
     def get_category_read_api_url(self, category):
-        return '{}?category={}'.format(
-            reverse('misago:api:thread-read'), category.pk)
+        return '{}?category={}'.format(reverse('misago:api:thread-read'), category.pk)
 
 
     def get_thread_absolute_url(self, thread, page=1):
     def get_thread_absolute_url(self, thread, page=1):
         if page > 1:
         if page > 1:
-            return reverse('misago:thread', kwargs={
-                'slug': thread.slug,
-                'pk': thread.pk,
-                'page': page
-            })
+            return reverse(
+                'misago:thread', kwargs={
+                    'slug': thread.slug,
+                    'pk': thread.pk,
+                    'page': page,
+                }
+            )
         else:
         else:
-            return reverse('misago:thread', kwargs={
-                'slug': thread.slug,
-                'pk': thread.pk
-            })
+            return reverse(
+                'misago:thread', kwargs={
+                    'slug': thread.slug,
+                    'pk': thread.pk,
+                }
+            )
 
 
     def get_thread_last_post_url(self, thread):
     def get_thread_last_post_url(self, thread):
-        return reverse('misago:thread-last', kwargs={
-            'slug': thread.slug,
-            'pk': thread.pk
-        })
+        return reverse(
+            'misago:thread-last', kwargs={
+                'slug': thread.slug,
+                'pk': thread.pk,
+            }
+        )
 
 
     def get_thread_new_post_url(self, thread):
     def get_thread_new_post_url(self, thread):
-        return reverse('misago:thread-new', kwargs={
-            'slug': thread.slug,
-            'pk': thread.pk
-        })
+        return reverse(
+            'misago:thread-new', kwargs={
+                'slug': thread.slug,
+                'pk': thread.pk,
+            }
+        )
 
 
     def get_thread_unapproved_post_url(self, thread):
     def get_thread_unapproved_post_url(self, thread):
-        return reverse('misago:thread-unapproved', kwargs={
-            'slug': thread.slug,
-            'pk': thread.pk
-        })
+        return reverse(
+            'misago:thread-unapproved', kwargs={
+                'slug': thread.slug,
+                'pk': thread.pk,
+            }
+        )
 
 
     def get_thread_api_url(self, thread):
     def get_thread_api_url(self, thread):
-        return reverse('misago:api:thread-detail', kwargs={
-            'pk': thread.pk
-        })
+        return reverse(
+            'misago:api:thread-detail', kwargs={
+                'pk': thread.pk,
+            }
+        )
 
 
     def get_thread_editor_api_url(self, thread):
     def get_thread_editor_api_url(self, thread):
-        return reverse('misago:api:thread-post-editor', kwargs={
-            'thread_pk': thread.pk
-        })
+        return reverse(
+            'misago:api:thread-post-editor', kwargs={
+                'thread_pk': thread.pk,
+            }
+        )
 
 
     def get_thread_merge_api_url(self, thread):
     def get_thread_merge_api_url(self, thread):
-        return reverse('misago:api:thread-merge', kwargs={
-            'pk': thread.pk
-        })
+        return reverse(
+            'misago:api:thread-merge', kwargs={
+                'pk': thread.pk,
+            }
+        )
 
 
     def get_thread_poll_api_url(self, thread):
     def get_thread_poll_api_url(self, thread):
-        return reverse('misago:api:thread-poll-list', kwargs={
-            'thread_pk': thread.pk
-        })
+        return reverse(
+            'misago:api:thread-poll-list', kwargs={
+                'thread_pk': thread.pk,
+            }
+        )
 
 
     def get_thread_posts_api_url(self, thread):
     def get_thread_posts_api_url(self, thread):
-        return reverse('misago:api:thread-post-list', kwargs={
-            'thread_pk': thread.pk
-        })
+        return reverse(
+            'misago:api:thread-post-list', kwargs={
+                'thread_pk': thread.pk,
+            }
+        )
 
 
     def get_poll_api_url(self, poll):
     def get_poll_api_url(self, poll):
-        return reverse('misago:api:thread-poll-detail', kwargs={
-            'thread_pk': poll.thread_id,
-            'pk': poll.pk
-        })
+        return reverse(
+            'misago:api:thread-poll-detail', kwargs={
+                'thread_pk': poll.thread_id,
+                'pk': poll.pk,
+            }
+        )
 
 
     def get_poll_votes_api_url(self, poll):
     def get_poll_votes_api_url(self, poll):
-        return reverse('misago:api:thread-poll-votes', kwargs={
-            'thread_pk': poll.thread_id,
-            'pk': poll.pk
-        })
+        return reverse(
+            'misago:api:thread-poll-votes', kwargs={
+                'thread_pk': poll.thread_id,
+                'pk': poll.pk,
+            }
+        )
 
 
     def get_post_merge_api_url(self, thread):
     def get_post_merge_api_url(self, thread):
-        return reverse('misago:api:thread-post-merge', kwargs={
-            'thread_pk': thread.pk
-        })
+        return reverse(
+            'misago:api:thread-post-merge', kwargs={
+                'thread_pk': thread.pk,
+            }
+        )
 
 
     def get_post_move_api_url(self, thread):
     def get_post_move_api_url(self, thread):
-        return reverse('misago:api:thread-post-move', kwargs={
-            'thread_pk': thread.pk
-        })
+        return reverse(
+            'misago:api:thread-post-move', kwargs={
+                'thread_pk': thread.pk,
+            }
+        )
 
 
     def get_post_split_api_url(self, thread):
     def get_post_split_api_url(self, thread):
-        return reverse('misago:api:thread-post-split', kwargs={
-            'thread_pk': thread.pk
-        })
+        return reverse(
+            'misago:api:thread-post-split', kwargs={
+                'thread_pk': thread.pk,
+            }
+        )
 
 
     def get_post_absolute_url(self, post):
     def get_post_absolute_url(self, post):
-        return reverse('misago:thread-post', kwargs={
-            'slug': post.thread.slug,
-            'pk': post.thread.pk,
-            'post': post.pk
-        })
+        return reverse(
+            'misago:thread-post',
+            kwargs={
+                'slug': post.thread.slug,
+                'pk': post.thread.pk,
+                'post': post.pk,
+            }
+        )
 
 
     def get_post_api_url(self, post):
     def get_post_api_url(self, post):
-        return reverse('misago:api:thread-post-detail', kwargs={
-            'thread_pk': post.thread_id,
-            'pk': post.pk
-        })
+        return reverse(
+            'misago:api:thread-post-detail', kwargs={
+                'thread_pk': post.thread_id,
+                'pk': post.pk,
+            }
+        )
 
 
     def get_post_likes_api_url(self, post):
     def get_post_likes_api_url(self, post):
-        return reverse('misago:api:thread-post-likes', kwargs={
-            'thread_pk': post.thread_id,
-            'pk': post.pk
-        })
+        return reverse(
+            'misago:api:thread-post-likes', kwargs={
+                'thread_pk': post.thread_id,
+                'pk': post.pk,
+            }
+        )
 
 
     def get_post_editor_api_url(self, post):
     def get_post_editor_api_url(self, post):
-        return reverse('misago:api:thread-post-editor', kwargs={
-            'thread_pk': post.thread_id,
-            'pk': post.pk
-        })
+        return reverse(
+            'misago:api:thread-post-editor', kwargs={
+                'thread_pk': post.thread_id,
+                'pk': post.pk,
+            }
+        )
 
 
     def get_post_edits_api_url(self, post):
     def get_post_edits_api_url(self, post):
-        return reverse('misago:api:thread-post-edits', kwargs={
-            'thread_pk': post.thread_id,
-            'pk': post.pk
-        })
+        return reverse(
+            'misago:api:thread-post-edits', kwargs={
+                'thread_pk': post.thread_id,
+                'pk': post.pk,
+            }
+        )
 
 
     def get_post_read_api_url(self, post):
     def get_post_read_api_url(self, post):
-        return reverse('misago:api:thread-post-read', kwargs={
-            'thread_pk': post.thread_id,
-            'pk': post.pk
-        })
+        return reverse(
+            'misago:api:thread-post-read', kwargs={
+                'thread_pk': post.thread_id,
+                'pk': post.pk,
+            }
+        )

+ 1 - 0
misago/threads/threadtypes/treesmap.py

@@ -5,6 +5,7 @@ from misago.conf import settings
 
 
 class TreesMap(object):
 class TreesMap(object):
     """Object that maps trees to strategies"""
     """Object that maps trees to strategies"""
+
     def __init__(self, types_modules):
     def __init__(self, types_modules):
         self.is_loaded = False
         self.is_loaded = False
         self.types_modules = types_modules
         self.types_modules = types_modules

+ 54 - 58
misago/threads/urls/__init__.py

@@ -5,19 +5,12 @@ from misago.conf import settings
 from misago.threads.views.attachment import attachment_server
 from misago.threads.views.attachment import attachment_server
 from misago.threads.views.goto import (
 from misago.threads.views.goto import (
     ThreadGotoPostView, ThreadGotoLastView, ThreadGotoNewView, ThreadGotoUnapprovedView,
     ThreadGotoPostView, ThreadGotoLastView, ThreadGotoNewView, ThreadGotoUnapprovedView,
-    PrivateThreadGotoPostView, PrivateThreadGotoLastView, PrivateThreadGotoNewView)
-from misago.threads.views.list import ForumThreads, CategoryThreads, PrivateThreads
-from misago.threads.views.thread import Thread, PrivateThread
-
-
-LISTS_TYPES = (
-    'all',
-    'my',
-    'new',
-    'unread',
-    'subscribed',
-    'unapproved',
+    PrivateThreadGotoPostView, PrivateThreadGotoLastView, PrivateThreadGotoNewView
 )
 )
+from misago.threads.views.list import ForumThreadsList, CategoryThreadsList, PrivateThreadsList
+from misago.threads.views.thread import ThreadView, PrivateThreadView
+
+LISTS_TYPES = ('all', 'my', 'new', 'unread', 'subscribed', 'unapproved', )
 
 
 
 
 def threads_list_patterns(prefix, view, patterns):
 def threads_list_patterns(prefix, view, patterns):
@@ -28,64 +21,63 @@ def threads_list_patterns(prefix, view, patterns):
         else:
         else:
             url_name = prefix
             url_name = prefix
 
 
-        urls.append(url(
-            pattern,
-            view.as_view(),
-            name=url_name,
-            kwargs={'list_type': LISTS_TYPES[i]},
-        ))
+        urls.append(
+            url(
+                pattern,
+                view.as_view(),
+                name=url_name,
+                kwargs={'list_type': LISTS_TYPES[i]},
+            )
+        )
     return urls
     return urls
 
 
 
 
 if settings.MISAGO_THREADS_ON_INDEX:
 if settings.MISAGO_THREADS_ON_INDEX:
-    urlpatterns = threads_list_patterns('threads', ForumThreads, (
-        r'^$',
-        r'^my/$',
-        r'^new/$',
-        r'^unread/$',
-        r'^subscribed/$',
-        r'^unapproved/$',
-    ))
+    urlpatterns = threads_list_patterns(
+        'threads', ForumThreadsList,
+        (r'^$', r'^my/$', r'^new/$', r'^unread/$', r'^subscribed/$', r'^unapproved/$', )
+    )
 else:
 else:
-    urlpatterns = threads_list_patterns('threads', ForumThreads, (
-        r'^threads/$',
-        r'^threads/my/$',
-        r'^threads/new/$',
-        r'^threads/unread/$',
-        r'^threads/subscribed/$',
-        r'^threads/unapproved/$',
-    ))
-
-
-urlpatterns += threads_list_patterns('category', CategoryThreads, (
-    r'^c/(?P<slug>[-a-zA-Z0-9]+)/(?P<pk>\d+)/$',
-    r'^c/(?P<slug>[-a-zA-Z0-9]+)/(?P<pk>\d+)/my/$',
-    r'^c/(?P<slug>[-a-zA-Z0-9]+)/(?P<pk>\d+)/new/$',
-    r'^c/(?P<slug>[-a-zA-Z0-9]+)/(?P<pk>\d+)/unread/$',
-    r'^c/(?P<slug>[-a-zA-Z0-9]+)/(?P<pk>\d+)/subscribed/$',
-    r'^c/(?P<slug>[-a-zA-Z0-9]+)/(?P<pk>\d+)/unapproved/$',
-))
-
-
-urlpatterns += threads_list_patterns('private-threads', PrivateThreads, (
-    r'^private-threads/$',
-    r'^private-threads/my/$',
-    r'^private-threads/new/$',
-    r'^private-threads/unread/$',
-    r'^private-threads/subscribed/$',
-))
+    urlpatterns = threads_list_patterns(
+        'threads', ForumThreadsList, (
+            r'^threads/$', r'^threads/my/$', r'^threads/new/$', r'^threads/unread/$',
+            r'^threads/subscribed/$', r'^threads/unapproved/$',
+        )
+    )
+
+urlpatterns += threads_list_patterns(
+    'category', CategoryThreadsList, (
+        r'^c/(?P<slug>[-a-zA-Z0-9]+)/(?P<pk>\d+)/$',
+        r'^c/(?P<slug>[-a-zA-Z0-9]+)/(?P<pk>\d+)/my/$',
+        r'^c/(?P<slug>[-a-zA-Z0-9]+)/(?P<pk>\d+)/new/$',
+        r'^c/(?P<slug>[-a-zA-Z0-9]+)/(?P<pk>\d+)/unread/$',
+        r'^c/(?P<slug>[-a-zA-Z0-9]+)/(?P<pk>\d+)/subscribed/$',
+        r'^c/(?P<slug>[-a-zA-Z0-9]+)/(?P<pk>\d+)/unapproved/$',
+    )
+)
+
+urlpatterns += threads_list_patterns(
+    'private-threads', PrivateThreadsList, (
+        r'^private-threads/$', r'^private-threads/my/$', r'^private-threads/new/$',
+        r'^private-threads/unread/$', r'^private-threads/subscribed/$',
+    )
+)
 
 
 
 
 def thread_view_patterns(prefix, view):
 def thread_view_patterns(prefix, view):
     urls = [
     urls = [
         url(r'^%s/(?P<slug>[-a-zA-Z0-9]+)/(?P<pk>\d+)/$' % prefix[0], view.as_view(), name=prefix),
         url(r'^%s/(?P<slug>[-a-zA-Z0-9]+)/(?P<pk>\d+)/$' % prefix[0], view.as_view(), name=prefix),
-        url(r'^%s/(?P<slug>[-a-zA-Z0-9]+)/(?P<pk>\d+)/(?P<page>\d+)/$' % prefix[0], view.as_view(), name=prefix),
+        url(
+            r'^%s/(?P<slug>[-a-zA-Z0-9]+)/(?P<pk>\d+)/(?P<page>\d+)/$' % prefix[0],
+            view.as_view(),
+            name=prefix
+        ),
     ]
     ]
     return urls
     return urls
 
 
 
 
-urlpatterns += thread_view_patterns('thread', Thread)
-urlpatterns += thread_view_patterns('private-thread', PrivateThread)
+urlpatterns += thread_view_patterns('thread', ThreadView)
+urlpatterns += thread_view_patterns('private-thread', PrivateThreadView)
 
 
 
 
 def goto_patterns(prefix, **views):
 def goto_patterns(prefix, **views):
@@ -120,8 +112,12 @@ urlpatterns += goto_patterns(
     new=PrivateThreadGotoNewView,
     new=PrivateThreadGotoNewView,
 )
 )
 
 
-
 urlpatterns += [
 urlpatterns += [
     url(r'^a/(?P<secret>[-a-zA-Z0-9]+)/(?P<pk>\d+)/', attachment_server, name='attachment'),
     url(r'^a/(?P<secret>[-a-zA-Z0-9]+)/(?P<pk>\d+)/', attachment_server, name='attachment'),
-    url(r'^a/thumb/(?P<secret>[-a-zA-Z0-9]+)/(?P<pk>\d+)/', attachment_server, name='attachment-thumbnail', kwargs={'thumbnail': True}),
+    url(
+        r'^a/thumb/(?P<secret>[-a-zA-Z0-9]+)/(?P<pk>\d+)/',
+        attachment_server,
+        name='attachment-thumbnail',
+        kwargs={'thumbnail': True}
+    ),
 ]
 ]

+ 8 - 2
misago/threads/urls/api.py

@@ -10,10 +10,16 @@ router = MisagoApiRouter()
 router.register(r'attachments', AttachmentViewSet, base_name='attachment')
 router.register(r'attachments', AttachmentViewSet, base_name='attachment')
 
 
 router.register(r'threads', ThreadViewSet, base_name='thread')
 router.register(r'threads', ThreadViewSet, base_name='thread')
-router.register(r'threads/(?P<thread_pk>[^/.]+)/posts', ThreadPostsViewSet, base_name='thread-post')
+router.register(
+    r'threads/(?P<thread_pk>[^/.]+)/posts', ThreadPostsViewSet, base_name='thread-post'
+)
 router.register(r'threads/(?P<thread_pk>[^/.]+)/poll', ThreadPollViewSet, base_name='thread-poll')
 router.register(r'threads/(?P<thread_pk>[^/.]+)/poll', ThreadPollViewSet, base_name='thread-poll')
 
 
 router.register(r'private-threads', PrivateThreadViewSet, base_name='private-thread')
 router.register(r'private-threads', PrivateThreadViewSet, base_name='private-thread')
-router.register(r'private-threads/(?P<thread_pk>[^/.]+)/posts', PrivateThreadPostsViewSet, base_name='private-thread-post')
+router.register(
+    r'private-threads/(?P<thread_pk>[^/.]+)/posts',
+    PrivateThreadPostsViewSet,
+    base_name='private-thread-post'
+)
 
 
 urlpatterns = router.urls
 urlpatterns = router.urls

+ 4 - 7
misago/threads/utils.py

@@ -25,16 +25,14 @@ def add_categories_to_items(root_category, categories, items):
         elif root_category.has_child(item.category):
         elif root_category.has_child(item.category):
             # item in subcategory resolution
             # item in subcategory resolution
             for category in categories:
             for category in categories:
-                if (category.parent_id == root_category.pk and
-                        category.has_child(item.category)):
+                if (category.parent_id == root_category.pk and category.has_child(item.category)):
                     top_categories_map[item.category_id] = category
                     top_categories_map[item.category_id] = category
                     item.top_category = category
                     item.top_category = category
         else:
         else:
             # item from other category's scope
             # item from other category's scope
             for category in categories:
             for category in categories:
-                if category.level == 1 and (
-                        category == item.category or
-                        category.has_child(item.category)):
+                category_is_parent = category.has_child(item.category)
+                if category.level == 1 and (category == item.category or category_is_parent):
                     top_categories_map[item.category_id] = category
                     top_categories_map[item.category_id] = category
                     item.top_category = category
                     item.top_category = category
 
 
@@ -48,8 +46,7 @@ def add_likes_to_posts(user, posts):
         posts_map[post.id] = post
         posts_map[post.id] = post
         post.is_liked = False
         post.is_liked = False
 
 
-    queryset = PostLike.objects.filter(
-        liker=user, post_id__in=posts_map.keys())
+    queryset = PostLike.objects.filter(liker=user, post_id__in=posts_map.keys())
 
 
     for like in queryset.values('post_id'):
     for like in queryset.values('post_id'):
         posts_map[like['post_id']].is_liked = True
         posts_map[like['post_id']].is_liked = True

+ 32 - 20
misago/threads/validators.py

@@ -43,21 +43,27 @@ def validate_post(post):
         message = ungettext(
         message = ungettext(
             "Posted message should be at least %(limit_value)s character long (it has %(show_value)s).",
             "Posted message should be at least %(limit_value)s character long (it has %(show_value)s).",
             "Posted message should be at least %(limit_value)s characters long (it has %(show_value)s).",
             "Posted message should be at least %(limit_value)s characters long (it has %(show_value)s).",
-            settings.post_length_min)
-        raise ValidationError(message % {
-            'limit_value': settings.post_length_min,
-            'show_value': post_len
-        })
+            settings.post_length_min,
+        )
+        raise ValidationError(
+            message % {
+                'limit_value': settings.post_length_min,
+                'show_value': post_len,
+            }
+        )
 
 
     if settings.post_length_max and post_len > settings.post_length_max:
     if settings.post_length_max and post_len > settings.post_length_max:
         message = ungettext(
         message = ungettext(
             "Posted message cannot be longer than %(limit_value)s character (it has %(show_value)s).",
             "Posted message cannot be longer than %(limit_value)s character (it has %(show_value)s).",
             "Posted message cannot be longer than %(limit_value)s characters (it has %(show_value)s).",
             "Posted message cannot be longer than %(limit_value)s characters (it has %(show_value)s).",
-            settings.post_length_max)
-        raise ValidationError(message % {
-            'limit_value': settings.post_length_max,
-            'show_value': post_len
-        })
+            settings.post_length_max,
+        )
+        raise ValidationError(
+            message % {
+                'limit_value': settings.post_length_max,
+                'show_value': post_len,
+            }
+        )
 
 
 
 
 def validate_title(title):
 def validate_title(title):
@@ -70,21 +76,27 @@ def validate_title(title):
         message = ungettext(
         message = ungettext(
             "Thread title should be at least %(limit_value)s character long (it has %(show_value)s).",
             "Thread title should be at least %(limit_value)s character long (it has %(show_value)s).",
             "Thread title should be at least %(limit_value)s characters long (it has %(show_value)s).",
             "Thread title should be at least %(limit_value)s characters long (it has %(show_value)s).",
-            settings.thread_title_length_min)
-        raise ValidationError(message % {
-            'limit_value': settings.thread_title_length_min,
-            'show_value': title_len
-        })
+            settings.thread_title_length_min,
+        )
+        raise ValidationError(
+            message % {
+                'limit_value': settings.thread_title_length_min,
+                'show_value': title_len,
+            }
+        )
 
 
     if title_len > settings.thread_title_length_max:
     if title_len > settings.thread_title_length_max:
         message = ungettext(
         message = ungettext(
             "Thread title cannot be longer than %(limit_value)s character (it has %(show_value)s).",
             "Thread title cannot be longer than %(limit_value)s character (it has %(show_value)s).",
             "Thread title cannot be longer than %(limit_value)s characters (it has %(show_value)s).",
             "Thread title cannot be longer than %(limit_value)s characters (it has %(show_value)s).",
-            settings.thread_title_length_max)
-        raise ValidationError(message % {
-            'limit_value': settings.thread_title_length_max,
-            'show_value': title_len
-        })
+            settings.thread_title_length_max,
+        )
+        raise ValidationError(
+            message % {
+                'limit_value': settings.thread_title_length_max,
+                'show_value': title_len,
+            }
+        )
 
 
     error_not_sluggable = _("Thread title should contain alpha-numeric characters.")
     error_not_sluggable = _("Thread title should contain alpha-numeric characters.")
     error_slug_too_long = _("Thread title is too long.")
     error_slug_too_long = _("Thread title is too long.")

+ 18 - 11
misago/threads/viewmodels/category.py

@@ -41,23 +41,19 @@ class ViewModel(BaseViewModel):
         return categories[0]
         return categories[0]
 
 
     def get_frontend_context(self):
     def get_frontend_context(self):
-        return {
-            'CATEGORIES': BasicCategorySerializer(self._categories, many=True).data
-        }
+        return {'CATEGORIES': BasicCategorySerializer(self._categories, many=True).data}
 
 
     def get_template_context(self):
     def get_template_context(self):
-        return {
-            'category': self._model,
-            'subcategories': self._children
-        }
+        return {'category': self._model, 'subcategories': self._children}
 
 
 
 
 class ThreadsRootCategory(ViewModel):
 class ThreadsRootCategory(ViewModel):
     def get_categories(self, request):
     def get_categories(self, request):
         return [Category.objects.root_category()] + list(
         return [Category.objects.root_category()] + list(
             Category.objects.all_categories().filter(
             Category.objects.all_categories().filter(
-                id__in=request.user.acl_cache['browseable_categories']
-            ).select_related('parent'))
+                id__in=request.user.acl_cache['browseable_categories'],
+            ).select_related('parent')
+        )
 
 
 
 
 class ThreadsCategory(ThreadsRootCategory):
 class ThreadsCategory(ThreadsRootCategory):
@@ -91,5 +87,16 @@ class PrivateThreadsCategory(ViewModel):
 
 
 
 
 BasicCategorySerializer = CategorySerializer.subset_fields(
 BasicCategorySerializer = CategorySerializer.subset_fields(
-    'id', 'parent', 'name', 'description', 'is_closed', 'css_class',
-    'absolute_url', 'api_url', 'level', 'lft', 'rght', 'is_read')
+    'id',
+    'parent',
+    'name',
+    'description',
+    'is_closed',
+    'css_class',
+    'absolute_url',
+    'api_url',
+    'level',
+    'lft',
+    'rght',
+    'is_read',
+)

+ 2 - 7
misago/threads/viewmodels/post.py

@@ -26,11 +26,7 @@ class ViewModel(BaseViewModel):
         if select_for_update:
         if select_for_update:
             queryset = queryset.select_for_update()
             queryset = queryset.select_for_update()
         else:
         else:
-            queryset = queryset.select_related(
-                'poster',
-                'poster__rank',
-                'poster__ban_cache'
-            )
+            queryset = queryset.select_related('poster', 'poster__rank', 'poster__ban_cache')
 
 
         post = get_object_or_404(queryset, pk=pk)
         post = get_object_or_404(queryset, pk=pk)
 
 
@@ -40,8 +36,7 @@ class ViewModel(BaseViewModel):
         return post
         return post
 
 
     def get_queryset(self, request, thread):
     def get_queryset(self, request, thread):
-        return exclude_invisible_posts(
-            request.user, thread.category, thread.post_set)
+        return exclude_invisible_posts(request.user, thread.category, thread.post_set)
 
 
 
 
 class ThreadPost(ViewModel):
 class ThreadPost(ViewModel):

+ 7 - 5
misago/threads/viewmodels/posts.py

@@ -23,8 +23,9 @@ class ViewModel(object):
 
 
         posts_limit = settings.MISAGO_POSTS_PER_PAGE
         posts_limit = settings.MISAGO_POSTS_PER_PAGE
         posts_orphans = settings.MISAGO_POSTS_TAIL
         posts_orphans = settings.MISAGO_POSTS_TAIL
-        list_page = paginate(posts_queryset, page, posts_limit, posts_orphans,
-                             paginator=PostsPaginator)
+        list_page = paginate(
+            posts_queryset, page, posts_limit, posts_orphans, paginator=PostsPaginator
+        )
         paginator = pagination_dict(list_page)
         paginator = pagination_dict(list_page)
 
 
         posts = list(list_page.object_list)
         posts = list(list_page.object_list)
@@ -53,7 +54,8 @@ class ViewModel(object):
 
 
             events_limit = settings.MISAGO_EVENTS_PER_PAGE
             events_limit = settings.MISAGO_EVENTS_PER_PAGE
             posts += self.get_events_queryset(
             posts += self.get_events_queryset(
-                request, thread_model, events_limit, first_post, last_post)
+                request, thread_model, events_limit, first_post, last_post
+            )
 
 
             # sort both by pk
             # sort both by pk
             posts.sort(key=lambda p: p.pk)
             posts.sort(key=lambda p: p.pk)
@@ -72,7 +74,7 @@ class ViewModel(object):
             'poster',
             'poster',
             'poster__rank',
             'poster__rank',
             'poster__ban_cache',
             'poster__ban_cache',
-            'poster__online_tracker'
+            'poster__online_tracker',
         ).filter(is_event=False).order_by('id')
         ).filter(is_event=False).order_by('id')
         return exclude_invisible_posts(request.user, thread.category, queryset)
         return exclude_invisible_posts(request.user, thread.category, queryset)
 
 
@@ -99,7 +101,7 @@ class ViewModel(object):
     def get_template_context(self):
     def get_template_context(self):
         return {
         return {
             'posts': self.posts,
             'posts': self.posts,
-            'paginator': self.paginator
+            'paginator': self.paginator,
         }
         }
 
 
 
 

+ 20 - 13
misago/threads/viewmodels/thread.py

@@ -18,20 +18,27 @@ from misago.threads.threadtypes import trees_map
 
 
 __all__ = ['ForumThread', 'PrivateThread']
 __all__ = ['ForumThread', 'PrivateThread']
 
 
-
-BASE_RELATIONS = (
+BASE_RELATIONS = [
     'category',
     'category',
     'poll',
     'poll',
     'starter',
     'starter',
     'starter__rank',
     'starter__rank',
     'starter__ban_cache',
     'starter__ban_cache',
-    'starter__online_tracker'
-)
+    'starter__online_tracker',
+]
 
 
 
 
 class ViewModel(BaseViewModel):
 class ViewModel(BaseViewModel):
-    def __init__(self, request, pk, slug=None, read_aware=False,
-            subscription_aware=False, poll_votes_aware=False, select_for_update=False):
+    def __init__(
+            self,
+            request,
+            pk,
+            slug=None,
+            read_aware=False,
+            subscription_aware=False,
+            poll_votes_aware=False,
+            select_for_update=False
+    ):
         model = self.get_thread(request, pk, slug, select_for_update)
         model = self.get_thread(request, pk, slug, select_for_update)
 
 
         model.path = self.get_thread_path(model.category)
         model.path = self.get_thread_path(model.category)
@@ -60,16 +67,16 @@ class ViewModel(BaseViewModel):
         return self._poll
         return self._poll
 
 
     def get_thread(self, request, pk, slug=None, select_for_update=False):
     def get_thread(self, request, pk, slug=None, select_for_update=False):
-        raise NotImplementedError('Thread view model has to implement get_thread(request, pk, slug=None)')
+        raise NotImplementedError(
+            'Thread view model has to implement get_thread(request, pk, slug=None)'
+        )
 
 
     def get_thread_path(self, category):
     def get_thread_path(self, category):
         thread_path = []
         thread_path = []
 
 
         if category.level:
         if category.level:
             categories = Category.objects.filter(
             categories = Category.objects.filter(
-                tree_id=category.tree_id,
-                lft__lte=category.lft,
-                rght__gte=category.rght
+                tree_id=category.tree_id, lft__lte=category.lft, rght__gte=category.rght
             ).order_by('level')
             ).order_by('level')
             thread_path = list(categories)
             thread_path = list(categories)
         else:
         else:
@@ -89,7 +96,7 @@ class ViewModel(BaseViewModel):
             'thread': self._model,
             'thread': self._model,
             'poll': self._poll,
             'poll': self._poll,
             'category': self._model.category,
             'category': self._model.category,
-            'breadcrumbs': self._model.path
+            'breadcrumbs': self._model.path,
         }
         }
 
 
 
 
@@ -103,7 +110,7 @@ class ForumThread(ViewModel):
         thread = get_object_or_404(
         thread = get_object_or_404(
             queryset,
             queryset,
             pk=pk,
             pk=pk,
-            category__tree_id=trees_map.get_tree_id_for_root(THREADS_ROOT_NAME)
+            category__tree_id=trees_map.get_tree_id_for_root(THREADS_ROOT_NAME),
         )
         )
 
 
         allow_see_thread(request.user, thread)
         allow_see_thread(request.user, thread)
@@ -130,7 +137,7 @@ class PrivateThread(ViewModel):
         thread = get_object_or_404(
         thread = get_object_or_404(
             queryset,
             queryset,
             pk=pk,
             pk=pk,
-            category__tree_id=trees_map.get_tree_id_for_root(PRIVATE_THREADS_ROOT_NAME)
+            category__tree_id=trees_map.get_tree_id_for_root(PRIVATE_THREADS_ROOT_NAME),
         )
         )
 
 
         make_participants_aware(request.user, thread)
         make_participants_aware(request.user, thread)

+ 23 - 24
misago/threads/viewmodels/threads.py

@@ -21,7 +21,6 @@ from misago.threads.utils import add_categories_to_items
 
 
 __all__ = ['ForumThreads', 'PrivateThreads', 'filter_read_threads_queryset']
 __all__ = ['ForumThreads', 'PrivateThreads', 'filter_read_threads_queryset']
 
 
-
 LISTS_NAMES = {
 LISTS_NAMES = {
     'all': None,
     'all': None,
     'my': ugettext_lazy("Your threads"),
     'my': ugettext_lazy("Your threads"),
@@ -49,15 +48,21 @@ class ViewModel(object):
         base_queryset = self.get_base_queryset(request, category.categories, list_type)
         base_queryset = self.get_base_queryset(request, category.categories, list_type)
         threads_categories = [category_model] + category.subcategories
         threads_categories = [category_model] + category.subcategories
 
 
-        threads_queryset = self.get_remaining_threads_queryset(base_queryset, category_model, threads_categories)
+        threads_queryset = self.get_remaining_threads_queryset(
+            base_queryset, category_model, threads_categories
+        )
 
 
-        list_page = paginate(threads_queryset, page, settings.MISAGO_THREADS_PER_PAGE, settings.MISAGO_THREADS_TAIL)
+        list_page = paginate(
+            threads_queryset, page, settings.MISAGO_THREADS_PER_PAGE, settings.MISAGO_THREADS_TAIL
+        )
         paginator = pagination_dict(list_page)
         paginator = pagination_dict(list_page)
 
 
         if list_page.number > 1:
         if list_page.number > 1:
             threads = list(list_page.object_list)
             threads = list(list_page.object_list)
         else:
         else:
-            pinned_threads = list(self.get_pinned_threads(base_queryset, category_model, threads_categories))
+            pinned_threads = list(
+                self.get_pinned_threads(base_queryset, category_model, threads_categories)
+            )
             threads = list(pinned_threads) + list(list_page.object_list)
             threads = list(pinned_threads) + list(list_page.object_list)
 
 
         if list_type in ('new', 'unread'):
         if list_type in ('new', 'unread'):
@@ -90,13 +95,15 @@ class ViewModel(object):
             has_permission = request.user.acl_cache['can_see_unapproved_content_lists']
             has_permission = request.user.acl_cache['can_see_unapproved_content_lists']
             if list_type == 'unapproved' and not has_permission:
             if list_type == 'unapproved' and not has_permission:
                 raise PermissionDenied(
                 raise PermissionDenied(
-                    _("You don't have permission to see unapproved content lists."))
+                    _("You don't have permission to see unapproved content lists.")
+                )
 
 
     def get_list_name(self, list_type):
     def get_list_name(self, list_type):
         return LISTS_NAMES[list_type]
         return LISTS_NAMES[list_type]
 
 
     def get_base_queryset(self, request, threads_categories, list_type):
     def get_base_queryset(self, request, threads_categories, list_type):
-        return get_threads_queryset(request.user, threads_categories, list_type).order_by('-last_post_id')
+        return get_threads_queryset(request.user, threads_categories,
+                                    list_type).order_by('-last_post_id')
 
 
     def get_pinned_threads(self, queryset, category, threads_categories):
     def get_pinned_threads(self, queryset, category, threads_categories):
         return []
         return []
@@ -105,13 +112,13 @@ class ViewModel(object):
         return []
         return []
 
 
     def filter_threads(self, request, threads):
     def filter_threads(self, request, threads):
-        pass # hook for custom thread types to add features to extend threads
+        pass  # hook for custom thread types to add features to extend threads
 
 
     def get_frontend_context(self):
     def get_frontend_context(self):
         context = {
         context = {
             'THREADS': {
             'THREADS': {
                 'results': ThreadsListSerializer(self.threads, many=True).data,
                 'results': ThreadsListSerializer(self.threads, many=True).data,
-                'subcategories': [c.pk for c in self.category.children]
+                'subcategories': [c.pk for c in self.category.children],
             },
             },
         }
         }
 
 
@@ -122,19 +129,16 @@ class ViewModel(object):
         return {
         return {
             'list_name': self.get_list_name(self.list_type),
             'list_name': self.get_list_name(self.list_type),
             'list_type': self.list_type,
             'list_type': self.list_type,
-
             'threads': self.threads,
             'threads': self.threads,
-            'paginator': self.paginator
+            'paginator': self.paginator,
         }
         }
 
 
 
 
 class ForumThreads(ViewModel):
 class ForumThreads(ViewModel):
     def get_pinned_threads(self, queryset, category, threads_categories):
     def get_pinned_threads(self, queryset, category, threads_categories):
         if category.level:
         if category.level:
-            return list(queryset.filter(weight=2)) + list(queryset.filter(
-                weight=1,
-                category__in=threads_categories
-            ))
+            return list(queryset.filter(weight=2)
+                        ) + list(queryset.filter(weight=1, category__in=threads_categories))
         else:
         else:
             return queryset.filter(weight=2)
             return queryset.filter(weight=2)
 
 
@@ -153,14 +157,14 @@ class ForumThreads(ViewModel):
 
 
 class PrivateThreads(ViewModel):
 class PrivateThreads(ViewModel):
     def get_base_queryset(self, request, threads_categories, list_type):
     def get_base_queryset(self, request, threads_categories, list_type):
-        queryset = super(PrivateThreads, self).get_base_queryset(request, threads_categories, list_type)
+        queryset = super(PrivateThreads,
+                         self).get_base_queryset(request, threads_categories, list_type)
 
 
         # limit queryset to threads we are participant of
         # limit queryset to threads we are participant of
         participated_threads = request.user.threadparticipant_set.values('thread_id')
         participated_threads = request.user.threadparticipant_set.values('thread_id')
 
 
         if request.user.acl_cache['can_moderate_private_threads']:
         if request.user.acl_cache['can_moderate_private_threads']:
-            queryset = queryset.filter(
-                Q(id__in=participated_threads) | Q(has_reported_posts=True))
+            queryset = queryset.filter(Q(id__in=participated_threads) | Q(has_reported_posts=True))
         else:
         else:
             queryset = queryset.filter(id__in=participated_threads)
             queryset = queryset.filter(id__in=participated_threads)
 
 
@@ -173,9 +177,6 @@ class PrivateThreads(ViewModel):
         make_participants_aware(request.user, threads)
         make_participants_aware(request.user, threads)
 
 
 
 
-"""
-Thread queryset utils
-"""
 def get_threads_queryset(user, categories, list_type):
 def get_threads_queryset(user, categories, list_type):
     queryset = exclude_invisible_threads(user, categories, Thread.objects)
     queryset = exclude_invisible_threads(user, categories, Thread.objects)
 
 
@@ -214,9 +215,7 @@ def filter_read_threads_queryset(user, categories, list_type, queryset):
     if list_type == 'new':
     if list_type == 'new':
         # new threads have no entry in reads table
         # new threads have no entry in reads table
         # AND were started after cutoff date
         # AND were started after cutoff date
-        read_threads = user.threadread_set.filter(
-            category__in=categories
-        ).values('thread_id')
+        read_threads = user.threadread_set.filter(category__in=categories).values('thread_id')
 
 
         condition = Q(last_post_on__lte=cutoff_date)
         condition = Q(last_post_on__lte=cutoff_date)
         condition = condition | Q(id__in=read_threads)
         condition = condition | Q(id__in=read_threads)
@@ -235,7 +234,7 @@ def filter_read_threads_queryset(user, categories, list_type, queryset):
         read_threads = user.threadread_set.filter(
         read_threads = user.threadread_set.filter(
             category__in=categories,
             category__in=categories,
             thread__last_post_on__gt=cutoff_date,
             thread__last_post_on__gt=cutoff_date,
-            last_read_on__lt=F('thread__last_post_on')
+            last_read_on__lt=F('thread__last_post_on'),
         ).values('thread_id')
         ).values('thread_id')
 
 
         queryset = queryset.filter(id__in=read_threads)
         queryset = queryset.filter(id__in=read_threads)

+ 6 - 9
misago/threads/views/admin/attachments.py

@@ -1,8 +1,5 @@
 from django.contrib import messages
 from django.contrib import messages
 from django.db import transaction
 from django.db import transaction
-from django.db.models import Count
-from django.shortcuts import redirect
-from django.urls import reverse
 from django.utils.translation import ugettext_lazy as _
 from django.utils.translation import ugettext_lazy as _
 
 
 from misago.admin.views import generic
 from misago.admin.views import generic
@@ -23,14 +20,14 @@ class AttachmentAdmin(generic.AdminBaseMixin):
 
 
 class AttachmentsList(AttachmentAdmin, generic.ListView):
 class AttachmentsList(AttachmentAdmin, generic.ListView):
     items_per_page = 20
     items_per_page = 20
-    ordering = (
+    ordering = [
         ('-id', _("From newest")),
         ('-id', _("From newest")),
         ('id', _("From oldest")),
         ('id', _("From oldest")),
         ('filename', _("A to z")),
         ('filename', _("A to z")),
         ('-filename', _("Z to a")),
         ('-filename', _("Z to a")),
         ('size', _("Smallest files")),
         ('size', _("Smallest files")),
         ('-size', _("Largest files")),
         ('-size', _("Largest files")),
-    )
+    ]
     selection_label = _('With attachments: 0')
     selection_label = _('With attachments: 0')
     empty_selection_label = _('Select attachments')
     empty_selection_label = _('Select attachments')
     mass_actions = [
     mass_actions = [
@@ -39,8 +36,8 @@ class AttachmentsList(AttachmentAdmin, generic.ListView):
             'name': _("Delete attachments"),
             'name': _("Delete attachments"),
             'icon': 'fa fa-times-circle',
             'icon': 'fa fa-times-circle',
             'confirmation': _("Are you sure you want to delete selected attachments?"),
             'confirmation': _("Are you sure you want to delete selected attachments?"),
-            'is_atomic': False
-        }
+            'is_atomic': False,
+        },
     ]
     ]
 
 
     def get_search_form(self, request):
     def get_search_form(self, request):
@@ -68,7 +65,7 @@ class AttachmentsList(AttachmentAdmin, generic.ListView):
 
 
     def delete_from_cache(self, post, attachments):
     def delete_from_cache(self, post, attachments):
         if not post.attachments_cache:
         if not post.attachments_cache:
-            return # admin action may be taken due to desynced state
+            return  # admin action may be taken due to desynced state
 
 
         clean_cache = []
         clean_cache = []
         for a in post.attachments_cache:
         for a in post.attachments_cache:
@@ -89,7 +86,7 @@ class DeleteAttachment(AttachmentAdmin, generic.ButtonView):
 
 
     def delete_from_cache(self, attachment):
     def delete_from_cache(self, attachment):
         if not attachment.post.attachments_cache:
         if not attachment.post.attachments_cache:
-            return # admin action may be taken due to desynced state
+            return  # admin action may be taken due to desynced state
 
 
         clean_cache = []
         clean_cache = []
         for a in attachment.post.attachments_cache:
         for a in attachment.post.attachments_cache:

+ 4 - 4
misago/threads/views/admin/attachmenttypes.py

@@ -1,7 +1,5 @@
 from django.contrib import messages
 from django.contrib import messages
 from django.db.models import Count
 from django.db.models import Count
-from django.shortcuts import redirect
-from django.urls import reverse
 from django.utils.translation import ugettext_lazy as _
 from django.utils.translation import ugettext_lazy as _
 
 
 from misago.admin.views import generic
 from misago.admin.views import generic
@@ -27,7 +25,7 @@ class AttachmentTypeAdmin(generic.AdminBaseMixin):
 
 
 
 
 class AttachmentTypesList(AttachmentTypeAdmin, generic.ListView):
 class AttachmentTypesList(AttachmentTypeAdmin, generic.ListView):
-    ordering = (('name', None),)
+    ordering = (('name', None), )
 
 
     def get_queryset(self):
     def get_queryset(self):
         queryset = super(AttachmentTypesList, self).get_queryset()
         queryset = super(AttachmentTypesList, self).get_queryset()
@@ -45,7 +43,9 @@ class EditAttachmentType(AttachmentTypeAdmin, generic.ModelFormView):
 class DeleteAttachmentType(AttachmentTypeAdmin, generic.ButtonView):
 class DeleteAttachmentType(AttachmentTypeAdmin, generic.ButtonView):
     def check_permissions(self, request, target):
     def check_permissions(self, request, target):
         if target.attachment_set.exists():
         if target.attachment_set.exists():
-            message = _('Attachment type "%(name)s" has associated attachments and can\'t be deleted.')
+            message = _(
+                'Attachment type "%(name)s" has associated attachments and can\'t be deleted.'
+            )
             return message % {'name': target.name}
             return message % {'name': target.name}
 
 
     def button_action(self, request, target):
     def button_action(self, request, target):

+ 0 - 3
misago/threads/views/attachment.py

@@ -1,9 +1,6 @@
 from __future__ import unicode_literals
 from __future__ import unicode_literals
 
 
-import os
-
 from django.core.exceptions import PermissionDenied
 from django.core.exceptions import PermissionDenied
-from django.db.models import F
 from django.http import Http404
 from django.http import Http404
 from django.shortcuts import get_object_or_404, redirect
 from django.shortcuts import get_object_or_404, redirect
 
 

+ 16 - 6
misago/threads/views/goto.py

@@ -12,7 +12,7 @@ from misago.threads.viewmodels import ForumThread, PrivateThread
 
 
 class GotoView(View):
 class GotoView(View):
     thread = None
     thread = None
-    read_aware=False
+    read_aware = False
 
 
     def get(self, request, pk, slug, **kwargs):
     def get(self, request, pk, slug, **kwargs):
         thread = self.get_thread(request, pk, slug).unwrap()
         thread = self.get_thread(request, pk, slug).unwrap()
@@ -40,7 +40,7 @@ class GotoView(View):
 
 
         thread_len = posts_queryset.count()
         thread_len = posts_queryset.count()
         if thread_len <= settings.MISAGO_POSTS_PER_PAGE + settings.MISAGO_POSTS_TAIL:
         if thread_len <= settings.MISAGO_POSTS_PER_PAGE + settings.MISAGO_POSTS_TAIL:
-            return 1 # no chance for post to be on other page than only page
+            return 1  # no chance for post to be on other page than only page
 
 
         # compute total count of thread pages
         # compute total count of thread pages
         hits = max(1, thread_len - settings.MISAGO_POSTS_TAIL)
         hits = max(1, thread_len - settings.MISAGO_POSTS_TAIL)
@@ -89,7 +89,9 @@ class ThreadGotoNewView(GotoView):
 
 
     def get_target_post(self, thread, posts_queryset, **kwargs):
     def get_target_post(self, thread, posts_queryset, **kwargs):
         if thread.is_new:
         if thread.is_new:
-            return posts_queryset.filter(posted_on__gt=thread.last_read_on).order_by('id').first()
+            return posts_queryset.filter(
+                posted_on__gt=thread.last_read_on,
+            ).order_by('id').first()
         else:
         else:
             return posts_queryset.order_by('id').last()
             return posts_queryset.order_by('id').last()
 
 
@@ -100,10 +102,16 @@ class ThreadGotoUnapprovedView(GotoView):
     def test_permissions(self, request, thread):
     def test_permissions(self, request, thread):
         if not thread.acl['can_approve']:
         if not thread.acl['can_approve']:
             raise PermissionDenied(
             raise PermissionDenied(
-                _("You need permission to approve content to be able to go to first unapproved post."))
+                _(
+                    "You need permission to approve content to "
+                    "be able to go to first unapproved post."
+                )
+            )
 
 
     def get_target_post(self, thread, posts_queryset, **kwargs):
     def get_target_post(self, thread, posts_queryset, **kwargs):
-        unapproved_post = posts_queryset.filter(is_unapproved=True).order_by('id').first()
+        unapproved_post = posts_queryset.filter(
+            is_unapproved=True,
+        ).order_by('id').first()
         if unapproved_post:
         if unapproved_post:
             return unapproved_post
             return unapproved_post
         else:
         else:
@@ -130,6 +138,8 @@ class PrivateThreadGotoNewView(GotoView):
 
 
     def get_target_post(self, thread, posts_queryset, **kwargs):
     def get_target_post(self, thread, posts_queryset, **kwargs):
         if thread.is_new:
         if thread.is_new:
-            return posts_queryset.filter(posted_on__gt=thread.last_read_on).order_by('id').first()
+            return posts_queryset.filter(
+                posted_on__gt=thread.last_read_on,
+            ).order_by('id').first()
         else:
         else:
             return posts_queryset.order_by('id').last()
             return posts_queryset.order_by('id').last()

+ 6 - 7
misago/threads/views/list.py

@@ -1,4 +1,3 @@
-from django.conf import settings
 from django.http import Http404
 from django.http import Http404
 from django.shortcuts import render
 from django.shortcuts import render
 from django.urls import reverse
 from django.urls import reverse
@@ -9,7 +8,7 @@ from misago.threads.viewmodels import (
     ForumThreads, PrivateThreads, PrivateThreadsCategory, ThreadsCategory, ThreadsRootCategory)
     ForumThreads, PrivateThreads, PrivateThreadsCategory, ThreadsCategory, ThreadsRootCategory)
 
 
 
 
-class ListBase(View):
+class ThreadsList(View):
     category = None
     category = None
     threads = None
     threads = None
 
 
@@ -56,7 +55,7 @@ class ListBase(View):
         return {}
         return {}
 
 
 
 
-class ForumThreads(ListBase):
+class ForumThreadsList(ThreadsList):
     category = ThreadsRootCategory
     category = ThreadsRootCategory
     threads = ForumThreads
     threads = ForumThreads
 
 
@@ -68,19 +67,19 @@ class ForumThreads(ListBase):
         }
         }
 
 
 
 
-class CategoryThreads(ForumThreads):
+class CategoryThreadsList(ForumThreadsList):
     category = ThreadsCategory
     category = ThreadsCategory
 
 
     template_name = 'misago/threadslist/category.html'
     template_name = 'misago/threadslist/category.html'
 
 
     def get_category(self, request, **kwargs):
     def get_category(self, request, **kwargs):
-        category = super(CategoryThreads, self).get_category(request, **kwargs)
+        category = super(CategoryThreadsList, self).get_category(request, **kwargs)
         if not category.level:
         if not category.level:
-            raise Http404() # disallow root category access
+            raise Http404()  # disallow root category access
         return category
         return category
 
 
 
 
-class PrivateThreads(ListBase):
+class PrivateThreadsList(ThreadsList):
     category = PrivateThreadsCategory
     category = PrivateThreadsCategory
     threads = PrivateThreads
     threads = PrivateThreads
 
 

+ 7 - 8
misago/threads/views/thread.py

@@ -23,10 +23,7 @@ class ThreadBase(View):
 
 
     def get_thread(self, request, pk, slug):
     def get_thread(self, request, pk, slug):
         return self.thread(
         return self.thread(
-            request, pk, slug,
-            read_aware=True,
-            subscription_aware=True,
-            poll_votes_aware=True
+            request, pk, slug, read_aware=True, subscription_aware=True, poll_votes_aware=True
         )
         )
 
 
     def get_posts(self, request, thread, page):
     def get_posts(self, request, thread, page):
@@ -47,7 +44,9 @@ class ThreadBase(View):
 
 
     def get_template_context(self, request, thread, posts):
     def get_template_context(self, request, thread, posts):
         context = {
         context = {
-            'url_name': ':'.join(request.resolver_match.namespaces + [request.resolver_match.url_name])
+            'url_name': ':'.join(request.resolver_match.namespaces + [
+                request.resolver_match.url_name,
+            ])
         }
         }
 
 
         context.update(thread.get_template_context())
         context.update(thread.get_template_context())
@@ -56,16 +55,16 @@ class ThreadBase(View):
         return context
         return context
 
 
 
 
-class Thread(ThreadBase):
+class ThreadView(ThreadBase):
     thread = ForumThread
     thread = ForumThread
     template_name = 'misago/thread/thread.html'
     template_name = 'misago/thread/thread.html'
 
 
     def get_default_frontend_context(self):
     def get_default_frontend_context(self):
         return {
         return {
-            'THREADS_API': reverse('misago:api:thread-list')
+            'THREADS_API': reverse('misago:api:thread-list'),
         }
         }
 
 
 
 
-class PrivateThread(ThreadBase):
+class PrivateThreadView(ThreadBase):
     thread = PrivateThread
     thread = PrivateThread
     template_name = 'misago/thread/private_thread.html'
     template_name = 'misago/thread/private_thread.html'

+ 4 - 7
misago/urls.py

@@ -14,10 +14,10 @@ urlpatterns = [
     url(r'^', include('misago.search.urls')),
     url(r'^', include('misago.search.urls')),
 
 
     # default robots.txt
     # default robots.txt
-    url(r'^robots.txt$', TemplateView.as_view(
-        content_type='text/plain',
-        template_name='misago/robots.txt'
-    )),
+    url(
+        r'^robots.txt$',
+        TemplateView.as_view(content_type='text/plain', template_name='misago/robots.txt')
+    ),
 
 
     # "misago:index" link symbolises "root" of Misago links space
     # "misago:index" link symbolises "root" of Misago links space
     # any request with path that falls below this one is assumed to be directed
     # any request with path that falls below this one is assumed to be directed
@@ -26,7 +26,6 @@ urlpatterns = [
     url(r'^$', forum_index, name='index'),
     url(r'^$', forum_index, name='index'),
 ]
 ]
 
 
-
 # Register API
 # Register API
 apipatterns = [
 apipatterns = [
     url(r'^', include('misago.categories.urls.api')),
     url(r'^', include('misago.categories.urls.api')),
@@ -40,7 +39,6 @@ urlpatterns += [
     url(r'^api/', include(apipatterns, namespace='api')),
     url(r'^api/', include(apipatterns, namespace='api')),
 ]
 ]
 
 
-
 # Register Misago ACP
 # Register Misago ACP
 if settings.MISAGO_ADMIN_PATH:
 if settings.MISAGO_ADMIN_PATH:
     # Admin patterns recognised by Misago
     # Admin patterns recognised by Misago
@@ -53,7 +51,6 @@ if settings.MISAGO_ADMIN_PATH:
         url(admin_prefix, include(adminpatterns, namespace='admin')),
         url(admin_prefix, include(adminpatterns, namespace='admin')),
     ]
     ]
 
 
-
 # Make error pages accessible casually in DEBUG
 # Make error pages accessible casually in DEBUG
 if settings.DEBUG:
 if settings.DEBUG:
     from misago.core import errorpages
     from misago.core import errorpages

+ 3 - 8
misago/users/activepostersranking.py

@@ -38,15 +38,10 @@ def build_active_posters_ranking():
         ranked_categories.append(category.pk)
         ranked_categories.append(category.pk)
 
 
     queryset = UserModel.objects.filter(
     queryset = UserModel.objects.filter(
-        is_active=True,
-        posts__gt=0
+        is_active=True, posts__gt=0
     ).filter(
     ).filter(
-        post__posted_on__gte=tracked_since,
-        post__category__in=ranked_categories
+        post__posted_on__gte=tracked_since, post__category__in=ranked_categories
     ).annotate(score=Count('post'))
     ).annotate(score=Count('post'))
 
 
     for ranking in queryset[:settings.MISAGO_RANKING_SIZE].iterator():
     for ranking in queryset[:settings.MISAGO_RANKING_SIZE].iterator():
-        ActivityRanking.objects.create(
-            user=ranking,
-            score=ranking.score
-        )
+        ActivityRanking.objects.create(user=ranking, score=ranking.score)

+ 16 - 5
misago/users/admin.py

@@ -21,19 +21,29 @@ class MisagoAdminExtension(object):
 
 
         # Accounts
         # Accounts
         urlpatterns.namespace(r'^accounts/', 'accounts', 'users')
         urlpatterns.namespace(r'^accounts/', 'accounts', 'users')
-        urlpatterns.patterns('users:accounts',
+        urlpatterns.patterns(
+            'users:accounts',
             url(r'^$', UsersList.as_view(), name='index'),
             url(r'^$', UsersList.as_view(), name='index'),
             url(r'^(?P<page>\d+)/$', UsersList.as_view(), name='index'),
             url(r'^(?P<page>\d+)/$', UsersList.as_view(), name='index'),
             url(r'^new/$', NewUser.as_view(), name='new'),
             url(r'^new/$', NewUser.as_view(), name='new'),
             url(r'^edit/(?P<pk>\d+)/$', EditUser.as_view(), name='edit'),
             url(r'^edit/(?P<pk>\d+)/$', EditUser.as_view(), name='edit'),
-            url(r'^delete-threads/(?P<pk>\d+)/$', DeleteThreadsStep.as_view(), name='delete-threads'),
+            url(
+                r'^delete-threads/(?P<pk>\d+)/$',
+                DeleteThreadsStep.as_view(),
+                name='delete-threads'
+            ),
             url(r'^delete-posts/(?P<pk>\d+)/$', DeletePostsStep.as_view(), name='delete-posts'),
             url(r'^delete-posts/(?P<pk>\d+)/$', DeletePostsStep.as_view(), name='delete-posts'),
-            url(r'^delete-account/(?P<pk>\d+)/$', DeleteAccountStep.as_view(), name='delete-account'),
+            url(
+                r'^delete-account/(?P<pk>\d+)/$',
+                DeleteAccountStep.as_view(),
+                name='delete-account'
+            ),
         )
         )
 
 
         # Ranks
         # Ranks
         urlpatterns.namespace(r'^ranks/', 'ranks', 'users')
         urlpatterns.namespace(r'^ranks/', 'ranks', 'users')
-        urlpatterns.patterns('users:ranks',
+        urlpatterns.patterns(
+            'users:ranks',
             url(r'^$', RanksList.as_view(), name='index'),
             url(r'^$', RanksList.as_view(), name='index'),
             url(r'^new/$', NewRank.as_view(), name='new'),
             url(r'^new/$', NewRank.as_view(), name='new'),
             url(r'^edit/(?P<pk>\d+)/$', EditRank.as_view(), name='edit'),
             url(r'^edit/(?P<pk>\d+)/$', EditRank.as_view(), name='edit'),
@@ -46,7 +56,8 @@ class MisagoAdminExtension(object):
 
 
         # Bans
         # Bans
         urlpatterns.namespace(r'^bans/', 'bans', 'users')
         urlpatterns.namespace(r'^bans/', 'bans', 'users')
-        urlpatterns.patterns('users:bans',
+        urlpatterns.patterns(
+            'users:bans',
             url(r'^$', BansList.as_view(), name='index'),
             url(r'^$', BansList.as_view(), name='index'),
             url(r'^(?P<page>\d+)/$', BansList.as_view(), name='index'),
             url(r'^(?P<page>\d+)/$', BansList.as_view(), name='index'),
             url(r'^new/$', NewBan.as_view(), name='new'),
             url(r'^new/$', NewBan.as_view(), name='new'),

+ 78 - 64
misago/users/api/auth.py

@@ -29,28 +29,30 @@ def gateway(request):
         return session_user(request)
         return session_user(request)
 
 
 
 
-"""
-POST /auth/ with CSRF, username and password
-will attempt to authenticate new user
-"""
 @api_view(['POST'])
 @api_view(['POST'])
-@permission_classes((UnbannedAnonOnly,))
+@permission_classes((UnbannedAnonOnly, ))
 @csrf_protect
 @csrf_protect
 def login(request):
 def login(request):
+    """
+    POST /auth/ with CSRF, username and password
+    will attempt to authenticate new user
+    """
     form = AuthenticationForm(request, data=request.data)
     form = AuthenticationForm(request, data=request.data)
     if form.is_valid():
     if form.is_valid():
         auth.login(request, form.user_cache)
         auth.login(request, form.user_cache)
-        return Response(AuthenticatedUserSerializer(form.user_cache).data)
+        return Response(
+            AuthenticatedUserSerializer(form.user_cache).data,
+        )
     else:
     else:
-        return Response(form.get_errors_dict(),
-                        status=status.HTTP_400_BAD_REQUEST)
+        return Response(
+            form.get_errors_dict(),
+            status=status.HTTP_400_BAD_REQUEST,
+        )
 
 
 
 
-"""
-GET /auth/ will return current auth user, either User or AnonymousUser
-"""
 @api_view()
 @api_view()
 def session_user(request):
 def session_user(request):
+    """GET /auth/ will return current auth user, either User or AnonymousUser"""
     if request.user.is_authenticated:
     if request.user.is_authenticated:
         UserSerializer = AuthenticatedUserSerializer
         UserSerializer = AuthenticatedUserSerializer
     else:
     else:
@@ -59,11 +61,9 @@ def session_user(request):
     return Response(UserSerializer(request.user).data)
     return Response(UserSerializer(request.user).data)
 
 
 
 
-"""
-GET /auth/criteria/ will return password and username criteria for accounts
-"""
 @api_view(['GET'])
 @api_view(['GET'])
 def get_criteria(request):
 def get_criteria(request):
+    """GET /auth/criteria/ will return password and username criteria for accounts"""
     criteria = {
     criteria = {
         'username': {
         'username': {
             'min_length': settings.username_length_min,
             'min_length': settings.username_length_min,
@@ -73,9 +73,7 @@ def get_criteria(request):
     }
     }
 
 
     for validator in settings.AUTH_PASSWORD_VALIDATORS:
     for validator in settings.AUTH_PASSWORD_VALIDATORS:
-        validator_dict = {
-            'name': validator['NAME'].split('.')[-1]
-        }
+        validator_dict = {'name': validator['NAME'].split('.')[-1]}
 
 
         validator_dict.update(validator.get('OPTIONS', {}))
         validator_dict.update(validator.get('OPTIONS', {}))
 
 
@@ -84,84 +82,96 @@ def get_criteria(request):
     return Response(criteria)
     return Response(criteria)
 
 
 
 
-"""
-POST /auth/send-activation/ with CSRF token and email
-will mail account activation link to requester
-"""
 @api_view(['POST'])
 @api_view(['POST'])
-@permission_classes((UnbannedAnonOnly,))
+@permission_classes((UnbannedAnonOnly, ))
 @csrf_protect
 @csrf_protect
 def send_activation(request):
 def send_activation(request):
+    """
+    POST /auth/send-activation/ with CSRF token and email
+    will mail account activation link to requester
+    """
     form = ResendActivationForm(request.data)
     form = ResendActivationForm(request.data)
     if form.is_valid():
     if form.is_valid():
         requesting_user = form.user_cache
         requesting_user = form.user_cache
 
 
-        mail_subject = _("Activate %(user)s account on %(forum_name)s forums")
-        subject_formats = {
+        mail_subject = _("Activate %(user)s account on %(forum_name)s forums") % {
             'user': requesting_user.username,
             'user': requesting_user.username,
             'forum_name': settings.forum_name,
             'forum_name': settings.forum_name,
         }
         }
-        mail_subject = mail_subject % subject_formats
 
 
-        mail_user(request, requesting_user, mail_subject,
-                  'misago/emails/activation/by_user',
-                  {'activation_token': make_activation_token(requesting_user)})
+        mail_user(
+            request,
+            requesting_user,
+            mail_subject,
+            'misago/emails/activation/by_user',
+            {
+                'activation_token': make_activation_token(requesting_user),
+            },
+        )
 
 
         return Response({
         return Response({
-                'username': form.user_cache.username,
-                'email': form.user_cache.email
-            })
+            'username': form.user_cache.username,
+            'email': form.user_cache.email,
+        })
     else:
     else:
-        return Response(form.get_errors_dict(),
-                        status=status.HTTP_400_BAD_REQUEST)
+        return Response(
+            form.get_errors_dict(),
+            status=status.HTTP_400_BAD_REQUEST,
+        )
 
 
 
 
-"""
-POST /auth/send-password-form/ with CSRF token and email
-will mail change password form link to requester
-"""
 @api_view(['POST'])
 @api_view(['POST'])
-@permission_classes((UnbannedOnly,))
+@permission_classes((UnbannedOnly, ))
 @csrf_protect
 @csrf_protect
 def send_password_form(request):
 def send_password_form(request):
+    """
+    POST /auth/send-password-form/ with CSRF token and email
+    will mail change password form link to requester
+    """
     form = ResetPasswordForm(request.data)
     form = ResetPasswordForm(request.data)
     if form.is_valid():
     if form.is_valid():
         requesting_user = form.user_cache
         requesting_user = form.user_cache
 
 
-        mail_subject = _("Change %(user)s password on %(forum_name)s forums")
-        subject_formats = {
+        mail_subject = _("Change %(user)s password on %(forum_name)s forums") % {
             'user': requesting_user.username,
             'user': requesting_user.username,
             'forum_name': settings.forum_name,
             'forum_name': settings.forum_name,
         }
         }
-        mail_subject = mail_subject % subject_formats
 
 
         confirmation_token = make_password_change_token(requesting_user)
         confirmation_token = make_password_change_token(requesting_user)
 
 
-        mail_user(request, requesting_user, mail_subject,
-                  'misago/emails/change_password_form_link',
-                  {'confirmation_token': confirmation_token})
+        mail_user(
+            request,
+            requesting_user,
+            mail_subject,
+            'misago/emails/change_password_form_link',
+            {
+                'confirmation_token': confirmation_token,
+            },
+        )
 
 
         return Response({
         return Response({
-                'username': form.user_cache.username,
-                'email': form.user_cache.email
-            })
+            'username': form.user_cache.username,
+            'email': form.user_cache.email,
+        })
     else:
     else:
-        return Response(form.get_errors_dict(),
-                        status=status.HTTP_400_BAD_REQUEST)
+        return Response(
+            form.get_errors_dict(),
+            status=status.HTTP_400_BAD_REQUEST,
+        )
 
 
 
 
-"""
-POST /auth/change-password/user/token/ with CSRF and new password
-will change forgotten password
-"""
 class PasswordChangeFailed(Exception):
 class PasswordChangeFailed(Exception):
     pass
     pass
 
 
 
 
 @api_view(['POST'])
 @api_view(['POST'])
-@permission_classes((UnbannedOnly,))
+@permission_classes((UnbannedOnly, ))
 @csrf_protect
 @csrf_protect
 def change_forgotten_password(request, pk, token):
 def change_forgotten_password(request, pk, token):
+    """
+    POST /auth/change-password/user/token/ with CSRF and new password
+    will change forgotten password
+    """
     invalid_message = _("Form link is invalid. Please try again.")
     invalid_message = _("Form link is invalid. Please try again.")
     expired_message = _("Your link has expired. Please request new one.")
     expired_message = _("Your link has expired. Please request new one.")
 
 
@@ -181,9 +191,12 @@ def change_forgotten_password(request, pk, token):
         if get_user_ban(user):
         if get_user_ban(user):
             raise PasswordChangeFailed(expired_message)
             raise PasswordChangeFailed(expired_message)
     except PasswordChangeFailed as e:
     except PasswordChangeFailed as e:
-        return Response({
-                'detail': e.args[0]
-            }, status=status.HTTP_400_BAD_REQUEST)
+        return Response(
+            {
+                'detail': e.args[0],
+            },
+            status=status.HTTP_400_BAD_REQUEST,
+        )
 
 
     try:
     try:
         new_password = request.data.get('password', '').strip()
         new_password = request.data.get('password', '').strip()
@@ -191,10 +204,11 @@ def change_forgotten_password(request, pk, token):
         user.set_password(new_password)
         user.set_password(new_password)
         user.save()
         user.save()
     except ValidationError as e:
     except ValidationError as e:
-        return Response({
-                'detail': e.messages[0]
-            }, status=status.HTTP_400_BAD_REQUEST)
-
-    return Response({
-            'username': user.username
-        })
+        return Response(
+            {
+                'detail': e.messages[0],
+            },
+            status=status.HTTP_400_BAD_REQUEST,
+        )
+
+    return Response({'username': user.username})

+ 2 - 1
misago/users/api/rest_permissions.py

@@ -15,7 +15,8 @@ class UnbannedOnly(BasePermission):
             hydrated_ban = Ban(
             hydrated_ban = Ban(
                 check_type=Ban.IP,
                 check_type=Ban.IP,
                 user_message=ban['message'],
                 user_message=ban['message'],
-                expires_on=ban['expires_on'])
+                expires_on=ban['expires_on'],
+            )
             raise Banned(hydrated_ban)
             raise Banned(hydrated_ban)
 
 
     def has_permission(self, request, view):
     def has_permission(self, request, view):

+ 38 - 30
misago/users/api/userendpoints/avatar.py

@@ -3,7 +3,7 @@ import json
 from rest_framework import status
 from rest_framework import status
 from rest_framework.response import Response
 from rest_framework.response import Response
 
 
-from django.core.exceptions import PermissionDenied, ValidationError
+from django.core.exceptions import ValidationError
 from django.utils.translation import ugettext as _
 from django.utils.translation import ugettext as _
 
 
 from misago.conf import settings
 from misago.conf import settings
@@ -16,15 +16,17 @@ from misago.users.serializers import ModerateAvatarSerializer
 def avatar_endpoint(request, pk=None):
 def avatar_endpoint(request, pk=None):
     if request.user.is_avatar_locked:
     if request.user.is_avatar_locked:
         if request.user.avatar_lock_user_message:
         if request.user.avatar_lock_user_message:
-            reason = format_plaintext_for_html(
-                request.user.avatar_lock_user_message)
+            reason = format_plaintext_for_html(request.user.avatar_lock_user_message)
         else:
         else:
             reason = None
             reason = None
 
 
-        return Response({
-            'detail': _("Your avatar is locked. You can't change it."),
-            'reason': reason
-        }, status=status.HTTP_403_FORBIDDEN)
+        return Response(
+            {
+                'detail': _("Your avatar is locked. You can't change it."),
+                'reason': reason,
+            },
+            status=status.HTTP_403_FORBIDDEN,
+        )
 
 
     avatar_options = get_avatar_options(request.user)
     avatar_options = get_avatar_options(request.user)
     if request.method == 'POST':
     if request.method == 'POST':
@@ -41,7 +43,7 @@ def get_avatar_options(user):
         'crop_src': False,
         'crop_src': False,
         'crop_tmp': False,
         'crop_tmp': False,
         'upload': False,
         'upload': False,
-        'galleries': False
+        'galleries': False,
     }
     }
 
 
     # Allow existing galleries
     # Allow existing galleries
@@ -52,11 +54,11 @@ def get_avatar_options(user):
             for image in gallery['images']:
             for image in gallery['images']:
                 gallery_images.append({
                 gallery_images.append({
                     'id': image.id,
                     'id': image.id,
-                    'url': image.url
+                    'url': image.url,
                 })
                 })
             options['galleries'].append({
             options['galleries'].append({
                 'name': gallery['name'],
                 'name': gallery['name'],
-                'images': gallery_images
+                'images': gallery_images,
             })
             })
 
 
     # Can't have custom avatar?
     # Can't have custom avatar?
@@ -72,7 +74,7 @@ def get_avatar_options(user):
             options['crop_src'] = {
             options['crop_src'] = {
                 'url': user.avatar_src.url,
                 'url': user.avatar_src.url,
                 'crop': json.loads(user.avatar_crop),
                 'crop': json.loads(user.avatar_crop),
-                'size': max(settings.MISAGO_AVATARS_SIZES)
+                'size': max(settings.MISAGO_AVATARS_SIZES),
             }
             }
         except (TypeError, ValueError):
         except (TypeError, ValueError):
             pass
             pass
@@ -81,7 +83,7 @@ def get_avatar_options(user):
     if avatars.uploaded.has_temporary_avatar(user):
     if avatars.uploaded.has_temporary_avatar(user):
         options['crop_tmp'] = {
         options['crop_tmp'] = {
             'url': user.avatar_tmp.url,
             'url': user.avatar_tmp.url,
-            'size': max(settings.MISAGO_AVATARS_SIZES)
+            'size': max(settings.MISAGO_AVATARS_SIZES),
         }
         }
 
 
     # Allow upload conditions
     # Allow upload conditions
@@ -102,22 +104,31 @@ def avatar_post(options, user, data):
     try:
     try:
         type_options = options[data.get('avatar', 'nope')]
         type_options = options[data.get('avatar', 'nope')]
         if not type_options:
         if not type_options:
-            return Response({
-                'detail': _("This avatar type is not allowed.")
-            }, status=status.HTTP_400_BAD_REQUEST)
+            return Response(
+                {
+                    'detail': _("This avatar type is not allowed."),
+                },
+                status=status.HTTP_400_BAD_REQUEST,
+            )
 
 
         rpc_handler = AVATAR_TYPES[data.get('avatar', 'nope')]
         rpc_handler = AVATAR_TYPES[data.get('avatar', 'nope')]
     except KeyError:
     except KeyError:
-        return Response({
-            'detail': _("Unknown avatar type.")
-        }, status=status.HTTP_400_BAD_REQUEST)
+        return Response(
+            {
+                'detail': _("Unknown avatar type."),
+            },
+            status=status.HTTP_400_BAD_REQUEST,
+        )
 
 
     try:
     try:
         response_dict = {'detail': rpc_handler(user, data)}
         response_dict = {'detail': rpc_handler(user, data)}
     except AvatarError as e:
     except AvatarError as e:
-        return Response({
-            'detail': e.args[0]
-        }, status=status.HTTP_400_BAD_REQUEST)
+        return Response(
+            {
+                'detail': e.args[0],
+            },
+            status=status.HTTP_400_BAD_REQUEST,
+        )
 
 
     user.save()
     user.save()
 
 
@@ -125,9 +136,6 @@ def avatar_post(options, user, data):
     return Response(response_dict)
     return Response(response_dict)
 
 
 
 
-"""
-Avatar rpc handlers
-"""
 def avatar_generate(user, data):
 def avatar_generate(user, data):
     avatars.dynamic.set_avatar(user)
     avatars.dynamic.set_avatar(user)
     return _("New avatar based on your account was set.")
     return _("New avatar based on your account was set.")
@@ -138,8 +146,7 @@ def avatar_gravatar(user, data):
         avatars.gravatar.set_avatar(user)
         avatars.gravatar.set_avatar(user)
         return _("Gravatar was downloaded and set as new avatar.")
         return _("Gravatar was downloaded and set as new avatar.")
     except avatars.gravatar.NoGravatarAvailable:
     except avatars.gravatar.NoGravatarAvailable:
-        raise AvatarError(
-            _("No Gravatar is associated with your e-mail address."))
+        raise AvatarError(_("No Gravatar is associated with your e-mail address."))
     except avatars.gravatar.GravatarError:
     except avatars.gravatar.GravatarError:
         raise AvatarError(_("Failed to connect to Gravatar servers."))
         raise AvatarError(_("Failed to connect to Gravatar servers."))
 
 
@@ -182,8 +189,7 @@ def avatar_crop_tmp(user, data):
 
 
 def avatar_crop(user, data, suffix):
 def avatar_crop(user, data, suffix):
     try:
     try:
-        crop = avatars.uploaded.crop_source_image(
-            user, suffix, data.get('crop', {}))
+        crop = avatars.uploaded.crop_source_image(user, suffix, data.get('crop', {}))
         user.avatar_crop = json.dumps(crop)
         user.avatar_crop = json.dumps(crop)
     except ValidationError as e:
     except ValidationError as e:
         raise AvatarError(e.args[0])
         raise AvatarError(e.args[0])
@@ -194,7 +200,6 @@ AVATAR_TYPES = {
     'gravatar': avatar_gravatar,
     'gravatar': avatar_gravatar,
     'galleries': avatar_gallery,
     'galleries': avatar_gallery,
     'upload': avatar_upload,
     'upload': avatar_upload,
-
     'crop_src': avatar_crop_src,
     'crop_src': avatar_crop_src,
     'crop_tmp': avatar_crop_tmp,
     'crop_tmp': avatar_crop_tmp,
 }
 }
@@ -216,7 +221,10 @@ def moderate_avatar_endpoint(request, profile):
                 'avatar_lock_staff_message': profile.avatar_lock_staff_message,
                 'avatar_lock_staff_message': profile.avatar_lock_staff_message,
             })
             })
         else:
         else:
-            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+            return Response(
+                serializer.errors,
+                status=status.HTTP_400_BAD_REQUEST,
+            )
     else:
     else:
         return Response({
         return Response({
             'is_avatar_locked': int(profile.is_avatar_locked),
             'is_avatar_locked': int(profile.is_avatar_locked),

+ 5 - 11
misago/users/api/userendpoints/changeemail.py

@@ -10,16 +10,10 @@ from misago.users.serializers import ChangeEmailSerializer
 
 
 
 
 def change_email_endpoint(request, pk=None):
 def change_email_endpoint(request, pk=None):
-    serializer = ChangeEmailSerializer(
-        data=request.data,
-        context={
-            'user': request.user
-        }
-    )
+    serializer = ChangeEmailSerializer(data=request.data, context={'user': request.user})
 
 
     if serializer.is_valid():
     if serializer.is_valid():
-        token = store_new_credential(
-            request, 'email', serializer.validated_data['new_email'])
+        token = store_new_credential(request, 'email', serializer.validated_data['new_email'])
 
 
         mail_subject = _("Confirm e-mail change on %(forum_name)s forums")
         mail_subject = _("Confirm e-mail change on %(forum_name)s forums")
         mail_subject = mail_subject % {'forum_name': settings.forum_name}
         mail_subject = mail_subject % {'forum_name': settings.forum_name}
@@ -27,9 +21,9 @@ def change_email_endpoint(request, pk=None):
         # swap address with new one so email is sent to new address
         # swap address with new one so email is sent to new address
         request.user.email = serializer.validated_data['new_email']
         request.user.email = serializer.validated_data['new_email']
 
 
-        mail_user(request, request.user, mail_subject,
-                  'misago/emails/change_email',
-                  {'token': token})
+        mail_user(
+            request, request.user, mail_subject, 'misago/emails/change_email', {'token': token}
+        )
 
 
         message = _("E-mail change confirmation link was sent to new address.")
         message = _("E-mail change confirmation link was sent to new address.")
         return Response({'detail': message})
         return Response({'detail': message})

+ 9 - 12
misago/users/api/userendpoints/changepassword.py

@@ -10,25 +10,22 @@ from misago.users.serializers import ChangePasswordSerializer
 
 
 
 
 def change_password_endpoint(request, pk=None):
 def change_password_endpoint(request, pk=None):
-    serializer = ChangePasswordSerializer(
-        data=request.data,
-        context={
-            'user': request.user
-        }
-    )
+    serializer = ChangePasswordSerializer(data=request.data, context={'user': request.user})
 
 
     if serializer.is_valid():
     if serializer.is_valid():
         token = store_new_credential(
         token = store_new_credential(
-            request, 'password', serializer.validated_data['new_password'])
+            request, 'password', serializer.validated_data['new_password']
+        )
 
 
         mail_subject = _("Confirm password change on %(forum_name)s forums")
         mail_subject = _("Confirm password change on %(forum_name)s forums")
         mail_subject = mail_subject % {'forum_name': settings.forum_name}
         mail_subject = mail_subject % {'forum_name': settings.forum_name}
 
 
-        mail_user(request, request.user, mail_subject,
-                  'misago/emails/change_password',
-                  {'token': token})
+        mail_user(
+            request, request.user, mail_subject, 'misago/emails/change_password', {'token': token}
+        )
 
 
-        return Response({'detail': _("Password change confirmation link "
-                                     "was sent to your address.")})
+        return Response({
+            'detail': _("Password change confirmation link was sent to your address.")
+        })
     else:
     else:
         return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
         return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

+ 8 - 15
misago/users/api/userendpoints/create.py

@@ -10,7 +10,6 @@ from misago.conf import settings
 from misago.core.mail import mail_user
 from misago.core.mail import mail_user
 from misago.users import captcha
 from misago.users import captcha
 from misago.users.forms.register import RegisterForm
 from misago.users.forms.register import RegisterForm
-from misago.users.serializers import AuthenticatedUserSerializer
 from misago.users.tokens import make_activation_token
 from misago.users.tokens import make_activation_token
 
 
 
 
@@ -34,13 +33,9 @@ def create_endpoint(request):
 
 
     activation_kwargs = {}
     activation_kwargs = {}
     if settings.account_activation == 'user':
     if settings.account_activation == 'user':
-        activation_kwargs = {
-            'requires_activation': UserModel.ACTIVATION_USER
-        }
+        activation_kwargs = {'requires_activation': UserModel.ACTIVATION_USER}
     elif settings.account_activation == 'admin':
     elif settings.account_activation == 'admin':
-        activation_kwargs = {
-            'requires_activation': UserModel.ACTIVATION_ADMIN
-        }
+        activation_kwargs = {'requires_activation': UserModel.ACTIVATION_ADMIN}
 
 
     new_user = UserModel.objects.create_user(
     new_user = UserModel.objects.create_user(
         form.cleaned_data['username'],
         form.cleaned_data['username'],
@@ -56,12 +51,11 @@ def create_endpoint(request):
 
 
     if settings.account_activation == 'none':
     if settings.account_activation == 'none':
         authenticated_user = authenticate(
         authenticated_user = authenticate(
-            username=new_user.email,
-            password=form.cleaned_data['password'])
+            username=new_user.email, password=form.cleaned_data['password']
+        )
         login(request, authenticated_user)
         login(request, authenticated_user)
 
 
-        mail_user(request, new_user, mail_subject,
-                  'misago/emails/register/complete')
+        mail_user(request, new_user, mail_subject, 'misago/emails/register/complete')
 
 
         return Response({
         return Response({
             'activation': 'active',
             'activation': 'active',
@@ -75,13 +69,12 @@ def create_endpoint(request):
         activation_by_user = new_user.requires_activation_by_user
         activation_by_user = new_user.requires_activation_by_user
 
 
         mail_user(
         mail_user(
-            request, new_user, mail_subject,
-            'misago/emails/register/inactive',
-            {
+            request, new_user, mail_subject, 'misago/emails/register/inactive', {
                 'activation_token': activation_token,
                 'activation_token': activation_token,
                 'activation_by_admin': activation_by_admin,
                 'activation_by_admin': activation_by_admin,
                 'activation_by_user': activation_by_user,
                 'activation_by_user': activation_by_user,
-            })
+            }
+        )
 
 
         if activation_by_admin:
         if activation_by_admin:
             activation_method = 'admin'
             activation_method = 'admin'

+ 2 - 10
misago/users/api/userendpoints/list.py

@@ -1,18 +1,10 @@
-from datetime import timedelta
-
 from rest_framework.response import Response
 from rest_framework.response import Response
 
 
 from django.contrib.auth import get_user_model
 from django.contrib.auth import get_user_model
-from django.db.models import Count
-from django.http import Http404
 from django.shortcuts import get_object_or_404
 from django.shortcuts import get_object_or_404
-from django.utils import timezone
 
 
-from misago.conf import settings
-from misago.core.cache import cache
-from misago.core.shortcuts import get_int_or_404, paginate
+from misago.core.shortcuts import get_int_or_404
 from misago.users.models import Rank
 from misago.users.models import Rank
-from misago.users.online.utils import make_users_status_aware
 from misago.users.serializers import UserCardSerializer
 from misago.users.serializers import UserCardSerializer
 from misago.users.viewmodels import ActivePosters, RankUsers
 from misago.users.viewmodels import ActivePosters, RankUsers
 
 
@@ -31,7 +23,7 @@ def rank_users(request):
 
 
     page = get_int_or_404(request.GET.get('page', 0))
     page = get_int_or_404(request.GET.get('page', 0))
     if page == 1:
     if page == 1:
-        page = 0 # api allows explicit first page
+        page = 0  # api allows explicit first page
 
 
     users = RankUsers(request, rank, page)
     users = RankUsers(request, rank, page)
     return Response(users.get_frontend_context())
     return Response(users.get_frontend_context())

+ 11 - 15
misago/users/api/userendpoints/signature.py

@@ -6,29 +6,27 @@ from django.utils.translation import ugettext as _
 
 
 from misago.conf import settings
 from misago.conf import settings
 from misago.core.utils import format_plaintext_for_html
 from misago.core.utils import format_plaintext_for_html
-from misago.users.signatures import is_user_signature_valid, set_user_signature
 from misago.users.serializers import EditSignatureSerializer
 from misago.users.serializers import EditSignatureSerializer
+from misago.users.signatures import is_user_signature_valid, set_user_signature
 
 
 
 
 def signature_endpoint(request):
 def signature_endpoint(request):
     user = request.user
     user = request.user
 
 
     if not user.acl_cache['can_have_signature']:
     if not user.acl_cache['can_have_signature']:
-        raise PermissionDenied(
-            _("You don't have permission to change signature."))
+        raise PermissionDenied(_("You don't have permission to change signature."))
 
 
     if user.is_signature_locked:
     if user.is_signature_locked:
         if user.signature_lock_user_message:
         if user.signature_lock_user_message:
-            reason = format_plaintext_for_html(
-                user.signature_lock_user_message)
+            reason = format_plaintext_for_html(user.signature_lock_user_message)
         else:
         else:
             reason = None
             reason = None
 
 
         return Response({
         return Response({
-                'detail': _("Your signature is locked. You can't change it."),
-                'reason': reason
-            },
-            status=status.HTTP_403_FORBIDDEN)
+            'detail': _("Your signature is locked. You can't change it."),
+            'reason': reason
+        },
+                        status=status.HTTP_403_FORBIDDEN)
 
 
     if request.method == 'POST':
     if request.method == 'POST':
         return edit_signature(request, user)
         return edit_signature(request, user)
@@ -57,13 +55,11 @@ def get_signature_options(user):
 def edit_signature(request, user):
 def edit_signature(request, user):
     serializer = EditSignatureSerializer(user, data=request.data)
     serializer = EditSignatureSerializer(user, data=request.data)
     if serializer.is_valid():
     if serializer.is_valid():
-        set_user_signature(
-                request, user, serializer.validated_data['signature'])
-        user.save(update_fields=[
-            'signature', 'signature_parsed', 'signature_checksum'
-        ])
+        set_user_signature(request, user, serializer.validated_data['signature'])
+        user.save(update_fields=['signature', 'signature_parsed', 'signature_checksum'])
         return get_signature_options(user)
         return get_signature_options(user)
     else:
     else:
         return Response({
         return Response({
             'detail': serializer.errors['non_field_errors'][0]
             'detail': serializer.errors['non_field_errors'][0]
-        }, status=status.HTTP_400_BAD_REQUEST)
+        },
+                        status=status.HTTP_400_BAD_REQUEST)

+ 11 - 20
misago/users/api/userendpoints/username.py

@@ -5,8 +5,8 @@ from django.db import IntegrityError
 from django.utils.translation import ugettext as _
 from django.utils.translation import ugettext as _
 
 
 from misago.conf import settings
 from misago.conf import settings
-from misago.users.serializers import ChangeUsernameSerializer
 from misago.users.namechanges import UsernameChanges
 from misago.users.namechanges import UsernameChanges
+from misago.users.serializers import ChangeUsernameSerializer
 
 
 
 
 def username_endpoint(request):
 def username_endpoint(request):
@@ -39,14 +39,9 @@ def change_username(request):
             'detail': _("You can't change your username now."),
             'detail': _("You can't change your username now."),
             'options': options
             'options': options
         },
         },
-        status=status.HTTP_400_BAD_REQUEST)
+                        status=status.HTTP_400_BAD_REQUEST)
 
 
-    serializer = ChangeUsernameSerializer(
-        data=request.data,
-        context={
-            'user': request.user
-        }
-    )
+    serializer = ChangeUsernameSerializer(data=request.data, context={'user': request.user})
 
 
     if serializer.is_valid():
     if serializer.is_valid():
         try:
         try:
@@ -59,23 +54,18 @@ def change_username(request):
         except IntegrityError:
         except IntegrityError:
             return Response({
             return Response({
                 'detail': _("Error changing username. Please try again."),
                 'detail': _("Error changing username. Please try again."),
-                'options': options
             },
             },
-            status=status.HTTP_400_BAD_REQUEST)
+                            status=status.HTTP_400_BAD_REQUEST)
     else:
     else:
         return Response({
         return Response({
             'detail': serializer.errors['non_field_errors'][0]
             'detail': serializer.errors['non_field_errors'][0]
-        }, status=status.HTTP_400_BAD_REQUEST)
+        },
+                        status=status.HTTP_400_BAD_REQUEST)
 
 
 
 
 def moderate_username_endpoint(request, profile):
 def moderate_username_endpoint(request, profile):
     if request.method == 'POST':
     if request.method == 'POST':
-        serializer = ChangeUsernameSerializer(
-            data=request.data,
-            context={
-                'user': profile
-            }
-        )
+        serializer = ChangeUsernameSerializer(data=request.data, context={'user': profile})
 
 
         if serializer.is_valid():
         if serializer.is_valid():
             try:
             try:
@@ -87,12 +77,13 @@ def moderate_username_endpoint(request, profile):
             except IntegrityError:
             except IntegrityError:
                 return Response({
                 return Response({
                     'detail': _("Error changing username. Please try again."),
                     'detail': _("Error changing username. Please try again."),
-                    'options': options
-                }, status=status.HTTP_400_BAD_REQUEST)
+                },
+                                status=status.HTTP_400_BAD_REQUEST)
         else:
         else:
             return Response({
             return Response({
                 'detail': serializer.errors['non_field_errors'][0]
                 'detail': serializer.errors['non_field_errors'][0]
-            }, status=status.HTTP_400_BAD_REQUEST)
+            },
+                            status=status.HTTP_400_BAD_REQUEST)
     else:
     else:
         return Response({
         return Response({
             'length_min': settings.username_length_min,
             'length_min': settings.username_length_min,

+ 9 - 11
misago/users/api/usernamechanges.py

@@ -1,4 +1,4 @@
-from rest_framework import mixins, status, viewsets
+from rest_framework import viewsets
 from rest_framework.response import Response
 from rest_framework.response import Response
 
 
 from django.contrib.auth import get_user_model
 from django.contrib.auth import get_user_model
@@ -27,13 +27,12 @@ class UsernameChangesViewSetPermission(BasePermission):
         if user_pk == request.user.pk:
         if user_pk == request.user.pk:
             return True
             return True
         elif not request.user.acl_cache.get('can_see_users_name_history'):
         elif not request.user.acl_cache.get('can_see_users_name_history'):
-            raise PermissionDenied(
-                _("You don't have permission to see other users name history."))
+            raise PermissionDenied(_("You don't have permission to see other users name history."))
         return True
         return True
 
 
 
 
 class UsernameChangesViewSet(viewsets.GenericViewSet):
 class UsernameChangesViewSet(viewsets.GenericViewSet):
-    permission_classes = (UsernameChangesViewSetPermission,)
+    permission_classes = (UsernameChangesViewSetPermission, )
     serializer_class = UsernameChangeSerializer
     serializer_class = UsernameChangeSerializer
 
 
     def get_queryset(self):
     def get_queryset(self):
@@ -41,16 +40,15 @@ class UsernameChangesViewSet(viewsets.GenericViewSet):
 
 
         if self.request.query_params.get('user'):
         if self.request.query_params.get('user'):
             user_pk = get_int_or_404(self.request.query_params.get('user'))
             user_pk = get_int_or_404(self.request.query_params.get('user'))
-            queryset = get_object_or_404(
-                UserModel.objects, pk=user_pk).namechanges
+            queryset = get_object_or_404(UserModel.objects, pk=user_pk).namechanges
 
 
         if self.request.query_params.get('search'):
         if self.request.query_params.get('search'):
             search_phrase = self.request.query_params.get('search').strip()
             search_phrase = self.request.query_params.get('search').strip()
             if search_phrase:
             if search_phrase:
                 queryset = queryset.filter(
                 queryset = queryset.filter(
-                    Q(changed_by_username__istartswith=search_phrase) |
-                    Q(new_username__istartswith=search_phrase) |
-                    Q(old_username__istartswith=search_phrase)
+                    Q(changed_by_username__istartswith=search_phrase) | Q(
+                        new_username__istartswith=search_phrase
+                    ) | Q(old_username__istartswith=search_phrase)
                 )
                 )
 
 
         return queryset.select_related('user', 'changed_by').order_by('-id')
         return queryset.select_related('user', 'changed_by').order_by('-id')
@@ -58,7 +56,7 @@ class UsernameChangesViewSet(viewsets.GenericViewSet):
     def list(self, request):
     def list(self, request):
         page = get_int_or_404(request.GET.get('page', 0))
         page = get_int_or_404(request.GET.get('page', 0))
         if page == 1:
         if page == 1:
-            page = 0 # api allows explicit first page
+            page = 0  # api allows explicit first page
 
 
         queryset = self.get_queryset()
         queryset = self.get_queryset()
 
 
@@ -66,7 +64,7 @@ class UsernameChangesViewSet(viewsets.GenericViewSet):
 
 
         data = pagination_dict(list_page)
         data = pagination_dict(list_page)
         data.update({
         data.update({
-            'results': UsernameChangeSerializer(list_page.object_list, many=True).data
+            'results': UsernameChangeSerializer(list_page.object_list, many=True).data,
         })
         })
 
 
         return Response(data)
         return Response(data)

+ 35 - 22
misago/users/api/users.py

@@ -1,9 +1,8 @@
-from rest_framework import mixins, status, viewsets
-from rest_framework.decorators import detail_route, list_route
+from rest_framework import status, viewsets
+from rest_framework.decorators import detail_route
 from rest_framework.parsers import FormParser, JSONParser, MultiPartParser
 from rest_framework.parsers import FormParser, JSONParser, MultiPartParser
 from rest_framework.response import Response
 from rest_framework.response import Response
 
 
-from django.conf import settings
 from django.contrib.auth import get_user_model
 from django.contrib.auth import get_user_model
 from django.core.exceptions import PermissionDenied
 from django.core.exceptions import PermissionDenied
 from django.db import transaction
 from django.db import transaction
@@ -14,7 +13,6 @@ from django.utils.translation import ugettext as _
 
 
 from misago.acl import add_acl
 from misago.acl import add_acl
 from misago.categories.models import Category
 from misago.categories.models import Category
-from misago.core.cache import cache
 from misago.core.rest_permissions import IsAuthenticatedOrReadOnly
 from misago.core.rest_permissions import IsAuthenticatedOrReadOnly
 from misago.core.shortcuts import get_int_or_404
 from misago.core.shortcuts import get_int_or_404
 from misago.threads.moderation import hide_post, hide_thread
 from misago.threads.moderation import hide_post, hide_thread
@@ -56,8 +54,8 @@ def allow_self_only(user, pk, message):
 
 
 
 
 class UserViewSet(viewsets.GenericViewSet):
 class UserViewSet(viewsets.GenericViewSet):
-    permission_classes = (UserViewSetPermission,)
-    parser_classes=(FormParser, JSONParser, MultiPartParser)
+    permission_classes = (UserViewSetPermission, )
+    parser_classes = (FormParser, JSONParser, MultiPartParser)
     queryset = UserModel.objects
     queryset = UserModel.objects
 
 
     def get_queryset(self):
     def get_queryset(self):
@@ -106,9 +104,7 @@ class UserViewSet(viewsets.GenericViewSet):
         serializer = ForumOptionsSerializer(request.user, data=request.data)
         serializer = ForumOptionsSerializer(request.user, data=request.data)
         if serializer.is_valid():
         if serializer.is_valid():
             serializer.save()
             serializer.save()
-            return Response({
-                'detail': _("Your forum options have been changed.")
-            })
+            return Response({'detail': _("Your forum options have been changed.")})
         else:
         else:
             return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
             return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
 
 
@@ -166,10 +162,7 @@ class UserViewSet(viewsets.GenericViewSet):
             profile.save(update_fields=['followers'])
             profile.save(update_fields=['followers'])
             request.user.save(update_fields=['following'])
             request.user.save(update_fields=['following'])
 
 
-            return Response({
-                'is_followed': followed,
-                'followers': profile_followers
-            })
+            return Response({'is_followed': followed, 'followers': profile_followers})
 
 
     @detail_route()
     @detail_route()
     def ban(self, request, pk=None):
     def ban(self, request, pk=None):
@@ -215,7 +208,9 @@ class UserViewSet(viewsets.GenericViewSet):
                         categories_to_sync.add(thread.category_id)
                         categories_to_sync.add(thread.category_id)
                         hide_thread(request, thread)
                         hide_thread(request, thread)
 
 
-                    posts = profile.post_set.select_related('category', 'thread', 'thread__category')
+                    posts = profile.post_set.select_related(
+                        'category', 'thread', 'thread__category'
+                    )
                     for post in posts.filter(is_hidden=False).iterator():
                     for post in posts.filter(is_hidden=False).iterator():
                         categories_to_sync.add(post.category_id)
                         categories_to_sync.add(post.category_id)
                         hide_post(request.user, post)
                         hide_post(request.user, post)
@@ -237,7 +232,7 @@ class UserViewSet(viewsets.GenericViewSet):
 
 
         page = get_int_or_404(request.query_params.get('page', 0))
         page = get_int_or_404(request.query_params.get('page', 0))
         if page == 1:
         if page == 1:
-            page = 0 # api allows explicit first page
+            page = 0  # api allows explicit first page
 
 
         search = request.query_params.get('search')
         search = request.query_params.get('search')
 
 
@@ -251,7 +246,7 @@ class UserViewSet(viewsets.GenericViewSet):
 
 
         page = get_int_or_404(request.query_params.get('page', 0))
         page = get_int_or_404(request.query_params.get('page', 0))
         if page == 1:
         if page == 1:
-            page = 0 # api allows explicit first page
+            page = 0  # api allows explicit first page
 
 
         search = request.query_params.get('search')
         search = request.query_params.get('search')
 
 
@@ -265,7 +260,7 @@ class UserViewSet(viewsets.GenericViewSet):
 
 
         page = get_int_or_404(request.query_params.get('page', 0))
         page = get_int_or_404(request.query_params.get('page', 0))
         if page == 1:
         if page == 1:
-            page = 0 # api allows explicit first page
+            page = 0  # api allows explicit first page
 
 
         feed = UserThreads(request, profile, page)
         feed = UserThreads(request, profile, page)
 
 
@@ -277,7 +272,7 @@ class UserViewSet(viewsets.GenericViewSet):
 
 
         page = get_int_or_404(request.query_params.get('page', 0))
         page = get_int_or_404(request.query_params.get('page', 0))
         if page == 1:
         if page == 1:
-            page = 0 # api allows explicit first page
+            page = 0  # api allows explicit first page
 
 
         feed = UserPosts(request, profile, page)
         feed = UserPosts(request, profile, page)
 
 
@@ -285,7 +280,25 @@ class UserViewSet(viewsets.GenericViewSet):
 
 
 
 
 UserProfileSerializer = UserSerializer.subset_fields(
 UserProfileSerializer = UserSerializer.subset_fields(
-    'id', 'username', 'slug', 'email', 'joined_on', 'rank', 'title', 'avatars',
-    'is_avatar_locked', 'signature', 'is_signature_locked', 'followers', 'following',
-    'threads', 'posts', 'acl', 'is_followed', 'is_blocked', 'status', 'absolute_url',
-    'api_url')
+    'id',
+    'username',
+    'slug',
+    'email',
+    'joined_on',
+    'rank',
+    'title',
+    'avatars',
+    'is_avatar_locked',
+    'signature',
+    'is_signature_locked',
+    'followers',
+    'following',
+    'threads',
+    'posts',
+    'acl',
+    'is_followed',
+    'is_blocked',
+    'status',
+    'absolute_url',
+    'api_url',
+)

+ 3 - 5
misago/users/apps.py

@@ -10,16 +10,13 @@ class MisagoUsersConfig(AppConfig):
     verbose_name = "Misago Auth"
     verbose_name = "Misago Auth"
 
 
     def ready(self):
     def ready(self):
-        from . import signals
+        from . import signals as _
 
 
         self.register_default_usercp_pages()
         self.register_default_usercp_pages()
         self.register_default_users_list_pages()
         self.register_default_users_list_pages()
         self.register_default_user_profile_pages()
         self.register_default_user_profile_pages()
 
 
     def register_default_usercp_pages(self):
     def register_default_usercp_pages(self):
-        def show_signature_cp(request):
-            return request.user.acl_cache['can_have_signature']
-
         usercp.add_section(
         usercp.add_section(
             link='misago:usercp-change-forum-options',
             link='misago:usercp-change-forum-options',
             name=_('Forum options'),
             name=_('Forum options'),
@@ -43,7 +40,8 @@ class MisagoUsersConfig(AppConfig):
         users_list.add_section(
         users_list.add_section(
             link='misago:users-active-posters',
             link='misago:users-active-posters',
             component='active-posters',
             component='active-posters',
-            name=_('Active posters'))
+            name=_('Active posters')
+        )
 
 
     def register_default_user_profile_pages(self):
     def register_default_user_profile_pages(self):
         def can_see_names_history(request, profile):
         def can_see_names_history(request, profile):

+ 1 - 3
misago/users/avatars/__init__.py

@@ -2,14 +2,12 @@ from misago.conf import settings
 
 
 from . import store, gravatar, dynamic, gallery, uploaded
 from . import store, gravatar, dynamic, gallery, uploaded
 
 
-
 AVATAR_TYPES = ('gravatar', 'dynamic', 'gallery', 'uploaded')
 AVATAR_TYPES = ('gravatar', 'dynamic', 'gallery', 'uploaded')
 
 
-
 SET_DEFAULT_AVATAR = {
 SET_DEFAULT_AVATAR = {
     'gravatar': gravatar.set_avatar,
     'gravatar': gravatar.set_avatar,
     'dynamic': dynamic.set_avatar,
     'dynamic': dynamic.set_avatar,
-    'gallery': gallery.set_random_avatar
+    'gallery': gallery.set_random_avatar,
 }
 }
 
 
 
 

+ 12 - 31
misago/users/avatars/dynamic.py

@@ -1,14 +1,22 @@
-import math
 import os
 import os
 from importlib import import_module
 from importlib import import_module
 
 
-from PIL import Image, ImageColor, ImageDraw, ImageFilter, ImageFont
+from PIL import Image, ImageColor, ImageDraw, ImageFont
 
 
 from misago.conf import settings
 from misago.conf import settings
 
 
 from . import store
 from . import store
 
 
 
 
+COLOR_WHEEL = (
+    '#d32f2f', '#c2185b', '#7b1fa2', '#512da8', '#303f9f', '#1976d2', '#0288D1', '#0288d1',
+    '#0097a7', '#00796b', '#388e3c', '#689f38', '#afb42b', '#fbc02d', '#ffa000', '#f57c00',
+    '#e64a19',
+)
+COLOR_WHEEL_LEN = len(COLOR_WHEEL)
+FONT_FILE = os.path.join(os.path.dirname(__file__), 'font.ttf')
+
+
 def set_avatar(user):
 def set_avatar(user):
     name_bits = settings.MISAGO_DYNAMIC_AVATAR_DRAWER.split('.')
     name_bits = settings.MISAGO_DYNAMIC_AVATAR_DRAWER.split('.')
 
 
@@ -20,10 +28,8 @@ def set_avatar(user):
     store.store_new_avatar(user, image)
     store.store_new_avatar(user, image)
 
 
 
 
-"""
-Default drawer
-"""
 def draw_default(user):
 def draw_default(user):
+    """default avatar drawer that draws username's first letter on color"""
     image_size = max(settings.MISAGO_AVATARS_SIZES)
     image_size = max(settings.MISAGO_AVATARS_SIZES)
 
 
     image = Image.new("RGBA", (image_size, image_size), 0)
     image = Image.new("RGBA", (image_size, image_size), 0)
@@ -33,14 +39,6 @@ def draw_default(user):
     return image
     return image
 
 
 
 
-COLOR_WHEEL = ('#d32f2f', '#c2185b', '#7b1fa2', '#512da8',
-               '#303f9f', '#1976d2', '#0288D1', '#0288d1',
-               '#0097a7', '#00796b', '#388e3c', '#689f38',
-               '#afb42b', '#fbc02d', '#ffa000', '#f57c00',
-               '#e64a19')
-COLOR_WHEEL_LEN = len(COLOR_WHEEL)
-
-
 def draw_avatar_bg(user, image):
 def draw_avatar_bg(user, image):
     image_size = image.size
     image_size = image.size
 
 
@@ -55,9 +53,6 @@ def draw_avatar_bg(user, image):
     return image
     return image
 
 
 
 
-FONT_FILE = os.path.join(os.path.dirname(__file__), 'font.ttf')
-
-
 def draw_avatar_flavour(user, image):
 def draw_avatar_flavour(user, image):
     string = user.username[0]
     string = user.username[0]
 
 
@@ -67,23 +62,9 @@ def draw_avatar_flavour(user, image):
     font = ImageFont.truetype(FONT_FILE, size=size)
     font = ImageFont.truetype(FONT_FILE, size=size)
 
 
     text_size = font.getsize(string)
     text_size = font.getsize(string)
-    text_pos = ((image_size - text_size[0]) / 2,
-                (image_size - text_size[1]) / 2)
+    text_pos = ((image_size - text_size[0]) / 2, (image_size - text_size[1]) / 2, )
 
 
     writer = ImageDraw.Draw(image)
     writer = ImageDraw.Draw(image)
     writer.text(text_pos, string, font=font)
     writer.text(text_pos, string, font=font)
 
 
     return image
     return image
-
-
-"""
-Some utils for drawring avatar programmatically
-"""
-CHARS = 'qwertyuiopasdfghjklzxcvbnm1234567890'
-
-
-def string_to_int(string):
-    value = 0
-    for p, c in enumerate(string.lower()):
-        value += p * (CHARS.find(c))
-    return value

+ 5 - 8
misago/users/avatars/gallery.py

@@ -30,10 +30,7 @@ def get_available_galleries(include_default=False):
             continue
             continue
 
 
         if image.gallery not in galleries_dicts:
         if image.gallery not in galleries_dicts:
-            galleries_dicts[image.gallery] = {
-                'name': image.gallery,
-                'images': []
-            }
+            galleries_dicts[image.gallery] = {'name': image.gallery, 'images': []}
 
 
             galleries.append(galleries_dicts[image.gallery])
             galleries.append(galleries_dicts[image.gallery])
 
 
@@ -61,10 +58,10 @@ def load_avatar_galleries():
 
 
         for image in images:
         for image in images:
             with open(image, 'rb') as image_file:
             with open(image, 'rb') as image_file:
-                galleries.append(AvatarGallery.objects.create(
-                    gallery=gallery_name,
-                    image=ContentFile(image_file.read(), 'image')
-                ))
+                galleries.append(
+                    AvatarGallery.objects.
+                    create(gallery=gallery_name, image=ContentFile(image_file.read(), 'image'))
+                )
     return galleries
     return galleries
 
 
 
 

+ 8 - 7
misago/users/avatars/store.py

@@ -39,11 +39,13 @@ def store_avatar(user, image):
         image = image.resize((size, size), Image.ANTIALIAS)
         image = image.resize((size, size), Image.ANTIALIAS)
         image.save(image_stream, "PNG")
         image.save(image_stream, "PNG")
 
 
-        avatars.append(Avatar.objects.create(
-            user=user,
-            size=size,
-            image=ContentFile(image_stream.getvalue(), 'avatar')
-        ))
+        avatars.append(
+            Avatar.objects.create(
+                user=user,
+                size=size,
+                image=ContentFile(image_stream.getvalue(), 'avatar'),
+            )
+        )
 
 
     user.avatars = [{'size': a.size, 'url': a.url} for a in avatars]
     user.avatars = [{'size': a.size, 'url': a.url} for a in avatars]
     user.save(update_fields=['avatars'])
     user.save(update_fields=['avatars'])
@@ -80,5 +82,4 @@ def upload_to(instance, filename):
     secret = get_random_string(32)
     secret = get_random_string(32)
     filename_clean = '%s.png' % get_random_string(32)
     filename_clean = '%s.png' % get_random_string(32)
 
 
-    return os.path.join(
-        'avatars', spread_path[:2], spread_path[2:4], secret, filename_clean)
+    return os.path.join('avatars', spread_path[:2], spread_path[2:4], secret, filename_clean)

+ 2 - 8
misago/users/avatars/uploaded.py

@@ -1,6 +1,3 @@
-from hashlib import sha256
-from math import floor
-
 from path import Path
 from path import Path
 from PIL import Image
 from PIL import Image
 
 
@@ -41,8 +38,7 @@ def validate_dimensions(uploaded_file):
 
 
     min_size = max(settings.MISAGO_AVATARS_SIZES)
     min_size = max(settings.MISAGO_AVATARS_SIZES)
     if min(image.size) < min_size:
     if min(image.size) < min_size:
-        message = _("Uploaded image should be at "
-                    "least %(size)s pixels tall and wide.")
+        message = _("Uploaded image should be at least %(size)s pixels tall and wide.")
         raise ValidationError(message % {'size': min_size})
         raise ValidationError(message % {'size': min_size})
 
 
     if image.size[0] * image.size[1] > 2000 * 3000:
     if image.size[0] * image.size[1] > 2000 * 3000:
@@ -85,7 +81,6 @@ def clean_crop(image, crop):
         crop_dict = {
         crop_dict = {
             'x': float(crop['offset']['x']),
             'x': float(crop['offset']['x']),
             'y': float(crop['offset']['y']),
             'y': float(crop['offset']['y']),
-
             'zoom': float(crop['zoom']),
             'zoom': float(crop['zoom']),
         }
         }
     except (KeyError, TypeError, ValueError):
     except (KeyError, TypeError, ValueError):
@@ -134,8 +129,7 @@ def crop_source_image(user, source, crop):
     else:
     else:
         upscale = 1.0 / crop['zoom']
         upscale = 1.0 / crop['zoom']
         cropped_image = image.crop((
         cropped_image = image.crop((
-            int(round(crop['x'] * upscale * -1, 0)),
-            int(round(crop['y'] * upscale * -1, 0)),
+            int(round(crop['x'] * upscale * -1, 0)), int(round(crop['y'] * upscale * -1, 0)),
             int(round((crop['x'] - min_size) * upscale * -1, 0)),
             int(round((crop['x'] - min_size) * upscale * -1, 0)),
             int(round((crop['y'] - min_size) * upscale * -1, 0)),
             int(round((crop['y'] - min_size) * upscale * -1, 0)),
         ))
         ))

+ 11 - 25
misago/users/bans.py

@@ -1,4 +1,3 @@
-
 """
 """
 API for checking values for bans
 API for checking values for bans
 
 
@@ -67,10 +66,7 @@ def _set_user_ban_cache(user):
     ban_cache.bans_version = cachebuster.get_version(VERSION_KEY)
     ban_cache.bans_version = cachebuster.get_version(VERSION_KEY)
 
 
     try:
     try:
-        user_ban = Ban.objects.get_ban(
-            username=user.username,
-            email=user.email
-        )
+        user_ban = Ban.objects.get_ban(username=user.username, email=user.email)
 
 
         ban_cache.ban = user_ban
         ban_cache.ban = user_ban
         ban_cache.expires_on = user_ban.expires_on
         ban_cache.expires_on = user_ban.expires_on
@@ -86,13 +82,13 @@ def _set_user_ban_cache(user):
     return ban_cache
     return ban_cache
 
 
 
 
-"""
-Utility for checking if request came from banned IP
-
-This check may be performed frequently, which is why there is extra
-boilerplate that caches ban check result in session
-"""
 def get_request_ip_ban(request):
 def get_request_ip_ban(request):
+    """
+    Utility for checking if request came from banned IP
+
+    This check may be performed frequently, which is why there is extra
+    boilerplate that caches ban check result in session
+    """
     session_ban_cache = _get_session_bancache(request)
     session_ban_cache = _get_session_bancache(request)
     if session_ban_cache:
     if session_ban_cache:
         if session_ban_cache['is_banned']:
         if session_ban_cache['is_banned']:
@@ -113,10 +109,7 @@ def get_request_ip_ban(request):
         else:
         else:
             ban_cache['expires_on'] = None
             ban_cache['expires_on'] = None
 
 
-        ban_cache.update({
-                'is_banned': True,
-                'message': found_ban.user_message
-            })
+        ban_cache.update({'is_banned': True, 'message': found_ban.user_message})
         request.session[CACHE_SESSION_KEY] = ban_cache
         request.session[CACHE_SESSION_KEY] = ban_cache
         return _hydrate_session_cache(request.session[CACHE_SESSION_KEY])
         return _hydrate_session_cache(request.session[CACHE_SESSION_KEY])
     else:
     else:
@@ -134,9 +127,6 @@ def _get_session_bancache(request):
         if not cachebuster.is_valid(VERSION_KEY, ban_cache['version']):
         if not cachebuster.is_valid(VERSION_KEY, ban_cache['version']):
             return None
             return None
         if ban_cache.get('expires_on'):
         if ban_cache.get('expires_on'):
-            """
-            Hydrate ban date
-            """
             if ban_cache['expires_on'] < timezone.today():
             if ban_cache['expires_on'] < timezone.today():
                 return None
                 return None
         return ban_cache
         return ban_cache
@@ -153,11 +143,8 @@ def _hydrate_session_cache(ban_cache):
     return hydrated
     return hydrated
 
 
 
 
-"""
-Utilities for front-end based bans
-"""
-def ban_user(user, user_message=None, staff_message=None, length=None,
-             expires_on=None):
+# Utilities for front-end based bans
+def ban_user(user, user_message=None, staff_message=None, length=None, expires_on=None):
     if not expires_on and length:
     if not expires_on and length:
         expires_on = timezone.now() + timedelta(**length)
         expires_on = timezone.now() + timedelta(**length)
 
 
@@ -171,8 +158,7 @@ def ban_user(user, user_message=None, staff_message=None, length=None,
     return ban
     return ban
 
 
 
 
-def ban_ip(ip, user_message=None, staff_message=None, length=None,
-           expires_on=None):
+def ban_ip(ip, user_message=None, staff_message=None, length=None, expires_on=None):
     if not expires_on and length:
     if not expires_on and length:
         expires_on = timezone.now() + timedelta(**length)
         expires_on = timezone.now() + timedelta(**length)
 
 

+ 10 - 6
misago/users/captcha.py

@@ -7,11 +7,14 @@ from misago.conf import settings
 
 
 
 
 def recaptcha_test(request):
 def recaptcha_test(request):
-    r = requests.post('https://www.google.com/recaptcha/api/siteverify', data={
-        'secret': settings.recaptcha_secret_key,
-        'response': request.data.get('captcha'),
-        'remoteip': request.user_ip
-    })
+    r = requests.post(
+        'https://www.google.com/recaptcha/api/siteverify',
+        data={
+            'secret': settings.recaptcha_secret_key,
+            'response': request.data.get('captcha'),
+            'remoteip': request.user_ip
+        }
+    )
 
 
     if r.status_code == 200:
     if r.status_code == 200:
         response_json = r.json()
         response_json = r.json()
@@ -32,7 +35,7 @@ def qacaptcha_test(request):
 
 
 
 
 def nocaptcha_test(request):
 def nocaptcha_test(request):
-    return # no captcha means no validation
+    return  # no captcha means no validation
 
 
 
 
 CAPTCHA_TESTS = {
 CAPTCHA_TESTS = {
@@ -41,5 +44,6 @@ CAPTCHA_TESTS = {
     'no': nocaptcha_test,
     'no': nocaptcha_test,
 }
 }
 
 
+
 def test_request(request):
 def test_request(request):
     CAPTCHA_TESTS[settings.captcha_type](request)
     CAPTCHA_TESTS[settings.captcha_type](request)

+ 2 - 10
misago/users/context_processors.py

@@ -9,16 +9,12 @@ def user_links(request):
         request.frontend_context.update({
         request.frontend_context.update({
             'REQUEST_ACTIVATION_URL': reverse('misago:request-activation'),
             'REQUEST_ACTIVATION_URL': reverse('misago:request-activation'),
             'FORGOTTEN_PASSWORD_URL': reverse('misago:forgotten-password'),
             'FORGOTTEN_PASSWORD_URL': reverse('misago:forgotten-password'),
-
             'BANNED_URL': reverse('misago:banned'),
             'BANNED_URL': reverse('misago:banned'),
-
             'USERCP_URL': reverse('misago:options'),
             'USERCP_URL': reverse('misago:options'),
             'USERS_LIST_URL': reverse('misago:users'),
             'USERS_LIST_URL': reverse('misago:users'),
-
             'AUTH_API': reverse('misago:api:auth'),
             'AUTH_API': reverse('misago:api:auth'),
             'AUTH_CRITERIA_API': reverse('misago:api:auth-criteria'),
             'AUTH_CRITERIA_API': reverse('misago:api:auth-criteria'),
             'USERS_API': reverse('misago:api:user-list'),
             'USERS_API': reverse('misago:api:user-list'),
-
             'CAPTCHA_API': reverse('misago:api:captcha-question'),
             'CAPTCHA_API': reverse('misago:api:captcha-question'),
             'USERNAME_CHANGES_API': reverse('misago:api:usernamechange-list'),
             'USERNAME_CHANGES_API': reverse('misago:api:usernamechange-list'),
         })
         })
@@ -39,12 +35,8 @@ def preload_user_json(request):
     })
     })
 
 
     if request.user.is_authenticated:
     if request.user.is_authenticated:
-        request.frontend_context.update({
-            'user': AuthenticatedUserSerializer(request.user).data
-        })
+        request.frontend_context.update({'user': AuthenticatedUserSerializer(request.user).data})
     else:
     else:
-        request.frontend_context.update({
-            'user': AnonymousUserSerializer(request.user).data
-        })
+        request.frontend_context.update({'user': AnonymousUserSerializer(request.user).data})
 
 
     return {}
     return {}

+ 3 - 8
misago/users/credentialchange.py

@@ -44,13 +44,8 @@ def read_new_credential(request, credential_type, link_token):
 
 
 def _make_change_token(user, token_type):
 def _make_change_token(user, token_type):
     seeds = (
     seeds = (
-        user.pk,
-        user.email,
-        user.password,
-        user.last_login.replace(microsecond=0, tzinfo=None),
-        settings.SECRET_KEY,
-        six.text_type(token_type)
+        user.pk, user.email, user.password, user.last_login.replace(microsecond=0, tzinfo=None),
+        settings.SECRET_KEY, six.text_type(token_type)
     )
     )
 
 
-    return sha256(
-        force_bytes('+'.join([six.text_type(s) for s in seeds]))).hexdigest()
+    return sha256(force_bytes('+'.join([six.text_type(s) for s in seeds]))).hexdigest()

+ 6 - 8
misago/users/decorators.py

@@ -1,5 +1,4 @@
 from django.core.exceptions import PermissionDenied
 from django.core.exceptions import PermissionDenied
-from django.shortcuts import redirect
 from django.utils.translation import gettext as _
 from django.utils.translation import gettext as _
 
 
 from misago.core.exceptions import Banned
 from misago.core.exceptions import Banned
@@ -11,20 +10,20 @@ from .models import Ban
 def deny_authenticated(f):
 def deny_authenticated(f):
     def decorator(request, *args, **kwargs):
     def decorator(request, *args, **kwargs):
         if request.user.is_authenticated:
         if request.user.is_authenticated:
-            raise PermissionDenied(
-                _("This page is not available to signed in users."))
+            raise PermissionDenied(_("This page is not available to signed in users."))
         else:
         else:
             return f(request, *args, **kwargs)
             return f(request, *args, **kwargs)
+
     return decorator
     return decorator
 
 
 
 
 def deny_guests(f):
 def deny_guests(f):
     def decorator(request, *args, **kwargs):
     def decorator(request, *args, **kwargs):
         if request.user.is_anonymous:
         if request.user.is_anonymous:
-            raise PermissionDenied(
-                _("You have to sign in to access this page."))
+            raise PermissionDenied(_("You have to sign in to access this page."))
         else:
         else:
             return f(request, *args, **kwargs)
             return f(request, *args, **kwargs)
+
     return decorator
     return decorator
 
 
 
 
@@ -33,11 +32,10 @@ def deny_banned_ips(f):
         ban = get_request_ip_ban(request)
         ban = get_request_ip_ban(request)
         if ban:
         if ban:
             hydrated_ban = Ban(
             hydrated_ban = Ban(
-                check_type=Ban.IP,
-                user_message=ban['message'],
-                expires_on=ban['expires_on']
+                check_type=Ban.IP, user_message=ban['message'], expires_on=ban['expires_on']
             )
             )
             raise Banned(hydrated_ban)
             raise Banned(hydrated_ban)
         else:
         else:
             return f(request, *args, **kwargs)
             return f(request, *args, **kwargs)
+
     return decorator
     return decorator

+ 23 - 51
misago/users/djangoadmin.py

@@ -30,33 +30,21 @@ class UserAdminForm(forms.ModelForm):
     it is kind of overkill - overwrite the whole template just to add one
     it is kind of overkill - overwrite the whole template just to add one
     button - isn't it?
     button - isn't it?
     """
     """
-    #: pseudo-field
     edit_from_misago_link = forms.Field()
     edit_from_misago_link = forms.Field()
 
 
     def __init__(self, *args, **kwargs):
     def __init__(self, *args, **kwargs):
-        # noinspection PyArgumentList
         super(UserAdminForm, self).__init__(*args, **kwargs)
         super(UserAdminForm, self).__init__(*args, **kwargs)
         self.init_edit_from_misago_link_field()
         self.init_edit_from_misago_link_field()
 
 
     def init_edit_from_misago_link_field(self):
     def init_edit_from_misago_link_field(self):
-        """
-        Init for the pseudo-field, and replace it's widget `render`.
-        """
+        """init for the pseudo-field, and replace it's widget `render`"""
         field = self.fields['edit_from_misago_link']
         field = self.fields['edit_from_misago_link']
         field.required = False
         field.required = False
         field.label = ''
         field.label = ''
         field.widget.render = self.render_edit_from_misago_link
         field.widget.render = self.render_edit_from_misago_link
 
 
-    # noinspection PyUnusedLocal
     def render_edit_from_misago_link(self, *args, **kwargs):
     def render_edit_from_misago_link(self, *args, **kwargs):
-        """
-        Composes an html hyperlink for the pseudo-field render.
-
-        `*args` and `**kwargs` arguments need to mimic widget `render()`
-        signature.
-
-        :rtype: str
-        """
+        """composes an html hyperlink for the pseudo-field render"""
         text = _('Edit this user in Misago admin panel')
         text = _('Edit this user in Misago admin panel')
         link_html_template = ('<a href="{}" target="blank">' + text + '</a>')
         link_html_template = ('<a href="{}" target="blank">' + text + '</a>')
         link_url = reverse(
         link_url = reverse(
@@ -79,48 +67,32 @@ class UserAdmin(admin.ModelAdmin):
     that).
     that).
     Replaces default form with custom `UserAdminForm`.
     Replaces default form with custom `UserAdminForm`.
     """
     """
-    list_display = (
-        'username',
-        'email',
-        'is_staff',
-        'is_superuser',
-    )
-    search_fields = ('username', 'email')
-    list_filter = ('groups', 'is_staff', 'is_superuser')
+    list_display = ['username', 'email', 'is_staff', 'is_superuser']
+    search_fields = ['username', 'email']
+    list_filter = ['groups', 'is_staff', 'is_superuser']
 
 
     form = UserAdminForm
     form = UserAdminForm
     actions = None
     actions = None
-    readonly_fields = (
-        'username',
-        'email',
-        'rank',
-        'last_login',
-        'joined_on',
-        'is_staff',
-        'is_superuser',
-    )
-    fieldsets = (
-        (
+    readonly_fields = [
+        'username', 'email', 'rank', 'last_login', 'joined_on', 'is_staff', 'is_superuser'
+    ]
+    fieldsets = [
+        [
             _('Misago user data'),
             _('Misago user data'),
-            {'fields': (
-                'username',
-                'email',
-                'rank',
-                'last_login',
-                'joined_on',
-                'is_staff',
-                'is_superuser',
-                'edit_from_misago_link',
-            )},
-        ),
-        (
+            {
+                'fields': (
+                    'username', 'email', 'rank', 'last_login', 'joined_on', 'is_staff',
+                    'is_superuser', 'edit_from_misago_link',
+                )
+            },
+        ],
+        [
             _('Edit permissions and groups'),
             _('Edit permissions and groups'),
-            {'fields': (
-                'groups',
-                'user_permissions',
-            )},
-        ),
-    )
+            {
+                'fields': ('groups', 'user_permissions', )
+            },
+        ],
+    ]
 
 
     def has_add_permission(self, request):
     def has_add_permission(self, request):
         return False
         return False

+ 46 - 87
misago/users/forms/admin.py

@@ -16,9 +16,6 @@ from misago.users.validators import validate_email, validate_username
 UserModel = get_user_model()
 UserModel = get_user_model()
 
 
 
 
-"""
-Users
-"""
 class UserBaseForm(forms.ModelForm):
 class UserBaseForm(forms.ModelForm):
     username = forms.CharField(label=_("Username"))
     username = forms.CharField(label=_("Username"))
     title = forms.CharField(label=_("Custom title"), required=False)
     title = forms.CharField(label=_("Custom title"), required=False)
@@ -58,10 +55,7 @@ class UserBaseForm(forms.ModelForm):
 
 
 
 
 class NewUserForm(UserBaseForm):
 class NewUserForm(UserBaseForm):
-    new_password = forms.CharField(
-        label=_("Password"),
-        widget=forms.PasswordInput
-    )
+    new_password = forms.CharField(label=_("Password"), widget=forms.PasswordInput)
 
 
     class Meta:
     class Meta:
         model = UserModel
         model = UserModel
@@ -90,8 +84,8 @@ class EditUserForm(UserBaseForm):
         "Turning this off is non-destructible way to remove user accounts."
         "Turning this off is non-destructible way to remove user accounts."
     )
     )
 
 
-    IS_ACTIVE_STAFF_MESSAGE_LABEL=_("Staff message")
-    IS_ACTIVE_STAFF_MESSAGE_HELP_TEXT=_(
+    IS_ACTIVE_STAFF_MESSAGE_LABEL = _("Staff message")
+    IS_ACTIVE_STAFF_MESSAGE_HELP_TEXT = _(
         "Optional message for forum team members explaining "
         "Optional message for forum team members explaining "
         "why user's account has been disabled."
         "why user's account has been disabled."
     )
     )
@@ -99,7 +93,7 @@ class EditUserForm(UserBaseForm):
     new_password = forms.CharField(
     new_password = forms.CharField(
         label=_("Change password to"),
         label=_("Change password to"),
         widget=forms.PasswordInput,
         widget=forms.PasswordInput,
-        required=False
+        required=False,
     )
     )
 
 
     is_avatar_locked = YesNoSwitch(
     is_avatar_locked = YesNoSwitch(
@@ -132,7 +126,7 @@ class EditUserForm(UserBaseForm):
     signature = forms.CharField(
     signature = forms.CharField(
         label=_("Signature contents"),
         label=_("Signature contents"),
         widget=forms.Textarea(attrs={'rows': 3}),
         widget=forms.Textarea(attrs={'rows': 3}),
-        required=False
+        required=False,
     )
     )
     is_signature_locked = YesNoSwitch(
     is_signature_locked = YesNoSwitch(
         label=_("Lock signature"),
         label=_("Lock signature"),
@@ -143,17 +137,13 @@ class EditUserForm(UserBaseForm):
     )
     )
     signature_lock_user_message = forms.CharField(
     signature_lock_user_message = forms.CharField(
         label=_("User message"),
         label=_("User message"),
-        help_text=_(
-            "Optional message to user explaining why his/hers signature is locked."
-        ),
+        help_text=_("Optional message to user explaining why his/hers signature is locked."),
         widget=forms.Textarea(attrs={'rows': 3}),
         widget=forms.Textarea(attrs={'rows': 3}),
         required=False
         required=False
     )
     )
     signature_lock_staff_message = forms.CharField(
     signature_lock_staff_message = forms.CharField(
         label=_("Staff message"),
         label=_("Staff message"),
-        help_text=_(
-            "Optional message to team members explaining why user signature is locked."
-        ),
+        help_text=_("Optional message to team members explaining why user signature is locked."),
         widget=forms.Textarea(attrs={'rows': 3}),
         widget=forms.Textarea(attrs={'rows': 3}),
         required=False
         required=False
     )
     )
@@ -167,14 +157,10 @@ class EditUserForm(UserBaseForm):
     )
     )
 
 
     subscribe_to_started_threads = forms.TypedChoiceField(
     subscribe_to_started_threads = forms.TypedChoiceField(
-        label=_("Started threads"),
-        coerce=int,
-        choices=UserModel.SUBSCRIBE_CHOICES
+        label=_("Started threads"), coerce=int, choices=UserModel.SUBSCRIBE_CHOICES
     )
     )
     subscribe_to_replied_threads = forms.TypedChoiceField(
     subscribe_to_replied_threads = forms.TypedChoiceField(
-        label=_("Replid threads"),
-        coerce=int,
-        choices=UserModel.SUBSCRIBE_CHOICES
+        label=_("Replid threads"), coerce=int, choices=UserModel.SUBSCRIBE_CHOICES
     )
     )
 
 
     class Meta:
     class Meta:
@@ -201,11 +187,13 @@ class EditUserForm(UserBaseForm):
 
 
         length_limit = settings.signature_length_max
         length_limit = settings.signature_length_max
         if len(data) > length_limit:
         if len(data) > length_limit:
-            raise forms.ValidationError(ungettext(
-                "Signature can't be longer than %(limit)s character.",
-                "Signature can't be longer than %(limit)s characters.",
-                length_limit
-            ) % {'limit': length_limit})
+            raise forms.ValidationError(
+                ungettext(
+                    "Signature can't be longer than %(limit)s character.",
+                    "Signature can't be longer than %(limit)s characters.",
+                    length_limit,
+                ) % {'limit': length_limit}
+            )
 
 
         return data
         return data
 
 
@@ -227,15 +215,13 @@ def UserFormFactory(FormType, instance):
 
 
     extra_fields['roles'] = forms.ModelMultipleChoiceField(
     extra_fields['roles'] = forms.ModelMultipleChoiceField(
         label=_("Roles"),
         label=_("Roles"),
-        help_text=_(
-            'Individual roles of this user. All users must have "member" role.'
-        ),
+        help_text=_('Individual roles of this user. All users must have "member" role.'),
         queryset=roles,
         queryset=roles,
         initial=instance.roles.all() if instance.pk else None,
         initial=instance.roles.all() if instance.pk else None,
         widget=forms.CheckboxSelectMultiple
         widget=forms.CheckboxSelectMultiple
     )
     )
 
 
-    return type('UserFormFinal', (FormType,), extra_fields)
+    return type('UserFormFinal', (FormType, ), extra_fields)
 
 
 
 
 def StaffFlagUserFormFactory(FormType, instance):
 def StaffFlagUserFormFactory(FormType, instance):
@@ -252,7 +238,7 @@ def StaffFlagUserFormFactory(FormType, instance):
         ),
         ),
     }
     }
 
 
-    return type('StaffUserForm', (FormType,), staff_fields)
+    return type('StaffUserForm', (FormType, ), staff_fields)
 
 
 
 
 def UserIsActiveFormFactory(FormType, instance):
 def UserIsActiveFormFactory(FormType, instance):
@@ -271,11 +257,10 @@ def UserIsActiveFormFactory(FormType, instance):
         ),
         ),
     }
     }
 
 
-    return type('UserIsActiveForm', (FormType,), is_active_fields)
+    return type('UserIsActiveForm', (FormType, ), is_active_fields)
 
 
 
 
-def EditUserFormFactory(FormType, instance,
-        add_is_active_fields=False, add_admin_fields=False):
+def EditUserFormFactory(FormType, instance, add_is_active_fields=False, add_admin_fields=False):
     FormType = UserFormFactory(FormType, instance)
     FormType = UserFormFactory(FormType, instance)
 
 
     if add_is_active_fields:
     if add_is_active_fields:
@@ -296,12 +281,10 @@ class SearchUsersFormBase(forms.Form):
 
 
     def filter_queryset(self, criteria, queryset):
     def filter_queryset(self, criteria, queryset):
         if criteria.get('username'):
         if criteria.get('username'):
-            queryset = queryset.filter(
-                slug__startswith=criteria.get('username').lower())
+            queryset = queryset.filter(slug__startswith=criteria.get('username').lower())
 
 
         if criteria.get('email'):
         if criteria.get('email'):
-            queryset = queryset.filter(
-                email__istartswith=criteria.get('email'))
+            queryset = queryset.filter(email__istartswith=criteria.get('email'))
 
 
         if criteria.get('rank'):
         if criteria.get('rank'):
             queryset = queryset.filter(rank_id=criteria.get('rank'))
             queryset = queryset.filter(rank_id=criteria.get('rank'))
@@ -346,24 +329,20 @@ def SearchUsersForm(*args, **kwargs):
             label=_("Has rank"),
             label=_("Has rank"),
             coerce=int,
             coerce=int,
             required=False,
             required=False,
-            choices=ranks_choices
+            choices=ranks_choices,
         ),
         ),
         'role': forms.TypedChoiceField(
         'role': forms.TypedChoiceField(
             label=_("Has role"),
             label=_("Has role"),
             coerce=int,
             coerce=int,
             required=False,
             required=False,
-            choices=roles_choices
+            choices=roles_choices,
         )
         )
     }
     }
 
 
-    FinalForm = type(
-        'SearchUsersFormFinal', (SearchUsersFormBase,), extra_fields)
+    FinalForm = type('SearchUsersFormFinal', (SearchUsersFormBase, ), extra_fields)
     return FinalForm(*args, **kwargs)
     return FinalForm(*args, **kwargs)
 
 
 
 
-"""
-Ranks
-"""
 class RankForm(forms.ModelForm):
 class RankForm(forms.ModelForm):
     name = forms.CharField(
     name = forms.CharField(
         label=_("Name"),
         label=_("Name"),
@@ -401,17 +380,14 @@ class RankForm(forms.ModelForm):
     css_class = forms.CharField(
     css_class = forms.CharField(
         label=_("CSS class"),
         label=_("CSS class"),
         required=False,
         required=False,
-        help_text=_(
-            "Optional css class added to content belonging to this rank owner."
-        )
+        help_text=_("Optional css class added to content belonging to this rank owner.")
     )
     )
     is_tab = forms.BooleanField(
     is_tab = forms.BooleanField(
         label=_("Give rank dedicated tab on users list"),
         label=_("Give rank dedicated tab on users list"),
         required=False,
         required=False,
         help_text=_(
         help_text=_(
-            "Selecting this option will make users with this rank "
-            "easily discoverable by others trough dedicated page on "
-            "forum users list."
+            "Selecting this option will make users with this rank easily discoverable "
+            "by others through dedicated page on forum users list."
         )
         )
     )
     )
 
 
@@ -435,27 +411,23 @@ class RankForm(forms.ModelForm):
             unique_qs = unique_qs.exclude(pk=self.instance.pk)
             unique_qs = unique_qs.exclude(pk=self.instance.pk)
 
 
         if unique_qs.exists():
         if unique_qs.exists():
-            raise forms.ValidationError(
-                _("This name collides with other rank."))
+            raise forms.ValidationError(_("This name collides with other rank."))
 
 
         return data
         return data
 
 
 
 
-"""
-Bans
-"""
 class BanUsersForm(forms.Form):
 class BanUsersForm(forms.Form):
     ban_type = forms.MultipleChoiceField(
     ban_type = forms.MultipleChoiceField(
         label=_("Values to ban"),
         label=_("Values to ban"),
         widget=forms.CheckboxSelectMultiple,
         widget=forms.CheckboxSelectMultiple,
-        choices=(
+        choices=[
             ('usernames', _('Usernames')),
             ('usernames', _('Usernames')),
             ('emails', _('E-mails')),
             ('emails', _('E-mails')),
             ('domains', _('E-mail domains')),
             ('domains', _('E-mail domains')),
             ('ip', _('IP addresses')),
             ('ip', _('IP addresses')),
             ('ip_first', _('First segment of IP addresses')),
             ('ip_first', _('First segment of IP addresses')),
-            ('ip_two', _('First two segments of IP addresses'))
-        )
+            ('ip_two', _('First two segments of IP addresses')),
+        ]
     )
     )
     user_message = forms.CharField(
     user_message = forms.CharField(
         label=_("User message"),
         label=_("User message"),
@@ -464,7 +436,7 @@ class BanUsersForm(forms.Form):
         help_text=_("Optional message displayed to users instead of default one."),
         help_text=_("Optional message displayed to users instead of default one."),
         widget=forms.Textarea(attrs={'rows': 3}),
         widget=forms.Textarea(attrs={'rows': 3}),
         error_messages={
         error_messages={
-            'max_length': _("Message can't be longer than 1000 characters.")
+            'max_length': _("Message can't be longer than 1000 characters."),
         }
         }
     )
     )
     staff_message = forms.CharField(
     staff_message = forms.CharField(
@@ -474,7 +446,7 @@ class BanUsersForm(forms.Form):
         help_text=_("Optional ban message for moderators and administrators."),
         help_text=_("Optional ban message for moderators and administrators."),
         widget=forms.Textarea(attrs={'rows': 3}),
         widget=forms.Textarea(attrs={'rows': 3}),
         error_messages={
         error_messages={
-            'max_length': _("Message can't be longer than 1000 characters.")
+            'max_length': _("Message can't be longer than 1000 characters."),
         }
         }
     )
     )
     expires_on = IsoDateTimeField(
     expires_on = IsoDateTimeField(
@@ -485,11 +457,7 @@ class BanUsersForm(forms.Form):
 
 
 
 
 class BanForm(forms.ModelForm):
 class BanForm(forms.ModelForm):
-    check_type = forms.TypedChoiceField(
-        label=_("Check type"),
-        coerce=int,
-        choices=Ban.CHOICES
-    )
+    check_type = forms.TypedChoiceField(label=_("Check type"), coerce=int, choices=Ban.CHOICES)
     banned_value = forms.CharField(
     banned_value = forms.CharField(
         label=_("Banned value"),
         label=_("Banned value"),
         max_length=250,
         max_length=250,
@@ -499,8 +467,7 @@ class BanForm(forms.ModelForm):
             '"83.*" will ban all IP addresses beginning with "83.".'
             '"83.*" will ban all IP addresses beginning with "83.".'
         ),
         ),
         error_messages={
         error_messages={
-            'max_length': _("Banned value can't be longer "
-                            "than 250 characters.")
+            'max_length': _("Banned value can't be longer than 250 characters."),
         }
         }
     )
     )
     user_message = forms.CharField(
     user_message = forms.CharField(
@@ -510,7 +477,7 @@ class BanForm(forms.ModelForm):
         help_text=_("Optional message displayed to user instead of default one."),
         help_text=_("Optional message displayed to user instead of default one."),
         widget=forms.Textarea(attrs={'rows': 3}),
         widget=forms.Textarea(attrs={'rows': 3}),
         error_messages={
         error_messages={
-            'max_length': _("Message can't be longer than 1000 characters.")
+            'max_length': _("Message can't be longer than 1000 characters."),
         }
         }
     )
     )
     staff_message = forms.CharField(
     staff_message = forms.CharField(
@@ -520,7 +487,7 @@ class BanForm(forms.ModelForm):
         help_text=_("Optional ban message for moderators and administrators."),
         help_text=_("Optional ban message for moderators and administrators."),
         widget=forms.Textarea(attrs={'rows': 3}),
         widget=forms.Textarea(attrs={'rows': 3}),
         error_messages={
         error_messages={
-            'max_length': _("Message can't be longer than 1000 characters.")
+            'max_length': _("Message can't be longer than 1000 characters."),
         }
         }
     )
     )
     expires_on = IsoDateTimeField(
     expires_on = IsoDateTimeField(
@@ -551,30 +518,23 @@ class BanForm(forms.ModelForm):
 
 
 
 
 class SearchBansForm(forms.Form):
 class SearchBansForm(forms.Form):
-    SARCH_CHOICES = (
+    SARCH_CHOICES = [
         ('', _('All bans')),
         ('', _('All bans')),
         ('names', _('Usernames')),
         ('names', _('Usernames')),
         ('emails', _('E-mails')),
         ('emails', _('E-mails')),
         ('ips', _('IPs')),
         ('ips', _('IPs')),
-    )
+    ]
 
 
-    check_type = forms.ChoiceField(
-        label=_("Type"),
-        required=False,
-        choices=SARCH_CHOICES
-    )
-    value = forms.CharField(
-        label=_("Banned value begins with"),
-        required=False
-    )
+    check_type = forms.ChoiceField(label=_("Type"), required=False, choices=SARCH_CHOICES)
+    value = forms.CharField(label=_("Banned value begins with"), required=False)
     state = forms.ChoiceField(
     state = forms.ChoiceField(
         label=_("State"),
         label=_("State"),
         required=False,
         required=False,
-        choices=(
+        choices=[
             ('', _('Any')),
             ('', _('Any')),
             ('used', _('Active')),
             ('used', _('Active')),
             ('unused', _('Expired')),
             ('unused', _('Expired')),
-        )
+        ]
     )
     )
 
 
     def filter_queryset(self, search_criteria, queryset):
     def filter_queryset(self, search_criteria, queryset):
@@ -589,8 +549,7 @@ class SearchBansForm(forms.Form):
             queryset = queryset.filter(check_type=2)
             queryset = queryset.filter(check_type=2)
 
 
         if criteria.get('value'):
         if criteria.get('value'):
-            queryset = queryset.filter(
-                banned_value__startswith=criteria.get('value').lower())
+            queryset = queryset.filter(banned_value__startswith=criteria.get('value').lower())
 
 
         if criteria.get('state') == 'used':
         if criteria.get('state') == 'used':
             queryset = queryset.filter(is_checked=True)
             queryset = queryset.filter(is_checked=True)

+ 27 - 47
misago/users/forms/auth.py

@@ -15,21 +15,18 @@ class MisagoAuthMixin(object):
     error_messages = {
     error_messages = {
         'empty_data': _("Fill out both fields."),
         'empty_data': _("Fill out both fields."),
         'invalid_login': _("Login or password is incorrect."),
         'invalid_login': _("Login or password is incorrect."),
-        'inactive_user': _("You have to activate your account before "
-                           "you will be able to sign in."),
-        'inactive_admin': _("Your account has to be activated by "
-                            "Administrator before you will be able "
-                            "to sign in."),
+        'inactive_user': _("You have to activate your account before you will be able to sign in."),
+        'inactive_admin': _(
+            "Your account has to be activated by Administrator before you will be able to sign in."
+        ),
     }
     }
 
 
     def confirm_user_active(self, user):
     def confirm_user_active(self, user):
         if user.requires_activation_by_admin:
         if user.requires_activation_by_admin:
-            raise ValidationError(
-                self.error_messages['inactive_admin'], code='inactive_admin')
+            raise ValidationError(self.error_messages['inactive_admin'], code='inactive_admin')
 
 
         if user.requires_activation_by_user:
         if user.requires_activation_by_user:
-            raise ValidationError(
-                self.error_messages['inactive_user'], code='inactive_user')
+            raise ValidationError(self.error_messages['inactive_user'], code='inactive_user')
 
 
     def confirm_user_not_banned(self, user):
     def confirm_user_not_banned(self, user):
         if not user.is_staff:
         if not user.is_staff:
@@ -44,10 +41,7 @@ class MisagoAuthMixin(object):
         else:
         else:
             error.message = error.messages[0]
             error.message = error.messages[0]
 
 
-        return {
-            'detail': error.message,
-            'code': error.code
-        }
+        return {'detail': error.message, 'code': error.code}
 
 
 
 
 class AuthenticationForm(MisagoAuthMixin, BaseAuthenticationForm):
 class AuthenticationForm(MisagoAuthMixin, BaseAuthenticationForm):
@@ -55,33 +49,22 @@ class AuthenticationForm(MisagoAuthMixin, BaseAuthenticationForm):
     Base class for authenticating users, Floppy-forms and
     Base class for authenticating users, Floppy-forms and
     Misago login field compliant
     Misago login field compliant
     """
     """
-    username = forms.CharField(
-        label=_("Username or e-mail"),
-        required=False,
-        max_length=254
-    )
-    password = forms.CharField(
-        label=_("Password"),
-        required=False,
-        widget=forms.PasswordInput
-    )
+    username = forms.CharField(label=_("Username or e-mail"), required=False, max_length=254)
+    password = forms.CharField(label=_("Password"), required=False, widget=forms.PasswordInput)
 
 
     def clean(self):
     def clean(self):
         username = self.cleaned_data.get('username')
         username = self.cleaned_data.get('username')
         password = self.cleaned_data.get('password')
         password = self.cleaned_data.get('password')
 
 
         if username and password:
         if username and password:
-            self.user_cache = authenticate(
-                username=username, password=password)
+            self.user_cache = authenticate(username=username, password=password)
 
 
             if self.user_cache is None or not self.user_cache.is_active:
             if self.user_cache is None or not self.user_cache.is_active:
-                raise ValidationError(
-                    self.error_messages['invalid_login'], code='invalid_login')
+                raise ValidationError(self.error_messages['invalid_login'], code='invalid_login')
             else:
             else:
                 self.confirm_login_allowed(self.user_cache)
                 self.confirm_login_allowed(self.user_cache)
         else:
         else:
-            raise ValidationError(
-                self.error_messages['empty_data'], code='empty_data')
+            raise ValidationError(self.error_messages['empty_data'], code='empty_data')
 
 
         return self.cleaned_data
         return self.cleaned_data
 
 
@@ -95,15 +78,14 @@ class AdminAuthenticationForm(AuthenticationForm):
 
 
     def __init__(self, *args, **kwargs):
     def __init__(self, *args, **kwargs):
         self.error_messages.update({
         self.error_messages.update({
-            'not_staff': _("Your account does not have admin privileges.")
+            'not_staff': _("Your account does not have admin privileges."),
         })
         })
 
 
         super(AdminAuthenticationForm, self).__init__(*args, **kwargs)
         super(AdminAuthenticationForm, self).__init__(*args, **kwargs)
 
 
     def confirm_login_allowed(self, user):
     def confirm_login_allowed(self, user):
         if not user.is_staff:
         if not user.is_staff:
-            raise forms.ValidationError(
-                self.error_messages['not_staff'], code='not_staff')
+            raise forms.ValidationError(self.error_messages['not_staff'], code='not_staff')
 
 
 
 
 class GetUserForm(MisagoAuthMixin, forms.Form):
 class GetUserForm(MisagoAuthMixin, forms.Form):
@@ -114,14 +96,12 @@ class GetUserForm(MisagoAuthMixin, forms.Form):
 
 
         email = data.get('email')
         email = data.get('email')
         if not email or len(email) > 250:
         if not email or len(email) > 250:
-            raise forms.ValidationError(
-                _("Enter e-mail address."), code='empty_email')
+            raise forms.ValidationError(_("Enter e-mail address."), code='empty_email')
 
 
         try:
         try:
             validate_email(email)
             validate_email(email)
         except forms.ValidationError:
         except forms.ValidationError:
-            raise forms.ValidationError(
-                _("Entered e-mail is invalid."), code='invalid_email')
+            raise forms.ValidationError(_("Entered e-mail is invalid."), code='invalid_email')
 
 
         try:
         try:
             user = UserModel.objects.get_by_email(data['email'])
             user = UserModel.objects.get_by_email(data['email'])
@@ -129,8 +109,7 @@ class GetUserForm(MisagoAuthMixin, forms.Form):
                 raise UserModel.DoesNotExist()
                 raise UserModel.DoesNotExist()
             self.user_cache = user
             self.user_cache = user
         except UserModel.DoesNotExist:
         except UserModel.DoesNotExist:
-            raise forms.ValidationError(
-                _("No user with this e-mail exists."), code='not_found')
+            raise forms.ValidationError(_("No user with this e-mail exists."), code='not_found')
 
 
         self.confirm_allowed(user)
         self.confirm_allowed(user)
 
 
@@ -146,22 +125,23 @@ class ResendActivationForm(GetUserForm):
 
 
         if not user.requires_activation:
         if not user.requires_activation:
             message = _("%(user)s, your account is already active.")
             message = _("%(user)s, your account is already active.")
-            raise forms.ValidationError(
-                message % username_format, code='already_active')
+            raise forms.ValidationError(message % username_format, code='already_active')
 
 
         if user.requires_activation_by_admin:
         if user.requires_activation_by_admin:
             message = _("%(user)s, only administrator may activate your account.")
             message = _("%(user)s, only administrator may activate your account.")
-            raise forms.ValidationError(
-                message % username_format, code='inactive_admin')
+            raise forms.ValidationError(message % username_format, code='inactive_admin')
 
 
 
 
 class ResetPasswordForm(GetUserForm):
 class ResetPasswordForm(GetUserForm):
     error_messages = {
     error_messages = {
-        'inactive_user': _("You have to activate your account before "
-                           "you will be able to request new password."),
-        'inactive_admin': _("Administrator has to activate your account "
-                            "before you will be able to request "
-                            "new password."),
+        'inactive_user': _(
+            "You have to activate your account before "
+            "you will be able to request new password."
+        ),
+        'inactive_admin': _(
+            "Administrator has to activate your account before "
+            "you will be able to request new password."
+        ),
     }
     }
 
 
     def confirm_allowed(self, user):
     def confirm_allowed(self, user):

+ 1 - 1
misago/users/forms/register.py

@@ -27,7 +27,7 @@ class RegisterForm(forms.Form):
                 user=UserModel(
                 user=UserModel(
                     username=cleaned_data.get('username'),
                     username=cleaned_data.get('username'),
                     email=cleaned_data.get('email'),
                     email=cleaned_data.get('email'),
-                )
+                ),
             )
             )
 
 
     def clean(self):
     def clean(self):

+ 0 - 1
misago/users/management/commands/buildactivepostersranking.py

@@ -1,6 +1,5 @@
 from django.core.management.base import BaseCommand
 from django.core.management.base import BaseCommand
 
 
-from misago.core.management.progressbar import show_progress
 from misago.users.activepostersranking import build_active_posters_ranking
 from misago.users.activepostersranking import build_active_posters_ranking
 
 
 
 

+ 45 - 25
misago/users/management/commands/createsuperuser.py

@@ -27,24 +27,45 @@ class Command(BaseCommand):
     help = 'Used to create a superuser.'
     help = 'Used to create a superuser.'
 
 
     def add_arguments(self, parser):
     def add_arguments(self, parser):
-        parser.add_argument('--username', dest='username', default=None,
-                    help='Specifies the username for the superuser.')
-        parser.add_argument('--email', dest='email', default=None,
-                    help='Specifies the username for the superuser.')
-        parser.add_argument('--password', dest='password', default=None,
-                    help='Specifies the username for the superuser.')
-        parser.add_argument('--noinput', action='store_false', dest='interactive',
-                    default=True,
-                    help=('Tells Misago to NOT prompt the user for input '
-                          'of any kind. You must use --username with '
-                          '--noinput, along with an option for any other '
-                          'required field. Superusers created with '
-                          '--noinput will  not be able to log in until '
-                          'they\'re given a valid password.'))
-        parser.add_argument('--database', action='store', dest='database',
-                    default=DEFAULT_DB_ALIAS,
-                    help=('Specifies the database to use. '
-                          'Default is "default".'))
+        parser.add_argument(
+            '--username',
+            dest='username',
+            default=None,
+            help="Specifies the username for the superuser.",
+        )
+        parser.add_argument(
+            '--email',
+            dest='email',
+            default=None,
+            help="Specifies the username for the superuser.",
+        )
+        parser.add_argument(
+            '--password',
+            dest='password',
+            default=None,
+            help="Specifies the username for the superuser.",
+        )
+        parser.add_argument(
+            '--noinput',
+            action='store_false',
+            dest='interactive',
+            default=True,
+            help=(
+                "Tells Misago to NOT prompt the user for input "
+                "of any kind. You must use --username with "
+                "--noinput, along with an option for any other "
+                "required field. Superusers created with "
+                "--noinput will  not be able to log in until "
+                "they're given a valid password."
+            ),
+        )
+        parser.add_argument(
+            '--database',
+            action='store',
+            dest='database',
+            default=DEFAULT_DB_ALIAS,
+            help=('Specifies the database to use. Default is "default".'),
+        )
 
 
     def execute(self, *args, **options):
     def execute(self, *args, **options):
         self.stdin = options.get('stdin', sys.stdin)  # Used for testing
         self.stdin = options.get('stdin', sys.stdin)  # Used for testing
@@ -114,15 +135,13 @@ class Command(BaseCommand):
                 while not password:
                 while not password:
                     try:
                     try:
                         raw_value = getpass("Enter password: ").strip()
                         raw_value = getpass("Enter password: ").strip()
-                        validate_password(raw_value, user=UserModel(
-                            username=username,
-                            email=email
-                        ))
+                        validate_password(
+                            raw_value, user=UserModel(username=username, email=email)
+                        )
 
 
                         repeat_raw_value = getpass("Repeat password: ").strip()
                         repeat_raw_value = getpass("Repeat password: ").strip()
                         if raw_value != repeat_raw_value:
                         if raw_value != repeat_raw_value:
-                            raise ValidationError(
-                                "Entered passwords are different.")
+                            raise ValidationError("Entered passwords are different.")
                         password = raw_value
                         password = raw_value
                     except ValidationError as e:
                     except ValidationError as e:
                         self.stderr.write(e.messages[0])
                         self.stderr.write(e.messages[0])
@@ -143,7 +162,8 @@ class Command(BaseCommand):
     def create_superuser(self, username, email, password, verbosity):
     def create_superuser(self, username, email, password, verbosity):
         try:
         try:
             user = UserModel.objects.create_superuser(
             user = UserModel.objects.create_superuser(
-                username, email, password, set_default_avatar=True)
+                username, email, password, set_default_avatar=True
+            )
 
 
             if verbosity >= 1:
             if verbosity >= 1:
                 message = "Superuser #%(pk)s has been created successfully."
                 message = "Superuser #%(pk)s has been created successfully."

+ 5 - 2
misago/users/management/commands/synchronizeusers.py

@@ -35,15 +35,18 @@ class Command(BaseCommand):
             user.threads = user.thread_set.filter(
             user.threads = user.thread_set.filter(
                 category__in=categories,
                 category__in=categories,
                 is_hidden=False,
                 is_hidden=False,
-                is_unapproved=False
+                is_unapproved=False,
             ).count()
             ).count()
+
             user.posts = user.post_set.filter(
             user.posts = user.post_set.filter(
                 category__in=categories,
                 category__in=categories,
                 is_event=False,
                 is_event=False,
-                is_unapproved=False
+                is_unapproved=False,
             ).count()
             ).count()
+
             user.followers = user.followed_by.count()
             user.followers = user.followed_by.count()
             user.following = user.follows.count()
             user.following = user.follows.count()
+
             user.save()
             user.save()
 
 
             synchronized_count += 1
             synchronized_count += 1

+ 0 - 3
misago/users/middleware.py

@@ -1,9 +1,6 @@
 from django.contrib.auth import logout
 from django.contrib.auth import logout
-from django.contrib.auth.models import AnonymousUser as DjAnonymousUser
 from django.utils.deprecation import MiddlewareMixin
 from django.utils.deprecation import MiddlewareMixin
 
 
-import pytz
-
 from .bans import get_request_ip_ban, get_user_ban
 from .bans import get_request_ip_ban, get_user_ban
 from .models import AnonymousUser, Online
 from .models import AnonymousUser, Online
 from .online import tracker
 from .online import tracker

+ 173 - 42
misago/users/migrations/0001_initial.py

@@ -22,34 +22,94 @@ class Migration(migrations.Migration):
         migrations.CreateModel(
         migrations.CreateModel(
             name='User',
             name='User',
             fields=[
             fields=[
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+                (
+                    'id', models.AutoField(
+                        verbose_name='ID', serialize=False, auto_created=True, primary_key=True
+                    )
+                ),
                 ('password', models.CharField(max_length=128, verbose_name='password')),
                 ('password', models.CharField(max_length=128, verbose_name='password')),
-                ('last_login', models.DateTimeField(null=True, blank=True, verbose_name='last login')),
+                (
+                    'last_login',
+                    models.DateTimeField(null=True, blank=True, verbose_name='last login')
+                ),
                 ('username', models.CharField(max_length=30)),
                 ('username', models.CharField(max_length=30)),
                 ('slug', models.CharField(unique=True, max_length=30)),
                 ('slug', models.CharField(unique=True, max_length=30)),
                 ('email', models.EmailField(max_length=255, db_index=True)),
                 ('email', models.EmailField(max_length=255, db_index=True)),
                 ('email_hash', models.CharField(unique=True, max_length=32)),
                 ('email_hash', models.CharField(unique=True, max_length=32)),
-                ('joined_on', models.DateTimeField(default=django.utils.timezone.now, verbose_name='joined on')),
+                (
+                    'joined_on', models.DateTimeField(
+                        default=django.utils.timezone.now, verbose_name='joined on'
+                    )
+                ),
                 ('joined_from_ip', models.GenericIPAddressField()),
                 ('joined_from_ip', models.GenericIPAddressField()),
                 ('last_ip', models.GenericIPAddressField(null=True, blank=True)),
                 ('last_ip', models.GenericIPAddressField(null=True, blank=True)),
                 ('is_hiding_presence', models.BooleanField(default=False)),
                 ('is_hiding_presence', models.BooleanField(default=False)),
                 ('title', models.CharField(max_length=255, null=True, blank=True)),
                 ('title', models.CharField(max_length=255, null=True, blank=True)),
                 ('requires_activation', models.PositiveIntegerField(default=0)),
                 ('requires_activation', models.PositiveIntegerField(default=0)),
-                ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into admin sites.', verbose_name='staff status')),
-                ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
+                (
+                    'is_staff', models.BooleanField(
+                        default=False,
+                        help_text='Designates whether the user can log into admin sites.',
+                        verbose_name='staff status'
+                    )
+                ),
+                (
+                    'is_superuser', models.BooleanField(
+                        default=False,
+                        help_text='Designates that this user has all permissions without explicitly assigning them.',
+                        verbose_name='superuser status'
+                    )
+                ),
                 ('acl_key', models.CharField(max_length=12, null=True, blank=True)),
                 ('acl_key', models.CharField(max_length=12, null=True, blank=True)),
-                ('is_active', models.BooleanField(
-                    db_index=True, default=True, verbose_name='active', help_text=(
-                        'Designates whether this user should be treated as active. Unselect this instead of deleting '
-                        'accounts.'
+                (
+                    'is_active', models.BooleanField(
+                        db_index=True,
+                        default=True,
+                        verbose_name='active',
+                        help_text=(
+                            'Designates whether this user should be treated as active. Unselect this instead of deleting '
+                            'accounts.'
+                        )
                     )
                     )
-                )),
+                ),
                 ('is_active_staff_message', models.TextField(null=True, blank=True)),
                 ('is_active_staff_message', models.TextField(null=True, blank=True)),
-                ('groups', models.ManyToManyField(related_query_name='user', related_name='user_set', to='auth.Group', blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of his/her group.', verbose_name='groups')),
+                (
+                    'groups', models.ManyToManyField(
+                        related_query_name='user',
+                        related_name='user_set',
+                        to='auth.Group',
+                        blank=True,
+                        help_text='The groups this user belongs to. A user will get all permissions granted to each of his/her group.',
+                        verbose_name='groups'
+                    )
+                ),
                 ('roles', models.ManyToManyField(to='misago_acl.Role')),
                 ('roles', models.ManyToManyField(to='misago_acl.Role')),
-                ('user_permissions', models.ManyToManyField(related_query_name='user', related_name='user_set', to='auth.Permission', blank=True, help_text='Specific permissions for this user.', verbose_name='user permissions')),
-                ('avatar_tmp', models.ImageField(max_length=255, upload_to=misago.users.avatars.store.upload_to, null=True, blank=True)),
-                ('avatar_src', models.ImageField(max_length=255, upload_to=misago.users.avatars.store.upload_to, null=True, blank=True)),
+                (
+                    'user_permissions', models.ManyToManyField(
+                        related_query_name='user',
+                        related_name='user_set',
+                        to='auth.Permission',
+                        blank=True,
+                        help_text='Specific permissions for this user.',
+                        verbose_name='user permissions'
+                    )
+                ),
+                (
+                    'avatar_tmp', models.ImageField(
+                        max_length=255,
+                        upload_to=misago.users.avatars.store.upload_to,
+                        null=True,
+                        blank=True
+                    )
+                ),
+                (
+                    'avatar_src', models.ImageField(
+                        max_length=255,
+                        upload_to=misago.users.avatars.store.upload_to,
+                        null=True,
+                        blank=True
+                    )
+                ),
                 ('avatar_crop', models.CharField(max_length=255, null=True, blank=True)),
                 ('avatar_crop', models.CharField(max_length=255, null=True, blank=True)),
                 ('avatars', JSONField(null=True, blank=True)),
                 ('avatars', JSONField(null=True, blank=True)),
                 ('is_avatar_locked', models.BooleanField(default=False)),
                 ('is_avatar_locked', models.BooleanField(default=False)),
@@ -75,7 +135,7 @@ class Migration(migrations.Migration):
             options={
             options={
                 'abstract': False,
                 'abstract': False,
             },
             },
-            bases=(models.Model,),
+            bases=(models.Model, ),
         ),
         ),
         CreatePartialIndex(
         CreatePartialIndex(
             field='User.is_staff',
             field='User.is_staff',
@@ -92,32 +152,57 @@ class Migration(migrations.Migration):
             fields=[
             fields=[
                 ('current_ip', models.GenericIPAddressField()),
                 ('current_ip', models.GenericIPAddressField()),
                 ('last_click', models.DateTimeField(default=django.utils.timezone.now)),
                 ('last_click', models.DateTimeField(default=django.utils.timezone.now)),
-                ('user', models.OneToOneField(related_name='online_tracker', primary_key=True, serialize=False, to=settings.AUTH_USER_MODEL)),
+                (
+                    'user', models.OneToOneField(
+                        related_name='online_tracker',
+                        primary_key=True,
+                        serialize=False,
+                        to=settings.AUTH_USER_MODEL
+                    )
+                ),
             ],
             ],
-            options={
-            },
-            bases=(models.Model,),
+            options={},
+            bases=(models.Model, ),
         ),
         ),
         migrations.CreateModel(
         migrations.CreateModel(
             name='UsernameChange',
             name='UsernameChange',
             fields=[
             fields=[
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+                (
+                    'id', models.AutoField(
+                        verbose_name='ID', serialize=False, auto_created=True, primary_key=True
+                    )
+                ),
                 ('changed_by_username', models.CharField(max_length=30)),
                 ('changed_by_username', models.CharField(max_length=30)),
                 ('changed_on', models.DateTimeField(default=django.utils.timezone.now)),
                 ('changed_on', models.DateTimeField(default=django.utils.timezone.now)),
                 ('new_username', models.CharField(max_length=255)),
                 ('new_username', models.CharField(max_length=255)),
                 ('old_username', models.CharField(max_length=255)),
                 ('old_username', models.CharField(max_length=255)),
-                ('changed_by', models.ForeignKey(related_name='user_renames', on_delete=django.db.models.deletion.SET_NULL, blank=True, to=settings.AUTH_USER_MODEL, null=True)),
-                ('user', models.ForeignKey(related_name='namechanges', to=settings.AUTH_USER_MODEL)),
+                (
+                    'changed_by', models.ForeignKey(
+                        related_name='user_renames',
+                        on_delete=django.db.models.deletion.SET_NULL,
+                        blank=True,
+                        to=settings.AUTH_USER_MODEL,
+                        null=True
+                    )
+                ),
+                (
+                    'user',
+                    models.ForeignKey(related_name='namechanges', to=settings.AUTH_USER_MODEL)
+                ),
             ],
             ],
             options={
             options={
                 'get_latest_by': b'changed_on',
                 'get_latest_by': b'changed_on',
             },
             },
-            bases=(models.Model,),
+            bases=(models.Model, ),
         ),
         ),
         migrations.CreateModel(
         migrations.CreateModel(
             name='Rank',
             name='Rank',
             fields=[
             fields=[
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+                (
+                    'id', models.AutoField(
+                        verbose_name='ID', serialize=False, auto_created=True, primary_key=True
+                    )
+                ),
                 ('name', models.CharField(max_length=255)),
                 ('name', models.CharField(max_length=255)),
                 ('slug', models.CharField(unique=True, max_length=255)),
                 ('slug', models.CharField(unique=True, max_length=255)),
                 ('description', models.TextField(null=True, blank=True)),
                 ('description', models.TextField(null=True, blank=True)),
@@ -131,12 +216,18 @@ class Migration(migrations.Migration):
             options={
             options={
                 'get_latest_by': b'order',
                 'get_latest_by': b'order',
             },
             },
-            bases=(models.Model,),
+            bases=(models.Model, ),
         ),
         ),
         migrations.AddField(
         migrations.AddField(
             model_name='user',
             model_name='user',
             name='rank',
             name='rank',
-            field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to_field='id', blank=True, to='misago_users.Rank', null=True),
+            field=models.ForeignKey(
+                on_delete=django.db.models.deletion.PROTECT,
+                to_field='id',
+                blank=True,
+                to='misago_users.Rank',
+                null=True
+            ),
             preserve_default=True,
             preserve_default=True,
         ),
         ),
         migrations.AddField(
         migrations.AddField(
@@ -154,29 +245,52 @@ class Migration(migrations.Migration):
         migrations.CreateModel(
         migrations.CreateModel(
             name='ActivityRanking',
             name='ActivityRanking',
             fields=[
             fields=[
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+                (
+                    'id', models.AutoField(
+                        verbose_name='ID', serialize=False, auto_created=True, primary_key=True
+                    )
+                ),
                 ('user', models.ForeignKey(related_name='+', to=settings.AUTH_USER_MODEL)),
                 ('user', models.ForeignKey(related_name='+', to=settings.AUTH_USER_MODEL)),
                 ('score', models.PositiveIntegerField(default=0, db_index=True)),
                 ('score', models.PositiveIntegerField(default=0, db_index=True)),
             ],
             ],
-            options={
-            },
-            bases=(models.Model,),
+            options={},
+            bases=(models.Model, ),
         ),
         ),
         migrations.CreateModel(
         migrations.CreateModel(
             name='Avatar',
             name='Avatar',
             fields=[
             fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+                (
+                    'id', models.AutoField(
+                        auto_created=True, primary_key=True, serialize=False, verbose_name='ID'
+                    )
+                ),
+                (
+                    'user', models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
+                    )
+                ),
                 ('size', models.PositiveIntegerField(default=0)),
                 ('size', models.PositiveIntegerField(default=0)),
-                ('image', models.ImageField(max_length=255, upload_to=misago.users.avatars.store.upload_to)),
+                (
+                    'image', models.ImageField(
+                        max_length=255, upload_to=misago.users.avatars.store.upload_to
+                    )
+                ),
             ],
             ],
         ),
         ),
         migrations.CreateModel(
         migrations.CreateModel(
             name='AvatarGallery',
             name='AvatarGallery',
             fields=[
             fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                (
+                    'id', models.AutoField(
+                        auto_created=True, primary_key=True, serialize=False, verbose_name='ID'
+                    )
+                ),
                 ('gallery', models.CharField(max_length=255)),
                 ('gallery', models.CharField(max_length=255)),
-                ('image', models.ImageField(max_length=255, upload_to=misago.users.avatars.store.upload_to)),
+                (
+                    'image', models.ImageField(
+                        max_length=255, upload_to=misago.users.avatars.store.upload_to
+                    )
+                ),
             ],
             ],
             options={
             options={
                 'ordering': ['gallery', 'pk'],
                 'ordering': ['gallery', 'pk'],
@@ -185,7 +299,11 @@ class Migration(migrations.Migration):
         migrations.CreateModel(
         migrations.CreateModel(
             name='Ban',
             name='Ban',
             fields=[
             fields=[
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+                (
+                    'id', models.AutoField(
+                        verbose_name='ID', serialize=False, auto_created=True, primary_key=True
+                    )
+                ),
                 ('check_type', models.PositiveIntegerField(default=0, db_index=True)),
                 ('check_type', models.PositiveIntegerField(default=0, db_index=True)),
                 ('banned_value', models.CharField(max_length=255, db_index=True)),
                 ('banned_value', models.CharField(max_length=255, db_index=True)),
                 ('user_message', models.TextField(null=True, blank=True)),
                 ('user_message', models.TextField(null=True, blank=True)),
@@ -193,7 +311,7 @@ class Migration(migrations.Migration):
                 ('expires_on', models.DateTimeField(null=True, blank=True, db_index=True)),
                 ('expires_on', models.DateTimeField(null=True, blank=True, db_index=True)),
                 ('is_checked', models.BooleanField(default=True, db_index=True)),
                 ('is_checked', models.BooleanField(default=True, db_index=True)),
             ],
             ],
-            bases=(models.Model,),
+            bases=(models.Model, ),
         ),
         ),
         migrations.CreateModel(
         migrations.CreateModel(
             name='BanCache',
             name='BanCache',
@@ -202,11 +320,24 @@ class Migration(migrations.Migration):
                 ('staff_message', models.TextField(null=True, blank=True)),
                 ('staff_message', models.TextField(null=True, blank=True)),
                 ('bans_version', models.PositiveIntegerField(default=0)),
                 ('bans_version', models.PositiveIntegerField(default=0)),
                 ('expires_on', models.DateTimeField(null=True, blank=True)),
                 ('expires_on', models.DateTimeField(null=True, blank=True)),
-                ('ban', models.ForeignKey(on_delete=django.db.models.deletion.SET_NULL, blank=True, to='misago_users.Ban', null=True)),
-                ('user', models.OneToOneField(related_name='ban_cache', primary_key=True, serialize=False, to=settings.AUTH_USER_MODEL)),
+                (
+                    'ban', models.ForeignKey(
+                        on_delete=django.db.models.deletion.SET_NULL,
+                        blank=True,
+                        to='misago_users.Ban',
+                        null=True
+                    )
+                ),
+                (
+                    'user', models.OneToOneField(
+                        related_name='ban_cache',
+                        primary_key=True,
+                        serialize=False,
+                        to=settings.AUTH_USER_MODEL
+                    )
+                ),
             ],
             ],
-            options={
-            },
-            bases=(models.Model,),
+            options={},
+            bases=(models.Model, ),
         ),
         ),
     ]
     ]

+ 228 - 214
misago/users/migrations/0002_users_settings.py

@@ -10,239 +10,253 @@ _ = lambda x: x
 
 
 
 
 def create_users_settings_group(apps, schema_editor):
 def create_users_settings_group(apps, schema_editor):
-    migrate_settings_group(apps,{
-        'key': 'users',
-        'name': _("Users"),
-        'description': _("Those settings control user accounts default behaviour and features availability."),
-        'settings': (
-            {
-                'setting': 'account_activation',
-                'name': _("New accounts activation"),
-                'legend': _("New accounts"),
-                'value': 'none',
-                'form_field': 'select',
-                'field_extra': {
-                    'choices': (
-                        ('none', _("No activation required")),
-                        ('user', _("Activation token sent to User")),
-                        ('admin', _("Activation by administrator")),
-                        ('closed', _("Don't allow new registrations"))
-                    )
+    migrate_settings_group(
+        apps, {
+            'key': 'users',
+            'name': _("Users"),
+            'description': _(
+                "Those settings control user accounts default behaviour and features availability."
+            ),
+            'settings': [
+                {
+                    'setting': 'account_activation',
+                    'name': _("New accounts activation"),
+                    'legend': _("New accounts"),
+                    'value': 'none',
+                    'form_field': 'select',
+                    'field_extra': {
+                        'choices': [
+                            ('none', _("No activation required")),
+                            ('user', _("Activation token sent to User")),
+                            ('admin', _("Activation by administrator")),
+                            ('closed', _("Don't allow new registrations")),
+                        ],
+                    },
+                    'is_public': True,
                 },
                 },
-                'is_public': True,
-            },
-            {
-                'setting': 'username_length_min',
-                'name': _("Minimum length"),
-                'description': _("Minimum allowed username length."),
-                'legend': _("User names"),
-                'python_type': 'int',
-                'value': 3,
-                'field_extra': {
-                    'min_value': 2,
-                    'max_value': 20,
+                {
+                    'setting': 'username_length_min',
+                    'name': _("Minimum length"),
+                    'description': _("Minimum allowed username length."),
+                    'legend': _("User names"),
+                    'python_type': 'int',
+                    'value': 3,
+                    'field_extra': {
+                        'min_value': 2,
+                        'max_value': 20,
+                    },
+                    'is_public': True,
                 },
                 },
-                'is_public': True,
-            },
-            {
-                'setting': 'username_length_max',
-                'name': _("Maximum length"),
-                'description': _("Maximum allowed username length."),
-                'python_type': 'int',
-                'value': 14,
-                'field_extra': {
-                    'min_value': 2,
-                    'max_value': 20,
+                {
+                    'setting': 'username_length_max',
+                    'name': _("Maximum length"),
+                    'description': _("Maximum allowed username length."),
+                    'python_type': 'int',
+                    'value': 14,
+                    'field_extra': {
+                        'min_value': 2,
+                        'max_value': 20,
+                    },
+                    'is_public': True,
                 },
                 },
-                'is_public': True,
-            },
-            {
-                'setting': 'password_length_min',
-                'name': _("Minimum length"),
-                'description': _("Minimum allowed user password length."),
-                'legend': _("Passwords"),
-                'python_type': 'int',
-                'value': 5,
-                'field_extra': {
-                    'min_value': 2,
-                    'max_value': 255,
+                {
+                    'setting': 'password_length_min',
+                    'name': _("Minimum length"),
+                    'description': _("Minimum allowed user password length."),
+                    'legend': _("Passwords"),
+                    'python_type': 'int',
+                    'value': 5,
+                    'field_extra': {
+                        'min_value': 2,
+                        'max_value': 255,
+                    },
+                    'is_public': True,
                 },
                 },
-                'is_public': True,
-            },
-            {
-                'setting': 'allow_custom_avatars',
-                'name': _("Allow custom avatars"),
-                'legend': _("Avatars"),
-                'description': _("Turning this option off will forbid "
-                                 "forum users from using avatars from "
-                                 "outside forums. Good for forums "
-                                 "adressed at young users."),
-                'python_type': 'bool',
-                'value': True,
-                'form_field': 'yesno',
-            },
-            {
-                'setting': 'default_avatar',
-                'name': _("Default avatar"),
-                'value': 'gravatar',
-                'form_field': 'select',
-                'field_extra': {
-                    'choices': (
-                        ('dynamic', _("Individual")),
-                        ('gravatar', _("Gravatar")),
-                        ('gallery', _("Random avatar from gallery")),
+                {
+                    'setting': 'allow_custom_avatars',
+                    'name': _("Allow custom avatars"),
+                    'legend': _("Avatars"),
+                    'description': _(
+                        "Turning this option off will forbid "
+                        "forum users from using avatars from "
+                        "outside forums. Good for forums "
+                        "adressed at young users."
                     ),
                     ),
+                    'python_type': 'bool',
+                    'value': True,
+                    'form_field': 'yesno',
                 },
                 },
-            },
-            {
-                'setting': 'default_gravatar_fallback',
-                'name': _("Fallback for default gravatar"),
-                'description': _("Select which avatar to use when user "
-                                 "has no gravatar associated with his "
-                                 "e-mail address."),
-                'value': 'dynamic',
-                'form_field': 'select',
-                'field_extra': {
-                    'choices': (
-                        ('dynamic', _("Individual")),
-                        ('gallery', _("Random avatar from gallery")),
+                {
+                    'setting': 'default_avatar',
+                    'name': _("Default avatar"),
+                    'value': 'gravatar',
+                    'form_field': 'select',
+                    'field_extra': {
+                        'choices': [
+                            ('dynamic', _("Individual")),
+                            ('gravatar', _("Gravatar")),
+                            ('gallery', _("Random avatar from gallery")),
+                        ],
+                    },
+                },
+                {
+                    'setting': 'default_gravatar_fallback',
+                    'name': _("Fallback for default gravatar"),
+                    'description': _(
+                        "Select which avatar to use when user has no "
+                        "gravatar associated with his e-mail address."
                     ),
                     ),
+                    'value': 'dynamic',
+                    'form_field': 'select',
+                    'field_extra': {
+                        'choices': [
+                            ('dynamic', _("Individual")),
+                            ('gallery', _("Random avatar from gallery")),
+                        ],
+                    },
                 },
                 },
-            },
-            {
-                'setting': 'avatar_upload_limit',
-                'name': _("Maximum size of uploaded avatar"),
-                'description': _("Enter maximum allowed file size "
-                                 "(in KB) for avatar uploads"),
-                'python_type': 'int',
-                'value': 1536,
-                'field_extra': {
-                    'min_value': 0,
+                {
+                    'setting': 'avatar_upload_limit',
+                    'name': _("Maximum size of uploaded avatar"),
+                    'description': _("Enter maximum allowed file size (in KB) for avatar uploads."),
+                    'python_type': 'int',
+                    'value': 1536,
+                    'field_extra': {
+                        'min_value': 0,
+                    },
+                    'is_public': True,
                 },
                 },
-                'is_public': True,
-            },
-            {
-                'setting': 'signature_length_max',
-                'name': _("Maximum length"),
-                'legend': _("Signatures"),
-                'description': _("Maximum allowed signature length."),
-                'python_type': 'int',
-                'value': 256,
-                'field_extra': {
-                    'min_value': 10,
-                    'max_value': 5000,
+                {
+                    'setting': 'signature_length_max',
+                    'name': _("Maximum length"),
+                    'legend': _("Signatures"),
+                    'description': _("Maximum allowed signature length."),
+                    'python_type': 'int',
+                    'value': 256,
+                    'field_extra': {
+                        'min_value': 10,
+                        'max_value': 5000,
+                    },
+                    'is_public': True,
                 },
                 },
-                'is_public': True,
-            },
-            {
-                'setting': 'subscribe_start',
-                'name': _("Started threads"),
-                'legend': _("Default subscriptions settings"),
-                'value': 'watch_email',
-                'form_field': 'select',
-                'field_extra': {
-                    'choices': (
-                        ('no', _("Don't watch")),
-                        ('watch', _("Put on watched threads list")),
-                        ('watch_email', _("Put on watched threads "
-                                          "list and e-mail user when "
-                                          "somebody replies")),
-                    ),
+                {
+                    'setting': 'subscribe_start',
+                    'name': _("Started threads"),
+                    'legend': _("Default subscriptions settings"),
+                    'value': 'watch_email',
+                    'form_field': 'select',
+                    'field_extra': {
+                        'choices': [
+                            ('no', _("Don't watch")),
+                            ('watch', _("Put on watched threads list")),
+                            (
+                                'watch_email', _(
+                                    "Put on watched threads "
+                                    "list and e-mail user when "
+                                    "somebody replies"
+                                )
+                            ),
+                        ],
+                    },
                 },
                 },
-            },
-            {
-                'setting': 'subscribe_reply',
-                'name': _("Replied threads"),
-                'value': 'watch_email',
-                'form_field': 'select',
-                'field_extra': {
-                    'choices': (
-                        ('no', _("Don't watch")),
-                        ('watch', _("Put on watched threads list")),
-                        ('watch_email', _("Put on watched threads "
-                                          "list and e-mail user when "
-                                          "somebody replies")),
-                    ),
+                {
+                    'setting': 'subscribe_reply',
+                    'name': _("Replied threads"),
+                    'value': 'watch_email',
+                    'form_field': 'select',
+                    'field_extra': {
+                        'choices': [
+                            ('no', _("Don't watch")),
+                            ('watch', _("Put on watched threads list")),
+                            (
+                                'watch_email', _(
+                                    "Put on watched threads "
+                                    "list and e-mail user when "
+                                    "somebody replies"
+                                )
+                            ),
+                        ],
+                    },
                 },
                 },
-            },
-        )
-    })
+            ],
+        }
+    )
 
 
-    migrate_settings_group(apps,{
-        'key': 'captcha',
-        'name': _("CAPTCHA"),
-        'description': _("Those settings allow you to combat automatic "
-                         "registrations on your forum."),
-        'settings': (
-            {
-                'setting': 'captcha_type',
-                'name': _("Select CAPTCHA type"),
-                'legend': _("CAPTCHA type"),
-                'value': 'no',
-                'form_field': 'select',
-                'field_extra': {
-                    'choices': (
-                        ('no', _("No CAPTCHA")),
-                        ('re', _("reCaptcha")),
-                        ('qa', _("Question and answer")),
-                    ),
+    migrate_settings_group(
+        apps, {
+            'key': 'captcha',
+            'name': _("CAPTCHA"),
+            'description': _("Those settings allow you to combat automatic registrations on your forum."),
+            'settings': [
+                {
+                    'setting': 'captcha_type',
+                    'name': _("Select CAPTCHA type"),
+                    'legend': _("CAPTCHA type"),
+                    'value': 'no',
+                    'form_field': 'select',
+                    'field_extra': {
+                        'choices': [
+                            ('no', _("No CAPTCHA")),
+                            ('re', _("reCaptcha")),
+                            ('qa', _("Question and answer")),
+                        ],
+                    },
+                    'is_public': True,
                 },
                 },
-                'is_public': True,
-            },
-            {
-                'setting': 'recaptcha_site_key',
-                'name': _("Site key"),
-                'legend': _("reCAPTCHA"),
-                'value': '',
-                'field_extra': {
-                    'required': False,
-                    'max_length': 100,
+                {
+                    'setting': 'recaptcha_site_key',
+                    'name': _("Site key"),
+                    'legend': _("reCAPTCHA"),
+                    'value': '',
+                    'field_extra': {
+                        'required': False,
+                        'max_length': 100,
+                    },
+                    'is_public': True,
                 },
                 },
-                'is_public': True,
-            },
-            {
-                'setting': 'recaptcha_secret_key',
-                'name': _("Secret key"),
-                'value': '',
-                'field_extra': {
-                    'required': False,
-                    'max_length': 100,
+                {
+                    'setting': 'recaptcha_secret_key',
+                    'name': _("Secret key"),
+                    'value': '',
+                    'field_extra': {
+                        'required': False,
+                        'max_length': 100,
+                    },
                 },
                 },
-            },
-            {
-                'setting': 'qa_question',
-                'name': _("Test question"),
-                'legend': _("Question and answer"),
-                'value': '',
-                'field_extra': {
-                    'required': False,
-                    'max_length': 250,
+                {
+                    'setting': 'qa_question',
+                    'name': _("Test question"),
+                    'legend': _("Question and answer"),
+                    'value': '',
+                    'field_extra': {
+                        'required': False,
+                        'max_length': 250,
+                    },
                 },
                 },
-            },
-            {
-                'setting': 'qa_help_text',
-                'name': _("Question help text"),
-                'value': '',
-                'field_extra': {
-                    'required': False,
-                    'max_length': 250,
+                {
+                    'setting': 'qa_help_text',
+                    'name': _("Question help text"),
+                    'value': '',
+                    'field_extra': {
+                        'required': False,
+                        'max_length': 250,
+                    },
                 },
                 },
-            },
-            {
-                'setting': 'qa_answers',
-                'name': _("Valid answers"),
-                'description': _("Enter each answer in new line. "
-                                 "Answers are case-insensitive."),
-                'value': '',
-                'form_field': 'textarea',
-                'field_extra': {
-                    'rows': 4,
-                    'required': False,
-                    'max_length': 250,
+                {
+                    'setting': 'qa_answers',
+                    'name': _("Valid answers"),
+                    'description': _("Enter each answer in new line. Answers are case-insensitive."),
+                    'value': '',
+                    'form_field': 'textarea',
+                    'field_extra': {
+                        'rows': 4,
+                        'required': False,
+                        'max_length': 250,
+                    },
                 },
                 },
-            },
-        )
-    })
+            ],
+        }
+    )
 
 
 
 
 class Migration(migrations.Migration):
 class Migration(migrations.Migration):

+ 1 - 1
misago/users/migrations/0003_bans_version_tracker.py

@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 # -*- coding: utf-8 -*-
 from __future__ import unicode_literals
 from __future__ import unicode_literals
 
 
-from django.db import migrations, models
+from django.db import migrations
 
 
 from misago.core.migrationutils import cachebuster_register_cache
 from misago.core.migrationutils import cachebuster_register_cache
 from misago.users.constants import BANS_CACHEBUSTER
 from misago.users.constants import BANS_CACHEBUSTER

+ 2 - 5
misago/users/migrations/0004_default_ranks.py

@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 # -*- coding: utf-8 -*-
 from __future__ import unicode_literals
 from __future__ import unicode_literals
 
 
-from django.db import migrations, models
+from django.db import migrations
 from django.utils.translation import ugettext as _
 from django.utils.translation import ugettext as _
 
 
 from misago.core.utils import slugify
 from misago.core.utils import slugify
@@ -20,10 +20,7 @@ def create_default_ranks(apps, schema_editor):
     )
     )
 
 
     member = Rank.objects.create(
     member = Rank.objects.create(
-        name=_("Members"),
-        slug=slugify(_("Members")),
-        is_default=True,
-        order=1
+        name=_("Members"), slug=slugify(_("Members")), is_default=True, order=1
     )
     )
 
 
     Role = apps.get_model('misago_acl', 'Role')
     Role = apps.get_model('misago_acl', 'Role')

+ 8 - 1
misago/users/migrations/0005_dj_19_update.py

@@ -28,6 +28,13 @@ class Migration(migrations.Migration):
         migrations.AlterField(
         migrations.AlterField(
             model_name='user',
             model_name='user',
             name='groups',
             name='groups',
-            field=models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups'),
+            field=models.ManyToManyField(
+                blank=True,
+                help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.',
+                related_name='user_set',
+                related_query_name='user',
+                to='auth.Group',
+                verbose_name='groups'
+            ),
         ),
         ),
     ]
     ]

+ 145 - 131
misago/users/migrations/0006_update_settings.py

@@ -11,148 +11,162 @@ _ = lambda x: x
 
 
 
 
 def update_users_settings(apps, schema_editor):
 def update_users_settings(apps, schema_editor):
-    migrate_settings_group(apps,{
-        'key': 'users',
-        'name': _("Users"),
-        'description': _("Those settings control user accounts default behaviour and features availability."),
-        'settings': (
-            {
-                'setting': 'account_activation',
-                'name': _("New accounts activation"),
-                'legend': _("New accounts"),
-                'value': 'none',
-                'form_field': 'select',
-                'field_extra': {
-                    'choices': (
-                        ('none', _("No activation required")),
-                        ('user', _("Activation token sent to User")),
-                        ('admin', _("Activation by administrator")),
-                        ('closed', _("Don't allow new registrations"))
-                    )
+    migrate_settings_group(
+        apps, {
+            'key': 'users',
+            'name': _("Users"),
+            'description': _(
+                "Those settings control user accounts default behaviour and features availability."
+            ),
+            'settings': [
+                {
+                    'setting': 'account_activation',
+                    'name': _("New accounts activation"),
+                    'legend': _("New accounts"),
+                    'value': 'none',
+                    'form_field': 'select',
+                    'field_extra': {
+                        'choices': [
+                            ('none', _("No activation required")),
+                            ('user', _("Activation token sent to User")),
+                            ('admin', _("Activation by administrator")),
+                            ('closed', _("Don't allow new registrations")),
+                        ],
+                    },
+                    'is_public': True,
                 },
                 },
-                'is_public': True,
-            },
-            {
-                'setting': 'username_length_min',
-                'name': _("Minimum length"),
-                'description': _("Minimum allowed username length."),
-                'legend': _("User names"),
-                'python_type': 'int',
-                'default_value': 3,
-                'field_extra': {
-                    'min_value': 2,
-                    'max_value': 20,
+                {
+                    'setting': 'username_length_min',
+                    'name': _("Minimum length"),
+                    'description': _("Minimum allowed username length."),
+                    'legend': _("User names"),
+                    'python_type': 'int',
+                    'default_value': 3,
+                    'field_extra': {
+                        'min_value': 2,
+                        'max_value': 20,
+                    },
                 },
                 },
-            },
-            {
-                'setting': 'username_length_max',
-                'name': _("Maximum length"),
-                'description': _("Maximum allowed username length."),
-                'python_type': 'int',
-                'default_value': 14,
-                'field_extra': {
-                    'min_value': 2,
-                    'max_value': 20,
+                {
+                    'setting': 'username_length_max',
+                    'name': _("Maximum length"),
+                    'description': _("Maximum allowed username length."),
+                    'python_type': 'int',
+                    'default_value': 14,
+                    'field_extra': {
+                        'min_value': 2,
+                        'max_value': 20,
+                    },
                 },
                 },
-            },
-            {
-                'setting': 'allow_custom_avatars',
-                'name': _("Allow custom avatars"),
-                'legend': _("Avatars"),
-                'description': _("Turning this option off will forbid "
-                                 "forum users from using avatars from "
-                                 "outside forums. Good for forums "
-                                 "adressed at young users."),
-                'python_type': 'bool',
-                'value': True,
-                'form_field': 'yesno',
-            },
-            {
-                'setting': 'default_avatar',
-                'name': _("Default avatar"),
-                'value': 'gravatar',
-                'form_field': 'select',
-                'field_extra': {
-                    'choices': (
-                        ('dynamic', _("Individual")),
-                        ('gravatar', _("Gravatar")),
-                        ('gallery', _("Random avatar from gallery")),
+                {
+                    'setting': 'allow_custom_avatars',
+                    'name': _("Allow custom avatars"),
+                    'legend': _("Avatars"),
+                    'description': _(
+                        "Turning this option off will forbid "
+                        "forum users from using avatars from "
+                        "outside forums. Good for forums "
+                        "adressed at young users."
                     ),
                     ),
+                    'python_type': 'bool',
+                    'value': True,
+                    'form_field': 'yesno',
                 },
                 },
-            },
-            {
-                'setting': 'default_gravatar_fallback',
-                'name': _("Fallback for default gravatar"),
-                'description': _("Select which avatar to use when user "
-                                 "has no gravatar associated with his "
-                                 "e-mail address."),
-                'value': 'dynamic',
-                'form_field': 'select',
-                'field_extra': {
-                    'choices': (
-                        ('dynamic', _("Individual")),
-                        ('gallery', _("Random avatar from gallery")),
+                {
+                    'setting': 'default_avatar',
+                    'name': _("Default avatar"),
+                    'value': 'gravatar',
+                    'form_field': 'select',
+                    'field_extra': {
+                        'choices': [
+                            ('dynamic', _("Individual")),
+                            ('gravatar', _("Gravatar")),
+                            ('gallery', _("Random avatar from gallery")),
+                        ],
+                    },
+                },
+                {
+                    'setting': 'default_gravatar_fallback',
+                    'name': _("Fallback for default gravatar"),
+                    'description': _(
+                        "Select which avatar to use when user has no "
+                        "gravatar associated with his e-mail address."
                     ),
                     ),
+                    'value': 'dynamic',
+                    'form_field': 'select',
+                    'field_extra': {
+                        'choices': [
+                            ('dynamic', _("Individual")),
+                            ('gallery', _("Random avatar from gallery")),
+                        ],
+                    },
                 },
                 },
-            },
-            {
-                'setting': 'avatar_upload_limit',
-                'name': _("Maximum size of uploaded avatar"),
-                'description': _("Enter maximum allowed file size "
-                                 "(in KB) for avatar uploads"),
-                'python_type': 'int',
-                'default_value': 1536,
-                'field_extra': {
-                    'min_value': 0,
+                {
+                    'setting': 'avatar_upload_limit',
+                    'name': _("Maximum size of uploaded avatar"),
+                    'description': _("Enter maximum allowed file size (in KB) for avatar uploads."),
+                    'python_type': 'int',
+                    'default_value': 1536,
+                    'field_extra': {
+                        'min_value': 0,
+                    },
+                    'is_public': True,
                 },
                 },
-                'is_public': True,
-            },
-            {
-                'setting': 'signature_length_max',
-                'name': _("Maximum length"),
-                'legend': _("Signatures"),
-                'description': _("Maximum allowed signature length."),
-                'python_type': 'int',
-                'default_value': 256,
-                'field_extra': {
-                    'min_value': 10,
-                    'max_value': 5000,
+                {
+                    'setting': 'signature_length_max',
+                    'name': _("Maximum length"),
+                    'legend': _("Signatures"),
+                    'description': _("Maximum allowed signature length."),
+                    'python_type': 'int',
+                    'default_value': 256,
+                    'field_extra': {
+                        'min_value': 10,
+                        'max_value': 5000,
+                    },
+                    'is_public': True,
                 },
                 },
-                'is_public': True,
-            },
-            {
-                'setting': 'subscribe_start',
-                'name': _("Started threads"),
-                'legend': _("Default subscriptions settings"),
-                'value': 'watch_email',
-                'form_field': 'select',
-                'field_extra': {
-                    'choices': (
-                        ('no', _("Don't watch")),
-                        ('watch', _("Put on watched threads list")),
-                        ('watch_email', _("Put on watched threads "
-                                          "list and e-mail user when "
-                                          "somebody replies")),
-                    ),
+                {
+                    'setting': 'subscribe_start',
+                    'name': _("Started threads"),
+                    'legend': _("Default subscriptions settings"),
+                    'value': 'watch_email',
+                    'form_field': 'select',
+                    'field_extra': {
+                        'choices': [
+                            ('no', _("Don't watch")),
+                            ('watch', _("Put on watched threads list")),
+                            (
+                                'watch_email', _(
+                                    "Put on watched threads "
+                                    "list and e-mail user when "
+                                    "somebody replies"
+                                )
+                            ),
+                        ],
+                    },
                 },
                 },
-            },
-            {
-                'setting': 'subscribe_reply',
-                'name': _("Replied threads"),
-                'value': 'watch_email',
-                'form_field': 'select',
-                'field_extra': {
-                    'choices': (
-                        ('no', _("Don't watch")),
-                        ('watch', _("Put on watched threads list")),
-                        ('watch_email', _("Put on watched threads "
-                                          "list and e-mail user when "
-                                          "somebody replies")),
-                    ),
+                {
+                    'setting': 'subscribe_reply',
+                    'name': _("Replied threads"),
+                    'value': 'watch_email',
+                    'form_field': 'select',
+                    'field_extra': {
+                        'choices': [
+                            ('no', _("Don't watch")),
+                            ('watch', _("Put on watched threads list")),
+                            (
+                                'watch_email', _(
+                                    "Put on watched threads "
+                                    "list and e-mail user when "
+                                    "somebody replies"
+                                )
+                            ),
+                        ],
+                    },
                 },
                 },
-            },
-        )
-    })
+            ],
+        }
+    )
 
 
     delete_settings_cache()
     delete_settings_cache()
 
 

+ 9 - 3
misago/users/migrations/0007_auto_20170219_1639.py

@@ -15,16 +15,22 @@ class Migration(migrations.Migration):
         migrations.AlterField(
         migrations.AlterField(
             model_name='user',
             model_name='user',
             name='limits_private_thread_invites_to',
             name='limits_private_thread_invites_to',
-            field=models.PositiveIntegerField(choices=[(0, 'Everybody'), (1, 'Users I follow'), (2, 'Nobody')], default=0),
+            field=models.PositiveIntegerField(
+                choices=[(0, 'Everybody'), (1, 'Users I follow'), (2, 'Nobody')], default=0
+            ),
         ),
         ),
         migrations.AlterField(
         migrations.AlterField(
             model_name='user',
             model_name='user',
             name='subscribe_to_replied_threads',
             name='subscribe_to_replied_threads',
-            field=models.PositiveIntegerField(choices=[(0, 'No'), (1, 'Notify'), (2, 'Notify with e-mail')], default=0),
+            field=models.PositiveIntegerField(
+                choices=[(0, 'No'), (1, 'Notify'), (2, 'Notify with e-mail')], default=0
+            ),
         ),
         ),
         migrations.AlterField(
         migrations.AlterField(
             model_name='user',
             model_name='user',
             name='subscribe_to_started_threads',
             name='subscribe_to_started_threads',
-            field=models.PositiveIntegerField(choices=[(0, 'No'), (1, 'Notify'), (2, 'Notify with e-mail')], default=0),
+            field=models.PositiveIntegerField(
+                choices=[(0, 'No'), (1, 'Notify'), (2, 'Notify with e-mail')], default=0
+            ),
         ),
         ),
     ]
     ]

+ 1 - 4
misago/users/models/avatar.py

@@ -5,10 +5,7 @@ from misago.users.avatars import store
 
 
 
 
 class Avatar(models.Model):
 class Avatar(models.Model):
-    user = models.ForeignKey(
-        settings.AUTH_USER_MODEL,
-        on_delete=models.CASCADE
-    )
+    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
     size = models.PositiveIntegerField(default=0)
     size = models.PositiveIntegerField(default=0)
     image = models.ImageField(max_length=255, upload_to=store.upload_to)
     image = models.ImageField(max_length=255, upload_to=store.upload_to)
 
 

+ 11 - 9
misago/users/models/ban.py

@@ -43,11 +43,9 @@ class BansManager(models.Manager):
         for ban in queryset.order_by('-id').iterator():
         for ban in queryset.order_by('-id').iterator():
             if ban.is_expired:
             if ban.is_expired:
                 continue
                 continue
-            elif (ban.check_type == self.model.USERNAME and username and
-                    ban.check_value(username)):
+            elif (ban.check_type == self.model.USERNAME and username and ban.check_value(username)):
                 return ban
                 return ban
-            elif (ban.check_type == self.model.EMAIL and email and
-                    ban.check_value(email)):
+            elif (ban.check_type == self.model.EMAIL and email and ban.check_value(email)):
                 return ban
                 return ban
             elif ban.check_type == self.model.IP and ip and ban.check_value(ip):
             elif ban.check_type == self.model.IP and ip and ban.check_value(ip):
                 return ban
                 return ban
@@ -60,11 +58,11 @@ class Ban(models.Model):
     EMAIL = 1
     EMAIL = 1
     IP = 2
     IP = 2
 
 
-    CHOICES = (
+    CHOICES = [
         (USERNAME, _('Username')),
         (USERNAME, _('Username')),
         (EMAIL, _('E-mail address')),
         (EMAIL, _('E-mail address')),
         (IP, _('IP address')),
         (IP, _('IP address')),
-    )
+    ]
 
 
     check_type = models.PositiveIntegerField(default=USERNAME, db_index=True)
     check_type = models.PositiveIntegerField(default=USERNAME, db_index=True)
     banned_value = models.CharField(max_length=255, db_index=True)
     banned_value = models.CharField(max_length=255, db_index=True)
@@ -112,7 +110,11 @@ class Ban(models.Model):
 
 
 
 
 class BanCache(models.Model):
 class BanCache(models.Model):
-    user = models.OneToOneField(settings.AUTH_USER_MODEL, primary_key=True, related_name='ban_cache')
+    user = models.OneToOneField(
+        settings.AUTH_USER_MODEL,
+        primary_key=True,
+        related_name='ban_cache',
+    )
     ban = models.ForeignKey(Ban, null=True, blank=True, on_delete=models.SET_NULL)
     ban = models.ForeignKey(Ban, null=True, blank=True, on_delete=models.SET_NULL)
     bans_version = models.PositiveIntegerField(default=0)
     bans_version = models.PositiveIntegerField(default=0)
     user_message = models.TextField(null=True, blank=True)
     user_message = models.TextField(null=True, blank=True)
@@ -123,7 +125,7 @@ class BanCache(models.Model):
         try:
         try:
             super(BanCache, self).save(*args, **kwargs)
             super(BanCache, self).save(*args, **kwargs)
         except IntegrityError:
         except IntegrityError:
-            pass # first come is first serve with ban cache
+            pass  # first come is first serve with ban cache
 
 
     def get_serialized_message(self):
     def get_serialized_message(self):
         from misago.users.serializers import BanMessageSerializer
         from misago.users.serializers import BanMessageSerializer
@@ -132,7 +134,7 @@ class BanCache(models.Model):
             check_type=Ban.USERNAME,
             check_type=Ban.USERNAME,
             user_message=self.user_message,
             user_message=self.user_message,
             staff_message=self.staff_message,
             staff_message=self.staff_message,
-            expires_on=self.expires_on
+            expires_on=self.expires_on,
         )
         )
         return BanMessageSerializer(temp_ban).data
         return BanMessageSerializer(temp_ban).data
 
 

+ 58 - 64
misago/users/models/user.py

@@ -7,7 +7,6 @@ from django.contrib.auth.password_validation import validate_password
 from django.contrib.postgres.fields import JSONField
 from django.contrib.postgres.fields import JSONField
 from django.core.mail import send_mail
 from django.core.mail import send_mail
 from django.db import IntegrityError, models, transaction
 from django.db import IntegrityError, models, transaction
-from django.dispatch import receiver
 from django.urls import reverse
 from django.urls import reverse
 from django.utils import timezone
 from django.utils import timezone
 from django.utils.translation import ugettext_lazy as _
 from django.utils.translation import ugettext_lazy as _
@@ -17,7 +16,7 @@ from misago.acl.models import Role
 from misago.conf import settings
 from misago.conf import settings
 from misago.core.utils import slugify
 from misago.core.utils import slugify
 from misago.users import avatars
 from misago.users import avatars
-from misago.users.signatures import is_user_signature_valid, make_signature_checksum
+from misago.users.signatures import is_user_signature_valid
 from misago.users.utils import hash_email
 from misago.users.utils import hash_email
 
 
 from .rank import Rank
 from .rank import Rank
@@ -25,7 +24,9 @@ from .rank import Rank
 
 
 class UserManager(BaseUserManager):
 class UserManager(BaseUserManager):
     @transaction.atomic
     @transaction.atomic
-    def create_user(self, username, email, password=None, set_default_avatar=False, **extra_fields):
+    def create_user(
+            self, username, email, password=None, set_default_avatar=False, **extra_fields
+    ):
         from misago.users.validators import validate_email, validate_username
         from misago.users.validators import validate_email, validate_username
 
 
         email = self.normalize_email(email)
         email = self.normalize_email(email)
@@ -40,9 +41,9 @@ class UserManager(BaseUserManager):
             extra_fields['joined_from_ip'] = '127.0.0.1'
             extra_fields['joined_from_ip'] = '127.0.0.1'
 
 
         WATCH_DICT = {
         WATCH_DICT = {
-            'no':  self.model.SUBSCRIBE_NONE,
-            'watch':  self.model.SUBSCRIBE_NOTIFY,
-            'watch_email':  self.model.SUBSCRIBE_ALL,
+            'no': self.model.SUBSCRIBE_NONE,
+            'watch': self.model.SUBSCRIBE_NOTIFY,
+            'watch_email': self.model.SUBSCRIBE_ALL,
         }
         }
 
 
         if not 'subscribe_to_started_threads' in extra_fields:
         if not 'subscribe_to_started_threads' in extra_fields:
@@ -53,17 +54,10 @@ class UserManager(BaseUserManager):
             new_value = WATCH_DICT[settings.subscribe_reply]
             new_value = WATCH_DICT[settings.subscribe_reply]
             extra_fields['subscribe_to_replied_threads'] = new_value
             extra_fields['subscribe_to_replied_threads'] = new_value
 
 
-        extra_fields.update({
-            'is_staff': False,
-            'is_superuser': False
-        })
+        extra_fields.update({'is_staff': False, 'is_superuser': False})
 
 
         now = timezone.now()
         now = timezone.now()
-        user = self.model(
-            last_login=now,
-            joined_on=now,
-            **extra_fields
-        )
+        user = self.model(last_login=now, joined_on=now, **extra_fields)
 
 
         user.set_username(username)
         user.set_username(username)
         user.set_email(email)
         user.set_email(email)
@@ -79,8 +73,9 @@ class UserManager(BaseUserManager):
         user.save(using=self._db)
         user.save(using=self._db)
 
 
         if set_default_avatar:
         if set_default_avatar:
-            avatars.set_default_avatar(user, settings.default_avatar,
-                                       settings.default_gravatar_fallback)
+            avatars.set_default_avatar(
+                user, settings.default_avatar, settings.default_gravatar_fallback
+            )
         else:
         else:
             # just for test purposes
             # just for test purposes
             user.avatars = [{'size': 400, 'url': '/placekitten.com/400/400'}]
             user.avatars = [{'size': 400, 'url': '/placekitten.com/400/400'}]
@@ -102,9 +97,10 @@ class UserManager(BaseUserManager):
         return user
         return user
 
 
     @transaction.atomic
     @transaction.atomic
-    def create_superuser(self, username, email, password,
-                         set_default_avatar=False):
-        user = self.create_user(username, email,
+    def create_superuser(self, username, email, password, set_default_avatar=False):
+        user = self.create_user(
+            username,
+            email,
             password=password,
             password=password,
             set_default_avatar=set_default_avatar,
             set_default_avatar=set_default_avatar,
         )
         )
@@ -143,39 +139,36 @@ class User(AbstractBaseUser, PermissionsMixin):
     SUBSCRIBE_NOTIFY = 1
     SUBSCRIBE_NOTIFY = 1
     SUBSCRIBE_ALL = 2
     SUBSCRIBE_ALL = 2
 
 
-    SUBSCRIBE_CHOICES = (
+    SUBSCRIBE_CHOICES = [
         (SUBSCRIBE_NONE, _("No")),
         (SUBSCRIBE_NONE, _("No")),
         (SUBSCRIBE_NOTIFY, _("Notify")),
         (SUBSCRIBE_NOTIFY, _("Notify")),
-        (SUBSCRIBE_ALL, _("Notify with e-mail"))
-    )
+        (SUBSCRIBE_ALL, _("Notify with e-mail")),
+    ]
 
 
     LIMIT_INVITES_TO_NONE = 0
     LIMIT_INVITES_TO_NONE = 0
     LIMIT_INVITES_TO_FOLLOWED = 1
     LIMIT_INVITES_TO_FOLLOWED = 1
     LIMIT_INVITES_TO_NOBODY = 2
     LIMIT_INVITES_TO_NOBODY = 2
 
 
-    LIMIT_INVITES_TO_CHOICES = (
+    LIMIT_INVITES_TO_CHOICES = [
         (LIMIT_INVITES_TO_NONE, _("Everybody")),
         (LIMIT_INVITES_TO_NONE, _("Everybody")),
         (LIMIT_INVITES_TO_FOLLOWED, _("Users I follow")),
         (LIMIT_INVITES_TO_FOLLOWED, _("Users I follow")),
         (LIMIT_INVITES_TO_NOBODY, _("Nobody")),
         (LIMIT_INVITES_TO_NOBODY, _("Nobody")),
-    )
-
-    """
-    Note that "username" field is purely for shows.
-    When searching users by their names, always use lowercased string
-    and slug field instead that is normalized around DB engines
-    differences in case handling.
-    """
+    ]
+    # Note that "username" field is purely for shows.
+    # When searching users by their names, always use lowercased string
+    # and slug field instead that is normalized around DB engines
+    # differences in case handling.
     username = models.CharField(max_length=30)
     username = models.CharField(max_length=30)
     slug = models.CharField(max_length=30, unique=True)
     slug = models.CharField(max_length=30, unique=True)
-    """
-    Misago stores user email in two fields:
-    "email" holds normalized email address
-    "email_hash" is lowercase hash of email address used to identify account
-    as well as enforcing on database level that no more than one user can be
-    using one email address
-    """
+
+    # Misago stores user email in two fields:
+    # "email" holds normalized email address
+    # "email_hash" is lowercase hash of email address used to identify account
+    # as well as enforcing on database level that no more than one user can be
+    # using one email address
     email = models.EmailField(max_length=255, db_index=True)
     email = models.EmailField(max_length=255, db_index=True)
     email_hash = models.CharField(max_length=32, unique=True)
     email_hash = models.CharField(max_length=32, unique=True)
+
     joined_on = models.DateTimeField(_('joined on'), default=timezone.now)
     joined_on = models.DateTimeField(_('joined on'), default=timezone.now)
     joined_from_ip = models.GenericIPAddressField()
     joined_from_ip = models.GenericIPAddressField()
     last_ip = models.GenericIPAddressField(null=True, blank=True)
     last_ip = models.GenericIPAddressField(null=True, blank=True)
@@ -185,7 +178,8 @@ class User(AbstractBaseUser, PermissionsMixin):
     title = models.CharField(max_length=255, null=True, blank=True)
     title = models.CharField(max_length=255, null=True, blank=True)
     requires_activation = models.PositiveIntegerField(default=ACTIVATION_NONE)
     requires_activation = models.PositiveIntegerField(default=ACTIVATION_NONE)
 
 
-    is_staff = models.BooleanField(_('staff status'),
+    is_staff = models.BooleanField(
+        _('staff status'),
         default=False,
         default=False,
         help_text=_('Designates whether the user can log into admin sites.'),
         help_text=_('Designates whether the user can log into admin sites.'),
     )
     )
@@ -208,13 +202,13 @@ class User(AbstractBaseUser, PermissionsMixin):
         max_length=255,
         max_length=255,
         upload_to=avatars.store.upload_to,
         upload_to=avatars.store.upload_to,
         null=True,
         null=True,
-        blank=True
+        blank=True,
     )
     )
     avatar_src = models.ImageField(
     avatar_src = models.ImageField(
         max_length=255,
         max_length=255,
         upload_to=avatars.store.upload_to,
         upload_to=avatars.store.upload_to,
         null=True,
         null=True,
-        blank=True
+        blank=True,
     )
     )
     avatar_crop = models.CharField(max_length=255, null=True, blank=True)
     avatar_crop = models.CharField(max_length=255, null=True, blank=True)
     avatars = JSONField(null=True, blank=True)
     avatars = JSONField(null=True, blank=True)
@@ -232,11 +226,13 @@ class User(AbstractBaseUser, PermissionsMixin):
     followers = models.PositiveIntegerField(default=0)
     followers = models.PositiveIntegerField(default=0)
     following = models.PositiveIntegerField(default=0)
     following = models.PositiveIntegerField(default=0)
 
 
-    follows = models.ManyToManyField('self',
+    follows = models.ManyToManyField(
+        'self',
         related_name='followed_by',
         related_name='followed_by',
         symmetrical=False,
         symmetrical=False,
     )
     )
-    blocks = models.ManyToManyField('self',
+    blocks = models.ManyToManyField(
+        'self',
         related_name='blocked_by',
         related_name='blocked_by',
         symmetrical=False,
         symmetrical=False,
     )
     )
@@ -250,11 +246,11 @@ class User(AbstractBaseUser, PermissionsMixin):
 
 
     subscribe_to_started_threads = models.PositiveIntegerField(
     subscribe_to_started_threads = models.PositiveIntegerField(
         default=SUBSCRIBE_NONE,
         default=SUBSCRIBE_NONE,
-        choices=SUBSCRIBE_CHOICES
+        choices=SUBSCRIBE_CHOICES,
     )
     )
     subscribe_to_replied_threads = models.PositiveIntegerField(
     subscribe_to_replied_threads = models.PositiveIntegerField(
         default=SUBSCRIBE_NONE,
         default=SUBSCRIBE_NONE,
-        choices=SUBSCRIBE_CHOICES
+        choices=SUBSCRIBE_CHOICES,
     )
     )
 
 
     threads = models.PositiveIntegerField(default=0)
     threads = models.PositiveIntegerField(default=0)
@@ -272,7 +268,7 @@ class User(AbstractBaseUser, PermissionsMixin):
         self.email = self.__class__.objects.normalize_email(self.email)
         self.email = self.__class__.objects.normalize_email(self.email)
 
 
     def lock(self):
     def lock(self):
-        """Locks user in DB"""
+        """locks user in DB, shortcut for locking user model in views"""
         return User.objects.select_for_update().get(pk=self.pk)
         return User.objects.select_for_update().get(pk=self.pk)
 
 
     def delete(self, *args, **kwargs):
     def delete(self, *args, **kwargs):
@@ -331,15 +327,15 @@ class User(AbstractBaseUser, PermissionsMixin):
         return is_user_signature_valid(self)
         return is_user_signature_valid(self)
 
 
     def get_absolute_url(self):
     def get_absolute_url(self):
-        return reverse('misago:user', kwargs={
-            'slug': self.slug,
-            'pk': self.pk,
-        })
+        return reverse(
+            'misago:user', kwargs={
+                'slug': self.slug,
+                'pk': self.pk,
+            }
+        )
 
 
     def get_username(self):
     def get_username(self):
-        """
-        Dirty hack: return real username instead of normalized slug
-        """
+        """dirty hack: return real username instead of normalized slug"""
         return self.username
         return self.username
 
 
     def get_full_name(self):
     def get_full_name(self):
@@ -357,8 +353,7 @@ class User(AbstractBaseUser, PermissionsMixin):
 
 
             if self.pk:
             if self.pk:
                 changed_by = changed_by or self
                 changed_by = changed_by or self
-                self.record_name_change(
-                    changed_by, new_username, old_username)
+                self.record_name_change(changed_by, new_username, old_username)
 
 
                 from misago.users.signals import username_changed
                 from misago.users.signals import username_changed
                 username_changed.send(sender=self)
                 username_changed.send(sender=self)
@@ -407,9 +402,7 @@ class User(AbstractBaseUser, PermissionsMixin):
         self.acl_key = md5(','.join(roles_pks).encode()).hexdigest()[:12]
         self.acl_key = md5(','.join(roles_pks).encode()).hexdigest()[:12]
 
 
     def email_user(self, subject, message, from_email=None, **kwargs):
     def email_user(self, subject, message, from_email=None, **kwargs):
-        """
-        Sends an email to this User.
-        """
+        """sends an email to this user (for compat with Django)"""
         send_mail(subject, message, from_email, [self.email], **kwargs)
         send_mail(subject, message, from_email, [self.email], **kwargs)
 
 
     def is_following(self, user):
     def is_following(self, user):
@@ -440,14 +433,15 @@ class Online(models.Model):
         try:
         try:
             super(Online, self).save(*args, **kwargs)
             super(Online, self).save(*args, **kwargs)
         except IntegrityError:
         except IntegrityError:
-            pass # first come is first serve in online tracker
+            pass  # first come is first serve in online tracker
 
 
 
 
 class UsernameChange(models.Model):
 class UsernameChange(models.Model):
-    user = models.ForeignKey(settings.AUTH_USER_MODEL,
-        related_name='namechanges')
-    changed_by = models.ForeignKey(settings.AUTH_USER_MODEL,
-        null=True, blank=True,
+    user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='namechanges')
+    changed_by = models.ForeignKey(
+        settings.AUTH_USER_MODEL,
+        null=True,
+        blank=True,
         related_name='user_renames',
         related_name='user_renames',
         on_delete=models.SET_NULL,
         on_delete=models.SET_NULL,
     )
     )

+ 0 - 1
misago/users/online/utils.py

@@ -17,7 +17,6 @@ def get_user_status(viewer, user):
         'is_offline_hidden': False,
         'is_offline_hidden': False,
         'is_online': False,
         'is_online': False,
         'is_offline': False,
         'is_offline': False,
-
         'banned_until': None,
         'banned_until': None,
         'last_click': user.last_login or user.joined_on,
         'last_click': user.last_login or user.joined_on,
     }
     }

+ 14 - 15
misago/users/permissions/account.py

@@ -6,22 +6,19 @@ from misago.acl.models import Role
 from misago.core.forms import YesNoSwitch
 from misago.core.forms import YesNoSwitch
 
 
 
 
-"""
-Admin Permissions Form
-"""
 class PermissionsForm(forms.Form):
 class PermissionsForm(forms.Form):
     legend = _("Account settings")
     legend = _("Account settings")
 
 
     name_changes_allowed = forms.IntegerField(
     name_changes_allowed = forms.IntegerField(
-        label=_("Allowed username changes number"),
-        min_value=0,
-        initial=1
+        label=_("Allowed username changes number"), min_value=0, initial=1
     )
     )
     name_changes_expire = forms.IntegerField(
     name_changes_expire = forms.IntegerField(
         label=_("Don't count username changes older than"),
         label=_("Don't count username changes older than"),
-        help_text=_("Number of days since name change that makes "
-                    "that change no longer count to limit. Enter "
-                    "zero to make all changes count."),
+        help_text=_(
+            "Number of days since name change that makes "
+            "that change no longer count to limit. Enter "
+            "zero to make all changes count."
+        ),
         min_value=0,
         min_value=0,
         initial=0
         initial=0
     )
     )
@@ -30,8 +27,10 @@ class PermissionsForm(forms.Form):
     allow_signature_images = YesNoSwitch(label=_("Can put images in signature"))
     allow_signature_images = YesNoSwitch(label=_("Can put images in signature"))
     allow_signature_blocks = YesNoSwitch(
     allow_signature_blocks = YesNoSwitch(
         label=_("Can use text blocks in signature"),
         label=_("Can use text blocks in signature"),
-        help_text=_("Controls whether or not users can put quote, code, "
-                    "spoiler blocks and horizontal lines in signatures.")
+        help_text=_(
+            "Controls whether or not users can put quote, code, "
+            "spoiler blocks and horizontal lines in signatures."
+        )
     )
     )
 
 
 
 
@@ -42,9 +41,6 @@ def change_permissions_form(role):
         return None
         return None
 
 
 
 
-"""
-ACL Builder
-"""
 def build_acl(acl, roles, key_name):
 def build_acl(acl, roles, key_name):
     new_acl = {
     new_acl = {
         'name_changes_allowed': 0,
         'name_changes_allowed': 0,
@@ -56,7 +52,10 @@ def build_acl(acl, roles, key_name):
     }
     }
     new_acl.update(acl)
     new_acl.update(acl)
 
 
-    return algebra.sum_acls(new_acl, roles=roles, key=key_name,
+    return algebra.sum_acls(
+        new_acl,
+        roles=roles,
+        key=key_name,
         name_changes_allowed=algebra.greater,
         name_changes_allowed=algebra.greater,
         name_changes_expire=algebra.lower_non_zero,
         name_changes_expire=algebra.lower_non_zero,
         can_have_signature=algebra.greater,
         can_have_signature=algebra.greater,

+ 2 - 0
misago/users/permissions/decorators.py

@@ -15,6 +15,7 @@ def authenticated_only(f):
         else:
         else:
             messsage = _("You have to sig in to perform this action.")
             messsage = _("You have to sig in to perform this action.")
             raise PermissionDenied(messsage)
             raise PermissionDenied(messsage)
+
     return perm_decorator
     return perm_decorator
 
 
 
 
@@ -25,4 +26,5 @@ def anonymous_only(f):
         else:
         else:
             messsage = _("Only guests can perform this action.")
             messsage = _("Only guests can perform this action.")
             raise PermissionDenied(messsage)
             raise PermissionDenied(messsage)
+
     return perm_decorator
     return perm_decorator

+ 18 - 24
misago/users/permissions/delete.py

@@ -18,9 +18,6 @@ __all__ = [
 ]
 ]
 
 
 
 
-"""
-Admin Permissions Form
-"""
 class PermissionsForm(forms.Form):
 class PermissionsForm(forms.Form):
     legend = _("Deleting users")
     legend = _("Deleting users")
 
 
@@ -28,13 +25,13 @@ class PermissionsForm(forms.Form):
         label=_("Maximum age of deleted account (in days)"),
         label=_("Maximum age of deleted account (in days)"),
         help_text=_("Enter zero to disable this check."),
         help_text=_("Enter zero to disable this check."),
         min_value=0,
         min_value=0,
-        initial=0
+        initial=0,
     )
     )
     can_delete_users_with_less_posts_than = forms.IntegerField(
     can_delete_users_with_less_posts_than = forms.IntegerField(
         label=_("Maximum number of posts on deleted account"),
         label=_("Maximum number of posts on deleted account"),
         help_text=_("Enter zero to disable this check."),
         help_text=_("Enter zero to disable this check."),
         min_value=0,
         min_value=0,
-        initial=0
+        initial=0,
     )
     )
 
 
 
 
@@ -45,9 +42,6 @@ def change_permissions_form(role):
         return None
         return None
 
 
 
 
-"""
-ACL Builder
-"""
 def build_acl(acl, roles, key_name):
 def build_acl(acl, roles, key_name):
     new_acl = {
     new_acl = {
         'can_delete_users_newer_than': 0,
         'can_delete_users_newer_than': 0,
@@ -55,15 +49,15 @@ def build_acl(acl, roles, key_name):
     }
     }
     new_acl.update(acl)
     new_acl.update(acl)
 
 
-    return algebra.sum_acls(new_acl, roles=roles, key=key_name,
+    return algebra.sum_acls(
+        new_acl,
+        roles=roles,
+        key=key_name,
         can_delete_users_newer_than=algebra.greater,
         can_delete_users_newer_than=algebra.greater,
-        can_delete_users_with_less_posts_than=algebra.greater
+        can_delete_users_with_less_posts_than=algebra.greater,
     )
     )
 
 
 
 
-"""
-ACL's for targets
-"""
 def add_acl_to_user(user, target):
 def add_acl_to_user(user, target):
     target.acl['can_delete'] = can_delete_user(user, target)
     target.acl['can_delete'] = can_delete_user(user, target)
     if target.acl['can_delete']:
     if target.acl['can_delete']:
@@ -74,9 +68,6 @@ def register_with(registry):
     registry.acl_annotator(get_user_model(), add_acl_to_user)
     registry.acl_annotator(get_user_model(), add_acl_to_user)
 
 
 
 
-"""
-ACL tests
-"""
 def allow_delete_user(user, target):
 def allow_delete_user(user, target):
     newer_than = user.acl_cache['can_delete_users_newer_than']
     newer_than = user.acl_cache['can_delete_users_newer_than']
     less_posts_than = user.acl_cache['can_delete_users_with_less_posts_than']
     less_posts_than = user.acl_cache['can_delete_users_with_less_posts_than']
@@ -90,17 +81,20 @@ def allow_delete_user(user, target):
 
 
     if newer_than:
     if newer_than:
         if target.joined_on < timezone.now() - timedelta(days=newer_than):
         if target.joined_on < timezone.now() - timedelta(days=newer_than):
-            message = ungettext("You can't delete users that are "
-                                "members for more than %(days)s day.",
-                                "You can't delete users that are "
-                                "members for more than %(days)s days.",
-                                newer_than) % {'days': newer_than}
-            raise PermissionDenied(message)
+            message = ungettext(
+                "You can't delete users that are members for more than %(days)s day.",
+                "You can't delete users that are members for more than %(days)s days.",
+                newer_than,
+            )
+            raise PermissionDenied(message % {'days': newer_than})
     if less_posts_than:
     if less_posts_than:
         if target.posts > less_posts_than:
         if target.posts > less_posts_than:
             message = ungettext(
             message = ungettext(
                 "You can't delete users that made more than %(posts)s post.",
                 "You can't delete users that made more than %(posts)s post.",
                 "You can't delete users that made more than %(posts)s posts.",
                 "You can't delete users that made more than %(posts)s posts.",
-                less_posts_than) % {'posts': less_posts_than}
-            raise PermissionDenied(message)
+                less_posts_than,
+            )
+            raise PermissionDenied(message % {'posts': less_posts_than})
+
+
 can_delete_user = return_boolean(allow_delete_user)
 can_delete_user = return_boolean(allow_delete_user)

+ 21 - 22
misago/users/permissions/moderation.py

@@ -28,9 +28,6 @@ __all__ = [
 ]
 ]
 
 
 
 
-"""
-Admin Permissions Form
-"""
 class PermissionsForm(forms.Form):
 class PermissionsForm(forms.Form):
     legend = _("Users moderation")
     legend = _("Users moderation")
 
 
@@ -42,14 +39,14 @@ class PermissionsForm(forms.Form):
         label=_("Max length, in days, of imposed ban"),
         label=_("Max length, in days, of imposed ban"),
         help_text=_("Enter zero to let moderators impose permanent bans."),
         help_text=_("Enter zero to let moderators impose permanent bans."),
         min_value=0,
         min_value=0,
-        initial=0
+        initial=0,
     )
     )
     can_lift_bans = YesNoSwitch(label=_("Can lift bans"))
     can_lift_bans = YesNoSwitch(label=_("Can lift bans"))
     max_lifted_ban_length = forms.IntegerField(
     max_lifted_ban_length = forms.IntegerField(
         label=_("Max length, in days, of lifted ban"),
         label=_("Max length, in days, of lifted ban"),
         help_text=_("Enter zero to let moderators lift permanent bans."),
         help_text=_("Enter zero to let moderators lift permanent bans."),
         min_value=0,
         min_value=0,
-        initial=0
+        initial=0,
     )
     )
 
 
 
 
@@ -60,9 +57,6 @@ def change_permissions_form(role):
         return None
         return None
 
 
 
 
-"""
-ACL Builder
-"""
 def build_acl(acl, roles, key_name):
 def build_acl(acl, roles, key_name):
     new_acl = {
     new_acl = {
         'can_rename_users': 0,
         'can_rename_users': 0,
@@ -75,20 +69,20 @@ def build_acl(acl, roles, key_name):
     }
     }
     new_acl.update(acl)
     new_acl.update(acl)
 
 
-    return algebra.sum_acls(new_acl, roles=roles, key=key_name,
+    return algebra.sum_acls(
+        new_acl,
+        roles=roles,
+        key=key_name,
         can_rename_users=algebra.greater,
         can_rename_users=algebra.greater,
         can_moderate_avatars=algebra.greater,
         can_moderate_avatars=algebra.greater,
         can_moderate_signatures=algebra.greater,
         can_moderate_signatures=algebra.greater,
         can_ban_users=algebra.greater,
         can_ban_users=algebra.greater,
         max_ban_length=algebra.greater_or_zero,
         max_ban_length=algebra.greater_or_zero,
         can_lift_bans=algebra.greater,
         can_lift_bans=algebra.greater,
-        max_lifted_ban_length=algebra.greater_or_zero
+        max_lifted_ban_length=algebra.greater_or_zero,
     )
     )
 
 
 
 
-"""
-ACL's for targets
-"""
 def add_acl_to_user(user, target):
 def add_acl_to_user(user, target):
     target.acl['can_rename'] = can_rename_user(user, target)
     target.acl['can_rename'] = can_rename_user(user, target)
     target.acl['can_moderate_avatar'] = can_moderate_avatar(user, target)
     target.acl['can_moderate_avatar'] = can_moderate_avatar(user, target)
@@ -97,12 +91,12 @@ def add_acl_to_user(user, target):
     target.acl['max_ban_length'] = user.acl_cache['max_ban_length']
     target.acl['max_ban_length'] = user.acl_cache['max_ban_length']
     target.acl['can_lift_ban'] = can_lift_ban(user, target)
     target.acl['can_lift_ban'] = can_lift_ban(user, target)
 
 
-    mod_permissions = (
+    mod_permissions = [
         'can_rename',
         'can_rename',
         'can_moderate_avatar',
         'can_moderate_avatar',
         'can_moderate_signature',
         'can_moderate_signature',
         'can_ban',
         'can_ban',
-    )
+    ]
 
 
     for permission in mod_permissions:
     for permission in mod_permissions:
         if target.acl[permission]:
         if target.acl[permission]:
@@ -114,14 +108,13 @@ def register_with(registry):
     registry.acl_annotator(get_user_model(), add_acl_to_user)
     registry.acl_annotator(get_user_model(), add_acl_to_user)
 
 
 
 
-"""
-ACL tests
-"""
 def allow_rename_user(user, target):
 def allow_rename_user(user, target):
     if not user.acl_cache['can_rename_users']:
     if not user.acl_cache['can_rename_users']:
         raise PermissionDenied(_("You can't rename users."))
         raise PermissionDenied(_("You can't rename users."))
     if not user.is_superuser and (target.is_staff or target.is_superuser):
     if not user.is_superuser and (target.is_staff or target.is_superuser):
         raise PermissionDenied(_("You can't rename administrators."))
         raise PermissionDenied(_("You can't rename administrators."))
+
+
 can_rename_user = return_boolean(allow_rename_user)
 can_rename_user = return_boolean(allow_rename_user)
 
 
 
 
@@ -130,6 +123,8 @@ def allow_moderate_avatar(user, target):
         raise PermissionDenied(_("You can't moderate avatars."))
         raise PermissionDenied(_("You can't moderate avatars."))
     if not user.is_superuser and (target.is_staff or target.is_superuser):
     if not user.is_superuser and (target.is_staff or target.is_superuser):
         raise PermissionDenied(_("You can't moderate administrators avatars."))
         raise PermissionDenied(_("You can't moderate administrators avatars."))
+
+
 can_moderate_avatar = return_boolean(allow_moderate_avatar)
 can_moderate_avatar = return_boolean(allow_moderate_avatar)
 
 
 
 
@@ -139,6 +134,8 @@ def allow_moderate_signature(user, target):
     if not user.is_superuser and (target.is_staff or target.is_superuser):
     if not user.is_superuser and (target.is_staff or target.is_superuser):
         message = _("You can't moderate administrators signatures.")
         message = _("You can't moderate administrators signatures.")
         raise PermissionDenied(message)
         raise PermissionDenied(message)
+
+
 can_moderate_signature = return_boolean(allow_moderate_signature)
 can_moderate_signature = return_boolean(allow_moderate_signature)
 
 
 
 
@@ -147,6 +144,8 @@ def allow_ban_user(user, target):
         raise PermissionDenied(_("You can't ban users."))
         raise PermissionDenied(_("You can't ban users."))
     if target.is_staff or target.is_superuser:
     if target.is_staff or target.is_superuser:
         raise PermissionDenied(_("You can't ban administrators."))
         raise PermissionDenied(_("You can't ban administrators."))
+
+
 can_ban_user = return_boolean(allow_ban_user)
 can_ban_user = return_boolean(allow_ban_user)
 
 
 
 
@@ -162,8 +161,8 @@ def allow_lift_ban(user, target):
         if not ban.valid_until:
         if not ban.valid_until:
             raise PermissionDenied(_("You can't lift permanent bans."))
             raise PermissionDenied(_("You can't lift permanent bans."))
         elif ban.valid_until > lift_cutoff:
         elif ban.valid_until > lift_cutoff:
-            message = _("You can't lift bans that "
-                        "expire after %(expiration)s.")
-            message = message % {'expiration': format_date(lift_cutoff)}
-            raise PermissionDenied(message)
+            message = _("You can't lift bans that expire after %(expiration)s.")
+            raise PermissionDenied(message % {'expiration': format_date(lift_cutoff)})
+
+
 can_lift_ban = return_boolean(allow_lift_ban)
 can_lift_ban = return_boolean(allow_lift_ban)

+ 22 - 48
misago/users/permissions/profiles.py

@@ -22,21 +22,9 @@ __all__ = [
     'can_see_ban_details',
     'can_see_ban_details',
 ]
 ]
 
 
-
-"""
-Admin Permissions Form
-"""
-CAN_BROWSE_USERS_LIST = YesNoSwitch(
-    label=_("Can browse users list"),
-    initial=1
-)
-CAN_SEARCH_USERS = YesNoSwitch(
-    label=_("Can search user profiles"),
-    initial=1
-)
-CAN_SEE_USER_NAME_HISTORY = YesNoSwitch(
-    label=_("Can see other members name history")
-)
+CAN_BROWSE_USERS_LIST = YesNoSwitch(label=_("Can browse users list"), initial=1)
+CAN_SEARCH_USERS = YesNoSwitch(label=_("Can search user profiles"), initial=1)
+CAN_SEE_USER_NAME_HISTORY = YesNoSwitch(label=_("Can see other members name history"))
 CAN_SEE_DETAILS = YesNoSwitch(
 CAN_SEE_DETAILS = YesNoSwitch(
     label=_("Can see members bans details"),
     label=_("Can see members bans details"),
     help_text=_("Allows users with this permission to see user and staff ban messages.")
     help_text=_("Allows users with this permission to see user and staff ban messages.")
@@ -55,25 +43,13 @@ class LimitedPermissionsForm(forms.Form):
 class PermissionsForm(LimitedPermissionsForm):
 class PermissionsForm(LimitedPermissionsForm):
     can_browse_users_list = CAN_BROWSE_USERS_LIST
     can_browse_users_list = CAN_BROWSE_USERS_LIST
     can_search_users = CAN_SEARCH_USERS
     can_search_users = CAN_SEARCH_USERS
-    can_follow_users = YesNoSwitch(
-        label=_("Can follow other users"),
-        initial=1
-    )
-    can_be_blocked = YesNoSwitch(
-        label=_("Can be blocked by other users"),
-        initial=0
-    )
+    can_follow_users = YesNoSwitch(label=_("Can follow other users"), initial=1)
+    can_be_blocked = YesNoSwitch(label=_("Can be blocked by other users"), initial=0)
     can_see_users_name_history = CAN_SEE_USER_NAME_HISTORY
     can_see_users_name_history = CAN_SEE_USER_NAME_HISTORY
     can_see_ban_details = CAN_SEE_DETAILS
     can_see_ban_details = CAN_SEE_DETAILS
-    can_see_users_emails = YesNoSwitch(
-        label=_("Can see members e-mails")
-    )
-    can_see_users_ips = YesNoSwitch(
-        label=_("Can see members IPs")
-    )
-    can_see_hidden_users = YesNoSwitch(
-        label=_("Can see members that hide their presence")
-    )
+    can_see_users_emails = YesNoSwitch(label=_("Can see members e-mails"))
+    can_see_users_ips = YesNoSwitch(label=_("Can see members IPs"))
+    can_see_hidden_users = YesNoSwitch(label=_("Can see members that hide their presence"))
 
 
 
 
 def change_permissions_form(role):
 def change_permissions_form(role):
@@ -86,9 +62,6 @@ def change_permissions_form(role):
         return None
         return None
 
 
 
 
-"""
-ACL Builder
-"""
 def build_acl(acl, roles, key_name):
 def build_acl(acl, roles, key_name):
     new_acl = {
     new_acl = {
         'can_browse_users_list': 0,
         'can_browse_users_list': 0,
@@ -103,7 +76,10 @@ def build_acl(acl, roles, key_name):
     }
     }
     new_acl.update(acl)
     new_acl.update(acl)
 
 
-    return algebra.sum_acls(new_acl, roles=roles, key=key_name,
+    return algebra.sum_acls(
+        new_acl,
+        roles=roles,
+        key=key_name,
         can_browse_users_list=algebra.greater,
         can_browse_users_list=algebra.greater,
         can_search_users=algebra.greater,
         can_search_users=algebra.greater,
         can_follow_users=algebra.greater,
         can_follow_users=algebra.greater,
@@ -112,23 +88,16 @@ def build_acl(acl, roles, key_name):
         can_see_ban_details=algebra.greater,
         can_see_ban_details=algebra.greater,
         can_see_users_emails=algebra.greater,
         can_see_users_emails=algebra.greater,
         can_see_users_ips=algebra.greater,
         can_see_users_ips=algebra.greater,
-        can_see_hidden_users=algebra.greater
+        can_see_hidden_users=algebra.greater,
     )
     )
 
 
 
 
-"""
-ACL's for targets
-"""
 def add_acl_to_user(user, target):
 def add_acl_to_user(user, target):
     target.acl['can_have_attitude'] = False
     target.acl['can_have_attitude'] = False
     target.acl['can_follow'] = can_follow_user(user, target)
     target.acl['can_follow'] = can_follow_user(user, target)
     target.acl['can_block'] = can_block_user(user, target)
     target.acl['can_block'] = can_block_user(user, target)
 
 
-    mod_permissions = (
-        'can_have_attitude',
-        'can_follow',
-        'can_block',
-    )
+    mod_permissions = ('can_have_attitude', 'can_follow', 'can_block', )
 
 
     for permission in mod_permissions:
     for permission in mod_permissions:
         if target.acl[permission]:
         if target.acl[permission]:
@@ -140,12 +109,11 @@ def register_with(registry):
     registry.acl_annotator(get_user_model(), add_acl_to_user)
     registry.acl_annotator(get_user_model(), add_acl_to_user)
 
 
 
 
-"""
-ACL tests
-"""
 def allow_browse_users_list(user):
 def allow_browse_users_list(user):
     if not user.acl_cache['can_browse_users_list']:
     if not user.acl_cache['can_browse_users_list']:
         raise PermissionDenied(_("You can't browse users list."))
         raise PermissionDenied(_("You can't browse users list."))
+
+
 can_browse_users_list = return_boolean(allow_browse_users_list)
 can_browse_users_list = return_boolean(allow_browse_users_list)
 
 
 
 
@@ -155,6 +123,8 @@ def allow_follow_user(user, target):
         raise PermissionDenied(_("You can't follow other users."))
         raise PermissionDenied(_("You can't follow other users."))
     if user.pk == target.pk:
     if user.pk == target.pk:
         raise PermissionDenied(_("You can't add yourself to followed."))
         raise PermissionDenied(_("You can't add yourself to followed."))
+
+
 can_follow_user = return_boolean(allow_follow_user)
 can_follow_user = return_boolean(allow_follow_user)
 
 
 
 
@@ -167,6 +137,8 @@ def allow_block_user(user, target):
     if not target.acl_cache['can_be_blocked'] or target.is_superuser:
     if not target.acl_cache['can_be_blocked'] or target.is_superuser:
         message = _("%(user)s can't be blocked.") % {'user': target.username}
         message = _("%(user)s can't be blocked.") % {'user': target.username}
         raise PermissionDenied(message)
         raise PermissionDenied(message)
+
+
 can_block_user = return_boolean(allow_block_user)
 can_block_user = return_boolean(allow_block_user)
 
 
 
 
@@ -174,4 +146,6 @@ can_block_user = return_boolean(allow_block_user)
 def allow_see_ban_details(user, target):
 def allow_see_ban_details(user, target):
     if not user.acl_cache['can_see_ban_details']:
     if not user.acl_cache['can_see_ban_details']:
         raise PermissionDenied(_("You can't see users bans details."))
         raise PermissionDenied(_("You can't see users bans details."))
+
+
 can_see_ban_details = return_boolean(allow_see_ban_details)
 can_see_ban_details = return_boolean(allow_see_ban_details)

+ 9 - 14
misago/users/search.py

@@ -20,27 +20,21 @@ class SearchUsers(SearchProvider):
 
 
     def allow_search(self):
     def allow_search(self):
         if not self.request.user.acl_cache['can_search_users']:
         if not self.request.user.acl_cache['can_search_users']:
-            raise PermissionDenied(
-                _("You don't have permission to search users."))
+            raise PermissionDenied(_("You don't have permission to search users."))
 
 
     def search(self, query, page=1):
     def search(self, query, page=1):
         if query:
         if query:
-            results = search_users(
-                search_disabled=self.request.user.is_staff,
-                username=query
-            )
+            results = search_users(search_disabled=self.request.user.is_staff, username=query)
         else:
         else:
             results = []
             results = []
 
 
-        return {
-            'results': UserCardSerializer(results, many=True).data,
-            'count': len(results)
-        }
+        return {'results': UserCardSerializer(results, many=True).data, 'count': len(results)}
 
 
 
 
 def search_users(**filters):
 def search_users(**filters):
     queryset = UserModel.objects.order_by('slug').select_related(
     queryset = UserModel.objects.order_by('slug').select_related(
-        'rank', 'ban_cache', 'online_tracker')
+        'rank', 'ban_cache', 'online_tracker'
+    )
 
 
     if not filters.get('search_disabled', False):
     if not filters.get('search_disabled', False):
         queryset = queryset.filter(is_active=True)
         queryset = queryset.filter(is_active=True)
@@ -51,8 +45,9 @@ def search_users(**filters):
 
 
     # lets grab head and tail results:
     # lets grab head and tail results:
     results += list(queryset.filter(slug__startswith=username)[:HEAD_RESULTS])
     results += list(queryset.filter(slug__startswith=username)[:HEAD_RESULTS])
-    results += list(queryset.filter(
-        slug__contains=username
-    ).exclude(pk__in=[r.pk for r in results])[:TAIL_RESULTS])
+    results += list(
+        queryset.filter(slug__contains=username).exclude(pk__in=[r.pk
+                                                                 for r in results])[:TAIL_RESULTS]
+    )
 
 
     return results
     return results

+ 15 - 15
misago/users/serializers/auth.py

@@ -31,15 +31,14 @@ class AuthenticatedUserSerializer(UserSerializer, AuthFlags):
 
 
     class Meta:
     class Meta:
         model = UserModel
         model = UserModel
-        fields = UserSerializer.Meta.fields + (
+        fields = UserSerializer.Meta.fields + [
             'is_hiding_presence',
             'is_hiding_presence',
             'limits_private_thread_invites_to',
             'limits_private_thread_invites_to',
             'subscribe_to_started_threads',
             'subscribe_to_started_threads',
             'subscribe_to_replied_threads',
             'subscribe_to_replied_threads',
-
             'is_authenticated',
             'is_authenticated',
             'is_anonymous',
             'is_anonymous',
-        )
+        ]
 
 
     def get_acl(self, obj):
     def get_acl(self, obj):
         return serialize_acl(obj)
         return serialize_acl(obj)
@@ -49,21 +48,22 @@ class AuthenticatedUserSerializer(UserSerializer, AuthFlags):
 
 
     def get_api_url(self, obj):
     def get_api_url(self, obj):
         return {
         return {
-            'avatar': reverse(
-                'misago:api:user-avatar', kwargs={'pk': obj.pk}),
-            'options': reverse(
-                'misago:api:user-forum-options', kwargs={'pk': obj.pk}),
-            'username': reverse(
-                'misago:api:user-username', kwargs={'pk': obj.pk}),
-            'change_email': reverse(
-                'misago:api:user-change-email', kwargs={'pk': obj.pk}),
-            'change_password': reverse(
-                'misago:api:user-change-password', kwargs={'pk': obj.pk}),
+            'avatar': reverse('misago:api:user-avatar', kwargs={'pk': obj.pk}),
+            'options': reverse('misago:api:user-forum-options', kwargs={'pk': obj.pk}),
+            'username': reverse('misago:api:user-username', kwargs={'pk': obj.pk}),
+            'change_email': reverse('misago:api:user-change-email', kwargs={'pk': obj.pk}),
+            'change_password': reverse('misago:api:user-change-password', kwargs={'pk': obj.pk}),
         }
         }
 
 
+
 AuthenticatedUserSerializer = AuthenticatedUserSerializer.exclude_fields(
 AuthenticatedUserSerializer = AuthenticatedUserSerializer.exclude_fields(
-    'is_avatar_locked', 'is_blocked', 'is_followed', 'is_signature_locked',
-    'meta', 'signature', 'status',
+    'is_avatar_locked',
+    'is_blocked',
+    'is_followed',
+    'is_signature_locked',
+    'meta',
+    'signature',
+    'status',
 )
 )
 
 
 
 

+ 10 - 3
misago/users/serializers/ban.py

@@ -16,7 +16,7 @@ def serialize_message(message):
     if message:
     if message:
         return {
         return {
             'plain': message,
             'plain': message,
-            'html': format_plaintext_for_html(message)
+            'html': format_plaintext_for_html(message),
         }
         }
     else:
     else:
         return None
         return None
@@ -27,7 +27,10 @@ class BanMessageSerializer(serializers.ModelSerializer):
 
 
     class Meta:
     class Meta:
         model = Ban
         model = Ban
-        fields = ('message', 'expires_on')
+        fields = [
+            'message',
+            'expires_on',
+        ]
 
 
     def get_message(self, obj):
     def get_message(self, obj):
         if obj.user_message:
         if obj.user_message:
@@ -46,7 +49,11 @@ class BanDetailsSerializer(serializers.ModelSerializer):
 
 
     class Meta:
     class Meta:
         model = Ban
         model = Ban
-        fields = ('user_message', 'staff_message', 'expires_on')
+        fields = [
+            'user_message',
+            'staff_message',
+            'expires_on',
+        ]
 
 
     def get_user_message(self, obj):
     def get_user_message(self, obj):
         return serialize_message(obj.user_message)
         return serialize_message(obj.user_message)

+ 9 - 5
misago/users/serializers/moderation.py

@@ -13,6 +13,7 @@ __all__ = [
     'ModerateSignatureSerializer',
     'ModerateSignatureSerializer',
 ]
 ]
 
 
+
 class ModerateAvatarSerializer(serializers.ModelSerializer):
 class ModerateAvatarSerializer(serializers.ModelSerializer):
     class Meta:
     class Meta:
         model = UserModel
         model = UserModel
@@ -30,15 +31,18 @@ class ModerateSignatureSerializer(serializers.ModelSerializer):
             'signature',
             'signature',
             'is_signature_locked',
             'is_signature_locked',
             'signature_lock_user_message',
             'signature_lock_user_message',
-            'signature_lock_staff_message'
+            'signature_lock_staff_message',
         ]
         ]
 
 
     def validate_signature(self, value):
     def validate_signature(self, value):
         length_limit = settings.signature_length_max
         length_limit = settings.signature_length_max
         if len(value) > length_limit:
         if len(value) > length_limit:
-            raise forms.ValidationError(ungettext(
-                "Signature can't be longer than %(limit)s character.",
-                "Signature can't be longer than %(limit)s characters.",
-                length_limit) % {'limit': length_limit})
+            raise serializers.ValidationError(
+                ungettext(
+                    "Signature can't be longer than %(limit)s character.",
+                    "Signature can't be longer than %(limit)s characters.",
+                    length_limit,
+                ) % {'limit': length_limit}
+            )
 
 
         return value
         return value

+ 4 - 8
misago/users/serializers/options.py

@@ -23,10 +23,8 @@ class ForumOptionsSerializer(serializers.ModelSerializer):
     class Meta:
     class Meta:
         model = UserModel
         model = UserModel
         fields = [
         fields = [
-            'is_hiding_presence',
-            'limits_private_thread_invites_to',
-            'subscribe_to_started_threads',
-            'subscribe_to_replied_threads'
+            'is_hiding_presence', 'limits_private_thread_invites_to',
+            'subscribe_to_started_threads', 'subscribe_to_replied_threads'
         ]
         ]
         extra_kwargs = {
         extra_kwargs = {
             'limits_private_thread_invites_to': {
             'limits_private_thread_invites_to': {
@@ -63,16 +61,14 @@ class ChangeUsernameSerializer(serializers.Serializer):
             raise serializers.ValidationError(_("Enter new username."))
             raise serializers.ValidationError(_("Enter new username."))
 
 
         if username == self.context['user'].username:
         if username == self.context['user'].username:
-            raise serializers.ValidationError(
-                _("New username is same as current one."))
+            raise serializers.ValidationError(_("New username is same as current one."))
 
 
         validate_username(username)
         validate_username(username)
 
 
         return data
         return data
 
 
     def change_username(self, changed_by):
     def change_username(self, changed_by):
-        self.context['user'].set_username(
-            self.validated_data['username'], changed_by=changed_by)
+        self.context['user'].set_username(self.validated_data['username'], changed_by=changed_by)
         self.context['user'].save(update_fields=['username', 'slug'])
         self.context['user'].save(update_fields=['username', 'slug'])
 
 
 
 

+ 2 - 2
misago/users/serializers/rank.py

@@ -13,7 +13,7 @@ class RankSerializer(serializers.ModelSerializer):
 
 
     class Meta:
     class Meta:
         model = Rank
         model = Rank
-        fields = (
+        fields = [
             'id',
             'id',
             'name',
             'name',
             'slug',
             'slug',
@@ -23,7 +23,7 @@ class RankSerializer(serializers.ModelSerializer):
             'is_default',
             'is_default',
             'is_tab',
             'is_tab',
             'absolute_url',
             'absolute_url',
-        )
+        ]
 
 
     def get_description(self, obj):
     def get_description(self, obj):
         if obj.description:
         if obj.description:

+ 17 - 12
misago/users/serializers/user.py

@@ -41,7 +41,7 @@ class UserSerializer(serializers.ModelSerializer, MutableFields):
 
 
     class Meta:
     class Meta:
         model = UserModel
         model = UserModel
-        fields = (
+        fields = [
             'id',
             'id',
             'username',
             'username',
             'slug',
             'slug',
@@ -57,23 +57,20 @@ class UserSerializer(serializers.ModelSerializer, MutableFields):
             'following',
             'following',
             'threads',
             'threads',
             'posts',
             'posts',
-
             'acl',
             'acl',
             'is_followed',
             'is_followed',
             'is_blocked',
             'is_blocked',
             'meta',
             'meta',
             'status',
             'status',
-
             'absolute_url',
             'absolute_url',
             'api_url',
             'api_url',
-        )
+        ]
 
 
     def get_acl(self, obj):
     def get_acl(self, obj):
         return obj.acl
         return obj.acl
 
 
     def get_email(self, obj):
     def get_email(self, obj):
-        if (obj == self.context['user'] or
-                self.context['user'].acl_cache['can_see_users_emails']):
+        if (obj == self.context['user'] or self.context['user'].acl_cache['can_see_users_emails']):
             return obj.email
             return obj.email
         else:
         else:
             return None
             return None
@@ -113,10 +110,8 @@ class UserSerializer(serializers.ModelSerializer, MutableFields):
             'root': reverse('misago:api:user-detail', kwargs={'pk': obj.pk}),
             'root': reverse('misago:api:user-detail', kwargs={'pk': obj.pk}),
             'follow': reverse('misago:api:user-follow', kwargs={'pk': obj.pk}),
             'follow': reverse('misago:api:user-follow', kwargs={'pk': obj.pk}),
             'ban': reverse('misago:api:user-ban', kwargs={'pk': obj.pk}),
             'ban': reverse('misago:api:user-ban', kwargs={'pk': obj.pk}),
-            'moderate_avatar': reverse(
-                'misago:api:user-moderate-avatar', kwargs={'pk': obj.pk}),
-            'moderate_username': reverse(
-                'misago:api:user-moderate-username', kwargs={'pk': obj.pk}),
+            'moderate_avatar': reverse('misago:api:user-moderate-avatar', kwargs={'pk': obj.pk}),
+            'moderate_username': reverse('misago:api:user-moderate-username', kwargs={'pk': obj.pk}),
             'delete': reverse('misago:api:user-delete', kwargs={'pk': obj.pk}),
             'delete': reverse('misago:api:user-delete', kwargs={'pk': obj.pk}),
             'followers': reverse('misago:api:user-followers', kwargs={'pk': obj.pk}),
             'followers': reverse('misago:api:user-followers', kwargs={'pk': obj.pk}),
             'follows': reverse('misago:api:user-follows', kwargs={'pk': obj.pk}),
             'follows': reverse('misago:api:user-follows', kwargs={'pk': obj.pk}),
@@ -126,5 +121,15 @@ class UserSerializer(serializers.ModelSerializer, MutableFields):
 
 
 
 
 UserCardSerializer = UserSerializer.subset_fields(
 UserCardSerializer = UserSerializer.subset_fields(
-    'id', 'username', 'joined_on', 'rank', 'title', 'avatars', 'followers',
-    'threads', 'posts', 'status', 'absolute_url')
+    'id',
+    'username',
+    'joined_on',
+    'rank',
+    'title',
+    'avatars',
+    'followers',
+    'threads',
+    'posts',
+    'status',
+    'absolute_url',
+)

+ 4 - 6
misago/users/serializers/usernamechange.py

@@ -7,9 +7,7 @@ from .user import UserSerializer as BaseUserSerializer
 
 
 __all__ = ['UsernameChangeSerializer']
 __all__ = ['UsernameChangeSerializer']
 
 
-
-UserSerializer = BaseUserSerializer.subset_fields(
-    'id', 'username', 'avatars', 'absolute_url')
+UserSerializer = BaseUserSerializer.subset_fields('id', 'username', 'avatars', 'absolute_url')
 
 
 
 
 class UsernameChangeSerializer(serializers.ModelSerializer):
 class UsernameChangeSerializer(serializers.ModelSerializer):
@@ -18,12 +16,12 @@ class UsernameChangeSerializer(serializers.ModelSerializer):
 
 
     class Meta:
     class Meta:
         model = UsernameChange
         model = UsernameChange
-        fields = (
+        fields = [
             'id',
             'id',
             'user',
             'user',
             'changed_by',
             'changed_by',
             'changed_by_username',
             'changed_by_username',
             'changed_on',
             'changed_on',
             'new_username',
             'new_username',
-            'old_username'
-        )
+            'old_username',
+        ]

+ 1 - 6
misago/users/signals.py

@@ -5,11 +5,6 @@ delete_user_content = Signal()
 username_changed = Signal()
 username_changed = Signal()
 
 
 
 
-"""
-Signal handlers
-"""
 @receiver(username_changed)
 @receiver(username_changed)
 def handle_name_change(sender, **kwargs):
 def handle_name_change(sender, **kwargs):
-    sender.user_renames.update(
-        changed_by_username=sender.username
-    )
+    sender.user_renames.update(changed_by_username=sender.username)

+ 1 - 1
misago/users/signatures.py

@@ -6,7 +6,7 @@ def set_user_signature(request, user, signature):
 
 
     if signature:
     if signature:
         user.signature_parsed = signature_flavour(request, user, signature)
         user.signature_parsed = signature_flavour(request, user, signature)
-        user.signature_checksum = make_signature_checksum( user.signature_parsed, user)
+        user.signature_checksum = make_signature_checksum(user.signature_parsed, user)
     else:
     else:
         user.signature_parsed = ''
         user.signature_parsed = ''
         user.signature_checksum = ''
         user.signature_checksum = ''

+ 0 - 3
misago/users/templatetags/misago_avatars.py

@@ -1,7 +1,4 @@
 from django import template
 from django import template
-from django.urls import reverse
-
-from misago.conf import settings
 
 
 
 
 register = template.Library()
 register = template.Library()

+ 55 - 28
misago/users/tests/test_activation_views.py

@@ -19,7 +19,8 @@ class ActivationViewsTests(TestCase):
     def test_view_activate_banned(self):
     def test_view_activate_banned(self):
         """activate banned user shows error"""
         """activate banned user shows error"""
         test_user = UserModel.objects.create_user(
         test_user = UserModel.objects.create_user(
-            'Bob', 'bob@test.com', 'Pass.123', requires_activation=1)
+            'Bob', 'bob@test.com', 'Pass.123', requires_activation=1
+        )
         Ban.objects.create(
         Ban.objects.create(
             check_type=Ban.USERNAME,
             check_type=Ban.USERNAME,
             banned_value='bob',
             banned_value='bob',
@@ -28,12 +29,16 @@ class ActivationViewsTests(TestCase):
 
 
         activation_token = make_activation_token(test_user)
         activation_token = make_activation_token(test_user)
 
 
-        response = self.client.get(reverse('misago:activate-by-token', kwargs={
-            'pk': test_user.pk,
-            'token': activation_token,
-        }))
-        self.assertContains(
-            response, encode_json_html("<p>Nope!</p>"), status_code=403)
+        response = self.client.get(
+            reverse(
+                'misago:activate-by-token',
+                kwargs={
+                    'pk': test_user.pk,
+                    'token': activation_token,
+                }
+            )
+        )
+        self.assertContains(response, encode_json_html("<p>Nope!</p>"), status_code=403)
 
 
         test_user = UserModel.objects.get(pk=test_user.pk)
         test_user = UserModel.objects.get(pk=test_user.pk)
         self.assertEqual(test_user.requires_activation, 1)
         self.assertEqual(test_user.requires_activation, 1)
@@ -41,14 +46,20 @@ class ActivationViewsTests(TestCase):
     def test_view_activate_invalid_token(self):
     def test_view_activate_invalid_token(self):
         """activate with invalid token shows error"""
         """activate with invalid token shows error"""
         test_user = UserModel.objects.create_user(
         test_user = UserModel.objects.create_user(
-            'Bob', 'bob@test.com', 'Pass.123', requires_activation=1)
+            'Bob', 'bob@test.com', 'Pass.123', requires_activation=1
+        )
 
 
         activation_token = make_activation_token(test_user)
         activation_token = make_activation_token(test_user)
 
 
-        response = self.client.get(reverse('misago:activate-by-token', kwargs={
-            'pk': test_user.pk,
-            'token': activation_token + 'acd',
-        }))
+        response = self.client.get(
+            reverse(
+                'misago:activate-by-token',
+                kwargs={
+                    'pk': test_user.pk,
+                    'token': activation_token + 'acd',
+                }
+            )
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         test_user = UserModel.objects.get(pk=test_user.pk)
         test_user = UserModel.objects.get(pk=test_user.pk)
@@ -57,27 +68,37 @@ class ActivationViewsTests(TestCase):
     def test_view_activate_disabled(self):
     def test_view_activate_disabled(self):
         """activate disabled user shows error"""
         """activate disabled user shows error"""
         test_user = UserModel.objects.create_user(
         test_user = UserModel.objects.create_user(
-            'Bob', 'bob@test.com', 'Pass.123', is_active=False)
+            'Bob', 'bob@test.com', 'Pass.123', is_active=False
+        )
 
 
         activation_token = make_activation_token(test_user)
         activation_token = make_activation_token(test_user)
 
 
-        response = self.client.get(reverse('misago:activate-by-token', kwargs={
-            'pk': test_user.pk,
-            'token': activation_token,
-        }))
+        response = self.client.get(
+            reverse(
+                'misago:activate-by-token',
+                kwargs={
+                    'pk': test_user.pk,
+                    'token': activation_token,
+                }
+            )
+        )
         self.assertEqual(response.status_code, 404)
         self.assertEqual(response.status_code, 404)
 
 
     def test_view_activate_active(self):
     def test_view_activate_active(self):
         """activate active user shows error"""
         """activate active user shows error"""
-        test_user = UserModel.objects.create_user(
-            'Bob', 'bob@test.com', 'Pass.123')
+        test_user = UserModel.objects.create_user('Bob', 'bob@test.com', 'Pass.123')
 
 
         activation_token = make_activation_token(test_user)
         activation_token = make_activation_token(test_user)
 
 
-        response = self.client.get(reverse('misago:activate-by-token', kwargs={
-            'pk': test_user.pk,
-            'token': activation_token,
-        }))
+        response = self.client.get(
+            reverse(
+                'misago:activate-by-token',
+                kwargs={
+                    'pk': test_user.pk,
+                    'token': activation_token,
+                }
+            )
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         test_user = UserModel.objects.get(pk=test_user.pk)
         test_user = UserModel.objects.get(pk=test_user.pk)
@@ -86,14 +107,20 @@ class ActivationViewsTests(TestCase):
     def test_view_activate_inactive(self):
     def test_view_activate_inactive(self):
         """activate inactive user passess"""
         """activate inactive user passess"""
         test_user = UserModel.objects.create_user(
         test_user = UserModel.objects.create_user(
-            'Bob', 'bob@test.com', 'Pass.123', requires_activation=1)
+            'Bob', 'bob@test.com', 'Pass.123', requires_activation=1
+        )
 
 
         activation_token = make_activation_token(test_user)
         activation_token = make_activation_token(test_user)
 
 
-        response = self.client.get(reverse('misago:activate-by-token', kwargs={
-            'pk': test_user.pk,
-            'token': activation_token,
-        }))
+        response = self.client.get(
+            reverse(
+                'misago:activate-by-token',
+                kwargs={
+                    'pk': test_user.pk,
+                    'token': activation_token,
+                }
+            )
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
         self.assertContains(response, "your account has been activated!")
         self.assertContains(response, "your account has been activated!")
 
 

+ 2 - 4
misago/users/tests/test_activepostersranking.py

@@ -36,8 +36,7 @@ class TestActivePostersRanking(AuthenticatedUserTestCase):
         self.assertEqual(empty_ranking['users_count'], 0)
         self.assertEqual(empty_ranking['users_count'], 0)
 
 
         # other user
         # other user
-        other_user = UserModel.objects.create_user(
-            "OtherUser", "other@user.com", "pass123")
+        other_user = UserModel.objects.create_user("OtherUser", "other@user.com", "pass123")
 
 
         other_user.posts = 1
         other_user.posts = 1
         other_user.save()
         other_user.save()
@@ -67,8 +66,7 @@ class TestActivePostersRanking(AuthenticatedUserTestCase):
         self.assertEqual(ranking['users'][1].score, 1)
         self.assertEqual(ranking['users'][1].score, 1)
 
 
         # disabled users are not ranked
         # disabled users are not ranked
-        disabled = UserModel.objects.create_user(
-            "DisabledUser", "disabled@user.com", "pass123")
+        disabled = UserModel.objects.create_user("DisabledUser", "disabled@user.com", "pass123")
 
 
         disabled.is_active = False
         disabled.is_active = False
         disabled.save()
         disabled.save()

+ 154 - 78
misago/users/tests/test_auth_api.py

@@ -13,8 +13,11 @@ class GatewayTests(TestCase):
     def test_api_invalid_credentials(self):
     def test_api_invalid_credentials(self):
         """login api returns 400 on invalid POST"""
         """login api returns 400 on invalid POST"""
         response = self.client.post(
         response = self.client.post(
-            '/api/auth/',
-            data={'username': 'nope', 'password': 'nope'})
+            '/api/auth/', data={
+                'username': 'nope',
+                'password': 'nope',
+            }
+        )
 
 
         self.assertContains(response, "Login or password is incorrect.", status_code=400)
         self.assertContains(response, "Login or password is incorrect.", status_code=400)
 
 
@@ -28,10 +31,13 @@ class GatewayTests(TestCase):
         """api signs user in"""
         """api signs user in"""
         user = UserModel.objects.create_user('Bob', 'bob@test.com', 'Pass.123')
         user = UserModel.objects.create_user('Bob', 'bob@test.com', 'Pass.123')
 
 
-        response = self.client.post('/api/auth/', data={
-            'username': 'Bob',
-            'password': 'Pass.123',
-        })
+        response = self.client.post(
+            '/api/auth/',
+            data={
+                'username': 'Bob',
+                'password': 'Pass.123',
+            },
+        )
 
 
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
@@ -57,18 +63,21 @@ class GatewayTests(TestCase):
             user_message='You are tragically banned.',
             user_message='You are tragically banned.',
         )
         )
 
 
-        response = self.client.post('/api/auth/', data={
-            'username': 'Bob',
-            'password': 'Pass.123',
-        })
+        response = self.client.post(
+            '/api/auth/',
+            data={
+                'username': 'Bob',
+                'password': 'Pass.123',
+            },
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         response_json = response.json()
         response_json = response.json()
         self.assertEqual(response_json['code'], 'banned')
         self.assertEqual(response_json['code'], 'banned')
-        self.assertEqual(response_json['detail']['message']['plain'],
-                         ban.user_message)
-        self.assertEqual(response_json['detail']['message']['html'],
-                         '<p>%s</p>' % ban.user_message)
+        self.assertEqual(response_json['detail']['message']['plain'], ban.user_message)
+        self.assertEqual(
+            response_json['detail']['message']['html'], '<p>%s</p>' % ban.user_message
+        )
 
 
         response = self.client.get('/api/auth/')
         response = self.client.get('/api/auth/')
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
@@ -83,16 +92,19 @@ class GatewayTests(TestCase):
         user.is_staff = True
         user.is_staff = True
         user.save()
         user.save()
 
 
-        ban = Ban.objects.create(
+        Ban.objects.create(
             check_type=Ban.USERNAME,
             check_type=Ban.USERNAME,
             banned_value='bob',
             banned_value='bob',
             user_message='You are tragically banned.',
             user_message='You are tragically banned.',
         )
         )
 
 
-        response = self.client.post('/api/auth/', data={
-            'username': 'Bob',
-            'password': 'Pass.123',
-        })
+        response = self.client.post(
+            '/api/auth/',
+            data={
+                'username': 'Bob',
+                'password': 'Pass.123',
+            },
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         response = self.client.get('/api/auth/')
         response = self.client.get('/api/auth/')
@@ -104,13 +116,15 @@ class GatewayTests(TestCase):
 
 
     def test_login_inactive_admin(self):
     def test_login_inactive_admin(self):
         """login api fails to sign admin-activated user in"""
         """login api fails to sign admin-activated user in"""
-        UserModel.objects.create_user(
-            'Bob', 'bob@test.com', 'Pass.123', requires_activation=1)
+        UserModel.objects.create_user('Bob', 'bob@test.com', 'Pass.123', requires_activation=1)
 
 
-        response = self.client.post('/api/auth/', data={
-            'username': 'Bob',
-            'password': 'Pass.123',
-        })
+        response = self.client.post(
+            '/api/auth/',
+            data={
+                'username': 'Bob',
+                'password': 'Pass.123',
+            },
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         response_json = response.json()
         response_json = response.json()
@@ -124,13 +138,15 @@ class GatewayTests(TestCase):
 
 
     def test_login_inactive_user(self):
     def test_login_inactive_user(self):
         """login api fails to sign user-activated user in"""
         """login api fails to sign user-activated user in"""
-        UserModel.objects.create_user(
-            'Bob', 'bob@test.com', 'Pass.123', requires_activation=2)
+        UserModel.objects.create_user('Bob', 'bob@test.com', 'Pass.123', requires_activation=2)
 
 
-        response = self.client.post('/api/auth/', data={
-            'username': 'Bob',
-            'password': 'Pass.123',
-        })
+        response = self.client.post(
+            '/api/auth/',
+            data={
+                'username': 'Bob',
+                'password': 'Pass.123',
+            },
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         response_json = response.json()
         response_json = response.json()
@@ -144,16 +160,18 @@ class GatewayTests(TestCase):
 
 
     def test_login_disabled_user(self):
     def test_login_disabled_user(self):
         """its impossible to sign in to disabled account"""
         """its impossible to sign in to disabled account"""
-        user = UserModel.objects.create_user(
-            'Bob', 'bob@test.com', 'Pass.123', is_active=False)
+        user = UserModel.objects.create_user('Bob', 'bob@test.com', 'Pass.123', is_active=False)
 
 
         user.is_staff = True
         user.is_staff = True
         user.save()
         user.save()
 
 
-        response = self.client.post('/api/auth/', data={
-            'username': 'Bob',
-            'password': 'Pass.123',
-        })
+        response = self.client.post(
+            '/api/auth/',
+            data={
+                'username': 'Bob',
+                'password': 'Pass.123',
+            },
+        )
         self.assertContains(response, "Login or password is incorrect.", status_code=400)
         self.assertContains(response, "Login or password is incorrect.", status_code=400)
 
 
         response = self.client.get('/api/auth/')
         response = self.client.get('/api/auth/')
@@ -180,7 +198,12 @@ class SendActivationAPITests(TestCase):
 
 
     def test_submit_valid(self):
     def test_submit_valid(self):
         """request activation link api sends reset link mail"""
         """request activation link api sends reset link mail"""
-        response = self.client.post(self.link, data={'email': self.user.email})
+        response = self.client.post(
+            self.link,
+            data={
+                'email': self.user.email,
+            },
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         self.assertIn('Activate Bob', mail.outbox[0].subject)
         self.assertIn('Activate Bob', mail.outbox[0].subject)
@@ -193,7 +216,12 @@ class SendActivationAPITests(TestCase):
             user_message='Nope!',
             user_message='Nope!',
         )
         )
 
 
-        response = self.client.post(self.link, data={'email': self.user.email})
+        response = self.client.post(
+            self.link,
+            data={
+                'email': self.user.email,
+            },
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         self.assertIn('Activate Bob', mail.outbox[0].subject)
         self.assertIn('Activate Bob', mail.outbox[0].subject)
@@ -203,7 +231,12 @@ class SendActivationAPITests(TestCase):
         self.user.is_active = False
         self.user.is_active = False
         self.user.save()
         self.user.save()
 
 
-        response = self.client.post(self.link, data={'email': self.user.email})
+        response = self.client.post(
+            self.link,
+            data={
+                'email': self.user.email,
+            },
+        )
         self.assertContains(response, 'not_found', status_code=400)
         self.assertContains(response, 'not_found', status_code=400)
 
 
         self.assertTrue(not mail.outbox)
         self.assertTrue(not mail.outbox)
@@ -217,7 +250,12 @@ class SendActivationAPITests(TestCase):
 
 
     def test_submit_invalid(self):
     def test_submit_invalid(self):
         """request activation link api errors for invalid email"""
         """request activation link api errors for invalid email"""
-        response = self.client.post(self.link, data={'email': 'fake@mail.com'})
+        response = self.client.post(
+            self.link,
+            data={
+                'email': 'fake@mail.com',
+            },
+        )
         self.assertContains(response, 'not_found', status_code=400)
         self.assertContains(response, 'not_found', status_code=400)
 
 
         self.assertTrue(not mail.outbox)
         self.assertTrue(not mail.outbox)
@@ -227,7 +265,12 @@ class SendActivationAPITests(TestCase):
         self.user.requires_activation = 0
         self.user.requires_activation = 0
         self.user.save()
         self.user.save()
 
 
-        response = self.client.post(self.link, data={'email': self.user.email})
+        response = self.client.post(
+            self.link,
+            data={
+                'email': self.user.email,
+            },
+        )
         self.assertContains(response, 'Bob, your account is already active.', status_code=400)
         self.assertContains(response, 'Bob, your account is already active.', status_code=400)
 
 
     def test_submit_inactive_user(self):
     def test_submit_inactive_user(self):
@@ -235,7 +278,12 @@ class SendActivationAPITests(TestCase):
         self.user.requires_activation = 2
         self.user.requires_activation = 2
         self.user.save()
         self.user.save()
 
 
-        response = self.client.post(self.link, data={'email': self.user.email})
+        response = self.client.post(
+            self.link,
+            data={
+                'email': self.user.email,
+            },
+        )
         self.assertContains(response, 'inactive_admin', status_code=400)
         self.assertContains(response, 'inactive_admin', status_code=400)
 
 
         self.assertTrue(not mail.outbox)
         self.assertTrue(not mail.outbox)
@@ -244,7 +292,11 @@ class SendActivationAPITests(TestCase):
         self.user.requires_activation = 1
         self.user.requires_activation = 1
         self.user.save()
         self.user.save()
 
 
-        response = self.client.post(self.link, data={'email': self.user.email})
+        response = self.client.post(
+            self.link, data={
+                'email': self.user.email,
+            }
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         self.assertTrue(mail.outbox)
         self.assertTrue(mail.outbox)
@@ -258,7 +310,12 @@ class SendPasswordFormAPITests(TestCase):
 
 
     def test_submit_valid(self):
     def test_submit_valid(self):
         """request change password form link api sends reset link mail"""
         """request change password form link api sends reset link mail"""
-        response = self.client.post(self.link, data={'email': self.user.email})
+        response = self.client.post(
+            self.link,
+            data={
+                'email': self.user.email,
+            },
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         self.assertIn('Change Bob password', mail.outbox[0].subject)
         self.assertIn('Change Bob password', mail.outbox[0].subject)
@@ -271,7 +328,12 @@ class SendPasswordFormAPITests(TestCase):
             user_message='Nope!',
             user_message='Nope!',
         )
         )
 
 
-        response = self.client.post(self.link, data={'email': self.user.email})
+        response = self.client.post(
+            self.link,
+            data={
+                'email': self.user.email,
+            },
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         self.assertIn('Change Bob password', mail.outbox[0].subject)
         self.assertIn('Change Bob password', mail.outbox[0].subject)
@@ -281,7 +343,12 @@ class SendPasswordFormAPITests(TestCase):
         self.user.is_active = False
         self.user.is_active = False
         self.user.save()
         self.user.save()
 
 
-        response = self.client.post(self.link, data={'email': self.user.email})
+        response = self.client.post(
+            self.link,
+            data={
+                'email': self.user.email,
+            },
+        )
         self.assertContains(response, 'not_found', status_code=400)
         self.assertContains(response, 'not_found', status_code=400)
 
 
         self.assertTrue(not mail.outbox)
         self.assertTrue(not mail.outbox)
@@ -295,7 +362,12 @@ class SendPasswordFormAPITests(TestCase):
 
 
     def test_submit_invalid(self):
     def test_submit_invalid(self):
         """request change password form link api errors for invalid email"""
         """request change password form link api errors for invalid email"""
-        response = self.client.post(self.link, data={'email': 'fake@mail.com'})
+        response = self.client.post(
+            self.link,
+            data={
+                'email': 'fake@mail.com',
+            },
+        )
         self.assertContains(response, 'not_found', status_code=400)
         self.assertContains(response, 'not_found', status_code=400)
 
 
         self.assertTrue(not mail.outbox)
         self.assertTrue(not mail.outbox)
@@ -305,13 +377,23 @@ class SendPasswordFormAPITests(TestCase):
         self.user.requires_activation = 1
         self.user.requires_activation = 1
         self.user.save()
         self.user.save()
 
 
-        response = self.client.post(self.link, data={'email': self.user.email})
+        response = self.client.post(
+            self.link,
+            data={
+                'email': self.user.email,
+            },
+        )
         self.assertContains(response, 'inactive_user', status_code=400)
         self.assertContains(response, 'inactive_user', status_code=400)
 
 
         self.user.requires_activation = 2
         self.user.requires_activation = 2
         self.user.save()
         self.user.save()
 
 
-        response = self.client.post(self.link, data={'email': self.user.email})
+        response = self.client.post(
+            self.link,
+            data={
+                'email': self.user.email,
+            },
+        )
         self.assertContains(response, 'inactive_admin', status_code=400)
         self.assertContains(response, 'inactive_admin', status_code=400)
 
 
         self.assertTrue(not mail.outbox)
         self.assertTrue(not mail.outbox)
@@ -325,10 +407,12 @@ class ChangePasswordAPITests(TestCase):
 
 
     def test_submit_valid(self):
     def test_submit_valid(self):
         """submit change password form api changes password"""
         """submit change password form api changes password"""
-        response = self.client.post(self.link % (
-            self.user.pk,
-            make_password_change_token(self.user)
-        ), data={'password': 'n3wp4ss!'})
+        response = self.client.post(
+            self.link % (self.user.pk, make_password_change_token(self.user)),
+            data={
+                'password': 'n3wp4ss!',
+            },
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         user = UserModel.objects.get(id=self.user.pk)
         user = UserModel.objects.get(id=self.user.pk)
@@ -336,10 +420,7 @@ class ChangePasswordAPITests(TestCase):
 
 
     def test_invalid_token_link(self):
     def test_invalid_token_link(self):
         """api errors on invalid user id link"""
         """api errors on invalid user id link"""
-        response = self.client.post(self.link % (
-            self.user.pk,
-            'asda7ad89sa7d9s789as'
-        ))
+        response = self.client.post(self.link % (self.user.pk, 'asda7ad89sa7d9s789as'))
 
 
         self.assertContains(response, "Form link is invalid.", status_code=400)
         self.assertContains(response, "Form link is invalid.", status_code=400)
 
 
@@ -351,10 +432,9 @@ class ChangePasswordAPITests(TestCase):
             user_message='Nope!',
             user_message='Nope!',
         )
         )
 
 
-        response = self.client.post(self.link % (
-            self.user.pk,
-            make_password_change_token(self.user)
-        ))
+        response = self.client.post(
+            self.link % (self.user.pk, make_password_change_token(self.user))
+        )
         self.assertContains(response, "Your link has expired.", status_code=400)
         self.assertContains(response, "Your link has expired.", status_code=400)
 
 
     def test_inactive_user(self):
     def test_inactive_user(self):
@@ -362,19 +442,17 @@ class ChangePasswordAPITests(TestCase):
         self.user.requires_activation = 1
         self.user.requires_activation = 1
         self.user.save()
         self.user.save()
 
 
-        response = self.client.post(self.link % (
-            self.user.pk,
-            make_password_change_token(self.user)
-        ))
+        response = self.client.post(
+            self.link % (self.user.pk, make_password_change_token(self.user))
+        )
         self.assertContains(response, "Your link has expired.", status_code=400)
         self.assertContains(response, "Your link has expired.", status_code=400)
 
 
         self.user.requires_activation = 2
         self.user.requires_activation = 2
         self.user.save()
         self.user.save()
 
 
-        response = self.client.post(self.link % (
-            self.user.pk,
-            make_password_change_token(self.user)
-        ))
+        response = self.client.post(
+            self.link % (self.user.pk, make_password_change_token(self.user))
+        )
         self.assertContains(response, "Your link has expired.", status_code=400)
         self.assertContains(response, "Your link has expired.", status_code=400)
 
 
     def test_disabled_user(self):
     def test_disabled_user(self):
@@ -382,16 +460,14 @@ class ChangePasswordAPITests(TestCase):
         self.user.is_active = False
         self.user.is_active = False
         self.user.save()
         self.user.save()
 
 
-        response = self.client.post(self.link % (
-            self.user.pk,
-            make_password_change_token(self.user)
-        ))
+        response = self.client.post(
+            self.link % (self.user.pk, make_password_change_token(self.user))
+        )
         self.assertContains(response, "Form link is invalid.", status_code=400)
         self.assertContains(response, "Form link is invalid.", status_code=400)
 
 
     def test_submit_empty(self):
     def test_submit_empty(self):
         """change password api errors for empty body"""
         """change password api errors for empty body"""
-        response = self.client.post(self.link % (
-            self.user.pk,
-            make_password_change_token(self.user)
-        ))
+        response = self.client.post(
+            self.link % (self.user.pk, make_password_change_token(self.user))
+        )
         self.assertContains(response, "This password is too shor", status_code=400)
         self.assertContains(response, "This password is too shor", status_code=400)

+ 6 - 7
misago/users/tests/test_auth_backend.py

@@ -12,14 +12,13 @@ backend = MisagoBackend()
 class MisagoBackendTests(TestCase):
 class MisagoBackendTests(TestCase):
     def setUp(self):
     def setUp(self):
         self.password = 'Pass.123'
         self.password = 'Pass.123'
-        self.user = UserModel.objects.create_user(
-            'BobBoberson', 'bob@test.com', self.password)
+        self.user = UserModel.objects.create_user('BobBoberson', 'bob@test.com', self.password)
 
 
     def test_authenticate_username(self):
     def test_authenticate_username(self):
         """auth authenticates with username"""
         """auth authenticates with username"""
         user = backend.authenticate(
         user = backend.authenticate(
             username=self.user.username,
             username=self.user.username,
-            password=self.password
+            password=self.password,
         )
         )
 
 
         self.assertEqual(user, self.user)
         self.assertEqual(user, self.user)
@@ -28,7 +27,7 @@ class MisagoBackendTests(TestCase):
         """auth authenticates with email instead of username"""
         """auth authenticates with email instead of username"""
         user = backend.authenticate(
         user = backend.authenticate(
             username=self.user.email,
             username=self.user.email,
-            password=self.password
+            password=self.password,
         )
         )
 
 
         self.assertEqual(user, self.user)
         self.assertEqual(user, self.user)
@@ -37,7 +36,7 @@ class MisagoBackendTests(TestCase):
         """auth handles invalid credentials"""
         """auth handles invalid credentials"""
         user = backend.authenticate(
         user = backend.authenticate(
             username='InvalidCredential',
             username='InvalidCredential',
-            password=self.password
+            password=self.password,
         )
         )
 
 
         self.assertIsNone(user)
         self.assertIsNone(user)
@@ -46,7 +45,7 @@ class MisagoBackendTests(TestCase):
         """auth validates password"""
         """auth validates password"""
         user = backend.authenticate(
         user = backend.authenticate(
             username=self.user.email,
             username=self.user.email,
-            password='Invalid'
+            password='Invalid',
         )
         )
 
 
         self.assertIsNone(user)
         self.assertIsNone(user)
@@ -58,7 +57,7 @@ class MisagoBackendTests(TestCase):
 
 
         user = backend.authenticate(
         user = backend.authenticate(
             username=self.user.email,
             username=self.user.email,
-            password=self.password
+            password=self.password,
         )
         )
 
 
         self.assertIsNone(user)
         self.assertIsNone(user)

+ 20 - 13
misago/users/tests/test_auth_views.py

@@ -1,9 +1,5 @@
-import json
-
-from django.contrib.auth import get_user_model
 from django.test import TestCase
 from django.test import TestCase
 from django.urls import reverse
 from django.urls import reverse
-from django.utils.encoding import smart_str
 
 
 
 
 class AuthViewsTests(TestCase):
 class AuthViewsTests(TestCase):
@@ -24,17 +20,23 @@ class AuthViewsTests(TestCase):
     def test_login_view_redirect_to(self):
     def test_login_view_redirect_to(self):
         """login view respects redirect_to POST"""
         """login view respects redirect_to POST"""
         # valid redirect
         # valid redirect
-        response = self.client.post(reverse('misago:login'), data={
-            'redirect_to': '/redirect/'
-        })
+        response = self.client.post(
+            reverse('misago:login'),
+            data={
+                'redirect_to': '/redirect/',
+            },
+        )
 
 
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response['location'], '/redirect/')
         self.assertEqual(response['location'], '/redirect/')
 
 
         # invalid redirect (redirects to other site)
         # invalid redirect (redirects to other site)
-        response = self.client.post(reverse('misago:login'), data={
-            'redirect_to': 'http://somewhereelse.com/page.html'
-        })
+        response = self.client.post(
+            reverse('misago:login'),
+            data={
+                'redirect_to': 'http://somewhereelse.com/page.html',
+            },
+        )
 
 
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response['location'], '/')
         self.assertEqual(response['location'], '/')
@@ -42,14 +44,19 @@ class AuthViewsTests(TestCase):
     def test_logout_view(self):
     def test_logout_view(self):
         """logout view logs user out on post"""
         """logout view logs user out on post"""
         response = self.client.post(
         response = self.client.post(
-            '/api/auth/', data={'username': 'nope', 'password': 'nope'})
+            '/api/auth/',
+            data={
+                'username': 'nope',
+                'password': 'nope',
+            },
+        )
 
 
         self.assertContains(response, "Login or password is incorrect.", status_code=400)
         self.assertContains(response, "Login or password is incorrect.", status_code=400)
 
 
         response = self.client.get('/api/auth/')
         response = self.client.get('/api/auth/')
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        user_json = json.loads(smart_str(response.content))
+        user_json = response.json()
         self.assertIsNone(user_json['id'])
         self.assertIsNone(user_json['id'])
 
 
         response = self.client.post(reverse('misago:logout'))
         response = self.client.post(reverse('misago:logout'))
@@ -58,5 +65,5 @@ class AuthViewsTests(TestCase):
         response = self.client.get('/api/auth/')
         response = self.client.get('/api/auth/')
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        user_json = json.loads(smart_str(response.content))
+        user_json = response.json()
         self.assertIsNone(user_json['id'])
         self.assertIsNone(user_json['id'])

+ 19 - 22
misago/users/tests/test_avatars.py

@@ -3,7 +3,7 @@ from PIL import Image
 
 
 from django.contrib.auth import get_user_model
 from django.contrib.auth import get_user_model
 from django.core.exceptions import ValidationError
 from django.core.exceptions import ValidationError
-from django.test import TestCase, override_settings
+from django.test import TestCase
 from django.utils.crypto import get_random_string
 from django.utils.crypto import get_random_string
 
 
 from misago.conf import settings
 from misago.conf import settings
@@ -17,14 +17,13 @@ UserModel = get_user_model()
 class AvatarsStoreTests(TestCase):
 class AvatarsStoreTests(TestCase):
     def test_store(self):
     def test_store(self):
         """store successfully stores and deletes avatar"""
         """store successfully stores and deletes avatar"""
-        user = UserModel.objects.create_user(
-            'Bob', 'bob@bob.com', 'pass123')
+        user = UserModel.objects.create_user('Bob', 'bob@bob.com', 'pass123')
 
 
         test_image = Image.new("RGBA", (100, 100), 0)
         test_image = Image.new("RGBA", (100, 100), 0)
         store.store_new_avatar(user, test_image)
         store.store_new_avatar(user, test_image)
 
 
         # reload user
         # reload user
-        test_user = UserModel.objects.get(pk=user.pk)
+        UserModel.objects.get(pk=user.pk)
 
 
         # assert that avatars were stored in media
         # assert that avatars were stored in media
         avatars_dict = {}
         avatars_dict = {}
@@ -85,8 +84,7 @@ class AvatarsStoreTests(TestCase):
 
 
 class AvatarSetterTests(TestCase):
 class AvatarSetterTests(TestCase):
     def setUp(self):
     def setUp(self):
-        self.user = UserModel.objects.create_user(
-            'Bob', 'kontakt@rpiton.com', 'pass123')
+        self.user = UserModel.objects.create_user('Bob', 'kontakt@rpiton.com', 'pass123')
 
 
         self.user.avatars = None
         self.user.avatars = None
         self.user.save()
         self.user.save()
@@ -158,7 +156,9 @@ class AvatarSetterTests(TestCase):
     def test_default_avatar_gravatar_fallback_dynamic(self):
     def test_default_avatar_gravatar_fallback_dynamic(self):
         """default gravatar fails but fallback dynamic works"""
         """default gravatar fails but fallback dynamic works"""
         gibberish_email = '%s@%s.%s' % (
         gibberish_email = '%s@%s.%s' % (
-            get_random_string(6), get_random_string(6), get_random_string(3))
+            get_random_string(6), get_random_string(6), get_random_string(3)
+        )
+
         self.user.set_email(gibberish_email)
         self.user.set_email(gibberish_email)
         self.user.save()
         self.user.save()
 
 
@@ -169,7 +169,8 @@ class AvatarSetterTests(TestCase):
     def test_default_avatar_gravatar_fallback_empty_gallery(self):
     def test_default_avatar_gravatar_fallback_empty_gallery(self):
         """default both gravatar and fallback fail set"""
         """default both gravatar and fallback fail set"""
         gibberish_email = '%s@%s.%s' % (
         gibberish_email = '%s@%s.%s' % (
-            get_random_string(6), get_random_string(6), get_random_string(3))
+            get_random_string(6), get_random_string(6), get_random_string(3)
+        )
         self.user.set_email(gibberish_email)
         self.user.set_email(gibberish_email)
         self.user.save()
         self.user.save()
 
 
@@ -198,22 +199,18 @@ class UploadedAvatarTests(TestCase):
             uploaded.clean_crop(image, {'offset': {'x': 'ugabuga'}})
             uploaded.clean_crop(image, {'offset': {'x': 'ugabuga'}})
 
 
         with self.assertRaises(ValidationError):
         with self.assertRaises(ValidationError):
-            uploaded.clean_crop(image, {
-                    'offset': {
-                        'x': 0,
-                        'y': 0,
-                    },
-                    'zoom': -2
-                })
+            uploaded.clean_crop(image, {'offset': {
+                'x': 0,
+                'y': 0,
+            },
+                                        'zoom': -2})
 
 
         with self.assertRaises(ValidationError):
         with self.assertRaises(ValidationError):
-            uploaded.clean_crop(image, {
-                    'offset': {
-                        'x': 0,
-                        'y': 0,
-                    },
-                    'zoom': 2
-                })
+            uploaded.clean_crop(image, {'offset': {
+                'x': 0,
+                'y': 0,
+            },
+                                        'zoom': 2})
 
 
     def test_uploaded_image_size_validation(self):
     def test_uploaded_image_size_validation(self):
         """uploaded image size is validated"""
         """uploaded image size is validated"""

+ 26 - 17
misago/users/tests/test_avatarserver_views.py

@@ -15,15 +15,15 @@ class AvatarServerTests(TestCase):
         self.user.avatars = [
         self.user.avatars = [
             {
             {
                 'size': 200,
                 'size': 200,
-                'url': '/media/avatars/avatar-200.png'
+                'url': '/media/avatars/avatar-200.png',
             },
             },
             {
             {
                 'size': 100,
                 'size': 100,
-                'url': '/media/avatars/avatar-100.png'
+                'url': '/media/avatars/avatar-100.png',
             },
             },
             {
             {
                 'size': 50,
                 'size': 50,
-                'url': '/media/avatars/avatar-50.png'
+                'url': '/media/avatars/avatar-50.png',
             },
             },
         ]
         ]
 
 
@@ -31,34 +31,43 @@ class AvatarServerTests(TestCase):
 
 
     def test_get_user_avatar_exact_size(self):
     def test_get_user_avatar_exact_size(self):
         """avatar server resolved valid avatar url for user"""
         """avatar server resolved valid avatar url for user"""
-        avatar_url = reverse('misago:user-avatar', kwargs={
-            'pk': self.user.pk,
-            'size': 100,
-        })
+        avatar_url = reverse(
+            'misago:user-avatar',
+            kwargs={
+                'pk': self.user.pk,
+                'size': 100,
+            },
+        )
 
 
         response = self.client.get(avatar_url)
         response = self.client.get(avatar_url)
 
 
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
-        self.assertEqual(response['location'],  self.user.avatars[1]['url'])
+        self.assertEqual(response['location'], self.user.avatars[1]['url'])
 
 
     def test_get_user_avatar_inexact_size(self):
     def test_get_user_avatar_inexact_size(self):
         """avatar server resolved valid avatar fallback for user"""
         """avatar server resolved valid avatar fallback for user"""
-        avatar_url = reverse('misago:user-avatar', kwargs={
-            'pk': self.user.pk,
-            'size': 150,
-        })
+        avatar_url = reverse(
+            'misago:user-avatar',
+            kwargs={
+                'pk': self.user.pk,
+                'size': 150,
+            },
+        )
 
 
         response = self.client.get(avatar_url)
         response = self.client.get(avatar_url)
 
 
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
-        self.assertEqual(response['location'],  self.user.avatars[0]['url'])
+        self.assertEqual(response['location'], self.user.avatars[0]['url'])
 
 
     def test_get_notfound_user_avatar(self):
     def test_get_notfound_user_avatar(self):
         """avatar server handles deleted user avatar requests"""
         """avatar server handles deleted user avatar requests"""
-        avatar_url = reverse('misago:user-avatar', kwargs={
-            'pk': self.user.pk + 1,
-            'size': 150,
-        })
+        avatar_url = reverse(
+            'misago:user-avatar',
+            kwargs={
+                'pk': self.user.pk + 1,
+                'size': 150,
+            },
+        )
         response = self.client.get(avatar_url)
         response = self.client.get(avatar_url)
 
 
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)

+ 3 - 12
misago/users/tests/test_ban_model.py

@@ -7,18 +7,9 @@ from misago.users.models import Ban
 class BansManagerTests(TestCase):
 class BansManagerTests(TestCase):
     def setUp(self):
     def setUp(self):
         Ban.objects.bulk_create([
         Ban.objects.bulk_create([
-            Ban(
-                check_type=Ban.USERNAME,
-                banned_value='bob'
-            ),
-            Ban(
-                check_type=Ban.EMAIL,
-                banned_value='bob@test.com'
-            ),
-            Ban(
-                check_type=Ban.IP,
-                banned_value='127.0.0.1'
-            ),
+            Ban(check_type=Ban.USERNAME, banned_value='bob'),
+            Ban(check_type=Ban.EMAIL, banned_value='bob@test.com'),
+            Ban(check_type=Ban.IP, banned_value='127.0.0.1'),
         ])
         ])
 
 
     def test_get_ban_for_banned_name(self):
     def test_get_ban_for_banned_name(self):

+ 67 - 40
misago/users/tests/test_banadmin_views.py

@@ -1,7 +1,6 @@
-from datetime import date, datetime, timedelta
+from datetime import datetime, timedelta
 
 
 from django.urls import reverse
 from django.urls import reverse
-from django.utils.six.moves import range
 
 
 from misago.admin.testutils import AdminTestCase
 from misago.admin.testutils import AdminTestCase
 from misago.users.models import Ban
 from misago.users.models import Ban
@@ -28,13 +27,16 @@ class BanAdminViewsTests(AdminTestCase):
         test_date = datetime.now() + timedelta(days=180)
         test_date = datetime.now() + timedelta(days=180)
 
 
         for i in range(10):
         for i in range(10):
-            response = self.client.post(reverse('misago:admin:users:bans:new'), data={
-                'check_type': '1',
-                'banned_value': '%stest@test.com' % i,
-                'user_message': 'Lorem ipsum dolor met',
-                'staff_message': 'Sit amet elit',
-                'expires_on': test_date.isoformat(),
-            })
+            response = self.client.post(
+                reverse('misago:admin:users:bans:new'),
+                data={
+                    'check_type': '1',
+                    'banned_value': '%stest@test.com' % i,
+                    'user_message': 'Lorem ipsum dolor met',
+                    'staff_message': 'Sit amet elit',
+                    'expires_on': test_date.isoformat(),
+                },
+            )
             self.assertEqual(response.status_code, 302)
             self.assertEqual(response.status_code, 302)
 
 
         self.assertEqual(Ban.objects.count(), 10)
         self.assertEqual(Ban.objects.count(), 10)
@@ -43,10 +45,13 @@ class BanAdminViewsTests(AdminTestCase):
         for ban in Ban.objects.iterator():
         for ban in Ban.objects.iterator():
             bans_pks.append(ban.pk)
             bans_pks.append(ban.pk)
 
 
-        response = self.client.post(reverse('misago:admin:users:bans:index'), data={
-            'action': 'delete',
-            'selected_items': bans_pks
-        })
+        response = self.client.post(
+            reverse('misago:admin:users:bans:index'),
+            data={
+                'action': 'delete',
+                'selected_items': bans_pks,
+            },
+        )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
         self.assertEqual(Ban.objects.count(), 0)
         self.assertEqual(Ban.objects.count(), 0)
 
 
@@ -57,13 +62,16 @@ class BanAdminViewsTests(AdminTestCase):
 
 
         test_date = datetime.now() + timedelta(days=180)
         test_date = datetime.now() + timedelta(days=180)
 
 
-        response = self.client.post(reverse('misago:admin:users:bans:new'), data={
-            'check_type': '1',
-            'banned_value': 'test@test.com',
-            'user_message': 'Lorem ipsum dolor met',
-            'staff_message': 'Sit amet elit',
-            'expires_on': test_date.isoformat(),
-        })
+        response = self.client.post(
+            reverse('misago:admin:users:bans:new'),
+            data={
+                'check_type': '1',
+                'banned_value': 'test@test.com',
+                'user_message': 'Lorem ipsum dolor met',
+                'staff_message': 'Sit amet elit',
+                'expires_on': test_date.isoformat(),
+            },
+        )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
         response = self.client.get(reverse('misago:admin:users:bans:index'))
         response = self.client.get(reverse('misago:admin:users:bans:index'))
@@ -73,21 +81,32 @@ class BanAdminViewsTests(AdminTestCase):
 
 
     def test_edit_view(self):
     def test_edit_view(self):
         """edit ban view has no showstoppers"""
         """edit ban view has no showstoppers"""
-        self.client.post(reverse('misago:admin:users:bans:new'), data={
-            'check_type': '0',
-            'banned_value': 'Admin',
-        })
+        self.client.post(
+            reverse('misago:admin:users:bans:new'),
+            data={
+                'check_type': '0',
+                'banned_value': 'Admin',
+            },
+        )
 
 
         test_ban = Ban.objects.get(banned_value='admin')
         test_ban = Ban.objects.get(banned_value='admin')
-        form_link = reverse('misago:admin:users:bans:edit', kwargs={'pk': test_ban.pk})
-
-        response = self.client.post(form_link, data={
-            'check_type': '1',
-            'banned_value': 'test@test.com',
-            'user_message': 'Lorem ipsum dolor met',
-            'staff_message': 'Sit amet elit',
-            'expires_on': '',
-        })
+        form_link = reverse(
+            'misago:admin:users:bans:edit',
+            kwargs={
+                'pk': test_ban.pk,
+            },
+        )
+
+        response = self.client.post(
+            form_link,
+            data={
+                'check_type': '1',
+                'banned_value': 'test@test.com',
+                'user_message': 'Lorem ipsum dolor met',
+                'staff_message': 'Sit amet elit',
+                'expires_on': '',
+            },
+        )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
         response = self.client.get(reverse('misago:admin:users:bans:index'))
         response = self.client.get(reverse('misago:admin:users:bans:index'))
@@ -97,16 +116,24 @@ class BanAdminViewsTests(AdminTestCase):
 
 
     def test_delete_view(self):
     def test_delete_view(self):
         """delete ban view has no showstoppers"""
         """delete ban view has no showstoppers"""
-        self.client.post(reverse('misago:admin:users:bans:new'), data={
-            'check_type': '0',
-            'banned_value': 'TestBan',
-        })
+        self.client.post(
+            reverse('misago:admin:users:bans:new'),
+            data={
+                'check_type': '0',
+                'banned_value': 'TestBan',
+            },
+        )
 
 
         test_ban = Ban.objects.get(banned_value='testban')
         test_ban = Ban.objects.get(banned_value='testban')
 
 
-        response = self.client.post(reverse('misago:admin:users:bans:delete', kwargs={
-            'pk': test_ban.pk
-        }))
+        response = self.client.post(
+            reverse(
+                'misago:admin:users:bans:delete',
+                kwargs={
+                    'pk': test_ban.pk,
+                },
+            )
+        )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
         response = self.client.get(reverse('misago:admin:users:bans:index'))
         response = self.client.get(reverse('misago:admin:users:bans:index'))

+ 18 - 20
misago/users/tests/test_bans.py

@@ -20,7 +20,7 @@ class GetBanTests(TestCase):
 
 
         Ban.objects.create(
         Ban.objects.create(
             banned_value='expired',
             banned_value='expired',
-            expires_on=timezone.now() - timedelta(days=7)
+            expires_on=timezone.now() - timedelta(days=7),
         )
         )
 
 
         expired_ban = get_username_ban('expired')
         expired_ban = get_username_ban('expired')
@@ -28,7 +28,7 @@ class GetBanTests(TestCase):
 
 
         Ban.objects.create(
         Ban.objects.create(
             banned_value='wrongtype',
             banned_value='wrongtype',
-            check_type=Ban.EMAIL
+            check_type=Ban.EMAIL,
         )
         )
 
 
         wrong_type_ban = get_username_ban('wrongtype')
         wrong_type_ban = get_username_ban('wrongtype')
@@ -36,7 +36,7 @@ class GetBanTests(TestCase):
 
 
         valid_ban = Ban.objects.create(
         valid_ban = Ban.objects.create(
             banned_value='admi*',
             banned_value='admi*',
-            expires_on=timezone.now() + timedelta(days=7)
+            expires_on=timezone.now() + timedelta(days=7),
         )
         )
         self.assertEqual(get_username_ban('admiral').pk, valid_ban.pk)
         self.assertEqual(get_username_ban('admiral').pk, valid_ban.pk)
 
 
@@ -48,7 +48,7 @@ class GetBanTests(TestCase):
         Ban.objects.create(
         Ban.objects.create(
             banned_value='ex@pired.com',
             banned_value='ex@pired.com',
             check_type=Ban.EMAIL,
             check_type=Ban.EMAIL,
-            expires_on=timezone.now() - timedelta(days=7)
+            expires_on=timezone.now() - timedelta(days=7),
         )
         )
 
 
         expired_ban = get_email_ban('ex@pired.com')
         expired_ban = get_email_ban('ex@pired.com')
@@ -56,7 +56,7 @@ class GetBanTests(TestCase):
 
 
         Ban.objects.create(
         Ban.objects.create(
             banned_value='wrong@type.com',
             banned_value='wrong@type.com',
-            check_type=Ban.IP
+            check_type=Ban.IP,
         )
         )
 
 
         wrong_type_ban = get_email_ban('wrong@type.com')
         wrong_type_ban = get_email_ban('wrong@type.com')
@@ -65,7 +65,7 @@ class GetBanTests(TestCase):
         valid_ban = Ban.objects.create(
         valid_ban = Ban.objects.create(
             banned_value='*.ru',
             banned_value='*.ru',
             check_type=Ban.EMAIL,
             check_type=Ban.EMAIL,
-            expires_on=timezone.now() + timedelta(days=7)
+            expires_on=timezone.now() + timedelta(days=7),
         )
         )
         self.assertEqual(get_email_ban('banned@mail.ru').pk, valid_ban.pk)
         self.assertEqual(get_email_ban('banned@mail.ru').pk, valid_ban.pk)
 
 
@@ -77,7 +77,7 @@ class GetBanTests(TestCase):
         Ban.objects.create(
         Ban.objects.create(
             banned_value='124.0.0.1',
             banned_value='124.0.0.1',
             check_type=Ban.IP,
             check_type=Ban.IP,
-            expires_on=timezone.now() - timedelta(days=7)
+            expires_on=timezone.now() - timedelta(days=7),
         )
         )
 
 
         expired_ban = get_ip_ban('124.0.0.1')
         expired_ban = get_ip_ban('124.0.0.1')
@@ -85,7 +85,7 @@ class GetBanTests(TestCase):
 
 
         Ban.objects.create(
         Ban.objects.create(
             banned_value='wrongtype',
             banned_value='wrongtype',
-            check_type=Ban.EMAIL
+            check_type=Ban.EMAIL,
         )
         )
 
 
         wrong_type_ban = get_ip_ban('wrongtype')
         wrong_type_ban = get_ip_ban('wrongtype')
@@ -94,15 +94,14 @@ class GetBanTests(TestCase):
         valid_ban = Ban.objects.create(
         valid_ban = Ban.objects.create(
             banned_value='125.0.0.*',
             banned_value='125.0.0.*',
             check_type=Ban.IP,
             check_type=Ban.IP,
-            expires_on=timezone.now() + timedelta(days=7)
+            expires_on=timezone.now() + timedelta(days=7),
         )
         )
         self.assertEqual(get_ip_ban('125.0.0.1').pk, valid_ban.pk)
         self.assertEqual(get_ip_ban('125.0.0.1').pk, valid_ban.pk)
 
 
 
 
 class UserBansTests(TestCase):
 class UserBansTests(TestCase):
     def setUp(self):
     def setUp(self):
-        self.user = UserModel.objects.create_user(
-            'Bob', 'bob@boberson.com', 'pass123')
+        self.user = UserModel.objects.create_user('Bob', 'bob@boberson.com', 'pass123')
 
 
     def test_no_ban(self):
     def test_no_ban(self):
         """user is not caught by ban"""
         """user is not caught by ban"""
@@ -114,7 +113,7 @@ class UserBansTests(TestCase):
         Ban.objects.create(
         Ban.objects.create(
             banned_value='bob',
             banned_value='bob',
             user_message='User reason',
             user_message='User reason',
-            staff_message='Staff reason'
+            staff_message='Staff reason',
         )
         )
 
 
         user_ban = get_user_ban(self.user)
         user_ban = get_user_ban(self.user)
@@ -129,7 +128,7 @@ class UserBansTests(TestCase):
             banned_value='bo*',
             banned_value='bo*',
             user_message='User reason',
             user_message='User reason',
             staff_message='Staff reason',
             staff_message='Staff reason',
-            expires_on=timezone.now() + timedelta(days=7)
+            expires_on=timezone.now() + timedelta(days=7),
         )
         )
 
 
         user_ban = get_user_ban(self.user)
         user_ban = get_user_ban(self.user)
@@ -142,7 +141,7 @@ class UserBansTests(TestCase):
         """user is not caught by expired ban"""
         """user is not caught by expired ban"""
         Ban.objects.create(
         Ban.objects.create(
             banned_value='bo*',
             banned_value='bo*',
-            expires_on=timezone.now() - timedelta(days=7)
+            expires_on=timezone.now() - timedelta(days=7),
         )
         )
 
 
         self.assertIsNone(get_user_ban(self.user))
         self.assertIsNone(get_user_ban(self.user))
@@ -152,7 +151,7 @@ class UserBansTests(TestCase):
         """user is not caught by expired but checked ban"""
         """user is not caught by expired but checked ban"""
         Ban.objects.create(
         Ban.objects.create(
             banned_value='bo*',
             banned_value='bo*',
-            expires_on=timezone.now() - timedelta(days=7)
+            expires_on=timezone.now() - timedelta(days=7),
         )
         )
         Ban.objects.update(is_checked=True)
         Ban.objects.update(is_checked=True)
 
 
@@ -177,7 +176,7 @@ class RequestIPBansTests(TestCase):
         Ban.objects.create(
         Ban.objects.create(
             check_type=Ban.IP,
             check_type=Ban.IP,
             banned_value='127.0.0.1',
             banned_value='127.0.0.1',
-            user_message='User reason'
+            user_message='User reason',
         )
         )
 
 
         ip_ban = get_request_ip_ban(FakeRequest())
         ip_ban = get_request_ip_ban(FakeRequest())
@@ -194,7 +193,7 @@ class RequestIPBansTests(TestCase):
             check_type=Ban.IP,
             check_type=Ban.IP,
             banned_value='127.0.0.1',
             banned_value='127.0.0.1',
             user_message='User reason',
             user_message='User reason',
-            expires_on=timezone.now() + timedelta(days=7)
+            expires_on=timezone.now() + timedelta(days=7),
         )
         )
 
 
         ip_ban = get_request_ip_ban(FakeRequest())
         ip_ban = get_request_ip_ban(FakeRequest())
@@ -211,7 +210,7 @@ class RequestIPBansTests(TestCase):
             check_type=Ban.IP,
             check_type=Ban.IP,
             banned_value='127.0.0.1',
             banned_value='127.0.0.1',
             user_message='User reason',
             user_message='User reason',
-            expires_on=timezone.now() - timedelta(days=7)
+            expires_on=timezone.now() - timedelta(days=7),
         )
         )
 
 
         ip_ban = get_request_ip_ban(FakeRequest())
         ip_ban = get_request_ip_ban(FakeRequest())
@@ -224,8 +223,7 @@ class RequestIPBansTests(TestCase):
 class BanUserTests(TestCase):
 class BanUserTests(TestCase):
     def test_ban_user(self):
     def test_ban_user(self):
         """ban_user utility bans user"""
         """ban_user utility bans user"""
-        user = UserModel.objects.create_user(
-            'Bob', 'bob@boberson.com', 'pass123')
+        user = UserModel.objects.create_user('Bob', 'bob@boberson.com', 'pass123')
 
 
         ban = ban_user(user, 'User reason', 'Staff reason')
         ban = ban_user(user, 'User reason', 'Staff reason')
         self.assertEqual(ban.user_message, 'User reason')
         self.assertEqual(ban.user_message, 'User reason')

+ 1 - 4
misago/users/tests/test_captcha_api.py

@@ -1,8 +1,5 @@
-import json
-
 from django.test import TestCase
 from django.test import TestCase
 from django.urls import reverse
 from django.urls import reverse
-from django.utils.encoding import smart_str
 
 
 from misago.conf import settings
 from misago.conf import settings
 
 
@@ -29,6 +26,6 @@ class AuthenticateAPITests(TestCase):
         response = self.client.get(self.api_link)
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response_json = json.loads(smart_str(response.content))
+        response_json = response.json()
         self.assertEqual(response_json['question'], 'Do you like pies?')
         self.assertEqual(response_json['question'], 'Do you like pies?')
         self.assertEqual(response_json['help_text'], 'Type in "yes".')
         self.assertEqual(response_json['help_text'], 'Type in "yes".')

+ 1 - 2
misago/users/tests/test_commands.py

@@ -9,12 +9,11 @@ UserModel = get_user_model()
 class CreateSuperUserTests(TestCase):
 class CreateSuperUserTests(TestCase):
     def test_createsuperuser(self):
     def test_createsuperuser(self):
         """createsuperuser creates user account in perfect conditions"""
         """createsuperuser creates user account in perfect conditions"""
-
         opts = {
         opts = {
             'username': 'Boberson',
             'username': 'Boberson',
             'email': 'bob@test.com',
             'email': 'bob@test.com',
             'password': 'Pass.123',
             'password': 'Pass.123',
-            'verbosity': 0
+            'verbosity': 0,
         }
         }
 
 
         call_command('createsuperuser', **opts)
         call_command('createsuperuser', **opts)

+ 4 - 2
misago/users/tests/test_createsuperuser.py

@@ -11,20 +11,22 @@ class CreateSuperuserTests(TestCase):
     def test_create_superuser(self):
     def test_create_superuser(self):
         """command creates superuser"""
         """command creates superuser"""
         out = StringIO()
         out = StringIO()
+
         call_command(
         call_command(
             "createsuperuser",
             "createsuperuser",
             interactive=False,
             interactive=False,
             username="joe",
             username="joe",
             email="joe@somewhere.org",
             email="joe@somewhere.org",
             password="Pass.123",
             password="Pass.123",
-            stdout=out
+            stdout=out,
         )
         )
 
 
         new_user = UserModel.objects.order_by('-id')[:1][0]
         new_user = UserModel.objects.order_by('-id')[:1][0]
 
 
         self.assertEqual(
         self.assertEqual(
             out.getvalue().splitlines()[-1].strip(),
             out.getvalue().splitlines()[-1].strip(),
-            'Superuser #%s has been created successfully.' % new_user.pk)
+            'Superuser #%s has been created successfully.' % new_user.pk,
+        )
 
 
         self.assertEqual(new_user.username, 'joe')
         self.assertEqual(new_user.username, 'joe')
         self.assertEqual(new_user.email, 'joe@somewhere.org')
         self.assertEqual(new_user.email, 'joe@somewhere.org')

+ 4 - 8
misago/users/tests/test_credentialchange.py

@@ -20,8 +20,7 @@ class CredentialChangeTests(TestCase):
     def test_valid_token_generation(self):
     def test_valid_token_generation(self):
         """credentialchange module allows for store and read of change token"""
         """credentialchange module allows for store and read of change token"""
         request = MockRequest(self.user)
         request = MockRequest(self.user)
-        token = credentialchange.store_new_credential(
-            request, 'email', 'newbob@test.com')
+        token = credentialchange.store_new_credential(request, 'email', 'newbob@test.com')
 
 
         email = credentialchange.read_new_credential(request, 'email', token)
         email = credentialchange.read_new_credential(request, 'email', token)
         self.assertEqual(email, 'newbob@test.com')
         self.assertEqual(email, 'newbob@test.com')
@@ -29,8 +28,7 @@ class CredentialChangeTests(TestCase):
     def test_email_change_invalidated_token(self):
     def test_email_change_invalidated_token(self):
         """token is invalidated by email change"""
         """token is invalidated by email change"""
         request = MockRequest(self.user)
         request = MockRequest(self.user)
-        token = credentialchange.store_new_credential(
-            request, 'email', 'newbob@test.com')
+        token = credentialchange.store_new_credential(request, 'email', 'newbob@test.com')
 
 
         self.user.set_email('egebege@test.com')
         self.user.set_email('egebege@test.com')
         self.user.save()
         self.user.save()
@@ -41,8 +39,7 @@ class CredentialChangeTests(TestCase):
     def test_password_change_invalidated_token(self):
     def test_password_change_invalidated_token(self):
         """token is invalidated by password change"""
         """token is invalidated by password change"""
         request = MockRequest(self.user)
         request = MockRequest(self.user)
-        token = credentialchange.store_new_credential(
-            request, 'email', 'newbob@test.com')
+        token = credentialchange.store_new_credential(request, 'email', 'newbob@test.com')
 
 
         self.user.set_password('Egebeg!123')
         self.user.set_password('Egebeg!123')
         self.user.save()
         self.user.save()
@@ -53,8 +50,7 @@ class CredentialChangeTests(TestCase):
     def test_invalid_token_is_handled(self):
     def test_invalid_token_is_handled(self):
         """there are no explosions in invalid tokens handling"""
         """there are no explosions in invalid tokens handling"""
         request = MockRequest(self.user)
         request = MockRequest(self.user)
-        token = credentialchange.store_new_credential(
-            request, 'email', 'newbob@test.com')
+        token = credentialchange.store_new_credential(request, 'email', 'newbob@test.com')
 
 
         email = credentialchange.read_new_credential(request, 'em4il', token)
         email = credentialchange.read_new_credential(request, 'em4il', token)
         self.assertIsNone(email)
         self.assertIsNone(email)

+ 3 - 4
misago/users/tests/test_decorators.py

@@ -39,7 +39,7 @@ class DenyBannedIPTests(UserTestCase):
         Ban.objects.create(
         Ban.objects.create(
             check_type=Ban.IP,
             check_type=Ban.IP,
             banned_value='83.*',
             banned_value='83.*',
-            user_message="Ya got banned!"
+            user_message="Ya got banned!",
         )
         )
 
 
         response = self.client.post(reverse('misago:request-activation'))
         response = self.client.post(reverse('misago:request-activation'))
@@ -50,9 +50,8 @@ class DenyBannedIPTests(UserTestCase):
         Ban.objects.create(
         Ban.objects.create(
             check_type=Ban.IP,
             check_type=Ban.IP,
             banned_value='127.*',
             banned_value='127.*',
-            user_message="Ya got banned!"
+            user_message="Ya got banned!",
         )
         )
 
 
         response = self.client.post(reverse('misago:request-activation'))
         response = self.client.post(reverse('misago:request-activation'))
-        self.assertContains(
-            response, encode_json_html("<p>Ya got banned!</p>"), status_code=403)
+        self.assertContains(response, encode_json_html("<p>Ya got banned!</p>"), status_code=403)

+ 7 - 4
misago/users/tests/test_djangoadmin_auth.py

@@ -17,10 +17,13 @@ class DjangoAdminAuthTests(AdminTestCase):
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         # form handles login
         # form handles login
-        response = self.client.post(reverse('admin:index'), data={
-            'username': self.user.email,
-            'password': self.USER_PASSWORD,
-        })
+        response = self.client.post(
+            reverse('admin:index'),
+            data={
+                'username': self.user.email,
+                'password': self.USER_PASSWORD,
+            },
+        )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
         response = self.client.get(reverse('admin:index'))
         response = self.client.get(reverse('admin:index'))

+ 33 - 20
misago/users/tests/test_forgottenpassword_views.py

@@ -36,12 +36,15 @@ class ForgottenPasswordViewsTests(UserTestCase):
         password_token = make_password_change_token(test_user)
         password_token = make_password_change_token(test_user)
 
 
         response = self.client.get(
         response = self.client.get(
-            reverse('misago:forgotten-password-change-form', kwargs={
-                'pk': test_user.pk,
-                'token': password_token,
-            }))
-        self.assertContains(
-            response, encode_json_html("<p>Nope!</p>"), status_code=403)
+            reverse(
+                'misago:forgotten-password-change-form',
+                kwargs={
+                    'pk': test_user.pk,
+                    'token': password_token,
+                },
+            )
+        )
+        self.assertContains(response, encode_json_html("<p>Nope!</p>"), status_code=403)
 
 
     def test_change_password_on_other_user(self):
     def test_change_password_on_other_user(self):
         """change other user password errors"""
         """change other user password errors"""
@@ -52,23 +55,29 @@ class ForgottenPasswordViewsTests(UserTestCase):
         self.login_user(self.get_authenticated_user())
         self.login_user(self.get_authenticated_user())
 
 
         response = self.client.get(
         response = self.client.get(
-            reverse('misago:forgotten-password-change-form', kwargs={
-                'pk': test_user.pk,
-                'token': password_token,
-            }))
+            reverse(
+                'misago:forgotten-password-change-form',
+                kwargs={
+                    'pk': test_user.pk,
+                    'token': password_token,
+                },
+            )
+        )
         self.assertContains(response, 'your link has expired', status_code=400)
         self.assertContains(response, 'your link has expired', status_code=400)
 
 
     def test_change_password_invalid_token(self):
     def test_change_password_invalid_token(self):
         """invalid form token errors"""
         """invalid form token errors"""
         test_user = UserModel.objects.create_user('Bob', 'bob@test.com', 'Pass.123')
         test_user = UserModel.objects.create_user('Bob', 'bob@test.com', 'Pass.123')
 
 
-        password_token = make_password_change_token(test_user)
-
         response = self.client.get(
         response = self.client.get(
-            reverse('misago:forgotten-password-change-form', kwargs={
-                'pk': test_user.pk,
-                'token': 'abcdfghqsads',
-            }))
+            reverse(
+                'misago:forgotten-password-change-form',
+                kwargs={
+                    'pk': test_user.pk,
+                    'token': 'abcdfghqsads',
+                },
+            )
+        )
         self.assertContains(response, 'your link is invalid', status_code=400)
         self.assertContains(response, 'your link is invalid', status_code=400)
 
 
     def test_change_password_form(self):
     def test_change_password_form(self):
@@ -78,8 +87,12 @@ class ForgottenPasswordViewsTests(UserTestCase):
         password_token = make_password_change_token(test_user)
         password_token = make_password_change_token(test_user)
 
 
         response = self.client.get(
         response = self.client.get(
-            reverse('misago:forgotten-password-change-form', kwargs={
-                'pk': test_user.pk,
-                'token': password_token,
-            }))
+            reverse(
+                'misago:forgotten-password-change-form',
+                kwargs={
+                    'pk': test_user.pk,
+                    'token': password_token,
+                },
+            )
+        )
         self.assertContains(response, password_token)
         self.assertContains(response, password_token)

+ 7 - 5
misago/users/tests/test_invalidatebans.py

@@ -5,7 +5,6 @@ from django.core.management import call_command
 from django.test import TestCase
 from django.test import TestCase
 from django.utils import timezone
 from django.utils import timezone
 from django.utils.six import StringIO
 from django.utils.six import StringIO
-from django.utils.six.moves import range
 
 
 from misago.users import bans
 from misago.users import bans
 from misago.users.management.commands import invalidatebans
 from misago.users.management.commands import invalidatebans
@@ -19,9 +18,9 @@ class InvalidateBansTests(TestCase):
     def test_expired_bans_handling(self):
     def test_expired_bans_handling(self):
         """expired bans are flagged as such"""
         """expired bans are flagged as such"""
         # create 5 bans then update their valid date to past one
         # create 5 bans then update their valid date to past one
-        for i in range(5):
+        for _ in range(5):
             Ban.objects.create(banned_value="abcd")
             Ban.objects.create(banned_value="abcd")
-        expired_date = (timezone.now() - timedelta(days=10))
+        expired_date = timezone.now() - timedelta(days=10)
         Ban.objects.all().update(expires_on=expired_date, is_checked=True)
         Ban.objects.all().update(expires_on=expired_date, is_checked=True)
 
 
         self.assertEqual(Ban.objects.filter(is_checked=True).count(), 5)
         self.assertEqual(Ban.objects.filter(is_checked=True).count(), 5)
@@ -58,8 +57,11 @@ class InvalidateBansTests(TestCase):
         self.assertEqual(Ban.objects.filter(is_checked=True).count(), 1)
         self.assertEqual(Ban.objects.filter(is_checked=True).count(), 1)
 
 
         # expire bans
         # expire bans
-        expired_date = (timezone.now() - timedelta(days=10))
-        Ban.objects.all().update(expires_on=expired_date, is_checked=True)
+        expired_date = timezone.now() - timedelta(days=10)
+        Ban.objects.all().update(
+            expires_on=expired_date,
+            is_checked=True,
+        )
         BanCache.objects.all().update(expires_on=expired_date)
         BanCache.objects.all().update(expires_on=expired_date)
 
 
         # invalidate expired ban cache
         # invalidate expired ban cache

+ 35 - 11
misago/users/tests/test_lists_views.py

@@ -1,6 +1,5 @@
 from django.contrib.auth import get_user_model
 from django.contrib.auth import get_user_model
 from django.urls import reverse
 from django.urls import reverse
-from django.utils.six.moves import range
 
 
 from misago.acl.testutils import override_acl
 from misago.acl.testutils import override_acl
 from misago.categories.models import Category
 from misago.categories.models import Category
@@ -35,8 +34,7 @@ class UsersListLanderTests(UsersListTestCase):
         """lander returns redirect to valid page if user has permission"""
         """lander returns redirect to valid page if user has permission"""
         response = self.client.get(reverse('misago:users'))
         response = self.client.get(reverse('misago:users'))
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
-        self.assertTrue(response['location'].endswith(
-                        reverse('misago:users-active-posters')))
+        self.assertTrue(response['location'].endswith(reverse('misago:users-active-posters')))
 
 
 
 
 class ActivePostersTests(UsersListTestCase):
 class ActivePostersTests(UsersListTestCase):
@@ -58,7 +56,11 @@ class ActivePostersTests(UsersListTestCase):
         # Create 50 test users and see if errors appeared
         # Create 50 test users and see if errors appeared
         for i in range(50):
         for i in range(50):
             user = UserModel.objects.create_user(
             user = UserModel.objects.create_user(
-                'Bob%s' % i, 'm%s@te.com' % i, 'Pass.123', posts=12345)
+                'Bob%s' % i,
+                'm%s@te.com' % i,
+                'Pass.123',
+                posts=12345,
+            )
             post_thread(category, poster=user)
             post_thread(category, poster=user)
 
 
         build_active_posters_ranking()
         build_active_posters_ranking()
@@ -70,14 +72,18 @@ class ActivePostersTests(UsersListTestCase):
 class UsersRankTests(UsersListTestCase):
 class UsersRankTests(UsersListTestCase):
     def test_ranks(self):
     def test_ranks(self):
         """ranks lists are handled correctly"""
         """ranks lists are handled correctly"""
-        rank_user = UserModel.objects.create_user(
-            'Visible', 'visible@te.com', 'Pass.123')
+        rank_user = UserModel.objects.create_user('Visible', 'visible@te.com', 'Pass.123')
 
 
         for rank in Rank.objects.iterator():
         for rank in Rank.objects.iterator():
             rank_user.rank = rank
             rank_user.rank = rank
             rank_user.save()
             rank_user.save()
 
 
-            rank_link = reverse('misago:users-rank', kwargs={'slug': rank.slug})
+            rank_link = reverse(
+                'misago:users-rank',
+                kwargs={
+                    'slug': rank.slug,
+                },
+            )
             response = self.client.get(rank_link)
             response = self.client.get(rank_link)
 
 
             if rank.is_tab:
             if rank.is_tab:
@@ -89,13 +95,22 @@ class UsersRankTests(UsersListTestCase):
     def test_disabled_users(self):
     def test_disabled_users(self):
         """ranks lists excludes disabled accounts"""
         """ranks lists excludes disabled accounts"""
         rank_user = UserModel.objects.create_user(
         rank_user = UserModel.objects.create_user(
-            'Visible', 'visible@te.com', 'Pass.123', is_active=False)
+            'Visible',
+            'visible@te.com',
+            'Pass.123',
+            is_active=False,
+        )
 
 
         for rank in Rank.objects.iterator():
         for rank in Rank.objects.iterator():
             rank_user.rank = rank
             rank_user.rank = rank
             rank_user.save()
             rank_user.save()
 
 
-            rank_link = reverse('misago:users-rank', kwargs={'slug': rank.slug})
+            rank_link = reverse(
+                'misago:users-rank',
+                kwargs={
+                    'slug': rank.slug,
+                },
+            )
             response = self.client.get(rank_link)
             response = self.client.get(rank_link)
 
 
             if rank.is_tab:
             if rank.is_tab:
@@ -110,13 +125,22 @@ class UsersRankTests(UsersListTestCase):
         self.user.save()
         self.user.save()
 
 
         rank_user = UserModel.objects.create_user(
         rank_user = UserModel.objects.create_user(
-            'Visible', 'visible@te.com', 'Pass.123', is_active=False)
+            'Visible',
+            'visible@te.com',
+            'Pass.123',
+            is_active=False,
+        )
 
 
         for rank in Rank.objects.iterator():
         for rank in Rank.objects.iterator():
             rank_user.rank = rank
             rank_user.rank = rank
             rank_user.save()
             rank_user.save()
 
 
-            rank_link = reverse('misago:users-rank', kwargs={'slug': rank.slug})
+            rank_link = reverse(
+                'misago:users-rank',
+                kwargs={
+                    'slug': rank.slug,
+                },
+            )
             response = self.client.get(rank_link)
             response = self.client.get(rank_link)
 
 
             if rank.is_tab:
             if rank.is_tab:

+ 33 - 17
misago/users/tests/test_options_views.py

@@ -12,9 +12,14 @@ class OptionsViewsTests(AuthenticatedUserTestCase):
 
 
     def test_form_view_returns_200(self):
     def test_form_view_returns_200(self):
         """/options/some-form has no show stoppers"""
         """/options/some-form has no show stoppers"""
-        response = self.client.get(reverse('misago:options-form', kwargs={
-            'form_name': 'some-fake-form'
-        }))
+        response = self.client.get(
+            reverse(
+                'misago:options-form',
+                kwargs={
+                    'form_name': 'some-fake-form',
+                },
+            )
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
 
 
@@ -23,10 +28,10 @@ class ConfirmChangeEmailTests(AuthenticatedUserTestCase):
         super(ConfirmChangeEmailTests, self).setUp()
         super(ConfirmChangeEmailTests, self).setUp()
         link = '/api/users/%s/change-email/' % self.user.pk
         link = '/api/users/%s/change-email/' % self.user.pk
 
 
-        response = self.client.post(link, data={
-            'new_email': 'n3w@email.com',
-            'password': self.USER_PASSWORD
-        })
+        response = self.client.post(
+            link, data={'new_email': 'n3w@email.com',
+                        'password': self.USER_PASSWORD}
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         for line in [l.strip() for l in mail.outbox[0].body.splitlines()]:
         for line in [l.strip() for l in mail.outbox[0].body.splitlines()]:
@@ -37,9 +42,13 @@ class ConfirmChangeEmailTests(AuthenticatedUserTestCase):
     def test_invalid_token(self):
     def test_invalid_token(self):
         """invalid token is rejected"""
         """invalid token is rejected"""
         response = self.client.get(
         response = self.client.get(
-            reverse('misago:options-confirm-email-change', kwargs={
-                'token': 'invalid'
-            }))
+            reverse(
+                'misago:options-confirm-email-change',
+                kwargs={
+                    'token': 'invalid',
+                },
+            )
+        )
 
 
         self.assertContains(response, "Change confirmation link is invalid.", status_code=400)
         self.assertContains(response, "Change confirmation link is invalid.", status_code=400)
 
 
@@ -58,10 +67,13 @@ class ConfirmChangePasswordTests(AuthenticatedUserTestCase):
         super(ConfirmChangePasswordTests, self).setUp()
         super(ConfirmChangePasswordTests, self).setUp()
         link = '/api/users/%s/change-password/' % self.user.pk
         link = '/api/users/%s/change-password/' % self.user.pk
 
 
-        response = self.client.post(link, data={
-            'new_password': 'n3wp4ssword',
-            'password': self.USER_PASSWORD
-        })
+        response = self.client.post(
+            link,
+            data={
+                'new_password': 'n3wp4ssword',
+                'password': self.USER_PASSWORD,
+            },
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         for line in [l.strip() for l in mail.outbox[0].body.splitlines()]:
         for line in [l.strip() for l in mail.outbox[0].body.splitlines()]:
@@ -72,9 +84,13 @@ class ConfirmChangePasswordTests(AuthenticatedUserTestCase):
     def test_invalid_token(self):
     def test_invalid_token(self):
         """invalid token is rejected"""
         """invalid token is rejected"""
         response = self.client.get(
         response = self.client.get(
-            reverse('misago:options-confirm-password-change', kwargs={
-                'token': 'invalid'
-            }))
+            reverse(
+                'misago:options-confirm-password-change',
+                kwargs={
+                    'token': 'invalid',
+                },
+            )
+        )
 
 
         self.assertContains(response, "Change confirmation link is invalid.", status_code=400)
         self.assertContains(response, "Change confirmation link is invalid.", status_code=400)
 
 

+ 53 - 30
misago/users/tests/test_profile_views.py

@@ -1,6 +1,5 @@
 from django.contrib.auth import get_user_model
 from django.contrib.auth import get_user_model
 from django.urls import reverse
 from django.urls import reverse
-from django.utils.six.moves import range
 
 
 from misago.acl.testutils import override_acl
 from misago.acl.testutils import override_acl
 from misago.categories.models import Category
 from misago.categories.models import Category
@@ -15,18 +14,18 @@ UserModel = get_user_model()
 class UserProfileViewsTests(AuthenticatedUserTestCase):
 class UserProfileViewsTests(AuthenticatedUserTestCase):
     def setUp(self):
     def setUp(self):
         super(UserProfileViewsTests, self).setUp()
         super(UserProfileViewsTests, self).setUp()
-        self.link_kwargs = {
-            'slug': self.user.slug,
-            'pk': self.user.pk
-        }
+        self.link_kwargs = {'slug': self.user.slug, 'pk': self.user.pk}
 
 
         self.category = Category.objects.get(slug='first-category')
         self.category = Category.objects.get(slug='first-category')
 
 
     def test_outdated_slugs(self):
     def test_outdated_slugs(self):
-        """user profile view redirects to valid slig"""
-        invalid_kwargs = {'slug': 'baww', 'pk': self.user.pk}
-        response = self.client.get(reverse('misago:user-posts',
-                                           kwargs=invalid_kwargs))
+        """user profile view redirects to valid slug"""
+        response = self.client.get(
+            reverse('misago:user-posts', kwargs={
+                'slug': 'baww',
+                'pk': self.user.pk,
+            })
+        )
 
 
         self.assertEqual(response.status_code, 301)
         self.assertEqual(response.status_code, 301)
 
 
@@ -61,7 +60,10 @@ class UserProfileViewsTests(AuthenticatedUserTestCase):
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
         self.assertContains(response, "You have posted no messages")
         self.assertContains(response, "You have posted no messages")
 
 
-        thread = testutils.post_thread(category=self.category, poster=self.user)
+        thread = testutils.post_thread(
+            category=self.category,
+            poster=self.user,
+        )
 
 
         response = self.client.get(link)
         response = self.client.get(link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
@@ -83,7 +85,10 @@ class UserProfileViewsTests(AuthenticatedUserTestCase):
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
         self.assertContains(response, "You have no started threads.")
         self.assertContains(response, "You have no started threads.")
 
 
-        thread = testutils.post_thread(category=self.category, poster=self.user)
+        thread = testutils.post_thread(
+            category=self.category,
+            poster=self.user,
+        )
 
 
         response = self.client.get(link)
         response = self.client.get(link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
@@ -99,8 +104,10 @@ class UserProfileViewsTests(AuthenticatedUserTestCase):
 
 
     def test_user_followers(self):
     def test_user_followers(self):
         """user profile followers list has no showstoppers"""
         """user profile followers list has no showstoppers"""
-        response = self.client.get(reverse('misago:user-followers',
-                                           kwargs=self.link_kwargs))
+        response = self.client.get(reverse(
+            'misago:user-followers',
+            kwargs=self.link_kwargs,
+        ))
 
 
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
         self.assertContains(response, 'You have no followers.')
         self.assertContains(response, 'You have no followers.')
@@ -111,16 +118,20 @@ class UserProfileViewsTests(AuthenticatedUserTestCase):
             followers.append(UserModel.objects.create_user(*user_data))
             followers.append(UserModel.objects.create_user(*user_data))
             self.user.followed_by.add(followers[-1])
             self.user.followed_by.add(followers[-1])
 
 
-        response = self.client.get(reverse('misago:user-followers',
-                                           kwargs=self.link_kwargs))
+        response = self.client.get(reverse(
+            'misago:user-followers',
+            kwargs=self.link_kwargs,
+        ))
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
         for i in range(10):
         for i in range(10):
             self.assertContains(response, "Follower%s" % i)
             self.assertContains(response, "Follower%s" % i)
 
 
     def test_user_follows(self):
     def test_user_follows(self):
         """user profile follows list has no showstoppers"""
         """user profile follows list has no showstoppers"""
-        response = self.client.get(reverse('misago:user-follows',
-                                           kwargs=self.link_kwargs))
+        response = self.client.get(reverse(
+            'misago:user-follows',
+            kwargs=self.link_kwargs,
+        ))
 
 
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
         self.assertContains(response, 'You are not following any users.')
         self.assertContains(response, 'You are not following any users.')
@@ -131,16 +142,20 @@ class UserProfileViewsTests(AuthenticatedUserTestCase):
             followers.append(UserModel.objects.create_user(*user_data))
             followers.append(UserModel.objects.create_user(*user_data))
             followers[-1].followed_by.add(self.user)
             followers[-1].followed_by.add(self.user)
 
 
-        response = self.client.get(reverse('misago:user-follows',
-                                           kwargs=self.link_kwargs))
+        response = self.client.get(reverse(
+            'misago:user-follows',
+            kwargs=self.link_kwargs,
+        ))
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
         for i in range(10):
         for i in range(10):
             self.assertContains(response, "Follower%s" % i)
             self.assertContains(response, "Follower%s" % i)
 
 
     def test_username_history_list(self):
     def test_username_history_list(self):
         """user name changes history list has no showstoppers"""
         """user name changes history list has no showstoppers"""
-        response = self.client.get(reverse('misago:username-history',
-                                           kwargs=self.link_kwargs))
+        response = self.client.get(reverse(
+            'misago:username-history',
+            kwargs=self.link_kwargs,
+        ))
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
         self.assertContains(response, 'Your username was never changed.')
         self.assertContains(response, 'Your username was never changed.')
 
 
@@ -149,8 +164,10 @@ class UserProfileViewsTests(AuthenticatedUserTestCase):
         self.user.set_username('TestUser')
         self.user.set_username('TestUser')
         self.user.save()
         self.user.save()
 
 
-        response = self.client.get(
-            reverse('misago:username-history', kwargs=self.link_kwargs))
+        response = self.client.get(reverse(
+            'misago:username-history',
+            kwargs=self.link_kwargs,
+        ))
 
 
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
         self.assertContains(response, "TestUser")
         self.assertContains(response, "TestUser")
@@ -165,16 +182,20 @@ class UserProfileViewsTests(AuthenticatedUserTestCase):
         test_user = UserModel.objects.create_user("Bob", "bob@bob.com", 'pass.123')
         test_user = UserModel.objects.create_user("Bob", "bob@bob.com", 'pass.123')
         link_kwargs = {'slug': test_user.slug, 'pk': test_user.pk}
         link_kwargs = {'slug': test_user.slug, 'pk': test_user.pk}
 
 
-        response = self.client.get(reverse('misago:user-ban',
-                                           kwargs=link_kwargs))
+        response = self.client.get(reverse(
+            'misago:user-ban',
+            kwargs=link_kwargs,
+        ))
         self.assertEqual(response.status_code, 404)
         self.assertEqual(response.status_code, 404)
 
 
         override_acl(self.user, {
         override_acl(self.user, {
             'can_see_ban_details': 1,
             'can_see_ban_details': 1,
         })
         })
 
 
-        response = self.client.get(reverse('misago:user-ban',
-                                           kwargs=link_kwargs))
+        response = self.client.get(reverse(
+            'misago:user-ban',
+            kwargs=link_kwargs,
+        ))
         self.assertEqual(response.status_code, 404)
         self.assertEqual(response.status_code, 404)
 
 
         override_acl(self.user, {
         override_acl(self.user, {
@@ -186,11 +207,13 @@ class UserProfileViewsTests(AuthenticatedUserTestCase):
             banned_value=test_user.username,
             banned_value=test_user.username,
             user_message="User m3ss4ge.",
             user_message="User m3ss4ge.",
             staff_message="Staff m3ss4ge.",
             staff_message="Staff m3ss4ge.",
-            is_checked=True
+            is_checked=True,
         )
         )
 
 
-        response = self.client.get(
-            reverse('misago:user-ban', kwargs=link_kwargs))
+        response = self.client.get(reverse(
+            'misago:user-ban',
+            kwargs=link_kwargs,
+        ))
 
 
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
         self.assertContains(response, 'User m3ss4ge')
         self.assertContains(response, 'User m3ss4ge')

+ 86 - 33
misago/users/tests/test_rankadmin_views.py

@@ -8,8 +8,7 @@ from misago.users.models import Rank
 class RankAdminViewsTests(AdminTestCase):
 class RankAdminViewsTests(AdminTestCase):
     def test_link_registered(self):
     def test_link_registered(self):
         """admin nav contains ranks link"""
         """admin nav contains ranks link"""
-        response = self.client.get(
-            reverse('misago:admin:users:accounts:index'))
+        response = self.client.get(reverse('misago:admin:users:accounts:index'))
 
 
         response = self.client.get(response['location'])
         response = self.client.get(response['location'])
         self.assertContains(response, reverse('misago:admin:users:ranks:index'))
         self.assertContains(response, reverse('misago:admin:users:ranks:index'))
@@ -27,8 +26,7 @@ class RankAdminViewsTests(AdminTestCase):
         test_role_b = Role.objects.create(name='Test Role B')
         test_role_b = Role.objects.create(name='Test Role B')
         test_role_c = Role.objects.create(name='Test Role C')
         test_role_c = Role.objects.create(name='Test Role C')
 
 
-        response = self.client.get(
-            reverse('misago:admin:users:ranks:new'))
+        response = self.client.get(reverse('misago:admin:users:ranks:new'))
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         response = self.client.post(
         response = self.client.post(
@@ -40,7 +38,8 @@ class RankAdminViewsTests(AdminTestCase):
                 'style': 'test',
                 'style': 'test',
                 'is_tab': '1',
                 'is_tab': '1',
                 'roles': [test_role_a.pk, test_role_c.pk],
                 'roles': [test_role_a.pk, test_role_c.pk],
-            })
+            },
+        )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
         response = self.client.get(reverse('misago:admin:users:ranks:index'))
         response = self.client.get(reverse('misago:admin:users:ranks:index'))
@@ -68,24 +67,35 @@ class RankAdminViewsTests(AdminTestCase):
                 'style': 'test',
                 'style': 'test',
                 'is_tab': '1',
                 'is_tab': '1',
                 'roles': [test_role_a.pk, test_role_c.pk],
                 'roles': [test_role_a.pk, test_role_c.pk],
-            })
+            },
+        )
 
 
         test_rank = Rank.objects.get(slug='test-rank')
         test_rank = Rank.objects.get(slug='test-rank')
 
 
         response = self.client.get(
         response = self.client.get(
-            reverse('misago:admin:users:ranks:edit',
-                    kwargs={'pk': test_rank.pk}))
+            reverse(
+                'misago:admin:users:ranks:edit',
+                kwargs={
+                    'pk': test_rank.pk,
+                },
+            )
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
         self.assertContains(response, test_rank.name)
         self.assertContains(response, test_rank.name)
         self.assertContains(response, test_rank.title)
         self.assertContains(response, test_rank.title)
 
 
         response = self.client.post(
         response = self.client.post(
-            reverse('misago:admin:users:ranks:edit',
-                    kwargs={'pk': test_rank.pk}),
+            reverse(
+                'misago:admin:users:ranks:edit',
+                kwargs={
+                    'pk': test_rank.pk,
+                },
+            ),
             data={
             data={
                 'name': 'Top Lel',
                 'name': 'Top Lel',
                 'roles': [test_role_b.pk],
                 'roles': [test_role_b.pk],
-            })
+            },
+        )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
         test_rank = Rank.objects.get(slug='top-lel')
         test_rank = Rank.objects.get(slug='top-lel')
@@ -109,13 +119,19 @@ class RankAdminViewsTests(AdminTestCase):
                 'title': 'Test Title',
                 'title': 'Test Title',
                 'style': 'test',
                 'style': 'test',
                 'is_tab': '1',
                 'is_tab': '1',
-            })
+            },
+        )
 
 
         test_rank = Rank.objects.get(slug='test-rank')
         test_rank = Rank.objects.get(slug='test-rank')
 
 
         response = self.client.post(
         response = self.client.post(
-            reverse('misago:admin:users:ranks:default',
-                    kwargs={'pk': test_rank.pk}))
+            reverse(
+                'misago:admin:users:ranks:default',
+                kwargs={
+                    'pk': test_rank.pk,
+                },
+            )
+        )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
         test_rank = Rank.objects.get(slug='test-rank')
         test_rank = Rank.objects.get(slug='test-rank')
@@ -131,13 +147,19 @@ class RankAdminViewsTests(AdminTestCase):
                 'title': 'Test Title',
                 'title': 'Test Title',
                 'style': 'test',
                 'style': 'test',
                 'is_tab': '1',
                 'is_tab': '1',
-            })
+            },
+        )
 
 
         test_rank = Rank.objects.get(slug='test-rank')
         test_rank = Rank.objects.get(slug='test-rank')
 
 
         response = self.client.post(
         response = self.client.post(
-            reverse('misago:admin:users:ranks:up',
-                    kwargs={'pk': test_rank.pk}))
+            reverse(
+                'misago:admin:users:ranks:up',
+                kwargs={
+                    'pk': test_rank.pk,
+                },
+            )
+        )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
         changed_rank = Rank.objects.get(slug='test-rank')
         changed_rank = Rank.objects.get(slug='test-rank')
@@ -153,18 +175,29 @@ class RankAdminViewsTests(AdminTestCase):
                 'title': 'Test Title',
                 'title': 'Test Title',
                 'style': 'test',
                 'style': 'test',
                 'is_tab': '1',
                 'is_tab': '1',
-            })
+            },
+        )
 
 
         test_rank = Rank.objects.get(slug='test-rank')
         test_rank = Rank.objects.get(slug='test-rank')
 
 
         # Move rank up
         # Move rank up
         response = self.client.post(
         response = self.client.post(
-            reverse('misago:admin:users:ranks:up',
-                    kwargs={'pk': test_rank.pk}))
+            reverse(
+                'misago:admin:users:ranks:up',
+                kwargs={
+                    'pk': test_rank.pk,
+                },
+            )
+        )
 
 
         response = self.client.post(
         response = self.client.post(
-            reverse('misago:admin:users:ranks:down',
-                    kwargs={'pk': test_rank.pk}))
+            reverse(
+                'misago:admin:users:ranks:down',
+                kwargs={
+                    'pk': test_rank.pk,
+                },
+            )
+        )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
         # Test move down
         # Test move down
@@ -181,12 +214,19 @@ class RankAdminViewsTests(AdminTestCase):
                 'title': 'Test Title',
                 'title': 'Test Title',
                 'style': 'test',
                 'style': 'test',
                 'is_tab': '1',
                 'is_tab': '1',
-            })
+            },
+        )
 
 
         test_rank = Rank.objects.get(slug='test-rank')
         test_rank = Rank.objects.get(slug='test-rank')
 
 
-        response = self.client.get(reverse('misago:admin:users:ranks:users',
-                                           kwargs={'pk': test_rank.pk}))
+        response = self.client.get(
+            reverse(
+                'misago:admin:users:ranks:users',
+                kwargs={
+                    'pk': test_rank.pk,
+                },
+            )
+        )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
     def test_delete_view(self):
     def test_delete_view(self):
@@ -199,13 +239,19 @@ class RankAdminViewsTests(AdminTestCase):
                 'title': 'Test Title',
                 'title': 'Test Title',
                 'style': 'test',
                 'style': 'test',
                 'is_tab': '1',
                 'is_tab': '1',
-            })
+            },
+        )
 
 
         test_rank = Rank.objects.get(slug='test-rank')
         test_rank = Rank.objects.get(slug='test-rank')
 
 
         response = self.client.post(
         response = self.client.post(
-            reverse('misago:admin:users:ranks:delete',
-                    kwargs={'pk': test_rank.pk}))
+            reverse(
+                'misago:admin:users:ranks:delete',
+                kwargs={
+                    'pk': test_rank.pk,
+                },
+            )
+        )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
         self.client.get(reverse('misago:admin:users:ranks:index'))
         self.client.get(reverse('misago:admin:users:ranks:index'))
@@ -228,7 +274,8 @@ class RankAdminViewsTests(AdminTestCase):
                 'style': 'test',
                 'style': 'test',
                 'is_tab': '1',
                 'is_tab': '1',
                 'roles': [test_role_a.pk],
                 'roles': [test_role_a.pk],
-            })
+            }
+        )
 
 
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
         self.assertContains(response, "This name collides with other rank.")
         self.assertContains(response, "This name collides with other rank.")
@@ -242,16 +289,22 @@ class RankAdminViewsTests(AdminTestCase):
                 'style': 'test',
                 'style': 'test',
                 'is_tab': '1',
                 'is_tab': '1',
                 'roles': [test_role_a.pk],
                 'roles': [test_role_a.pk],
-            })
+            }
+        )
 
 
         test_rank = Rank.objects.get(slug='test-rank')
         test_rank = Rank.objects.get(slug='test-rank')
 
 
         response = self.client.post(
         response = self.client.post(
-            reverse('misago:admin:users:ranks:edit',
-                    kwargs={'pk': test_rank.pk}),
+            reverse(
+                'misago:admin:users:ranks:edit',
+                kwargs={
+                    'pk': test_rank.pk,
+                },
+            ),
             data={
             data={
                 'name': 'Members',
                 'name': 'Members',
                 'roles': [test_role_a.pk],
                 'roles': [test_role_a.pk],
-            })
+            },
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
         self.assertContains(response, "This name collides with other rank.")
         self.assertContains(response, "This name collides with other rank.")

+ 1 - 2
misago/users/tests/test_realip_middleware.py

@@ -24,5 +24,4 @@ class RealIPMiddlewareTests(TestCase):
         request = MockRequest('127.0.0.1', '83.42.13.77')
         request = MockRequest('127.0.0.1', '83.42.13.77')
         RealIPMiddleware().process_request(request)
         RealIPMiddleware().process_request(request)
 
 
-        self.assertEqual(request.user_ip,
-                         request.META['HTTP_X_FORWARDED_FOR'])
+        self.assertEqual(request.user_ip, request.META['HTTP_X_FORWARDED_FOR'])

+ 32 - 20
misago/users/tests/test_rest_permissions.py

@@ -11,9 +11,11 @@ class UnbannedOnlyTests(UserTestCase):
     def test_api_allows_guests(self):
     def test_api_allows_guests(self):
         """policy allows guests"""
         """policy allows guests"""
         response = self.client.post(
         response = self.client.post(
-            reverse('misago:api:send-password-form'), data={
-                'email': self.user.email
-            })
+            reverse('misago:api:send-password-form'),
+            data={
+                'email': self.user.email,
+            },
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
     def test_api_allows_authenticated(self):
     def test_api_allows_authenticated(self):
@@ -21,9 +23,11 @@ class UnbannedOnlyTests(UserTestCase):
         self.login_user(self.user)
         self.login_user(self.user)
 
 
         response = self.client.post(
         response = self.client.post(
-            reverse('misago:api:send-password-form'), data={
-                'email': self.user.email
-            })
+            reverse('misago:api:send-password-form'),
+            data={
+                'email': self.user.email,
+            },
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
     def test_api_blocks_banned(self):
     def test_api_blocks_banned(self):
@@ -31,13 +35,15 @@ class UnbannedOnlyTests(UserTestCase):
         Ban.objects.create(
         Ban.objects.create(
             check_type=Ban.IP,
             check_type=Ban.IP,
             banned_value='127.*',
             banned_value='127.*',
-            user_message='Ya got banned!'
+            user_message='Ya got banned!',
         )
         )
 
 
         response = self.client.post(
         response = self.client.post(
-            reverse('misago:api:send-password-form'), data={
-                'email': self.user.email
-            })
+            reverse('misago:api:send-password-form'),
+            data={
+                'email': self.user.email,
+            },
+        )
         self.assertEqual(response.status_code, 403)
         self.assertEqual(response.status_code, 403)
 
 
 
 
@@ -51,9 +57,11 @@ class UnbannedAnonOnlyTests(UserTestCase):
         self.user.save()
         self.user.save()
 
 
         response = self.client.post(
         response = self.client.post(
-            reverse('misago:api:send-activation'), data={
-                'email': self.user.email
-            })
+            reverse('misago:api:send-activation'),
+            data={
+                'email': self.user.email,
+            },
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
     def test_api_allows_authenticated(self):
     def test_api_allows_authenticated(self):
@@ -61,9 +69,11 @@ class UnbannedAnonOnlyTests(UserTestCase):
         self.login_user(self.user)
         self.login_user(self.user)
 
 
         response = self.client.post(
         response = self.client.post(
-            reverse('misago:api:send-activation'), data={
-                'email': self.user.email
-            })
+            reverse('misago:api:send-activation'),
+            data={
+                'email': self.user.email,
+            },
+        )
         self.assertEqual(response.status_code, 403)
         self.assertEqual(response.status_code, 403)
 
 
     def test_api_blocks_banned(self):
     def test_api_blocks_banned(self):
@@ -71,11 +81,13 @@ class UnbannedAnonOnlyTests(UserTestCase):
         Ban.objects.create(
         Ban.objects.create(
             check_type=Ban.IP,
             check_type=Ban.IP,
             banned_value='127.*',
             banned_value='127.*',
-            user_message='Ya got banned!'
+            user_message='Ya got banned!',
         )
         )
 
 
         response = self.client.post(
         response = self.client.post(
-            reverse('misago:api:send-activation'), data={
-                'email': self.user.email
-            })
+            reverse('misago:api:send-activation'),
+            data={
+                'email': self.user.email,
+            },
+        )
         self.assertEqual(response.status_code, 403)
         self.assertEqual(response.status_code, 403)

+ 11 - 7
misago/users/tests/test_search.py

@@ -16,9 +16,7 @@ class SearchApiTests(AuthenticatedUserTestCase):
 
 
     def test_no_permission(self):
     def test_no_permission(self):
         """api respects permission to search users"""
         """api respects permission to search users"""
-        override_acl(self.user, {
-            'can_search_users': 0
-        })
+        override_acl(self.user, {'can_search_users': 0})
 
 
         response = self.client.get(self.api_link)
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
@@ -105,7 +103,11 @@ class SearchApiTests(AuthenticatedUserTestCase):
     def test_search_disabled(self):
     def test_search_disabled(self):
         """api respects disabled users visibility"""
         """api respects disabled users visibility"""
         disabled_user = UserModel.objects.create_user(
         disabled_user = UserModel.objects.create_user(
-            'DisabledUser', 'visible@te.com', 'Pass.123', is_active=False)
+            'DisabledUser',
+            'visible@te.com',
+            'Pass.123',
+            is_active=False,
+        )
 
 
         response = self.client.get('%s?q=DisabledUser' % self.api_link)
         response = self.client.get('%s?q=DisabledUser' % self.api_link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
@@ -138,6 +140,8 @@ class SearchProviderApiTests(SearchApiTests):
     def setUp(self):
     def setUp(self):
         super(SearchProviderApiTests, self).setUp()
         super(SearchProviderApiTests, self).setUp()
 
 
-        self.api_link = reverse('misago:api:search', kwargs={
-            'search_provider': 'users'
-        })
+        self.api_link = reverse(
+            'misago:api:search', kwargs={
+                'search_provider': 'users',
+            }
+        )

+ 1 - 2
misago/users/tests/test_signatures.py

@@ -25,8 +25,7 @@ class SignaturesTests(TestCase):
         self.assertEqual(test_user.signature_parsed, '')
         self.assertEqual(test_user.signature_parsed, '')
         self.assertEqual(test_user.signature_checksum, '')
         self.assertEqual(test_user.signature_checksum, '')
 
 
-        signatures.set_user_signature(
-            MockRequest(), test_user, 'Hello, world!')
+        signatures.set_user_signature(MockRequest(), test_user, 'Hello, world!')
 
 
         self.assertEqual(test_user.signature, 'Hello, world!')
         self.assertEqual(test_user.signature, 'Hello, world!')
         self.assertEqual(test_user.signature_parsed, '<p>Hello, world!</p>')
         self.assertEqual(test_user.signature_parsed, '<p>Hello, world!</p>')

+ 5 - 8
misago/users/tests/test_testutils.py

@@ -1,7 +1,4 @@
-import json
-
 from django.urls import reverse
 from django.urls import reverse
-from django.utils.encoding import smart_str
 
 
 from misago.users.testutils import AuthenticatedUserTestCase, SuperUserTestCase, UserTestCase
 from misago.users.testutils import AuthenticatedUserTestCase, SuperUserTestCase, UserTestCase
 
 
@@ -37,7 +34,7 @@ class UserTestCaseTests(UserTestCase):
         response = self.client.get('/api/auth/')
         response = self.client.get('/api/auth/')
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        user_json = json.loads(smart_str(response.content))
+        user_json = response.json()
         self.assertEqual(user_json['id'], user.id)
         self.assertEqual(user_json['id'], user.id)
 
 
     def test_login_superuser(self):
     def test_login_superuser(self):
@@ -48,7 +45,7 @@ class UserTestCaseTests(UserTestCase):
         response = self.client.get('/api/auth/')
         response = self.client.get('/api/auth/')
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        user_json = json.loads(smart_str(response.content))
+        user_json = response.json()
         self.assertEqual(user_json['id'], user.id)
         self.assertEqual(user_json['id'], user.id)
 
 
     def test_logout_user(self):
     def test_logout_user(self):
@@ -60,7 +57,7 @@ class UserTestCaseTests(UserTestCase):
         response = self.client.get('/api/auth/')
         response = self.client.get('/api/auth/')
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        user_json = json.loads(smart_str(response.content))
+        user_json = response.json()
         self.assertIsNone(user_json['id'])
         self.assertIsNone(user_json['id'])
 
 
     def test_logout_superuser(self):
     def test_logout_superuser(self):
@@ -72,7 +69,7 @@ class UserTestCaseTests(UserTestCase):
         response = self.client.get('/api/auth/')
         response = self.client.get('/api/auth/')
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        user_json = json.loads(smart_str(response.content))
+        user_json = response.json()
         self.assertIsNone(user_json['id'])
         self.assertIsNone(user_json['id'])
 
 
 
 
@@ -99,5 +96,5 @@ class SuperUserTestCaseTests(SuperUserTestCase):
         response = self.client.get('/api/auth/')
         response = self.client.get('/api/auth/')
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        user_json = json.loads(smart_str(response.content))
+        user_json = response.json()
         self.assertEqual(user_json['id'], self.user.id)
         self.assertEqual(user_json['id'], self.user.id)

+ 144 - 131
misago/users/tests/test_user_avatar_api.py

@@ -4,11 +4,8 @@ import os
 from path import Path
 from path import Path
 
 
 from django.contrib.auth import get_user_model
 from django.contrib.auth import get_user_model
-from django.urls import reverse
-from django.utils.encoding import smart_str
 
 
 from misago.acl.testutils import override_acl
 from misago.acl.testutils import override_acl
-from misago.conf import settings
 from misago.users.avatars import gallery, store
 from misago.users.avatars import gallery, store
 from misago.users.models import AvatarGallery
 from misago.users.models import AvatarGallery
 from misago.users.testutils import AuthenticatedUserTestCase
 from misago.users.testutils import AuthenticatedUserTestCase
@@ -21,9 +18,8 @@ UserModel = get_user_model()
 
 
 
 
 class UserAvatarTests(AuthenticatedUserTestCase):
 class UserAvatarTests(AuthenticatedUserTestCase):
-    """
-    tests for user avatar RPC (/api/users/1/avatar/)
-    """
+    """tests for user avatar RPC (/api/users/1/avatar/)"""
+
     def setUp(self):
     def setUp(self):
         super(UserAvatarTests, self).setUp()
         super(UserAvatarTests, self).setUp()
         self.link = '/api/users/%s/avatar/' % self.user.pk
         self.link = '/api/users/%s/avatar/' % self.user.pk
@@ -80,13 +76,14 @@ class UserAvatarTests(AuthenticatedUserTestCase):
 
 
     def test_other_user_avatar(self):
     def test_other_user_avatar(self):
         """requests to api error if user tries to access other user"""
         """requests to api error if user tries to access other user"""
-        self.logout_user();
+        self.logout_user()
 
 
         response = self.client.get(self.link)
         response = self.client.get(self.link)
         self.assertContains(response, "You have to sign in", status_code=403)
         self.assertContains(response, "You have to sign in", status_code=403)
 
 
-        self.login_user(UserModel.objects.create_user(
-            "BobUser", "bob@bob.com", self.USER_PASSWORD))
+        self.login_user(
+            UserModel.objects.create_user("BobUser", "bob@bob.com", self.USER_PASSWORD)
+        )
 
 
         response = self.client.get(self.link)
         response = self.client.get(self.link)
         self.assertContains(response, "can't change other users avatars", status_code=403)
         self.assertContains(response, "can't change other users avatars", status_code=403)
@@ -126,30 +123,33 @@ class UserAvatarTests(AuthenticatedUserTestCase):
         self.assertContains(response, "No file was sent.", status_code=400)
         self.assertContains(response, "No file was sent.", status_code=400)
 
 
         with open(TEST_AVATAR_PATH, 'rb') as avatar:
         with open(TEST_AVATAR_PATH, 'rb') as avatar:
-            response = self.client.post(self.link, data={
-                'avatar': 'upload',
-                'image': avatar
-            })
+            response = self.client.post(self.link, data={'avatar': 'upload', 'image': avatar})
             self.assertEqual(response.status_code, 200)
             self.assertEqual(response.status_code, 200)
 
 
             response_json = response.json()
             response_json = response.json()
             self.assertTrue(response_json['crop_tmp'])
             self.assertTrue(response_json['crop_tmp'])
             self.assertEqual(
             self.assertEqual(
-                self.get_current_user().avatar_tmp.url, response_json['crop_tmp']['url'])
+                self.get_current_user().avatar_tmp.url, response_json['crop_tmp']['url']
+            )
 
 
         avatar = Path(self.get_current_user().avatar_tmp.path)
         avatar = Path(self.get_current_user().avatar_tmp.path)
         self.assertTrue(avatar.exists())
         self.assertTrue(avatar.exists())
         self.assertTrue(avatar.isfile())
         self.assertTrue(avatar.isfile())
 
 
-        response = self.client.post(self.link, json.dumps({
-            'avatar': 'crop_tmp',
-            'crop': {
-                'offset': {
-                    'x': 0, 'y': 0
+        response = self.client.post(
+            self.link,
+            json.dumps({
+                'avatar': 'crop_tmp',
+                'crop': {
+                    'offset': {
+                        'x': 0,
+                        'y': 0
+                    },
+                    'zoom': 1,
                 },
                 },
-                'zoom': 1
-            }
-        }), content_type="application/json")
+            }),
+            content_type="application/json",
+        )
 
 
         response_json = response.json()
         response_json = response.json()
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
@@ -161,30 +161,39 @@ class UserAvatarTests(AuthenticatedUserTestCase):
         self.assertTrue(avatar.exists())
         self.assertTrue(avatar.exists())
         self.assertTrue(avatar.isfile())
         self.assertTrue(avatar.isfile())
 
 
-        response = self.client.post(self.link, json.dumps({
-            'avatar': 'crop_tmp',
-            'crop': {
-                'offset': {
-                    'x': 0, 'y': 0
+        response = self.client.post(
+            self.link,
+            json.dumps({
+                'avatar': 'crop_tmp',
+                'crop': {
+                    'offset': {
+                        'x': 0,
+                        'y': 0
+                    },
+                    'zoom': 1,
                 },
                 },
-                'zoom': 1
-            }
-        }), content_type="application/json")
+            }),
+            content_type="application/json",
+        )
         self.assertContains(response, "This avatar type is not allowed.", status_code=400)
         self.assertContains(response, "This avatar type is not allowed.", status_code=400)
 
 
-        response = self.client.post(self.link, json.dumps({
-            'avatar': 'crop_src',
-            'crop': {
-                'offset': {
-                    'x': 0, 'y': 0
+        response = self.client.post(
+            self.link,
+            json.dumps({
+                'avatar': 'crop_src',
+                'crop': {
+                    'offset': {
+                        'x': 0,
+                        'y': 0
+                    },
+                    'zoom': 1,
                 },
                 },
-                'zoom': 1
-            }
-        }), content_type="application/json")
+            }),
+            content_type="application/json",
+        )
         self.assertContains(response, "Avatar was re-cropped.")
         self.assertContains(response, "Avatar was re-cropped.")
 
 
         # delete user avatars, test if it deletes src and tmp
         # delete user avatars, test if it deletes src and tmp
-        user = self.get_current_user()
         store.delete_avatar(self.get_current_user())
         store.delete_avatar(self.get_current_user())
 
 
         self.assertTrue(self.get_current_user().avatar_src.path)
         self.assertTrue(self.get_current_user().avatar_src.path)
@@ -198,13 +207,9 @@ class UserAvatarTests(AuthenticatedUserTestCase):
         response = self.client.get(self.link)
         response = self.client.get(self.link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response = self.client.post(self.link, data={
-            'avatar': 'galleries',
-            'image': 123
-        })
+        response = self.client.post(self.link, data={'avatar': 'galleries', 'image': 123})
 
 
-        self.assertContains(
-            response, "This avatar type is not allowed.", status_code=400)
+        self.assertContains(response, "This avatar type is not allowed.", status_code=400)
 
 
     def test_gallery_image_validation(self):
     def test_gallery_image_validation(self):
         """gallery validates image to set"""
         """gallery validates image to set"""
@@ -214,16 +219,22 @@ class UserAvatarTests(AuthenticatedUserTestCase):
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         # no image id is handled
         # no image id is handled
-        response = self.client.post(self.link, data={
-            'avatar': 'galleries',
-        })
+        response = self.client.post(
+            self.link,
+            data={
+                'avatar': 'galleries',
+            },
+        )
         self.assertContains(response, "Incorrect image.", status_code=400)
         self.assertContains(response, "Incorrect image.", status_code=400)
 
 
         # invalid id is handled
         # invalid id is handled
-        response = self.client.post(self.link, data={
-            'avatar': 'galleries',
-            'image': 'asdsadsadsa'
-        })
+        response = self.client.post(
+            self.link,
+            data={
+                'avatar': 'galleries',
+                'image': 'asdsadsadsa',
+            },
+        )
         self.assertContains(response, "Incorrect image.", status_code=400)
         self.assertContains(response, "Incorrect image.", status_code=400)
 
 
         # nonexistant image is handled
         # nonexistant image is handled
@@ -234,21 +245,19 @@ class UserAvatarTests(AuthenticatedUserTestCase):
         self.assertTrue(options['galleries'])
         self.assertTrue(options['galleries'])
 
 
         test_avatar = options['galleries'][0]['images'][0]['id']
         test_avatar = options['galleries'][0]['images'][0]['id']
-        response = self.client.post(self.link, data={
-            'avatar': 'galleries',
-            'image': test_avatar + 5000
-        })
+        response = self.client.post(
+            self.link,
+            data={
+                'avatar': 'galleries',
+                'image': test_avatar + 5000,
+            },
+        )
         self.assertContains(response, "Incorrect image.", status_code=400)
         self.assertContains(response, "Incorrect image.", status_code=400)
 
 
         # default gallery image is handled
         # default gallery image is handled
-        AvatarGallery.objects.filter(pk=test_avatar).update(
-            gallery=gallery.DEFAULT_GALLERY
-        )
+        AvatarGallery.objects.filter(pk=test_avatar).update(gallery=gallery.DEFAULT_GALLERY)
 
 
-        response = self.client.post(self.link, data={
-            'avatar': 'galleries',
-            'image': test_avatar
-        })
+        response = self.client.post(self.link, data={'avatar': 'galleries', 'image': test_avatar})
         self.assertContains(response, "Incorrect image.", status_code=400)
         self.assertContains(response, "Incorrect image.", status_code=400)
 
 
     def test_gallery_set_valid_avatar(self):
     def test_gallery_set_valid_avatar(self):
@@ -262,23 +271,24 @@ class UserAvatarTests(AuthenticatedUserTestCase):
         self.assertTrue(options['galleries'])
         self.assertTrue(options['galleries'])
 
 
         test_avatar = options['galleries'][0]['images'][0]['id']
         test_avatar = options['galleries'][0]['images'][0]['id']
-        response = self.client.post(self.link, data={
-            'avatar': 'galleries',
-            'image': test_avatar
-        })
+        response = self.client.post(
+            self.link,
+            data={
+                'avatar': 'galleries',
+                'image': test_avatar,
+            },
+        )
 
 
         self.assertContains(response, "Avatar from gallery was set.")
         self.assertContains(response, "Avatar from gallery was set.")
 
 
 
 
 class UserAvatarModerationTests(AuthenticatedUserTestCase):
 class UserAvatarModerationTests(AuthenticatedUserTestCase):
-    """
-    tests for moderate user avatar RPC (/api/users/1/moderate-avatar/)
-    """
+    """tests for moderate user avatar RPC (/api/users/1/moderate-avatar/)"""
+
     def setUp(self):
     def setUp(self):
         super(UserAvatarModerationTests, self).setUp()
         super(UserAvatarModerationTests, self).setUp()
 
 
-        self.other_user = UserModel.objects.create_user(
-            "OtherUser", "other@user.com", "pass123")
+        self.other_user = UserModel.objects.create_user("OtherUser", "other@user.com", "pass123")
 
 
         self.link = '/api/users/%s/moderate-avatar/' % self.other_user.pk
         self.link = '/api/users/%s/moderate-avatar/' % self.other_user.pk
 
 
@@ -301,53 +311,56 @@ class UserAvatarModerationTests(AuthenticatedUserTestCase):
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         options = response.json()
         options = response.json()
+        self.assertEqual(options['is_avatar_locked'], self.other_user.is_avatar_locked)
         self.assertEqual(
         self.assertEqual(
-            options['is_avatar_locked'], self.other_user.is_avatar_locked)
-        self.assertEqual(
-            options['avatar_lock_user_message'], self.other_user.avatar_lock_user_message)
+            options['avatar_lock_user_message'], self.other_user.avatar_lock_user_message
+        )
         self.assertEqual(
         self.assertEqual(
-            options['avatar_lock_staff_message'], self.other_user.avatar_lock_staff_message)
+            options['avatar_lock_staff_message'], self.other_user.avatar_lock_staff_message
+        )
 
 
         override_acl(self.user, {
         override_acl(self.user, {
             'can_moderate_avatars': 1,
             'can_moderate_avatars': 1,
         })
         })
 
 
-        response = self.client.post(self.link, json.dumps({
-            'is_avatar_locked': True,
-            'avatar_lock_user_message': "Test user message.",
-            'avatar_lock_staff_message': "Test staff message.",
-        }),
-        content_type="application/json")
+        response = self.client.post(
+            self.link,
+            json.dumps({
+                'is_avatar_locked': True,
+                'avatar_lock_user_message': "Test user message.",
+                'avatar_lock_staff_message': "Test staff message.",
+            }),
+            content_type="application/json",
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         other_user = UserModel.objects.get(pk=self.other_user.pk)
         other_user = UserModel.objects.get(pk=self.other_user.pk)
 
 
         options = response.json()
         options = response.json()
         self.assertEqual(other_user.is_avatar_locked, True)
         self.assertEqual(other_user.is_avatar_locked, True)
-        self.assertEqual(
-            other_user.avatar_lock_user_message, "Test user message.")
-        self.assertEqual(
-            other_user.avatar_lock_staff_message, "Test staff message.")
+        self.assertEqual(other_user.avatar_lock_user_message, "Test user message.")
+        self.assertEqual(other_user.avatar_lock_staff_message, "Test staff message.")
 
 
+        self.assertEqual(options['avatars'], other_user.avatars)
+        self.assertEqual(options['is_avatar_locked'], other_user.is_avatar_locked)
+        self.assertEqual(options['avatar_lock_user_message'], other_user.avatar_lock_user_message)
         self.assertEqual(
         self.assertEqual(
-            options['avatars'], other_user.avatars)
-        self.assertEqual(
-            options['is_avatar_locked'], other_user.is_avatar_locked)
-        self.assertEqual(
-            options['avatar_lock_user_message'], other_user.avatar_lock_user_message)
-        self.assertEqual(
-            options['avatar_lock_staff_message'], other_user.avatar_lock_staff_message)
+            options['avatar_lock_staff_message'], other_user.avatar_lock_staff_message
+        )
 
 
         override_acl(self.user, {
         override_acl(self.user, {
             'can_moderate_avatars': 1,
             'can_moderate_avatars': 1,
         })
         })
 
 
-        response = self.client.post(self.link, json.dumps({
-            'is_avatar_locked': False,
-            'avatar_lock_user_message': None,
-            'avatar_lock_staff_message': None,
-        }),
-        content_type="application/json")
+        response = self.client.post(
+            self.link,
+            json.dumps({
+                'is_avatar_locked': False,
+                'avatar_lock_user_message': None,
+                'avatar_lock_staff_message': None,
+            }),
+            content_type="application/json",
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         other_user = UserModel.objects.get(pk=self.other_user.pk)
         other_user = UserModel.objects.get(pk=self.other_user.pk)
@@ -356,25 +369,26 @@ class UserAvatarModerationTests(AuthenticatedUserTestCase):
         self.assertIsNone(other_user.avatar_lock_staff_message)
         self.assertIsNone(other_user.avatar_lock_staff_message)
 
 
         options = response.json()
         options = response.json()
+        self.assertEqual(options['avatars'], other_user.avatars)
+        self.assertEqual(options['is_avatar_locked'], other_user.is_avatar_locked)
+        self.assertEqual(options['avatar_lock_user_message'], other_user.avatar_lock_user_message)
         self.assertEqual(
         self.assertEqual(
-            options['avatars'], other_user.avatars)
-        self.assertEqual(
-            options['is_avatar_locked'], other_user.is_avatar_locked)
-        self.assertEqual(
-            options['avatar_lock_user_message'], other_user.avatar_lock_user_message)
-        self.assertEqual(
-            options['avatar_lock_staff_message'], other_user.avatar_lock_staff_message)
+            options['avatar_lock_staff_message'], other_user.avatar_lock_staff_message
+        )
 
 
         override_acl(self.user, {
         override_acl(self.user, {
             'can_moderate_avatars': 1,
             'can_moderate_avatars': 1,
         })
         })
 
 
-        response = self.client.post(self.link, json.dumps({
-            'is_avatar_locked': True,
-            'avatar_lock_user_message': '',
-            'avatar_lock_staff_message': '',
-        }),
-        content_type="application/json")
+        response = self.client.post(
+            self.link,
+            json.dumps({
+                'is_avatar_locked': True,
+                'avatar_lock_user_message': '',
+                'avatar_lock_staff_message': '',
+            }),
+            content_type="application/json",
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         other_user = UserModel.objects.get(pk=self.other_user.pk)
         other_user = UserModel.objects.get(pk=self.other_user.pk)
@@ -383,23 +397,24 @@ class UserAvatarModerationTests(AuthenticatedUserTestCase):
         self.assertEqual(other_user.avatar_lock_staff_message, '')
         self.assertEqual(other_user.avatar_lock_staff_message, '')
 
 
         options = response.json()
         options = response.json()
+        self.assertEqual(options['avatars'], other_user.avatars)
+        self.assertEqual(options['is_avatar_locked'], other_user.is_avatar_locked)
+        self.assertEqual(options['avatar_lock_user_message'], other_user.avatar_lock_user_message)
         self.assertEqual(
         self.assertEqual(
-            options['avatars'], other_user.avatars)
-        self.assertEqual(
-            options['is_avatar_locked'], other_user.is_avatar_locked)
-        self.assertEqual(
-            options['avatar_lock_user_message'], other_user.avatar_lock_user_message)
-        self.assertEqual(
-            options['avatar_lock_staff_message'], other_user.avatar_lock_staff_message)
+            options['avatar_lock_staff_message'], other_user.avatar_lock_staff_message
+        )
 
 
         override_acl(self.user, {
         override_acl(self.user, {
             'can_moderate_avatars': 1,
             'can_moderate_avatars': 1,
         })
         })
 
 
-        response = self.client.post(self.link, json.dumps({
-            'is_avatar_locked': False,
-        }),
-        content_type="application/json")
+        response = self.client.post(
+            self.link,
+            json.dumps({
+                'is_avatar_locked': False,
+            }),
+            content_type="application/json",
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         other_user = UserModel.objects.get(pk=self.other_user.pk)
         other_user = UserModel.objects.get(pk=self.other_user.pk)
@@ -408,14 +423,12 @@ class UserAvatarModerationTests(AuthenticatedUserTestCase):
         self.assertEqual(other_user.avatar_lock_staff_message, '')
         self.assertEqual(other_user.avatar_lock_staff_message, '')
 
 
         options = response.json()
         options = response.json()
+        self.assertEqual(options['avatars'], other_user.avatars)
+        self.assertEqual(options['is_avatar_locked'], other_user.is_avatar_locked)
+        self.assertEqual(options['avatar_lock_user_message'], other_user.avatar_lock_user_message)
         self.assertEqual(
         self.assertEqual(
-            options['avatars'], other_user.avatars)
-        self.assertEqual(
-            options['is_avatar_locked'], other_user.is_avatar_locked)
-        self.assertEqual(
-            options['avatar_lock_user_message'], other_user.avatar_lock_user_message)
-        self.assertEqual(
-            options['avatar_lock_staff_message'], other_user.avatar_lock_staff_message)
+            options['avatar_lock_staff_message'], other_user.avatar_lock_staff_message
+        )
 
 
     def test_moderate_own_avatar(self):
     def test_moderate_own_avatar(self):
         """moderate own avatar"""
         """moderate own avatar"""
@@ -423,5 +436,5 @@ class UserAvatarModerationTests(AuthenticatedUserTestCase):
             'can_moderate_avatars': 1,
             'can_moderate_avatars': 1,
         })
         })
 
 
-        response = self.client.get( '/api/users/%s/moderate-avatar/' % self.user.pk)
+        response = self.client.get('/api/users/%s/moderate-avatar/' % self.user.pk)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)

+ 53 - 40
misago/users/tests/test_user_changeemail_api.py

@@ -9,9 +9,8 @@ UserModel = get_user_model()
 
 
 
 
 class UserChangeEmailTests(AuthenticatedUserTestCase):
 class UserChangeEmailTests(AuthenticatedUserTestCase):
-    """
-    tests for user change email RPC (/api/users/1/change-email/)
-    """
+    """tests for user change email RPC (/api/users/1/change-email/)"""
+
     def setUp(self):
     def setUp(self):
         super(UserChangeEmailTests, self).setUp()
         super(UserChangeEmailTests, self).setUp()
         self.link = '/api/users/%s/change-email/' % self.user.pk
         self.link = '/api/users/%s/change-email/' % self.user.pk
@@ -26,67 +25,76 @@ class UserChangeEmailTests(AuthenticatedUserTestCase):
         response = self.client.post(self.link, data={})
         response = self.client.post(self.link, data={})
 
 
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'new_email': [
-                "This field is required."
-            ],
-            'password': [
-                "This field is required."
-            ],
-        })
+        self.assertEqual(
+            response.json(), {
+                'new_email': ["This field is required."],
+                'password': ["This field is required."],
+            }
+        )
 
 
     def test_invalid_password(self):
     def test_invalid_password(self):
         """api errors correctly for invalid password"""
         """api errors correctly for invalid password"""
-        response = self.client.post(self.link, data={
-            'new_email': 'new@email.com',
-            'password': 'Lor3mIpsum'
-        })
+        response = self.client.post(
+            self.link,
+            data={
+                'new_email': 'new@email.com',
+                'password': 'Lor3mIpsum',
+            },
+        )
         self.assertContains(response, 'password is invalid', status_code=400)
         self.assertContains(response, 'password is invalid', status_code=400)
 
 
     def test_invalid_input(self):
     def test_invalid_input(self):
         """api errors correctly for invalid input"""
         """api errors correctly for invalid input"""
-        response = self.client.post(self.link, data={
-            'new_email': '',
-            'password': self.USER_PASSWORD
-        })
+        response = self.client.post(
+            self.link,
+            data={
+                'new_email': '',
+                'password': self.USER_PASSWORD,
+            },
+        )
 
 
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.json(), {
         self.assertEqual(response.json(), {
-            'new_email': [
-                "This field may not be blank."
-            ],
+            'new_email': ["This field may not be blank."],
         })
         })
 
 
-        response = self.client.post(self.link, data={
-            'new_email': 'newmail',
-            'password': self.USER_PASSWORD
-        })
+        response = self.client.post(
+            self.link,
+            data={
+                'new_email': 'newmail',
+                'password': self.USER_PASSWORD,
+            },
+        )
 
 
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.json(), {
         self.assertEqual(response.json(), {
-            'new_email': [
-                "Enter a valid email address."
-            ],
+            'new_email': ["Enter a valid email address."],
         })
         })
 
 
     def test_email_taken(self):
     def test_email_taken(self):
         """api validates email usage"""
         """api validates email usage"""
         UserModel.objects.create_user('BobBoberson', 'new@email.com', 'Pass.123')
         UserModel.objects.create_user('BobBoberson', 'new@email.com', 'Pass.123')
 
 
-        response = self.client.post(self.link, data={
-            'new_email': 'new@email.com',
-            'password': self.USER_PASSWORD
-        })
+        response = self.client.post(
+            self.link,
+            data={
+                'new_email': 'new@email.com',
+                'password': self.USER_PASSWORD,
+            },
+        )
         self.assertContains(response, 'not available', status_code=400)
         self.assertContains(response, 'not available', status_code=400)
 
 
     def test_change_email(self):
     def test_change_email(self):
         """api allows users to change their e-mail addresses"""
         """api allows users to change their e-mail addresses"""
         new_email = 'new@email.com'
         new_email = 'new@email.com'
 
 
-        response = self.client.post(self.link, data={
-            'new_email': new_email,
-            'password': self.USER_PASSWORD
-        })
+        response = self.client.post(
+            self.link,
+            data={
+                'new_email': new_email,
+                'password': self.USER_PASSWORD,
+            },
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         self.assertIn('Confirm e-mail change', mail.outbox[0].subject)
         self.assertIn('Confirm e-mail change', mail.outbox[0].subject)
@@ -97,9 +105,14 @@ class UserChangeEmailTests(AuthenticatedUserTestCase):
         else:
         else:
             self.fail("E-mail sent didn't contain confirmation url")
             self.fail("E-mail sent didn't contain confirmation url")
 
 
-        response = self.client.get(reverse('misago:options-confirm-email-change', kwargs={
-            'token': token
-        }))
+        response = self.client.get(
+            reverse(
+                'misago:options-confirm-email-change',
+                kwargs={
+                    'token': token,
+                },
+            )
+        )
 
 
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 

+ 49 - 42
misago/users/tests/test_user_changepassword_api.py

@@ -1,4 +1,3 @@
-from django.contrib.auth import get_user_model
 from django.core import mail
 from django.core import mail
 from django.urls import reverse
 from django.urls import reverse
 
 
@@ -6,9 +5,8 @@ from misago.users.testutils import AuthenticatedUserTestCase
 
 
 
 
 class UserChangePasswordTests(AuthenticatedUserTestCase):
 class UserChangePasswordTests(AuthenticatedUserTestCase):
-    """
-    tests for user change password RPC (/api/users/1/change-password/)
-    """
+    """tests for user change password RPC (/api/users/1/change-password/)"""
+
     def setUp(self):
     def setUp(self):
         super(UserChangePasswordTests, self).setUp()
         super(UserChangePasswordTests, self).setUp()
         self.link = '/api/users/%s/change-password/' % self.user.pk
         self.link = '/api/users/%s/change-password/' % self.user.pk
@@ -23,65 +21,72 @@ class UserChangePasswordTests(AuthenticatedUserTestCase):
         response = self.client.post(self.link, data={})
         response = self.client.post(self.link, data={})
 
 
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'new_password': [
-                "This field is required."
-            ],
-            'password': [
-                "This field is required."
-            ],
-        })
+        self.assertEqual(
+            response.json(), {
+                'new_password': ["This field is required."],
+                'password': ["This field is required."],
+            }
+        )
 
 
     def test_invalid_password(self):
     def test_invalid_password(self):
         """api errors correctly for invalid password"""
         """api errors correctly for invalid password"""
-        response = self.client.post(self.link, data={
-            'new_password': 'N3wP@55w0rd',
-            'password': 'Lor3mIpsum'
-        })
+        response = self.client.post(
+            self.link,
+            data={
+                'new_password': 'N3wP@55w0rd',
+                'password': 'Lor3mIpsum',
+            },
+        )
 
 
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.json(), {
         self.assertEqual(response.json(), {
-            'password': [
-                "Entered password is invalid."
-            ],
+            'password': ["Entered password is invalid."],
         })
         })
 
 
     def test_blank_input(self):
     def test_blank_input(self):
         """api errors correctly for blank input"""
         """api errors correctly for blank input"""
-        response = self.client.post(self.link, data={
-            'new_password': '',
-            'password': self.USER_PASSWORD
-        })
+        response = self.client.post(
+            self.link,
+            data={
+                'new_password': '',
+                'password': self.USER_PASSWORD,
+            },
+        )
 
 
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.json(), {
         self.assertEqual(response.json(), {
-            'new_password': [
-                "This field may not be blank."
-            ],
+            'new_password': ["This field may not be blank."],
         })
         })
 
 
     def test_short_new_pasword(self):
     def test_short_new_pasword(self):
         """api errors correctly for short new password"""
         """api errors correctly for short new password"""
-        response = self.client.post(self.link, data={
-            'new_password': 'n',
-            'password': self.USER_PASSWORD
-        })
+        response = self.client.post(
+            self.link,
+            data={
+                'new_password': 'n',
+                'password': self.USER_PASSWORD,
+            },
+        )
 
 
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'new_password': [
-                "This password is too short. It must contain at least 7 characters."
-            ],
-        })
+        self.assertEqual(
+            response.json(), {
+                'new_password':
+                    ["This password is too short. It must contain at least 7 characters."],
+            }
+        )
 
 
     def test_change_password(self):
     def test_change_password(self):
         """api allows users to change their passwords"""
         """api allows users to change their passwords"""
         new_password = 'N3wP@55w0rd'
         new_password = 'N3wP@55w0rd'
 
 
-        response = self.client.post(self.link, data={
-            'new_password': new_password,
-            'password': self.USER_PASSWORD
-        })
+        response = self.client.post(
+            self.link,
+            data={
+                'new_password': new_password,
+                'password': self.USER_PASSWORD,
+            },
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         self.assertIn('Confirm password change', mail.outbox[0].subject)
         self.assertIn('Confirm password change', mail.outbox[0].subject)
@@ -92,9 +97,11 @@ class UserChangePasswordTests(AuthenticatedUserTestCase):
         else:
         else:
             self.fail("E-mail sent didn't contain confirmation url")
             self.fail("E-mail sent didn't contain confirmation url")
 
 
-        response = self.client.get(reverse('misago:options-confirm-password-change', kwargs={
-            'token': token
-        }))
+        response = self.client.get(
+            reverse('misago:options-confirm-password-change', kwargs={
+                'token': token,
+            })
+        )
 
 
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 

+ 68 - 49
misago/users/tests/test_user_create_api.py

@@ -11,9 +11,8 @@ UserModel = get_user_model()
 
 
 
 
 class UserCreateTests(UserTestCase):
 class UserCreateTests(UserTestCase):
-    """
-    tests for new user registration (POST to /api/users/)
-    """
+    """tests for new user registration (POST to /api/users/)"""
+
     def setUp(self):
     def setUp(self):
         super(UserCreateTests, self).setUp()
         super(UserCreateTests, self).setUp()
         self.api_link = '/api/users/'
         self.api_link = '/api/users/'
@@ -40,43 +39,48 @@ class UserCreateTests(UserTestCase):
         """api validates usernames"""
         """api validates usernames"""
         user = self.get_authenticated_user()
         user = self.get_authenticated_user()
 
 
-        response = self.client.post(self.api_link, data={
-            'username': user.username,
-            'email': 'loremipsum@dolor.met',
-            'password': 'LoremP4ssword'
-        })
+        response = self.client.post(
+            self.api_link,
+            data={
+                'username': user.username,
+                'email': 'loremipsum@dolor.met',
+                'password': 'LoremP4ssword',
+            },
+        )
 
 
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.json(), {
         self.assertEqual(response.json(), {
-            'username': [
-                "This username is not available."
-            ]
+            'username': ["This username is not available."],
         })
         })
 
 
     def test_registration_validates_email(self):
     def test_registration_validates_email(self):
         """api validates usernames"""
         """api validates usernames"""
         user = self.get_authenticated_user()
         user = self.get_authenticated_user()
 
 
-        response = self.client.post(self.api_link, data={
-            'username': 'totallyNew',
-            'email': user.email,
-            'password': 'LoremP4ssword'
-        })
+        response = self.client.post(
+            self.api_link,
+            data={
+                'username': 'totallyNew',
+                'email': user.email,
+                'password': 'LoremP4ssword',
+            },
+        )
 
 
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.json(), {
         self.assertEqual(response.json(), {
-            'email': [
-                "This e-mail address is not available."
-            ]
+            'email': ["This e-mail address is not available."],
         })
         })
 
 
     def test_registration_validates_password(self):
     def test_registration_validates_password(self):
         """api uses django's validate_password to validate registrations"""
         """api uses django's validate_password to validate registrations"""
-        response = self.client.post(self.api_link, data={
-            'username': 'Bob',
-            'email': 'l.o.r.e.m.i.p.s.u.m@gmail.com',
-            'password': '123'
-        })
+        response = self.client.post(
+            self.api_link,
+            data={
+                'username': 'Bob',
+                'email': 'l.o.r.e.m.i.p.s.u.m@gmail.com',
+                'password': '123',
+            },
+        )
 
 
         self.assertContains(response, "password is too short", status_code=400)
         self.assertContains(response, "password is too short", status_code=400)
         self.assertContains(response, "password is entirely numeric", status_code=400)
         self.assertContains(response, "password is entirely numeric", status_code=400)
@@ -84,21 +88,27 @@ class UserCreateTests(UserTestCase):
 
 
     def test_registration_validates_password_similiarity(self):
     def test_registration_validates_password_similiarity(self):
         """api uses validate_password to validate registrations"""
         """api uses validate_password to validate registrations"""
-        response = self.client.post(self.api_link, data={
-            'username': 'BobBoberson',
-            'email': 'l.o.r.e.m.i.p.s.u.m@gmail.com',
-            'password': 'BobBoberson'
-        })
+        response = self.client.post(
+            self.api_link,
+            data={
+                'username': 'BobBoberson',
+                'email': 'l.o.r.e.m.i.p.s.u.m@gmail.com',
+                'password': 'BobBoberson',
+            },
+        )
 
 
         self.assertContains(response, "password is too similar to the username", status_code=400)
         self.assertContains(response, "password is too similar to the username", status_code=400)
 
 
     def test_registration_calls_validate_new_registration(self):
     def test_registration_calls_validate_new_registration(self):
         """api uses validate_new_registration to validate registrations"""
         """api uses validate_new_registration to validate registrations"""
-        response = self.client.post(self.api_link, data={
-            'username': 'Bob',
-            'email': 'l.o.r.e.m.i.p.s.u.m@gmail.com',
-            'password': 'pas123'
-        })
+        response = self.client.post(
+            self.api_link,
+            data={
+                'username': 'Bob',
+                'email': 'l.o.r.e.m.i.p.s.u.m@gmail.com',
+                'password': 'pas123',
+            },
+        )
 
 
         self.assertContains(response, "email is not allowed", status_code=400)
         self.assertContains(response, "email is not allowed", status_code=400)
 
 
@@ -106,11 +116,14 @@ class UserCreateTests(UserTestCase):
         """api creates active and signed in user on POST"""
         """api creates active and signed in user on POST"""
         settings.override_setting('account_activation', 'none')
         settings.override_setting('account_activation', 'none')
 
 
-        response = self.client.post(self.api_link, data={
-            'username': 'Bob',
-            'email': 'bob@bob.com',
-            'password': 'pass123'
-        })
+        response = self.client.post(
+            self.api_link,
+            data={
+                'username': 'Bob',
+                'email': 'bob@bob.com',
+                'password': 'pass123',
+            },
+        )
 
 
         self.assertContains(response, 'active')
         self.assertContains(response, 'active')
         self.assertContains(response, 'Bob')
         self.assertContains(response, 'Bob')
@@ -130,11 +143,14 @@ class UserCreateTests(UserTestCase):
         """api creates inactive user on POST"""
         """api creates inactive user on POST"""
         settings.override_setting('account_activation', 'user')
         settings.override_setting('account_activation', 'user')
 
 
-        response = self.client.post(self.api_link, data={
-            'username': 'Bob',
-            'email': 'bob@bob.com',
-            'password': 'pass123'
-        })
+        response = self.client.post(
+            self.api_link,
+            data={
+                'username': 'Bob',
+                'email': 'bob@bob.com',
+                'password': 'pass123',
+            },
+        )
 
 
         self.assertContains(response, 'user')
         self.assertContains(response, 'user')
         self.assertContains(response, 'Bob')
         self.assertContains(response, 'Bob')
@@ -149,11 +165,14 @@ class UserCreateTests(UserTestCase):
         """api creates admin activated user on POST"""
         """api creates admin activated user on POST"""
         settings.override_setting('account_activation', 'admin')
         settings.override_setting('account_activation', 'admin')
 
 
-        response = self.client.post(self.api_link, data={
-            'username': 'Bob',
-            'email': 'bob@bob.com',
-            'password': 'pass123'
-        })
+        response = self.client.post(
+            self.api_link,
+            data={
+                'username': 'Bob',
+                'email': 'bob@bob.com',
+                'password': 'pass123',
+            },
+        )
 
 
         self.assertContains(response, 'admin')
         self.assertContains(response, 'admin')
         self.assertContains(response, 'Bob')
         self.assertContains(response, 'Bob')

+ 66 - 14
misago/users/tests/test_user_feeds_api.py

@@ -8,17 +8,29 @@ class UserThreadsApiTests(ThreadsApiTestCase):
     def setUp(self):
     def setUp(self):
         super(UserThreadsApiTests, self).setUp()
         super(UserThreadsApiTests, self).setUp()
 
 
-        self.api_link = reverse('misago:api:user-threads', kwargs={'pk': self.user.pk})
+        self.api_link = reverse(
+            'misago:api:user-threads', kwargs={
+                'pk': self.user.pk,
+            }
+        )
 
 
     def test_invalid_user_id(self):
     def test_invalid_user_id(self):
         """api validates user id"""
         """api validates user id"""
-        link = reverse('misago:api:user-threads', kwargs={'pk': 'abcd'})
+        link = reverse(
+            'misago:api:user-threads', kwargs={
+                'pk': 'abcd',
+            }
+        )
         response = self.client.get(link)
         response = self.client.get(link)
         self.assertEqual(response.status_code, 404)
         self.assertEqual(response.status_code, 404)
 
 
     def test_nonexistant_user_id(self):
     def test_nonexistant_user_id(self):
         """api validates that user for id exists"""
         """api validates that user for id exists"""
-        link = reverse('misago:api:user-threads', kwargs={'pk': self.user.pk + 1})
+        link = reverse(
+            'misago:api:user-threads', kwargs={
+                'pk': self.user.pk + 1,
+            }
+        )
         response = self.client.get(link)
         response = self.client.get(link)
         self.assertEqual(response.status_code, 404)
         self.assertEqual(response.status_code, 404)
 
 
@@ -38,7 +50,11 @@ class UserThreadsApiTests(ThreadsApiTestCase):
 
 
     def test_user_event(self):
     def test_user_event(self):
         """events don't show in feeds at all"""
         """events don't show in feeds at all"""
-        testutils.reply_thread(self.thread, poster=self.user, is_event=True)
+        testutils.reply_thread(
+            self.thread,
+            poster=self.user,
+            is_event=True,
+        )
 
 
         response = self.client.get(self.api_link)
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
@@ -46,7 +62,10 @@ class UserThreadsApiTests(ThreadsApiTestCase):
 
 
     def test_user_thread(self):
     def test_user_thread(self):
         """user thread shows in feed"""
         """user thread shows in feed"""
-        thread = testutils.post_thread(category=self.category, poster=self.user)
+        thread = testutils.post_thread(
+            category=self.category,
+            poster=self.user,
+        )
 
 
         # this post will not show in feed
         # this post will not show in feed
         testutils.reply_thread(thread, poster=self.user)
         testutils.reply_thread(thread, poster=self.user)
@@ -58,7 +77,10 @@ class UserThreadsApiTests(ThreadsApiTestCase):
 
 
     def test_user_thread_anonymous(self):
     def test_user_thread_anonymous(self):
         """user thread shows in feed requested by unauthenticated user"""
         """user thread shows in feed requested by unauthenticated user"""
-        thread = testutils.post_thread(category=self.category, poster=self.user)
+        thread = testutils.post_thread(
+            category=self.category,
+            poster=self.user,
+        )
 
 
         self.logout_user()
         self.logout_user()
 
 
@@ -72,17 +94,29 @@ class UserPostsApiTests(ThreadsApiTestCase):
     def setUp(self):
     def setUp(self):
         super(UserPostsApiTests, self).setUp()
         super(UserPostsApiTests, self).setUp()
 
 
-        self.api_link = reverse('misago:api:user-posts', kwargs={'pk': self.user.pk})
+        self.api_link = reverse(
+            'misago:api:user-posts', kwargs={
+                'pk': self.user.pk,
+            }
+        )
 
 
     def test_invalid_user_id(self):
     def test_invalid_user_id(self):
         """api validates user id"""
         """api validates user id"""
-        link = reverse('misago:api:user-posts', kwargs={'pk': 'abcd'})
+        link = reverse(
+            'misago:api:user-posts', kwargs={
+                'pk': 'abcd',
+            }
+        )
         response = self.client.get(link)
         response = self.client.get(link)
         self.assertEqual(response.status_code, 404)
         self.assertEqual(response.status_code, 404)
 
 
     def test_nonexistant_user_id(self):
     def test_nonexistant_user_id(self):
         """api validates that user for id exists"""
         """api validates that user for id exists"""
-        link = reverse('misago:api:user-posts', kwargs={'pk': self.user.pk + 1})
+        link = reverse(
+            'misago:api:user-posts', kwargs={
+                'pk': self.user.pk + 1,
+            }
+        )
         response = self.client.get(link)
         response = self.client.get(link)
         self.assertEqual(response.status_code, 404)
         self.assertEqual(response.status_code, 404)
 
 
@@ -94,7 +128,11 @@ class UserPostsApiTests(ThreadsApiTestCase):
 
 
     def test_user_event(self):
     def test_user_event(self):
         """events don't show in feeds at all"""
         """events don't show in feeds at all"""
-        testutils.reply_thread(self.thread, poster=self.user, is_event=True)
+        testutils.reply_thread(
+            self.thread,
+            poster=self.user,
+            is_event=True,
+        )
 
 
         response = self.client.get(self.api_link)
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
@@ -102,7 +140,11 @@ class UserPostsApiTests(ThreadsApiTestCase):
 
 
     def test_user_hidden_post(self):
     def test_user_hidden_post(self):
         """hidden posts don't show in feeds at all"""
         """hidden posts don't show in feeds at all"""
-        post = testutils.reply_thread(self.thread, poster=self.user, is_hidden=True)
+        testutils.reply_thread(
+            self.thread,
+            poster=self.user,
+            is_hidden=True,
+        )
 
 
         response = self.client.get(self.api_link)
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
@@ -110,7 +152,11 @@ class UserPostsApiTests(ThreadsApiTestCase):
 
 
     def test_user_unapproved_post(self):
     def test_user_unapproved_post(self):
         """unapproved posts don't show in feeds at all"""
         """unapproved posts don't show in feeds at all"""
-        post = testutils.reply_thread(self.thread, poster=self.user, is_unapproved=True)
+        testutils.reply_thread(
+            self.thread,
+            poster=self.user,
+            is_unapproved=True,
+        )
 
 
         response = self.client.get(self.api_link)
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
@@ -129,7 +175,10 @@ class UserPostsApiTests(ThreadsApiTestCase):
 
 
     def test_user_thread(self):
     def test_user_thread(self):
         """user thread shows in feed"""
         """user thread shows in feed"""
-        thread = testutils.post_thread(category=self.category, poster=self.user)
+        thread = testutils.post_thread(
+            category=self.category,
+            poster=self.user,
+        )
         post = testutils.reply_thread(thread, poster=self.user)
         post = testutils.reply_thread(thread, poster=self.user)
 
 
         response = self.client.get(self.api_link)
         response = self.client.get(self.api_link)
@@ -153,7 +202,10 @@ class UserPostsApiTests(ThreadsApiTestCase):
 
 
     def test_user_thread_anonymous(self):
     def test_user_thread_anonymous(self):
         """user thread shows in feed requested by unauthenticated user"""
         """user thread shows in feed requested by unauthenticated user"""
-        thread = testutils.post_thread(category=self.category, poster=self.user)
+        thread = testutils.post_thread(
+            category=self.category,
+            poster=self.user,
+        )
         post = testutils.reply_thread(thread, poster=self.user)
         post = testutils.reply_thread(thread, poster=self.user)
 
 
         self.logout_user()
         self.logout_user()

+ 6 - 2
misago/users/tests/test_user_model.py

@@ -8,8 +8,12 @@ from misago.users.models import User
 class UserManagerTests(TestCase):
 class UserManagerTests(TestCase):
     def test_create_user(self):
     def test_create_user(self):
         """create_user created new user account successfully"""
         """create_user created new user account successfully"""
-        user = User.objects.create_user('Bob', 'bob@test.com', 'Pass.123',
-                                        set_default_avatar=True)
+        user = User.objects.create_user(
+            'Bob',
+            'bob@test.com',
+            'Pass.123',
+            set_default_avatar=True,
+        )
 
 
         db_user = User.objects.get(id=user.pk)
         db_user = User.objects.get(id=user.pk)
 
 

+ 27 - 27
misago/users/tests/test_user_signature_api.py

@@ -1,17 +1,10 @@
-import json
-
-from django.contrib.auth import get_user_model
-from django.utils.encoding import smart_str
-
 from misago.acl.testutils import override_acl
 from misago.acl.testutils import override_acl
-from misago.conf import settings
 from misago.users.testutils import AuthenticatedUserTestCase
 from misago.users.testutils import AuthenticatedUserTestCase
 
 
 
 
 class UserSignatureTests(AuthenticatedUserTestCase):
 class UserSignatureTests(AuthenticatedUserTestCase):
-    """
-    tests for user signature RPC (POST to /api/users/1/signature/)
-    """
+    """tests for user signature RPC (POST to /api/users/1/signature/)"""
+
     def setUp(self):
     def setUp(self):
         super(UserSignatureTests, self).setUp()
         super(UserSignatureTests, self).setUp()
         self.link = '/api/users/%s/signature/' % self.user.pk
         self.link = '/api/users/%s/signature/' % self.user.pk
@@ -50,8 +43,7 @@ class UserSignatureTests(AuthenticatedUserTestCase):
         response = self.client.get(self.link)
         response = self.client.get(self.link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response_json = json.loads(smart_str(response.content))
-        self.assertFalse(response_json['signature'])
+        self.assertFalse(response.json()['signature'])
 
 
     def test_post_empty_signature(self):
     def test_post_empty_signature(self):
         """empty POST empties user signature"""
         """empty POST empties user signature"""
@@ -62,11 +54,15 @@ class UserSignatureTests(AuthenticatedUserTestCase):
         self.user.is_signature_locked = False
         self.user.is_signature_locked = False
         self.user.save()
         self.user.save()
 
 
-        response = self.client.post(self.link, data={'signature': ''})
+        response = self.client.post(
+            self.link,
+            data={
+                'signature': '',
+            },
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response_json = json.loads(smart_str(response.content))
-        self.assertFalse(response_json['signature'])
+        self.assertFalse(response.json()['signature'])
 
 
     def test_post_too_long_signature(self):
     def test_post_too_long_signature(self):
         """too long new signature errors"""
         """too long new signature errors"""
@@ -77,9 +73,12 @@ class UserSignatureTests(AuthenticatedUserTestCase):
         self.user.is_signature_locked = False
         self.user.is_signature_locked = False
         self.user.save()
         self.user.save()
 
 
-        response = self.client.post(self.link, data={
-            'signature': 'abcd' * 1000
-        })
+        response = self.client.post(
+            self.link,
+            data={
+                'signature': 'abcd' * 1000,
+            },
+        )
         self.assertContains(response, 'too long', status_code=400)
         self.assertContains(response, 'too long', status_code=400)
 
 
     def test_post_good_signature(self):
     def test_post_good_signature(self):
@@ -91,18 +90,19 @@ class UserSignatureTests(AuthenticatedUserTestCase):
         self.user.is_signature_locked = False
         self.user.is_signature_locked = False
         self.user.save()
         self.user.save()
 
 
-        response = self.client.post(self.link, data={
-            'signature': 'Hello, **bros**!'
-        })
+        response = self.client.post(
+            self.link,
+            data={
+                'signature': 'Hello, **bros**!',
+            },
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response_json = json.loads(smart_str(response.content))
-        self.assertEqual(response_json['signature']['html'],
-                         '<p>Hello, <strong>bros</strong>!</p>')
-        self.assertEqual(response_json['signature']['plain'],
-                         'Hello, **bros**!')
+        self.assertEqual(
+            response.json()['signature']['html'], '<p>Hello, <strong>bros</strong>!</p>'
+        )
+        self.assertEqual(response.json()['signature']['plain'], 'Hello, **bros**!')
 
 
         self.reload_user()
         self.reload_user()
 
 
-        self.assertEqual(self.user.signature_parsed,
-                         '<p>Hello, <strong>bros</strong>!</p>')
+        self.assertEqual(self.user.signature_parsed, '<p>Hello, <strong>bros</strong>!</p>')

+ 66 - 53
misago/users/tests/test_user_username_api.py

@@ -1,8 +1,6 @@
 import json
 import json
 
 
 from django.contrib.auth import get_user_model
 from django.contrib.auth import get_user_model
-from django.utils.encoding import smart_str
-from django.utils.six.moves import range
 
 
 from misago.acl.testutils import override_acl
 from misago.acl.testutils import override_acl
 from misago.conf import settings
 from misago.conf import settings
@@ -13,9 +11,8 @@ UserModel = get_user_model()
 
 
 
 
 class UserUsernameTests(AuthenticatedUserTestCase):
 class UserUsernameTests(AuthenticatedUserTestCase):
-    """
-    tests for user change name RPC (POST to /api/users/1/username/)
-    """
+    """tests for user change name RPC (POST to /api/users/1/username/)"""
+
     def setUp(self):
     def setUp(self):
         super(UserUsernameTests, self).setUp()
         super(UserUsernameTests, self).setUp()
         self.link = '/api/users/%s/username/' % self.user.pk
         self.link = '/api/users/%s/username/' % self.user.pk
@@ -25,13 +22,11 @@ class UserUsernameTests(AuthenticatedUserTestCase):
         response = self.client.get(self.link)
         response = self.client.get(self.link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response_json = json.loads(smart_str(response.content))
+        response_json = response.json()
 
 
         self.assertIsNotNone(response_json['changes_left'])
         self.assertIsNotNone(response_json['changes_left'])
-        self.assertEqual(response_json['length_min'],
-                         settings.username_length_min)
-        self.assertEqual(response_json['length_max'],
-                         settings.username_length_max)
+        self.assertEqual(response_json['length_min'], settings.username_length_min)
+        self.assertEqual(response_json['length_max'], settings.username_length_max)
         self.assertIsNone(response_json['next_on'])
         self.assertIsNone(response_json['next_on'])
 
 
         for i in range(response_json['changes_left']):
         for i in range(response_json['changes_left']):
@@ -40,7 +35,7 @@ class UserUsernameTests(AuthenticatedUserTestCase):
         response = self.client.get(self.link)
         response = self.client.get(self.link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response_json = json.loads(smart_str(response.content))
+        response_json = response.json()
         self.assertEqual(response_json['changes_left'], 0)
         self.assertEqual(response_json['changes_left'], 0)
         self.assertIsNotNone(response_json['next_on'])
         self.assertIsNotNone(response_json['next_on'])
 
 
@@ -49,17 +44,20 @@ class UserUsernameTests(AuthenticatedUserTestCase):
         response = self.client.get(self.link)
         response = self.client.get(self.link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response_json = json.loads(smart_str(response.content))
+        response_json = response.json()
         for i in range(response_json['changes_left']):
         for i in range(response_json['changes_left']):
             self.user.set_username('NewName%s' % i, self.user)
             self.user.set_username('NewName%s' % i, self.user)
 
 
         response = self.client.get(self.link)
         response = self.client.get(self.link)
-        response_json = json.loads(smart_str(response.content))
+        response_json = response.json()
         self.assertEqual(response_json['changes_left'], 0)
         self.assertEqual(response_json['changes_left'], 0)
 
 
-        response = self.client.post(self.link, data={
-            'username': 'Pointless'
-        })
+        response = self.client.post(
+            self.link,
+            data={
+                'username': 'Pointless',
+            },
+        )
 
 
         self.assertContains(response, 'change your username now', status_code=400)
         self.assertContains(response, 'change your username now', status_code=400)
         self.assertTrue(self.user.username != 'Pointless')
         self.assertTrue(self.user.username != 'Pointless')
@@ -72,44 +70,48 @@ class UserUsernameTests(AuthenticatedUserTestCase):
 
 
     def test_change_username_invalid_name(self):
     def test_change_username_invalid_name(self):
         """api returns error 400 if new username is wrong"""
         """api returns error 400 if new username is wrong"""
-        response = self.client.post(self.link, data={
-            'username': '####'
-        })
+        response = self.client.post(
+            self.link,
+            data={
+                'username': '####',
+            },
+        )
 
 
         self.assertContains(response, 'can only contain latin', status_code=400)
         self.assertContains(response, 'can only contain latin', status_code=400)
 
 
     def test_change_username(self):
     def test_change_username(self):
         """api changes username and records change"""
         """api changes username and records change"""
         response = self.client.get(self.link)
         response = self.client.get(self.link)
-        changes_left = json.loads(smart_str(response.content))['changes_left']
+        changes_left = response.json()['changes_left']
 
 
-        username = self.user.username
+        old_username = self.user.username
         new_username = 'NewUsernamu'
         new_username = 'NewUsernamu'
 
 
-        response = self.client.post(self.link, data={
-            'username': new_username
-        })
+        response = self.client.post(
+            self.link,
+            data={
+                'username': new_username,
+            },
+        )
 
 
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
-        options = json.loads(smart_str(response.content))['options']
+        options = response.json()['options']
         self.assertEqual(changes_left, options['changes_left'] + 1)
         self.assertEqual(changes_left, options['changes_left'] + 1)
 
 
         self.reload_user()
         self.reload_user()
         self.assertEqual(self.user.username, new_username)
         self.assertEqual(self.user.username, new_username)
+        self.assertTrue(self.user.username != old_username)
 
 
-        self.assertEqual(self.user.namechanges.last().new_username,
-                         new_username)
+        self.assertEqual(self.user.namechanges.last().new_username, new_username)
 
 
 
 
 class UserUsernameModerationTests(AuthenticatedUserTestCase):
 class UserUsernameModerationTests(AuthenticatedUserTestCase):
-    """
-    tests for moderate username RPC (/api/users/1/moderate-username/)
-    """
+    """tests for moderate username RPC (/api/users/1/moderate-username/)"""
+
     def setUp(self):
     def setUp(self):
         super(UserUsernameModerationTests, self).setUp()
         super(UserUsernameModerationTests, self).setUp()
 
 
-        self.other_user = UserModel.objects.create_user(
-            "OtherUser", "other@user.com", "pass123")
+        self.other_user = UserModel.objects.create_user("OtherUser", "other@user.com", "pass123")
 
 
         self.link = '/api/users/%s/moderate-username/' % self.other_user.pk
         self.link = '/api/users/%s/moderate-username/' % self.other_user.pk
 
 
@@ -138,20 +140,21 @@ class UserUsernameModerationTests(AuthenticatedUserTestCase):
         response = self.client.get(self.link)
         response = self.client.get(self.link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        options = json.loads(smart_str(response.content))
-        self.assertEqual(options['length_min'],
-                         settings.username_length_min)
-        self.assertEqual(options['length_max'],
-                         settings.username_length_max)
+        options = response.json()
+        self.assertEqual(options['length_min'], settings.username_length_min)
+        self.assertEqual(options['length_max'], settings.username_length_max)
 
 
         override_acl(self.user, {
         override_acl(self.user, {
             'can_rename_users': 1,
             'can_rename_users': 1,
         })
         })
 
 
-        response = self.client.post(self.link, json.dumps({
+        response = self.client.post(
+            self.link,
+            json.dumps({
                 'username': '',
                 'username': '',
             }),
             }),
-            content_type="application/json")
+            content_type='application/json',
+        )
 
 
         self.assertContains(response, "Enter new username", status_code=400)
         self.assertContains(response, "Enter new username", status_code=400)
 
 
@@ -159,37 +162,48 @@ class UserUsernameModerationTests(AuthenticatedUserTestCase):
             'can_rename_users': 1,
             'can_rename_users': 1,
         })
         })
 
 
-        response = self.client.post(self.link, json.dumps({
+        response = self.client.post(
+            self.link,
+            json.dumps({
                 'username': '$$$',
                 'username': '$$$',
             }),
             }),
-            content_type="application/json")
+            content_type='application/json',
+        )
 
 
-        self.assertContains(response,
+        self.assertContains(
+            response,
             "Username can only contain latin alphabet letters and digits.",
             "Username can only contain latin alphabet letters and digits.",
-            status_code=400)
+            status_code=400
+        )
 
 
         override_acl(self.user, {
         override_acl(self.user, {
             'can_rename_users': 1,
             'can_rename_users': 1,
         })
         })
 
 
-        response = self.client.post(self.link, json.dumps({
+        response = self.client.post(
+            self.link,
+            json.dumps({
                 'username': 'a',
                 'username': 'a',
             }),
             }),
-            content_type="application/json")
+            content_type='application/json',
+        )
 
 
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
-        self.assertContains(response,
-            "Username must be at least 3 characters long.",
-            status_code=400)
+        self.assertContains(
+            response, "Username must be at least 3 characters long.", status_code=400
+        )
 
 
         override_acl(self.user, {
         override_acl(self.user, {
             'can_rename_users': 1,
             'can_rename_users': 1,
         })
         })
 
 
-        response = self.client.post(self.link, json.dumps({
+        response = self.client.post(
+            self.link,
+            json.dumps({
                 'username': 'BobBoberson',
                 'username': 'BobBoberson',
             }),
             }),
-            content_type="application/json")
+            content_type='application/json',
+        )
 
 
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
@@ -198,7 +212,7 @@ class UserUsernameModerationTests(AuthenticatedUserTestCase):
         self.assertEqual('BobBoberson', other_user.username)
         self.assertEqual('BobBoberson', other_user.username)
         self.assertEqual('bobboberson', other_user.slug)
         self.assertEqual('bobboberson', other_user.slug)
 
 
-        options = json.loads(smart_str(response.content))
+        options = response.json()
         self.assertEqual(options['username'], other_user.username)
         self.assertEqual(options['username'], other_user.username)
         self.assertEqual(options['slug'], other_user.slug)
         self.assertEqual(options['slug'], other_user.slug)
 
 
@@ -208,6 +222,5 @@ class UserUsernameModerationTests(AuthenticatedUserTestCase):
             'can_rename_users': 1,
             'can_rename_users': 1,
         })
         })
 
 
-        response = self.client.get(
-            '/api/users/%s/moderate-username/' % self.user.pk)
+        response = self.client.get('/api/users/%s/moderate-username/' % self.user.pk)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)

+ 265 - 239
misago/users/tests/test_useradmin_views.py

@@ -1,11 +1,7 @@
-import json
-
 from django.contrib.auth import get_user_model
 from django.contrib.auth import get_user_model
 from django.core import mail
 from django.core import mail
 from django.urls import reverse
 from django.urls import reverse
 from django.utils import six
 from django.utils import six
-from django.utils.encoding import smart_str
-from django.utils.six.moves import range
 
 
 from misago.acl.models import Role
 from misago.acl.models import Role
 from misago.admin.testutils import AdminTestCase
 from misago.admin.testutils import AdminTestCase
@@ -28,8 +24,7 @@ class UserAdminViewsTests(AdminTestCase):
 
 
     def test_list_view(self):
     def test_list_view(self):
         """users list view returns 200"""
         """users list view returns 200"""
-        response = self.client.get(
-            reverse('misago:admin:users:accounts:index'))
+        response = self.client.get(reverse('misago:admin:users:accounts:index'))
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
         response = self.client.get(response['location'])
         response = self.client.get(response['location'])
@@ -38,8 +33,7 @@ class UserAdminViewsTests(AdminTestCase):
 
 
     def test_list_search(self):
     def test_list_search(self):
         """users list is searchable"""
         """users list is searchable"""
-        response = self.client.get(
-            reverse('misago:admin:users:accounts:index'))
+        response = self.client.get(reverse('misago:admin:users:accounts:index'))
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
         link_base = response['location']
         link_base = response['location']
@@ -86,17 +80,23 @@ class UserAdminViewsTests(AdminTestCase):
                 'Bob%s' % i,
                 'Bob%s' % i,
                 'bob%s@test.com' % i,
                 'bob%s@test.com' % i,
                 'pass123',
                 'pass123',
-                requires_activation=1
+                requires_activation=1,
             )
             )
             user_pks.append(test_user.pk)
             user_pks.append(test_user.pk)
 
 
         response = self.client.post(
         response = self.client.post(
             reverse('misago:admin:users:accounts:index'),
             reverse('misago:admin:users:accounts:index'),
-            data={'action': 'activate', 'selected_items': user_pks})
+            data={
+                'action': 'activate',
+                'selected_items': user_pks,
+            }
+        )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
-        inactive_qs = UserModel.objects.filter(id__in=user_pks,
-                                          requires_activation=1)
+        inactive_qs = UserModel.objects.filter(
+            id__in=user_pks,
+            requires_activation=1,
+        )
         self.assertEqual(inactive_qs.count(), 0)
         self.assertEqual(inactive_qs.count(), 0)
         self.assertIn("has been activated", mail.outbox[0].subject)
         self.assertIn("has been activated", mail.outbox[0].subject)
 
 
@@ -108,13 +108,17 @@ class UserAdminViewsTests(AdminTestCase):
                 'Bob%s' % i,
                 'Bob%s' % i,
                 'bob%s@test.com' % i,
                 'bob%s@test.com' % i,
                 'pass123',
                 'pass123',
-                requires_activation=1
+                requires_activation=1,
             )
             )
             user_pks.append(test_user.pk)
             user_pks.append(test_user.pk)
 
 
         response = self.client.post(
         response = self.client.post(
             reverse('misago:admin:users:accounts:index'),
             reverse('misago:admin:users:accounts:index'),
-            data={'action': 'ban', 'selected_items': user_pks})
+            data={
+                'action': 'ban',
+                'selected_items': user_pks,
+            }
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         response = self.client.post(
         response = self.client.post(
@@ -122,12 +126,10 @@ class UserAdminViewsTests(AdminTestCase):
             data={
             data={
                 'action': 'ban',
                 'action': 'ban',
                 'selected_items': user_pks,
                 'selected_items': user_pks,
-                'ban_type': [
-                    'usernames', 'emails', 'domains',
-                    'ip', 'ip_first', 'ip_two'
-                ],
-                'finalize': ''
-            })
+                'ban_type': ['usernames', 'emails', 'domains', 'ip', 'ip_first', 'ip_two'],
+                'finalize': '',
+            },
+        )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
         self.assertEqual(Ban.objects.count(), 24)
         self.assertEqual(Ban.objects.count(), 24)
 
 
@@ -139,13 +141,17 @@ class UserAdminViewsTests(AdminTestCase):
                 'Bob%s' % i,
                 'Bob%s' % i,
                 'bob%s@test.com' % i,
                 'bob%s@test.com' % i,
                 'pass123',
                 'pass123',
-                requires_activation=1
+                requires_activation=1,
             )
             )
             user_pks.append(test_user.pk)
             user_pks.append(test_user.pk)
 
 
         response = self.client.post(
         response = self.client.post(
             reverse('misago:admin:users:accounts:index'),
             reverse('misago:admin:users:accounts:index'),
-            data={'action': 'delete_accounts', 'selected_items': user_pks})
+            data={
+                'action': 'delete_accounts',
+                'selected_items': user_pks,
+            }
+        )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
         self.assertEqual(UserModel.objects.count(), 1)
         self.assertEqual(UserModel.objects.count(), 1)
 
 
@@ -157,34 +163,39 @@ class UserAdminViewsTests(AdminTestCase):
                 'Bob%s' % i,
                 'Bob%s' % i,
                 'bob%s@test.com' % i,
                 'bob%s@test.com' % i,
                 'pass123',
                 'pass123',
-                requires_activation=1
+                requires_activation=1,
             )
             )
             user_pks.append(test_user.pk)
             user_pks.append(test_user.pk)
 
 
         response = self.client.post(
         response = self.client.post(
             reverse('misago:admin:users:accounts:index'),
             reverse('misago:admin:users:accounts:index'),
-            data={'action': 'delete_accounts', 'selected_items': user_pks})
+            data={
+                'action': 'delete_accounts',
+                'selected_items': user_pks,
+            }
+        )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
         self.assertEqual(UserModel.objects.count(), 1)
         self.assertEqual(UserModel.objects.count(), 1)
 
 
     def test_new_view(self):
     def test_new_view(self):
         """new user view creates account"""
         """new user view creates account"""
-        response = self.client.get(
-            reverse('misago:admin:users:accounts:new'))
+        response = self.client.get(reverse('misago:admin:users:accounts:new'))
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         default_rank = Rank.objects.get_default()
         default_rank = Rank.objects.get_default()
         authenticated_role = Role.objects.get(special_role='authenticated')
         authenticated_role = Role.objects.get(special_role='authenticated')
 
 
-        response = self.client.post(reverse('misago:admin:users:accounts:new'),
+        response = self.client.post(
+            reverse('misago:admin:users:accounts:new'),
             data={
             data={
                 'username': 'Bawww',
                 'username': 'Bawww',
                 'rank': six.text_type(default_rank.pk),
                 'rank': six.text_type(default_rank.pk),
                 'roles': six.text_type(authenticated_role.pk),
                 'roles': six.text_type(authenticated_role.pk),
                 'email': 'reg@stered.com',
                 'email': 'reg@stered.com',
                 'new_password': 'pass123',
                 'new_password': 'pass123',
-                'staff_level': '0'
-            })
+                'staff_level': '0',
+            }
+        )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
         UserModel.objects.get_by_username('Bawww')
         UserModel.objects.get_by_username('Bawww')
@@ -193,28 +204,34 @@ class UserAdminViewsTests(AdminTestCase):
     def test_edit_view(self):
     def test_edit_view(self):
         """edit user view changes account"""
         """edit user view changes account"""
         test_user = UserModel.objects.create_user('Bob', 'bob@test.com', 'pass123')
         test_user = UserModel.objects.create_user('Bob', 'bob@test.com', 'pass123')
-        test_link = reverse('misago:admin:users:accounts:edit',
-                            kwargs={'pk': test_user.pk})
+        test_link = reverse(
+            'misago:admin:users:accounts:edit', kwargs={
+                'pk': test_user.pk,
+            }
+        )
 
 
         response = self.client.get(test_link)
         response = self.client.get(test_link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response = self.client.post(test_link, data={
-            'username': 'Bawww',
-            'rank': six.text_type(test_user.rank_id),
-            'roles': six.text_type(test_user.roles.all()[0].pk),
-            'email': 'reg@stered.com',
-            'new_password': 'newpass123',
-            'staff_level': '0',
-            'signature': 'Hello world!',
-            'is_signature_locked': '1',
-            'is_hiding_presence': '0',
-            'limits_private_thread_invites_to': '0',
-            'signature_lock_staff_message': 'Staff message',
-            'signature_lock_user_message': 'User message',
-            'subscribe_to_started_threads': '2',
-            'subscribe_to_replied_threads': '2',
-        })
+        response = self.client.post(
+            test_link,
+            data={
+                'username': 'Bawww',
+                'rank': six.text_type(test_user.rank_id),
+                'roles': six.text_type(test_user.roles.all()[0].pk),
+                'email': 'reg@stered.com',
+                'new_password': 'newpass123',
+                'staff_level': '0',
+                'signature': 'Hello world!',
+                'is_signature_locked': '1',
+                'is_hiding_presence': '0',
+                'limits_private_thread_invites_to': '0',
+                'signature_lock_staff_message': 'Staff message',
+                'signature_lock_user_message': 'User message',
+                'subscribe_to_started_threads': '2',
+                'subscribe_to_replied_threads': '2',
+            }
+        )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
         updated_user = UserModel.objects.get(pk=test_user.pk)
         updated_user = UserModel.objects.get(pk=test_user.pk)
@@ -232,27 +249,33 @@ class UserAdminViewsTests(AdminTestCase):
         This is regression test for issue #640
         This is regression test for issue #640
         """
         """
         test_user = UserModel.objects.create_user('Bob', 'bob@test.com', 'pass123')
         test_user = UserModel.objects.create_user('Bob', 'bob@test.com', 'pass123')
-        test_link = reverse('misago:admin:users:accounts:edit',
-                            kwargs={'pk': test_user.pk})
+        test_link = reverse(
+            'misago:admin:users:accounts:edit', kwargs={
+                'pk': test_user.pk,
+            }
+        )
 
 
         response = self.client.get(test_link)
         response = self.client.get(test_link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response = self.client.post(test_link, data={
-            'username': 'Bob',
-            'rank': six.text_type(test_user.rank_id),
-            'roles': six.text_type(test_user.roles.all()[0].pk),
-            'email': 'reg@stered.com',
-            'new_password': 'pass123',
-            'signature': 'Hello world!',
-            'is_signature_locked': '1',
-            'is_hiding_presence': '0',
-            'limits_private_thread_invites_to': '0',
-            'signature_lock_staff_message': 'Staff message',
-            'signature_lock_user_message': 'User message',
-            'subscribe_to_started_threads': '2',
-            'subscribe_to_replied_threads': '2',
-        })
+        response = self.client.post(
+            test_link,
+            data={
+                'username': 'Bob',
+                'rank': six.text_type(test_user.rank_id),
+                'roles': six.text_type(test_user.roles.all()[0].pk),
+                'email': 'reg@stered.com',
+                'new_password': 'pass123',
+                'signature': 'Hello world!',
+                'is_signature_locked': '1',
+                'is_hiding_presence': '0',
+                'limits_private_thread_invites_to': '0',
+                'signature_lock_staff_message': 'Staff message',
+                'signature_lock_user_message': 'User message',
+                'subscribe_to_started_threads': '2',
+                'subscribe_to_replied_threads': '2',
+            }
+        )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
         updated_user = UserModel.objects.get(pk=test_user.pk)
         updated_user = UserModel.objects.get(pk=test_user.pk)
@@ -263,30 +286,36 @@ class UserAdminViewsTests(AdminTestCase):
     def test_edit_make_admin(self):
     def test_edit_make_admin(self):
         """edit user view allows super admin to make other user admin"""
         """edit user view allows super admin to make other user admin"""
         test_user = UserModel.objects.create_user('Bob', 'bob@test.com', 'pass123')
         test_user = UserModel.objects.create_user('Bob', 'bob@test.com', 'pass123')
-        test_link = reverse('misago:admin:users:accounts:edit',
-                            kwargs={'pk': test_user.pk})
+        test_link = reverse(
+            'misago:admin:users:accounts:edit', kwargs={
+                'pk': test_user.pk,
+            }
+        )
 
 
         response = self.client.get(test_link)
         response = self.client.get(test_link)
         self.assertContains(response, 'id="id_is_staff_1"')
         self.assertContains(response, 'id="id_is_staff_1"')
         self.assertContains(response, 'id="id_is_superuser_1"')
         self.assertContains(response, 'id="id_is_superuser_1"')
 
 
-        response = self.client.post(test_link, data={
-            'username': 'Bawww',
-            'rank': six.text_type(test_user.rank_id),
-            'roles': six.text_type(test_user.roles.all()[0].pk),
-            'email': 'reg@stered.com',
-            'new_password': 'pass123',
-            'is_staff': '1',
-            'is_superuser': '0',
-            'signature': 'Hello world!',
-            'is_signature_locked': '1',
-            'is_hiding_presence': '0',
-            'limits_private_thread_invites_to': '0',
-            'signature_lock_staff_message': 'Staff message',
-            'signature_lock_user_message': 'User message',
-            'subscribe_to_started_threads': '2',
-            'subscribe_to_replied_threads': '2',
-        })
+        response = self.client.post(
+            test_link,
+            data={
+                'username': 'Bawww',
+                'rank': six.text_type(test_user.rank_id),
+                'roles': six.text_type(test_user.roles.all()[0].pk),
+                'email': 'reg@stered.com',
+                'new_password': 'pass123',
+                'is_staff': '1',
+                'is_superuser': '0',
+                'signature': 'Hello world!',
+                'is_signature_locked': '1',
+                'is_hiding_presence': '0',
+                'limits_private_thread_invites_to': '0',
+                'signature_lock_staff_message': 'Staff message',
+                'signature_lock_user_message': 'User message',
+                'subscribe_to_started_threads': '2',
+                'subscribe_to_replied_threads': '2',
+            }
+        )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
         updated_user = UserModel.objects.get(pk=test_user.pk)
         updated_user = UserModel.objects.get(pk=test_user.pk)
@@ -296,30 +325,36 @@ class UserAdminViewsTests(AdminTestCase):
     def test_edit_make_superadmin_admin(self):
     def test_edit_make_superadmin_admin(self):
         """edit user view allows super admin to make other user super admin"""
         """edit user view allows super admin to make other user super admin"""
         test_user = UserModel.objects.create_user('Bob', 'bob@test.com', 'pass123')
         test_user = UserModel.objects.create_user('Bob', 'bob@test.com', 'pass123')
-        test_link = reverse('misago:admin:users:accounts:edit',
-                            kwargs={'pk': test_user.pk})
+        test_link = reverse(
+            'misago:admin:users:accounts:edit', kwargs={
+                'pk': test_user.pk,
+            }
+        )
 
 
         response = self.client.get(test_link)
         response = self.client.get(test_link)
         self.assertContains(response, 'id="id_is_staff_1"')
         self.assertContains(response, 'id="id_is_staff_1"')
         self.assertContains(response, 'id="id_is_superuser_1"')
         self.assertContains(response, 'id="id_is_superuser_1"')
 
 
-        response = self.client.post(test_link, data={
-            'username': 'Bawww',
-            'rank': six.text_type(test_user.rank_id),
-            'roles': six.text_type(test_user.roles.all()[0].pk),
-            'email': 'reg@stered.com',
-            'new_password': 'pass123',
-            'is_staff': '0',
-            'is_superuser': '1',
-            'signature': 'Hello world!',
-            'is_signature_locked': '1',
-            'is_hiding_presence': '0',
-            'limits_private_thread_invites_to': '0',
-            'signature_lock_staff_message': 'Staff message',
-            'signature_lock_user_message': 'User message',
-            'subscribe_to_started_threads': '2',
-            'subscribe_to_replied_threads': '2',
-        })
+        response = self.client.post(
+            test_link,
+            data={
+                'username': 'Bawww',
+                'rank': six.text_type(test_user.rank_id),
+                'roles': six.text_type(test_user.roles.all()[0].pk),
+                'email': 'reg@stered.com',
+                'new_password': 'pass123',
+                'is_staff': '0',
+                'is_superuser': '1',
+                'signature': 'Hello world!',
+                'is_signature_locked': '1',
+                'is_hiding_presence': '0',
+                'limits_private_thread_invites_to': '0',
+                'signature_lock_staff_message': 'Staff message',
+                'signature_lock_user_message': 'User message',
+                'subscribe_to_started_threads': '2',
+                'subscribe_to_replied_threads': '2',
+            }
+        )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
         updated_user = UserModel.objects.get(pk=test_user.pk)
         updated_user = UserModel.objects.get(pk=test_user.pk)
@@ -332,30 +367,36 @@ class UserAdminViewsTests(AdminTestCase):
         self.user.save()
         self.user.save()
 
 
         test_user = UserModel.objects.create_user('Bob', 'bob@test.com', 'pass123')
         test_user = UserModel.objects.create_user('Bob', 'bob@test.com', 'pass123')
-        test_link = reverse('misago:admin:users:accounts:edit',
-                            kwargs={'pk': test_user.pk})
+        test_link = reverse(
+            'misago:admin:users:accounts:edit', kwargs={
+                'pk': test_user.pk,
+            }
+        )
 
 
         response = self.client.get(test_link)
         response = self.client.get(test_link)
         self.assertNotContains(response, 'id="id_is_staff_1"')
         self.assertNotContains(response, 'id="id_is_staff_1"')
         self.assertNotContains(response, 'id="id_is_superuser_1"')
         self.assertNotContains(response, 'id="id_is_superuser_1"')
 
 
-        response = self.client.post(test_link, data={
-            'username': 'Bawww',
-            'rank': six.text_type(test_user.rank_id),
-            'roles': six.text_type(test_user.roles.all()[0].pk),
-            'email': 'reg@stered.com',
-            'new_password': 'pass123',
-            'is_staff': '1',
-            'is_superuser': '1',
-            'signature': 'Hello world!',
-            'is_signature_locked': '1',
-            'is_hiding_presence': '0',
-            'limits_private_thread_invites_to': '0',
-            'signature_lock_staff_message': 'Staff message',
-            'signature_lock_user_message': 'User message',
-            'subscribe_to_started_threads': '2',
-            'subscribe_to_replied_threads': '2',
-        })
+        response = self.client.post(
+            test_link,
+            data={
+                'username': 'Bawww',
+                'rank': six.text_type(test_user.rank_id),
+                'roles': six.text_type(test_user.roles.all()[0].pk),
+                'email': 'reg@stered.com',
+                'new_password': 'pass123',
+                'is_staff': '1',
+                'is_superuser': '1',
+                'signature': 'Hello world!',
+                'is_signature_locked': '1',
+                'is_hiding_presence': '0',
+                'limits_private_thread_invites_to': '0',
+                'signature_lock_staff_message': 'Staff message',
+                'signature_lock_user_message': 'User message',
+                'subscribe_to_started_threads': '2',
+                'subscribe_to_replied_threads': '2',
+            }
+        )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
         updated_user = UserModel.objects.get(pk=test_user.pk)
         updated_user = UserModel.objects.get(pk=test_user.pk)
@@ -368,32 +409,38 @@ class UserAdminViewsTests(AdminTestCase):
         self.user.save()
         self.user.save()
 
 
         test_user = UserModel.objects.create_user('Bob', 'bob@test.com', 'pass123')
         test_user = UserModel.objects.create_user('Bob', 'bob@test.com', 'pass123')
-        test_link = reverse('misago:admin:users:accounts:edit',
-                            kwargs={'pk': test_user.pk})
+        test_link = reverse(
+            'misago:admin:users:accounts:edit', kwargs={
+                'pk': test_user.pk,
+            }
+        )
 
 
         response = self.client.get(test_link)
         response = self.client.get(test_link)
         self.assertContains(response, 'id="id_is_active_1"')
         self.assertContains(response, 'id="id_is_active_1"')
         self.assertContains(response, 'id="id_is_active_staff_message"')
         self.assertContains(response, 'id="id_is_active_staff_message"')
 
 
-        response = self.client.post(test_link, data={
-            'username': 'Bawww',
-            'rank': six.text_type(test_user.rank_id),
-            'roles': six.text_type(test_user.roles.all()[0].pk),
-            'email': 'reg@stered.com',
-            'new_password': 'pass123',
-            'is_staff': '0',
-            'is_superuser': '0',
-            'signature': 'Hello world!',
-            'is_signature_locked': '1',
-            'is_hiding_presence': '0',
-            'limits_private_thread_invites_to': '0',
-            'signature_lock_staff_message': 'Staff message',
-            'signature_lock_user_message': 'User message',
-            'subscribe_to_started_threads': '2',
-            'subscribe_to_replied_threads': '2',
-            'is_active': '0',
-            'is_active_staff_message': "Disabled in test!"
-        })
+        response = self.client.post(
+            test_link,
+            data={
+                'username': 'Bawww',
+                'rank': six.text_type(test_user.rank_id),
+                'roles': six.text_type(test_user.roles.all()[0].pk),
+                'email': 'reg@stered.com',
+                'new_password': 'pass123',
+                'is_staff': '0',
+                'is_superuser': '0',
+                'signature': 'Hello world!',
+                'is_signature_locked': '1',
+                'is_hiding_presence': '0',
+                'limits_private_thread_invites_to': '0',
+                'signature_lock_staff_message': 'Staff message',
+                'signature_lock_user_message': 'User message',
+                'subscribe_to_started_threads': '2',
+                'subscribe_to_replied_threads': '2',
+                'is_active': '0',
+                'is_active_staff_message': "Disabled in test!"
+            }
+        )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
         updated_user = UserModel.objects.get(pk=test_user.pk)
         updated_user = UserModel.objects.get(pk=test_user.pk)
@@ -410,32 +457,38 @@ class UserAdminViewsTests(AdminTestCase):
         test_user.is_staff = True
         test_user.is_staff = True
         test_user.save()
         test_user.save()
 
 
-        test_link = reverse('misago:admin:users:accounts:edit',
-                            kwargs={'pk': test_user.pk})
+        test_link = reverse(
+            'misago:admin:users:accounts:edit', kwargs={
+                'pk': test_user.pk,
+            }
+        )
 
 
         response = self.client.get(test_link)
         response = self.client.get(test_link)
         self.assertContains(response, 'id="id_is_active_1"')
         self.assertContains(response, 'id="id_is_active_1"')
         self.assertContains(response, 'id="id_is_active_staff_message"')
         self.assertContains(response, 'id="id_is_active_staff_message"')
 
 
-        response = self.client.post(test_link, data={
-            'username': 'Bawww',
-            'rank': six.text_type(test_user.rank_id),
-            'roles': six.text_type(test_user.roles.all()[0].pk),
-            'email': 'reg@stered.com',
-            'new_password': 'pass123',
-            'is_staff': '1',
-            'is_superuser': '0',
-            'signature': 'Hello world!',
-            'is_signature_locked': '1',
-            'is_hiding_presence': '0',
-            'limits_private_thread_invites_to': '0',
-            'signature_lock_staff_message': 'Staff message',
-            'signature_lock_user_message': 'User message',
-            'subscribe_to_started_threads': '2',
-            'subscribe_to_replied_threads': '2',
-            'is_active': '0',
-            'is_active_staff_message': "Disabled in test!"
-        })
+        response = self.client.post(
+            test_link,
+            data={
+                'username': 'Bawww',
+                'rank': six.text_type(test_user.rank_id),
+                'roles': six.text_type(test_user.roles.all()[0].pk),
+                'email': 'reg@stered.com',
+                'new_password': 'pass123',
+                'is_staff': '1',
+                'is_superuser': '0',
+                'signature': 'Hello world!',
+                'is_signature_locked': '1',
+                'is_hiding_presence': '0',
+                'limits_private_thread_invites_to': '0',
+                'signature_lock_staff_message': 'Staff message',
+                'signature_lock_user_message': 'User message',
+                'subscribe_to_started_threads': '2',
+                'subscribe_to_replied_threads': '2',
+                'is_active': '0',
+                'is_active_staff_message': "Disabled in test!"
+            }
+        )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
         updated_user = UserModel.objects.get(pk=test_user.pk)
         updated_user = UserModel.objects.get(pk=test_user.pk)
@@ -452,135 +505,108 @@ class UserAdminViewsTests(AdminTestCase):
         test_user.is_staff = True
         test_user.is_staff = True
         test_user.save()
         test_user.save()
 
 
-        test_link = reverse('misago:admin:users:accounts:edit',
-                            kwargs={'pk': test_user.pk})
+        test_link = reverse(
+            'misago:admin:users:accounts:edit', kwargs={
+                'pk': test_user.pk,
+            }
+        )
 
 
         response = self.client.get(test_link)
         response = self.client.get(test_link)
         self.assertNotContains(response, 'id="id_is_active_1"')
         self.assertNotContains(response, 'id="id_is_active_1"')
         self.assertNotContains(response, 'id="id_is_active_staff_message"')
         self.assertNotContains(response, 'id="id_is_active_staff_message"')
 
 
-        response = self.client.post(test_link, data={
-            'username': 'Bawww',
-            'rank': six.text_type(test_user.rank_id),
-            'roles': six.text_type(test_user.roles.all()[0].pk),
-            'email': 'reg@stered.com',
-            'new_password': 'pass123',
-            'is_staff': '1',
-            'is_superuser': '0',
-            'signature': 'Hello world!',
-            'is_signature_locked': '1',
-            'is_hiding_presence': '0',
-            'limits_private_thread_invites_to': '0',
-            'signature_lock_staff_message': 'Staff message',
-            'signature_lock_user_message': 'User message',
-            'subscribe_to_started_threads': '2',
-            'subscribe_to_replied_threads': '2',
-            'is_active': '0',
-            'is_active_staff_message': "Disabled in test!"
-        })
+        response = self.client.post(
+            test_link,
+            data={
+                'username': 'Bawww',
+                'rank': six.text_type(test_user.rank_id),
+                'roles': six.text_type(test_user.roles.all()[0].pk),
+                'email': 'reg@stered.com',
+                'new_password': 'pass123',
+                'is_staff': '1',
+                'is_superuser': '0',
+                'signature': 'Hello world!',
+                'is_signature_locked': '1',
+                'is_hiding_presence': '0',
+                'limits_private_thread_invites_to': '0',
+                'signature_lock_staff_message': 'Staff message',
+                'signature_lock_user_message': 'User message',
+                'subscribe_to_started_threads': '2',
+                'subscribe_to_replied_threads': '2',
+                'is_active': '0',
+                'is_active_staff_message': "Disabled in test!"
+            }
+        )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
         updated_user = UserModel.objects.get(pk=test_user.pk)
         updated_user = UserModel.objects.get(pk=test_user.pk)
         self.assertTrue(updated_user.is_active)
         self.assertTrue(updated_user.is_active)
         self.assertFalse(updated_user.is_active_staff_message)
         self.assertFalse(updated_user.is_active_staff_message)
 
 
-    def test_edit_superuser_disable_admin(self):
-        """edit user view allows superuser to disable admin"""
-        self.user.is_superuser = True
-        self.user.save()
-
-        test_user = UserModel.objects.create_user('Bob', 'bob@test.com', 'pass123')
-
-        test_user.is_staff = True
-        test_user.save()
-
-        test_link = reverse('misago:admin:users:accounts:edit',
-                            kwargs={'pk': test_user.pk})
-
-        response = self.client.get(test_link)
-        self.assertContains(response, 'id="id_is_active_1"')
-        self.assertContains(response, 'id="id_is_active_staff_message"')
-
-        response = self.client.post(test_link, data={
-            'username': 'Bawww',
-            'rank': six.text_type(test_user.rank_id),
-            'roles': six.text_type(test_user.roles.all()[0].pk),
-            'email': 'reg@stered.com',
-            'new_password': 'pass123',
-            'is_staff': '1',
-            'is_superuser': '0',
-            'signature': 'Hello world!',
-            'is_signature_locked': '1',
-            'is_hiding_presence': '0',
-            'limits_private_thread_invites_to': '0',
-            'signature_lock_staff_message': 'Staff message',
-            'signature_lock_user_message': 'User message',
-            'subscribe_to_started_threads': '2',
-            'subscribe_to_replied_threads': '2',
-            'is_active': '0',
-            'is_active_staff_message': "Disabled in test!"
-        })
-        self.assertEqual(response.status_code, 302)
-
-        updated_user = UserModel.objects.get(pk=test_user.pk)
-        self.assertFalse(updated_user.is_active)
-        self.assertEqual(updated_user.is_active_staff_message, "Disabled in test!")
-
     def test_delete_threads_view(self):
     def test_delete_threads_view(self):
         """delete user threads view deletes threads"""
         """delete user threads view deletes threads"""
         test_user = UserModel.objects.create_user('Bob', 'bob@test.com', 'pass123')
         test_user = UserModel.objects.create_user('Bob', 'bob@test.com', 'pass123')
-        test_link = reverse('misago:admin:users:accounts:delete-threads',
-                            kwargs={'pk': test_user.pk})
+        test_link = reverse(
+            'misago:admin:users:accounts:delete-threads', kwargs={
+                'pk': test_user.pk,
+            }
+        )
 
 
         category = Category.objects.all_categories()[:1][0]
         category = Category.objects.all_categories()[:1][0]
-        [post_thread(category, poster=test_user) for i in range(10)]
+        [post_thread(category, poster=test_user) for _ in range(10)]
 
 
         response = self.client.post(test_link, **self.AJAX_HEADER)
         response = self.client.post(test_link, **self.AJAX_HEADER)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response_dict = json.loads(smart_str(response.content))
+        response_dict = response.json()
         self.assertEqual(response_dict['deleted_count'], 10)
         self.assertEqual(response_dict['deleted_count'], 10)
         self.assertFalse(response_dict['is_completed'])
         self.assertFalse(response_dict['is_completed'])
 
 
         response = self.client.post(test_link, **self.AJAX_HEADER)
         response = self.client.post(test_link, **self.AJAX_HEADER)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response_dict = json.loads(smart_str(response.content))
+        response_dict = response.json()
         self.assertEqual(response_dict['deleted_count'], 0)
         self.assertEqual(response_dict['deleted_count'], 0)
         self.assertTrue(response_dict['is_completed'])
         self.assertTrue(response_dict['is_completed'])
 
 
     def test_delete_posts_view(self):
     def test_delete_posts_view(self):
         """delete user posts view deletes posts"""
         """delete user posts view deletes posts"""
         test_user = UserModel.objects.create_user('Bob', 'bob@test.com', 'pass123')
         test_user = UserModel.objects.create_user('Bob', 'bob@test.com', 'pass123')
-        test_link = reverse('misago:admin:users:accounts:delete-posts',
-                            kwargs={'pk': test_user.pk})
+        test_link = reverse(
+            'misago:admin:users:accounts:delete-posts', kwargs={
+                'pk': test_user.pk,
+            }
+        )
 
 
         category = Category.objects.all_categories()[:1][0]
         category = Category.objects.all_categories()[:1][0]
         thread = post_thread(category)
         thread = post_thread(category)
-        [reply_thread(thread, poster=test_user) for i in range(10)]
+        [reply_thread(thread, poster=test_user) for _ in range(10)]
 
 
         response = self.client.post(test_link, **self.AJAX_HEADER)
         response = self.client.post(test_link, **self.AJAX_HEADER)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response_dict = json.loads(smart_str(response.content))
+        response_dict = response.json()
         self.assertEqual(response_dict['deleted_count'], 10)
         self.assertEqual(response_dict['deleted_count'], 10)
         self.assertFalse(response_dict['is_completed'])
         self.assertFalse(response_dict['is_completed'])
 
 
         response = self.client.post(test_link, **self.AJAX_HEADER)
         response = self.client.post(test_link, **self.AJAX_HEADER)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response_dict = json.loads(smart_str(response.content))
+        response_dict = response.json()
         self.assertEqual(response_dict['deleted_count'], 0)
         self.assertEqual(response_dict['deleted_count'], 0)
         self.assertTrue(response_dict['is_completed'])
         self.assertTrue(response_dict['is_completed'])
 
 
     def test_delete_account_view(self):
     def test_delete_account_view(self):
         """delete user account view deletes user account"""
         """delete user account view deletes user account"""
         test_user = UserModel.objects.create_user('Bob', 'bob@test.com', 'pass123')
         test_user = UserModel.objects.create_user('Bob', 'bob@test.com', 'pass123')
-        test_link = reverse('misago:admin:users:accounts:delete-account',
-                            kwargs={'pk': test_user.pk})
+        test_link = reverse(
+            'misago:admin:users:accounts:delete-account', kwargs={
+                'pk': test_user.pk,
+            }
+        )
 
 
         response = self.client.post(test_link, **self.AJAX_HEADER)
         response = self.client.post(test_link, **self.AJAX_HEADER)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response_dict = json.loads(smart_str(response.content))
+        response_dict = response.json()
         self.assertTrue(response_dict['is_completed'])
         self.assertTrue(response_dict['is_completed'])

+ 3 - 8
misago/users/tests/test_usernamechanges_api.py

@@ -1,5 +1,3 @@
-from django.contrib.auth import get_user_model
-
 from misago.acl.testutils import override_acl
 from misago.acl.testutils import override_acl
 from misago.users.testutils import AuthenticatedUserTestCase
 from misago.users.testutils import AuthenticatedUserTestCase
 
 
@@ -43,14 +41,12 @@ class UsernameChangesApiTests(AuthenticatedUserTestCase):
 
 
         override_acl(self.user, {'can_see_users_name_history': False})
         override_acl(self.user, {'can_see_users_name_history': False})
 
 
-        response = self.client.get(
-            '%s?user=%s&search=new' % (self.link, self.user.pk))
+        response = self.client.get('%s?user=%s&search=new' % (self.link, self.user.pk))
 
 
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
         self.assertContains(response, self.user.username)
         self.assertContains(response, self.user.username)
 
 
-        response = self.client.get(
-            '%s?user=%s&search=usernew' % (self.link, self.user.pk))
+        response = self.client.get('%s?user=%s&search=usernew' % (self.link, self.user.pk))
 
 
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
         self.assertContains(response, '[]')
         self.assertContains(response, '[]')
@@ -59,8 +55,7 @@ class UsernameChangesApiTests(AuthenticatedUserTestCase):
         """list denies permission for other user (or all) if no access"""
         """list denies permission for other user (or all) if no access"""
         override_acl(self.user, {'can_see_users_name_history': False})
         override_acl(self.user, {'can_see_users_name_history': False})
 
 
-        response = self.client.get(
-            '%s?user=%s' % (self.link, self.user.pk + 1))
+        response = self.client.get('%s?user=%s' % (self.link, self.user.pk + 1))
         self.assertContains(response, "don't have permission to", status_code=403)
         self.assertContains(response, "don't have permission to", status_code=403)
 
 
         response = self.client.get(self.link)
         response = self.client.get(self.link)

+ 179 - 156
misago/users/tests/test_users_api.py

@@ -7,7 +7,6 @@ from django.utils.encoding import smart_str
 
 
 from misago.acl.testutils import override_acl
 from misago.acl.testutils import override_acl
 from misago.categories.models import Category
 from misago.categories.models import Category
-from misago.conf import settings
 from misago.core import threadstore
 from misago.core import threadstore
 from misago.core.cache import cache
 from misago.core.cache import cache
 from misago.threads.models import Post, Thread
 from misago.threads.models import Post, Thread
@@ -21,9 +20,8 @@ UserModel = get_user_model()
 
 
 
 
 class ActivePostersListTests(AuthenticatedUserTestCase):
 class ActivePostersListTests(AuthenticatedUserTestCase):
-    """
-    tests for active posters list (GET /users/?list=active)
-    """
+    """tests for active posters list (GET /users/?list=active)"""
+
     def setUp(self):
     def setUp(self):
         super(ActivePostersListTests, self).setUp()
         super(ActivePostersListTests, self).setUp()
         self.link = '/api/users/?list=active'
         self.link = '/api/users/?list=active'
@@ -69,9 +67,8 @@ class ActivePostersListTests(AuthenticatedUserTestCase):
 
 
 
 
 class FollowersListTests(AuthenticatedUserTestCase):
 class FollowersListTests(AuthenticatedUserTestCase):
-    """
-    tests for generic list (GET /users/) filtered by followers
-    """
+    """tests for generic list (GET /users/) filtered by followers"""
+
     def setUp(self):
     def setUp(self):
         super(FollowersListTests, self).setUp()
         super(FollowersListTests, self).setUp()
         self.link = '/api/users/%s/followers/'
         self.link = '/api/users/%s/followers/'
@@ -89,7 +86,10 @@ class FollowersListTests(AuthenticatedUserTestCase):
     def test_filled_list(self):
     def test_filled_list(self):
         """user with followers returns 200"""
         """user with followers returns 200"""
         test_follower = UserModel.objects.create_user(
         test_follower = UserModel.objects.create_user(
-            "TestFollower", "test@follower.com", self.USER_PASSWORD)
+            "TestFollower",
+            "test@follower.com",
+            self.USER_PASSWORD,
+        )
         self.user.followed_by.add(test_follower)
         self.user.followed_by.add(test_follower)
 
 
         response = self.client.get(self.link % self.user.pk)
         response = self.client.get(self.link % self.user.pk)
@@ -99,7 +99,10 @@ class FollowersListTests(AuthenticatedUserTestCase):
     def test_filled_list_search(self):
     def test_filled_list_search(self):
         """followers list is searchable"""
         """followers list is searchable"""
         test_follower = UserModel.objects.create_user(
         test_follower = UserModel.objects.create_user(
-            "TestFollower", "test@follower.com", self.USER_PASSWORD)
+            "TestFollower",
+            "test@follower.com",
+            self.USER_PASSWORD,
+        )
         self.user.followed_by.add(test_follower)
         self.user.followed_by.add(test_follower)
 
 
         api_link = self.link % self.user.pk
         api_link = self.link % self.user.pk
@@ -110,9 +113,8 @@ class FollowersListTests(AuthenticatedUserTestCase):
 
 
 
 
 class FollowsListTests(AuthenticatedUserTestCase):
 class FollowsListTests(AuthenticatedUserTestCase):
-    """
-    tests for generic list (GET /users/) filtered by follows
-    """
+    """tests for generic list (GET /users/) filtered by follows"""
+
     def setUp(self):
     def setUp(self):
         super(FollowsListTests, self).setUp()
         super(FollowsListTests, self).setUp()
         self.link = '/api/users/%s/follows/'
         self.link = '/api/users/%s/follows/'
@@ -130,7 +132,10 @@ class FollowsListTests(AuthenticatedUserTestCase):
     def test_filled_list(self):
     def test_filled_list(self):
         """user with follows returns 200"""
         """user with follows returns 200"""
         test_follower = UserModel.objects.create_user(
         test_follower = UserModel.objects.create_user(
-            "TestFollower", "test@follower.com", self.USER_PASSWORD)
+            "TestFollower",
+            "test@follower.com",
+            self.USER_PASSWORD,
+        )
         self.user.follows.add(test_follower)
         self.user.follows.add(test_follower)
 
 
         response = self.client.get(self.link % self.user.pk)
         response = self.client.get(self.link % self.user.pk)
@@ -140,7 +145,10 @@ class FollowsListTests(AuthenticatedUserTestCase):
     def test_filled_list_search(self):
     def test_filled_list_search(self):
         """follows list is searchable"""
         """follows list is searchable"""
         test_follower = UserModel.objects.create_user(
         test_follower = UserModel.objects.create_user(
-            "TestFollower", "test@follower.com", self.USER_PASSWORD)
+            "TestFollower",
+            "test@follower.com",
+            self.USER_PASSWORD,
+        )
         self.user.follows.add(test_follower)
         self.user.follows.add(test_follower)
 
 
         api_link = self.link % self.user.pk
         api_link = self.link % self.user.pk
@@ -151,9 +159,8 @@ class FollowsListTests(AuthenticatedUserTestCase):
 
 
 
 
 class RankListTests(AuthenticatedUserTestCase):
 class RankListTests(AuthenticatedUserTestCase):
-    """
-    tests for generic list (GET /users/) filtered by rank
-    """
+    """tests for generic list (GET /users/) filtered by rank"""
+
     def setUp(self):
     def setUp(self):
         super(RankListTests, self).setUp()
         super(RankListTests, self).setUp()
         self.link = '/api/users/?rank=%s'
         self.link = '/api/users/?rank=%s'
@@ -168,7 +175,7 @@ class RankListTests(AuthenticatedUserTestCase):
         test_rank = Rank.objects.create(
         test_rank = Rank.objects.create(
             name="Test rank",
             name="Test rank",
             slug="test-rank",
             slug="test-rank",
-            is_tab=True
+            is_tab=True,
         )
         )
 
 
         response = self.client.get(self.link % test_rank.pk)
         response = self.client.get(self.link % test_rank.pk)
@@ -203,12 +210,15 @@ class RankListTests(AuthenticatedUserTestCase):
         test_rank = Rank.objects.create(
         test_rank = Rank.objects.create(
             name="Test rank",
             name="Test rank",
             slug="test-rank",
             slug="test-rank",
-            is_tab=True
+            is_tab=True,
         )
         )
 
 
         test_user = UserModel.objects.create_user(
         test_user = UserModel.objects.create_user(
-            'Visible', 'visible@te.com', 'Pass.123',
-            rank=test_rank, is_active=False
+            'Visible',
+            'visible@te.com',
+            'Pass.123',
+            rank=test_rank,
+            is_active=False,
         )
         )
 
 
         response = self.client.get(self.link % test_rank.pk)
         response = self.client.get(self.link % test_rank.pk)
@@ -223,9 +233,8 @@ class RankListTests(AuthenticatedUserTestCase):
 
 
 
 
 class SearchNamesListTests(AuthenticatedUserTestCase):
 class SearchNamesListTests(AuthenticatedUserTestCase):
-    """
-    tests for generic list (GET /users/) filtered by username disallowing searches
-    """
+    """tests for generic list (GET /users/) filtered by username disallowing searches"""
+
     def setUp(self):
     def setUp(self):
         super(SearchNamesListTests, self).setUp()
         super(SearchNamesListTests, self).setUp()
         self.link = '/api/users/?&name='
         self.link = '/api/users/?&name='
@@ -246,9 +255,11 @@ class UserRetrieveTests(AuthenticatedUserTestCase):
         super(UserRetrieveTests, self).setUp()
         super(UserRetrieveTests, self).setUp()
 
 
         self.test_user = UserModel.objects.create_user('Tyrael', 't123@test.com', 'pass123')
         self.test_user = UserModel.objects.create_user('Tyrael', 't123@test.com', 'pass123')
-        self.link = reverse('misago:api:user-detail', kwargs={
-            'pk': self.test_user.pk
-        })
+        self.link = reverse(
+            'misago:api:user-detail', kwargs={
+                'pk': self.test_user.pk,
+            }
+        )
 
 
     def test_get_user(self):
     def test_get_user(self):
         """api user retrieve endpoint has no showstoppers"""
         """api user retrieve endpoint has no showstoppers"""
@@ -274,9 +285,8 @@ class UserRetrieveTests(AuthenticatedUserTestCase):
 
 
 
 
 class UserForumOptionsTests(AuthenticatedUserTestCase):
 class UserForumOptionsTests(AuthenticatedUserTestCase):
-    """
-    tests for user forum options RPC (POST to /api/users/1/forum-options/)
-    """
+    """tests for user forum options RPC (POST to /api/users/1/forum-options/)"""
+
     def setUp(self):
     def setUp(self):
         super(UserForumOptionsTests, self).setUp()
         super(UserForumOptionsTests, self).setUp()
         self.link = '/api/users/%s/forum-options/' % self.user.pk
         self.link = '/api/users/%s/forum-options/' % self.user.pk
@@ -286,79 +296,95 @@ class UserForumOptionsTests(AuthenticatedUserTestCase):
         response = self.client.post(self.link)
         response = self.client.post(self.link)
 
 
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'limits_private_thread_invites_to': [
-                'This field is required.',
-            ],
-            'subscribe_to_started_threads': [
-                'This field is required.',
-            ],
-            'subscribe_to_replied_threads': [
-                'This field is required.',
-            ],
-        })
+        self.assertEqual(
+            response.json(), {
+                'limits_private_thread_invites_to': [
+                    'This field is required.',
+                ],
+                'subscribe_to_started_threads': [
+                    'This field is required.',
+                ],
+                'subscribe_to_replied_threads': [
+                    'This field is required.',
+                ],
+            }
+        )
 
 
     def test_change_forum_invalid_ranges(self):
     def test_change_forum_invalid_ranges(self):
         """api validates ranges for fields"""
         """api validates ranges for fields"""
-        response = self.client.post(self.link, data={
-            'limits_private_thread_invites_to': 541,
-            'subscribe_to_started_threads': 44,
-            'subscribe_to_replied_threads': 321
-        })
+        response = self.client.post(
+            self.link,
+            data={
+                'limits_private_thread_invites_to': 541,
+                'subscribe_to_started_threads': 44,
+                'subscribe_to_replied_threads': 321,
+            }
+        )
 
 
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {
-            'limits_private_thread_invites_to': [
-                '"541" is not a valid choice.',
-            ],
-            'subscribe_to_started_threads': [
-                '"44" is not a valid choice.',
-            ],
-            'subscribe_to_replied_threads': [
-                '"321" is not a valid choice.',
-            ],
-        })
+        self.assertEqual(
+            response.json(), {
+                'limits_private_thread_invites_to': [
+                    '"541" is not a valid choice.',
+                ],
+                'subscribe_to_started_threads': [
+                    '"44" is not a valid choice.',
+                ],
+                'subscribe_to_replied_threads': [
+                    '"321" is not a valid choice.',
+                ],
+            }
+        )
 
 
     def test_change_forum_options(self):
     def test_change_forum_options(self):
         """forum options are changed"""
         """forum options are changed"""
-        response = self.client.post(self.link, data={
-            'limits_private_thread_invites_to': 1,
-            'subscribe_to_started_threads': 2,
-            'subscribe_to_replied_threads': 1
-        })
+        response = self.client.post(
+            self.link,
+            data={
+                'limits_private_thread_invites_to': 1,
+                'subscribe_to_started_threads': 2,
+                'subscribe_to_replied_threads': 1,
+            }
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        self.reload_user();
+        self.reload_user()
 
 
         self.assertFalse(self.user.is_hiding_presence)
         self.assertFalse(self.user.is_hiding_presence)
         self.assertEqual(self.user.limits_private_thread_invites_to, 1)
         self.assertEqual(self.user.limits_private_thread_invites_to, 1)
         self.assertEqual(self.user.subscribe_to_started_threads, 2)
         self.assertEqual(self.user.subscribe_to_started_threads, 2)
         self.assertEqual(self.user.subscribe_to_replied_threads, 1)
         self.assertEqual(self.user.subscribe_to_replied_threads, 1)
 
 
-        response = self.client.post(self.link, data={
-            'is_hiding_presence': True,
-            'limits_private_thread_invites_to': 1,
-            'subscribe_to_started_threads': 2,
-            'subscribe_to_replied_threads': 1
-        })
+        response = self.client.post(
+            self.link,
+            data={
+                'is_hiding_presence': True,
+                'limits_private_thread_invites_to': 1,
+                'subscribe_to_started_threads': 2,
+                'subscribe_to_replied_threads': 1,
+            }
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        self.reload_user();
+        self.reload_user()
 
 
         self.assertTrue(self.user.is_hiding_presence)
         self.assertTrue(self.user.is_hiding_presence)
         self.assertEqual(self.user.limits_private_thread_invites_to, 1)
         self.assertEqual(self.user.limits_private_thread_invites_to, 1)
         self.assertEqual(self.user.subscribe_to_started_threads, 2)
         self.assertEqual(self.user.subscribe_to_started_threads, 2)
         self.assertEqual(self.user.subscribe_to_replied_threads, 1)
         self.assertEqual(self.user.subscribe_to_replied_threads, 1)
 
 
-        response = self.client.post(self.link, data={
-            'is_hiding_presence': False,
-            'limits_private_thread_invites_to': 1,
-            'subscribe_to_started_threads': 2,
-            'subscribe_to_replied_threads': 1
-        })
+        response = self.client.post(
+            self.link,
+            data={
+                'is_hiding_presence': False,
+                'limits_private_thread_invites_to': 1,
+                'subscribe_to_started_threads': 2,
+                'subscribe_to_replied_threads': 1,
+            }
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        self.reload_user();
+        self.reload_user()
 
 
         self.assertFalse(self.user.is_hiding_presence)
         self.assertFalse(self.user.is_hiding_presence)
         self.assertEqual(self.user.limits_private_thread_invites_to, 1)
         self.assertEqual(self.user.limits_private_thread_invites_to, 1)
@@ -367,14 +393,12 @@ class UserForumOptionsTests(AuthenticatedUserTestCase):
 
 
 
 
 class UserFollowTests(AuthenticatedUserTestCase):
 class UserFollowTests(AuthenticatedUserTestCase):
-    """
-    tests for user follow RPC (POST to /api/users/1/follow/)
-    """
+    """tests for user follow RPC (POST to /api/users/1/follow/)"""
+
     def setUp(self):
     def setUp(self):
         super(UserFollowTests, self).setUp()
         super(UserFollowTests, self).setUp()
 
 
-        self.other_user = UserModel.objects.create_user(
-            "OtherUser", "other@user.com", "pass123")
+        self.other_user = UserModel.objects.create_user("OtherUser", "other@user.com", "pass123")
 
 
         self.link = '/api/users/%s/follow/' % self.other_user.pk
         self.link = '/api/users/%s/follow/' % self.other_user.pk
 
 
@@ -404,7 +428,6 @@ class UserFollowTests(AuthenticatedUserTestCase):
         response = self.client.post(self.link)
         response = self.client.post(self.link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-
         user = UserModel.objects.get(pk=self.user.pk)
         user = UserModel.objects.get(pk=self.user.pk)
         self.assertEqual(user.followers, 0)
         self.assertEqual(user.followers, 0)
         self.assertEqual(user.following, 1)
         self.assertEqual(user.following, 1)
@@ -434,31 +457,25 @@ class UserFollowTests(AuthenticatedUserTestCase):
 
 
 
 
 class UserBanTests(AuthenticatedUserTestCase):
 class UserBanTests(AuthenticatedUserTestCase):
-    """
-    tests for ban endpoint (GET to /api/users/1/ban/)
-    """
+    """tests for ban endpoint (GET to /api/users/1/ban/)"""
+
     def setUp(self):
     def setUp(self):
         super(UserBanTests, self).setUp()
         super(UserBanTests, self).setUp()
 
 
-        self.other_user = UserModel.objects.create_user(
-            "OtherUser", "other@user.com", "pass123")
+        self.other_user = UserModel.objects.create_user("OtherUser", "other@user.com", "pass123")
 
 
         self.link = '/api/users/%s/ban/' % self.other_user.pk
         self.link = '/api/users/%s/ban/' % self.other_user.pk
 
 
     def test_no_permission(self):
     def test_no_permission(self):
         """user has no permission to access ban"""
         """user has no permission to access ban"""
-        override_acl(self.user, {
-            'can_see_ban_details': 0
-        })
+        override_acl(self.user, {'can_see_ban_details': 0})
 
 
         response = self.client.get(self.link)
         response = self.client.get(self.link)
         self.assertContains(response, "can't see users bans details", status_code=403)
         self.assertContains(response, "can't see users bans details", status_code=403)
 
 
     def test_no_ban(self):
     def test_no_ban(self):
         """api returns empty json"""
         """api returns empty json"""
-        override_acl(self.user, {
-            'can_see_ban_details': 1
-        })
+        override_acl(self.user, {'can_see_ban_details': 1})
 
 
         response = self.client.get(self.link)
         response = self.client.get(self.link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
@@ -466,33 +483,29 @@ class UserBanTests(AuthenticatedUserTestCase):
 
 
     def test_ban_details(self):
     def test_ban_details(self):
         """api returns ban json"""
         """api returns ban json"""
-        override_acl(self.user, {
-            'can_see_ban_details': 1
-        })
+        override_acl(self.user, {'can_see_ban_details': 1})
 
 
         Ban.objects.create(
         Ban.objects.create(
             check_type=Ban.USERNAME,
             check_type=Ban.USERNAME,
             banned_value=self.other_user.username,
             banned_value=self.other_user.username,
-            user_message='Nope!'
+            user_message='Nope!',
         )
         )
 
 
         response = self.client.get(self.link)
         response = self.client.get(self.link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        ban_json = json.loads(smart_str(response.content))
+        ban_json = response.json()
         self.assertEqual(ban_json['user_message']['plain'], 'Nope!')
         self.assertEqual(ban_json['user_message']['plain'], 'Nope!')
         self.assertEqual(ban_json['user_message']['html'], '<p>Nope!</p>')
         self.assertEqual(ban_json['user_message']['html'], '<p>Nope!</p>')
 
 
 
 
 class UserDeleteTests(AuthenticatedUserTestCase):
 class UserDeleteTests(AuthenticatedUserTestCase):
-    """
-    tests for user delete RPC (POST to /api/users/1/delete/)
-    """
+    """tests for user delete RPC (POST to /api/users/1/delete/)"""
+
     def setUp(self):
     def setUp(self):
         super(UserDeleteTests, self).setUp()
         super(UserDeleteTests, self).setUp()
 
 
-        self.other_user = UserModel.objects.create_user(
-            "OtherUser", "other@user.com", "pass123")
+        self.other_user = UserModel.objects.create_user("OtherUser", "other@user.com", "pass123")
 
 
         self.link = '/api/users/%s/delete/' % self.other_user.pk
         self.link = '/api/users/%s/delete/' % self.other_user.pk
 
 
@@ -508,24 +521,12 @@ class UserDeleteTests(AuthenticatedUserTestCase):
 
 
     def test_delete_no_permission(self):
     def test_delete_no_permission(self):
         """raises 403 error when no permission to delete"""
         """raises 403 error when no permission to delete"""
-        override_acl(self.user, {
-            'can_delete_users_newer_than': 0,
-            'can_delete_users_with_less_posts_than': 0,
-        })
-
-        response = self.client.post(self.link)
-        self.assertEqual(response.status_code, 403)
-        self.assertContains(response, "can't delete users", status_code=403)
-
-    def test_delete_too_many_posts(self):
-        """raises 403 error when user has too many posts"""
-        override_acl(self.user, {
-            'can_delete_users_newer_than': 0,
-            'can_delete_users_with_less_posts_than': 5,
-        })
-
-        self.other_user.posts = 6
-        self.other_user.save()
+        override_acl(
+            self.user, {
+                'can_delete_users_newer_than': 0,
+                'can_delete_users_with_less_posts_than': 0,
+            }
+        )
 
 
         response = self.client.post(self.link)
         response = self.client.post(self.link)
         self.assertEqual(response.status_code, 403)
         self.assertEqual(response.status_code, 403)
@@ -533,10 +534,12 @@ class UserDeleteTests(AuthenticatedUserTestCase):
 
 
     def test_delete_too_many_posts(self):
     def test_delete_too_many_posts(self):
         """raises 403 error when user has too many posts"""
         """raises 403 error when user has too many posts"""
-        override_acl(self.user, {
-            'can_delete_users_newer_than': 0,
-            'can_delete_users_with_less_posts_than': 5,
-        })
+        override_acl(
+            self.user, {
+                'can_delete_users_newer_than': 0,
+                'can_delete_users_with_less_posts_than': 5,
+            }
+        )
 
 
         self.other_user.posts = 6
         self.other_user.posts = 6
         self.other_user.save()
         self.other_user.save()
@@ -548,10 +551,12 @@ class UserDeleteTests(AuthenticatedUserTestCase):
 
 
     def test_delete_too_old_member(self):
     def test_delete_too_old_member(self):
         """raises 403 error when user is too old"""
         """raises 403 error when user is too old"""
-        override_acl(self.user, {
-            'can_delete_users_newer_than': 5,
-            'can_delete_users_with_less_posts_than': 0,
-        })
+        override_acl(
+            self.user, {
+                'can_delete_users_newer_than': 5,
+                'can_delete_users_with_less_posts_than': 0,
+            }
+        )
 
 
         self.other_user.joined_on -= timedelta(days=6)
         self.other_user.joined_on -= timedelta(days=6)
         self.other_user.save()
         self.other_user.save()
@@ -563,20 +568,24 @@ class UserDeleteTests(AuthenticatedUserTestCase):
 
 
     def test_delete_self(self):
     def test_delete_self(self):
         """raises 403 error when attempting to delete oneself"""
         """raises 403 error when attempting to delete oneself"""
-        override_acl(self.user, {
-            'can_delete_users_newer_than': 10,
-            'can_delete_users_with_less_posts_than': 10,
-        })
+        override_acl(
+            self.user, {
+                'can_delete_users_newer_than': 10,
+                'can_delete_users_with_less_posts_than': 10,
+            }
+        )
 
 
         response = self.client.post('/api/users/%s/delete/' % self.user.pk)
         response = self.client.post('/api/users/%s/delete/' % self.user.pk)
         self.assertContains(response, "can't delete yourself", status_code=403)
         self.assertContains(response, "can't delete yourself", status_code=403)
 
 
     def test_delete_admin(self):
     def test_delete_admin(self):
         """raises 403 error when attempting to delete admin"""
         """raises 403 error when attempting to delete admin"""
-        override_acl(self.user, {
-            'can_delete_users_newer_than': 10,
-            'can_delete_users_with_less_posts_than': 10,
-        })
+        override_acl(
+            self.user, {
+                'can_delete_users_newer_than': 10,
+                'can_delete_users_with_less_posts_than': 10,
+            }
+        )
 
 
         self.other_user.is_staff = True
         self.other_user.is_staff = True
         self.other_user.save()
         self.other_user.save()
@@ -586,10 +595,12 @@ class UserDeleteTests(AuthenticatedUserTestCase):
 
 
     def test_delete_superadmin(self):
     def test_delete_superadmin(self):
         """raises 403 error when attempting to delete superadmin"""
         """raises 403 error when attempting to delete superadmin"""
-        override_acl(self.user, {
-            'can_delete_users_newer_than': 10,
-            'can_delete_users_with_less_posts_than': 10,
-        })
+        override_acl(
+            self.user, {
+                'can_delete_users_newer_than': 10,
+                'can_delete_users_with_less_posts_than': 10,
+            }
+        )
 
 
         self.other_user.is_superuser = True
         self.other_user.is_superuser = True
         self.other_user.save()
         self.other_user.save()
@@ -599,14 +610,20 @@ class UserDeleteTests(AuthenticatedUserTestCase):
 
 
     def test_delete_with_content(self):
     def test_delete_with_content(self):
         """returns 200 and deletes user with content"""
         """returns 200 and deletes user with content"""
-        override_acl(self.user, {
-            'can_delete_users_newer_than': 10,
-            'can_delete_users_with_less_posts_than': 10,
-        })
+        override_acl(
+            self.user, {
+                'can_delete_users_newer_than': 10,
+                'can_delete_users_with_less_posts_than': 10,
+            }
+        )
 
 
-        response = self.client.post(self.link, json.dumps({
-            'with_content': True
-        }), content_type="application/json")
+        response = self.client.post(
+            self.link,
+            json.dumps({
+                'with_content': True
+            }),
+            content_type='application/json',
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         with self.assertRaises(UserModel.DoesNotExist):
         with self.assertRaises(UserModel.DoesNotExist):
@@ -617,14 +634,20 @@ class UserDeleteTests(AuthenticatedUserTestCase):
 
 
     def test_delete_without_content(self):
     def test_delete_without_content(self):
         """returns 200 and deletes user without content"""
         """returns 200 and deletes user without content"""
-        override_acl(self.user, {
-            'can_delete_users_newer_than': 10,
-            'can_delete_users_with_less_posts_than': 10,
-        })
+        override_acl(
+            self.user, {
+                'can_delete_users_newer_than': 10,
+                'can_delete_users_with_less_posts_than': 10,
+            }
+        )
 
 
-        response = self.client.post(self.link, json.dumps({
-            'with_content': False
-        }), content_type="application/json")
+        response = self.client.post(
+            self.link,
+            json.dumps({
+                'with_content': False
+            }),
+            content_type='application/json',
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         with self.assertRaises(UserModel.DoesNotExist):
         with self.assertRaises(UserModel.DoesNotExist):

+ 2 - 4
misago/users/tests/test_utils.py

@@ -7,10 +7,8 @@ from misago.users.utils import hash_email
 class HashEmailTests(TestCase):
 class HashEmailTests(TestCase):
     def test_is_case_insensitive(self):
     def test_is_case_insensitive(self):
         """util is case insensitive"""
         """util is case insensitive"""
-        self.assertEqual(hash_email('abc@test.com'),
-                         hash_email('aBc@tEst.cOm'))
+        self.assertEqual(hash_email('abc@test.com'), hash_email('aBc@tEst.cOm'))
 
 
     def test_handles_unicode(self):
     def test_handles_unicode(self):
         """util works with unicode strings"""
         """util works with unicode strings"""
-        self.assertEqual(hash_email(u'łóć@test.com'),
-                         hash_email(u'ŁÓĆ@tEst.cOm'))
+        self.assertEqual(hash_email(u'łóć@test.com'), hash_email(u'ŁÓĆ@tEst.cOm'))

+ 5 - 8
misago/users/tests/test_validators.py

@@ -16,8 +16,7 @@ UserModel = get_user_model()
 
 
 class ValidateEmailAvailableTests(TestCase):
 class ValidateEmailAvailableTests(TestCase):
     def setUp(self):
     def setUp(self):
-        self.test_user = UserModel.objects.create_user(
-            'EricTheFish', 'eric@test.com', 'pass123')
+        self.test_user = UserModel.objects.create_user('EricTheFish', 'eric@test.com', 'pass123')
 
 
     def test_valid_email(self):
     def test_valid_email(self):
         """validate_email_available allows available emails"""
         """validate_email_available allows available emails"""
@@ -34,7 +33,7 @@ class ValidateEmailBannedTests(TestCase):
     def setUp(self):
     def setUp(self):
         Ban.objects.create(
         Ban.objects.create(
             check_type=Ban.EMAIL,
             check_type=Ban.EMAIL,
-            banned_value="ban@test.com"
+            banned_value="ban@test.com",
         )
         )
 
 
     def test_unbanned_name(self):
     def test_unbanned_name(self):
@@ -65,14 +64,12 @@ class ValidateUsernameTests(TestCase):
 
 
 class ValidateUsernameAvailableTests(TestCase):
 class ValidateUsernameAvailableTests(TestCase):
     def setUp(self):
     def setUp(self):
-        self.test_user = UserModel.objects.create_user(
-            'EricTheFish', 'eric@test.com', 'pass123')
+        self.test_user = UserModel.objects.create_user('EricTheFish', 'eric@test.com', 'pass123')
 
 
     def test_valid_name(self):
     def test_valid_name(self):
         """validate_username_available allows available names"""
         """validate_username_available allows available names"""
         validate_username_available('BobBoberson')
         validate_username_available('BobBoberson')
-        validate_username_available(
-            self.test_user.username, exclude=self.test_user)
+        validate_username_available(self.test_user.username, exclude=self.test_user)
 
 
     def test_invalid_name(self):
     def test_invalid_name(self):
         """validate_username_available disallows unvailable names"""
         """validate_username_available disallows unvailable names"""
@@ -84,7 +81,7 @@ class ValidateUsernameBannedTests(TestCase):
     def setUp(self):
     def setUp(self):
         Ban.objects.create(
         Ban.objects.create(
             check_type=Ban.USERNAME,
             check_type=Ban.USERNAME,
-            banned_value="Bob"
+            banned_value="Bob",
         )
         )
 
 
     def test_unbanned_name(self):
     def test_unbanned_name(self):

+ 3 - 3
misago/users/testutils.py

@@ -22,12 +22,12 @@ class UserTestCase(MisagoTestCase):
         return AnonymousUser()
         return AnonymousUser()
 
 
     def get_authenticated_user(self):
     def get_authenticated_user(self):
-        return UserModel.objects.create_user(
-            "TestUser", "test@user.com", self.USER_PASSWORD)
+        return UserModel.objects.create_user("TestUser", "test@user.com", self.USER_PASSWORD)
 
 
     def get_superuser(self):
     def get_superuser(self):
         return UserModel.objects.create_superuser(
         return UserModel.objects.create_superuser(
-            "TestSuperUser", "test@superuser.com", self.USER_PASSWORD)
+            "TestSuperUser", "test@superuser.com", self.USER_PASSWORD
+        )
 
 
     def login_user(self, user, password=None):
     def login_user(self, user, password=None):
         self.client.force_login(user)
         self.client.force_login(user)

+ 13 - 21
misago/users/tokens.py

@@ -1,12 +1,3 @@
-import base64
-from hashlib import sha256
-from time import time
-
-from django.conf import settings
-from django.utils import six
-from django.utils.encoding import force_bytes
-
-
 """
 """
 Token creation
 Token creation
 
 
@@ -16,6 +7,15 @@ Token is base encoded string containing three values:
 - hash unique for current state of user model
 - hash unique for current state of user model
 - token checksum for discovering manipulations
 - token checksum for discovering manipulations
 """
 """
+import base64
+from hashlib import sha256
+from time import time
+
+from django.conf import settings
+from django.utils import six
+from django.utils.encoding import force_bytes
+
+
 def make(user, token_type):
 def make(user, token_type):
     user_hash = _make_hash(user, token_type)
     user_hash = _make_hash(user, token_type)
     creation_day = _days_since_epoch()
     creation_day = _days_since_epoch()
@@ -45,17 +45,16 @@ def is_valid(user, token_type, token):
 
 
 
 
 def _make_hash(user, token_type):
 def _make_hash(user, token_type):
-    seeds = (
+    seeds = [
         user.pk,
         user.pk,
         user.email,
         user.email,
         user.password,
         user.password,
         user.last_login.replace(microsecond=0, tzinfo=None),
         user.last_login.replace(microsecond=0, tzinfo=None),
         token_type,
         token_type,
         settings.SECRET_KEY,
         settings.SECRET_KEY,
-    )
+    ]
 
 
-    return sha256(force_bytes(
-        '+'.join([six.text_type(s) for s in seeds]))).hexdigest()[:8]
+    return sha256(force_bytes('+'.join([six.text_type(s) for s in seeds]))).hexdigest()[:8]
 
 
 
 
 def _days_since_epoch():
 def _days_since_epoch():
@@ -63,13 +62,9 @@ def _days_since_epoch():
 
 
 
 
 def _make_checksum(obfuscated):
 def _make_checksum(obfuscated):
-    return sha256(force_bytes(
-        '%s:%s' % (settings.SECRET_KEY, obfuscated))).hexdigest()[:8]
+    return sha256(force_bytes('%s:%s' % (settings.SECRET_KEY, obfuscated))).hexdigest()[:8]
 
 
 
 
-"""
-Convenience functions for activation token
-"""
 ACTIVATION_TOKEN = 'activation'
 ACTIVATION_TOKEN = 'activation'
 
 
 
 
@@ -81,9 +76,6 @@ def is_activation_token_valid(user, token):
     return is_valid(user, ACTIVATION_TOKEN, token)
     return is_valid(user, ACTIVATION_TOKEN, token)
 
 
 
 
-"""
-Convenience functions for password change token
-"""
 PASSWORD_CHANGE_TOKEN = 'change_password'
 PASSWORD_CHANGE_TOKEN = 'change_password'
 
 
 
 

+ 53 - 29
misago/users/urls/__init__.py

@@ -4,57 +4,81 @@ from misago.core.views import home_redirect
 
 
 from misago.users.views import activation, auth, avatarserver, forgottenpassword, lists, options, profile
 from misago.users.views import activation, auth, avatarserver, forgottenpassword, lists, options, profile
 
 
-
 urlpatterns = [
 urlpatterns = [
     url(r'^banned/$', home_redirect, name='banned'),
     url(r'^banned/$', home_redirect, name='banned'),
-
     url(r'^login/$', auth.login, name='login'),
     url(r'^login/$', auth.login, name='login'),
     url(r'^logout/$', auth.logout, name='logout'),
     url(r'^logout/$', auth.logout, name='logout'),
-
     url(r'^request-activation/$', activation.request_activation, name='request-activation'),
     url(r'^request-activation/$', activation.request_activation, name='request-activation'),
-    url(r'^activation/(?P<pk>\d+)/(?P<token>[a-zA-Z0-9]+)/$', activation.activate_by_token, name='activate-by-token'),
-
+    url(
+        r'^activation/(?P<pk>\d+)/(?P<token>[a-zA-Z0-9]+)/$',
+        activation.activate_by_token,
+        name='activate-by-token'
+    ),
     url(r'^forgotten-password/$', forgottenpassword.request_reset, name='forgotten-password'),
     url(r'^forgotten-password/$', forgottenpassword.request_reset, name='forgotten-password'),
-    url(r'^forgotten-password/(?P<pk>\d+)/(?P<token>[a-zA-Z0-9]+)/$', forgottenpassword.reset_password_form, name='forgotten-password-change-form'),
+    url(
+        r'^forgotten-password/(?P<pk>\d+)/(?P<token>[a-zA-Z0-9]+)/$',
+        forgottenpassword.reset_password_form,
+        name='forgotten-password-change-form'
+    ),
 ]
 ]
 
 
-
 urlpatterns += [
 urlpatterns += [
     url(r'^options/$', options.index, name='options'),
     url(r'^options/$', options.index, name='options'),
     url(r'^options/(?P<form_name>[-a-zA-Z]+)/$', options.index, name='options-form'),
     url(r'^options/(?P<form_name>[-a-zA-Z]+)/$', options.index, name='options-form'),
-
     url(r'^options/forum-options/$', options.index, name='usercp-change-forum-options'),
     url(r'^options/forum-options/$', options.index, name='usercp-change-forum-options'),
     url(r'^options/change-username/$', options.index, name='usercp-change-username'),
     url(r'^options/change-username/$', options.index, name='usercp-change-username'),
     url(r'^options/sign-in-credentials/$', options.index, name='usercp-change-email-password'),
     url(r'^options/sign-in-credentials/$', options.index, name='usercp-change-email-password'),
-
-    url(r'^options/change-email/(?P<token>[a-zA-Z0-9]+)/$', options.confirm_email_change, name='options-confirm-email-change'),
-    url(r'^options/change-password/(?P<token>[a-zA-Z0-9]+)/$', options.confirm_password_change, name='options-confirm-password-change'),
+    url(
+        r'^options/change-email/(?P<token>[a-zA-Z0-9]+)/$',
+        options.confirm_email_change,
+        name='options-confirm-email-change'
+    ),
+    url(
+        r'^options/change-password/(?P<token>[a-zA-Z0-9]+)/$',
+        options.confirm_password_change,
+        name='options-confirm-password-change'
+    ),
 ]
 ]
 
 
-
 urlpatterns += [
 urlpatterns += [
-    url(r'^users/', include([
-        url(r'^$', lists.landing, name='users'),
-        url(r'^active-posters/$', lists.ActivePostersView.as_view(), name='users-active-posters'),
-        url(r'^(?P<slug>[-a-zA-Z0-9]+)/$', lists.RankUsersView.as_view(), name='users-rank'),
-        url(r'^(?P<slug>[-a-zA-Z0-9]+)/(?P<page>\d+)/$', lists.RankUsersView.as_view(), name='users-rank'),
-    ]))
+    url(
+        r'^users/',
+        include([
+            url(r'^$', lists.landing, name='users'),
+            url(
+                r'^active-posters/$',
+                lists.ActivePostersView.as_view(),
+                name='users-active-posters'
+            ),
+            url(r'^(?P<slug>[-a-zA-Z0-9]+)/$', lists.RankUsersView.as_view(), name='users-rank'),
+            url(
+                r'^(?P<slug>[-a-zA-Z0-9]+)/(?P<page>\d+)/$',
+                lists.RankUsersView.as_view(),
+                name='users-rank'
+            ),
+        ])
+    )
 ]
 ]
 
 
-
 urlpatterns += [
 urlpatterns += [
-    url(r'^u/(?P<slug>[a-zA-Z0-9]+)/(?P<pk>\d+)/', include([
-        url(r'^$', profile.LandingView.as_view(), name='user'),
-        url(r'^posts/$', profile.UserPostsView.as_view(), name='user-posts'),
-        url(r'^threads/$', profile.UserThreadsView.as_view(), name='user-threads'),
-        url(r'^followers/$', profile.UserFollowersView.as_view(), name='user-followers'),
-        url(r'^follows/$', profile.UserFollowsView.as_view(), name='user-follows'),
-        url(r'^username-history/$', profile.UserUsernameHistoryView.as_view(), name='username-history'),
-        url(r'^ban-details/$', profile.UserBanView.as_view(), name='user-ban'),
-    ]))
+    url(
+        r'^u/(?P<slug>[a-zA-Z0-9]+)/(?P<pk>\d+)/',
+        include([
+            url(r'^$', profile.LandingView.as_view(), name='user'),
+            url(r'^posts/$', profile.UserPostsView.as_view(), name='user-posts'),
+            url(r'^threads/$', profile.UserThreadsView.as_view(), name='user-threads'),
+            url(r'^followers/$', profile.UserFollowersView.as_view(), name='user-followers'),
+            url(r'^follows/$', profile.UserFollowsView.as_view(), name='user-follows'),
+            url(
+                r'^username-history/$',
+                profile.UserUsernameHistoryView.as_view(),
+                name='username-history'
+            ),
+            url(r'^ban-details/$', profile.UserBanView.as_view(), name='user-ban'),
+        ])
+    )
 ]
 ]
 
 
-
 urlpatterns += [
 urlpatterns += [
     url(r'^avatar/$', avatarserver.blank_avatar, name='blank-avatar'),
     url(r'^avatar/$', avatarserver.blank_avatar, name='blank-avatar'),
     url(r'^avatar/(?P<pk>\d+)/(?P<size>\d+)/$', avatarserver.user_avatar, name='user-avatar'),
     url(r'^avatar/(?P<pk>\d+)/(?P<size>\d+)/$', avatarserver.user_avatar, name='user-avatar'),

+ 5 - 3
misago/users/urls/api.py

@@ -12,12 +12,14 @@ urlpatterns = [
     url(r'^auth/criteria/$', auth.get_criteria, name='auth-criteria'),
     url(r'^auth/criteria/$', auth.get_criteria, name='auth-criteria'),
     url(r'^auth/send-activation/$', auth.send_activation, name='send-activation'),
     url(r'^auth/send-activation/$', auth.send_activation, name='send-activation'),
     url(r'^auth/send-password-form/$', auth.send_password_form, name='send-password-form'),
     url(r'^auth/send-password-form/$', auth.send_password_form, name='send-password-form'),
-    url(r'^auth/change-password/(?P<pk>\d+)/(?P<token>[a-zA-Z0-9]+)/$', auth.change_forgotten_password, name='change-forgotten-password'),
-
+    url(
+        r'^auth/change-password/(?P<pk>\d+)/(?P<token>[a-zA-Z0-9]+)/$',
+        auth.change_forgotten_password,
+        name='change-forgotten-password'
+    ),
     url(r'^captcha-question/$', captcha.question, name='captcha-question'),
     url(r'^captcha-question/$', captcha.question, name='captcha-question'),
 ]
 ]
 
 
-
 router = MisagoApiRouter()
 router = MisagoApiRouter()
 router.register(r'ranks', RanksViewSet)
 router.register(r'ranks', RanksViewSet)
 router.register(r'users', UserViewSet)
 router.register(r'users', UserViewSet)

+ 15 - 27
misago/users/validators.py

@@ -21,9 +21,7 @@ USERNAME_RE = re.compile(r'^[0-9a-z]+$', re.IGNORECASE)
 UserModel = get_user_model()
 UserModel = get_user_model()
 
 
 
 
-"""
-Email validators
-"""
+# E-mail validators
 def validate_email_available(value, exclude=None):
 def validate_email_available(value, exclude=None):
     try:
     try:
         user = UserModel.objects.get_by_email(value)
         user = UserModel.objects.get_by_email(value)
@@ -50,9 +48,7 @@ def validate_email(value, exclude=None):
     validate_email_banned(value)
     validate_email_banned(value)
 
 
 
 
-"""
-Username validators
-"""
+# Username validators
 def validate_username_available(value, exclude=None):
 def validate_username_available(value, exclude=None):
     try:
     try:
         user = UserModel.objects.get_by_username(value)
         user = UserModel.objects.get_by_username(value)
@@ -74,8 +70,7 @@ def validate_username_banned(value):
 
 
 def validate_username_content(value):
 def validate_username_content(value):
     if not USERNAME_RE.match(value):
     if not USERNAME_RE.match(value):
-        raise ValidationError(
-            _("Username can only contain latin alphabet letters and digits."))
+        raise ValidationError(_("Username can only contain latin alphabet letters and digits."))
 
 
 
 
 def validate_username_length(value):
 def validate_username_length(value):
@@ -83,17 +78,17 @@ def validate_username_length(value):
         message = ungettext(
         message = ungettext(
             "Username must be at least %(limit_value)s character long.",
             "Username must be at least %(limit_value)s character long.",
             "Username must be at least %(limit_value)s characters long.",
             "Username must be at least %(limit_value)s characters long.",
-            settings.username_length_min)
-        message = message % {'limit_value': settings.username_length_min}
-        raise ValidationError(message)
+            settings.username_length_min
+        )
+        raise ValidationError(message % {'limit_value': settings.username_length_min})
 
 
     if len(value) > settings.username_length_max:
     if len(value) > settings.username_length_max:
         message = ungettext(
         message = ungettext(
             "Username cannot be longer than %(limit_value)s characters.",
             "Username cannot be longer than %(limit_value)s characters.",
             "Username cannot be longer than %(limit_value)s characters.",
             "Username cannot be longer than %(limit_value)s characters.",
-            settings.username_length_max)
-        message = message % {'limit_value': settings.username_length_max}
-        raise ValidationError(message)
+            settings.username_length_max
+        )
+        raise ValidationError(message % {'limit_value': settings.username_length_max})
 
 
 
 
 def validate_username(value, exclude=None):
 def validate_username(value, exclude=None):
@@ -104,9 +99,7 @@ def validate_username(value, exclude=None):
     validate_username_banned(value)
     validate_username_banned(value)
 
 
 
 
-"""
-New account validators
-"""
+# New account validators
 SFS_API_URL = u'http://api.stopforumspam.org/api?email=%(email)s&ip=%(ip)s&f=json&confidence'  # noqa
 SFS_API_URL = u'http://api.stopforumspam.org/api?email=%(email)s&ip=%(ip)s&f=json&confidence'  # noqa
 
 
 
 
@@ -117,10 +110,7 @@ def validate_with_sfs(request, form, cleaned_data):
 
 
 def _real_validate_with_sfs(ip, email):
 def _real_validate_with_sfs(ip, email):
     try:
     try:
-        r = requests.get(SFS_API_URL % {
-            'email': email,
-            'ip': ip
-        }, timeout=5)
+        r = requests.get(SFS_API_URL % {'email': email, 'ip': ip}, timeout=5)
 
 
         r.raise_for_status()
         r.raise_for_status()
 
 
@@ -133,7 +123,7 @@ def _real_validate_with_sfs(ip, email):
         if api_score > settings.MISAGO_STOP_FORUM_SPAM_MIN_CONFIDENCE:
         if api_score > settings.MISAGO_STOP_FORUM_SPAM_MIN_CONFIDENCE:
             raise ValidationError(_("Data entered was found in spammers database."))
             raise ValidationError(_("Data entered was found in spammers database."))
     except requests.exceptions.RequestException:
     except requests.exceptions.RequestException:
-        pass # todo: log those somewhere
+        pass  # todo: log those somewhere
 
 
 
 
 def validate_gmail_email(request, form, cleaned_data):
 def validate_gmail_email(request, form, cleaned_data):
@@ -146,12 +136,10 @@ def validate_gmail_email(request, form, cleaned_data):
         form.add_error('email', ValidationError(_("This email is not allowed.")))
         form.add_error('email', ValidationError(_("This email is not allowed.")))
 
 
 
 
-"""
-Registration validation
-"""
-def load_registration_validators(validators_list):
+# Registration validation
+def load_registration_validators(validators):
     loaded_validators = []
     loaded_validators = []
-    for path in validators_list:
+    for path in validators:
         module = import_module('.'.join(path.split('.')[:-1]))
         module = import_module('.'.join(path.split('.')[:-1]))
         loaded_validators.append(getattr(module, path.split('.')[-1]))
         loaded_validators.append(getattr(module, path.split('.')[-1]))
     return loaded_validators
     return loaded_validators

+ 3 - 2
misago/users/viewmodels/activeposters.py

@@ -17,14 +17,15 @@ class ActivePosters(object):
         return {
         return {
             'tracked_period': self.tracked_period,
             'tracked_period': self.tracked_period,
             'results': ScoredUserSerializer(self.users, many=True).data,
             'results': ScoredUserSerializer(self.users, many=True).data,
-            'count': self.count
+            'count': self.count,
         }
         }
 
 
     def get_template_context(self):
     def get_template_context(self):
         return {
         return {
             'tracked_period': self.tracked_period,
             'tracked_period': self.tracked_period,
             'users': self.users,
             'users': self.users,
-            'users_count': self.count
+            'users_count': self.count,
         }
         }
 
 
+
 ScoredUserSerializer = UserCardSerializer.extend_fields('meta')
 ScoredUserSerializer = UserCardSerializer.extend_fields('meta')

+ 3 - 4
misago/users/viewmodels/followers.py

@@ -1,6 +1,7 @@
+from django.http import Http404
+
 from misago.conf import settings
 from misago.conf import settings
 from misago.core.shortcuts import paginate, pagination_dict
 from misago.core.shortcuts import paginate, pagination_dict
-from django.http import Http404
 from misago.users.online.utils import make_users_status_aware
 from misago.users.online.utils import make_users_status_aware
 from misago.users.serializers import UserCardSerializer
 from misago.users.serializers import UserCardSerializer
 
 
@@ -29,9 +30,7 @@ class Followers(object):
         return profile.followed_by
         return profile.followed_by
 
 
     def get_frontend_context(self):
     def get_frontend_context(self):
-        context = {
-            'results': UserCardSerializer(self.users, many=True).data
-        }
+        context = {'results': UserCardSerializer(self.users, many=True).data}
         context.update(self.paginator)
         context.update(self.paginator)
         return context
         return context
 
 

+ 2 - 3
misago/users/viewmodels/posts.py

@@ -6,10 +6,9 @@ from .threads import UserThreads
 
 
 class UserPosts(UserThreads):
 class UserPosts(UserThreads):
     def get_threads_queryset(self, request, threads_categories, profile):
     def get_threads_queryset(self, request, threads_categories, profile):
-        return exclude_invisible_threads(
-            request.user, threads_categories, Thread.objects)
+        return exclude_invisible_threads(request.user, threads_categories, Thread.objects)
 
 
     def get_posts_queryset(self, user, profile, threads_queryset):
     def get_posts_queryset(self, user, profile, threads_queryset):
         return profile.post_set.select_related('thread', 'poster').filter(
         return profile.post_set.select_related('thread', 'poster').filter(
-            thread_id__in=threads_queryset.values('id')
+            thread_id__in=threads_queryset.values('id'),
         )
         )

+ 5 - 5
misago/users/viewmodels/rankusers.py

@@ -1,6 +1,5 @@
 from misago.conf import settings
 from misago.conf import settings
 from misago.core.shortcuts import paginate, pagination_dict
 from misago.core.shortcuts import paginate, pagination_dict
-from django.http import Http404
 from misago.users.online.utils import make_users_status_aware
 from misago.users.online.utils import make_users_status_aware
 from misago.users.serializers import UserCardSerializer
 from misago.users.serializers import UserCardSerializer
 
 
@@ -8,7 +7,10 @@ from misago.users.serializers import UserCardSerializer
 class RankUsers(object):
 class RankUsers(object):
     def __init__(self, request, rank, page=0):
     def __init__(self, request, rank, page=0):
         queryset = rank.user_set.select_related(
         queryset = rank.user_set.select_related(
-            'rank', 'ban_cache', 'online_tracker').order_by('slug')
+            'rank',
+            'ban_cache',
+            'online_tracker',
+        ).order_by('slug')
 
 
         if not request.user.is_staff:
         if not request.user.is_staff:
             queryset = queryset.filter(is_active=True)
             queryset = queryset.filter(is_active=True)
@@ -20,9 +22,7 @@ class RankUsers(object):
         self.paginator = pagination_dict(list_page)
         self.paginator = pagination_dict(list_page)
 
 
     def get_frontend_context(self):
     def get_frontend_context(self):
-        context = {
-            'results': UserCardSerializer(self.users, many=True).data
-        }
+        context = {'results': UserCardSerializer(self.users, many=True).data}
         context.update(self.paginator)
         context.update(self.paginator)
         return context
         return context
 
 

+ 10 - 15
misago/users/viewmodels/threads.py

@@ -13,19 +13,17 @@ class UserThreads(object):
         root_category = ThreadsRootCategory(request)
         root_category = ThreadsRootCategory(request)
         threads_categories = [root_category.unwrap()] + root_category.subcategories
         threads_categories = [root_category.unwrap()] + root_category.subcategories
 
 
-        threads_queryset = self.get_threads_queryset(
-            request, threads_categories, profile)
+        threads_queryset = self.get_threads_queryset(request, threads_categories, profile)
 
 
-        posts_queryset = self.get_posts_queryset(
-            request.user, profile, threads_queryset
-        ).filter(
+        posts_queryset = self.get_posts_queryset(request.user, profile, threads_queryset).filter(
             is_event=False,
             is_event=False,
             is_hidden=False,
             is_hidden=False,
-            is_unapproved=False
+            is_unapproved=False,
         ).order_by('-id')
         ).order_by('-id')
 
 
         list_page = paginate(
         list_page = paginate(
-            posts_queryset, page, settings.MISAGO_POSTS_PER_PAGE, settings.MISAGO_POSTS_TAIL)
+            posts_queryset, page, settings.MISAGO_POSTS_PER_PAGE, settings.MISAGO_POSTS_TAIL
+        )
         paginator = pagination_dict(list_page)
         paginator = pagination_dict(list_page)
 
 
         posts = list(list_page.object_list)
         posts = list(list_page.object_list)
@@ -34,8 +32,7 @@ class UserThreads(object):
         for post in posts:
         for post in posts:
             threads.append(post.thread)
             threads.append(post.thread)
 
 
-        add_categories_to_items(
-            root_category.unwrap(), threads_categories, posts + threads)
+        add_categories_to_items(root_category.unwrap(), threads_categories, posts + threads)
 
 
         add_acl(request.user, threads)
         add_acl(request.user, threads)
         add_acl(request.user, posts)
         add_acl(request.user, posts)
@@ -52,18 +49,16 @@ class UserThreads(object):
         self.paginator = paginator
         self.paginator = paginator
 
 
     def get_threads_queryset(self, request, threads_categories, profile):
     def get_threads_queryset(self, request, threads_categories, profile):
-        return exclude_invisible_threads(
-            request.user, threads_categories, profile.thread_set)
+        return exclude_invisible_threads(request.user, threads_categories, profile.thread_set)
 
 
     def get_posts_queryset(self, user, profile, threads_queryset):
     def get_posts_queryset(self, user, profile, threads_queryset):
         return profile.post_set.select_related('thread', 'poster').filter(
         return profile.post_set.select_related('thread', 'poster').filter(
-            id__in=threads_queryset.values('first_post_id')
+            id__in=threads_queryset.values('first_post_id'),
         )
         )
 
 
     def get_frontend_context(self):
     def get_frontend_context(self):
         context = {
         context = {
-            'results': UserFeedSerializer(
-                self.posts, many=True, context={'user': self._user}).data
+            'results': UserFeedSerializer(self.posts, many=True, context={'user': self._user}).data
         }
         }
 
 
         context.update(self.paginator)
         context.update(self.paginator)
@@ -73,7 +68,7 @@ class UserThreads(object):
     def get_template_context(self):
     def get_template_context(self):
         return {
         return {
             'posts': self.posts,
             'posts': self.posts,
-            'paginator': self.paginator
+            'paginator': self.paginator,
         }
         }
 
 
 
 

+ 24 - 16
misago/users/views/activation.py

@@ -3,9 +3,7 @@ from django.shortcuts import get_object_or_404, render
 from django.urls import reverse
 from django.urls import reverse
 from django.utils.translation import ugettext as _
 from django.utils.translation import ugettext as _
 
 
-from misago.conf import settings
 from misago.core.exceptions import Banned
 from misago.core.exceptions import Banned
-from misago.core.mail import mail_user
 from misago.users.bans import get_user_ban
 from misago.users.bans import get_user_ban
 from misago.users.decorators import deny_authenticated, deny_banned_ips
 from misago.users.decorators import deny_authenticated, deny_banned_ips
 from misago.users.tokens import is_activation_token_valid
 from misago.users.tokens import is_activation_token_valid
@@ -19,13 +17,14 @@ def activation_view(f):
     @deny_banned_ips
     @deny_banned_ips
     def decorator(*args, **kwargs):
     def decorator(*args, **kwargs):
         return f(*args, **kwargs)
         return f(*args, **kwargs)
+
     return decorator
     return decorator
 
 
 
 
 @activation_view
 @activation_view
 def request_activation(request):
 def request_activation(request):
     request.frontend_context.update({
     request.frontend_context.update({
-        'SEND_ACTIVATION_API': reverse('misago:api:send-activation')
+        'SEND_ACTIVATION_API': reverse('misago:api:send-activation'),
     })
     })
     return render(request, 'misago/activation/request.html')
     return render(request, 'misago/activation/request.html')
 
 
@@ -45,32 +44,41 @@ def activate_by_token(request, pk, token):
     try:
     try:
         if not inactive_user.requires_activation:
         if not inactive_user.requires_activation:
             message = _("%(user)s, your account is already active.")
             message = _("%(user)s, your account is already active.")
-            message = message % {'user': inactive_user.username}
-            raise ActivationStopped(message)
+            raise ActivationStopped(message % {'user': inactive_user.username})
 
 
         if not is_activation_token_valid(inactive_user, token):
         if not is_activation_token_valid(inactive_user, token):
-            message = _("%(user)s, your activation link is invalid. "
-                        "Try again or request new activation link.")
-            message = message % {'user': inactive_user.username}
-            raise ActivationError(message)
+            message = _(
+                "%(user)s, your activation link is invalid. "
+                "Try again or request new activation link."
+            )
+            raise ActivationError(message % {'user': inactive_user.username})
 
 
         ban = get_user_ban(inactive_user)
         ban = get_user_ban(inactive_user)
         if ban:
         if ban:
             raise Banned(ban)
             raise Banned(ban)
     except ActivationStopped as e:
     except ActivationStopped as e:
         return render(request, 'misago/activation/stopped.html', {
         return render(request, 'misago/activation/stopped.html', {
-                'message': e.args[0],
-            })
+            'message': e.args[0],
+        })
     except ActivationError as e:
     except ActivationError as e:
-        return render(request, 'misago/activation/error.html', {
+        return render(
+            request,
+            'misago/activation/error.html',
+            {
                 'message': e.args[0],
                 'message': e.args[0],
-            }, status=400)
+            },
+            status=400,
+        )
 
 
     inactive_user.requires_activation = UserModel.ACTIVATION_NONE
     inactive_user.requires_activation = UserModel.ACTIVATION_NONE
     inactive_user.save(update_fields=['requires_activation'])
     inactive_user.save(update_fields=['requires_activation'])
 
 
     message = _("%(user)s, your account has been activated!")
     message = _("%(user)s, your account has been activated!")
 
 
-    return render(request, 'misago/activation/done.html', {
-            'message': message % {'user': inactive_user.username},
-        })
+    return render(
+        request, 'misago/activation/done.html', {
+            'message': message % {
+                'user': inactive_user.username,
+            },
+        }
+    )

+ 8 - 10
misago/users/views/admin/bans.py

@@ -20,23 +20,21 @@ class BanAdmin(generic.AdminBaseMixin):
 
 
 class BansList(BanAdmin, generic.ListView):
 class BansList(BanAdmin, generic.ListView):
     items_per_page = 30
     items_per_page = 30
-    ordering = (
+    ordering = [
         ('-id', _("From newest")),
         ('-id', _("From newest")),
         ('id', _("From oldest")),
         ('id', _("From oldest")),
         ('banned_value', _("A to z")),
         ('banned_value', _("A to z")),
         ('-banned_value', _("Z to a")),
         ('-banned_value', _("Z to a")),
-    )
+    ]
     search_form = SearchBansForm
     search_form = SearchBansForm
     selection_label = _('With bans: 0')
     selection_label = _('With bans: 0')
     empty_selection_label = _('Select bans')
     empty_selection_label = _('Select bans')
-    mass_actions = (
-        {
-            'action': 'delete',
-            'icon': 'fa fa-times',
-            'name': _('Remove bans'),
-            'confirmation': _('Are you sure you want to remove those bans?')
-        },
-    )
+    mass_actions = ({
+        'action': 'delete',
+        'icon': 'fa fa-times',
+        'name': _('Remove bans'),
+        'confirmation': _('Are you sure you want to remove those bans?')
+    }, )
 
 
     def action_delete(self, request, items):
     def action_delete(self, request, items):
         items.delete()
         items.delete()

+ 1 - 1
misago/users/views/admin/ranks.py

@@ -26,7 +26,7 @@ class RankAdmin(generic.AdminBaseMixin):
 
 
 
 
 class RanksList(RankAdmin, generic.ListView):
 class RanksList(RankAdmin, generic.ListView):
-    ordering = (('order', None),)
+    ordering = (('order', None), )
 
 
 
 
 class NewRank(RankAdmin, generic.ModelFormView):
 class NewRank(RankAdmin, generic.ModelFormView):

+ 41 - 48
misago/users/views/admin/users.py

@@ -15,7 +15,7 @@ from misago.threads.models import Thread
 from misago.users.avatars.dynamic import set_avatar as set_dynamic_avatar
 from misago.users.avatars.dynamic import set_avatar as set_dynamic_avatar
 from misago.users.forms.admin import (
 from misago.users.forms.admin import (
     BanUsersForm, EditUserForm, EditUserFormFactory, NewUserForm, SearchUsersForm)
     BanUsersForm, EditUserForm, EditUserFormFactory, NewUserForm, SearchUsersForm)
-from misago.users.models import Ban, User
+from misago.users.models import Ban
 from misago.users.signatures import set_user_signature
 from misago.users.signatures import set_user_signature
 
 
 
 
@@ -41,7 +41,8 @@ class UserAdmin(generic.AdminBaseMixin):
             add_admin_fields = request.user.pk != target.pk
             add_admin_fields = request.user.pk != target.pk
 
 
         return EditUserFormFactory(
         return EditUserFormFactory(
-            self.form, target,
+            self.form,
+            target,
             add_is_active_fields=add_is_active_fields,
             add_is_active_fields=add_is_active_fields,
             add_admin_fields=add_admin_fields,
             add_admin_fields=add_admin_fields,
         )
         )
@@ -49,14 +50,14 @@ class UserAdmin(generic.AdminBaseMixin):
 
 
 class UsersList(UserAdmin, generic.ListView):
 class UsersList(UserAdmin, generic.ListView):
     items_per_page = 24
     items_per_page = 24
-    ordering = (
+    ordering = [
         ('-id', _("From newest")),
         ('-id', _("From newest")),
         ('id', _("From oldest")),
         ('id', _("From oldest")),
         ('slug', _("A to z")),
         ('slug', _("A to z")),
         ('-slug', _("Z to a")),
         ('-slug', _("Z to a")),
         ('posts', _("Biggest posters")),
         ('posts', _("Biggest posters")),
         ('-posts', _("Smallest posters")),
         ('-posts', _("Smallest posters")),
-    )
+    ]
     selection_label = _('With users: 0')
     selection_label = _('With users: 0')
     empty_selection_label = _('Select users')
     empty_selection_label = _('Select users')
     mass_actions = [
     mass_actions = [
@@ -80,11 +81,12 @@ class UsersList(UserAdmin, generic.ListView):
             'action': 'delete_all',
             'action': 'delete_all',
             'name': _("Delete all"),
             'name': _("Delete all"),
             'icon': 'fa fa-eraser',
             'icon': 'fa fa-eraser',
-            'confirmation': _("Are you sure you want to delete selected "
-                              "users? This will also delete all content "
-                              "associated with their accounts."),
+            'confirmation': _(
+                "Are you sure you want to delete selected users? "
+                "This will also delete all content associated with their accounts."
+            ),
             'is_atomic': False,
             'is_atomic': False,
-        }
+        },
     ]
     ]
 
 
     def get_queryset(self):
     def get_queryset(self):
@@ -109,15 +111,11 @@ class UsersList(UserAdmin, generic.ListView):
             queryset.update(requires_activation=UserModel.ACTIVATION_NONE)
             queryset.update(requires_activation=UserModel.ACTIVATION_NONE)
 
 
             subject = _("Your account on %(forum_name)s forums has been activated")
             subject = _("Your account on %(forum_name)s forums has been activated")
-            mail_subject = subject % {
-                'forum_name': settings.forum_name
-            }
+            mail_subject = subject % {'forum_name': settings.forum_name}
 
 
-            mail_users(request, inactive_users, mail_subject,
-                       'misago/emails/activation/by_admin')
+            mail_users(request, inactive_users, mail_subject, 'misago/emails/activation/by_admin')
 
 
-            message = _("Selected users accounts have been activated.")
-            messages.success(request, message)
+            messages.success(request, _("Selected users accounts have been activated."))
 
 
     def action_ban(self, request, users):
     def action_ban(self, request, users):
         users = users.order_by('slug')
         users = users.order_by('slug')
@@ -137,7 +135,7 @@ class UsersList(UserAdmin, generic.ListView):
                 ban_kwargs = {
                 ban_kwargs = {
                     'user_message': cleaned_data.get('user_message'),
                     'user_message': cleaned_data.get('user_message'),
                     'staff_message': cleaned_data.get('staff_message'),
                     'staff_message': cleaned_data.get('staff_message'),
-                    'expires_on': cleaned_data.get('expires_on')
+                    'expires_on': cleaned_data.get('expires_on'),
                 }
                 }
 
 
                 for user in users:
                 for user in users:
@@ -172,38 +170,35 @@ class UsersList(UserAdmin, generic.ListView):
                             if ban == 'ip_first':
                             if ban == 'ip_first':
                                 formats = (bits[0], ip_separator)
                                 formats = (bits[0], ip_separator)
                             if ban == 'ip_two':
                             if ban == 'ip_two':
-                                formats = (
-                                    bits[0], ip_separator,
-                                    bits[1], ip_separator
-                                )
+                                formats = (bits[0], ip_separator, bits[1], ip_separator)
                             banned_value = '%s*' % (''.join(formats))
                             banned_value = '%s*' % (''.join(formats))
 
 
                         if banned_value not in banned_values:
                         if banned_value not in banned_values:
                             ban_kwargs.update({
                             ban_kwargs.update({
                                 'check_type': check_type,
                                 'check_type': check_type,
-                                'banned_value': banned_value
+                                'banned_value': banned_value,
                             })
                             })
                             Ban.objects.create(**ban_kwargs)
                             Ban.objects.create(**ban_kwargs)
                             banned_values.append(banned_value)
                             banned_values.append(banned_value)
 
 
-
                 Ban.objects.invalidate_cache()
                 Ban.objects.invalidate_cache()
-                message = _("Selected users have been banned.")
-                messages.success(request, message)
+                messages.success(request, _("Selected users have been banned."))
                 return None
                 return None
 
 
         return self.render(
         return self.render(
-            request, template='misago/admin/users/ban.html', context={
+            request,
+            template='misago/admin/users/ban.html',
+            context={
                 'users': users,
                 'users': users,
                 'form': form,
                 'form': form,
-            })
+            }
+        )
 
 
     def action_delete_accounts(self, request, users):
     def action_delete_accounts(self, request, users):
         for user in users:
         for user in users:
             if user.is_staff or user.is_superuser:
             if user.is_staff or user.is_superuser:
-                message = _("%(user)s is admin and can't be deleted.")
-                mesage = message % {'user': user.username}
-                raise generic.MassActionError(mesage)
+                message = _("%(user)s is admin and can't be deleted.") % {'user': user.username}
+                raise generic.MassActionError(message)
 
 
         for user in users:
         for user in users:
             user.delete()
             user.delete()
@@ -212,22 +207,21 @@ class UsersList(UserAdmin, generic.ListView):
         messages.success(request, message)
         messages.success(request, message)
 
 
     def action_delete_all(self, request, users):
     def action_delete_all(self, request, users):
-        return self.render(
-            request, template='misago/admin/users/delete.html', context={
-                'users': users,
-            })
-
         for user in users:
         for user in users:
             if user.is_staff or user.is_superuser:
             if user.is_staff or user.is_superuser:
-                message = _("%(user)s is admin and can't be deleted.")
-                mesage = message % {'user': user.username}
-                raise generic.MassActionError(mesage)
+                message = _("%(user)s is admin and can't be deleted.") % {'user': user.username}
+                raise generic.MassActionError(message)
 
 
         for user in users:
         for user in users:
             user.delete(delete_content=True)
             user.delete(delete_content=True)
 
 
-        message = _("Selected users and their content has been deleted.")
-        messages.success(request, message)
+        messages.success(request, _("Selected users and their content has been deleted."))
+
+        return self.render(
+            request, template='misago/admin/users/delete.html', context={
+                'users': users,
+            }
+        )
 
 
 
 
 class NewUser(UserAdmin, generic.ModelFormView):
 class NewUser(UserAdmin, generic.ModelFormView):
@@ -255,8 +249,7 @@ class NewUser(UserAdmin, generic.ModelFormView):
         new_user.update_acl_key()
         new_user.update_acl_key()
         new_user.save()
         new_user.save()
 
 
-        messages.success(
-            request, self.message_submit % {'user': target.username})
+        messages.success(request, self.message_submit % {'user': target.username})
         return redirect('misago:admin:users:accounts:edit', pk=new_user.pk)
         return redirect('misago:admin:users:accounts:edit', pk=new_user.pk)
 
 
 
 
@@ -273,8 +266,7 @@ class EditUser(UserAdmin, generic.ModelFormView):
     def handle_form(self, form, request, target):
     def handle_form(self, form, request, target):
         target.username = target.old_username
         target.username = target.old_username
         if target.username != form.cleaned_data.get('username'):
         if target.username != form.cleaned_data.get('username'):
-            target.set_username(
-                form.cleaned_data.get('username'), changed_by=request.user)
+            target.set_username(form.cleaned_data.get('username'), changed_by=request.user)
 
 
         if form.cleaned_data.get('new_password'):
         if form.cleaned_data.get('new_password'):
             target.set_password(form.cleaned_data['new_password'])
             target.set_password(form.cleaned_data['new_password'])
@@ -310,8 +302,7 @@ class EditUser(UserAdmin, generic.ModelFormView):
         target.update_acl_key()
         target.update_acl_key()
         target.save()
         target.save()
 
 
-        messages.success(
-            request, self.message_submit % {'user': target.username})
+        messages.success(request, self.message_submit % {'user': target.username})
 
 
 
 
 class DeletionStep(UserAdmin, generic.ButtonView):
 class DeletionStep(UserAdmin, generic.ButtonView):
@@ -326,7 +317,9 @@ class DeletionStep(UserAdmin, generic.ButtonView):
 
 
     def execute_step(self, user):
     def execute_step(self, user):
         raise NotImplementedError(
         raise NotImplementedError(
-            "execute_step method should return dict with number of deleted_count and is_completed keys")
+            "execute_step method should return dict with "
+            "number of deleted_count and is_completed keys"
+        )
 
 
     def button_action(self, request, target):
     def button_action(self, request, target):
         return JsonResponse(self.execute_step(target))
         return JsonResponse(self.execute_step(target))
@@ -354,7 +347,7 @@ class DeleteThreadsStep(DeletionStep):
 
 
         return {
         return {
             'deleted_count': deleted_threads,
             'deleted_count': deleted_threads,
-            'is_completed': is_completed
+            'is_completed': is_completed,
         }
         }
 
 
 
 
@@ -387,7 +380,7 @@ class DeletePostsStep(DeletionStep):
 
 
         return {
         return {
             'deleted_count': deleted_posts,
             'deleted_count': deleted_posts,
-            'is_completed': is_completed
+            'is_completed': is_completed,
         }
         }
 
 
 
 

+ 1 - 2
misago/users/views/auth.py

@@ -15,8 +15,7 @@ def login(request):
     if request.method == 'POST':
     if request.method == 'POST':
         redirect_to = request.POST.get('redirect_to')
         redirect_to = request.POST.get('redirect_to')
         if redirect_to:
         if redirect_to:
-            is_redirect_safe = is_safe_url(
-                url=redirect_to, host=request.get_host())
+            is_redirect_safe = is_safe_url(url=redirect_to, host=request.get_host())
             if is_redirect_safe:
             if is_redirect_safe:
                 redirect_to_path = urlparse(redirect_to).path
                 redirect_to_path = urlparse(redirect_to).path
                 return redirect(redirect_to_path)
                 return redirect(redirect_to_path)

+ 18 - 18
misago/users/views/forgottenpassword.py

@@ -13,6 +13,7 @@ def reset_view(f):
     @deny_banned_ips
     @deny_banned_ips
     def decorator(*args, **kwargs):
     def decorator(*args, **kwargs):
         return f(*args, **kwargs)
         return f(*args, **kwargs)
+
     return decorator
     return decorator
 
 
 
 
@@ -33,31 +34,30 @@ def reset_password_form(request, pk, token):
     requesting_user = get_object_or_404(get_user_model(), pk=pk)
     requesting_user = get_object_or_404(get_user_model(), pk=pk)
 
 
     try:
     try:
-        if (request.user.is_authenticated and
-                request.user.id != requesting_user.id):
-            message = _("%(user)s, your link has expired. "
-                        "Please request new link and try again.")
-            message = message % {'user': requesting_user.username}
-            raise ResetError(message)
+        if (request.user.is_authenticated and request.user.id != requesting_user.id):
+            message = _("%(user)s, your link has expired. Please request new link and try again.")
+            raise ResetError(message % {'user': requesting_user.username})
 
 
         if not is_password_change_token_valid(requesting_user, token):
         if not is_password_change_token_valid(requesting_user, token):
-            message = _("%(user)s, your link is invalid. "
-                        "Please try again or request new link.")
-            message = message % {'user': requesting_user.username}
-            raise ResetError(message)
+            message = _("%(user)s, your link is invalid. Please try again or request new link.")
+            raise ResetError(message % {'user': requesting_user.username})
 
 
         ban = get_user_ban(requesting_user)
         ban = get_user_ban(requesting_user)
         if ban:
         if ban:
             raise Banned(ban)
             raise Banned(ban)
     except ResetError as e:
     except ResetError as e:
-        return render(request, 'misago/forgottenpassword/error.html', {
-            'message': e.args[0],
-        }, status=400)
-
-    api_url = reverse('misago:api:change-forgotten-password', kwargs={
-        'pk': pk,
-        'token': token,
-    })
+        return render(
+            request, 'misago/forgottenpassword/error.html', {
+                'message': e.args[0],
+            }, status=400
+        )
+
+    api_url = reverse(
+        'misago:api:change-forgotten-password', kwargs={
+            'pk': pk,
+            'token': token,
+        }
+    )
 
 
     request.frontend_context['CHANGE_PASSWORD_API'] = api_url
     request.frontend_context['CHANGE_PASSWORD_API'] = api_url
     return render(request, 'misago/forgottenpassword/form.html')
     return render(request, 'misago/forgottenpassword/form.html')

+ 2 - 5
misago/users/views/lists.py

@@ -1,5 +1,4 @@
-from django.shortcuts import render
-from django.shortcuts import get_object_or_404, redirect
+from django.shortcuts import get_object_or_404, redirect, render
 from django.urls import reverse
 from django.urls import reverse
 from django.utils import six
 from django.utils import six
 from django.views import View
 from django.views import View
@@ -33,9 +32,7 @@ class ListView(View):
         for rank in Rank.objects.filter(is_tab=True).order_by('order'):
         for rank in Rank.objects.filter(is_tab=True).order_by('order'):
             context_data['pages'].append({
             context_data['pages'].append({
                 'name': rank.name,
                 'name': rank.name,
-                'reversed_link': reverse('misago:users-rank', kwargs={
-                    'slug': rank.slug
-                }),
+                'reversed_link': reverse('misago:users-rank', kwargs={'slug': rank.slug}),
                 'is_active': active_rank.pk == rank.pk if active_rank else None
                 'is_active': active_rank.pk == rank.pk if active_rank else None
             })
             })
 
 

+ 18 - 13
misago/users/views/options.py

@@ -1,7 +1,6 @@
 from django.contrib.auth import update_session_auth_hash
 from django.contrib.auth import update_session_auth_hash
 from django.db import IntegrityError
 from django.db import IntegrityError
 from django.shortcuts import render
 from django.shortcuts import render
-from django.urls import reverse
 from django.utils import six
 from django.utils import six
 from django.utils.translation import ugettext as _
 from django.utils.translation import ugettext as _
 
 
@@ -20,9 +19,7 @@ def index(request, *args, **kwargs):
             'component': section['component'],
             'component': section['component'],
         })
         })
 
 
-    request.frontend_context.update({
-        'USER_OPTIONS': user_options
-    })
+    request.frontend_context.update({'USER_OPTIONS': user_options})
 
 
     return render(request, 'misago/options/noscript.html')
     return render(request, 'misago/options/noscript.html')
 
 
@@ -36,9 +33,9 @@ def confirm_change_view(f):
     def decorator(request, token):
     def decorator(request, token):
         try:
         try:
             return f(request, token)
             return f(request, token)
-        except ChangeError as e:
-            return render(request, 'misago/options/credentials_error.html',
-                status=400)
+        except ChangeError:
+            return render(request, 'misago/options/credentials_error.html', status=400)
+
     return decorator
     return decorator
 
 
 
 
@@ -55,9 +52,13 @@ def confirm_email_change(request, token):
         raise ChangeError()
         raise ChangeError()
 
 
     message = _("%(user)s, your e-mail has been changed.")
     message = _("%(user)s, your e-mail has been changed.")
-    return render(request, 'misago/options/credentials_changed.html', {
-            'message': message % {'user': request.user.username},
-        })
+    return render(
+        request, 'misago/options/credentials_changed.html', {
+            'message': message % {
+                'user': request.user.username,
+            },
+        }
+    )
 
 
 
 
 @confirm_change_view
 @confirm_change_view
@@ -71,6 +72,10 @@ def confirm_password_change(request, token):
     request.user.save(update_fields=['password'])
     request.user.save(update_fields=['password'])
 
 
     message = _("%(user)s, your password has been changed.")
     message = _("%(user)s, your password has been changed.")
-    return render(request, 'misago/options/credentials_changed.html', {
-            'message': message % {'user': request.user.username},
-        })
+    return render(
+        request, 'misago/options/credentials_changed.html', {
+            'message': message % {
+                'user': request.user.username,
+            },
+        }
+    )

+ 11 - 19
misago/users/views/profile.py

@@ -1,7 +1,6 @@
 from django.contrib.auth import get_user_model
 from django.contrib.auth import get_user_model
 from django.http import Http404
 from django.http import Http404
-from django.shortcuts import render
-from django.shortcuts import get_object_or_404, redirect
+from django.shortcuts import get_object_or_404, redirect, render
 from django.utils import six
 from django.utils import six
 from django.views import View
 from django.views import View
 
 
@@ -10,8 +9,7 @@ from misago.core.shortcuts import paginate, pagination_dict, validate_slug
 from misago.users.bans import get_user_ban
 from misago.users.bans import get_user_ban
 from misago.users.online.utils import get_user_status
 from misago.users.online.utils import get_user_status
 from misago.users.pages import user_profile
 from misago.users.pages import user_profile
-from misago.users.serializers import (
-    BanDetailsSerializer, UsernameChangeSerializer, UserSerializer)
+from misago.users.serializers import BanDetailsSerializer, UsernameChangeSerializer, UserSerializer
 from misago.users.viewmodels import Followers, Follows, UserPosts, UserThreads
 from misago.users.viewmodels import Followers, Follows, UserPosts, UserThreads
 
 
 
 
@@ -38,8 +36,7 @@ class ProfileView(View):
         return render(request, self.template_name, context_data)
         return render(request, self.template_name, context_data)
 
 
     def get_profile(self, request, pk, slug):
     def get_profile(self, request, pk, slug):
-        queryset = UserModel.objects.select_related(
-            'rank', 'online_tracker', 'ban_cache')
+        queryset = UserModel.objects.select_related('rank', 'online_tracker', 'ban_cache')
 
 
         profile = get_object_or_404(queryset, pk=pk)
         profile = get_object_or_404(queryset, pk=pk)
 
 
@@ -70,7 +67,8 @@ class ProfileView(View):
             })
             })
 
 
         request.frontend_context['PROFILE'] = UserProfileSerializer(
         request.frontend_context['PROFILE'] = UserProfileSerializer(
-            profile, context={'user': request.user}).data
+            profile, context={'user': request.user}
+        ).data
 
 
         if not profile.is_active:
         if not profile.is_active:
             request.frontend_context['PROFILE']['is_active'] = False
             request.frontend_context['PROFILE']['is_active'] = False
@@ -104,11 +102,7 @@ class LandingView(ProfileView):
     def get(self, request, *args, **kwargs):
     def get(self, request, *args, **kwargs):
         profile = self.get_profile(request, kwargs.pop('pk'), kwargs.pop('slug'))
         profile = self.get_profile(request, kwargs.pop('pk'), kwargs.pop('slug'))
 
 
-        return redirect(
-            user_profile.get_default_link(),
-            slug=profile.slug,
-            pk=profile.pk
-        )
+        return redirect(user_profile.get_default_link(), slug=profile.slug, pk=profile.pk)
 
 
 
 
 class UserPostsView(ProfileView):
 class UserPostsView(ProfileView):
@@ -161,9 +155,7 @@ class UserUsernameHistoryView(ProfileView):
         page = paginate(queryset, None, 14, 4)
         page = paginate(queryset, None, 14, 4)
 
 
         data = pagination_dict(page)
         data = pagination_dict(page)
-        data.update({
-            'results': UsernameChangeSerializer(page.object_list, many=True).data
-        })
+        data.update({'results': UsernameChangeSerializer(page.object_list, many=True).data})
 
 
         request.frontend_context['PROFILE_NAME_HISTORY'] = data
         request.frontend_context['PROFILE_NAME_HISTORY'] = data
 
 
@@ -187,7 +179,7 @@ class UserBanView(ProfileView):
 
 
 
 
 UserProfileSerializer = UserSerializer.subset_fields(
 UserProfileSerializer = UserSerializer.subset_fields(
-    'id', 'username', 'slug', 'email', 'joined_on', 'rank', 'title', 'avatars',
-    'is_avatar_locked', 'signature', 'is_signature_locked', 'followers', 'following',
-    'threads', 'posts', 'acl', 'is_followed', 'is_blocked', 'status', 'absolute_url',
-    'api_url')
+    'id', 'username', 'slug', 'email', 'joined_on', 'rank', 'title', 'avatars', 'is_avatar_locked',
+    'signature', 'is_signature_locked', 'followers', 'following', 'threads', 'posts', 'acl',
+    'is_followed', 'is_blocked', 'status', 'absolute_url', 'api_url'
+)

+ 38 - 0
pycodestyle.py

@@ -0,0 +1,38 @@
+"""
+Code style cleanups done after yapf
+"""
+import argparse
+import codecs
+import os
+
+from extras import fixdictsformatting
+
+
+CLEANUPS = [
+    fixdictsformatting,
+]
+
+
+def walk_directory(root, dirs, files):
+    for filename in files:
+        if filename.lower().endswith('.py'):
+            with codecs.open(os.path.join(root, filename), 'r', 'utf-8') as f:
+                filesource = f.read()
+
+            org_source = filesource
+
+            for cleanup in CLEANUPS:
+                filesource = cleanup.fix_formatting(filesource)
+
+            if org_source != filesource:
+                print 'afterclean: %s' % os.path.join(root, filename)
+                with codecs.open(os.path.join(root, filename), 'w', 'utf-8') as f:
+                    f.write(filesource)
+
+
+if __name__ == '__main__':
+    parser = argparse.ArgumentParser()
+    parser.add_argument('path', nargs='?', default='./')
+
+    for args in os.walk(parser.parse_args().path):
+        walk_directory(*args)