Browse Source

first pass of cleansource script

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

+ 0 - 1
.style.yapf

@@ -5,7 +5,6 @@ dedent_closing_brackets = true
 each_dict_entry_on_separate_line = true
 each_dict_entry_on_separate_line = true
 indent_dictionary_value = true
 indent_dictionary_value = true
 join_multiple_lines = false
 join_multiple_lines = false
-spaces_before_comment = 4
 split_arguments_when_comma_terminated = true
 split_arguments_when_comma_terminated = true
 split_before_first_argument = true
 split_before_first_argument = true
 split_before_logical_operator = true
 split_before_logical_operator = true

+ 1 - 1
cleansource

@@ -1,5 +1,5 @@
 #!/bin/bash
 #!/bin/bash
 
 
+yapf -ir misago -e '*/project_template/**/*.py'
 isort -rc misago
 isort -rc misago
-yapf -ir misago
 pylint misago
 pylint misago

+ 6 - 6
misago/acl/algebra.py

@@ -9,17 +9,17 @@ 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():

+ 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

+ 4 - 6
misago/acl/forms.py

@@ -25,8 +25,7 @@ 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)
 
 
@@ -34,9 +33,8 @@ def get_permissions_forms(role, data=None):
             if data:
             if data:
                 perms_forms.append(FormType(data, prefix=extension))
                 perms_forms.append(FormType(data, prefix=extension))
             else:
             else:
-                perms_forms.append(FormType(
-                    initial=role_permissions.get(extension),
-                    prefix=extension
-                ))
+                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, ),
         ),
         ),
     ]
     ]

+ 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,

+ 3 - 3
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)
 
 

+ 24 - 12
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,24 @@ 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}),
             reverse('misago:admin:permissions:users:edit', kwargs={'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)
 
 
@@ -63,23 +69,29 @@ 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

+ 8 - 12
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)
@@ -53,13 +52,11 @@ class RoleFormMixin(object):
             elif form.is_valid() and len(perms_forms) != valid_forms:
             elif form.is_valid() and len(perms_forms) != valid_forms:
                 form.add_error(None, _("Form contains errors."))
                 form.add_error(None, _("Form contains errors."))
 
 
-        return self.render(
-            request,
-            {
-                'form': form,
-                'target': target,
-                'perms_forms': perms_forms,
-            })
+        return self.render(request, {
+            'form': form,
+            'target': target,
+            'perms_forms': perms_forms,
+        })
 
 
 
 
 class NewRole(RoleFormMixin, RoleAdmin, generic.ModelFormView):
 class NewRole(RoleFormMixin, RoleAdmin, generic.ModelFormView):
@@ -73,8 +70,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'

+ 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)

+ 22 - 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,21 @@ 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 +128,21 @@ 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")

+ 20 - 26
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)
@@ -57,8 +49,9 @@ class AdminLoginViewTests(TestCase):
     def test_login_returns_200_on_invalid_post(self):
     def test_login_returns_200_on_invalid_post(self):
         """form handles invalid data gracefully"""
         """form handles invalid data gracefully"""
         response = self.client.post(
         response = self.client.post(
-            reverse('misago:admin:index'),
-            data={'username': 'Nope', 'password': 'Nope'})
+            reverse('misago:admin:index'), 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")
@@ -74,8 +67,9 @@ class AdminLoginViewTests(TestCase):
         user.save()
         user.save()
 
 
         response = self.client.post(
         response = self.client.post(
-            reverse('misago:admin:index'),
-            data={'username': 'Bob', 'password': 'Pass.123'})
+            reverse('misago:admin:index'), 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.")
 
 
@@ -88,8 +82,9 @@ class AdminLoginViewTests(TestCase):
         user.save()
         user.save()
 
 
         response = self.client.post(
         response = self.client.post(
-            reverse('misago:admin:index'),
-            data={'username': 'Bob', 'password': 'Pass.123'})
+            reverse('misago:admin:index'), 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.")
 
 
@@ -102,8 +97,9 @@ class AdminLoginViewTests(TestCase):
         user.save()
         user.save()
 
 
         response = self.client.post(
         response = self.client.post(
-            reverse('misago:admin:index'),
-            data={'username': 'Bob', 'password': 'Pass.123'})
+            reverse('misago:admin:index'), data={'username': 'Bob',
+                                                 'password': 'Pass.123'}
+        )
 
 
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
@@ -116,8 +112,9 @@ class AdminLoginViewTests(TestCase):
         user.save()
         user.save()
 
 
         response = self.client.post(
         response = self.client.post(
-            reverse('misago:admin:index'),
-            data={'username': 'Bob', 'password': 'Pass.123'})
+            reverse('misago:admin:index'), data={'username': 'Bob',
+                                                 'password': 'Pass.123'}
+        )
 
 
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
@@ -199,8 +196,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 +210,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)

+ 5 - 4
misago/admin/testutils.py

@@ -9,8 +9,9 @@ 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'))

+ 0 - 1
misago/admin/urls.py

@@ -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')

+ 5 - 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,8 @@ 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 +49,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)

+ 12 - 11
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
@@ -69,7 +69,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 "
             "You have to define your own handle_form method to handle "
-            "form submissions.")
+            "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)

+ 13 - 18
misago/admin/views/generic/list.py

@@ -68,6 +68,7 @@ class ListView(AdminView):
     """
     """
     Dispatch response
     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 +77,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 +109,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 +122,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 +151,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,7 +169,8 @@ 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
 
 
@@ -236,11 +228,13 @@ 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
@@ -290,8 +284,7 @@ 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:]
@@ -300,6 +293,7 @@ class ListView(AdminView):
     """
     """
     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'))
@@ -332,6 +326,7 @@ class ListView(AdminView):
     """
     """
     Querystring builder
     Querystring builder
     """
     """
+
     def make_querystring(self, context):
     def make_querystring(self, context):
         values = {}
         values = {}
         filter_values = {}
         filter_values = {}

+ 17 - 23
misago/admin/views/index.py

@@ -21,18 +21,21 @@ UserModel = get_user_model()
 
 
 def admin_index(request):
 def admin_index(request):
     db_stats = {
     db_stats = {
-        'threads': Thread.objects.count(),
-        'posts': Post.objects.count(),
-        'users': UserModel.objects.count(),
-        'inactive_users': UserModel.objects.exclude(
-            requires_activation=UserModel.ACTIVATION_NONE
-        ).count()
+        'threads':
+            Thread.objects.count(),
+        'posts':
+            Post.objects.count(),
+        'users':
+            UserModel.objects.count(),
+        'inactive_users':
+            UserModel.objects.exclude(requires_activation=UserModel.ACTIVATION_NONE).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):
@@ -54,15 +57,9 @@ def check_version(request):
             for i in range(3):
             for i in range(3):
                 if latest[i] > current[i]:
                 if latest[i] > current[i]:
                     message = _("Outdated: %(current)s < %(latest)s")
                     message = _("Outdated: %(current)s < %(latest)s")
-                    formats = {
-                        'latest': latest_version,
-                        'current': __version__
-                    }
-
-                    version = {
-                        'is_error': True,
-                        'message': message % formats
-                    }
+                    formats = {'latest': latest_version, 'current': __version__}
+
+                    version = {'is_error': True, 'message': message % formats}
                     break
                     break
             else:
             else:
                 formats = {'current': __version__}
                 formats = {'current': __version__}
@@ -74,8 +71,5 @@ def check_version(request):
             cache.set(VERSION_CHECK_CACHE_KEY, version, 180)
             cache.set(VERSION_CHECK_CACHE_KEY, version, 180)
         except (RequestException, IndexError, KeyError, ValueError):
         except (RequestException, IndexError, KeyError, ValueError):
             message = _("Failed to connect to GitHub API. Try again later.")
             message = _("Failed to connect to GitHub API. Try again later.")
-            version = {
-                'is_error': True,
-                'message': message
-            }
+            version = {'is_error': True, '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'

+ 6 - 3
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'),
         )
         )
 
 

+ 94 - 70
misago/categories/forms.py

@@ -16,6 +16,8 @@ from .models import Category, CategoryRole
 """
 """
 Fields
 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,19 +44,17 @@ class AdminCategoryChoiceField(AdminCategoryFieldMixin, TreeNodeChoiceField):
     pass
     pass
 
 
 
 
-class AdminCategoryMultipleChoiceField(
-        AdminCategoryFieldMixin, TreeNodeMultipleChoiceField):
+class AdminCategoryMultipleChoiceField(AdminCategoryFieldMixin, TreeNodeMultipleChoiceField):
     pass
     pass
 
 
 
 
 """
 """
 Forms
 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,
@@ -65,8 +65,10 @@ class CategoryFormBase(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 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"),
@@ -77,22 +79,28 @@ class CategoryFormBase(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 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 +142,39 @@ 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 +187,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
@@ -186,13 +205,14 @@ class DeleteCategoryFormBase(forms.ModelForm):
 def DeleteFormFactory(instance):
 def DeleteFormFactory(instance):
     content_queryset = Category.objects.all_categories().order_by('lft')
     content_queryset = Category.objects.all_categories().order_by('lft')
     fields = {
     fields = {
-        'move_threads_to': AdminCategoryChoiceField(
-            label=_("Move category threads to"),
-            queryset=content_queryset,
-            initial=instance.parent,
-            empty_label=_('Delete with category'),
-            required=False
-        )
+        'move_threads_to':
+            AdminCategoryChoiceField(
+                label=_("Move category threads to"),
+                queryset=content_queryset,
+                initial=instance.parent,
+                empty_label=_('Delete with category'),
+                required=False
+            )
     }
     }
 
 
     not_siblings = models.Q(lft__lt=instance.lft)
     not_siblings = models.Q(lft__lt=instance.lft)
@@ -208,7 +228,7 @@ def DeleteFormFactory(instance):
             required=False
             required=False
         )
         )
 
 
-    return type('DeleteCategoryFormFinal', (DeleteCategoryFormBase,), fields)
+    return type('DeleteCategoryFormFinal', (DeleteCategoryFormBase, ), fields)
 
 
 
 
 class CategoryRoleForm(forms.ModelForm):
 class CategoryRoleForm(forms.ModelForm):
@@ -221,29 +241,33 @@ class CategoryRoleForm(forms.ModelForm):
 
 
 def RoleCategoryACLFormFactory(category, category_roles, selected_role):
 def RoleCategoryACLFormFactory(category, category_roles, selected_role):
     attrs = {
     attrs = {
-        'category': category,
-        'role': forms.ModelChoiceField(
-            label=_("Role"),
-            required=False,
-            queryset=category_roles,
-            initial=selected_role,
-            empty_label=_("No access")
-        )
+        'category':
+            category,
+        'role':
+            forms.ModelChoiceField(
+                label=_("Role"),
+                required=False,
+                queryset=category_roles,
+                initial=selected_role,
+                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):
     attrs = {
     attrs = {
-        'role': role,
-        'category_role': forms.ModelChoiceField(
-            label=_("Role"),
-            required=False,
-            queryset=category_roles,
-            initial=selected_role,
-            empty_label=_("No access")
-        )
+        'role':
+            role,
+        'category_role':
+            forms.ModelChoiceField(
+                label=_("Role"),
+                required=False,
+                queryset=category_roles,
+                initial=selected_role,
+                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 - 6
misago/categories/migrations/0003_categories_roles.py

@@ -162,9 +162,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(
@@ -174,9 +172,7 @@ def create_default_categories_roles(apps, schema_editor):
     )
     )
 
 
     RoleCategoryACL.objects.create(
     RoleCategoryACL.objects.create(
-        role=Role.objects.get(special_role='anonymous'),
-        category=category,
-        category_role=read_only
+        role=Role.objects.get(special_role='anonymous'), category=category, category_role=read_only
     )
     )
 
 
 
 

+ 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,
         ),
         ),
     ]
     ]

+ 3 - 16
misago/categories/models.py

@@ -60,12 +60,7 @@ class CategoryManager(TreeManager):
 
 
 @python_2_unicode_compatible
 @python_2_unicode_compatible
 class Category(MPTTModel):
 class Category(MPTTModel):
-    parent = TreeForeignKey(
-        'self',
-        null=True,
-        blank=True,
-        related_name='children'
-    )
+    parent = TreeForeignKey('self', null=True, blank=True, 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)
     slug = models.CharField(max_length=255)
     slug = models.CharField(max_length=255)
@@ -75,11 +70,7 @@ class Category(MPTTModel):
     posts = models.PositiveIntegerField(default=0)
     posts = models.PositiveIntegerField(default=0)
     last_post_on = models.DateTimeField(null=True, blank=True)
     last_post_on = models.DateTimeField(null=True, blank=True)
     last_thread = models.ForeignKey(
     last_thread = models.ForeignKey(
-        'misago_threads.Thread',
-        related_name='+',
-        null=True,
-        blank=True,
-        on_delete=models.SET_NULL
+        'misago_threads.Thread', related_name='+', null=True, blank=True, 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)
@@ -95,11 +86,7 @@ class Category(MPTTModel):
     prune_started_after = models.PositiveIntegerField(default=0)
     prune_started_after = models.PositiveIntegerField(default=0)
     prune_replied_after = models.PositiveIntegerField(default=0)
     prune_replied_after = models.PositiveIntegerField(default=0)
     archive_pruned_in = models.ForeignKey(
     archive_pruned_in = models.ForeignKey(
-        'self',
-        related_name='pruned_archive',
-        null=True,
-        blank=True,
-        on_delete=models.SET_NULL
+        'self', related_name='pruned_archive', null=True, blank=True, 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)
 
 

+ 16 - 1
misago/categories/permissions.py

@@ -15,6 +15,8 @@ from .models import Category, CategoryRole, RoleCategoryACL
 """
 """
 Admin Permissions Form
 Admin Permissions Form
 """
 """
+
+
 class PermissionsForm(forms.Form):
 class PermissionsForm(forms.Form):
     legend = _("Category access")
     legend = _("Category access")
 
 
@@ -32,6 +34,8 @@ def change_permissions_form(role):
 """
 """
 ACL Builder
 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,7 +79,10 @@ 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
     )
     )
@@ -91,6 +98,8 @@ def build_category_acl(acl, category, categories_roles, key_name):
 """
 """
 ACL's for targets
 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)
@@ -121,6 +130,8 @@ def register_with(registry):
 """
 """
 ACL tests
 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 +140,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 +150,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 - 31
misago/categories/serializers.py

@@ -13,19 +13,17 @@ __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')
-                )):
+        if not all((acl.get('can_see'), acl.get('can_browse'), acl.get('can_see_all_threads'))):
             return None
             return None
 
 
         return f(self, obj)
         return f(self, obj)
+
     return decorator
     return decorator
 
 
 
 
@@ -44,28 +42,10 @@ class CategorySerializer(serializers.ModelSerializer, MutableFields):
     class Meta:
     class Meta:
         model = Category
         model = Category
         fields = (
         fields = (
-            'id',
-            'parent',
-            'name',
-            'description',
-            'is_closed',
-            'threads',
-            'posts',
-            'last_post_on',
-            'last_thread_title',
-            'last_poster_name',
-            'css_class',
-            'is_read',
-            'subcategories',
-            'absolute_url',
-            'last_thread_url',
-            'last_post_url',
-            'last_poster_url',
-            'acl',
-            'api_url',
-            'level',
-            'lft',
-            'rght',
+            'id', 'parent', 'name', 'description', 'is_closed', 'threads', 'posts', 'last_post_on',
+            'last_thread_title', 'last_poster_name', 'css_class', 'is_read', 'subcategories',
+            'absolute_url', 'last_thread_url', 'last_post_url', 'last_poster_url', 'acl', 'api_url',
+            'level', 'lft', 'rght',
         )
         )
 
 
     def get_description(self, obj):
     def get_description(self, obj):
@@ -103,10 +83,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
 
 

+ 3 - 4
misago/categories/signals.py

@@ -7,14 +7,13 @@ from .models import Category
 
 
 delete_category_content = Signal()
 delete_category_content = Signal()
 move_category_content = Signal(providing_args=["new_category"])
 move_category_content = Signal(providing_args=["new_category"])
-
-
 """
 """
 Signal handlers
 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_slug=sender.slug
+        last_poster_name=sender.username, last_poster_slug=sender.slug
     )
     )

+ 140 - 148
misago/categories/tests/test_categories_admin_views.py

@@ -14,52 +14,41 @@ 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.rght - root.lft + 1,
+                category, category.level, category.lft - 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 +57,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 +67,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 +78,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 +101,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 +122,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 +136,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):
@@ -158,15 +146,13 @@ class CategoryAdminViewsTests(CategoryAdminTestCate):
         first_category = Category.objects.get(slug='first-category')
         first_category = Category.objects.get(slug='first-category')
 
 
         response = self.client.get(
         response = self.client.get(
-            reverse('misago:admin:categories:nodes:edit', kwargs={
-                'pk': private_threads.pk
-            }))
+            reverse('misago:admin:categories:nodes:edit', kwargs={'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={
-                'pk': root.pk
-            }))
+            reverse('misago:admin:categories:nodes:edit', kwargs={'pk': root.pk})
+        )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
         response = self.client.post(
         response = self.client.post(
@@ -177,29 +163,28 @@ 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={
-                'pk': test_category.pk
-            }))
+            reverse('misago:admin:categories:nodes:edit', kwargs={'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={
-                'pk': test_category.pk
-            }),
+            reverse('misago:admin:categories:nodes:edit', kwargs={'pk': test_category.pk}),
             data={
             data={
                 'name': 'Test Category Edited',
                 'name': 'Test Category Edited',
                 'new_parent': root.pk,
                 'new_parent': root.pk,
                 '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,21 +193,19 @@ 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={
-                'pk': test_category.pk
-            }),
+            reverse('misago:admin:categories:nodes:edit', kwargs={'pk': test_category.pk}),
             data={
             data={
                 'name': 'Test Category Edited',
                 'name': 'Test Category Edited',
                 'new_parent': first_category.pk,
                 'new_parent': first_category.pk,
                 '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 +214,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 +222,31 @@ 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={
-                'pk': category_b.pk
-            }))
+            reverse('misago:admin:categories:nodes:up', kwargs={'pk': category_b.pk})
+        )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
         self.assertValidTree([
         self.assertValidTree([
@@ -270,9 +257,8 @@ class CategoryAdminViewsTests(CategoryAdminTestCate):
         ])
         ])
 
 
         response = self.client.post(
         response = self.client.post(
-            reverse('misago:admin:categories:nodes:up', kwargs={
-                'pk': category_b.pk
-            }))
+            reverse('misago:admin:categories:nodes:up', kwargs={'pk': category_b.pk})
+        )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
         self.assertValidTree([
         self.assertValidTree([
@@ -283,9 +269,8 @@ class CategoryAdminViewsTests(CategoryAdminTestCate):
         ])
         ])
 
 
         response = self.client.post(
         response = self.client.post(
-            reverse('misago:admin:categories:nodes:down', kwargs={
-                'pk': category_b.pk
-            }))
+            reverse('misago:admin:categories:nodes:down', kwargs={'pk': category_b.pk})
+        )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
         self.assertValidTree([
         self.assertValidTree([
@@ -296,9 +281,8 @@ class CategoryAdminViewsTests(CategoryAdminTestCate):
         ])
         ])
 
 
         response = self.client.post(
         response = self.client.post(
-            reverse('misago:admin:categories:nodes:down', kwargs={
-                'pk': category_b.pk
-            }))
+            reverse('misago:admin:categories:nodes:down', kwargs={'pk': category_b.pk})
+        )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
         self.assertValidTree([
         self.assertValidTree([
@@ -309,9 +293,8 @@ class CategoryAdminViewsTests(CategoryAdminTestCate):
         ])
         ])
 
 
         response = self.client.post(
         response = self.client.post(
-            reverse('misago:admin:categories:nodes:down', kwargs={
-                'pk': category_b.pk
-            }))
+            reverse('misago:admin:categories:nodes:down', kwargs={'pk': category_b.pk})
+        )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
         self.assertValidTree([
         self.assertValidTree([
@@ -327,7 +310,6 @@ class CategoryAdminDeleteViewTests(CategoryAdminTestCate):
         super(CategoryAdminDeleteViewTests, self).setUp()
         super(CategoryAdminDeleteViewTests, self).setUp()
         self.root = Category.objects.root_category()
         self.root = Category.objects.root_category()
         self.first_category = Category.objects.get(slug='first-category')
         self.first_category = Category.objects.get(slug='first-category')
-
         """
         """
         Create categories tree for test cases:
         Create categories tree for test cases:
 
 
@@ -341,53 +323,71 @@ 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,
-        })
+        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):
@@ -397,19 +397,17 @@ class CategoryAdminDeleteViewTests(CategoryAdminTestCate):
         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={
-                'pk': self.category_b.pk
-            }))
+            reverse('misago:admin:categories:nodes:delete', kwargs={'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={
-                'pk': self.category_b.pk
-            }),
+            reverse('misago:admin:categories:nodes:delete', kwargs={'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)
@@ -430,19 +428,15 @@ class CategoryAdminDeleteViewTests(CategoryAdminTestCate):
             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={
-                'pk': self.category_b.pk
-            }))
+            reverse('misago:admin:categories:nodes:delete', kwargs={'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={
-                'pk': self.category_b.pk
-            }),
-            data={
-                'move_children_to': '',
-                'move_threads_to': ''
-            })
+            reverse('misago:admin:categories:nodes:delete', kwargs={'pk': self.category_b.pk}),
+            data={'move_children_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)
@@ -463,19 +457,17 @@ class CategoryAdminDeleteViewTests(CategoryAdminTestCate):
         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={
-                'pk': self.category_d.pk
-            }))
+            reverse('misago:admin:categories:nodes:delete', kwargs={'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={
-                'pk': self.category_d.pk
-            }),
+            reverse('misago:admin:categories:nodes:delete', kwargs={'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)

+ 116 - 100
misago/categories/tests/test_permissions_admin_views.py

@@ -13,76 +13,78 @@ 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={
-                'pk': test_role.pk
-            }))
+            reverse('misago:admin:permissions:categories:edit', kwargs={'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={
-                'pk': test_role.pk
-            }),
-            data=fake_data({'name': 'Top Lel'}))
+            reverse('misago:admin:permissions:categories:edit', kwargs={'pk': test_role.pk}),
+            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={
-                'pk': test_role.pk
-            }))
+            reverse('misago:admin:permissions:categories:delete', kwargs={'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 +92,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 +101,55 @@ 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 +158,25 @@ 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(
-            category_role_set.get(role=test_role_b).category_role_id,
-            role_comments.pk)
+        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)
 
 
     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')
 
 
@@ -179,11 +186,9 @@ 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={
-                'pk': test_role.pk
-            }))
+            reverse('misago:admin:permissions:users:categories', kwargs={'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,45 +198,56 @@ 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)
 
 
         # 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={
-                'pk': test_role.pk
-            }))
+            reverse('misago:admin:permissions:users:categories', kwargs={'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 +256,46 @@ 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={
-                'pk': test_role.pk
-            }))
+            reverse('misago:admin:permissions:users:categories', kwargs={'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={
-                'pk': test_role.pk
-            }),
+            reverse('misago:admin:permissions:users:categories', kwargs={'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)

+ 24 - 21
misago/categories/tests/test_utils.py

@@ -12,7 +12,6 @@ class CategoriesUtilsTests(AuthenticatedUserTestCase):
 
 
         self.root = Category.objects.root_category()
         self.root = Category.objects.root_category()
         self.first_category = Category.objects.get(slug='first-category')
         self.first_category = Category.objects.get(slug='first-category')
-
         """
         """
         Create categories tree for test cases:
         Create categories tree for test cases:
 
 
@@ -29,43 +28,52 @@ class CategoriesUtilsTests(AuthenticatedUserTestCase):
         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 +81,19 @@ 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(
-            self.user, Category.objects.get(slug='subcategory-f'))
+        categories_tree = get_categories_tree(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 - 3
misago/categories/utils.py

@@ -13,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)
 
 

+ 10 - 8
misago/categories/views/categoriesadmin.py

@@ -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)

+ 25 - 22
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)
@@ -58,13 +57,11 @@ class RoleFormMixin(object):
             elif form.is_valid() and len(perms_forms) != valid_forms:
             elif form.is_valid() and len(perms_forms) != valid_forms:
                 form.add_error(None, _("Form contains errors."))
                 form.add_error(None, _("Form contains errors."))
 
 
-        return self.render(
-            request,
-            {
-                'form': form,
-                'target': target,
-                'perms_forms': perms_forms,
-            })
+        return self.render(request, {
+            'form': form,
+            'target': target,
+            'perms_forms': perms_forms,
+        })
 
 
 
 
 class NewCategoryRole(RoleFormMixin, CategoryRoleAdmin, generic.ModelFormView):
 class NewCategoryRole(RoleFormMixin, CategoryRoleAdmin, generic.ModelFormView):
@@ -78,8 +75,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):
@@ -92,6 +88,8 @@ class DeleteCategoryRole(CategoryRoleAdmin, generic.ButtonView):
 Create category roles view for assinging roles to category,
 Create category roles view for assinging roles to category,
 add link to it in categories list
 add link to it in categories list
 """
 """
+
+
 class CategoryPermissions(CategoryAdmin, generic.ModelFormView):
 class CategoryPermissions(CategoryAdmin, generic.ModelFormView):
     templates_dir = 'misago/admin/categoryroles'
     templates_dir = 'misago/admin/categoryroles'
     template = 'categoryroles.html'
     template = 'categoryroles.html'
@@ -107,7 +105,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))
@@ -126,7 +125,8 @@ class CategoryPermissions(CategoryAdmin, generic.ModelFormView):
                             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,18 +144,19 @@ 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,
 Create role categories view for assinging categories to role,
 add link to it in user roles list
 add link to it in user roles list
 """
 """
+
+
 class RoleCategoriesACL(RoleAdmin, generic.ModelFormView):
 class RoleCategoriesACL(RoleAdmin, generic.ModelFormView):
     templates_dir = 'misago/admin/categoryroles'
     templates_dir = 'misago/admin/categoryroles'
     template = 'rolecategories.html'
     template = 'rolecategories.html'
@@ -176,9 +177,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 +192,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,6 +215,7 @@ 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',

+ 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 - 11
misago/conf/context_processors.py

@@ -12,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,
@@ -32,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,
     })
     })
 
 

+ 21 - 40
misago/conf/defaults.py

@@ -11,7 +11,6 @@ instead of Django's `django.conf.settings`.
 
 
 _MISAGO_JS_DEBUG = False
 _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
 
 
@@ -28,19 +27,16 @@ MISAGO_ACL_EXTENSIONS = [
     'misago.search.permissions',
     'misago.search.permissions',
 ]
 ]
 
 
-
 # Custom markup extensions
 # Custom markup extensions
 
 
 MISAGO_MARKUP_EXTENSIONS = []
 MISAGO_MARKUP_EXTENSIONS = []
 
 
-
 # Posting middlewares
 # Posting middlewares
 # https://misago.readthedocs.io/en/latest/developers/posting_process.html
 # https://misago.readthedocs.io/en/latest/developers/posting_process.html
 
 
 MISAGO_POSTING_MIDDLEWARES = [
 MISAGO_POSTING_MIDDLEWARES = [
     # Always keep FloodProtectionMiddleware middleware first one
     # Always keep FloodProtectionMiddleware middleware first one
     'misago.threads.api.postingendpoint.floodprotection.FloodProtectionMiddleware',
     'misago.threads.api.postingendpoint.floodprotection.FloodProtectionMiddleware',
-
     'misago.threads.api.postingendpoint.category.CategoryMiddleware',
     'misago.threads.api.postingendpoint.category.CategoryMiddleware',
     'misago.threads.api.postingendpoint.privatethread.PrivateThreadMiddleware',
     'misago.threads.api.postingendpoint.privatethread.PrivateThreadMiddleware',
     'misago.threads.api.postingendpoint.reply.ReplyMiddleware',
     'misago.threads.api.postingendpoint.reply.ReplyMiddleware',
@@ -63,7 +59,6 @@ MISAGO_POSTING_MIDDLEWARES = [
     'misago.threads.api.postingendpoint.emailnotification.EmailNotificationMiddleware',
     'misago.threads.api.postingendpoint.emailnotification.EmailNotificationMiddleware',
 ]
 ]
 
 
-
 # Configured thread types
 # Configured thread types
 
 
 MISAGO_THREAD_TYPES = [
 MISAGO_THREAD_TYPES = [
@@ -71,7 +66,6 @@ MISAGO_THREAD_TYPES = [
     'misago.threads.threadtypes.privatethread.PrivateThread',
     'misago.threads.threadtypes.privatethread.PrivateThread',
 ]
 ]
 
 
-
 # Search extensions
 # Search extensions
 
 
 MISAGO_SEARCH_EXTENSIONS = [
 MISAGO_SEARCH_EXTENSIONS = [
@@ -79,13 +73,11 @@ MISAGO_SEARCH_EXTENSIONS = [
     'misago.users.search.SearchUsers',
     'misago.users.search.SearchUsers',
 ]
 ]
 
 
-
 # Misago-admin specific date formats
 # Misago-admin specific date formats
 
 
 MISAGO_COMPACT_DATE_FORMAT_DAY_MONTH = 'j M'
 MISAGO_COMPACT_DATE_FORMAT_DAY_MONTH = 'j M'
 MISAGO_COMPACT_DATE_FORMAT_DAY_MONTH_YEAR = 'M \'y'
 MISAGO_COMPACT_DATE_FORMAT_DAY_MONTH_YEAR = 'M \'y'
 
 
-
 # Additional registration validators
 # Additional registration validators
 # https://misago.readthedocs.io/en/latest/developers/validating_registrations.html
 # https://misago.readthedocs.io/en/latest/developers/validating_registrations.html
 
 
@@ -94,24 +86,20 @@ MISAGO_NEW_REGISTRATIONS_VALIDATORS = [
     'misago.users.validators.validate_with_sfs',
     'misago.users.validators.validate_with_sfs',
 ]
 ]
 
 
-
 # Stop Forum Spam settings
 # Stop Forum Spam settings
 
 
 MISAGO_USE_STOP_FORUM_SPAM = True
 MISAGO_USE_STOP_FORUM_SPAM = True
 MISAGO_STOP_FORUM_SPAM_MIN_CONFIDENCE = 80
 MISAGO_STOP_FORUM_SPAM_MIN_CONFIDENCE = 80
 
 
-
 # Login API URL
 # Login API URL
 
 
 MISAGO_LOGIN_API_URL = 'auth'
 MISAGO_LOGIN_API_URL = 'auth'
 
 
-
 # Misago Admin Path
 # Misago Admin Path
 # Omit starting and trailing slashes. To disable Misago admin, empty this value.
 # Omit starting and trailing slashes. To disable Misago admin, empty this value.
 
 
 MISAGO_ADMIN_PATH = 'admincp'
 MISAGO_ADMIN_PATH = 'admincp'
 
 
-
 # Admin urls namespaces that Misago's AdminAuthMiddleware should protect
 # Admin urls namespaces that Misago's AdminAuthMiddleware should protect
 
 
 MISAGO_ADMIN_NAMESPACES = [
 MISAGO_ADMIN_NAMESPACES = [
@@ -119,82 +107,68 @@ MISAGO_ADMIN_NAMESPACES = [
     'misago:admin',
     'misago:admin',
 ]
 ]
 
 
-
 # How long (in minutes) since previous request to admin namespace should admin session last.
 # How long (in minutes) since previous request to admin namespace should admin session last.
 
 
 MISAGO_ADMIN_SESSION_EXPIRATION = 60
 MISAGO_ADMIN_SESSION_EXPIRATION = 60
 
 
-
 # Display threads on forum index
 # Display threads on forum index
 # Change this to false to display categories list instead
 # Change this to false to display categories list instead
 
 
 MISAGO_THREADS_ON_INDEX = True
 MISAGO_THREADS_ON_INDEX = True
 
 
-
 # Max age of notifications in days
 # Max age of notifications in days
 # Notifications older than this are deleted. On very active forums its better to keep this smaller.
 # Notifications older than this are deleted. On very active forums its better to keep this smaller.
 
 
 MISAGO_NOTIFICATIONS_MAX_AGE = 40
 MISAGO_NOTIFICATIONS_MAX_AGE = 40
 
 
-
 # Fail-safe limits in case forum is raided by spambot
 # Fail-safe limits in case forum is raided by spambot
 # No user may exceed those limits, however you may disable them by changing them to 0.
 # No user may exceed those limits, however you may disable them by changing them to 0.
 
 
 MISAGO_DIALY_POST_LIMIT = 600
 MISAGO_DIALY_POST_LIMIT = 600
 MISAGO_HOURLY_POST_LIMIT = 100
 MISAGO_HOURLY_POST_LIMIT = 100
 
 
-
 # Function used for generating individual avatar for user
 # Function used for generating individual avatar for user
 
 
 MISAGO_DYNAMIC_AVATAR_DRAWER = 'misago.users.avatars.dynamic.draw_default'
 MISAGO_DYNAMIC_AVATAR_DRAWER = 'misago.users.avatars.dynamic.draw_default'
 
 
-
 # Path to directory containing avatar galleries
 # Path to directory containing avatar galleries
 # Those galleries can be loaded by running loadavatargallery command
 # Those galleries can be loaded by running loadavatargallery command
 
 
 MISAGO_AVATAR_GALLERY = None
 MISAGO_AVATAR_GALLERY = None
 
 
-
 # Save user avatars for sizes
 # Save user avatars for sizes
 # Keep sizes ordered from greatest to smallest
 # Keep sizes ordered from greatest to smallest
 # Max size also controls min size of uploaded image as well as crop size
 # Max size also controls min size of uploaded image as well as crop size
 
 
 MISAGO_AVATARS_SIZES = [400, 200, 150, 100, 64, 50, 30]
 MISAGO_AVATARS_SIZES = [400, 200, 150, 100, 64, 50, 30]
 
 
-
 # Path to blank avatar image used for guests and removed users.
 # Path to blank avatar image used for guests and removed users.
 
 
 MISAGO_BLANK_AVATAR = 'blank-avatar.png'
 MISAGO_BLANK_AVATAR = 'blank-avatar.png'
 
 
-
 # Threads lists pagination settings
 # Threads lists pagination settings
 
 
 MISAGO_THREADS_PER_PAGE = 25
 MISAGO_THREADS_PER_PAGE = 25
 MISAGO_THREADS_TAIL = 15
 MISAGO_THREADS_TAIL = 15
 
 
-
 # Posts lists pagination settings
 # Posts lists pagination settings
 
 
 MISAGO_POSTS_PER_PAGE = 18
 MISAGO_POSTS_PER_PAGE = 18
 MISAGO_POSTS_TAIL = 6
 MISAGO_POSTS_TAIL = 6
 
 
-
 # Number of events displayed on single thread page
 # Number of events displayed on single thread page
 # If there's more events than specified, oldest events will be trimmed
 # If there's more events than specified, oldest events will be trimmed
 
 
 MISAGO_EVENTS_PER_PAGE = 20
 MISAGO_EVENTS_PER_PAGE = 20
 
 
-
 # Number of attachments possible to assign to single post
 # Number of attachments possible to assign to single post
 
 
 MISAGO_POST_ATTACHMENTS_LIMIT = 16
 MISAGO_POST_ATTACHMENTS_LIMIT = 16
 
 
-
 # Max allowed size of image before Misago will generate thumbnail for it
 # Max allowed size of image before Misago will generate thumbnail for it
 
 
 MISAGO_ATTACHMENT_IMAGE_SIZE_LIMIT = (500, 500)
 MISAGO_ATTACHMENT_IMAGE_SIZE_LIMIT = (500, 500)
 
 
-
 # Length of secret used for attachments url tokens and filenames
 # Length of secret used for attachments url tokens and filenames
 
 
 MISAGO_ATTACHMENT_SECRET_LENGTH = 64
 MISAGO_ATTACHMENT_SECRET_LENGTH = 64
@@ -204,14 +178,12 @@ MISAGO_ATTACHMENT_SECRET_LENGTH = 64
 
 
 MISAGO_ATTACHMENT_ORPHANED_EXPIRE = 24 * 60
 MISAGO_ATTACHMENT_ORPHANED_EXPIRE = 24 * 60
 
 
-
 # Names of files served when user requests file that doesn't exist or is unavailable
 # Names of files served when user requests file that doesn't exist or is unavailable
 # Those files will be sought within STATIC_ROOT directory
 # Those files will be sought within STATIC_ROOT directory
 
 
 MISAGO_404_IMAGE = 'misago/img/error-404.png'
 MISAGO_404_IMAGE = 'misago/img/error-404.png'
 MISAGO_403_IMAGE = 'misago/img/error-403.png'
 MISAGO_403_IMAGE = 'misago/img/error-403.png'
 
 
-
 # Controls max age in days of items that Misago has to process to make rankings
 # Controls max age in days of items that Misago has to process to make rankings
 # Used for active posters and most liked users lists
 # Used for active posters and most liked users lists
 # If your forum runs out of memory when trying to generate users rankings list
 # If your forum runs out of memory when trying to generate users rankings list
@@ -224,12 +196,10 @@ MISAGO_RANKING_LENGTH = 30
 
 
 MISAGO_RANKING_SIZE = 50
 MISAGO_RANKING_SIZE = 50
 
 
-
 # Controls number of users displayed on single page
 # Controls number of users displayed on single page
 
 
 MISAGO_USERS_PER_PAGE = 12
 MISAGO_USERS_PER_PAGE = 12
 
 
-
 # Controls amount of data used by readtracking system
 # Controls amount of data used by readtracking system
 # Items older than number of days specified below are considered read
 # Items older than number of days specified below are considered read
 # Depending on amount of new content being posted on your forum you may want
 # Depending on amount of new content being posted on your forum you may want
@@ -237,12 +207,14 @@ MISAGO_USERS_PER_PAGE = 12
 
 
 MISAGO_READTRACKER_CUTOFF = 40
 MISAGO_READTRACKER_CUTOFF = 40
 
 
-
 # Available Moment.js locales
 # Available Moment.js locales
 
 
 MISAGO_MOMENT_JS_LOCALES = [
 MISAGO_MOMENT_JS_LOCALES = [
     'af',
     'af',
-    'ar-ma', 'ar-sa', 'ar-tn', 'ar',
+    'ar-ma',
+    'ar-sa',
+    'ar-tn',
+    'ar',
     'az',
     'az',
     'be',
     'be',
     'bg',
     'bg',
@@ -255,9 +227,12 @@ MISAGO_MOMENT_JS_LOCALES = [
     'cv',
     'cv',
     'cy',
     'cy',
     'da',
     'da',
-    'de-at', 'de',
+    'de-at',
+    'de',
     'el',
     'el',
-    'en-au', 'en-ca', 'en-gb',
+    'en-au',
+    'en-ca',
+    'en-gb',
     'eo',
     'eo',
     'es',
     'es',
     'et',
     'et',
@@ -272,7 +247,8 @@ MISAGO_MOMENT_JS_LOCALES = [
     'he',
     'he',
     'hi',
     'hi',
     'hr',
     'hr',
-    'hu', 'hy-am',
+    'hu',
+    'hy-am',
     'id',
     'id',
     'is',
     'is',
     'it',
     'it',
@@ -286,27 +262,32 @@ MISAGO_MOMENT_JS_LOCALES = [
     'mk',
     'mk',
     'ml',
     'ml',
     'mr',
     'mr',
-    'ms-my', 'my',
+    'ms-my',
+    'my',
     'nb',
     'nb',
     'ne',
     'ne',
     'nl',
     'nl',
     'nn',
     'nn',
     'pl',
     'pl',
-    'pt-br', 'pt',
+    'pt-br',
+    'pt',
     'ro',
     'ro',
     'ru',
     'ru',
     'sk',
     'sk',
     'sl',
     'sl',
     'sq',
     'sq',
-    'sr-cyrl', 'sr',
+    'sr-cyrl',
+    'sr',
     'sv',
     'sv',
     'ta',
     'ta',
     'th',
     'th',
     'tl-ph',
     'tl-ph',
     'tr',
     'tr',
-    'tzm-latn', 'tzm',
+    'tzm-latn',
+    'tzm',
     'uk',
     'uk',
     'uz',
     'uz',
     'vi',
     'vi',
-    'zh-cn', 'zh-tw',
+    'zh-cn',
+    'zh-tw',
 ]
 ]

+ 10 - 20
misago/conf/forms.py

@@ -19,16 +19,16 @@ class ValidateChoicesNum(object):
         if self.min_choices and self.min_choices > data_len:
         if self.min_choices and self.min_choices > data_len:
             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.',
-                self.min_choices)
+                'You have to select at least %(choices)d options.', self.min_choices
+            )
             message = message % {'choices': self.min_choices}
             message = message % {'choices': self.min_choices}
             raise forms.ValidationError(message)
             raise forms.ValidationError(message)
 
 
         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.',
-                self.max_choices)
+                'You cannot select more than %(choices)d options.', self.max_choices
+            )
             message = message % {'choices': self.max_choices}
             message = message % {'choices': self.max_choices}
             raise forms.ValidationError(message)
             raise forms.ValidationError(message)
 
 
@@ -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,12 +128,9 @@ 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
 
 
@@ -144,6 +139,7 @@ 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
 
 
@@ -155,10 +151,7 @@ def ChangeSettingsForm(data=None, group=None):
     for setting in group.setting_set.order_by('order'):
     for setting in group.setting_set.order_by('order'):
         if setting.legend and setting.legend != fieldset_legend:
         if setting.legend and setting.legend != fieldset_legend:
             if fieldset_fields:
             if fieldset_fields:
-                fieldsets.append({
-                    'legend': fieldset_legend,
-                    'form': fieldset_form(data)
-                })
+                fieldsets.append({'legend': fieldset_legend, 'form': fieldset_form(data)})
             fieldset_legend = setting.legend
             fieldset_legend = setting.legend
             fieldset_form = FormType
             fieldset_form = FormType
             fieldset_fields = False
             fieldset_fields = False
@@ -166,9 +159,6 @@ def ChangeSettingsForm(data=None, group=None):
         fieldset_form = setting_field(fieldset_form, setting)
         fieldset_form = setting_field(fieldset_form, setting)
 
 
     if fieldset_fields:
     if fieldset_fields:
-        fieldsets.append({
-            'legend': fieldset_legend,
-            'form': fieldset_form(data)
-        })
+        fieldsets.append({'legend': fieldset_legend, '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 {}
 
 

+ 3 - 9
misago/conf/tests/test_admin_views.py

@@ -17,9 +17,7 @@ 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)
 
 
@@ -27,9 +25,7 @@ class AdminSettingsViewsTests(AdminTestCase):
         """
         """
         invalid group results in redirect to settings list
         invalid group results in redirect to settings list
         """
         """
-        group_link = reverse('misago:admin:system:settings:group', kwargs={
-            'key': 'invalid-group'
-        })
+        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'])
@@ -39,9 +35,7 @@ class AdminSettingsViewsTests(AdminTestCase):
         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)

+ 48 - 51
misago/conf/tests/test_migrationutils.py

@@ -9,28 +9,28 @@ from misago.core import threadstore
 class DBConfMigrationUtilsTests(TestCase):
 class DBConfMigrationUtilsTests(TestCase):
     def setUp(self):
     def setUp(self):
         self.test_group = {
         self.test_group = {
-            'key': 'test_group',
-            'name': "Test settings",
-            'description': "Those are test settings.",
-            'settings': (
-                {
-                    'setting': 'fish_name',
-                    'name': "Fish's name",
-                    'value': "Eric",
-                    'field_extra': {
-                           'min_length': 2,
-                           'max_length': 255
-                        },
+            'key':
+                'test_group',
+            'name':
+                "Test settings",
+            'description':
+                "Those are test settings.",
+            'settings': ({
+                'setting': 'fish_name',
+                'name': "Fish's name",
+                'value': "Eric",
+                'field_extra': {
+                    'min_length': 2,
+                    'max_length': 255
                 },
                 },
-                {
-                    'setting': 'fish_license_no',
-                    'name': "Fish's license number",
-                    'default_value': '123-456',
-                    'field_extra': {
-                            'max_length': 255
-                        },
+            }, {
+                'setting': 'fish_license_no',
+                'name': "Fish's license number",
+                'default_value': '123-456',
+                'field_extra': {
+                    '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)
@@ -60,40 +58,39 @@ class DBConfMigrationUtilsTests(TestCase):
         """migrate_settings_group changed group key"""
         """migrate_settings_group changed group key"""
 
 
         new_group = {
         new_group = {
-            'key': 'new_test_group',
-            'name': "New test settings",
-            'description': "Those are updated test settings.",
-            'settings': (
-                {
-                    'setting': 'fish_new_name',
-                    'name': "Fish's new name",
-                    'value': "Eric",
-                    'field_extra': {
-                            'min_length': 2,
-                            'max_length': 255
-                        },
+            'key':
+                'new_test_group',
+            'name':
+                "New test settings",
+            'description':
+                "Those are updated test settings.",
+            'settings': ({
+                'setting': 'fish_new_name',
+                'name': "Fish's new name",
+                'value': "Eric",
+                'field_extra': {
+                    'min_length': 2,
+                    'max_length': 255
                 },
                 },
-                {
-                    'setting': 'fish_new_license_no',
-                    'name': "Fish's changed license number",
-                    'default_value': '123-456',
-                    'field_extra': {
-                            'max_length': 255
-                        },
+            }, {
+                'setting': 'fish_new_license_no',
+                'name': "Fish's changed license number",
+                'default_value': '123-456',
+                'field_extra': {
+                    'max_length': 255
                 },
                 },
-            )
+            }, )
         }
         }
 
 
-        migrationutils.migrate_settings_group(
-            apps, new_group, old_group_key=self.test_group['key'])
+        migrationutils.migrate_settings_group(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'])

+ 11 - 18
misago/conf/tests/test_models.py

@@ -9,27 +9,20 @@ class SettingModelTests(TestCase):
         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)

+ 57 - 63
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
@@ -43,31 +41,31 @@ class GatewaySettingsTests(TestCase):
     def test_setting_public(self):
     def test_setting_public(self):
         """get_public_settings returns public settings"""
         """get_public_settings returns public settings"""
         test_group = {
         test_group = {
-            'key': 'test_group',
-            'name': "Test settings",
-            'description': "Those are test settings.",
-            'settings': (
-                {
-                    'setting': 'fish_name',
-                    'name': "Fish's name",
-                    'value': "Public Eric",
-                    'field_extra': {
-                            'min_length': 2,
-                            'max_length': 255
-                        },
-                    'is_public': True
+            'key':
+                'test_group',
+            'name':
+                "Test settings",
+            'description':
+                "Those are test settings.",
+            'settings': ({
+                'setting': 'fish_name',
+                'name': "Fish's name",
+                'value': "Public Eric",
+                'field_extra': {
+                    'min_length': 2,
+                    'max_length': 255
                 },
                 },
-                {
-                    'setting': 'private_fish_name',
-                    'name': "Fish's name",
-                    'value': "Private Eric",
-                    'field_extra': {
-                            'min_length': 2,
-                            'max_length': 255
-                        },
-                    'is_public': False
+                'is_public': True
+            }, {
+                'setting': 'private_fish_name',
+                'name': "Fish's name",
+                'value': "Private Eric",
+                'field_extra': {
+                    'min_length': 2,
+                    'max_length': 255
                 },
                 },
-            )
+                'is_public': False
+            }, )
         }
         }
 
 
         migrate_settings_group(apps, test_group)
         migrate_settings_group(apps, test_group)
@@ -79,44 +77,42 @@ 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',
-            'name': "Test settings",
-            'description': "Those are test settings.",
-            'settings': (
-                {
-                    'setting': 'fish_name',
-                    'name': "Fish's name",
-                    'value': "Greedy Eric",
-                    'field_extra': {
-                            'min_length': 2,
-                            'max_length': 255
-                        },
-                    'is_lazy': False
+            'key':
+                'test_group',
+            'name':
+                "Test settings",
+            'description':
+                "Those are test settings.",
+            'settings': ({
+                'setting': 'fish_name',
+                'name': "Fish's name",
+                'value': "Greedy Eric",
+                'field_extra': {
+                    'min_length': 2,
+                    'max_length': 255
                 },
                 },
-                {
-                    'setting': 'lazy_fish_name',
-                    'name': "Fish's name",
-                    'value': "Lazy Eric",
-                    'field_extra': {
-                            'min_length': 2,
-                            'max_length': 255
-                        },
-                    'is_lazy': True
+                'is_lazy': False
+            }, {
+                'setting': 'lazy_fish_name',
+                'name': "Fish's name",
+                'value': "Lazy Eric",
+                'field_extra': {
+                    'min_length': 2,
+                    'max_length': 255
                 },
                 },
-                {
-                    'setting': 'lazy_empty_setting',
-                    'name': "Fish's name",
-                    'field_extra': {
-                            'min_length': 2,
-                            'max_length': 255
-                        },
-                    'is_lazy': True
+                'is_lazy': True
+            }, {
+                'setting': 'lazy_empty_setting',
+                'name': "Fish's name",
+                'field_extra': {
+                    'min_length': 2,
+                    'max_length': 255
                 },
                 },
-            )
+                'is_lazy': True
+            }, )
         }
         }
 
 
         migrate_settings_group(apps, test_group)
         migrate_settings_group(apps, test_group)
@@ -125,11 +121,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,
+        }
+    )

+ 2 - 9
misago/core/__init__.py

@@ -1,11 +1,7 @@
 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 = (
-    'django.db.backends.postgresql',
-    'django.db.backends.postgresql_psycopg2',
-)
+SUPPORTED_ENGINES = ('django.db.backends.postgresql', 'django.db.backends.postgresql_psycopg2', )
 
 
 
 
 @register()
 @register()
@@ -16,10 +12,7 @@ def check_db_engine(app_configs, **kwargs):
         if settings.DATABASES['default']['ENGINE'] not in SUPPORTED_ENGINES:
         if settings.DATABASES['default']['ENGINE'] not in SUPPORTED_ENGINES:
             raise ValueError()
             raise ValueError()
     except (AttributeError, KeyError, ValueError):
     except (AttributeError, KeyError, ValueError):
-        errors.append(Critical(
-            msg='Misago requires PostgreSQL database.',
-            id='misago.001'
-        ))
+        errors.append(Critical(msg='Misago requires PostgreSQL database.', id='misago.001'))
 
 
     return errors
     return errors
 
 

+ 1 - 3
misago/core/apipatch.py

@@ -39,9 +39,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({
-                'detail': "PATCH request should be list of operations"
-            }, status=400)
+            return Response({'detail': "PATCH request should be list of operations"}, status=400)
 
 
         detail = []
         detail = []
         is_errored = False
         is_errored = False

+ 2 - 4
misago/core/apirouter.py

@@ -9,10 +9,8 @@ class MisagoApiRouter(DefaultRouter):
         # List route.
         # List route.
         Route(
         Route(
             url=r'^{prefix}{trailing_slash}$',
             url=r'^{prefix}{trailing_slash}$',
-            mapping={
-                'get': 'list',
-                'post': 'create'
-            },
+            mapping={'get': 'list',
+                     'post': 'create'},
             name='{basename}-list',
             name='{basename}-list',
             initkwargs={'suffix': 'List'}
             initkwargs={'suffix': 'List'}
         ),
         ),

+ 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):

+ 2 - 6
misago/core/context_processors.py

@@ -32,17 +32,13 @@ 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 {
-        'MOMENTJS_LOCALE_URL': get_locale_url(get_language())
-    }
+    return {'MOMENTJS_LOCALE_URL': get_locale_url(get_language())}
 
 
 
 
 def frontend_context(request):
 def frontend_context(request):

+ 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

+ 5 - 9
misago/core/errorpages.py

@@ -13,13 +13,9 @@ 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({
-        'CURRENT_LINK': 'misago:error-%s' % code
-    })
+    request.frontend_context.update({'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):
@@ -28,9 +24,7 @@ def banned(request, ban):
         '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 +62,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 +72,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

+ 7 - 10
misago/core/exceptionhandler.py

@@ -61,14 +61,12 @@ def handle_permission_denied_exception(request, exception):
     return errorpages.permission_denied(request, error_message)
     return errorpages.permission_denied(request, error_message)
 
 
 
 
-EXCEPTION_HANDLERS = (
-    (AjaxError, handle_ajax_error),
-    (Banned, handle_banned_exception),
-    (Http404, handle_http404_exception),
-    (ExplicitFirstPage, handle_explicit_first_page_exception),
-    (OutdatedSlug, handle_outdated_slug_exception),
-    (PermissionDenied, handle_permission_denied_exception),
-)
+EXCEPTION_HANDLERS = ((AjaxError, handle_ajax_error),
+                      (Banned, handle_banned_exception),
+                      (Http404, handle_http404_exception),
+                      (ExplicitFirstPage, handle_explicit_first_page_exception),
+                      (OutdatedSlug, handle_outdated_slug_exception),
+                      (PermissionDenied, handle_permission_denied_exception), )
 
 
 
 
 def get_exception_handler(exception):
 def get_exception_handler(exception):
@@ -76,8 +74,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):

+ 1 - 0
misago/core/exceptions.py

@@ -3,6 +3,7 @@ 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

+ 4 - 2
misago/core/forms.py

@@ -45,6 +45,8 @@ 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

+ 3 - 3
misago/core/management/commands/testemailsetup.py

@@ -24,7 +24,7 @@ class Command(BaseCommand):
             'Test Message',
             'Test Message',
             ("This message was sent to test if your "
             ("This message was sent to test if your "
              "site e-mail is configured correctly."),
              "site e-mail is configured correctly."),
-            settings.DEFAULT_FROM_EMAIL,
-            [email],
-            fail_silently=False)
+            settings.DEFAULT_FROM_EMAIL, [email],
+            fail_silently=False
+        )
         self.stdout.write("Test message was sent to %s" % email)
         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, ),
         ),
         ),
     ]
     ]

+ 30 - 26
misago/core/migrations/0002_basic_settings.py

@@ -10,13 +10,18 @@ _ = 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': (
-            {
+    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',
                 'setting': 'forum_name',
                 'name': _("Forum name"),
                 'name': _("Forum name"),
                 'legend': _("General"),
                 'legend': _("General"),
@@ -26,8 +31,7 @@ def create_basic_settings_group(apps, schema_editor):
                     'max_length': 255
                     'max_length': 255
                 },
                 },
                 'is_public': True,
                 'is_public': True,
-            },
-            {
+            }, {
                 'setting': 'forum_index_title',
                 'setting': 'forum_index_title',
                 'name': _("Index title"),
                 'name': _("Index title"),
                 'description': _("You may set custon title on "
                 'description': _("You may set custon title on "
@@ -37,8 +41,7 @@ def create_basic_settings_group(apps, schema_editor):
                     'max_length': 255
                     'max_length': 255
                 },
                 },
                 'is_public': True,
                 'is_public': True,
-            },
-            {
+            }, {
                 'setting': 'forum_index_meta_description',
                 'setting': 'forum_index_meta_description',
                 'name': _("Meta Description"),
                 'name': _("Meta Description"),
                 'description': _("Short description of your forum "
                 'description': _("Short description of your forum "
@@ -46,8 +49,7 @@ def create_basic_settings_group(apps, schema_editor):
                 'field_extra': {
                 'field_extra': {
                     'max_length': 255
                     'max_length': 255
                 },
                 },
-            },
-            {
+            }, {
                 'setting': 'forum_branding_display',
                 'setting': 'forum_branding_display',
                 'name': _("Display branding"),
                 'name': _("Display branding"),
                 'description': _("Switch branding in forum's navbar."),
                 'description': _("Switch branding in forum's navbar."),
@@ -56,8 +58,7 @@ def create_basic_settings_group(apps, schema_editor):
                 'python_type': 'bool',
                 'python_type': 'bool',
                 'form_field': 'yesno',
                 'form_field': 'yesno',
                 'is_public': True,
                 'is_public': True,
-            },
-            {
+            }, {
                 'setting': 'forum_branding_text',
                 'setting': 'forum_branding_text',
                 'name': _("Branding text"),
                 'name': _("Branding text"),
                 'description': _("Optional text displayed besides "
                 'description': _("Optional text displayed besides "
@@ -67,20 +68,23 @@ def create_basic_settings_group(apps, schema_editor):
                     'max_length': 255
                     'max_length': 255
                 },
                 },
                 '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"),
+            }, {
+                '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': {
                 'field_extra': {
                     'max_length': 255
                     'max_length': 255
                 },
                 },
-            },
-        )
-    })
+            }, )
+        }
+    )
 
 
 
 
 class Migration(migrations.Migration):
 class Migration(migrations.Migration):

+ 12 - 12
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,17 @@ 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)
+                message = (
+                    "%s page hierarchy is invalid or too complex  to "
+                    "resolve. Sections left: %s" % self._unsorted_list
+                )
                 raise ValueError(message)
                 raise ValueError(message)
 
 
             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)
 
 
@@ -66,11 +67,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 +111,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

+ 5 - 10
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"
@@ -85,8 +82,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 +95,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

+ 3 - 9
misago/core/serializers.py

@@ -9,9 +9,7 @@ class MutableFields(object):
 
 
         Meta.fields = tuple(fields)
         Meta.fields = tuple(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):
@@ -28,9 +26,7 @@ class MutableFields(object):
 
 
         Meta.fields = tuple(final_fields)
         Meta.fields = tuple(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):
@@ -47,6 +43,4 @@ class MutableFields(object):
 
 
         Meta.fields = tuple(final_fields)
         Meta.fields = tuple(final_fields)
 
 
-        return type(name, (cls,), {
-            'Meta': Meta
-        })
+        return type(name, (cls, ), {'Meta': Meta})

+ 9 - 5
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
 
 
@@ -43,7 +45,9 @@ def start_misago_project():
 
 
     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)

+ 12 - 9
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,8 +26,8 @@ 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)
+            object_list, per_page, orphans=orphans, allow_empty_first_page=allow_empty_first_page
+        ).page(page)
     except (EmptyPage, InvalidPage):
     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)
 
 

+ 16 - 12
misago/core/templatetags/misago_forms.py

@@ -5,8 +5,6 @@ from django.template.loader import render_to_string
 
 
 
 
 register = template.Library()
 register = template.Library()
-
-
 """
 """
 Form row: renders single row in form
 Form row: renders single row in form
 
 
@@ -14,18 +12,20 @@ Syntax:
 {% form_row form.field %} # renders vertical field
 {% form_row form.field %} # renders vertical field
 {% form_row form.field "col-md-3" "col-md-9" %} # renders horizontal 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):
     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,22 @@ 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 ''
-        })
+        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
 Form input: renders given field input
 """
 """
+
+
 @register.tag
 @register.tag
 def form_input(parser, token):
 def form_input(parser, token):
     return crispy_forms_field.crispy_field(parser, token)
     return crispy_forms_field.crispy_field(parser, token)

+ 1 - 3
misago/core/testproject/serializers.py

@@ -5,9 +5,7 @@ class MockSerializer(serializers.Serializer):
     id = serializers.SerializerMethodField()
     id = serializers.SerializerMethodField()
 
 
     class Meta:
     class Meta:
-        fields = (
-            'id',
-        )
+        fields = ('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'),

+ 5 - 19
misago/core/testproject/views.py

@@ -20,19 +20,13 @@ 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!")
 
 
@@ -72,11 +66,7 @@ def test_paginated_response_data_serializer(request):
     data = [0, 1, 2, 3]
     data = [0, 1, 2, 3]
     page = paginate(data, 0, 10)
     page = paginate(data, 0, 10)
 
 
-    return paginated_response(
-        page,
-        data=['a', 'b', 'c', 'd'],
-        serializer=MockSerializer
-    )
+    return paginated_response(page, data=['a', 'b', 'c', 'd'], serializer=MockSerializer)
 
 
 
 
 @api_view()
 @api_view()
@@ -85,12 +75,8 @@ def test_paginated_response_data_extra(request):
     page = paginate(data, 0, 10)
     page = paginate(data, 0, 10)
 
 
     return paginated_response(
     return paginated_response(
-        page,
-        data=['a', 'b', 'c', 'd'],
-        extra={
-            'next': 'EXTRA',
-            'lorem': 'ipsum'
-        }
+        page, data=['a', 'b', 'c', 'd'], extra={'next': 'EXTRA',
+                                                'lorem': 'ipsum'}
     )
     )
 
 
 
 

+ 122 - 50
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,25 @@ 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:
@@ -116,12 +123,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 +140,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 +164,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 +209,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 +307,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 - 3
misago/core/tests/test_checks.py

@@ -6,9 +6,7 @@ from misago.core import SUPPORTED_ENGINES, check_db_engine
 
 
 
 
 INVALID_ENGINES = (
 INVALID_ENGINES = (
-    'django.db.backends.sqlite3',
-    'django.db.backends.mysql',
-    'django.db.backends.oracle',
+    'django.db.backends.sqlite3', 'django.db.backends.mysql', 'django.db.backends.oracle',
 )
 )
 
 
 
 

+ 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()))

+ 41 - 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,12 @@ 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': {
+        self.assertEqual(
+            context_processors.frontend_context(mock_request),
+            {'frontend_context': {
                 'someValue': 'Something'
                 '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), {})

+ 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"""

+ 7 - 13
misago/core/tests/test_exceptionhandlers.py

@@ -9,10 +9,7 @@ from misago.users.models import Ban
 
 
 
 
 INVALID_EXCEPTIONS = (
 INVALID_EXCEPTIONS = (
-    django_exceptions.ObjectDoesNotExist,
-    django_exceptions.ViewDoesNotExist,
-    TypeError,
-    ValueError,
+    django_exceptions.ObjectDoesNotExist, django_exceptions.ViewDoesNotExist, TypeError, ValueError,
     KeyError,
     KeyError,
 )
 )
 
 
@@ -37,8 +34,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 +55,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)
 
 

+ 5 - 24
misago/core/tests/test_momentjs.py

@@ -6,14 +6,9 @@ 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 = (
-            ('AF', 'af'),
-            ('ar-SA', 'ar-sa'),
-            ('de', 'de'),
-            ('de-NO', 'de'),
-            ('pl-pl', 'pl'),
-            ('zz', None),
-        )
+        TEST_CASES = (('AF', 'af'), ('ar-SA', 'ar-sa'), ('de', 'de'), ('de-NO', 'de'),
+                      ('pl-pl', 'pl'),
+                      ('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)
@@ -21,27 +16,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))

+ 4 - 10
misago/core/tests/test_page.py

@@ -9,19 +9,13 @@ class SiteTests(TestCase):
 
 
     def test_pages(self):
     def test_pages(self):
         """add_section adds section to page"""
         """add_section adds section to page"""
-        self.page.add_section(
-            link='misago:user-posts',
-            name='Posts',
-            after='misago:user-threads')
+        self.page.add_section(link='misago:user-posts', name='Posts', after='misago:user-threads')
 
 
-        self.page.add_section(
-            link='misago:user-threads',
-            name='Threads')
+        self.page.add_section(link='misago:user-threads', name='Threads')
 
 
         self.page.add_section(
         self.page.add_section(
-            link='misago:user-follows',
-            name='Follows',
-            before='misago:user-posts')
+            link='misago:user-follows', name='Follows', before='misago:user-posts'
+        )
 
 
         self.page.assert_is_finalized()
         self.page.assert_is_finalized()
 
 

+ 20 - 29
misago/core/tests/test_serializers.py

@@ -17,19 +17,18 @@ class MutableFieldsSerializerTests(TestCase):
         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)
 
 
@@ -46,11 +45,13 @@ class MutableFieldsSerializerTests(TestCase):
         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,7 +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',)
+        added_fields = ('category', )
 
 
         serializer = TestSerializer.extend_fields(*added_fields)
         serializer = TestSerializer.extend_fields(*added_fields)
 
 
@@ -73,17 +74,7 @@ class TestSerializer(serializers.ModelSerializer, MutableFields):
     class Meta:
     class Meta:
         model = Thread
         model = Thread
         fields = (
         fields = (
-            'id',
-            'title',
-            'replies',
-            'has_unapproved_posts',
-            'started_on',
-            'last_post_on',
-            'last_post_is_event',
-            'last_post',
-            'last_poster_name',
-            'is_unapproved',
-            'is_hidden',
-            'is_closed',
-            'weight',
+            'id', 'title', 'replies', 'has_unapproved_posts', 'started_on', 'last_post_on',
+            'last_post_is_event', 'last_post', 'last_poster_name', 'is_unapproved', 'is_hidden',
+            'is_closed', '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)
+        )

+ 115 - 104
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,29 +57,14 @@ 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 = (
-            ('0', 0),
-            ('123', 123),
-            ('000123', 123),
-            ('1', 1),
-        )
+        VALID_VALUES = (('0', 0), ('123', 123), ('000123', 123), ('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 = (
-            None,
-            '',
-            'bob',
-            '1bob',
-            'b0b',
-            'bob123',
-            '12.321',
-            '.4',
-            '5.',
-        )
+        INVALID_VALUES = (None, '', 'bob', '1bob', 'b0b', 'bob123', '12.321', '.4', '5.', )
 
 
         for value in INVALID_VALUES:
         for value in INVALID_VALUES:
             with self.assertRaises(Http404):
             with self.assertRaises(Http404):
@@ -92,94 +77,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'
+            }
+        )

+ 31 - 45
misago/core/tests/test_templatetags.py

@@ -45,12 +45,7 @@ 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 = (
-            ['l', 'o', 'r'],
-            ['e', 'm', 'i'],
-            ['p', 's', 'u'],
-            ['m'],
-        )
+        yields = (['l', 'o', 'r'], ['e', 'm', 'i'], ['p', 's', 'u'], ['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])
@@ -58,12 +53,7 @@ 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 = (
-            ['l', 'o', 'r'],
-            ['e', 'm', 'i'],
-            ['p', 's', 'u'],
-            ['m', None, None],
-        )
+        yields = (['l', 'o', 'r'], ['e', 'm', 'i'], ['p', 's', 'u'], ['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])
@@ -171,10 +161,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"""
@@ -185,10 +172,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"""
@@ -199,10 +183,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"""
@@ -213,10 +194,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):
@@ -229,9 +207,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):
@@ -244,9 +226,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"""
@@ -257,10 +237,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"""
@@ -271,9 +253,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"""
@@ -284,7 +268,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'
+        )

+ 85 - 102
misago/core/tests/test_utils.py

@@ -11,15 +11,9 @@ from misago.core.utils import (
     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,15 @@ 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),
-                '"%s" is overlapped by "%s"' % (path, misago_prefix))
+                is_request_to_misago(request), '"%s" is overlapped by "%s"' % (path, misago_prefix)
+            )
 
 
 
 
 class SlugifyTests(TestCase):
 class SlugifyTests(TestCase):
@@ -65,8 +60,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,15 +69,9 @@ 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 = (
-            ('Bob', 'bob'),
-            ('Eric The Fish', 'eric-the-fish'),
-            ('John   Snow', 'john-snow'),
-            ('J0n', 'j0n'),
-            ('An###ne', 'anne'),
-            ('S**t', 'st'),
-            ('Łók', 'lok'),
-        )
+        test_cases = (('Bob', 'bob'), ('Eric The Fish', 'eric-the-fish'),
+                      ('John   Snow', 'john-snow'), ('J0n', 'j0n'), ('An###ne', 'anne'),
+                      ('S**t', 'st'), ('Łók', 'lok'), )
 
 
         for original, slug in test_cases:
         for original, slug in test_cases:
             self.assertEqual(slugify(original), slug)
             self.assertEqual(slugify(original), slug)
@@ -93,10 +81,8 @@ 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.185085-01:00',
-            '2016-10-22T20:55:39-01:00',
-            '2016-10-22T20:55:39.185085+01:00',
+            '2016-10-22T20:55:39.185085Z', '2016-10-22T20:55:39.185085-01:00',
+            '2016-10-22T20:55:39-01:00', '2016-10-22T20:55:39.185085+01:00',
         )
         )
 
 
         for test_input in INPUTS:
         for test_input in INPUTS:
@@ -105,9 +91,7 @@ class ParseIso8601StringTests(TestCase):
     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-22T30:55:39.185085+11:00',
+            '', '2016-10-22', '2016-10-22T30:55:39.185085+11:00',
             '2016-10-22T20:55:39.18SSSSS5085Z',
             '2016-10-22T20:55:39.18SSSSS5085Z',
         )
         )
 
 
@@ -117,27 +101,11 @@ class ParseIso8601StringTests(TestCase):
 
 
 
 
 PLAINTEXT_FORMAT_CASES = (
 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>'
-    ),
-    (
+    ('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>'
     ),
     ),
@@ -171,88 +139,103 @@ 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"""

+ 1 - 0
misago/core/testutils.py

@@ -8,6 +8,7 @@ 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()

+ 8 - 4
misago/core/utils.py

@@ -37,10 +37,8 @@ def encode_json_html(string):
 """
 """
 Turn ISO 8601 string into datetime object
 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):
     value = force_text(value, strings_only=True).rstrip('Z')
     value = force_text(value, strings_only=True).rstrip('Z')
@@ -76,6 +74,8 @@ Mark request as having sensitive parameters
 We can't use decorator because of DRF uses custom HttpRequest
 We can't use decorator because of DRF uses custom HttpRequest
 that is incompatibile with Django's decorator
 that is incompatibile with Django's decorator
 """
 """
+
+
 def hide_post_parameters(request):
 def hide_post_parameters(request):
     request.sensitive_post_parameters = '__ALL__'
     request.sensitive_post_parameters = '__ALL__'
 
 
@@ -83,6 +83,8 @@ def hide_post_parameters(request):
 """
 """
 Return path utility
 Return path utility
 """
 """
+
+
 def clean_return_path(request):
 def clean_return_path(request):
     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)
@@ -125,6 +127,8 @@ def _get_return_path_from_referer(request):
 """
 """
 Utils for resolving requests destination
 Utils for resolving requests destination
 """
 """
+
+
 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')

+ 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 - 1
misago/core/views.py

@@ -6,7 +6,7 @@ from django.views.decorators.http import last_modified
 
 
 
 
 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):

+ 5 - 9
misago/datamover/attachments.py

@@ -13,11 +13,7 @@ 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):
@@ -38,13 +34,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']]

+ 3 - 3
misago/datamover/avatars.py

@@ -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)

+ 1 - 5
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():

+ 18 - 12
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)
 
 
@@ -69,10 +73,12 @@ 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/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()))

+ 4 - 14
misago/datamover/management/commands/runmigration.py

@@ -4,24 +4,14 @@ from misago.datamover.management.base import BaseCommand
 
 
 
 
 MOVE_COMMANDS = (
 MOVE_COMMANDS = (
-    'movesettings',
-    'moveusers',
-    'movecategories',
-    'movethreads',
-    'buildmovesindex',
-    'synchronizethreads',
-    'synchronizecategories',
-    'rebuildpostssearch',
-    'invalidatebans',
-    'populateonlinetracker',
-    'synchronizeusers',
+    'movesettings', 'moveusers', 'movecategories', 'movethreads', 'buildmovesindex',
+    'synchronizethreads', 'synchronizecategories', 'rebuildpostssearch', 'invalidatebans',
+    'populateonlinetracker', '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...")

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

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

+ 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(

+ 75 - 44
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
 
 
 
 
@@ -32,50 +34,79 @@ def convert_allow_custom_avatars(old_value):
 
 
 
 
 SETTING_CONVERTER = {
 SETTING_CONVERTER = {
-    'board_name': copy_value('forum_name'),
-    'board_index_title': copy_value('forum_index_title'),
-    'board_index_meta': copy_value('forum_index_meta_description'),
-    'board_header': copy_value('forum_branding_text'),
-    'email_footnote_plain': copy_value('email_footer'),
-    'tos_title': copy_value('terms_of_service_title'),
-    'tos_url': copy_value('terms_of_service_link'),
-    'tos_content': copy_value('terms_of_service'),
-    'board_credits': copy_value('forum_footnote'),
-    'thread_name_min': copy_value('thread_title_length_min'),
-    'thread_name_max': copy_value('thread_title_length_max'),
-    'post_length_min': copy_value('post_length_min'),
-    'account_activation': map_value('account_activation', {
-        'none': 'none',
-        'user': 'user',
-        'admin': 'admin',
-        'block': 'closed',
-    }),
-    'username_length_min': copy_value('username_length_min'),
-    'username_length_max': copy_value('username_length_max'),
-    'password_length': copy_value('password_length_min'),
-    'avatars_types': convert_allow_custom_avatars,
-    'default_avatar': copy_value('default_avatar'),
-    'upload_limit': copy_value('avatar_upload_limit'),
-    'subscribe_start': map_value('subscribe_start', {
-        '0': 'no',
-        '1': 'watch',
-        '2': 'watch_email',
-    }),
-    'subscribe_reply': map_value('subscribe_reply', {
-        '0': 'no',
-        '1': 'watch',
-        '2': 'watch_email',
-    }),
-    'bots_registration': map_value('captcha_type', {
-        'no': 'no',
-        'recaptcha': 're',
-        'qa': 'qa',
-    }),
-    'recaptcha_public': copy_value('recaptcha_site_key'),
-    'recaptcha_private': copy_value('recaptcha_secret_key'),
-    'qa_test': copy_value('qa_question'),
-    'qa_test_help': copy_value('qa_help_text'),
-    'qa_test_answers': copy_value('qa_answers'),
+    'board_name':
+        copy_value('forum_name'),
+    'board_index_title':
+        copy_value('forum_index_title'),
+    'board_index_meta':
+        copy_value('forum_index_meta_description'),
+    'board_header':
+        copy_value('forum_branding_text'),
+    'email_footnote_plain':
+        copy_value('email_footer'),
+    'tos_title':
+        copy_value('terms_of_service_title'),
+    'tos_url':
+        copy_value('terms_of_service_link'),
+    'tos_content':
+        copy_value('terms_of_service'),
+    'board_credits':
+        copy_value('forum_footnote'),
+    'thread_name_min':
+        copy_value('thread_title_length_min'),
+    'thread_name_max':
+        copy_value('thread_title_length_max'),
+    'post_length_min':
+        copy_value('post_length_min'),
+    'account_activation':
+        map_value(
+            'account_activation', {
+                'none': 'none',
+                'user': 'user',
+                'admin': 'admin',
+                'block': 'closed',
+            }
+        ),
+    'username_length_min':
+        copy_value('username_length_min'),
+    'username_length_max':
+        copy_value('username_length_max'),
+    'password_length':
+        copy_value('password_length_min'),
+    'avatars_types':
+        convert_allow_custom_avatars,
+    'default_avatar':
+        copy_value('default_avatar'),
+    'upload_limit':
+        copy_value('avatar_upload_limit'),
+    'subscribe_start':
+        map_value('subscribe_start', {
+            '0': 'no',
+            '1': 'watch',
+            '2': 'watch_email',
+        }),
+    'subscribe_reply':
+        map_value('subscribe_reply', {
+            '0': 'no',
+            '1': 'watch',
+            '2': 'watch_email',
+        }),
+    'bots_registration':
+        map_value('captcha_type', {
+            'no': 'no',
+            'recaptcha': 're',
+            'qa': 'qa',
+        }),
+    'recaptcha_public':
+        copy_value('recaptcha_site_key'),
+    'recaptcha_private':
+        copy_value('recaptcha_secret_key'),
+    'qa_test':
+        copy_value('qa_question'),
+    'qa_test_help':
+        copy_value('qa_help_text'),
+    'qa_test_answers':
+        copy_value('qa_answers'),
 }
 }
 
 
 
 

+ 21 - 32
misago/datamover/threads.py

@@ -17,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':
@@ -77,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
@@ -160,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)
@@ -215,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'])
 
 
 
 
@@ -232,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()
 
 
@@ -256,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():

+ 179 - 47
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),
@@ -32,67 +44,187 @@ urlpatterns += [
     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+)/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+)/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+)/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+)/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+)/(?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+)/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+)/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 - 21
misago/datamover/users.py

@@ -12,13 +12,7 @@ from . import fetch_assoc, localise_datetime, movedids
 
 
 UserModel = get_user_model()
 UserModel = get_user_model()
 
 
-
-PRIVATE_THREAD_INVITES = {
-    0: 0,
-    1: 0,
-    2: 1,
-    3: 2
-}
+PRIVATE_THREAD_INVITES = {0: 0, 1: 0, 2: 1, 3: 2}
 
 
 
 
 def move_users(stdout, style):
 def move_users(stdout, style):
@@ -31,15 +25,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 +57,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 +119,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)

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

@@ -15,19 +15,11 @@ class Command(BaseCommand):
 
 
     def add_arguments(self, parser):
     def add_arguments(self, parser):
         parser.add_argument(
         parser.add_argument(
-            'categories',
-            help="number of categories to create",
-            nargs='?',
-            type=int,
-            default=5
+            'categories', help="number of categories to create", nargs='?', type=int, default=5
         )
         )
 
 
         parser.add_argument(
         parser.add_argument(
-            'minlevel',
-            help="min. level of created categories",
-            nargs='?',
-            type=int,
-            default=0
+            'minlevel', help="min. level of created categories", nargs='?', type=int, default=0
         )
         )
 
 
     def handle(self, *args, **options):
     def handle(self, *args, **options):
@@ -65,25 +57,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('?')

+ 2 - 9
misago/faker/management/commands/createfakethreads.py

@@ -19,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)
 
 
@@ -32,11 +30,7 @@ class Command(BaseCommand):
 
 
     def add_arguments(self, parser):
     def add_arguments(self, parser):
         parser.add_argument(
         parser.add_argument(
-            'threads',
-            help="number of threads to create",
-            nargs='?',
-            type=int,
-            default=5
+            'threads', help="number of threads to create", nargs='?', type=int, default=5
         )
         )
 
 
     def handle(self, *args, **options):
     def handle(self, *args, **options):
@@ -160,8 +154,7 @@ 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)

+ 4 - 9
misago/faker/management/commands/createfakeusers.py

@@ -21,11 +21,7 @@ class Command(BaseCommand):
 
 
     def add_arguments(self, parser):
     def add_arguments(self, parser):
         parser.add_argument(
         parser.add_argument(
-            'users',
-            help="number of users to create",
-            nargs='?',
-            type=int,
-            default=5
+            'users', help="number of users to create", nargs='?', type=int, default=5
         )
         )
 
 
     def handle(self, *args, **options):
     def handle(self, *args, **options):
@@ -51,8 +47,8 @@ class Command(BaseCommand):
                 }
                 }
 
 
                 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, **kwargs
+                )
 
 
                 if random.randint(0, 100) > 90:
                 if random.randint(0, 100) > 90:
                     dynamic.set_avatar(user)
                     dynamic.set_avatar(user)
@@ -63,8 +59,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

+ 79 - 54
misago/legal/migrations/0001_initial.py

@@ -10,13 +10,16 @@ _ = 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': (
-            {
+    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',
                 'setting': 'terms_of_service_title',
                 'name': _("Terms title"),
                 'name': _("Terms title"),
                 'legend': _("Terms of Service"),
                 'legend': _("Terms of Service"),
@@ -28,37 +31,48 @@ def create_legal_settings_group(apps, schema_editor):
                     'required': False,
                     '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': "",
+            }, {
+                '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': {
                 'field_extra': {
                     'max_length': 255,
                     'max_length': 255,
                     'required': False,
                     'required': False,
                 },
                 },
-                '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',
+                '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': {
                 'field_extra': {
                     'max_length': 128000,
                     'max_length': 128000,
                     'required': False,
                     'required': False,
                     'rows': 8,
                     'rows': 8,
                 },
                 },
-                'is_public': True,
-                'is_lazy': True,
-            },
-            {
+                'is_public':
+                    True,
+                'is_lazy':
+                    True,
+            }, {
                 'setting': 'privacy_policy_title',
                 'setting': 'privacy_policy_title',
                 'name': _("Policy title"),
                 'name': _("Policy title"),
                 'legend': _("Privacy policy"),
                 'legend': _("Privacy policy"),
@@ -70,37 +84,48 @@ def create_legal_settings_group(apps, schema_editor):
                     'required': False,
                     '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': "",
+            }, {
+                'setting':
+                    'privacy_policy_link',
+                'name':
+                    _("Policy link"),
+                'description':
+                    _("If privacy policy is located on "
+                      "other page, enter there its link."),
+                'value':
+                    "",
                 'field_extra': {
                 'field_extra': {
                     'max_length': 255,
                     'max_length': 255,
                     'required': False,
                     'required': False,
                 },
                 },
-                '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',
+                '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': {
                 'field_extra': {
                     'max_length': 128000,
                     'max_length': 128000,
                     'required': False,
                     'required': False,
                     'rows': 8,
                     'rows': 8,
                 },
                 },
-                'is_public': True,
-                'is_lazy': True,
-            },
-            {
+                'is_public':
+                    True,
+                'is_lazy':
+                    True,
+            }, {
                 'setting': 'forum_footnote',
                 'setting': 'forum_footnote',
                 'name': _("Footnote"),
                 'name': _("Footnote"),
                 'description': _("Short message displayed "
                 'description': _("Short message displayed "
@@ -110,9 +135,9 @@ def create_legal_settings_group(apps, schema_editor):
                     'max_length': 300
                     'max_length': 300
                 },
                 },
                 'is_public': True,
                 'is_public': True,
-            },
-        )
-    })
+            }, )
+        }
+    )
 
 
 
 
 class Migration(migrations.Migration):
 class Migration(migrations.Migration):

+ 6 - 18
misago/legal/tests.py

@@ -56,26 +56,20 @@ class PrivacyPolicyTests(TestCase):
         settings.override_setting('privacy_policy', 'Lorem ipsum')
         settings.override_setting('privacy_policy', 'Lorem ipsum')
         context_dict = legal_links(MockRequest())
         context_dict = legal_links(MockRequest())
 
 
-        self.assertEqual(context_dict, {
-            'PRIVACY_POLICY_URL': reverse('misago:privacy-policy')
-        })
+        self.assertEqual(context_dict, {'PRIVACY_POLICY_URL': reverse('misago:privacy-policy')})
 
 
     def test_context_processor_remote_policy(self):
     def test_context_processor_remote_policy(self):
         """context processor has TOS link to remote url"""
         """context processor has TOS link to remote url"""
         settings.override_setting('privacy_policy_link', 'http://test.com')
         settings.override_setting('privacy_policy_link', 'http://test.com')
         context_dict = legal_links(MockRequest())
         context_dict = legal_links(MockRequest())
 
 
-        self.assertEqual(context_dict, {
-            'PRIVACY_POLICY_URL': 'http://test.com'
-        })
+        self.assertEqual(context_dict, {'PRIVACY_POLICY_URL': 'http://test.com'})
 
 
         # set misago view too
         # set misago view too
         settings.override_setting('privacy_policy', 'Lorem ipsum')
         settings.override_setting('privacy_policy', 'Lorem ipsum')
         context_dict = legal_links(MockRequest())
         context_dict = legal_links(MockRequest())
 
 
-        self.assertEqual(context_dict, {
-            'PRIVACY_POLICY_URL': 'http://test.com'
-        })
+        self.assertEqual(context_dict, {'PRIVACY_POLICY_URL': 'http://test.com'})
 
 
 
 
 class TermsOfServiceTests(TestCase):
 class TermsOfServiceTests(TestCase):
@@ -123,23 +117,17 @@ 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"""
         settings.override_setting('terms_of_service_link', 'http://test.com')
         settings.override_setting('terms_of_service_link', 'http://test.com')
         context_dict = legal_links(MockRequest())
         context_dict = legal_links(MockRequest())
 
 
-        self.assertEqual(context_dict, {
-            'TERMS_OF_SERVICE_URL': 'http://test.com'
-        })
+        self.assertEqual(context_dict, {'TERMS_OF_SERVICE_URL': 'http://test.com'})
 
 
         # set misago view too
         # set misago view too
         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': 'http://test.com'
-        })
+        self.assertEqual(context_dict, {'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 - 6
misago/markup/api.py

@@ -17,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 - 2
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

+ 1 - 3
misago/markup/context_processors.py

@@ -2,8 +2,6 @@ from django.urls import reverse
 
 
 
 
 def preload_api_url(request):
 def preload_api_url(request):
-    request.frontend_context.update({
-        'PARSE_MARKUP_API': reverse('misago:api:parse-markup')
-    })
+    request.frontend_context.update({'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):

+ 18 - 6
misago/markup/flavours.py

@@ -24,16 +24,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'
+        )

+ 12 - 4
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
 
 
@@ -133,8 +142,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']:

+ 2 - 0
misago/markup/pipeline.py

@@ -11,6 +11,7 @@ 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 +32,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]))

+ 20 - 42
misago/markup/tests/test_mentions.py

@@ -10,34 +10,18 @@ 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>'
-            ),
-            (
-                '<h1>Hello, <strong>@{}!</strong></h1>',
-                '<h1>Hello, <strong><a href="{}">@{}</a>!</strong></h1>'
-            ),
-            (
-                '<h1>Hello, <strong>@{}</strong>!</h1>',
-                '<h1>Hello, <strong><a href="{}">@{}</a></strong>!</h1>'
-            ),
-        )
+        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><a href="{}">@{}</a>!</strong></h1>'
+                      ), (
+                          '<h1>Hello, <strong>@{}</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)
 
 
@@ -48,17 +32,13 @@ 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)
 
 
@@ -70,12 +50,11 @@ class MentionsTests(AuthenticatedUserTestCase):
         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)
         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(
+            *formats
+        )
 
 
-        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)
@@ -86,12 +65,11 @@ class MentionsTests(AuthenticatedUserTestCase):
         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)
         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(
+            *formats
+        )
 
 
-        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')]

+ 6 - 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,8 +59,7 @@ 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,
@@ -87,10 +84,7 @@ def sync_record(user, category):
             last_read_on = timezone.now()
             last_read_on = timezone.now()
         else:
         else:
             last_read_on = cutoff_date
             last_read_on = cutoff_date
-        category_record = user.categoryread_set.create(
-            category=category,
-            last_read_on=last_read_on
-        )
+        category_record = user.categoryread_set.create(category=category, last_read_on=last_read_on)
 
 
 
 
 def read_category(user, category):
 def read_category(user, category):
@@ -98,7 +92,9 @@ def read_category(user, category):
     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']
             id__in=user.acl_cache['visible_categories']
-        ).values_list('id', flat=True)
+        ).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, ),
         ),
         ),
     ]
     ]

+ 2 - 2
misago/readtracker/signals.py

@@ -9,11 +9,11 @@ all_read = Signal()
 category_read = Signal(providing_args=["category"])
 category_read = Signal(providing_args=["category"])
 thread_tracked = Signal(providing_args=["thread"])
 thread_tracked = Signal(providing_args=["thread"])
 thread_read = Signal(providing_args=["thread"])
 thread_read = Signal(providing_args=["thread"])
-
-
 """
 """
 Signal handlers
 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))

+ 2 - 6
misago/readtracker/tests/test_readtracker.py

@@ -19,15 +19,11 @@ 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(
-            category=self.category,
-            started_on=datetime
-        )
+        return testutils.post_thread(category=self.category, started_on=datetime)
 
 
 
 
 class CategoriesTrackerTests(ReadTrackerTests):
 class CategoriesTrackerTests(ReadTrackerTests):

+ 6 - 7
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:
@@ -144,9 +145,7 @@ def sync_record(user, thread, last_read_reply):
         thread.read_record.save(update_fields=['last_read_on'])
         thread.read_record.save(update_fields=['last_read_on'])
     else:
     else:
         user.threadread_set.create(
         user.threadread_set.create(
-            category=thread.category,
-            thread=thread,
-            last_read_on=last_read_reply.posted_on
+            category=thread.category, thread=thread, 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)

+ 7 - 10
misago/search/permissions.py

@@ -9,13 +9,12 @@ from misago.core.forms import YesNoSwitch
 """
 """
 Admin Permissions Form
 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):
@@ -28,12 +27,10 @@ def change_permissions_form(role):
 """
 """
 ACL Builder
 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:

+ 8 - 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,9 @@ 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 +43,9 @@ 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'])

+ 6 - 10
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,8 +46,7 @@ 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):
         """
         """
@@ -60,6 +57,5 @@ class SearchProvidersTests(TestCase):
         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])

+ 10 - 23
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,28 @@ 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(
-            reverse('misago:search', kwargs={'search_provider': 'users'}))
+        response = self.client.get(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(
-            reverse('misago:search', kwargs={'search_provider': 'users'}))
+        response = self.client.get(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(
-            reverse('misago:search', kwargs={'search_provider': 'threads'}))
+        response = self.client.get(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'),
 ]
 ]
-

+ 4 - 2
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'),

+ 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):

+ 2 - 1
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."))

+ 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():

+ 8 - 4
misago/threads/api/postendpoints/merge.py

@@ -67,8 +67,8 @@ def clean_posts_for_merge(request, thread):
     elif len(posts_ids) > MERGE_LIMIT:
     elif len(posts_ids) > MERGE_LIMIT:
         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.",
-            MERGE_LIMIT)
+            "No more than %(limit)s posts can be merged at single time.", 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 +78,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 +95,9 @@ 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)

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

@@ -54,7 +54,11 @@ 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."))
@@ -73,8 +77,8 @@ def clean_posts_for_move(request, thread):
     elif len(posts_ids) > MOVE_LIMIT:
     elif len(posts_ids) > MOVE_LIMIT:
         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.",
-            MOVE_LIMIT)
+            "No more than %(limit)s posts can be moved at single time.", 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)
 
 
 
 

+ 11 - 4
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)
 
 
 
 
@@ -59,10 +61,7 @@ def patch_is_liked(request, post, value):
 
 
     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=['likes', 'last_likes'])
     post.save(update_fields=['likes', 'last_likes'])
 
 
@@ -71,6 +70,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 +82,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 +92,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 +106,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 - 3
misago/threads/api/postendpoints/read.py

@@ -10,7 +10,5 @@ def post_read_endpoint(request, thread, post):
         if thread.subscription and thread.subscription.last_read_on < post.posted_on:
         if thread.subscription and thread.subscription.last_read_on < post.posted_on:
             thread.subscription.last_read_on = post.posted_on
             thread.subscription.last_read_on = post.posted_on
             thread.subscription.save()
             thread.subscription.save()
-        return Response({
-            'thread_is_read': thread.last_post_on <= post.posted_on
-        })
+        return Response({'thread_is_read': thread.last_post_on <= post.posted_on})
     return Response({'thread_is_read': True})
     return Response({'thread_is_read': True})

+ 2 - 2
misago/threads/api/postendpoints/split.py

@@ -47,8 +47,8 @@ def clean_posts_for_split(request, thread):
     elif len(posts_ids) > SPLIT_LIMIT:
     elif len(posts_ids) > SPLIT_LIMIT:
         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.",
-            SPLIT_LIMIT)
+            "No more than %(limit)s posts can be split at single time.", 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)

+ 8 - 7
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)
 
 
@@ -108,7 +104,9 @@ class PostingEndpoint(object):
             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 +120,16 @@ 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)

+ 24 - 19
misago/threads/api/postingendpoint/attachments.py

@@ -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,7 +57,9 @@ 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.")
+                    message = _(
+                        "You don't have permission to remove \"%(attachment)s\" attachment."
+                    )
                     raise serializers.ValidationError(message % {'attachment': attachment.filename})
                     raise serializers.ValidationError(message % {'attachment': attachment.filename})
 
 
         if new_attachments:
         if new_attachments:
@@ -77,8 +80,7 @@ class AttachmentsSerializer(serializers.Serializer):
             return []
             return []
 
 
         queryset = user.attachment_set.select_related('filetype').filter(
         queryset = user.attachment_set.select_related('filetype').filter(
-            post__isnull=True,
-            id__in=ids
+            post__isnull=True, id__in=ids
         )
         )
 
 
         return list(queryset)
         return list(queryset)
@@ -122,8 +124,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
+            }
+        )

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

@@ -18,6 +18,7 @@ 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 +42,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 +58,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 +71,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.")
+            )

+ 5 - 14
misago/threads/api/postingendpoint/emailnotification.py

@@ -17,8 +17,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,
-            last_read_on__gte=self.previous_last_post_on
+            send_email=True, 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,18 +39,10 @@ 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,
-            subscriber,
-            subject % subject_formats,
-            'misago/emails/thread/reply',
-            {
-                'thread': self.thread,
-                'post': self.post
-            }
+            self.request, subscriber, subject % subject_formats, 'misago/emails/thread/reply',
+            {'thread': self.thread,
+             '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()

+ 11 - 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,12 @@ 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 +81,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

+ 1 - 0
misago/threads/api/postingendpoint/privatethread.py

@@ -9,6 +9,7 @@ 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

+ 3 - 7
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,

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

@@ -84,16 +84,12 @@ class ReplyMiddleware(PostingMiddleware):
 class ReplySerializer(serializers.Serializer):
 class ReplySerializer(serializers.Serializer):
     post = serializers.CharField(
     post = serializers.CharField(
         validators=[validate_post],
         validators=[validate_post],
-        error_messages={
-            'required': ugettext_lazy("You have to enter a message.")
-        }
+        error_messages={'required': ugettext_lazy("You have to enter a message.")}
     )
     )
 
 
 
 
 class ThreadSerializer(ReplySerializer):
 class ThreadSerializer(ReplySerializer):
     title = serializers.CharField(
     title = serializers.CharField(
         validators=[validate_title],
         validators=[validate_title],
-        error_messages={
-        'required': ugettext_lazy("You have to enter thread title.")
-        }
+        error_messages={'required': ugettext_lazy("You have to enter thread title.")}
     )
     )

+ 2 - 2
misago/threads/api/postingendpoint/syncprivatethreads.py

@@ -8,6 +8,7 @@ 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
@@ -15,6 +16,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,
-            exclude_user=self.user
+            participants=self.thread.participants_list, exclude_user=self.user
         )
         )

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

@@ -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')
 
 

+ 27 - 30
misago/threads/api/threadendpoints/merge.py

@@ -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,19 @@ 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({
         return Response({
-            'detail': _("The thread you have entered link to doesn't exist or you don't have permission to see it.")
-        }, status=400)
+            '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 +71,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)
 
 
@@ -106,9 +106,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 +123,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
 
 
@@ -152,8 +146,8 @@ def clean_threads_for_merge(request):
     elif len(threads_ids) > MERGE_LIMIT:
     elif len(threads_ids) > MERGE_LIMIT:
         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.",
-            MERGE_LIMIT)
+            "No more than %(limit)s threads can be merged at single time.", 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)
@@ -194,9 +188,11 @@ 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 +219,10 @@ 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

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

@@ -33,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)
 
 
 
 
@@ -51,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)
 
 
 
 
@@ -72,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)
 
 
 
 
@@ -81,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)
@@ -97,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,10 +130,9 @@ def patch_flatten_categories(request, thread, value):
             'top_category': thread.top_category.pk,
             'top_category': thread.top_category.pk,
         }
         }
     except AttributeError:
     except AttributeError:
-        return {
-            'category': thread.category_id,
-            'top_category': None
-        }
+        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)
 
 
 
 
@@ -142,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)
 
 
 
 
@@ -158,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)
 
 
 
 
@@ -171,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)
 
 
 
 
@@ -197,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)
 
 
 
 
@@ -206,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."))
@@ -219,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)
 
 
 
 
@@ -244,18 +257,14 @@ 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 {'deleted': False, 'participants': participants.data}
+
 
 
-        return {
-            'deleted': False,
-            'participants': participants.data
-        }
 thread_patch_dispatcher.remove('participants', patch_remove_participant)
 thread_patch_dispatcher.remove('participants', patch_remove_participant)
 
 
 
 
@@ -279,9 +288,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)
 
 
 
 
@@ -300,7 +309,7 @@ def thread_patch_endpoint(request, thread):
 
 
     title_changed = old_title != thread.title
     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 - 4
misago/threads/api/threadpoll.py

@@ -112,9 +112,7 @@ class ViewSet(viewsets.ViewSet):
         thread.has_poll = False
         thread.has_poll = False
         thread.save()
         thread.save()
 
 
-        return Response({
-            'can_start_poll': can_start_poll(request.user, thread)
-        })
+        return Response({'can_start_poll': can_start_poll(request.user, thread)})
 
 
     @detail_route(methods=['get', 'post'])
     @detail_route(methods=['get', 'post'])
     def votes(self, request, thread_pk, pk):
     def votes(self, request, thread_pk, pk):
@@ -152,7 +150,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)

+ 12 - 35
misago/threads/api/threadposts.py

@@ -32,22 +32,16 @@ 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,
-            get_int_or_404(pk),
-            None,
-            read_aware,
-            subscription_aware,
-            select_for_update
+            request, get_int_or_404(pk), None, read_aware, subscription_aware, 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,
-            read_aware=False,
-            subscription_aware=False,
-            select_for_update=True
+            request, pk, read_aware=False, subscription_aware=False, select_for_update=True
         )
         )
 
 
     def get_posts(self, request, thread, page):
     def get_posts(self, request, thread, page):
@@ -62,7 +56,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)
@@ -98,12 +92,7 @@ class ViewSet(viewsets.ViewSet):
         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(
-            request,
-            PostingEndpoint.REPLY,
-            thread=thread,
-            post=post
-        )
+        posting = PostingEndpoint(request, PostingEndpoint.REPLY, thread=thread, post=post)
 
 
         if posting.is_valid():
         if posting.is_valid():
             user_posts = request.user.posts
             user_posts = request.user.posts
@@ -128,12 +117,7 @@ class ViewSet(viewsets.ViewSet):
 
 
         allow_edit_post(request.user, post)
         allow_edit_post(request.user, post)
 
 
-        posting = PostingEndpoint(
-            request,
-            PostingEndpoint.EDIT,
-            thread=thread,
-            post=post
-        )
+        posting = PostingEndpoint(request, PostingEndpoint.EDIT, thread=thread, post=post)
 
 
         if posting.is_valid():
         if posting.is_valid():
             post_edits = post.edits
             post_edits = post.edits
@@ -193,12 +177,7 @@ class ViewSet(viewsets.ViewSet):
 
 
     @detail_route(methods=['get'], url_path='editor')
     @detail_route(methods=['get'], url_path='editor')
     def post_editor(self, request, thread_pk, pk):
     def post_editor(self, request, thread_pk, pk):
-        thread = self.get_thread(
-            request,
-            thread_pk,
-            read_aware=False,
-            subscription_aware=False
-        )
+        thread = self.get_thread(request, thread_pk, read_aware=False, subscription_aware=False)
         post = self.get_post(request, thread, pk).unwrap()
         post = self.get_post(request, thread, pk).unwrap()
 
 
         allow_edit_post(request.user, post)
         allow_edit_post(request.user, post)
@@ -208,7 +187,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,
@@ -223,10 +203,7 @@ class ViewSet(viewsets.ViewSet):
     @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)
 
 

+ 6 - 16
misago/threads/api/threads.py

@@ -24,22 +24,16 @@ 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,
-            get_int_or_404(pk),
-            None,
-            read_aware,
-            subscription_aware,
-            select_for_update
+            request, get_int_or_404(pk), None, read_aware, subscription_aware, 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,
-            read_aware=False,
-            subscription_aware=False,
-            select_for_update=True
+            request, pk, read_aware=False, subscription_aware=False, select_for_update=True
         )
         )
 
 
     def retrieve(self, request, pk):
     def retrieve(self, request, pk):
@@ -76,11 +70,7 @@ class ThreadViewSet(ViewSet):
 
 
         # Put them through posting pipeline
         # Put them through posting pipeline
         posting = PostingEndpoint(
         posting = PostingEndpoint(
-            request,
-            PostingEndpoint.START,
-            tree_name=THREADS_ROOT_NAME,
-            thread=thread,
-            post=post
+            request, PostingEndpoint.START, tree_name=THREADS_ROOT_NAME, thread=thread, post=post
         )
         )
 
 
         if posting.is_valid():
         if posting.is_valid():

+ 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'),
     })
     })
 
 

+ 29 - 19
misago/threads/forms.py

@@ -23,11 +23,7 @@ class SearchAttachmentsForm(forms.Form):
     is_orphan = forms.ChoiceField(
     is_orphan = forms.ChoiceField(
         label=_("State"),
         label=_("State"),
         required=False,
         required=False,
-        choices=(
-            ('', _("All")),
-            ('yes', _("Only orphaned")),
-            ('no', _("Not orphaned")),
-        ),
+        choices=(('', _("All")), ('yes', _("Only orphaned")), ('no', _("Not orphaned")), ),
     )
     )
 
 
     def filter_queryset(self, criteria, queryset):
     def filter_queryset(self, criteria, queryset):
@@ -58,19 +54,33 @@ class AttachmentTypeForm(forms.ModelForm):
             'limit_downloads_to': _("Limit downloads to"),
             'limit_downloads_to': _("Limit downloads to"),
         }
         }
         help_texts = {
         help_texts = {
-            '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."),
-            '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."),
+            '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."
+                ),
+            '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."
+                ),
         }
         }
         widgets = {
         widgets = {
             'limit_uploads_to': forms.CheckboxSelectMultiple,
             'limit_uploads_to': forms.CheckboxSelectMultiple,
@@ -78,7 +88,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 - 4
misago/threads/management/commands/clearattachments.py

@@ -15,10 +15,7 @@ class Command(BaseCommand):
 
 
     def handle(self, *args, **options):
     def handle(self, *args, **options):
         cutoff = timezone.now() - timedelta(minutes=settings.MISAGO_ATTACHMENT_ORPHANED_EXPIRE)
         cutoff = timezone.now() - timedelta(minutes=settings.MISAGO_ATTACHMENT_ORPHANED_EXPIRE)
-        queryset = Attachment.objects.filter(
-            post__isnull=True,
-            uploaded_on__lt=cutoff
-        )
+        queryset = Attachment.objects.filter(post__isnull=True, uploaded_on__lt=cutoff)
 
 
         attachments_to_sync = queryset.count()
         attachments_to_sync = queryset.count()
 
 

+ 7 - 8
misago/threads/middleware.py

@@ -20,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)
@@ -31,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',
+            ]
+        )

+ 278 - 57
misago/threads/migrations/0001_initial.py

@@ -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,27 @@ 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 +85,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 +101,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 +127,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 +143,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 +204,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 +222,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 +273,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 +296,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 +341,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 +461,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 +530,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 +553,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
+            ),
         ),
         ),
     ]
     ]

+ 30 - 24
misago/threads/migrations/0002_threads_settings.py

@@ -10,12 +10,15 @@ _ = 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': (
-            {
+    migrate_settings_group(
+        apps, {
+            'key':
+                'threads',
+            'name':
+                _("Threads"),
+            'description':
+                _("Those settings control threads and posts."),
+            'settings': ({
                 'setting': 'thread_title_length_min',
                 'setting': 'thread_title_length_min',
                 'name': _("Minimum length"),
                 'name': _("Minimum length"),
                 'description': _("Minimum allowed thread title length."),
                 'description': _("Minimum allowed thread title length."),
@@ -27,8 +30,7 @@ def create_threads_settings_group(apps, schema_editor):
                     'max_value': 255,
                     'max_value': 255,
                 },
                 },
                 'is_public': True,
                 'is_public': True,
-            },
-            {
+            }, {
                 'setting': 'thread_title_length_max',
                 'setting': 'thread_title_length_max',
                 'name': _("Maximum length"),
                 'name': _("Maximum length"),
                 'description': _("Maximum allowed thread length."),
                 'description': _("Maximum allowed thread length."),
@@ -39,8 +41,7 @@ def create_threads_settings_group(apps, schema_editor):
                     'max_value': 255,
                     'max_value': 255,
                 },
                 },
                 'is_public': True,
                 'is_public': True,
-            },
-            {
+            }, {
                 'setting': 'post_length_min',
                 'setting': 'post_length_min',
                 'name': _("Minimum length"),
                 'name': _("Minimum length"),
                 'description': _("Minimum allowed user post length."),
                 'description': _("Minimum allowed user post length."),
@@ -51,23 +52,28 @@ def create_threads_settings_group(apps, schema_editor):
                     'min_value': 1,
                     '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,
+            }, {
+                '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': {
                 'field_extra': {
                     'min_value': 0,
                     'min_value': 0,
                 },
                 },
-                'is_public': True,
-            },
-        )
-    })
+                'is_public':
+                    True,
+            }, )
+        }
+    )
 
 
 
 
 class Migration(migrations.Migration):
 class Migration(migrations.Migration):

+ 64 - 79
misago/threads/migrations/0003_attachment_types.py

@@ -5,85 +5,70 @@ from __future__ import unicode_literals
 from django.db import migrations
 from django.db import migrations
 
 
 
 
-ATTACHMENTS = (
-    {
-        'name': 'GIF',
-        'extensions': ('gif',),
-        'mimetypes': ('image/gif',),
-        'size_limit': 5 * 1024
-    },
-    {
-        'name': 'JPG',
-        'extensions': ('jpg', 'jpeg',),
-        'mimetypes': ('image/jpeg',),
-        'size_limit': 3 * 1024
-    },
-    {
-        'name': 'PNG',
-        'extensions': ('png',),
-        'mimetypes': ('image/png',),
-        'size_limit': 3 * 1024
-    },
-    {
-        'name': 'PDF',
-        'extensions': ('pdf',),
-        'mimetypes': (
-            'application/pdf',
-            'application/x-pdf',
-            'application/x-bzpdf',
-            'application/x-gzpdf'
-        ),
-        'size_limit': 4 * 1024
-    },
-    {
-        'name': 'Text',
-        'extensions': ('txt',),
-        'mimetypes': ('text/plain',),
-        'size_limit': 4 * 1024
-    },
-    {
-        'name': 'Markdown',
-        'extensions': ('md',),
-        'mimetypes': ('text/markdown',),
-        'size_limit': 4 * 1024
-    },
-    {
-        'name': 'reStructuredText',
-        'extensions': ('rst',),
-        'mimetypes': ('text/x-rst',),
-        'size_limit': 4 * 1024
-    },
-    {
-        'name': '7Z',
-        'extensions': ('7z',),
-        'mimetypes': ('application/x-7z-compressed',),
-        'size_limit': 4 * 1024
-    },
-    {
-        'name': 'RAR',
-        'extensions': ('rar',),
-        'mimetypes': ('application/vnd.rar',),
-        'size_limit': 4 * 1024
-    },
-    {
-        'name': 'TAR',
-        'extensions': ('tar',),
-        'mimetypes': ('application/x-tar',),
-        'size_limit': 4 * 1024
-    },
-    {
-        'name': 'GZ',
-        'extensions': ('gz',),
-        'mimetypes': ('application/gzip',),
-        'size_limit': 4 * 1024
-    },
-    {
-        'name': 'ZIP',
-        'extensions': ('zip', 'zipx',),
-        'mimetypes': ('application/zip',),
-        'size_limit': 4 * 1024
-    },
-)
+ATTACHMENTS = ({
+    'name': 'GIF',
+    'extensions': ('gif', ),
+    'mimetypes': ('image/gif', ),
+    'size_limit': 5 * 1024
+}, {
+    'name': 'JPG',
+    'extensions': ('jpg', 'jpeg', ),
+    'mimetypes': ('image/jpeg', ),
+    'size_limit': 3 * 1024
+}, {
+    'name': 'PNG',
+    'extensions': ('png', ),
+    'mimetypes': ('image/png', ),
+    'size_limit': 3 * 1024
+}, {
+    'name':
+        'PDF',
+    'extensions': ('pdf', ),
+    'mimetypes':
+        ('application/pdf', 'application/x-pdf', 'application/x-bzpdf', 'application/x-gzpdf'),
+    'size_limit':
+        4 * 1024
+}, {
+    'name': 'Text',
+    'extensions': ('txt', ),
+    'mimetypes': ('text/plain', ),
+    'size_limit': 4 * 1024
+}, {
+    'name': 'Markdown',
+    'extensions': ('md', ),
+    'mimetypes': ('text/markdown', ),
+    'size_limit': 4 * 1024
+}, {
+    'name': 'reStructuredText',
+    'extensions': ('rst', ),
+    'mimetypes': ('text/x-rst', ),
+    'size_limit': 4 * 1024
+}, {
+    'name': '7Z',
+    'extensions': ('7z', ),
+    'mimetypes': ('application/x-7z-compressed', ),
+    'size_limit': 4 * 1024
+}, {
+    'name': 'RAR',
+    'extensions': ('rar', ),
+    'mimetypes': ('application/vnd.rar', ),
+    'size_limit': 4 * 1024
+}, {
+    'name': 'TAR',
+    'extensions': ('tar', ),
+    'mimetypes': ('application/x-tar', ),
+    'size_limit': 4 * 1024
+}, {
+    'name': 'GZ',
+    'extensions': ('gz', ),
+    'mimetypes': ('application/gzip', ),
+    'size_limit': 4 * 1024
+}, {
+    'name': 'ZIP',
+    'extensions': ('zip', 'zipx', ),
+    'mimetypes': ('application/zip', ),
+    'size_limit': 4 * 1024
+}, )
 
 
 
 
 def create_attachment_types(apps, schema_editor):
 def create_attachment_types(apps, schema_editor):

+ 30 - 24
misago/threads/migrations/0004_update_settings.py

@@ -10,12 +10,15 @@ _ = 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': (
-            {
+    migrate_settings_group(
+        apps, {
+            'key':
+                'threads',
+            'name':
+                _("Threads"),
+            'description':
+                _("Those settings control threads and posts."),
+            'settings': ({
                 'setting': 'thread_title_length_min',
                 'setting': 'thread_title_length_min',
                 'name': _("Minimum length"),
                 'name': _("Minimum length"),
                 'description': _("Minimum allowed thread title length."),
                 'description': _("Minimum allowed thread title length."),
@@ -27,8 +30,7 @@ def update_threads_settings(apps, schema_editor):
                     'max_value': 255,
                     'max_value': 255,
                 },
                 },
                 'is_public': True,
                 'is_public': True,
-            },
-            {
+            }, {
                 'setting': 'thread_title_length_max',
                 'setting': 'thread_title_length_max',
                 'name': _("Maximum length"),
                 'name': _("Maximum length"),
                 'description': _("Maximum allowed thread length."),
                 'description': _("Maximum allowed thread length."),
@@ -39,8 +41,7 @@ def update_threads_settings(apps, schema_editor):
                     'max_value': 255,
                     'max_value': 255,
                 },
                 },
                 'is_public': True,
                 'is_public': True,
-            },
-            {
+            }, {
                 'setting': 'post_length_min',
                 'setting': 'post_length_min',
                 'name': _("Minimum length"),
                 'name': _("Minimum length"),
                 'description': _("Minimum allowed user post length."),
                 'description': _("Minimum allowed user post length."),
@@ -51,23 +52,28 @@ def update_threads_settings(apps, schema_editor):
                     'min_value': 1,
                     '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,
+            }, {
+                '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': {
                 'field_extra': {
                     'min_value': 0,
                     'min_value': 0,
                 },
                 },
-                'is_public': True,
-            },
-        )
-    })
+                'is_public':
+                    True,
+            }, )
+        }
+    )
 
 
     delete_settings_cache()
     delete_settings_cache()
 
 

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

@@ -25,33 +25,21 @@ 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)
 
 
     uploader = models.ForeignKey(
     uploader = models.ForeignKey(
-        settings.AUTH_USER_MODEL,
-        blank=True,
-        null=True,
-        on_delete=models.SET_NULL
+        settings.AUTH_USER_MODEL, blank=True, null=True, 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 +48,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 +80,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 +108,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 - 5
misago/threads/models/attachmenttype.py

@@ -17,11 +17,8 @@ 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=(
-            (ENABLED, _("Allow uploads and downloads")),
-            (LOCKED, _("Allow downloads only")),
-            (DISABLED, _("Disallow both uploading and downloading")),
-        )
+        choices=((ENABLED, _("Allow uploads and downloads")), (LOCKED, _("Allow downloads only")),
+                 (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)

+ 1 - 4
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):

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

@@ -82,7 +82,7 @@ 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')
         ]
         ]

+ 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']]

+ 7 - 23
misago/threads/models/thread.py

@@ -13,11 +13,9 @@ class Thread(models.Model):
     WEIGHT_PINNED = 1
     WEIGHT_PINNED = 1
     WEIGHT_GLOBAL = 2
     WEIGHT_GLOBAL = 2
 
 
-    WEIGHT_CHOICES = (
-        (WEIGHT_DEFAULT, _("Don't pin thread")),
-        (WEIGHT_PINNED, _("Pin thread within category")),
-        (WEIGHT_GLOBAL, _("Pin thread globally"))
-    )
+    WEIGHT_CHOICES = ((WEIGHT_DEFAULT, _("Don't pin thread")),
+                      (WEIGHT_PINNED, _("Pin thread within category")),
+                      (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)
@@ -35,27 +33,16 @@ class Thread(models.Model):
     last_post_on = models.DateTimeField(db_index=True)
     last_post_on = models.DateTimeField(db_index=True)
 
 
     first_post = models.ForeignKey(
     first_post = models.ForeignKey(
-        'misago_threads.Post',
-        related_name='+',
-        null=True,
-        blank=True,
-        on_delete=models.SET_NULL
+        'misago_threads.Post', related_name='+', null=True, blank=True, on_delete=models.SET_NULL
     )
     )
     starter = models.ForeignKey(
     starter = models.ForeignKey(
-        settings.AUTH_USER_MODEL,
-        null=True,
-        blank=True,
-        on_delete=models.SET_NULL
+        settings.AUTH_USER_MODEL, null=True, blank=True, 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)
 
 
     last_post = models.ForeignKey(
     last_post = models.ForeignKey(
-        'misago_threads.Post',
-        related_name='+',
-        null=True,
-        blank=True,
-        on_delete=models.SET_NULL
+        'misago_threads.Post', related_name='+', null=True, blank=True, 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(
@@ -119,10 +106,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 - 15
misago/threads/moderation/threads.py

@@ -33,9 +33,7 @@ def change_thread_title(request, thread, new_title):
         thread.first_post.update_search_vector()
         thread.first_post.update_search_vector()
         thread.first_post.save(update_fields=['search_vector'])
         thread.first_post.save(update_fields=['search_vector'])
 
 
-        record_event(request, thread, 'changed_title', {
-            'old_title': old_title
-        })
+        record_event(request, thread, 'changed_title', {'old_title': old_title})
         return True
         return True
     else:
     else:
         return False
         return False
@@ -77,12 +75,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
@@ -153,13 +153,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')

+ 3 - 4
misago/threads/paginator.py

@@ -6,13 +6,12 @@ class PostsPaginator(Paginator):
     Paginator that returns that makes last item on page
     Paginator that returns that makes last item on page
     repeat as first item on next page.
     repeat as first item on next page.
     """
     """
-    def __init__(self, object_list, per_page, orphans=0,
-                 allow_empty_first_page=True):
+
+    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):
         """
         """

+ 24 - 38
misago/threads/participants.py

@@ -27,10 +27,7 @@ def make_threads_participants_aware(user, threads):
         thread.participant = None
         thread.participant = None
         threads_dict[thread.pk] = thread
         threads_dict[thread.pk] = thread
 
 
-    participants_qs = ThreadParticipant.objects.filter(
-        user=user,
-        thread_id__in=threads_dict.keys()
-    )
+    participants_qs = ThreadParticipant.objects.filter(user=user, thread_id__in=threads_dict.keys())
 
 
     for participant in participants_qs:
     for participant in participants_qs:
         participant.user = user
         participant.user = user
@@ -51,8 +48,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]
@@ -64,9 +60,7 @@ 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):
@@ -82,17 +76,17 @@ def change_owner(request, thread, user):
     """
     """
     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,
-        exclude_user=request.user
+        participants=thread.participants_list, 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': {
+        record_event(
+            request, thread, 'changed_owner',
+            {'user': {
                 'username': user.username,
                 'username': user.username,
                 'url': user.get_absolute_url(),
                 'url': user.get_absolute_url(),
-            }
-        })
+            }}
+        )
     else:
     else:
         record_event(request, thread, 'tookover')
         record_event(request, thread, 'tookover')
 
 
@@ -106,12 +100,13 @@ def add_participant(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': {
+        record_event(
+            request, thread, 'added_participant',
+            {'user': {
                 'username': user.username,
                 'username': user.username,
                 'url': user.get_absolute_url(),
                 'url': user.get_absolute_url(),
-            }
-        })
+            }}
+        )
 
 
 
 
 def add_participants(request, thread, users):
 def add_participants(request, thread, users):
@@ -127,9 +122,7 @@ def add_participants(request, thread, users):
         thread_participants = []
         thread_participants = []
 
 
     set_users_unread_private_threads_sync(
     set_users_unread_private_threads_sync(
-        users=users,
-        participants=thread_participants,
-        exclude_user=request.user
+        users=users, participants=thread_participants, exclude_user=request.user
     )
     )
 
 
     emails = []
     emails = []
@@ -142,19 +135,11 @@ def add_participants(request, thread, users):
 
 
 def build_noticiation_email(request, thread, user):
 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 = {
-        'thread': thread.title,
-        'user': request.user.username
-    }
+    subject_formats = {'thread': thread.title, '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}
     )
     )
 
 
 
 
@@ -180,7 +165,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'
@@ -192,9 +177,10 @@ def remove_participant(request, thread, user):
             else:
             else:
                 event_type = 'removed_participant'
                 event_type = 'removed_participant'
 
 
-        record_event(request, thread, event_type, {
-            'user': {
+        record_event(
+            request, thread, event_type,
+            {'user': {
                 'username': user.username,
                 'username': user.username,
                 'url': user.get_absolute_url(),
                 'url': user.get_absolute_url(),
-            }
-        })
+            }}
+        )

+ 13 - 2
misago/threads/permissions/attachments.py

@@ -10,6 +10,8 @@ from misago.threads.models import Attachment
 """
 """
 Admin Permissions Form
 Admin Permissions Form
 """
 """
+
+
 class PermissionsForm(forms.Form):
 class PermissionsForm(forms.Form):
     legend = _("Attachments")
     legend = _("Attachments")
 
 
@@ -20,7 +22,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"))
 
 
 
 
@@ -43,6 +47,8 @@ def change_permissions_form(role):
 """
 """
 ACL Builder
 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,7 +57,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,
         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
@@ -61,6 +70,8 @@ def build_acl(acl, roles, key_name):
 """
 """
 ACL's for targets
 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({

+ 50 - 35
misago/threads/permissions/polls.py

@@ -23,11 +23,11 @@ __all__ = [
     'allow_see_poll_votes',
     'allow_see_poll_votes',
     'can_see_poll_votes',
     'can_see_poll_votes',
 ]
 ]
-
-
 """
 """
 Admin Permissions Forms
 Admin Permissions Forms
 """
 """
+
+
 class RolePermissionsForm(forms.Form):
 class RolePermissionsForm(forms.Form):
     legend = _("Polls")
     legend = _("Polls")
 
 
@@ -35,31 +35,19 @@ class RolePermissionsForm(forms.Form):
         label=_("Can start polls"),
         label=_("Can start polls"),
         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_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=(
-            (0, _("No")),
-            (1, _("Own polls")),
-            (2, _("All polls"))
-        )
+        choices=((0, _("No")), (1, _("Own 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=(
-            (0, _("No")),
-            (1, _("Own polls")),
-            (2, _("All polls"))
-        )
+        choices=((0, _("No")), (1, _("Own 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"),
@@ -83,6 +71,8 @@ def change_permissions_form(role):
 """
 """
 ACL Builder
 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,
@@ -92,7 +82,10 @@ def build_acl(acl, roles, key_name):
         '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,
@@ -104,6 +97,8 @@ def build_acl(acl, roles, key_name):
 """
 """
 ACL's for targets
 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),
@@ -114,9 +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({
-        'can_start_poll': can_start_poll(user, thread)
-    })
+    thread.acl.update({'can_start_poll': can_start_poll(user, thread)})
 
 
 
 
 def register_with(registry):
 def register_with(registry):
@@ -127,13 +120,17 @@ def register_with(registry):
 """
 """
 ACL tests
 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 +142,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 +151,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 +167,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 +179,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 +188,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 +204,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 +215,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 +229,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)
 
 
 
 

+ 43 - 25
misago/threads/permissions/privatethreads.py

@@ -28,11 +28,11 @@ __all__ = [
     'allow_message_user',
     'allow_message_user',
     'can_message_user',
     'can_message_user',
 ]
 ]
-
-
 """
 """
 Admin Permissions Form
 Admin Permissions Form
 """
 """
+
+
 class PermissionsForm(forms.Form):
 class PermissionsForm(forms.Form):
     legend = _("Private threads")
     legend = _("Private threads")
 
 
@@ -50,13 +50,17 @@ class PermissionsForm(forms.Form):
     )
     )
     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."
+        )
     )
     )
 
 
 
 
@@ -70,6 +74,8 @@ def change_permissions_form(role):
 """
 """
 ACL Builder
 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,
@@ -82,7 +88,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,
@@ -172,11 +181,15 @@ def register_with(registry):
 """
 """
 ACL tests
 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)
 
 
 
 
@@ -190,6 +203,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)
 
 
 
 
@@ -198,12 +213,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)
 
 
 
 
@@ -212,19 +227,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)
 
 
 
 
@@ -233,15 +247,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)
 
 
 
 
@@ -249,8 +263,7 @@ def allow_add_participant(user, target):
     message_format = {'user': target.username}
     message_format = {'user': target.username}
 
 
     if not can_use_private_threads(target):
     if not can_use_private_threads(target):
-        raise PermissionDenied(
-            _("%(user)s can't participate in private threads.") % message_format)
+        raise PermissionDenied(_("%(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
@@ -260,15 +273,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)

+ 127 - 106
misago/threads/permissions/threads.py

@@ -45,31 +45,35 @@ __all__ = [
     'exclude_invisible_threads',
     'exclude_invisible_threads',
     'exclude_invisible_posts',
     'exclude_invisible_posts',
 ]
 ]
-
-
 """
 """
 Admin Permissions Forms
 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"),
@@ -102,11 +106,7 @@ class CategoryPermissionsForm(forms.Form):
                     "with no replies can be hidden."),
                     "with no replies can be hidden."),
         coerce=int,
         coerce=int,
         initial=0,
         initial=0,
-        choices=(
-            (0, _("No")),
-            (1, _("Hide threads")),
-            (2, _("Delete threads"))
-        )
+        choices=((0, _("No")), (1, _("Hide 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"),
@@ -118,22 +118,14 @@ class CategoryPermissionsForm(forms.Form):
         label=_("Can hide all threads"),
         label=_("Can hide all threads"),
         coerce=int,
         coerce=int,
         initial=0,
         initial=0,
-        choices=(
-            (0, _("No")),
-            (1, _("Hide threads")),
-            (2, _("Delete threads"))
-        )
+        choices=((0, _("No")), (1, _("Hide 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=(
-            (0, _("No")),
-            (1, _("Locally")),
-            (2, _("Globally"))
-        )
+        choices=((0, _("No")), (1, _("Locally")), (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"))
@@ -150,11 +142,7 @@ class CategoryPermissionsForm(forms.Form):
         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=(
-            (0, _("No")),
-            (1, _("Hide posts")),
-            (2, _("Delete posts"))
-        )
+        choices=((0, _("No")), (1, _("Hide 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"),
@@ -166,22 +154,14 @@ class CategoryPermissionsForm(forms.Form):
         label=_("Can hide all posts"),
         label=_("Can hide all posts"),
         coerce=int,
         coerce=int,
         initial=0,
         initial=0,
-        choices=(
-            (0, _("No")),
-            (1, _("Hide posts")),
-            (2, _("Delete posts"))
-        )
+        choices=((0, _("No")), (1, _("Hide 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=(
-            (0, _("No")),
-            (1, _("Number only")),
-            (2, _("Number and list of likers"))
-        )
+        choices=((0, _("No")), (1, _("Number only")), (2, _("Number and list of likers")))
     )
     )
     can_like_posts = YesNoSwitch(
     can_like_posts = YesNoSwitch(
         label=_("Can like posts"),
         label=_("Can like posts"),
@@ -193,8 +173,7 @@ class CategoryPermissionsForm(forms.Form):
         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(
@@ -208,11 +187,7 @@ class CategoryPermissionsForm(forms.Form):
         label=_("Can hide events"),
         label=_("Can hide events"),
         coerce=int,
         coerce=int,
         initial=0,
         initial=0,
-        choices=(
-            (0, _("No")),
-            (1, _("Hide events")),
-            (2, _("Delete events"))
-        )
+        choices=((0, _("No")), (1, _("Hide events")), (2, _("Delete events")))
     )
     )
 
 
 
 
@@ -228,6 +203,8 @@ def change_permissions_form(role):
 """
 """
 ACL Builder
 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 +214,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 +230,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 +272,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,
@@ -324,6 +308,8 @@ def build_category_acl(acl, category, categories_roles, key_name):
 """
 """
 ACL's for targets
 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 +341,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,
@@ -480,11 +470,13 @@ def register_with(registry):
 """
 """
 ACL tests
 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 +490,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 +499,20 @@ 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 +520,10 @@ 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 +533,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 +542,7 @@ 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 +561,19 @@ 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 +584,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 +596,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 +621,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 +633,10 @@ 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 +658,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 +673,10 @@ 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 +698,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 +713,10 @@ 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 +738,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 +753,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 +770,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 +780,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 +789,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 +799,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 +812,14 @@ 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
 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
@@ -836,6 +855,8 @@ def has_time_to_edit_post(user, target):
 """
 """
 Queryset helpers
 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 = []

+ 10 - 7
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,10 @@ 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)
 
 

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

@@ -21,19 +21,8 @@ class AttachmentSerializer(serializers.ModelSerializer):
     class Meta:
     class Meta:
         model = Attachment
         model = Attachment
         fields = (
         fields = (
-            'id',
-            'filetype',
-            'post',
-            'uploaded_on',
-            'uploader_name',
-            'uploader_ip',
-            'filename',
-            'size',
-
-            'acl',
-            'is_image',
-
-            'url',
+            'id', 'filetype', 'post', 'uploaded_on', 'uploader_name', 'uploader_ip', 'filename',
+            'size', 'acl', 'is_image', 'url',
         )
         )
 
 
     def get_acl(self, obj):
     def get_acl(self, obj):
@@ -63,9 +52,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 - 17
misago/threads/serializers/feed.py

@@ -12,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):
@@ -31,18 +26,10 @@ 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 {
-            'title': obj.thread.title,
-            'url': obj.thread.get_absolute_url()
-        }
+        return {'title': obj.thread.title, 'url': obj.thread.get_absolute_url()}
 
 
     def get_top_category(self, obj):
     def get_top_category(self, obj):
         try:
         try:

+ 9 - 5
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,7 +70,7 @@ 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."))

+ 23 - 51
misago/threads/serializers/poll.py

@@ -28,21 +28,8 @@ class PollSerializer(serializers.ModelSerializer):
     class Meta:
     class Meta:
         model = Poll
         model = Poll
         fields = (
         fields = (
-            'id',
-            'poster_name',
-            'posted_on',
-            'length',
-            'question',
-            'allowed_choices',
-            'allow_revotes',
-            'votes',
-            'is_public',
-
-            'acl',
-            'choices',
-
-            'api',
-            'url',
+            'id', 'poster_name', 'posted_on', 'length', 'question', 'allowed_choices',
+            'allow_revotes', 'votes', 'is_public', 'acl', 'choices', 'api', 'url',
         )
         )
 
 
     def get_api(self, obj):
     def get_api(self, obj):
@@ -58,10 +45,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 +69,13 @@ 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 = (
-            'length',
-            'question',
-            'allowed_choices',
-            'allow_revotes',
-            'choices',
-        )
+        fields = ('length', 'question', 'allowed_choices', 'allow_revotes', '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,15 +88,10 @@ 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({
-                    'hash': get_random_string(12),
-                    'votes': 0
-                })
+                choice.update({'hash': get_random_string(12), 'votes': 0})
                 final_choices.append(choice)
                 final_choices.append(choice)
 
 
         self.validate_choices_num(final_choices)
         self.validate_choices_num(final_choices)
@@ -142,16 +120,18 @@ 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):
@@ -177,12 +157,7 @@ class NewPollSerializer(EditPollSerializer):
     class Meta:
     class Meta:
         model = Poll
         model = Poll
         fields = (
         fields = (
-            'length',
-            'question',
-            'allowed_choices',
-            'allow_revotes',
-            'is_public',
-            'choices',
+            'length', 'question', 'allowed_choices', 'allow_revotes', 'is_public', 'choices',
         )
         )
 
 
     def validate_choices(self, choices):
     def validate_choices(self, choices):
@@ -191,10 +166,7 @@ class NewPollSerializer(EditPollSerializer):
         self.validate_choices_num(clean_choices)
         self.validate_choices_num(clean_choices)
 
 
         for choice in clean_choices:
         for choice in clean_choices:
-            choice.update({
-                'hash': get_random_string(12),
-                'votes': 0
-            })
+            choice.update({'hash': get_random_string(12), 'votes': 0})
 
 
         return clean_choices
         return clean_choices
 
 

+ 7 - 11
misago/threads/serializers/pollvote.py

@@ -13,20 +13,16 @@ class PollVoteSerializer(serializers.Serializer):
     url = serializers.SerializerMethodField()
     url = serializers.SerializerMethodField()
 
 
     class Meta:
     class Meta:
-        fields = (
-            'voted_on',
-
-            'username',
-
-            'url',
-        )
+        fields = ('voted_on', 'username', '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'],
+                }
+            )

+ 10 - 12
misago/threads/serializers/post.py

@@ -9,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):
@@ -57,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',
         ]
         ]
@@ -151,18 +149,18 @@ 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

+ 7 - 14
misago/threads/serializers/postedit.py

@@ -17,16 +17,7 @@ class PostEditSerializer(serializers.ModelSerializer):
 
 
     class Meta:
     class Meta:
         model = PostEdit
         model = PostEdit
-        fields = (
-            'id',
-            'edited_on',
-            'editor_name',
-            'editor_slug',
-
-            'diff',
-
-            'url',
-        )
+        fields = ('id', 'edited_on', 'editor_name', 'editor_slug', 'diff', 'url', )
 
 
     def get_diff(self, obj):
     def get_diff(self, obj):
         return obj.get_diff()
         return obj.get_diff()
@@ -38,9 +29,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

+ 7 - 13
misago/threads/serializers/postlike.py

@@ -18,15 +18,7 @@ class PostLikeSerializer(serializers.ModelSerializer):
 
 
     class Meta:
     class Meta:
         model = PostLike
         model = PostLike
-        fields = (
-            'id',
-            'liked_on',
-
-            'liker_id',
-            'username',
-
-            'url',
-        )
+        fields = ('id', 'liked_on', 'liker_id', 'username', 'url', )
 
 
     def get_liker_id(self, obj):
     def get_liker_id(self, obj):
         return obj['liker_id']
         return obj['liker_id']
@@ -36,9 +28,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

+ 17 - 37
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):
@@ -38,30 +38,10 @@ class ThreadSerializer(serializers.ModelSerializer, MutableFields):
     class Meta:
     class Meta:
         model = Thread
         model = Thread
         fields = (
         fields = (
-            'id',
-            'category',
-            'title',
-            'replies',
-            'has_unapproved_posts',
-            'started_on',
-            'last_post_on',
-            'last_post_is_event',
-            'last_post',
-            'last_poster_name',
-            'is_unapproved',
-            'is_hidden',
-            'is_closed',
-            'weight',
-
-            'acl',
-            'is_new',
-            'is_read',
-            'path',
-            'poll',
-            'subscription',
-
-            'api',
-            'url',
+            'id', 'category', 'title', 'replies', 'has_unapproved_posts', 'started_on',
+            'last_post_on', 'last_post_is_event', 'last_post', 'last_poster_name', 'is_unapproved',
+            'is_hidden', 'is_closed', 'weight', 'acl', 'is_new', 'is_read', 'path', 'poll',
+            'subscription', 'api', 'url',
         )
         )
 
 
     def get_acl(self, obj):
     def get_acl(self, obj):
@@ -122,10 +102,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 +117,7 @@ class PrivateThreadSerializer(ThreadSerializer):
 
 
     class Meta:
     class Meta:
         model = Thread
         model = Thread
-        fields = ThreadSerializer.Meta.fields + (
-            'participants',
-        )
+        fields = ThreadSerializer.Meta.fields + ('participants', )
 
 
 
 
 class ThreadsListSerializer(ThreadSerializer):
 class ThreadsListSerializer(ThreadSerializer):
@@ -148,7 +128,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

+ 10 - 24
misago/threads/signals.py

@@ -17,11 +17,11 @@ merge_post = Signal(providing_args=["other_post"])
 merge_thread = Signal(providing_args=["other_thread"])
 merge_thread = Signal(providing_args=["other_thread"])
 move_post = Signal()
 move_post = Signal()
 move_thread = Signal()
 move_thread = Signal()
-
-
 """
 """
 Signal handlers
 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']
@@ -101,46 +101,32 @@ def delete_user_threads(sender, **kwargs):
 @receiver(username_changed)
 @receiver(username_changed)
 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_slug=sender.slug
+        starter_name=sender.username, 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_slug=sender.slug
+        last_poster_name=sender.username, 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_slug=sender.slug
+        last_editor_name=sender.username, last_editor_slug=sender.slug
     )
     )
 
 
     PostEdit.objects.filter(editor=sender).update(
     PostEdit.objects.filter(editor=sender).update(
-        editor_name=sender.username,
-        editor_slug=sender.slug
+        editor_name=sender.username, editor_slug=sender.slug
     )
     )
 
 
-    PostLike.objects.filter(liker=sender).update(
-        liker_name=sender.username,
-        liker_slug=sender.slug
-    )
+    PostLike.objects.filter(liker=sender).update(liker_name=sender.username, liker_slug=sender.slug)
 
 
     Attachment.objects.filter(uploader=sender).update(
     Attachment.objects.filter(uploader=sender).update(
-        uploader_name=sender.username,
-        uploader_slug=sender.slug
+        uploader_name=sender.username, uploader_slug=sender.slug
     )
     )
 
 
-    Poll.objects.filter(poster=sender).update(
-        poster_name=sender.username,
-        poster_slug=sender.slug
-    )
+    Poll.objects.filter(poster=sender).update(poster_name=sender.username, poster_slug=sender.slug)
 
 
-    PollVote.objects.filter(voter=sender).update(
-        voter_name=sender.username,
-        voter_slug=sender.slug
-    )
+    PollVote.objects.filter(voter=sender).update(voter_name=sender.username, voter_slug=sender.slug)
 
 
 
 
 @receiver(pre_delete, sender=get_user_model())
 @receiver(pre_delete, sender=get_user_model())

+ 1 - 3
misago/threads/subscriptions.py

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

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

@@ -28,22 +28,15 @@ 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.",
-        hidden_likes
+        "%(users)s and %(likes)s other users like this.", 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

+ 20 - 18
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()
 
 
@@ -57,7 +55,9 @@ class AttachmentAdminViewsTests(AdminTestCase):
         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())
@@ -78,10 +78,11 @@ class AttachmentAdminViewsTests(AdminTestCase):
         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)
@@ -93,14 +94,18 @@ class AttachmentAdminViewsTests(AdminTestCase):
     def test_delete_view(self):
     def test_delete_view(self):
         """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 = [
-            {'id': attachment.pk + 1},
-            {'id': attachment.pk},
-            {'id': attachment.pk + 2}
-        ]
+        self.post.attachments_cache = [{
+            '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 +119,4 @@ 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}])

+ 28 - 80
misago/threads/tests/test_attachments_api.py

@@ -43,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)
@@ -57,47 +55,33 @@ class AttachmentsApiTestCase(AuthenticatedUserTestCase):
 
 
     def test_invalid_extension(self):
     def test_invalid_extension(self):
         """uploaded file's extension is rejected as invalid"""
         """uploaded file's extension is rejected as invalid"""
-        AttachmentType.objects.create(
-            name="Test extension",
-            extensions='jpg,jpeg',
-            mimetypes=None
-        )
+        AttachmentType.objects.create(name="Test extension", extensions='jpg,jpeg', 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):
         """uploaded file's mimetype is rejected as invalid"""
         """uploaded file's mimetype is rejected as invalid"""
         AttachmentType.objects.create(
         AttachmentType.objects.create(
-            name="Test extension",
-            extensions='png',
-            mimetypes='loremipsum'
+            name="Test extension", extensions='png', 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):
         """user needs permission to upload files of this type"""
         """user needs permission to upload files of this type"""
         attachment_type = AttachmentType.objects.create(
         attachment_type = AttachmentType.objects.create(
-            name="Test extension",
-            extensions='png',
-            mimetypes='application/pdf'
+            name="Test extension", extensions='png', 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):
@@ -110,9 +94,7 @@ class AttachmentsApiTestCase(AuthenticatedUserTestCase):
         )
         )
 
 
         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):
@@ -125,70 +107,50 @@ class AttachmentsApiTestCase(AuthenticatedUserTestCase):
         )
         )
 
 
         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):
         """too big uploads are rejected"""
         """too big uploads are rejected"""
         AttachmentType.objects.create(
         AttachmentType.objects.create(
-            name="Test extension",
-            extensions='png',
-            mimetypes='image/png',
-            size_limit=100
+            name="Test extension", extensions='png', mimetypes='image/png', 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
-            })
+            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)
+        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",
-            extensions='png',
-            mimetypes='image/png'
+            name="Test extension", extensions='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"""
-        AttachmentType.objects.create(
-            name="Test extension",
-            extensions='gif'
-        )
+        AttachmentType.objects.create(name="Test extension", 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"""
         AttachmentType.objects.create(
         AttachmentType.objects.create(
-            name="Test extension",
-            extensions='pdf',
-            mimetypes='application/pdf'
+            name="Test extension", extensions='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 = response.json()
         response_json = response.json()
@@ -219,15 +181,11 @@ 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"""
         AttachmentType.objects.create(
         AttachmentType.objects.create(
-            name="Test extension",
-            extensions='jpeg,jpg',
-            mimetypes='image/jpeg'
+            name="Test extension", extensions='jpeg,jpg', 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 = response.json()
         response_json = response.json()
@@ -251,20 +209,14 @@ 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})
 
 
         AttachmentType.objects.create(
         AttachmentType.objects.create(
-            name="Test extension",
-            extensions='png',
-            mimetypes='image/png'
+            name="Test extension", extensions='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 = response.json()
         response_json = response.json()
@@ -307,15 +259,11 @@ 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"""
         AttachmentType.objects.create(
         AttachmentType.objects.create(
-            name="Test extension",
-            extensions='gif',
-            mimetypes='image/gif'
+            name="Test extension", extensions='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 = response.json()
         response_json = response.json()

+ 20 - 32
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,24 +49,17 @@ 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(
@@ -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(
@@ -105,30 +92,26 @@ class AttachmentsMiddlewareTests(AuthenticatedUserTestCase):
     def test_get_initial_attachments(self):
     def test_get_initial_attachments(self):
         """get_initial_attachments returns list of attachments already existing on post"""
         """get_initial_attachments returns list of attachments already existing on post"""
         middleware = AttachmentsMiddleware(
         middleware = AttachmentsMiddleware(
-            request=RequestMock(),
-            mode=PostingEndpoint.EDIT,
-            user=self.user,
-            post=self.post
+            request=RequestMock(), mode=PostingEndpoint.EDIT, user=self.user, 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):
         """get_initial_attachments returns list of attachments already existing on post"""
         """get_initial_attachments returns list of attachments already existing on post"""
         middleware = AttachmentsMiddleware(
         middleware = AttachmentsMiddleware(
-            request=RequestMock(),
-            mode=PostingEndpoint.EDIT,
-            user=self.user,
-            post=self.post
+            request=RequestMock(), mode=PostingEndpoint.EDIT, user=self.user, post=self.post
         )
         )
 
 
         serializer = middleware.get_serializer()
         serializer = middleware.get_serializer()
@@ -156,7 +139,9 @@ class AttachmentsMiddlewareTests(AuthenticatedUserTestCase):
         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
@@ -189,7 +174,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"""
@@ -218,7 +204,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"""
@@ -275,7 +262,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):

+ 82 - 61
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,22 @@ 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 +81,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 +111,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,24 +130,21 @@ 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', ['test']),
-            ('.test', ['test']),
-            ('.tar.gz', ['tar.gz']),
-            ('. test', ['test']),
-            ('test, test', ['test']),
-            ('test, tEst', ['test']),
-            ('test, other, tEst', ['test', 'other']),
-            ('test, other, tEst,OTher', ['test', 'other']),
-        )
+        TEST_CASES = (('test', ['test']), ('.test', ['test']), ('.tar.gz', ['tar.gz']),
+                      ('. test', ['test']), ('test, test', ['test']), ('test, tEst', ['test']),
+                      ('test, other, tEst', ['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 +152,22 @@ 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 +181,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 +204,9 @@ 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)

+ 19 - 26
misago/threads/tests/test_attachmentview.py

@@ -27,14 +27,8 @@ class AttachmentViewTestCase(AuthenticatedUserTestCase):
 
 
         self.api_link = reverse('misago:api:attachment-list')
         self.api_link = reverse('misago:api:attachment-list')
 
 
-        self.attachment_type_jpg = AttachmentType.objects.create(
-            name="JPG",
-            extensions='jpeg,jpg'
-        )
-        self.attachment_type_pdf = AttachmentType.objects.create(
-            name="PDF",
-            extensions='pdf'
-        )
+        self.attachment_type_jpg = AttachmentType.objects.create(name="JPG", extensions='jpeg,jpg')
+        self.attachment_type_pdf = AttachmentType.objects.create(name="PDF", extensions='pdf')
 
 
         self.override_acl()
         self.override_acl()
 
 
@@ -48,9 +42,7 @@ class AttachmentViewTestCase(AuthenticatedUserTestCase):
 
 
     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 +60,7 @@ 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 +84,10 @@ 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 +95,10 @@ 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 +125,13 @@ 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):

+ 11 - 31
misago/threads/tests/test_emailnotification_middleware.py

@@ -25,17 +25,13 @@ 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,
-            started_on=timezone.now() - timedelta(seconds=5)
+            category=self.category, 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)
@@ -60,17 +56,13 @@ class EmailNotificationTests(AuthenticatedUserTestCase):
         })
         })
 
 
         if hide:
         if hide:
-            new_acl['categories'][self.category.pk].update({
-                'can_browse': False
-            })
+            new_acl['categories'][self.category.pk].update({'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)
@@ -84,9 +76,7 @@ class EmailNotificationTests(AuthenticatedUserTestCase):
             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)
@@ -100,9 +90,7 @@ class EmailNotificationTests(AuthenticatedUserTestCase):
             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)
@@ -117,9 +105,7 @@ class EmailNotificationTests(AuthenticatedUserTestCase):
         )
         )
         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)
@@ -136,9 +122,7 @@ class EmailNotificationTests(AuthenticatedUserTestCase):
 
 
         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)
@@ -153,9 +137,7 @@ class EmailNotificationTests(AuthenticatedUserTestCase):
         )
         )
         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)
@@ -183,9 +165,7 @@ class EmailNotificationTests(AuthenticatedUserTestCase):
         )
         )
         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)

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

@@ -20,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()
 
 

+ 8 - 10
misago/threads/tests/test_floodprotection.py

@@ -17,9 +17,9 @@ 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,12 +34,10 @@ class PostMentionsTests(AuthenticatedUserTestCase):
 
 
     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
+        )

+ 1 - 3
misago/threads/tests/test_floodprotection_middleware.py

@@ -41,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())

+ 37 - 11
misago/threads/tests/test_gotoviews.py

@@ -25,7 +25,10 @@ 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())
@@ -49,7 +52,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(), 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())
@@ -65,7 +70,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())
@@ -87,7 +94,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,7 +107,10 @@ 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())
@@ -121,7 +133,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"""
@@ -150,7 +165,9 @@ 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_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"""
@@ -162,7 +179,9 @@ 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"""
@@ -173,7 +192,9 @@ 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)
+        )
 
 
 
 
 class GotoUnapprovedTests(GotoViewTestCase):
 class GotoUnapprovedTests(GotoViewTestCase):
@@ -197,7 +218,10 @@ 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"""
@@ -215,4 +239,6 @@ class GotoUnapprovedTests(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)
+        )

+ 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 = []

+ 36 - 42
misago/threads/tests/test_post_mentions.py

@@ -23,9 +23,9 @@ 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
@@ -45,9 +45,7 @@ class PostMentionsTests(AuthenticatedUserTestCase):
 
 
     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 +53,9 @@ 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 +63,9 @@ 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 +78,15 @@ 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 +99,9 @@ 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 +110,15 @@ 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 +126,7 @@ 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 +134,7 @@ 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 +145,9 @@ 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 +159,10 @@ 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()

+ 43 - 37
misago/threads/tests/test_post_model.py

@@ -72,49 +72,55 @@ class PostModelTests(TestCase):
 
 
         # can't merge with other users posts
         # can't merge with other users posts
         with self.assertRaises(ValueError):
         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),
-            ))
+            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"""

+ 243 - 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,39 @@ 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 +73,28 @@ 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 +102,15 @@ 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 +120,21 @@ 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 +143,29 @@ 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)
 
 
-        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 +186,15 @@ 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})
 
 
-        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 +211,15 @@ 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})
 
 
-        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 +239,13 @@ 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 +253,13 @@ 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 +267,13 @@ 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 +282,17 @@ 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 +302,17 @@ 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 +324,13 @@ 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 +362,13 @@ 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 +391,20 @@ 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 +431,13 @@ 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 +463,13 @@ 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 +494,13 @@ 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 +518,7 @@ 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 +526,13 @@ 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 +540,13 @@ 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 +555,30 @@ 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 +590,30 @@ 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 +633,20 @@ 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 +671,15 @@ 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 +705,15 @@ 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)
 
 

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

@@ -17,16 +17,15 @@ 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

+ 173 - 166
misago/threads/tests/test_privatethread_start_api.py

@@ -23,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"""
@@ -51,144 +52,145 @@ 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(), {
-            'to': [
-                "BobBoberson is blocking you."
-            ]
-        })
+        self.assertEqual(response.json(), {'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"""
@@ -196,51 +198,55 @@ 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"""
@@ -248,42 +254,48 @@ 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]
@@ -320,18 +332,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)
@@ -352,9 +356,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()

+ 2 - 9
misago/threads/tests/test_privatethreads.py

@@ -8,10 +8,7 @@ class PrivateThreadsTestCase(AuthenticatedUserTestCase):
         super(PrivateThreadsTestCase, self).setUp()
         super(PrivateThreadsTestCase, self).setUp()
         self.category = Category.objects.private_threads()
         self.category = Category.objects.private_threads()
 
 
-        override_acl(self.user, {
-            'can_use_private_threads': 1,
-            'can_start_private_threads': 1
-        })
+        override_acl(self.user, {'can_use_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):
@@ -32,8 +29,4 @@ class PrivateThreadsTestCase(AuthenticatedUserTestCase):
         if acl:
         if acl:
             final_acl.update(acl)
             final_acl.update(acl)
 
 
-        override_acl(self.user, {
-            'categories': {
-                self.category.pk: final_acl
-            }
-        })
+        override_acl(self.user, {'categories': {self.category.pk: final_acl}})

+ 20 - 37
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)
@@ -58,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)
@@ -87,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)
@@ -101,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)
@@ -125,15 +117,15 @@ 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'], [
-            {
+        self.assertEqual(
+            response_json['participants'], [{
                 'id': self.user.id,
                 'id': self.user.id,
                 'username': self.user.username,
                 'username': self.user.username,
                 'avatars': self.user.avatars,
                 'avatars': self.user.avatars,
                 'url': self.user.get_absolute_url(),
                 'url': self.user.get_absolute_url(),
                 'is_owner': True
                 '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"""
@@ -144,21 +136,19 @@ 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'], [
-            {
+        self.assertEqual(
+            response_json['participants'], [{
                 'id': self.user.id,
                 'id': self.user.id,
                 'username': self.user.username,
                 'username': self.user.username,
                 'avatars': self.user.avatars,
                 'avatars': self.user.avatars,
                 'url': self.user.get_absolute_url(),
                 'url': self.user.get_absolute_url(),
                 'is_owner': False
                 '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()
@@ -178,9 +168,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)
@@ -212,29 +200,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)

+ 2 - 6
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)
@@ -53,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)

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

@@ -82,8 +82,7 @@ class SearchApiTests(AuthenticatedUserTestCase):
     def test_hidden_post(self):
     def test_hidden_post(self):
         """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(
-            thread, message="Lorem ipsum dolor.", is_hidden=True)
+        post = testutils.reply_thread(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)
@@ -99,8 +98,7 @@ class SearchApiTests(AuthenticatedUserTestCase):
     def test_unapproved_post(self):
     def test_unapproved_post(self):
         """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(
-            thread, message="Lorem ipsum dolor.", is_unapproved=True)
+        post = testutils.reply_thread(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 +172,4 @@ 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'})

+ 30 - 33
misago/threads/tests/test_subscription_middleware.py

@@ -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,7 @@ 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 +113,7 @@ 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 +124,7 @@ 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 +138,7 @@ 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 +152,13 @@ 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

+ 4 - 13
misago/threads/tests/test_subscriptions.py

@@ -18,15 +18,11 @@ 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(
-            category=self.category,
-            started_on=datetime
-        )
+        return testutils.post_thread(category=self.category, started_on=datetime)
 
 
     def test_anon_subscription(self):
     def test_anon_subscription(self):
         """make single thread sub aware for anon"""
         """make single thread sub aware for anon"""
@@ -37,8 +33,7 @@ class SubscriptionsTests(TestCase):
         """make multiple threads list sub aware for anon"""
         """make multiple threads list sub aware for anon"""
         threads = []
         threads = []
         for _ in range(10):
         for _ in range(10):
-            threads.append(
-                self.post_thread(timezone.now() - timedelta(days=10)))
+            threads.append(self.post_thread(timezone.now() - timedelta(days=10)))
 
 
         make_subscription_aware(self.anon, threads)
         make_subscription_aware(self.anon, threads)
 
 
@@ -55,7 +50,6 @@ class SubscriptionsTests(TestCase):
         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,
         )
         )
@@ -67,14 +61,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,
                 )
                 )
@@ -82,7 +74,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)
 
 

+ 46 - 78
misago/threads/tests/test_thread_editreply_api.py

@@ -18,10 +18,11 @@ 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
@@ -65,70 +66,62 @@ 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()
@@ -137,9 +130,7 @@ class EditReplyTests(AuthenticatedUserTestCase):
         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)
@@ -151,11 +142,7 @@ 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(response.json(), {
-            '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):
         """events can't be edited"""
         """events can't be edited"""
@@ -172,25 +159,24 @@ 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(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_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)
 
 
         self.override_acl()
         self.override_acl()
@@ -211,9 +197,7 @@ 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)
 
 
         self.override_acl()
         self.override_acl()
@@ -239,36 +223,27 @@ 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(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
-        })
+        self.override_acl({'can_protect_posts': 1})
 
 
-        response = self.put(self.api_link, data={
-            'post': "Lorem ipsum dolor met!",
-            'protect': 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()
@@ -276,14 +251,9 @@ 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
-        })
+        self.override_acl({'can_protect_posts': 0})
 
 
-        response = self.put(self.api_link, data={
-            'post': "Lorem ipsum dolor met!",
-            'protect': 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()
@@ -293,7 +263,5 @@ 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)

+ 87 - 147
misago/threads/tests/test_thread_merge_api.py

@@ -15,7 +15,9 @@ 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})
@@ -45,56 +47,48 @@ 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, {
-            'thread_url': self.user.get_absolute_url()
-        })
+        response = self.client.post(self.api_link, {'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()
 
 
@@ -102,77 +96,59 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
         other_thread_url = other_thread.get_absolute_url()
         other_thread_url = other_thread.get_absolute_url()
         other_thread.delete()
         other_thread.delete()
 
 
-        response = self.client.post(self.api_link, {
-            'thread_url': other_thread_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_url})
+        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
@@ -184,20 +160,14 @@ 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
@@ -213,20 +183,14 @@ 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
@@ -242,29 +206,23 @@ 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)
@@ -272,27 +230,21 @@ 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)
 
 
         testutils.post_poll(self.thread, self.user)
         testutils.post_poll(self.thread, self.user)
         testutils.post_poll(other_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'
-        })
+        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)
@@ -300,22 +252,18 @@ 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)
         testutils.post_poll(self.thread, self.user)
         testutils.post_poll(self.thread, self.user)
         testutils.post_poll(other_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
-        })
+        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
@@ -331,22 +279,18 @@ 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
@@ -369,22 +313,18 @@ 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

+ 9 - 12
misago/threads/tests/test_thread_model.py

@@ -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)
 
 
@@ -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,
@@ -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)
 
 
@@ -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)
 
 
@@ -293,7 +290,9 @@ 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)
@@ -352,10 +351,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)

+ 326 - 285
misago/threads/tests/test_thread_patch_api.py

@@ -11,16 +11,13 @@ 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, [
-            {'op': 'add', 'path': 'acl', 'value': True}
-        ])
+        response = self.patch(self.api_link, [{'op': 'add', 'path': 'acl', 'value': True}])
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         response_json = response.json()
         response_json = response.json()
@@ -28,9 +25,7 @@ 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, [
-            {'op': 'add', 'path': 'acl', 'value': False}
-        ])
+        response = self.patch(self.api_link, [{'op': 'add', 'path': 'acl', 'value': False}])
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         response_json = response.json()
         response_json = response.json()
@@ -40,13 +35,15 @@ 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 +51,62 @@ 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
-        })
+        self.override_acl({'can_edit_threads': 2})
 
 
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'title', 'value': 12}
-        ])
+        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
-        })
+        self.override_acl({'can_pin_threads': 2})
 
 
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'weight', 'value': 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 +120,9 @@ 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
-        })
+        self.override_acl({'can_pin_threads': 2})
 
 
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'weight', 'value': 0}
-        ])
+        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 +130,15 @@ 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
-        })
+        self.override_acl({'can_pin_threads': 1})
 
 
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'weight', 'value': 2}
-        ])
+        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 +151,15 @@ 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
-        })
+        self.override_acl({'can_pin_threads': 1})
 
 
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'weight', 'value': 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 +168,9 @@ 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
-        })
+        self.override_acl({'can_pin_threads': 1})
 
 
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'weight', 'value': 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 +184,9 @@ 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
-        })
+        self.override_acl({'can_pin_threads': 1})
 
 
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'weight', 'value': 0}
-        ])
+        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 +194,15 @@ 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
-        })
+        self.override_acl({'can_pin_threads': 0})
 
 
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'weight', 'value': 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'], 0)
         self.assertEqual(thread_json['weight'], 0)
@@ -242,18 +215,15 @@ 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
-        })
+        self.override_acl({'can_pin_threads': 0})
 
 
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'weight', 'value': 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 +236,9 @@ 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 +260,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 +304,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 +339,22 @@ 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 +363,16 @@ 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 +385,23 @@ 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 +410,22 @@ 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 +434,13 @@ 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 +453,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 +477,15 @@ 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 +499,15 @@ 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
-        })
+        self.override_acl({'can_close_threads': True})
 
 
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'is-closed', 'value': False}
-        ])
+        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 +515,21 @@ 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 +542,21 @@ 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
-        })
+        self.override_acl({'can_close_threads': False})
 
 
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'is-closed', 'value': 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 +568,15 @@ 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
-        })
+        self.override_acl({'can_approve_content': 1})
 
 
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'is-unapproved', 'value': False}
-        ])
+        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 +584,36 @@ 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 +623,44 @@ 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
-        })
+        self.override_acl({'can_hide_threads': 1})
 
 
-        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, 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 +670,33 @@ 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
-        })
+        self.override_acl({'can_hide_threads': 0})
 
 
-        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, 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 +708,13 @@ 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 +726,13 @@ 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 +745,28 @@ 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)

+ 6 - 7
misago/threads/tests/test_thread_poll_api.py

@@ -16,9 +16,7 @@ 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')
@@ -52,7 +50,8 @@ 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}
+        )

+ 86 - 125
misago/threads/tests/test_thread_pollcreate_api.py

@@ -16,36 +16,28 @@ 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 +45,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 +60,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 +75,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,7 +90,9 @@ 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'}],
+            choices=[{
+                'hash': 't3st'
+            }],
             allowed_choices=1
             allowed_choices=1
         )
         )
 
 
@@ -125,156 +109,133 @@ 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': [
-                {
+        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'
-                },
-                {
+                }, {
                     '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': [
-                {
+        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': '\nRed '
-                },
-                {
+                }, {
                     'label': 'Green'
                     'label': 'Green'
-                },
-                {
+                }, {
                     'label': 'Blue'
                     'label': 'Blue'
-                }
-            ]
-        })
+                }]
+            }
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         response_json = response.json()
         response_json = response.json()

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

@@ -23,71 +23,70 @@ 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 +97,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 +107,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 +115,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 +130,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 +151,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)

+ 172 - 208
misago/threads/tests/test_thread_polledit_api.py

@@ -23,71 +23,70 @@ 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 +97,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 +107,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 +115,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 +130,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 +145,133 @@ 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': [
-                {
+        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'
-                },
-                {
+                }, {
                     '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': [
-                {
+        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': '\nRed  '
-                },
-                {
+                }, {
                     'label': 'Green'
                     'label': 'Green'
-                },
-                {
+                }, {
                     'label': 'Blue'
                     'label': 'Blue'
-                }
-            ]
-        })
+                }]
+            }
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         response_json = response.json()
         response_json = response.json()
@@ -332,35 +298,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': [
-                {
+        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',
                     'hash': 'aaaaaaaaaaaa',
                     'label': '\nFirst  ',
                     'label': '\nFirst  ',
                     'votes': 5555
                     'votes': 5555
-                },
-                {
+                }, {
                     'hash': 'bbbbbbbbbbbb',
                     'hash': 'bbbbbbbbbbbb',
                     'label': 'Second',
                     'label': 'Second',
                     'votes': 5555
                     'votes': 5555
-                },
-                {
+                }, {
                     'hash': 'gggggggggggg',
                     'hash': 'gggggggggggg',
                     'label': 'Third',
                     'label': 'Third',
                     'votes': 5555
                     'votes': 5555
-                },
-                {
+                }, {
                     'hash': 'dddddddddddd',
                     'hash': 'dddddddddddd',
                     'label': 'Fourth',
                     'label': 'Fourth',
                     'votes': 5555
                     'votes': 5555
-                }
-            ]
-        })
+                }]
+            }
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         response_json = response.json()
         response_json = response.json()
@@ -376,32 +345,29 @@ 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'], [
-            {
+        self.assertEqual(
+            response_json['choices'], [{
                 'hash': 'aaaaaaaaaaaa',
                 'hash': 'aaaaaaaaaaaa',
                 'label': 'First',
                 'label': 'First',
                 'votes': 1,
                 'votes': 1,
                 'selected': False
                 'selected': False
-            },
-            {
+            }, {
                 'hash': 'bbbbbbbbbbbb',
                 'hash': 'bbbbbbbbbbbb',
                 'label': 'Second',
                 'label': 'Second',
                 'votes': 0,
                 'votes': 0,
                 'selected': False
                 'selected': False
-            },
-            {
+            }, {
                 'hash': 'gggggggggggg',
                 'hash': 'gggggggggggg',
                 'label': 'Third',
                 'label': 'Third',
                 'votes': 2,
                 'votes': 2,
                 'selected': True
                 'selected': True
-            },
-            {
+            }, {
                 'hash': 'dddddddddddd',
                 'hash': 'dddddddddddd',
                 'label': 'Fourth',
                 'label': 'Fourth',
                 'votes': 1,
                 'votes': 1,
                 'selected': True
                 'selected': True
-            }
-        ])
+            }]
+        )
 
 
         # no votes were removed
         # no votes were removed
         self.assertEqual(response_json['votes'], 4)
         self.assertEqual(response_json['votes'], 4)
@@ -409,30 +375,34 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
 
 
     def test_poll_some_choices_edited(self):
     def test_poll_some_choices_edited(self):
         """api edits some poll choices"""
         """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': [
-                {
+        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',
                     'hash': 'aaaaaaaaaaaa',
                     'label': '\nFirst ',
                     'label': '\nFirst ',
                     'votes': 5555
                     'votes': 5555
-                },
-                {
+                }, {
                     'hash': 'bbbbbbbbbbbb',
                     'hash': 'bbbbbbbbbbbb',
                     'label': 'Second',
                     'label': 'Second',
                     'votes': 5555
                     'votes': 5555
-                },
-                {
+                }, {
                     'hash': 'dsadsadsa788',
                     'hash': 'dsadsadsa788',
                     'label': 'New Option',
                     'label': 'New Option',
                     'votes': 5555
                     'votes': 5555
-                }
-            ]
-        })
+                }]
+            }
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         response_json = response.json()
         response_json = response.json()
@@ -448,26 +418,24 @@ 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'], [
-            {
+        self.assertEqual(
+            response_json['choices'], [{
                 'hash': 'aaaaaaaaaaaa',
                 'hash': 'aaaaaaaaaaaa',
                 'label': 'First',
                 'label': 'First',
                 'votes': 1,
                 'votes': 1,
                 'selected': False
                 'selected': False
-            },
-            {
+            }, {
                 'hash': 'bbbbbbbbbbbb',
                 'hash': 'bbbbbbbbbbbb',
                 'label': 'Second',
                 'label': 'Second',
                 'votes': 0,
                 'votes': 0,
                 'selected': False
                 'selected': False
-            },
-            {
+            }, {
                 'hash': response_json['choices'][2]['hash'],
                 'hash': response_json['choices'][2]['hash'],
                 'label': 'New Option',
                 'label': 'New Option',
                 'votes': 0,
                 'votes': 0,
                 'selected': False
                 'selected': False
-            }
-        ])
+            }]
+        )
 
 
         # no votes were removed
         # no votes were removed
         self.assertEqual(response_json['votes'], 1)
         self.assertEqual(response_json['votes'], 1)
@@ -475,34 +443,30 @@ 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': [
-                {
+        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': '\nRed  '
-                },
-                {
+                }, {
                     'label': 'Green'
                     'label': 'Green'
-                },
-                {
+                }, {
                     'label': 'Blue'
                     'label': 'Blue'
-                }
-            ]
-        })
+                }]
+            }
+        )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         response_json = response.json()
         response_json = response.json()

+ 47 - 49
misago/threads/tests/test_thread_pollvotes_api.py

@@ -21,10 +21,11 @@ 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 +36,51 @@ 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()
@@ -111,14 +114,12 @@ class ThreadGetVotesTests(ThreadPollApiTestCase):
 
 
         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()
@@ -137,8 +138,8 @@ class ThreadGetVotesTests(ThreadPollApiTestCase):
 
 
         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 +148,11 @@ 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 +197,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 +213,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 +223,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 +240,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 +283,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 +310,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'])

+ 33 - 57
misago/threads/tests/test_thread_postdelete_api.py

@@ -15,10 +15,11 @@ 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,28 +30,22 @@ 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)
 
 
     def test_delete_other_user_post_no_permission(self):
     def test_delete_other_user_post_no_permission(self):
         """api valdiates if user can delete other users posts"""
         """api valdiates if user can delete other users posts"""
-        self.override_acl({
-            'post_edit_time': 0,
-            'can_hide_own_posts': 2,
-            'can_hide_posts': 0
-        })
+        self.override_acl({'post_edit_time': 0, 'can_hide_own_posts': 2, 'can_hide_posts': 0})
 
 
         self.post.poster = None
         self.post.poster = None
         self.post.save()
         self.post.save()
 
 
         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 +60,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 +76,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 +92,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,31 +108,25 @@ 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)
 
 
     def test_delete_event(self):
     def test_delete_event(self):
         """api differs posts from events"""
         """api differs posts from events"""
-        self.override_acl({
-            'can_hide_own_posts': 2,
-            'can_hide_posts': 2,
-
-            'can_hide_events': 0
-        })
+        self.override_acl({'can_hide_own_posts': 2, 'can_hide_posts': 2, 'can_hide_events': 0})
 
 
         self.post.is_event = True
         self.post.is_event = True
         self.post.save()
         self.post.save()
@@ -144,11 +136,7 @@ class PostDeleteApiTests(ThreadsApiTestCase):
 
 
     def test_delete_owned_post(self):
     def test_delete_owned_post(self):
         """api deletes owned thread post"""
         """api deletes owned thread post"""
-        self.override_acl({
-            'post_edit_time': 0,
-            'can_hide_own_posts': 2,
-            'can_hide_posts': 0
-        })
+        self.override_acl({'post_edit_time': 0, 'can_hide_own_posts': 2, 'can_hide_posts': 0})
 
 
         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)
@@ -161,10 +149,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 +167,11 @@ 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"""
@@ -196,24 +182,14 @@ class EventDeleteApiTests(ThreadsApiTestCase):
 
 
     def test_no_permission(self):
     def test_no_permission(self):
         """api validates permission to delete event"""
         """api validates permission to delete event"""
-        self.override_acl({
-            'can_hide_own_posts': 2,
-            'can_hide_posts': 2,
-
-            'can_hide_events': 0
-        })
+        self.override_acl({'can_hide_own_posts': 2, 'can_hide_posts': 2, 'can_hide_events': 0})
 
 
         response = self.client.delete(self.api_link)
         response = self.client.delete(self.api_link)
         self.assertContains(response, "You can't delete events in this category.", status_code=403)
         self.assertContains(response, "You can't delete events in this category.", status_code=403)
 
 
     def test_delete_event(self):
     def test_delete_event(self):
         """api differs posts from events"""
         """api differs posts from events"""
-        self.override_acl({
-            'can_hide_own_posts': 0,
-            'can_hide_posts': 0,
-
-            'can_hide_events': 2
-        })
+        self.override_acl({'can_hide_own_posts': 0, 'can_hide_posts': 0, 'can_hide_events': 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)

+ 9 - 14
misago/threads/tests/test_thread_postedits_api.py

@@ -14,10 +14,11 @@ 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()
 
 
@@ -32,8 +33,7 @@ class ThreadPostEditsApiTestCase(ThreadsApiTestCase):
                 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,
                 thread=self.thread,
                 thread=self.thread,
                 editor_name='Deleted',
                 editor_name='Deleted',
@@ -41,8 +41,7 @@ class ThreadPostEditsApiTestCase(ThreadsApiTestCase):
                 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,
                 thread=self.thread,
                 thread=self.thread,
                 editor=self.user,
                 editor=self.user,
@@ -132,9 +131,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"""
@@ -160,9 +157,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)

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

@@ -12,25 +12,22 @@ 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 +48,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,
+            ]
+        )

+ 186 - 112
misago/threads/tests/test_thread_postmerge_api.py

@@ -21,9 +21,9 @@ 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()
 
 
@@ -56,9 +56,7 @@ class ThreadPostMergeApiTestCase(AuthenticatedUserTestCase):
 
 
     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)
@@ -72,12 +70,12 @@ class ThreadPostMergeApiTestCase(AuthenticatedUserTestCase):
         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")
         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)
+        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"""
@@ -88,134 +86,200 @@ class ThreadPostMergeApiTestCase(AuthenticatedUserTestCase):
         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")
         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)
+        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")
         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)
+        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"""
@@ -224,9 +288,13 @@ class ThreadPostMergeApiTestCase(AuthenticatedUserTestCase):
 
 
         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()
@@ -240,30 +308,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=self.user, is_hidden=True).pk,
-                testutils.reply_thread(self.thread, poster=self.user, 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=self.user, is_unapproved=True).pk,
-                testutils.reply_thread(self.thread, poster=self.user, 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):
@@ -274,11 +346,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)

+ 138 - 104
misago/threads/tests/test_thread_postmove_api.py

@@ -20,14 +20,14 @@ 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()
@@ -75,10 +75,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"""
@@ -89,9 +91,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)
@@ -103,17 +103,15 @@ 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, {
-            'thread_url': self.user.get_absolute_url()
-        })
+        response = self.client.post(self.api_link, {'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"""
@@ -123,168 +121,204 @@ class ThreadPostMoveApiTestCase(AuthenticatedUserTestCase):
         other_thread_url = other_thread.get_absolute_url()
         other_thread_url = other_thread.get_absolute_url()
         other_thread.delete()
         other_thread.delete()
 
 
-        response = self.client.post(self.api_link, {
-            'thread_url': other_thread_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_url})
+        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

+ 348 - 290
misago/threads/tests/test_thread_postpatch_api.py

@@ -22,10 +22,11 @@ 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")
@@ -52,9 +53,7 @@ class ThreadPostPatchApiTestCase(AuthenticatedUserTestCase):
 class PostAddAclApiTests(ThreadPostPatchApiTestCase):
 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, [
-            {'op': 'add', 'path': 'acl', 'value': True}
-        ])
+        response = self.patch(self.api_link, [{'op': 'add', 'path': 'acl', 'value': True}])
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         response_json = response.json()
         response_json = response.json()
@@ -62,30 +61,28 @@ 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, [
-            {'op': 'add', 'path': 'acl', 'value': False}
-        ])
+        response = self.patch(self.api_link, [{'op': 'add', 'path': 'acl', 'value': False}])
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         response_json = response.json()
         response_json = response.json()
         self.assertIsNone(response_json['acl'])
         self.assertIsNone(response_json['acl'])
 
 
-        response = self.patch(self.api_link, [
-            {'op': 'add', 'path': 'acl', 'value': True}
-        ])
+        response = self.patch(self.api_link, [{'op': 'add', 'path': 'acl', 'value': True}])
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
 
 
 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 +93,15 @@ 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
-        })
+        self.override_acl({'can_protect_posts': 1})
 
 
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'is-protected', 'value': False}
-        ])
+        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 +109,15 @@ 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 +131,15 @@ 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
-        })
+        self.override_acl({'can_protect_posts': 0})
 
 
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'is-protected', 'value': False}
-        ])
+        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 +150,15 @@ 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 +174,15 @@ 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
-        })
+        self.override_acl({'can_approve_content': 1})
 
 
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'is-unapproved', 'value': False}
-        ])
+        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 +190,15 @@ 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 +209,15 @@ 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
-        })
+        self.override_acl({'can_approve_content': 0})
 
 
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'is-unapproved', 'value': False}
-        ])
+        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 +234,15 @@ 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
-        })
+        self.override_acl({'can_approve_content': 1})
 
 
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'is-unapproved', 'value': False}
-        ])
+        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 +257,21 @@ 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
-        })
+        self.override_acl({'can_approve_content': 1})
 
 
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'is-unapproved', 'value': False}
-        ])
+        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 +280,15 @@ 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 +302,15 @@ 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
-        })
+        self.override_acl({'can_hide_posts': 1})
 
 
-        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, 200)
         self.assertEqual(response.status_code, 200)
 
 
         self.refresh_post()
         self.refresh_post()
@@ -298,13 +318,15 @@ 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 +340,15 @@ 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
-        })
+        self.override_acl({'can_hide_own_posts': 1})
 
 
-        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, 200)
         self.assertEqual(response.status_code, 200)
 
 
         self.refresh_post()
         self.refresh_post()
@@ -332,13 +356,15 @@ 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 +381,15 @@ 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
-        })
+        self.override_acl({'can_hide_posts': 0})
 
 
-        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()
@@ -375,14 +403,15 @@ 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
-        })
+        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}
-        ])
+        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,17 +425,18 @@ 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()
@@ -420,17 +450,21 @@ 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
-        })
+        self.override_acl({'can_hide_own_posts': 1})
 
 
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'is-hidden', 'value': True}
-        ])
+        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 +475,21 @@ 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
-        })
+        self.override_acl({'can_hide_own_posts': 1})
 
 
-        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], "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 +499,21 @@ 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
-        })
+        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}
-        ])
+        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 +524,21 @@ 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
-        })
+        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}
-        ])
+        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 +548,21 @@ 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
-        })
+        self.override_acl({'can_hide_own_posts': 1})
 
 
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'is-hidden', 'value': True}
-        ])
+        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 +575,21 @@ 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
-        })
+        self.override_acl({'can_hide_own_posts': 1})
 
 
-        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 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 +599,21 @@ 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
-        })
+        self.override_acl({'can_hide_own_posts': 1})
 
 
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'is-hidden', 'value': True}
-        ])
+        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 +626,21 @@ 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
-        })
+        self.override_acl({'can_hide_own_posts': 1})
 
 
-        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 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 +650,15 @@ 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
-        })
+        self.override_acl({'can_hide_posts': 1})
 
 
-        response = self.patch(self.api_link, [
-            {'op': 'replace', 'path': 'is-hidden', 'value': True}
-        ])
+        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 +669,15 @@ 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
-        })
+        self.override_acl({'can_hide_posts': 1})
 
 
-        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()
@@ -623,42 +687,32 @@ 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
-        })
+        self.override_acl({'can_see_posts_likes': 0})
 
 
-        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.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
-        })
+        self.override_acl({'can_like_posts': False})
 
 
-        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.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'], [
-            {
+        self.assertEqual(
+            response_json['last_likes'], [{
                 'id': self.user.id,
                 'id': self.user.id,
                 'username': self.user.username
                 '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 +725,27 @@ 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'], [
-            {
+        self.assertEqual(
+            response_json['last_likes'], [{
                 'id': self.user.id,
                 'id': self.user.id,
                 'username': self.user.username
                 'username': self.user.username
-            },
-            {
+            }, {
                 'id': None,
                 'id': None,
                 'username': 'Miku'
                 'username': 'Miku'
-            },
-            {
+            }, {
                 'id': None,
                 'id': None,
                 'username': 'Bob'
                 'username': 'Bob'
-            },
-            {
+            }, {
                 'id': None,
                 'id': None,
                 'username': 'Mugi'
                 '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 +755,13 @@ 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 +777,18 @@ 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'], [
-            {
+        self.assertEqual(
+            response_json['last_likes'], [{
                 'id': self.user.id,
                 'id': self.user.id,
                 'username': self.user.username
                 '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 +796,13 @@ 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 +817,11 @@ 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)
@@ -776,18 +832,14 @@ class EventAnonPatchApiTests(ThreadEventPatchApiTestCase):
         """anonymous users can't change event state"""
         """anonymous users can't change event state"""
         self.logout_user()
         self.logout_user()
 
 
-        response = self.patch(self.api_link, [
-            {'op': 'add', 'path': 'acl', 'value': True}
-        ])
+        response = self.patch(self.api_link, [{'op': 'add', 'path': 'acl', 'value': True}])
         self.assertEqual(response.status_code, 403)
         self.assertEqual(response.status_code, 403)
 
 
 
 
 class EventAddAclApiTests(ThreadEventPatchApiTestCase):
 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, [
-            {'op': 'add', 'path': 'acl', 'value': True}
-        ])
+        response = self.patch(self.api_link, [{'op': 'add', 'path': 'acl', 'value': True}])
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         response_json = response.json()
         response_json = response.json()
@@ -795,30 +847,28 @@ 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, [
-            {'op': 'add', 'path': 'acl', 'value': False}
-        ])
+        response = self.patch(self.api_link, [{'op': 'add', 'path': 'acl', 'value': False}])
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         response_json = response.json()
         response_json = response.json()
         self.assertIsNone(response_json['acl'])
         self.assertIsNone(response_json['acl'])
 
 
-        response = self.patch(self.api_link, [
-            {'op': 'add', 'path': 'acl', 'value': True}
-        ])
+        response = self.patch(self.api_link, [{'op': 'add', 'path': 'acl', 'value': True}])
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
 
 
 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 +882,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': 1
-        })
+        self.override_acl({'can_hide_events': 1})
 
 
-        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, 200)
         self.assertEqual(response.status_code, 200)
 
 
         self.refresh_event()
         self.refresh_event()
@@ -846,17 +898,21 @@ 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 +925,13 @@ 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
-        })
+        self.override_acl({'can_hide_events': 0})
 
 
-        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, 404)
         self.assertEqual(response.status_code, 404)

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

@@ -10,16 +10,12 @@ class PostReadApiTests(ThreadsApiTestCase):
     def setUp(self):
     def setUp(self):
         super(PostReadApiTests, self).setUp()
         super(PostReadApiTests, self).setUp()
 
 
-        self.post = testutils.reply_thread(
-            self.thread,
-            poster=self.user,
-            posted_on=timezone.now()
-        )
+        self.post = testutils.reply_thread(self.thread, poster=self.user, 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"""

+ 301 - 219
misago/threads/tests/test_thread_postsplit_api.py

@@ -20,18 +20,19 @@ 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()
@@ -79,10 +80,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"""
@@ -93,9 +96,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)
@@ -103,319 +104,396 @@ 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 = 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_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 = 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_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 = 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_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 = 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_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 = 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_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 = 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_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 = 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_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 = 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_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 = 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_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 = 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_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 = 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_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 = 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_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 = 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_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
@@ -441,14 +519,18 @@ class ThreadPostSplitApiTestCase(AuthenticatedUserTestCase):
             '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

+ 29 - 40
misago/threads/tests/test_thread_reply_api.py

@@ -17,9 +17,7 @@ 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
@@ -58,49 +56,45 @@ 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)
@@ -111,33 +105,28 @@ 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(response.json(), {
-            '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(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_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)
@@ -175,7 +164,7 @@ 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)

+ 135 - 111
misago/threads/tests/test_thread_start_api.py

@@ -50,9 +50,7 @@ class StartThreadTests(AuthenticatedUserTestCase):
         """has no permission to see selected category"""
         """has no permission to see selected category"""
         self.override_acl({'can_see': 0})
         self.override_acl({'can_see': 0})
 
 
-        response = self.client.post(self.api_link, {
-            'category': self.category.pk
-        })
+        response = self.client.post(self.api_link, {'category': self.category.pk})
 
 
         self.assertContains(response, "Selected category is invalid.", status_code=400)
         self.assertContains(response, "Selected category is invalid.", status_code=400)
 
 
@@ -60,9 +58,7 @@ class StartThreadTests(AuthenticatedUserTestCase):
         """has no permission to browse selected category"""
         """has no permission to browse selected category"""
         self.override_acl({'can_browse': 0})
         self.override_acl({'can_browse': 0})
 
 
-        response = self.client.post(self.api_link, {
-            'category': self.category.pk
-        })
+        response = self.client.post(self.api_link, {'category': self.category.pk})
 
 
         self.assertContains(response, "Selected category is invalid.", status_code=400)
         self.assertContains(response, "Selected category is invalid.", status_code=400)
 
 
@@ -70,11 +66,11 @@ class StartThreadTests(AuthenticatedUserTestCase):
         """permission to start thread in category is validated"""
         """permission to start thread in category is validated"""
         self.override_acl({'can_start_threads': 0})
         self.override_acl({'can_start_threads': 0})
 
 
-        response = self.client.post(self.api_link, {
-            'category': self.category.pk
-        })
+        response = self.client.post(self.api_link, {'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"""
@@ -83,9 +79,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
-        })
+        response = self.client.post(self.api_link, {'category': self.category.pk})
 
 
         self.assertContains(response, "This category is closed.", status_code=400)
         self.assertContains(response, "This category is closed.", status_code=400)
 
 
@@ -96,9 +90,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)
 
 
@@ -108,60 +100,62 @@ 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]
@@ -210,12 +204,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]
@@ -225,12 +222,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]
@@ -240,12 +240,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]
@@ -255,12 +258,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]
@@ -270,12 +276,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]
@@ -285,12 +294,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]
@@ -300,12 +312,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]
@@ -315,12 +330,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]
@@ -333,12 +351,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]
@@ -348,9 +369,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)

+ 31 - 50
misago/threads/tests/test_threads_api.py

@@ -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,35 +119,31 @@ 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)
@@ -160,9 +152,7 @@ class ThreadRetrieveApiTests(ThreadsApiTestCase):
         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 +179,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 +215,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)

+ 171 - 190
misago/threads/tests/test_threads_editor_api.py

@@ -59,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):
@@ -123,16 +125,18 @@ class ThreadPostEditorApiTests(EditorApiTestCase):
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         response_json = response.json()
         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
+        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"""
@@ -144,16 +148,18 @@ class ThreadPostEditorApiTests(EditorApiTestCase):
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         response_json = response.json()
         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
+        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"""
@@ -166,16 +172,18 @@ class ThreadPostEditorApiTests(EditorApiTestCase):
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         response_json = response.json()
         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
+        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"""
@@ -188,16 +196,18 @@ class ThreadPostEditorApiTests(EditorApiTestCase):
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         response_json = response.json()
         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
+        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"""
@@ -210,16 +220,18 @@ class ThreadPostEditorApiTests(EditorApiTestCase):
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         response_json = response.json()
         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
+        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"""
@@ -232,16 +244,18 @@ class ThreadPostEditorApiTests(EditorApiTestCase):
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         response_json = response.json()
         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.assertEqual(
+            response_json[0], {
+                'id': self.category.pk,
+                'name': self.category.name,
+                'level': 0,
+                'post': {
+                    'close': 0,
+                    'hide': 1,
+                    'pin': 0
+                }
             }
             }
-        })
+        )
 
 
         self.override_acl({
         self.override_acl({
             'can_start_threads': 2,
             'can_start_threads': 2,
@@ -252,16 +266,18 @@ class ThreadPostEditorApiTests(EditorApiTestCase):
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         response_json = response.json()
         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
+        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):
@@ -269,9 +285,9 @@ 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"""
@@ -296,71 +312,59 @@ 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)
@@ -369,9 +373,7 @@ class ThreadReplyEditorApiTests(EditorApiTestCase):
         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)
 
 
@@ -388,9 +390,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)
 
 
@@ -400,20 +400,19 @@ 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(response.json(), {
-            '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):
@@ -423,10 +422,11 @@ 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"""
@@ -451,63 +451,50 @@ 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()
@@ -516,56 +503,42 @@ class EditReplyEditorApiTests(EditorApiTestCase):
         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)
@@ -587,11 +560,13 @@ class EditReplyEditorApiTests(EditorApiTestCase):
             'can_edit_posts': 1,
             '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({
         self.override_acl({
@@ -603,20 +578,18 @@ class EditReplyEditorApiTests(EditorApiTestCase):
 
 
     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)
@@ -629,9 +602,9 @@ class EditReplyEditorApiTests(EditorApiTestCase):
             })
             })
 
 
             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'))
@@ -652,15 +625,23 @@ class EditReplyEditorApiTests(EditorApiTestCase):
             add_acl(self.user, attachment)
             add_acl(self.user, attachment)
 
 
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
-        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,
-            ]
-        })
+        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,
+                ]
+            }
+        )

+ 370 - 258
misago/threads/tests/test_threads_merge_api.py

@@ -20,7 +20,9 @@ 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):
@@ -29,110 +31,128 @@ 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"""
         testutils.post_thread(category=self.category_b)
         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")
+        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"""
@@ -147,15 +167,18 @@ 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"""
@@ -168,16 +191,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"""
@@ -190,17 +219,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],
-            '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"""
@@ -213,17 +247,19 @@ 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"""
@@ -237,19 +273,21 @@ 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
-        }), 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"""
@@ -262,18 +300,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],
-            '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"""
@@ -286,20 +328,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.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"""
@@ -312,20 +357,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.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"""
@@ -339,18 +387,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,
-            '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"""
@@ -364,18 +417,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,
-            '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"""
@@ -388,20 +446,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.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"""
@@ -414,19 +475,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': 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"""
@@ -440,20 +506,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.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"""
@@ -467,19 +536,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': 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"""
@@ -494,11 +568,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?
@@ -534,14 +612,18 @@ 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,
-            '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?
@@ -581,12 +663,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?
@@ -618,11 +704,15 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         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()
@@ -644,11 +734,15 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         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()
@@ -671,20 +765,24 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         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)
@@ -701,17 +799,19 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         testutils.post_poll(self.thread, self.user)
         testutils.post_poll(self.thread, self.user)
         testutils.post_poll(other_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")
+        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(), {
-            '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)
@@ -728,12 +828,16 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         testutils.post_poll(self.thread, self.user)
         testutils.post_poll(self.thread, self.user)
         testutils.post_poll(other_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")
+        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
@@ -750,12 +854,16 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         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
@@ -776,12 +884,16 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         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

+ 8 - 6
misago/threads/tests/test_threads_moderation.py

@@ -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!")
@@ -139,12 +141,13 @@ 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)

+ 149 - 205
misago/threads/tests/test_threadslists.py

@@ -13,13 +13,7 @@ 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):
@@ -30,7 +24,6 @@ class ThreadsListTestCase(AuthenticatedUserTestCase):
 
 
         self.root = Category.objects.root_category()
         self.root = Category.objects.root_category()
         self.first_category = Category.objects.get(slug='first-category')
         self.first_category = Category.objects.get(slug='first-category')
-
         """
         """
         Create categories tree for test cases:
         Create categories tree for test cases:
 
 
@@ -48,12 +41,16 @@ class ThreadsListTestCase(AuthenticatedUserTestCase):
             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()
 
 
@@ -63,7 +60,9 @@ 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')
 
 
@@ -71,12 +70,16 @@ 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')
@@ -86,7 +89,9 @@ 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')
 
 
@@ -146,26 +151,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)
 
 
 
 
@@ -211,11 +207,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()
@@ -231,11 +226,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):
@@ -243,7 +237,9 @@ 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(
@@ -253,22 +249,16 @@ 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)
@@ -284,24 +274,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 = response.json()
         response_json = response.json()
-        self.assertEqual(
-            response_json['subcategories'][0], self.category_b.pk)
+        self.assertEqual(response_json['subcategories'][0], self.category_b.pk)
 
 
     def test_display_pinned_threads(self):
     def test_display_pinned_threads(self):
         """
         """
@@ -318,9 +303,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)
@@ -371,9 +354,7 @@ class AllThreadsListTests(ThreadsListTestCase):
         """threads list is paginated for users with js disabled"""
         """threads list is paginated for users with js disabled"""
         threads = []
         threads = []
         for _ in range(settings.MISAGO_THREADS_PER_PAGE * 3):
         for _ in range(settings.MISAGO_THREADS_PER_PAGE * 3):
-            threads.append(testutils.post_thread(
-                category=self.first_category
-            ))
+            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')
@@ -381,7 +362,8 @@ class AllThreadsListTests(ThreadsListTestCase):
 
 
         for thread in threads[:settings.MISAGO_THREADS_PER_PAGE]:
         for thread in threads[:settings.MISAGO_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[settings.MISAGO_THREADS_PER_PAGE:settings.MISAGO_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[settings.MISAGO_THREADS_PER_PAGE * 2:]:
             self.assertNotContains(response, thread.get_absolute_url())
             self.assertNotContains(response, thread.get_absolute_url())
@@ -412,7 +394,9 @@ 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:
@@ -427,40 +411,44 @@ 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):
@@ -478,9 +466,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)
@@ -540,14 +526,10 @@ 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()
@@ -567,10 +549,8 @@ 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()
@@ -587,12 +567,12 @@ 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)
+        ).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(
-            category=test_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)
@@ -604,7 +584,9 @@ 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)
+        ).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')
 
 
@@ -704,18 +686,14 @@ 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)
@@ -732,18 +710,14 @@ 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)
@@ -760,18 +734,14 @@ class ThreadsVisibilityTests(ThreadsListTestCase):
             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)
@@ -922,13 +892,10 @@ class NewThreadsListTests(ThreadsListTestCase):
         self.user.save()
         self.user.save()
 
 
         test_thread = testutils.post_thread(
         test_thread = testutils.post_thread(
-            category=self.category_a,
-            started_on=self.user.joined_on - timedelta(days=2)
+            category=self.category_a, 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()
 
 
@@ -966,9 +933,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()
@@ -1004,8 +969,7 @@ class NewThreadsListTests(ThreadsListTestCase):
         self.user.save()
         self.user.save()
 
 
         test_thread = testutils.post_thread(
         test_thread = testutils.post_thread(
-            category=self.category_a,
-            started_on=self.user.joined_on - timedelta(minutes=1)
+            category=self.category_a, started_on=self.user.joined_on - timedelta(minutes=1)
         )
         )
 
 
         self.access_all_categories()
         self.access_all_categories()
@@ -1040,9 +1004,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)
@@ -1079,9 +1041,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,
@@ -1140,7 +1100,9 @@ class UnreadThreadsListTests(ThreadsListTestCase):
         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 = response.json()
         response_json = response.json()
@@ -1151,9 +1113,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)
@@ -1182,7 +1142,9 @@ class UnreadThreadsListTests(ThreadsListTestCase):
         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 = response.json()
         response_json = response.json()
@@ -1194,9 +1156,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()
 
 
@@ -1219,7 +1179,9 @@ class UnreadThreadsListTests(ThreadsListTestCase):
         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 = response.json()
         response_json = response.json()
@@ -1230,9 +1192,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)
@@ -1258,7 +1218,9 @@ class UnreadThreadsListTests(ThreadsListTestCase):
         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 = response.json()
         response_json = response.json()
@@ -1271,9 +1233,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)
@@ -1302,7 +1262,9 @@ class UnreadThreadsListTests(ThreadsListTestCase):
         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 = response.json()
         response_json = response.json()
@@ -1314,8 +1276,7 @@ class UnreadThreadsListTests(ThreadsListTestCase):
         self.user.save()
         self.user.save()
 
 
         test_thread = testutils.post_thread(
         test_thread = testutils.post_thread(
-            category=self.category_a,
-            started_on=self.user.joined_on - timedelta(days=2)
+            category=self.category_a, 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)
@@ -1344,7 +1305,9 @@ class UnreadThreadsListTests(ThreadsListTestCase):
         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 = response.json()
         response_json = response.json()
@@ -1356,13 +1319,11 @@ class UnreadThreadsListTests(ThreadsListTestCase):
         self.user.save()
         self.user.save()
 
 
         test_thread = testutils.post_thread(
         test_thread = testutils.post_thread(
-            category=self.category_a,
-            started_on=self.user.joined_on - timedelta(days=2)
+            category=self.category_a, 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)
 
 
@@ -1392,7 +1353,9 @@ class UnreadThreadsListTests(ThreadsListTestCase):
         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 = response.json()
         response_json = response.json()
@@ -1402,9 +1365,7 @@ class UnreadThreadsListTests(ThreadsListTestCase):
 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,
@@ -1433,7 +1394,9 @@ class SubscribedThreadsListTests(ThreadsListTestCase):
         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 = response.json()
         response_json = response.json()
@@ -1442,9 +1405,7 @@ class SubscribedThreadsListTests(ThreadsListTestCase):
 
 
     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()
 
 
@@ -1468,7 +1429,9 @@ class SubscribedThreadsListTests(ThreadsListTestCase):
         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 = response.json()
         response_json = response.json()
@@ -1480,8 +1443,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,
         )
         )
 
 
@@ -1492,9 +1454,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)
@@ -1502,9 +1462,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={
-                'can_see_unapproved_content_lists': True
-            })
+            self.access_all_categories(base_acl={'can_see_unapproved_content_lists': True})
 
 
             self.access_all_categories()
             self.access_all_categories()
             response = self.client.get(test_url)
             response = self.client.get(test_url)
@@ -1524,9 +1482,7 @@ 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())
@@ -1534,9 +1490,7 @@ 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())
@@ -1545,9 +1499,7 @@ class UnapprovedListTests(ThreadsListTestCase):
         # test api
         # test api
         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())
@@ -1568,26 +1520,20 @@ class UnapprovedListTests(ThreadsListTestCase):
             is_unapproved=True,
             is_unapproved=True,
         )
         )
 
 
-        self.access_all_categories(base_acl={
-            'can_see_unapproved_content_lists': True
-        })
+        self.access_all_categories(base_acl={'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())
         self.assertNotContains(response, hidden_thread.get_absolute_url())
         self.assertNotContains(response, hidden_thread.get_absolute_url())
 
 
-        self.access_all_categories(base_acl={
-            'can_see_unapproved_content_lists': True
-        })
+        self.access_all_categories(base_acl={'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())
         self.assertNotContains(response, hidden_thread.get_absolute_url())
         self.assertNotContains(response, hidden_thread.get_absolute_url())
 
 
         # test api
         # test api
-        self.access_all_categories(base_acl={
-            'can_see_unapproved_content_lists': True
-        })
+        self.access_all_categories(base_acl={'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())
@@ -1602,9 +1548,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)

+ 23 - 53
misago/threads/tests/test_threadview.py

@@ -41,11 +41,7 @@ class ThreadViewTestCase(AuthenticatedUserTestCase):
         if acl:
         if acl:
             category_acl.update(acl)
             category_acl.update(acl)
 
 
-        override_acl(self.user, {
-            'categories': {
-                self.category.pk: category_acl
-            }
-        })
+        override_acl(self.user, {'categories': {self.category.pk: category_acl}})
 
 
 
 
 class ThreadVisibilityTests(ThreadViewTestCase):
 class ThreadVisibilityTests(ThreadViewTestCase):
@@ -56,9 +52,7 @@ class ThreadVisibilityTests(ThreadViewTestCase):
 
 
     def test_view_shows_owner_thread(self):
     def test_view_shows_owner_thread(self):
         """view handles "owned threads only" """
         """view handles "owned threads only" """
-        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.assertEqual(response.status_code, 404)
         self.assertEqual(response.status_code, 404)
@@ -66,34 +60,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()
@@ -102,9 +88,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)
@@ -114,18 +98,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()
@@ -141,9 +121,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)
@@ -189,9 +167,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())
@@ -199,13 +175,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)
 
 
@@ -218,9 +194,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())
@@ -231,9 +205,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())
@@ -375,7 +347,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')
@@ -442,8 +416,7 @@ class ThreadAttachmentsViewTests(ThreadViewTestCase):
                     'uploader': '/user/bobboberson-123/'
                     'uploader': '/user/bobboberson-123/'
                 },
                 },
                 'filename': 'Archiwum-1.zip',
                 'filename': 'Archiwum-1.zip',
-            }),
-            self.mock_attachment_cache({
+            }), self.mock_attachment_cache({
                 'url': {
                 'url': {
                     'index': '/attachment/loremipsum-223/',
                     'index': '/attachment/loremipsum-223/',
                     'thumb': '/attachment/thumb/loremipsum-223/',
                     'thumb': '/attachment/thumb/loremipsum-223/',
@@ -451,8 +424,7 @@ class ThreadAttachmentsViewTests(ThreadViewTestCase):
                 },
                 },
                 '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,
@@ -521,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')

+ 14 - 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,18 @@ 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 +98,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"
+            )

+ 22 - 10
misago/threads/tests/test_utils.py

@@ -9,7 +9,6 @@ class AddCategoriesToItemsTests(MisagoTestCase):
         super(AddCategoriesToItemsTests, self).setUp()
         super(AddCategoriesToItemsTests, self).setUp()
 
 
         self.root = Category.objects.root_category()
         self.root = Category.objects.root_category()
-
         """
         """
         Create categories tree for test cases:
         Create categories tree for test cases:
 
 
@@ -27,12 +26,16 @@ class AddCategoriesToItemsTests(MisagoTestCase):
             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 +44,25 @@ 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 +72,9 @@ 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 +88,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"""
@@ -230,7 +240,9 @@ class GetThreadIdFromUrlTests(MisagoTestCase):
         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 = (

+ 1 - 5
misago/threads/tests/test_validators.py

@@ -31,11 +31,7 @@ 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 = (
-            'Lorem ipsum dolor met',
-            '123 456 789 112'
-            'Ugabugagagagagaga',
-        )
+        VALID_TITLES = ('Lorem ipsum dolor met', '123 456 789 112' 'Ugabugagagagagaga', )
 
 
         for title in VALID_TITLES:
         for title in VALID_TITLES:
             validate_title(title)
             validate_title(title)

+ 44 - 43
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 = {
@@ -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 = {
@@ -110,28 +127,23 @@ def post_poll(thread, poster):
         poster_slug=poster.slug,
         poster_slug=poster.slug,
         poster_ip='127.0.0.1',
         poster_ip='127.0.0.1',
         question="Lorem ipsum dolor met?",
         question="Lorem ipsum dolor met?",
-        choices=[
-            {
-                'hash': 'aaaaaaaaaaaa',
-                'label': 'Alpha',
-                'votes': 1
-            },
-            {
-                'hash': 'bbbbbbbbbbbb',
-                'label': 'Beta',
-                'votes': 0
-            },
-            {
-                'hash': 'gggggggggggg',
-                'label': 'Gamma',
-                'votes': 2
-            },
-            {
-                'hash': 'dddddddddddd',
-                'label': 'Delta',
-                'votes': 1
-            }
-        ],
+        choices=[{
+            'hash': 'aaaaaaaaaaaa',
+            'label': 'Alpha',
+            'votes': 1
+        }, {
+            'hash': 'bbbbbbbbbbbb',
+            'label': 'Beta',
+            'votes': 0
+        }, {
+            'hash': 'gggggggggggg',
+            'label': 'Gamma',
+            'votes': 2
+        }, {
+            'hash': 'dddddddddddd',
+            'label': 'Delta',
+            'votes': 1
+        }],
         allowed_choices=2,
         allowed_choices=2,
         votes=4
         votes=4
     )
     )
@@ -140,8 +152,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,
@@ -200,12 +211,7 @@ def like_post(post, liker=None, username=None):
             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,
@@ -215,12 +221,7 @@ def like_post(post, liker=None, username=None):
             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()

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

@@ -16,98 +16,90 @@ 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}
+        )

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

@@ -17,145 +17,121 @@ 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

+ 49 - 54
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)
+    PrivateThreadGotoPostView, PrivateThreadGotoLastView, PrivateThreadGotoNewView
+)
 from misago.threads.views.list import ForumThreadsList, CategoryThreadsList, PrivateThreadsList
 from misago.threads.views.list import ForumThreadsList, CategoryThreadsList, PrivateThreadsList
 from misago.threads.views.thread import ThreadView, PrivateThreadView
 from misago.threads.views.thread import ThreadView, PrivateThreadView
 
 
-
-LISTS_TYPES = (
-    'all',
-    'my',
-    'new',
-    'unread',
-    'subscribed',
-    'unapproved',
-)
+LISTS_TYPES = ('all', 'my', 'new', 'unread', 'subscribed', 'unapproved', )
 
 
 
 
 def threads_list_patterns(prefix, view, patterns):
 def threads_list_patterns(prefix, view, patterns):
@@ -28,58 +21,56 @@ 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', ForumThreadsList, (
-        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', 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/$',
-))
+    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
 
 
@@ -120,8 +111,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}
+    ),
 ]
 ]

+ 5 - 1
misago/threads/urls/api.py

@@ -14,6 +14,10 @@ router.register(r'threads/(?P<thread_pk>[^/.]+)/posts', ThreadPostsViewSet, base
 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 - 6
misago/threads/utils.py

@@ -25,16 +25,15 @@ 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 (
                 if category.level == 1 and (
-                        category == item.category or
-                        category.has_child(item.category)):
+                        category == item.category or 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
 
 
@@ -48,8 +47,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

+ 24 - 20
misago/threads/validators.py

@@ -43,21 +43,23 @@ 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 +72,23 @@ 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.")

+ 8 - 12
misago/threads/viewmodels/category.py

@@ -41,23 +41,18 @@ 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(
-                id__in=request.user.acl_cache['browseable_categories']
-            ).select_related('parent'))
+            Category.objects.all_categories().
+            filter(id__in=request.user.acl_cache['browseable_categories']).select_related('parent')
+        )
 
 
 
 
 class ThreadsCategory(ThreadsRootCategory):
 class ThreadsCategory(ThreadsRootCategory):
@@ -91,5 +86,6 @@ 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 - 11
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)
@@ -69,10 +71,7 @@ class ViewModel(object):
 
 
     def get_posts_queryset(self, request, thread):
     def get_posts_queryset(self, request, thread):
         queryset = thread.post_set.select_related(
         queryset = thread.post_set.select_related(
-            'poster',
-            'poster__rank',
-            'poster__ban_cache',
-            'poster__online_tracker'
+            'poster', 'poster__rank', 'poster__ban_cache', '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)
 
 
@@ -97,10 +96,7 @@ class ViewModel(object):
         return context
         return context
 
 
     def get_template_context(self):
     def get_template_context(self):
-        return {
-            'posts': self.posts,
-            'paginator': self.paginator
-        }
+        return {'posts': self.posts, 'paginator': self.paginator}
 
 
 
 
 class ThreadPosts(ViewModel):
 class ThreadPosts(ViewModel):

+ 16 - 16
misago/threads/viewmodels/thread.py

@@ -18,20 +18,22 @@ from misago.threads.threadtypes import trees_map
 
 
 __all__ = ['ForumThread', 'PrivateThread']
 __all__ = ['ForumThread', 'PrivateThread']
 
 
-
 BASE_RELATIONS = (
 BASE_RELATIONS = (
-    'category',
-    'poll',
-    'starter',
-    'starter__rank',
-    'starter__ban_cache',
-    'starter__online_tracker'
+    'category', 'poll', 'starter', 'starter__rank', 'starter__ban_cache', '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 +62,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:
@@ -101,9 +103,7 @@ class ForumThread(ViewModel):
             queryset = Thread.objects.select_related(*BASE_RELATIONS)
             queryset = Thread.objects.select_related(*BASE_RELATIONS)
 
 
         thread = get_object_or_404(
         thread = get_object_or_404(
-            queryset,
-            pk=pk,
-            category__tree_id=trees_map.get_tree_id_for_root(THREADS_ROOT_NAME)
+            queryset, pk=pk, 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)

+ 32 - 23
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"),
@@ -32,11 +31,16 @@ LISTS_NAMES = {
 }
 }
 
 
 LIST_DENIED_MESSAGES = {
 LIST_DENIED_MESSAGES = {
-    'my': ugettext_lazy("You have to sign in to see list of threads that you have started."),
-    'new': ugettext_lazy("You have to sign in to see list of threads you haven't read."),
-    'unread': ugettext_lazy("You have to sign in to see list of threads with new replies."),
-    'subscribed': ugettext_lazy("You have to sign in to see list of threads you are subscribing."),
-    'unapproved': ugettext_lazy("You have to sign in to see list of threads with unapproved posts."),
+    'my':
+        ugettext_lazy("You have to sign in to see list of threads that you have started."),
+    'new':
+        ugettext_lazy("You have to sign in to see list of threads you haven't read."),
+    'unread':
+        ugettext_lazy("You have to sign in to see list of threads with new replies."),
+    'subscribed':
+        ugettext_lazy("You have to sign in to see list of threads you are subscribing."),
+    'unapproved':
+        ugettext_lazy("You have to sign in to see list of threads with unapproved posts."),
 }
 }
 
 
 
 
@@ -49,15 +53,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 +100,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,7 +117,7 @@ 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 = {
@@ -122,7 +134,6 @@ 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
         }
         }
@@ -131,10 +142,8 @@ class ViewModel(object):
 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 +162,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)
 
 
@@ -176,6 +185,8 @@ class PrivateThreads(ViewModel):
 """
 """
 Thread queryset utils
 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 +225,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)

+ 12 - 19
misago/threads/views/admin/attachments.py

@@ -20,25 +20,18 @@ class AttachmentAdmin(generic.AdminBaseMixin):
 
 
 class AttachmentsList(AttachmentAdmin, generic.ListView):
 class AttachmentsList(AttachmentAdmin, generic.ListView):
     items_per_page = 20
     items_per_page = 20
-    ordering = (
-        ('-id', _("From newest")),
-        ('id', _("From oldest")),
-        ('filename', _("A to z")),
-        ('-filename', _("Z to a")),
-        ('size', _("Smallest files")),
-        ('-size', _("Largest files")),
-    )
+    ordering = (('-id', _("From newest")), ('id', _("From oldest")), ('filename', _("A to z")),
+                ('-filename', _("Z to a")), ('size', _("Smallest 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 = [
-        {
-            'action': 'delete',
-            'name': _("Delete attachments"),
-            'icon': 'fa fa-times-circle',
-            'confirmation': _("Are you sure you want to delete selected attachments?"),
-            'is_atomic': False
-        }
-    ]
+    mass_actions = [{
+        'action': 'delete',
+        'name': _("Delete attachments"),
+        'icon': 'fa fa-times-circle',
+        'confirmation': _("Are you sure you want to delete selected attachments?"),
+        'is_atomic': False
+    }]
 
 
     def get_search_form(self, request):
     def get_search_form(self, request):
         return SearchAttachmentsForm
         return SearchAttachmentsForm
@@ -65,7 +58,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:
@@ -86,7 +79,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 - 2
misago/threads/views/admin/attachmenttypes.py

@@ -25,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()
@@ -43,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):

+ 6 - 3
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)
@@ -100,7 +100,10 @@ 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()

+ 1 - 1
misago/threads/views/list.py

@@ -75,7 +75,7 @@ class CategoryThreadsList(ForumThreadsList):
     def get_category(self, request, **kwargs):
     def get_category(self, request, **kwargs):
         category = super(CategoryThreadsList, 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
 
 
 
 

+ 4 - 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,8 @@ 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())
@@ -61,9 +59,7 @@ class ThreadView(ThreadBase):
     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 {
-            'THREADS_API': reverse('misago:api:thread-list')
-        }
+        return {'THREADS_API': reverse('misago:api:thread-list')}
 
 
 
 
 class PrivateThreadView(ThreadBase):
 class PrivateThreadView(ThreadBase):

+ 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'),

+ 33 - 36
misago/users/api/auth.py

@@ -33,8 +33,10 @@ def gateway(request):
 POST /auth/ with CSRF, username and password
 POST /auth/ with CSRF, username and password
 will attempt to authenticate new user
 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):
     form = AuthenticationForm(request, data=request.data)
     form = AuthenticationForm(request, data=request.data)
@@ -42,13 +44,14 @@ def login(request):
         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
 GET /auth/ will return current auth user, either User or AnonymousUser
 """
 """
+
+
 @api_view()
 @api_view()
 def session_user(request):
 def session_user(request):
     if request.user.is_authenticated:
     if request.user.is_authenticated:
@@ -62,6 +65,8 @@ def session_user(request):
 """
 """
 GET /auth/criteria/ will return password and username criteria for accounts
 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):
     criteria = {
     criteria = {
@@ -73,9 +78,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', {}))
 
 
@@ -88,8 +91,10 @@ def get_criteria(request):
 POST /auth/send-activation/ with CSRF token and email
 POST /auth/send-activation/ with CSRF token and email
 will mail account activation link to requester
 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):
     form = ResendActivationForm(request.data)
     form = ResendActivationForm(request.data)
@@ -103,25 +108,24 @@ def send_activation(request):
         }
         }
         mail_subject = mail_subject % subject_formats
         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({
-                'username': form.user_cache.username,
-                'email': form.user_cache.email
-            })
+        return Response({'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
 POST /auth/send-password-form/ with CSRF token and email
 will mail change password form link to requester
 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):
     form = ResetPasswordForm(request.data)
     form = ResetPasswordForm(request.data)
@@ -137,29 +141,28 @@ def send_password_form(request):
 
 
         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({
-                'username': form.user_cache.username,
-                'email': form.user_cache.email
-            })
+        return Response({'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
 POST /auth/change-password/user/token/ with CSRF and new password
 will change forgotten 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):
     invalid_message = _("Form link is invalid. Please try again.")
     invalid_message = _("Form link is invalid. Please try again.")
@@ -181,9 +184,7 @@ 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 +192,6 @@ 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({'detail': e.messages[0]}, status=status.HTTP_400_BAD_REQUEST)
 
 
-    return Response({
-            'username': user.username
-        })
+    return Response({'username': user.username})

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

@@ -13,9 +13,8 @@ class UnbannedOnly(BasePermission):
         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)
 
 
     def has_permission(self, request, view):
     def has_permission(self, request, view):

+ 13 - 23
misago/users/api/userendpoints/avatar.py

@@ -16,15 +16,15 @@ 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({
         return Response({
             'detail': _("Your avatar is locked. You can't change it."),
             'detail': _("Your avatar is locked. You can't change it."),
             'reason': reason
             'reason': reason
-        }, status=status.HTTP_403_FORBIDDEN)
+        },
+                        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':
@@ -50,14 +50,8 @@ def get_avatar_options(user):
         for gallery in avatars.gallery.get_available_galleries():
         for gallery in avatars.gallery.get_available_galleries():
             gallery_images = []
             gallery_images = []
             for image in gallery['images']:
             for image in gallery['images']:
-                gallery_images.append({
-                    'id': image.id,
-                    'url': image.url
-                })
-            options['galleries'].append({
-                'name': gallery['name'],
-                'images': gallery_images
-            })
+                gallery_images.append({'id': image.id, 'url': image.url})
+            options['galleries'].append({'name': gallery['name'], 'images': gallery_images})
 
 
     # Can't have custom avatar?
     # Can't have custom avatar?
     if not settings.allow_custom_avatars:
     if not settings.allow_custom_avatars:
@@ -104,20 +98,17 @@ def avatar_post(options, user, data):
         if not type_options:
         if not type_options:
             return Response({
             return Response({
                 'detail': _("This avatar type is not allowed.")
                 'detail': _("This avatar type is not allowed.")
-            }, status=status.HTTP_400_BAD_REQUEST)
+            },
+                            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()
 
 
@@ -128,6 +119,8 @@ def avatar_post(options, user, data):
 """
 """
 Avatar rpc handlers
 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 +131,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 +174,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 +185,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,
 }
 }

+ 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 - 13
misago/users/api/userendpoints/changepassword.py

@@ -10,25 +10,21 @@ 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(
-            request, 'password', serializer.validated_data['new_password'])
+        token = store_new_credential(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 - 14
misago/users/api/userendpoints/create.py

@@ -33,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'],
@@ -55,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',
@@ -74,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'

+ 1 - 1
misago/users/api/userendpoints/list.py

@@ -23,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 - 18
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:
@@ -60,21 +55,17 @@ def change_username(request):
             return Response({
             return Response({
                 'detail': _("Error changing username. Please try again."),
                 'detail': _("Error changing username. Please try again."),
             },
             },
-            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:
@@ -86,11 +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."),
-                }, 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,

+ 8 - 12
misago/users/api/usernamechanges.py

@@ -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,15 +56,13 @@ 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()
 
 
         list_page = paginate(queryset, page, 12, 4)
         list_page = paginate(queryset, page, 12, 4)
 
 
         data = pagination_dict(list_page)
         data = pagination_dict(list_page)
-        data.update({
-            'results': UsernameChangeSerializer(list_page.object_list, many=True).data
-        })
+        data.update({'results': UsernameChangeSerializer(list_page.object_list, many=True).data})
 
 
         return Response(data)
         return Response(data)

+ 15 - 18
misago/users/api/users.py

@@ -54,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):
@@ -104,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)
 
 
@@ -164,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):
@@ -213,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)
@@ -235,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')
 
 
@@ -249,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')
 
 
@@ -263,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)
 
 
@@ -275,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)
 
 
@@ -283,7 +280,7 @@ 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'
+)

+ 2 - 1
misago/users/apps.py

@@ -40,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):

+ 0 - 2
misago/users/avatars/__init__.py

@@ -2,10 +2,8 @@ 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,

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

@@ -22,6 +22,8 @@ def set_avatar(user):
 """
 """
 Default drawer
 Default drawer
 """
 """
+
+
 def draw_default(user):
 def draw_default(user):
     image_size = max(settings.MISAGO_AVATARS_SIZES)
     image_size = max(settings.MISAGO_AVATARS_SIZES)
 
 
@@ -32,11 +34,11 @@ 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 = (
+    '#d32f2f', '#c2185b', '#7b1fa2', '#512da8', '#303f9f', '#1976d2', '#0288D1', '#0288d1',
+    '#0097a7', '#00796b', '#388e3c', '#689f38', '#afb42b', '#fbc02d', '#ffa000', '#f57c00',
+    '#e64a19'
+)
 COLOR_WHEEL_LEN = len(COLOR_WHEEL)
 COLOR_WHEEL_LEN = len(COLOR_WHEEL)
 
 
 
 
@@ -66,8 +68,7 @@ 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)

+ 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
 
 
 
 

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

@@ -39,11 +39,10 @@ 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 +79,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 - 5
misago/users/avatars/uploaded.py

@@ -38,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:
@@ -82,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):
@@ -131,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)),
         ))
         ))

+ 8 - 13
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
@@ -92,6 +88,8 @@ Utility for checking if request came from banned IP
 This check may be performed frequently, which is why there is extra
 This check may be performed frequently, which is why there is extra
 boilerplate that caches ban check result in session
 boilerplate that caches ban check result in session
 """
 """
+
+
 def get_request_ip_ban(request):
 def get_request_ip_ban(request):
     session_ban_cache = _get_session_bancache(request)
     session_ban_cache = _get_session_bancache(request)
     if session_ban_cache:
     if session_ban_cache:
@@ -113,10 +111,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:
@@ -156,8 +151,9 @@ def _hydrate_session_cache(ban_cache):
 """
 """
 Utilities for front-end based bans
 Utilities for front-end based bans
 """
 """
-def ban_user(user, user_message=None, staff_message=None, length=None,
-             expires_on=None):
+
+
+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 +167,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 - 7
misago/users/decorators.py

@@ -10,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
 
 
 
 
@@ -32,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

+ 11 - 34
misago/users/djangoadmin.py

@@ -79,48 +79,25 @@ 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',
-    )
+    list_display = ('username', 'email', 'is_staff', 'is_superuser', )
     search_fields = ('username', 'email')
     search_fields = ('username', 'email')
     list_filter = ('groups', 'is_staff', 'is_superuser')
     list_filter = ('groups', 'is_staff', 'is_superuser')
 
 
     form = UserAdminForm
     form = UserAdminForm
     actions = None
     actions = None
     readonly_fields = (
     readonly_fields = (
-        'username',
-        'email',
-        'rank',
-        'last_login',
-        'joined_on',
-        'is_staff',
-        'is_superuser',
+        'username', 'email', 'rank', 'last_login', 'joined_on', 'is_staff', 'is_superuser',
     )
     )
-    fieldsets = (
-        (
-            _('Misago user data'),
-            {'fields': (
-                'username',
-                'email',
-                'rank',
-                'last_login',
-                'joined_on',
-                'is_staff',
-                'is_superuser',
+    fieldsets = ((
+        _('Misago user data'), {
+            'fields': (
+                'username', 'email', 'rank', 'last_login', 'joined_on', 'is_staff', 'is_superuser',
                 'edit_from_misago_link',
                 'edit_from_misago_link',
-            )},
-        ),
-        (
-            _('Edit permissions and groups'),
-            {'fields': (
-                'groups',
-                'user_permissions',
-            )},
-        ),
-    )
+            )
+        },
+    ), (_('Edit permissions and groups'), {
+        'fields': ('groups', 'user_permissions', )
+    }, ), )
 
 
     def has_add_permission(self, request):
     def has_add_permission(self, request):
         return False
         return False

+ 82 - 135
misago/users/forms/admin.py

@@ -14,11 +14,11 @@ from misago.users.validators import validate_email, validate_username
 
 
 
 
 UserModel = get_user_model()
 UserModel = get_user_model()
-
-
 """
 """
 Users
 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 +58,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,16 +87,14 @@ 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."
     )
     )
 
 
     new_password = forms.CharField(
     new_password = forms.CharField(
-        label=_("Change password to"),
-        widget=forms.PasswordInput,
-        required=False
+        label=_("Change password to"), widget=forms.PasswordInput, required=False
     )
     )
 
 
     is_avatar_locked = YesNoSwitch(
     is_avatar_locked = YesNoSwitch(
@@ -130,9 +125,7 @@ class EditUserForm(UserBaseForm):
     )
     )
 
 
     signature = forms.CharField(
     signature = forms.CharField(
-        label=_("Signature contents"),
-        widget=forms.Textarea(attrs={'rows': 3}),
-        required=False
+        label=_("Signature contents"), widget=forms.Textarea(attrs={'rows': 3}), required=False
     )
     )
     is_signature_locked = YesNoSwitch(
     is_signature_locked = YesNoSwitch(
         label=_("Lock signature"),
         label=_("Lock signature"),
@@ -143,17 +136,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 +156,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 +186,12 @@ 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,55 +213,56 @@ 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):
     staff_fields = {
     staff_fields = {
-        'is_staff': YesNoSwitch(
-            label=EditUserForm.IS_STAFF_LABEL,
-            help_text=EditUserForm.IS_STAFF_HELP_TEXT,
-            initial=instance.is_staff
-        ),
-        'is_superuser': YesNoSwitch(
-            label=EditUserForm.IS_SUPERUSER_LABEL,
-            help_text=EditUserForm.IS_SUPERUSER_HELP_TEXT,
-            initial=instance.is_superuser
-        ),
+        'is_staff':
+            YesNoSwitch(
+                label=EditUserForm.IS_STAFF_LABEL,
+                help_text=EditUserForm.IS_STAFF_HELP_TEXT,
+                initial=instance.is_staff
+            ),
+        'is_superuser':
+            YesNoSwitch(
+                label=EditUserForm.IS_SUPERUSER_LABEL,
+                help_text=EditUserForm.IS_SUPERUSER_HELP_TEXT,
+                initial=instance.is_superuser
+            ),
     }
     }
 
 
-    return type('StaffUserForm', (FormType,), staff_fields)
+    return type('StaffUserForm', (FormType, ), staff_fields)
 
 
 
 
 def UserIsActiveFormFactory(FormType, instance):
 def UserIsActiveFormFactory(FormType, instance):
     is_active_fields = {
     is_active_fields = {
-        'is_active': YesNoSwitch(
-            label=EditUserForm.IS_ACTIVE_LABEL,
-            help_text=EditUserForm.IS_ACTIVE_HELP_TEXT,
-            initial=instance.is_active
-        ),
-        'is_active_staff_message': forms.CharField(
-            label=EditUserForm.IS_ACTIVE_STAFF_MESSAGE_LABEL,
-            help_text=EditUserForm.IS_ACTIVE_STAFF_MESSAGE_HELP_TEXT,
-            initial=instance.is_active_staff_message,
-            widget=forms.Textarea(attrs={'rows': 3}),
-            required=False
-        ),
+        'is_active':
+            YesNoSwitch(
+                label=EditUserForm.IS_ACTIVE_LABEL,
+                help_text=EditUserForm.IS_ACTIVE_HELP_TEXT,
+                initial=instance.is_active
+            ),
+        'is_active_staff_message':
+            forms.CharField(
+                label=EditUserForm.IS_ACTIVE_STAFF_MESSAGE_LABEL,
+                help_text=EditUserForm.IS_ACTIVE_STAFF_MESSAGE_HELP_TEXT,
+                initial=instance.is_active_staff_message,
+                widget=forms.Textarea(attrs={'rows': 3}),
+                required=False
+            ),
     }
     }
 
 
-    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 +283,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'))
@@ -342,28 +327,25 @@ def SearchUsersForm(*args, **kwargs):
         threadstore.set('misago_admin_roles_choices', roles_choices)
         threadstore.set('misago_admin_roles_choices', roles_choices)
 
 
     extra_fields = {
     extra_fields = {
-        'rank': forms.TypedChoiceField(
-            label=_("Has rank"),
-            coerce=int,
-            required=False,
-            choices=ranks_choices
-        ),
-        'role': forms.TypedChoiceField(
-            label=_("Has role"),
-            coerce=int,
-            required=False,
-            choices=roles_choices
-        )
+        'rank':
+            forms.TypedChoiceField(
+                label=_("Has rank"), coerce=int, required=False, choices=ranks_choices
+            ),
+        'role':
+            forms.TypedChoiceField(
+                label=_("Has role"), coerce=int, required=False, choices=roles_choices
+            )
     }
     }
 
 
-    FinalForm = type(
-        'SearchUsersFormFinal', (SearchUsersFormBase,), extra_fields)
+    FinalForm = type('SearchUsersFormFinal', (SearchUsersFormBase, ), extra_fields)
     return FinalForm(*args, **kwargs)
     return FinalForm(*args, **kwargs)
 
 
 
 
 """
 """
 Ranks
 Ranks
 """
 """
+
+
 class RankForm(forms.ModelForm):
 class RankForm(forms.ModelForm):
     name = forms.CharField(
     name = forms.CharField(
         label=_("Name"),
         label=_("Name"),
@@ -401,9 +383,7 @@ 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"),
@@ -435,8 +415,7 @@ 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
 
 
@@ -444,18 +423,16 @@ class RankForm(forms.ModelForm):
 """
 """
 Bans
 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=(
-            ('usernames', _('Usernames')),
-            ('emails', _('E-mails')),
-            ('domains', _('E-mail domains')),
-            ('ip', _('IP addresses')),
-            ('ip_first', _('First segment of IP addresses')),
-            ('ip_two', _('First two segments of IP addresses'))
-        )
+        choices=(('usernames', _('Usernames')), ('emails', _('E-mails')),
+                 ('domains', _('E-mail domains')), ('ip', _('IP addresses')),
+                 ('ip_first', _('First segment 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"),
@@ -463,9 +440,7 @@ class BanUsersForm(forms.Form):
         max_length=1000,
         max_length=1000,
         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={
-            'max_length': _("Message can't be longer than 1000 characters.")
-        }
+        error_messages={'max_length': _("Message can't be longer than 1000 characters.")}
     )
     )
     staff_message = forms.CharField(
     staff_message = forms.CharField(
         label=_("Team message"),
         label=_("Team message"),
@@ -473,9 +448,7 @@ class BanUsersForm(forms.Form):
         max_length=1000,
         max_length=1000,
         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={
-            'max_length': _("Message can't be longer than 1000 characters.")
-        }
+        error_messages={'max_length': _("Message can't be longer than 1000 characters.")}
     )
     )
     expires_on = IsoDateTimeField(
     expires_on = IsoDateTimeField(
         label=_("Expires on"),
         label=_("Expires on"),
@@ -485,11 +458,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,
@@ -498,10 +467,8 @@ class BanForm(forms.ModelForm):
             'for rought matches. For example, making IP ban for value '
             'for rought matches. For example, making IP ban for value '
             '"83.*" will ban all IP addresses beginning with "83.".'
             '"83.*" will ban all IP addresses beginning with "83.".'
         ),
         ),
-        error_messages={
-            'max_length': _("Banned value can't be longer "
-                            "than 250 characters.")
-        }
+        error_messages={'max_length': _("Banned value can't be longer "
+                                        "than 250 characters.")}
     )
     )
     user_message = forms.CharField(
     user_message = forms.CharField(
         label=_("User message"),
         label=_("User message"),
@@ -509,9 +476,7 @@ class BanForm(forms.ModelForm):
         max_length=1000,
         max_length=1000,
         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={
-            'max_length': _("Message can't be longer than 1000 characters.")
-        }
+        error_messages={'max_length': _("Message can't be longer than 1000 characters.")}
     )
     )
     staff_message = forms.CharField(
     staff_message = forms.CharField(
         label=_("Team message"),
         label=_("Team message"),
@@ -519,9 +484,7 @@ class BanForm(forms.ModelForm):
         max_length=1000,
         max_length=1000,
         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={
-            'max_length': _("Message can't be longer than 1000 characters.")
-        }
+        error_messages={'max_length': _("Message can't be longer than 1000 characters.")}
     )
     )
     expires_on = IsoDateTimeField(
     expires_on = IsoDateTimeField(
         label=_("Expires on"),
         label=_("Expires on"),
@@ -551,30 +514,15 @@ class BanForm(forms.ModelForm):
 
 
 
 
 class SearchBansForm(forms.Form):
 class SearchBansForm(forms.Form):
-    SARCH_CHOICES = (
-        ('', _('All bans')),
-        ('names', _('Usernames')),
-        ('emails', _('E-mails')),
-        ('ips', _('IPs')),
-    )
+    SARCH_CHOICES = (('', _('All bans')), ('names', _('Usernames')), ('emails', _('E-mails')),
+                     ('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=(
-            ('', _('Any')),
-            ('used', _('Active')),
-            ('unused', _('Expired')),
-        )
+        choices=(('', _('Any')), ('used', _('Active')), ('unused', _('Expired')), )
     )
     )
 
 
     def filter_queryset(self, search_criteria, queryset):
     def filter_queryset(self, search_criteria, queryset):
@@ -589,8 +537,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)

+ 39 - 51
misago/users/forms/auth.py

@@ -13,23 +13,27 @@ UserModel = get_user_model()
 
 
 class MisagoAuthMixin(object):
 class MisagoAuthMixin(object):
     error_messages = {
     error_messages = {
-        'empty_data': _("Fill out both fields."),
-        '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."),
+        'empty_data':
+            _("Fill out both fields."),
+        '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."
+            ),
     }
     }
 
 
     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 +48,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 +56,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
 
 
@@ -94,16 +84,13 @@ class AdminAuthenticationForm(AuthenticationForm):
     required_css_class = 'required'
     required_css_class = 'required'
 
 
     def __init__(self, *args, **kwargs):
     def __init__(self, *args, **kwargs):
-        self.error_messages.update({
-            'not_staff': _("Your account does not have admin privileges.")
-        })
+        self.error_messages.update({'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 +101,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 +114,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 +130,26 @@ 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):

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

@@ -27,24 +27,43 @@ 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 +133,11 @@ 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 +158,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."

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

@@ -33,14 +33,10 @@ class Command(BaseCommand):
         start_time = time.time()
         start_time = time.time()
         for user in batch_update(UserModel.objects.all()):
         for user in batch_update(UserModel.objects.all()):
             user.threads = user.thread_set.filter(
             user.threads = user.thread_set.filter(
-                category__in=categories,
-                is_hidden=False,
-                is_unapproved=False
+                category__in=categories, is_hidden=False, is_unapproved=False
             ).count()
             ).count()
             user.posts = user.post_set.filter(
             user.posts = user.post_set.filter(
-                category__in=categories,
-                is_event=False,
-                is_unapproved=False
+                category__in=categories, is_event=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()

+ 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, ),
         ),
         ),
     ]
     ]

+ 112 - 106
misago/users/migrations/0002_users_settings.py

@@ -10,28 +10,30 @@ _ = 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': (
-            {
+    migrate_settings_group(
+        apps, {
+            'key':
+                'users',
+            'name':
+                _("Users"),
+            'description':
+                _(
+                    "Those settings control user accounts default behaviour and features availability."
+                ),
+            'settings': ({
                 'setting': 'account_activation',
                 'setting': 'account_activation',
                 'name': _("New accounts activation"),
                 'name': _("New accounts activation"),
                 'legend': _("New accounts"),
                 'legend': _("New accounts"),
                 'value': 'none',
                 'value': 'none',
                 'form_field': 'select',
                 'form_field': 'select',
                 'field_extra': {
                 'field_extra': {
-                    'choices': (
-                        ('none', _("No activation required")),
-                        ('user', _("Activation token sent to User")),
-                        ('admin', _("Activation by administrator")),
-                        ('closed', _("Don't allow new registrations"))
-                    )
+                    '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',
                 'setting': 'username_length_min',
                 'name': _("Minimum length"),
                 'name': _("Minimum length"),
                 'description': _("Minimum allowed username length."),
                 'description': _("Minimum allowed username length."),
@@ -43,8 +45,7 @@ def create_users_settings_group(apps, schema_editor):
                     'max_value': 20,
                     'max_value': 20,
                 },
                 },
                 'is_public': True,
                 'is_public': True,
-            },
-            {
+            }, {
                 'setting': 'username_length_max',
                 'setting': 'username_length_max',
                 'name': _("Maximum length"),
                 'name': _("Maximum length"),
                 'description': _("Maximum allowed username length."),
                 'description': _("Maximum allowed username length."),
@@ -55,8 +56,7 @@ def create_users_settings_group(apps, schema_editor):
                     'max_value': 20,
                     'max_value': 20,
                 },
                 },
                 'is_public': True,
                 'is_public': True,
-            },
-            {
+            }, {
                 'setting': 'password_length_min',
                 'setting': 'password_length_min',
                 'name': _("Minimum length"),
                 'name': _("Minimum length"),
                 'description': _("Minimum allowed user password length."),
                 'description': _("Minimum allowed user password length."),
@@ -68,48 +68,55 @@ def create_users_settings_group(apps, schema_editor):
                     'max_value': 255,
                     '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':
+                    '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',
                 'setting': 'default_avatar',
                 'name': _("Default avatar"),
                 'name': _("Default avatar"),
                 'value': 'gravatar',
                 'value': 'gravatar',
                 'form_field': 'select',
                 'form_field': 'select',
                 'field_extra': {
                 'field_extra': {
-                    'choices': (
-                        ('dynamic', _("Individual")),
-                        ('gravatar', _("Gravatar")),
-                        ('gallery', _("Random avatar from gallery")),
-                    ),
+                    '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':
+                    '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',
                 'setting': 'avatar_upload_limit',
                 'name': _("Maximum size of uploaded avatar"),
                 'name': _("Maximum size of uploaded avatar"),
                 'description': _("Enter maximum allowed file size "
                 'description': _("Enter maximum allowed file size "
@@ -120,8 +127,7 @@ def create_users_settings_group(apps, schema_editor):
                     'min_value': 0,
                     'min_value': 0,
                 },
                 },
                 'is_public': True,
                 'is_public': True,
-            },
-            {
+            }, {
                 'setting': 'signature_length_max',
                 'setting': 'signature_length_max',
                 'name': _("Maximum length"),
                 'name': _("Maximum length"),
                 'legend': _("Signatures"),
                 'legend': _("Signatures"),
@@ -133,63 +139,62 @@ def create_users_settings_group(apps, schema_editor):
                     'max_value': 5000,
                     'max_value': 5000,
                 },
                 },
                 'is_public': True,
                 'is_public': True,
-            },
-            {
+            }, {
                 'setting': 'subscribe_start',
                 'setting': 'subscribe_start',
                 'name': _("Started threads"),
                 'name': _("Started threads"),
                 'legend': _("Default subscriptions settings"),
                 'legend': _("Default subscriptions settings"),
                 'value': 'watch_email',
                 'value': 'watch_email',
                 'form_field': 'select',
                 'form_field': 'select',
                 'field_extra': {
                 '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")),
-                    ),
+                    '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',
                 'setting': 'subscribe_reply',
                 'name': _("Replied threads"),
                 'name': _("Replied threads"),
                 'value': 'watch_email',
                 'value': 'watch_email',
                 'form_field': 'select',
                 'form_field': 'select',
                 'field_extra': {
                 '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")),
-                    ),
+                    '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': (
-            {
+    migrate_settings_group(
+        apps, {
+            'key':
+                'captcha',
+            'name':
+                _("CAPTCHA"),
+            'description':
+                _("Those settings allow you to combat automatic "
+                  "registrations on your forum."),
+            'settings': ({
                 'setting': 'captcha_type',
                 'setting': 'captcha_type',
                 'name': _("Select CAPTCHA type"),
                 'name': _("Select CAPTCHA type"),
                 'legend': _("CAPTCHA type"),
                 'legend': _("CAPTCHA type"),
                 'value': 'no',
                 'value': 'no',
                 'form_field': 'select',
                 'form_field': 'select',
                 'field_extra': {
                 'field_extra': {
-                    'choices': (
-                        ('no', _("No CAPTCHA")),
-                        ('re', _("reCaptcha")),
-                        ('qa', _("Question and answer")),
-                    ),
+                    'choices': (('no', _("No CAPTCHA")), ('re', _("reCaptcha")),
+                                ('qa', _("Question and answer")), ),
                 },
                 },
                 'is_public': True,
                 'is_public': True,
-            },
-            {
+            }, {
                 'setting': 'recaptcha_site_key',
                 'setting': 'recaptcha_site_key',
                 'name': _("Site key"),
                 'name': _("Site key"),
                 'legend': _("reCAPTCHA"),
                 'legend': _("reCAPTCHA"),
@@ -199,8 +204,7 @@ def create_users_settings_group(apps, schema_editor):
                     'max_length': 100,
                     'max_length': 100,
                 },
                 },
                 'is_public': True,
                 'is_public': True,
-            },
-            {
+            }, {
                 'setting': 'recaptcha_secret_key',
                 'setting': 'recaptcha_secret_key',
                 'name': _("Secret key"),
                 'name': _("Secret key"),
                 'value': '',
                 'value': '',
@@ -208,8 +212,7 @@ def create_users_settings_group(apps, schema_editor):
                     'required': False,
                     'required': False,
                     'max_length': 100,
                     'max_length': 100,
                 },
                 },
-            },
-            {
+            }, {
                 'setting': 'qa_question',
                 'setting': 'qa_question',
                 'name': _("Test question"),
                 'name': _("Test question"),
                 'legend': _("Question and answer"),
                 'legend': _("Question and answer"),
@@ -218,8 +221,7 @@ def create_users_settings_group(apps, schema_editor):
                     'required': False,
                     'required': False,
                     'max_length': 250,
                     'max_length': 250,
                 },
                 },
-            },
-            {
+            }, {
                 'setting': 'qa_help_text',
                 'setting': 'qa_help_text',
                 'name': _("Question help text"),
                 'name': _("Question help text"),
                 'value': '',
                 'value': '',
@@ -227,22 +229,26 @@ def create_users_settings_group(apps, schema_editor):
                     'required': False,
                     'required': False,
                     'max_length': 250,
                     'max_length': 250,
                 },
                 },
-            },
-            {
-                'setting': 'qa_answers',
-                'name': _("Valid answers"),
-                'description': _("Enter each answer in new line. "
-                                 "Answers are case-insensitive."),
-                'value': '',
-                'form_field': 'textarea',
+            }, {
+                'setting':
+                    'qa_answers',
+                'name':
+                    _("Valid answers"),
+                'description':
+                    _("Enter each answer in new line. "
+                      "Answers are case-insensitive."),
+                'value':
+                    '',
+                'form_field':
+                    'textarea',
                 'field_extra': {
                 'field_extra': {
                     'rows': 4,
                     'rows': 4,
                     'required': False,
                     'required': False,
                     'max_length': 250,
                     'max_length': 250,
                 },
                 },
-            },
-        )
-    })
+            }, )
+        }
+    )
 
 
 
 
 class Migration(migrations.Migration):
 class Migration(migrations.Migration):

+ 1 - 4
misago/users/migrations/0004_default_ranks.py

@@ -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'
+            ),
         ),
         ),
     ]
     ]

+ 80 - 73
misago/users/migrations/0006_update_settings.py

@@ -11,28 +11,30 @@ _ = 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': (
-            {
+    migrate_settings_group(
+        apps, {
+            'key':
+                'users',
+            'name':
+                _("Users"),
+            'description':
+                _(
+                    "Those settings control user accounts default behaviour and features availability."
+                ),
+            'settings': ({
                 'setting': 'account_activation',
                 'setting': 'account_activation',
                 'name': _("New accounts activation"),
                 'name': _("New accounts activation"),
                 'legend': _("New accounts"),
                 'legend': _("New accounts"),
                 'value': 'none',
                 'value': 'none',
                 'form_field': 'select',
                 'form_field': 'select',
                 'field_extra': {
                 'field_extra': {
-                    'choices': (
-                        ('none', _("No activation required")),
-                        ('user', _("Activation token sent to User")),
-                        ('admin', _("Activation by administrator")),
-                        ('closed', _("Don't allow new registrations"))
-                    )
+                    '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',
                 'setting': 'username_length_min',
                 'name': _("Minimum length"),
                 'name': _("Minimum length"),
                 'description': _("Minimum allowed username length."),
                 'description': _("Minimum allowed username length."),
@@ -43,8 +45,7 @@ def update_users_settings(apps, schema_editor):
                     'min_value': 2,
                     'min_value': 2,
                     'max_value': 20,
                     'max_value': 20,
                 },
                 },
-            },
-            {
+            }, {
                 'setting': 'username_length_max',
                 'setting': 'username_length_max',
                 'name': _("Maximum length"),
                 'name': _("Maximum length"),
                 'description': _("Maximum allowed username length."),
                 'description': _("Maximum allowed username length."),
@@ -54,48 +55,55 @@ def update_users_settings(apps, schema_editor):
                     'min_value': 2,
                     'min_value': 2,
                     'max_value': 20,
                     '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':
+                    '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',
                 'setting': 'default_avatar',
                 'name': _("Default avatar"),
                 'name': _("Default avatar"),
                 'value': 'gravatar',
                 'value': 'gravatar',
                 'form_field': 'select',
                 'form_field': 'select',
                 'field_extra': {
                 'field_extra': {
-                    'choices': (
-                        ('dynamic', _("Individual")),
-                        ('gravatar', _("Gravatar")),
-                        ('gallery', _("Random avatar from gallery")),
-                    ),
+                    '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':
+                    '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',
                 'setting': 'avatar_upload_limit',
                 'name': _("Maximum size of uploaded avatar"),
                 'name': _("Maximum size of uploaded avatar"),
                 'description': _("Enter maximum allowed file size "
                 'description': _("Enter maximum allowed file size "
@@ -106,8 +114,7 @@ def update_users_settings(apps, schema_editor):
                     'min_value': 0,
                     'min_value': 0,
                 },
                 },
                 'is_public': True,
                 'is_public': True,
-            },
-            {
+            }, {
                 'setting': 'signature_length_max',
                 'setting': 'signature_length_max',
                 'name': _("Maximum length"),
                 'name': _("Maximum length"),
                 'legend': _("Signatures"),
                 'legend': _("Signatures"),
@@ -119,40 +126,40 @@ def update_users_settings(apps, schema_editor):
                     'max_value': 5000,
                     'max_value': 5000,
                 },
                 },
                 'is_public': True,
                 'is_public': True,
-            },
-            {
+            }, {
                 'setting': 'subscribe_start',
                 'setting': 'subscribe_start',
                 'name': _("Started threads"),
                 'name': _("Started threads"),
                 'legend': _("Default subscriptions settings"),
                 'legend': _("Default subscriptions settings"),
                 'value': 'watch_email',
                 'value': 'watch_email',
                 'form_field': 'select',
                 'form_field': 'select',
                 'field_extra': {
                 '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")),
-                    ),
+                    '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',
                 'setting': 'subscribe_reply',
                 'name': _("Replied threads"),
                 'name': _("Replied threads"),
                 'value': 'watch_email',
                 'value': 'watch_email',
                 'form_field': 'select',
                 'form_field': 'select',
                 'field_extra': {
                 '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")),
-                    ),
+                    '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)
 
 

+ 7 - 11
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,7 @@ class Ban(models.Model):
     EMAIL = 1
     EMAIL = 1
     IP = 2
     IP = 2
 
 
-    CHOICES = (
-        (USERNAME, _('Username')),
-        (EMAIL, _('E-mail address')),
-        (IP, _('IP address')),
-    )
+    CHOICES = ((USERNAME, _('Username')), (EMAIL, _('E-mail 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 +106,9 @@ 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 +119,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

+ 40 - 54
misago/users/models/user.py

@@ -39,9 +39,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:
@@ -52,17 +52,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)
@@ -78,8 +71,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'}]
@@ -101,9 +95,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,
         )
         )
@@ -142,22 +137,16 @@ class User(AbstractBaseUser, PermissionsMixin):
     SUBSCRIBE_NOTIFY = 1
     SUBSCRIBE_NOTIFY = 1
     SUBSCRIBE_ALL = 2
     SUBSCRIBE_ALL = 2
 
 
-    SUBSCRIBE_CHOICES = (
-        (SUBSCRIBE_NONE, _("No")),
-        (SUBSCRIBE_NOTIFY, _("Notify")),
-        (SUBSCRIBE_ALL, _("Notify with e-mail"))
-    )
+    SUBSCRIBE_CHOICES = ((SUBSCRIBE_NONE, _("No")), (SUBSCRIBE_NOTIFY, _("Notify")),
+                         (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_NONE, _("Everybody")),
-        (LIMIT_INVITES_TO_FOLLOWED, _("Users I follow")),
-        (LIMIT_INVITES_TO_NOBODY, _("Nobody")),
-    )
-
+    LIMIT_INVITES_TO_CHOICES = ((LIMIT_INVITES_TO_NONE, _("Everybody")),
+                                (LIMIT_INVITES_TO_FOLLOWED, _("Users I follow")),
+                                (LIMIT_INVITES_TO_NOBODY, _("Nobody")), )
     """
     """
     Note that "username" field is purely for shows.
     Note that "username" field is purely for shows.
     When searching users by their names, always use lowercased string
     When searching users by their names, always use lowercased string
@@ -184,7 +173,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.'),
     )
     )
@@ -204,16 +194,10 @@ class User(AbstractBaseUser, PermissionsMixin):
     is_active_staff_message = models.TextField(null=True, blank=True)
     is_active_staff_message = models.TextField(null=True, blank=True)
 
 
     avatar_tmp = models.ImageField(
     avatar_tmp = models.ImageField(
-        max_length=255,
-        upload_to=avatars.store.upload_to,
-        null=True,
-        blank=True
+        max_length=255, upload_to=avatars.store.upload_to, null=True, blank=True
     )
     )
     avatar_src = models.ImageField(
     avatar_src = models.ImageField(
-        max_length=255,
-        upload_to=avatars.store.upload_to,
-        null=True,
-        blank=True
+        max_length=255, upload_to=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)
@@ -231,11 +215,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,
     )
     )
@@ -248,12 +234,10 @@ class User(AbstractBaseUser, PermissionsMixin):
     sync_unread_private_threads = models.BooleanField(default=False)
     sync_unread_private_threads = models.BooleanField(default=False)
 
 
     subscribe_to_started_threads = models.PositiveIntegerField(
     subscribe_to_started_threads = models.PositiveIntegerField(
-        default=SUBSCRIBE_NONE,
-        choices=SUBSCRIBE_CHOICES
+        default=SUBSCRIBE_NONE, choices=SUBSCRIBE_CHOICES
     )
     )
     subscribe_to_replied_threads = models.PositiveIntegerField(
     subscribe_to_replied_threads = models.PositiveIntegerField(
-        default=SUBSCRIBE_NONE,
-        choices=SUBSCRIBE_CHOICES
+        default=SUBSCRIBE_NONE, choices=SUBSCRIBE_CHOICES
     )
     )
 
 
     threads = models.PositiveIntegerField(default=0)
     threads = models.PositiveIntegerField(default=0)
@@ -330,10 +314,12 @@ 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):
         """
         """
@@ -356,8 +342,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)
@@ -439,14 +424,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,
     }
     }

+ 18 - 9
misago/users/permissions/account.py

@@ -9,19 +9,21 @@ from misago.core.forms import YesNoSwitch
 """
 """
 Admin Permissions Form
 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 +32,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."
+        )
     )
     )
 
 
 
 
@@ -45,6 +49,8 @@ def change_permissions_form(role):
 """
 """
 ACL Builder
 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 +62,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

+ 25 - 10
misago/users/permissions/delete.py

@@ -16,11 +16,11 @@ __all__ = [
     'allow_delete_user',
     'allow_delete_user',
     'can_delete_user',
     'can_delete_user',
 ]
 ]
-
-
 """
 """
 Admin Permissions Form
 Admin Permissions Form
 """
 """
+
+
 class PermissionsForm(forms.Form):
 class PermissionsForm(forms.Form):
     legend = _("Deleting users")
     legend = _("Deleting users")
 
 
@@ -48,6 +48,8 @@ def change_permissions_form(role):
 """
 """
 ACL Builder
 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,7 +57,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_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
     )
     )
@@ -64,6 +69,8 @@ def build_acl(acl, roles, key_name):
 """
 """
 ACL's for targets
 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']:
@@ -77,6 +84,8 @@ def register_with(registry):
 """
 """
 ACL tests
 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 +99,23 @@ 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}
+            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)
             raise PermissionDenied(message)
     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.",
-                less_posts_than) % {'posts': less_posts_than}
+                "You can't delete users that made more than %(posts)s posts.", less_posts_than
+            ) % {
+                'posts': less_posts_than
+            }
             raise PermissionDenied(message)
             raise PermissionDenied(message)
+
+
 can_delete_user = return_boolean(allow_delete_user)
 can_delete_user = return_boolean(allow_delete_user)

+ 24 - 11
misago/users/permissions/moderation.py

@@ -26,11 +26,11 @@ __all__ = [
     'allow_lift_ban',
     'allow_lift_ban',
     'can_lift_ban',
     'can_lift_ban',
 ]
 ]
-
-
 """
 """
 Admin Permissions Form
 Admin Permissions Form
 """
 """
+
+
 class PermissionsForm(forms.Form):
 class PermissionsForm(forms.Form):
     legend = _("Users moderation")
     legend = _("Users moderation")
 
 
@@ -63,6 +63,8 @@ def change_permissions_form(role):
 """
 """
 ACL Builder
 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,7 +77,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_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,
@@ -89,6 +94,8 @@ def build_acl(acl, roles, key_name):
 """
 """
 ACL's for targets
 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 +104,7 @@ 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 = (
-        'can_rename',
-        'can_moderate_avatar',
-        'can_moderate_signature',
-        'can_ban',
-    )
+    mod_permissions = ('can_rename', 'can_moderate_avatar', 'can_moderate_signature', 'can_ban', )
 
 
     for permission in mod_permissions:
     for permission in mod_permissions:
         if target.acl[permission]:
         if target.acl[permission]:
@@ -117,11 +119,15 @@ def register_with(registry):
 """
 """
 ACL tests
 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 +136,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 +147,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 +157,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 +174,9 @@ 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 = _("You can't lift bans that " "expire after %(expiration)s.")
             message = message % {'expiration': format_date(lift_cutoff)}
             message = message % {'expiration': format_date(lift_cutoff)}
             raise PermissionDenied(message)
             raise PermissionDenied(message)
+
+
 can_lift_ban = return_boolean(allow_lift_ban)
 can_lift_ban = return_boolean(allow_lift_ban)

+ 27 - 36
misago/users/permissions/profiles.py

@@ -21,22 +21,12 @@ __all__ = [
     'allow_see_ban_details',
     'allow_see_ban_details',
     'can_see_ban_details',
     'can_see_ban_details',
 ]
 ]
-
-
 """
 """
 Admin Permissions Form
 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 +45,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):
@@ -89,6 +67,8 @@ def change_permissions_form(role):
 """
 """
 ACL Builder
 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 +83,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,
@@ -119,16 +102,14 @@ def build_acl(acl, roles, key_name):
 """
 """
 ACL's for targets
 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]:
@@ -143,9 +124,13 @@ def register_with(registry):
 """
 """
 ACL tests
 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 +140,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 +154,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 +163,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 - 18
misago/users/serializers/auth.py

@@ -32,12 +32,8 @@ class AuthenticatedUserSerializer(UserSerializer, AuthFlags):
     class Meta:
     class Meta:
         model = UserModel
         model = UserModel
         fields = UserSerializer.Meta.fields + (
         fields = UserSerializer.Meta.fields + (
-            'is_hiding_presence',
-            'limits_private_thread_invites_to',
-            'subscribe_to_started_threads',
-            'subscribe_to_replied_threads',
-
-            'is_authenticated',
+            'is_hiding_presence', 'limits_private_thread_invites_to',
+            'subscribe_to_started_threads', 'subscribe_to_replied_threads', 'is_authenticated',
             'is_anonymous',
             'is_anonymous',
         )
         )
 
 
@@ -49,21 +45,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',
 )
 )
 
 
 
 

+ 1 - 4
misago/users/serializers/ban.py

@@ -14,10 +14,7 @@ __all__ = [
 
 
 def serialize_message(message):
 def serialize_message(message):
     if message:
     if message:
-        return {
-            'plain': message,
-            'html': format_plaintext_for_html(message)
-        }
+        return {'plain': message, 'html': format_plaintext_for_html(message)}
     else:
     else:
         return None
         return None
 
 

+ 8 - 7
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
@@ -27,18 +28,18 @@ class ModerateSignatureSerializer(serializers.ModelSerializer):
     class Meta:
     class Meta:
         model = UserModel
         model = UserModel
         fields = [
         fields = [
-            'signature',
-            'is_signature_locked',
-            'signature_lock_user_message',
+            'signature', 'is_signature_locked', '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 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})
+            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'])
 
 
 
 

+ 1 - 8
misago/users/serializers/rank.py

@@ -14,14 +14,7 @@ class RankSerializer(serializers.ModelSerializer):
     class Meta:
     class Meta:
         model = Rank
         model = Rank
         fields = (
         fields = (
-            'id',
-            'name',
-            'slug',
-            'description',
-            'title',
-            'css_class',
-            'is_default',
-            'is_tab',
+            'id', 'name', 'slug', 'description', 'title', 'css_class', 'is_default', 'is_tab',
             'absolute_url',
             'absolute_url',
         )
         )
 
 

+ 28 - 40
misago/users/serializers/user.py

@@ -42,38 +42,17 @@ class UserSerializer(serializers.ModelSerializer, MutableFields):
     class Meta:
     class Meta:
         model = UserModel
         model = UserModel
         fields = (
         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',
-            'meta',
-            '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', 'meta', 'status',
+            'absolute_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
@@ -110,21 +89,30 @@ class UserSerializer(serializers.ModelSerializer, MutableFields):
 
 
     def get_api_url(self, obj):
     def get_api_url(self, obj):
         return {
         return {
-            'root': reverse('misago:api:user-detail', kwargs={'pk': obj.pk}),
-            'follow': reverse('misago:api:user-follow', 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}),
-            'delete': reverse('misago:api:user-delete', kwargs={'pk': obj.pk}),
-            'followers': reverse('misago:api:user-followers', kwargs={'pk': obj.pk}),
-            'follows': reverse('misago:api:user-follows', kwargs={'pk': obj.pk}),
-            'threads': reverse('misago:api:user-threads', kwargs={'pk': obj.pk}),
-            'posts': reverse('misago:api:user-posts', kwargs={'pk': obj.pk}),
+            'root':
+                reverse('misago:api:user-detail', kwargs={'pk': obj.pk}),
+            'follow':
+                reverse('misago:api:user-follow', 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}),
+            'delete':
+                reverse('misago:api:user-delete', kwargs={'pk': obj.pk}),
+            'followers':
+                reverse('misago:api:user-followers', kwargs={'pk': obj.pk}),
+            'follows':
+                reverse('misago:api:user-follows', kwargs={'pk': obj.pk}),
+            'threads':
+                reverse('misago:api:user-threads', kwargs={'pk': obj.pk}),
+            'posts':
+                reverse('misago:api:user-posts', kwargs={'pk': obj.pk}),
         }
         }
 
 
 
 
 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'
+)

+ 2 - 9
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):
@@ -19,11 +17,6 @@ class UsernameChangeSerializer(serializers.ModelSerializer):
     class Meta:
     class Meta:
         model = UsernameChange
         model = UsernameChange
         fields = (
         fields = (
-            'id',
-            'user',
-            'changed_by',
-            'changed_by_username',
-            'changed_on',
-            'new_username',
+            'id', 'user', 'changed_by', 'changed_by_username', 'changed_on', 'new_username',
             'old_username'
             'old_username'
         )
         )

+ 3 - 5
misago/users/signals.py

@@ -3,13 +3,11 @@ from django.dispatch import Signal, receiver
 
 
 delete_user_content = Signal()
 delete_user_content = Signal()
 username_changed = Signal()
 username_changed = Signal()
-
-
 """
 """
 Signal handlers
 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 = ''

+ 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()

+ 62 - 65
misago/users/tests/test_auth_api.py

@@ -12,9 +12,7 @@ UserModel = get_user_model()
 class GatewayTests(TestCase):
 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(
-            '/api/auth/',
-            data={'username': 'nope', 'password': 'nope'})
+        response = self.client.post('/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 +26,12 @@ 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 +57,18 @@ 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)
@@ -89,10 +89,12 @@ 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, 200)
         self.assertEqual(response.status_code, 200)
 
 
         response = self.client.get('/api/auth/')
         response = self.client.get('/api/auth/')
@@ -104,13 +106,14 @@ 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 +127,14 @@ 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 +148,17 @@ 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/')
@@ -325,10 +330,10 @@ 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 +341,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 +353,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 +363,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 +381,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 - 22
misago/users/tests/test_auth_backend.py

@@ -12,42 +12,29 @@ 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(
-            username=self.user.username,
-            password=self.password
-        )
+        user = backend.authenticate(username=self.user.username, password=self.password)
 
 
         self.assertEqual(user, self.user)
         self.assertEqual(user, self.user)
 
 
     def test_authenticate_email(self):
     def test_authenticate_email(self):
         """auth authenticates with email instead of username"""
         """auth authenticates with email instead of username"""
-        user = backend.authenticate(
-            username=self.user.email,
-            password=self.password
-        )
+        user = backend.authenticate(username=self.user.email, password=self.password)
 
 
         self.assertEqual(user, self.user)
         self.assertEqual(user, self.user)
 
 
     def test_authenticate_invalid_credential(self):
     def test_authenticate_invalid_credential(self):
         """auth handles invalid credentials"""
         """auth handles invalid credentials"""
-        user = backend.authenticate(
-            username='InvalidCredential',
-            password=self.password
-        )
+        user = backend.authenticate(username='InvalidCredential', password=self.password)
 
 
         self.assertIsNone(user)
         self.assertIsNone(user)
 
 
     def test_authenticate_invalid_password(self):
     def test_authenticate_invalid_password(self):
         """auth validates password"""
         """auth validates password"""
-        user = backend.authenticate(
-            username=self.user.email,
-            password='Invalid'
-        )
+        user = backend.authenticate(username=self.user.email, password='Invalid')
 
 
         self.assertIsNone(user)
         self.assertIsNone(user)
 
 
@@ -56,10 +43,7 @@ class MisagoBackendTests(TestCase):
         self.user.is_active = False
         self.user.is_active = False
         self.user.save()
         self.user.save()
 
 
-        user = backend.authenticate(
-            username=self.user.email,
-            password=self.password
-        )
+        user = backend.authenticate(username=self.user.email, password=self.password)
 
 
         self.assertIsNone(user)
         self.assertIsNone(user)
 
 

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

@@ -20,25 +20,22 @@ 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'], '/')
 
 
     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(
-            '/api/auth/', data={'username': 'nope', 'password': 'nope'})
+        response = self.client.post('/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)
 
 

+ 16 - 20
misago/users/tests/test_avatars.py

@@ -17,8 +17,7 @@ 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)
@@ -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,8 @@ 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 +168,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 +198,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"""

+ 20 - 14
misago/users/tests/test_avatarserver_views.py

@@ -31,34 +31,40 @@ 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):

+ 52 - 36
misago/users/tests/test_banadmin_views.py

@@ -27,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)
@@ -42,10 +45,11 @@ 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)
 
 
@@ -56,13 +60,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'))
@@ -72,21 +79,27 @@ 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})
         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': '',
-        })
+        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'))
@@ -96,16 +109,19 @@ 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'))

+ 11 - 38
misago/users/tests/test_bans.py

@@ -18,25 +18,18 @@ class GetBanTests(TestCase):
         nonexistent_ban = get_username_ban('nonexistent')
         nonexistent_ban = get_username_ban('nonexistent')
         self.assertIsNone(nonexistent_ban)
         self.assertIsNone(nonexistent_ban)
 
 
-        Ban.objects.create(
-            banned_value='expired',
-            expires_on=timezone.now() - timedelta(days=7)
-        )
+        Ban.objects.create(banned_value='expired', expires_on=timezone.now() - timedelta(days=7))
 
 
         expired_ban = get_username_ban('expired')
         expired_ban = get_username_ban('expired')
         self.assertIsNone(expired_ban)
         self.assertIsNone(expired_ban)
 
 
-        Ban.objects.create(
-            banned_value='wrongtype',
-            check_type=Ban.EMAIL
-        )
+        Ban.objects.create(banned_value='wrongtype', check_type=Ban.EMAIL)
 
 
         wrong_type_ban = get_username_ban('wrongtype')
         wrong_type_ban = get_username_ban('wrongtype')
         self.assertIsNone(wrong_type_ban)
         self.assertIsNone(wrong_type_ban)
 
 
         valid_ban = Ban.objects.create(
         valid_ban = Ban.objects.create(
-            banned_value='admi*',
-            expires_on=timezone.now() + timedelta(days=7)
+            banned_value='admi*', 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)
 
 
@@ -54,10 +47,7 @@ class GetBanTests(TestCase):
         expired_ban = get_email_ban('ex@pired.com')
         expired_ban = get_email_ban('ex@pired.com')
         self.assertIsNone(expired_ban)
         self.assertIsNone(expired_ban)
 
 
-        Ban.objects.create(
-            banned_value='wrong@type.com',
-            check_type=Ban.IP
-        )
+        Ban.objects.create(banned_value='wrong@type.com', check_type=Ban.IP)
 
 
         wrong_type_ban = get_email_ban('wrong@type.com')
         wrong_type_ban = get_email_ban('wrong@type.com')
         self.assertIsNone(wrong_type_ban)
         self.assertIsNone(wrong_type_ban)
@@ -83,10 +73,7 @@ class GetBanTests(TestCase):
         expired_ban = get_ip_ban('124.0.0.1')
         expired_ban = get_ip_ban('124.0.0.1')
         self.assertIsNone(expired_ban)
         self.assertIsNone(expired_ban)
 
 
-        Ban.objects.create(
-            banned_value='wrongtype',
-            check_type=Ban.EMAIL
-        )
+        Ban.objects.create(banned_value='wrongtype', check_type=Ban.EMAIL)
 
 
         wrong_type_ban = get_ip_ban('wrongtype')
         wrong_type_ban = get_ip_ban('wrongtype')
         self.assertIsNone(wrong_type_ban)
         self.assertIsNone(wrong_type_ban)
@@ -101,8 +88,7 @@ class GetBanTests(TestCase):
 
 
 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"""
@@ -112,9 +98,7 @@ class UserBansTests(TestCase):
     def test_permanent_ban(self):
     def test_permanent_ban(self):
         """user is caught by permanent ban"""
         """user is caught by permanent ban"""
         Ban.objects.create(
         Ban.objects.create(
-            banned_value='bob',
-            user_message='User reason',
-            staff_message='Staff reason'
+            banned_value='bob', user_message='User reason', staff_message='Staff reason'
         )
         )
 
 
         user_ban = get_user_ban(self.user)
         user_ban = get_user_ban(self.user)
@@ -140,20 +124,14 @@ class UserBansTests(TestCase):
 
 
     def test_expired_ban(self):
     def test_expired_ban(self):
         """user is not caught by expired ban"""
         """user is not caught by expired ban"""
-        Ban.objects.create(
-            banned_value='bo*',
-            expires_on=timezone.now() - timedelta(days=7)
-        )
+        Ban.objects.create(banned_value='bo*', expires_on=timezone.now() - timedelta(days=7))
 
 
         self.assertIsNone(get_user_ban(self.user))
         self.assertIsNone(get_user_ban(self.user))
         self.assertFalse(self.user.ban_cache.is_banned)
         self.assertFalse(self.user.ban_cache.is_banned)
 
 
     def test_expired_non_flagged_ban(self):
     def test_expired_non_flagged_ban(self):
         """user is not caught by expired but checked ban"""
         """user is not caught by expired but checked ban"""
-        Ban.objects.create(
-            banned_value='bo*',
-            expires_on=timezone.now() - timedelta(days=7)
-        )
+        Ban.objects.create(banned_value='bo*', expires_on=timezone.now() - timedelta(days=7))
         Ban.objects.update(is_checked=True)
         Ban.objects.update(is_checked=True)
 
 
         self.assertIsNone(get_user_ban(self.user))
         self.assertIsNone(get_user_ban(self.user))
@@ -174,11 +152,7 @@ class RequestIPBansTests(TestCase):
 
 
     def test_permanent_ban(self):
     def test_permanent_ban(self):
         """ip is caught by permanent ban"""
         """ip is caught by permanent ban"""
-        Ban.objects.create(
-            check_type=Ban.IP,
-            banned_value='127.0.0.1',
-            user_message='User reason'
-        )
+        Ban.objects.create(check_type=Ban.IP, banned_value='127.0.0.1', user_message='User reason')
 
 
         ip_ban = get_request_ip_ban(FakeRequest())
         ip_ban = get_request_ip_ban(FakeRequest())
         self.assertTrue(ip_ban['is_banned'])
         self.assertTrue(ip_ban['is_banned'])
@@ -224,8 +198,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')

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

@@ -24,7 +24,8 @@ class CreateSuperuserTests(TestCase):
 
 
         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 - 12
misago/users/tests/test_decorators.py

@@ -36,23 +36,14 @@ class DenyGuestsTests(UserTestCase):
 class DenyBannedIPTests(UserTestCase):
 class DenyBannedIPTests(UserTestCase):
     def test_success(self):
     def test_success(self):
         """deny_banned_ips decorator allowed unbanned request"""
         """deny_banned_ips decorator allowed unbanned request"""
-        Ban.objects.create(
-            check_type=Ban.IP,
-            banned_value='83.*',
-            user_message="Ya got banned!"
-        )
+        Ban.objects.create(check_type=Ban.IP, banned_value='83.*', user_message="Ya got banned!")
 
 
         response = self.client.post(reverse('misago:request-activation'))
         response = self.client.post(reverse('misago:request-activation'))
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
     def test_fail(self):
     def test_fail(self):
         """deny_banned_ips decorator denied banned request"""
         """deny_banned_ips decorator denied banned request"""
-        Ban.objects.create(
-            check_type=Ban.IP,
-            banned_value='127.*',
-            user_message="Ya got banned!"
-        )
+        Ban.objects.create(check_type=Ban.IP, banned_value='127.*', 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 - 18
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,10 +55,14 @@ 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):
@@ -63,10 +70,14 @@ class ForgottenPasswordViewsTests(UserTestCase):
         test_user = UserModel.objects.create_user('Bob', 'bob@test.com', 'Pass.123')
         test_user = UserModel.objects.create_user('Bob', 'bob@test.com', 'Pass.123')
 
 
         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):
@@ -76,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)

+ 8 - 7
misago/users/tests/test_lists_views.py

@@ -34,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):
@@ -57,7 +56,8 @@ 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()
@@ -69,8 +69,7 @@ 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
@@ -88,7 +87,8 @@ 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
@@ -109,7 +109,8 @@ 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

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

@@ -12,9 +12,9 @@ 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 +23,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 +37,8 @@ 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 +57,10 @@ 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 +71,8 @@ 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)
 
 

+ 11 - 24
misago/users/tests/test_profile_views.py

@@ -14,18 +14,14 @@ 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"""
         """user profile view redirects to valid slig"""
         invalid_kwargs = {'slug': 'baww', 'pk': self.user.pk}
         invalid_kwargs = {'slug': 'baww', 'pk': self.user.pk}
-        response = self.client.get(reverse('misago:user-posts',
-                                           kwargs=invalid_kwargs))
+        response = self.client.get(reverse('misago:user-posts', kwargs=invalid_kwargs))
 
 
         self.assertEqual(response.status_code, 301)
         self.assertEqual(response.status_code, 301)
 
 
@@ -98,8 +94,7 @@ 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.')
@@ -110,16 +105,14 @@ 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.')
@@ -130,16 +123,14 @@ 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.')
 
 
@@ -148,8 +139,7 @@ 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")
@@ -164,16 +154,14 @@ 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, {
@@ -188,8 +176,7 @@ class UserProfileViewsTests(AuthenticatedUserTestCase):
             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')

+ 41 - 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,25 @@ 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 +109,14 @@ 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 +132,14 @@ 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 +155,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')
 
 
         # 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 +184,14 @@ 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 +204,14 @@ 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 +234,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 +249,17 @@ 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'])

+ 14 - 28
misago/users/tests/test_rest_permissions.py

@@ -11,9 +11,8 @@ 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,23 +20,17 @@ 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):
         """policy blocked banned ip"""
         """policy blocked banned ip"""
-        Ban.objects.create(
-            check_type=Ban.IP,
-            banned_value='127.*',
-            user_message='Ya got banned!'
-        )
+        Ban.objects.create(check_type=Ban.IP, banned_value='127.*', 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 +44,8 @@ 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,21 +53,15 @@ 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):
         """policy blocked banned ip"""
         """policy blocked banned ip"""
-        Ban.objects.create(
-            check_type=Ban.IP,
-            banned_value='127.*',
-            user_message='Ya got banned!'
-        )
+        Ban.objects.create(check_type=Ban.IP, banned_value='127.*', 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)

+ 4 - 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,8 @@ 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 +137,4 @@ 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>')

+ 121 - 128
misago/users/tests/test_user_avatar_api.py

@@ -21,6 +21,7 @@ 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
@@ -77,13 +78,12 @@ 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)
@@ -123,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
-                },
-                'zoom': 1
-            }
-        }), content_type="application/json")
+        response = self.client.post(
+            self.link,
+            json.dumps({
+                'avatar': 'crop_tmp',
+                'crop': {
+                    'offset': {
+                        'x': 0,
+                        'y': 0
+                    },
+                    'zoom': 1
+                }
+            }),
+            content_type="application/json"
+        )
 
 
         response_json = response.json()
         response_json = response.json()
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
@@ -158,26 +161,36 @@ 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
-                },
-                'zoom': 1
-            }
-        }), content_type="application/json")
+        response = self.client.post(
+            self.link,
+            json.dumps({
+                'avatar': 'crop_tmp',
+                'crop': {
+                    'offset': {
+                        'x': 0,
+                        'y': 0
+                    },
+                    'zoom': 1
+                }
+            }),
+            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
-                },
-                'zoom': 1
-            }
-        }), content_type="application/json")
+        response = self.client.post(
+            self.link,
+            json.dumps({
+                'avatar': 'crop_src',
+                'crop': {
+                    'offset': {
+                        'x': 0,
+                        'y': 0
+                    },
+                    'zoom': 1
+                }
+            }),
+            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
@@ -194,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"""
@@ -210,16 +219,15 @@ 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
@@ -230,21 +238,16 @@ 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):
@@ -258,10 +261,7 @@ 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.")
 
 
@@ -270,11 +270,11 @@ 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
 
 
@@ -297,53 +297,54 @@ 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(
-            options['avatar_lock_staff_message'], other_user.avatar_lock_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(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)
@@ -352,25 +353,24 @@ 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(
-            options['avatar_lock_staff_message'], other_user.avatar_lock_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(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)
@@ -379,23 +379,20 @@ 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(
-            options['avatar_lock_staff_message'], other_user.avatar_lock_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(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)
@@ -404,14 +401,10 @@ 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(
-            options['avatar_lock_staff_message'], other_user.avatar_lock_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(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"""
@@ -419,5 +412,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)

+ 32 - 37
misago/users/tests/test_user_changeemail_api.py

@@ -12,6 +12,7 @@ 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 +27,61 @@ 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 +92,9 @@ 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)
 
 

+ 34 - 38
misago/users/tests/test_user_changepassword_api.py

@@ -8,6 +8,7 @@ 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
@@ -22,65 +23,60 @@ 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)
@@ -91,9 +87,9 @@ 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)
 
 

+ 54 - 50
misago/users/tests/test_user_create_api.py

@@ -14,6 +14,7 @@ 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 +41,40 @@ 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(), {
-            'username': [
-                "This username is not available."
-            ]
-        })
+        self.assertEqual(response.json(), {'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(), {
-            'email': [
-                "This e-mail address is not available."
-            ]
-        })
+        self.assertEqual(response.json(), {'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 +82,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 +110,11 @@ 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 +134,11 @@ 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 +153,11 @@ 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')

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

@@ -8,8 +8,7 @@ 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)
 
 

+ 8 - 12
misago/users/tests/test_user_signature_api.py

@@ -6,6 +6,7 @@ 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
@@ -69,9 +70,7 @@ 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):
@@ -83,17 +82,14 @@ 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)
 
 
-        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>')

+ 35 - 40
misago/users/tests/test_user_username_api.py

@@ -14,6 +14,7 @@ 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
@@ -26,10 +27,8 @@ class UserUsernameTests(AuthenticatedUserTestCase):
         response_json = response.json()
         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']):
@@ -55,9 +54,7 @@ class UserUsernameTests(AuthenticatedUserTestCase):
         response_json = response.json()
         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')
@@ -70,9 +67,7 @@ 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)
 
 
@@ -84,9 +79,7 @@ class UserUsernameTests(AuthenticatedUserTestCase):
         old_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 = response.json()['options']
         options = response.json()['options']
@@ -96,19 +89,18 @@ class UserUsernameTests(AuthenticatedUserTestCase):
         self.assertEqual(self.user.username, new_username)
         self.assertEqual(self.user.username, new_username)
         self.assertTrue(self.user.username != old_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,19 +130,18 @@ class UserUsernameModerationTests(AuthenticatedUserTestCase):
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         options = response.json()
         options = response.json()
-        self.assertEqual(options['length_min'],
-                         settings.username_length_min)
-        self.assertEqual(options['length_max'],
-                         settings.username_length_max)
+        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)
 
 
@@ -158,37 +149,42 @@ 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)
 
 
@@ -207,6 +203,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)

+ 205 - 196
misago/users/tests/test_useradmin_views.py

@@ -24,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'])
@@ -34,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']
@@ -79,20 +77,18 @@ class UserAdminViewsTests(AdminTestCase):
         user_pks = []
         user_pks = []
         for i in range(10):
         for i in range(10):
             test_user = UserModel.objects.create_user(
             test_user = UserModel.objects.create_user(
-                'Bob%s' % i,
-                'bob%s@test.com' % i,
-                'pass123',
-                requires_activation=1
+                'Bob%s' % i, 'bob%s@test.com' % i, 'pass123', 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)
 
 
@@ -101,16 +97,15 @@ class UserAdminViewsTests(AdminTestCase):
         user_pks = []
         user_pks = []
         for i in range(10):
         for i in range(10):
             test_user = UserModel.objects.create_user(
             test_user = UserModel.objects.create_user(
-                'Bob%s' % i,
-                'bob%s@test.com' % i,
-                'pass123',
-                requires_activation=1
+                'Bob%s' % i, 'bob%s@test.com' % i, 'pass123', 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(
@@ -118,12 +113,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'
-                ],
+                'ban_type': ['usernames', 'emails', 'domains', 'ip', 'ip_first', 'ip_two'],
                 'finalize': ''
                 '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)
 
 
@@ -132,16 +125,15 @@ class UserAdminViewsTests(AdminTestCase):
         user_pks = []
         user_pks = []
         for i in range(10):
         for i in range(10):
             test_user = UserModel.objects.create_user(
             test_user = UserModel.objects.create_user(
-                'Bob%s' % i,
-                'bob%s@test.com' % i,
-                'pass123',
-                requires_activation=1
+                'Bob%s' % i, 'bob%s@test.com' % i, 'pass123', 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)
 
 
@@ -150,29 +142,28 @@ class UserAdminViewsTests(AdminTestCase):
         user_pks = []
         user_pks = []
         for i in range(10):
         for i in range(10):
             test_user = UserModel.objects.create_user(
             test_user = UserModel.objects.create_user(
-                'Bob%s' % i,
-                'bob%s@test.com' % i,
-                'pass123',
-                requires_activation=1
+                'Bob%s' % i, 'bob%s@test.com' % i, 'pass123', 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),
@@ -180,7 +171,8 @@ class UserAdminViewsTests(AdminTestCase):
                 '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')
@@ -189,28 +181,30 @@ 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)
@@ -228,27 +222,29 @@ 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)
@@ -259,30 +255,32 @@ 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)
@@ -292,30 +290,32 @@ 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)
@@ -328,30 +328,32 @@ 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)
@@ -364,32 +366,34 @@ 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)
@@ -406,32 +410,34 @@ 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)
@@ -448,32 +454,34 @@ 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)
@@ -483,8 +491,9 @@ class UserAdminViewsTests(AdminTestCase):
     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 _ in range(10)]
         [post_thread(category, poster=test_user) for _ in range(10)]
@@ -506,8 +515,7 @@ class UserAdminViewsTests(AdminTestCase):
     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)
@@ -530,8 +538,9 @@ class UserAdminViewsTests(AdminTestCase):
     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)

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

@@ -41,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, '[]')
@@ -57,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)

+ 149 - 123
misago/users/tests/test_users_api.py

@@ -23,6 +23,7 @@ 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'
@@ -71,6 +72,7 @@ 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/'
@@ -88,7 +90,8 @@ 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)
@@ -98,7 +101,8 @@ 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
@@ -112,6 +116,7 @@ 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/'
@@ -129,7 +134,8 @@ 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)
@@ -139,7 +145,8 @@ 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
@@ -153,6 +160,7 @@ 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'
@@ -164,11 +172,7 @@ class RankListTests(AuthenticatedUserTestCase):
 
 
     def test_empty_list(self):
     def test_empty_list(self):
         """tab rank without members returns 200"""
         """tab rank without members returns 200"""
-        test_rank = Rank.objects.create(
-            name="Test rank",
-            slug="test-rank",
-            is_tab=True
-        )
+        test_rank = Rank.objects.create(name="Test rank", slug="test-rank", is_tab=True)
 
 
         response = self.client.get(self.link % test_rank.pk)
         response = self.client.get(self.link % test_rank.pk)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
@@ -199,15 +203,10 @@ class RankListTests(AuthenticatedUserTestCase):
 
 
     def test_disabled_users(self):
     def test_disabled_users(self):
         """api follows disabled users visibility"""
         """api follows disabled users visibility"""
-        test_rank = Rank.objects.create(
-            name="Test rank",
-            slug="test-rank",
-            is_tab=True
-        )
+        test_rank = Rank.objects.create(name="Test rank", slug="test-rank", 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)
@@ -225,6 +224,7 @@ 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='
@@ -245,9 +245,7 @@ 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"""
@@ -276,6 +274,7 @@ 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
@@ -285,79 +284,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)
@@ -369,11 +384,11 @@ 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
 
 
@@ -403,7 +418,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)
@@ -436,28 +450,24 @@ 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)
@@ -465,14 +475,10 @@ 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,
-            banned_value=self.other_user.username,
-            user_message='Nope!'
+            check_type=Ban.USERNAME, banned_value=self.other_user.username, user_message='Nope!'
         )
         )
 
 
         response = self.client.get(self.link)
         response = self.client.get(self.link)
@@ -487,11 +493,11 @@ 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
 
 
@@ -507,10 +513,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,
-        })
+        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)
@@ -518,10 +526,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()
@@ -533,10 +543,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()
@@ -548,20 +560,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()
@@ -571,10 +587,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()
@@ -584,14 +602,18 @@ 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):
@@ -602,14 +624,18 @@ 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 - 14
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"""
@@ -32,10 +31,7 @@ class ValidateEmailAvailableTests(TestCase):
 
 
 class ValidateEmailBannedTests(TestCase):
 class ValidateEmailBannedTests(TestCase):
     def setUp(self):
     def setUp(self):
-        Ban.objects.create(
-            check_type=Ban.EMAIL,
-            banned_value="ban@test.com"
-        )
+        Ban.objects.create(check_type=Ban.EMAIL, banned_value="ban@test.com")
 
 
     def test_unbanned_name(self):
     def test_unbanned_name(self):
         """unbanned email passes validation"""
         """unbanned email passes validation"""
@@ -65,14 +61,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"""
@@ -82,10 +76,7 @@ class ValidateUsernameAvailableTests(TestCase):
 
 
 class ValidateUsernameBannedTests(TestCase):
 class ValidateUsernameBannedTests(TestCase):
     def setUp(self):
     def setUp(self):
-        Ban.objects.create(
-            check_type=Ban.USERNAME,
-            banned_value="Bob"
-        )
+        Ban.objects.create(check_type=Ban.USERNAME, banned_value="Bob")
 
 
     def test_unbanned_name(self):
     def test_unbanned_name(self):
         """unbanned name passes validation"""
         """unbanned name passes validation"""

+ 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)

+ 6 - 10
misago/users/tokens.py

@@ -16,6 +16,8 @@ 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
 """
 """
+
+
 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()
@@ -46,16 +48,11 @@ def is_valid(user, token_type, token):
 
 
 def _make_hash(user, token_type):
 def _make_hash(user, token_type):
     seeds = (
     seeds = (
-        user.pk,
-        user.email,
-        user.password,
-        user.last_login.replace(microsecond=0, tzinfo=None),
-        token_type,
-        settings.SECRET_KEY,
+        user.pk, user.email, user.password, user.last_login.replace(microsecond=0, tzinfo=None),
+        token_type, 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,8 +60,7 @@ 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]
 
 
 
 
 """
 """

+ 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)

+ 13 - 11
misago/users/validators.py

@@ -19,11 +19,11 @@ from .bans import get_email_ban, get_username_ban
 USERNAME_RE = re.compile(r'^[0-9a-z]+$', re.IGNORECASE)
 USERNAME_RE = re.compile(r'^[0-9a-z]+$', re.IGNORECASE)
 
 
 UserModel = get_user_model()
 UserModel = get_user_model()
-
-
 """
 """
 Email validators
 Email 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)
@@ -53,6 +53,8 @@ def validate_email(value, exclude=None):
 """
 """
 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 +76,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,7 +84,8 @@ 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)
+            settings.username_length_min
+        )
         message = message % {'limit_value': settings.username_length_min}
         message = message % {'limit_value': settings.username_length_min}
         raise ValidationError(message)
         raise ValidationError(message)
 
 
@@ -91,7 +93,8 @@ def validate_username_length(value):
         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)
+            settings.username_length_max
+        )
         message = message % {'limit_value': settings.username_length_max}
         message = message % {'limit_value': settings.username_length_max}
         raise ValidationError(message)
         raise ValidationError(message)
 
 
@@ -117,10 +120,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 +133,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):
@@ -149,6 +149,8 @@ def validate_gmail_email(request, form, cleaned_data):
 """
 """
 Registration validation
 Registration validation
 """
 """
+
+
 def load_registration_validators(validators):
 def load_registration_validators(validators):
     loaded_validators = []
     loaded_validators = []
     for path in validators:
     for path in validators:

+ 1 - 0
misago/users/viewmodels/activeposters.py

@@ -27,4 +27,5 @@ class ActivePosters(object):
             '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
 
 

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

@@ -6,8 +6,7 @@ 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(

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

@@ -6,8 +6,8 @@ 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(
-            'rank', 'ban_cache', 'online_tracker').order_by('slug')
+        queryset = rank.user_set.select_related('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)
@@ -19,9 +19,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 - 20
misago/users/viewmodels/threads.py

@@ -13,19 +13,15 @@ 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)
-
-        posts_queryset = self.get_posts_queryset(
-            request.user, profile, threads_queryset
-        ).filter(
-            is_event=False,
-            is_hidden=False,
-            is_unapproved=False
+        threads_queryset = self.get_threads_queryset(request, threads_categories, profile)
+
+        posts_queryset = self.get_posts_queryset(request.user, profile, threads_queryset).filter(
+            is_event=False, is_hidden=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 +30,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,8 +47,7 @@ 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(
@@ -62,8 +56,7 @@ class UserThreads(object):
 
 
     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)
@@ -71,10 +64,7 @@ class UserThreads(object):
         return context
         return context
 
 
     def get_template_context(self):
     def get_template_context(self):
-        return {
-            'posts': self.posts,
-            'paginator': self.paginator
-        }
+        return {'posts': self.posts, 'paginator': self.paginator}
 
 
 
 
 UserFeedSerializer = FeedSerializer.exclude_fields('poster')
 UserFeedSerializer = FeedSerializer.exclude_fields('poster')

+ 19 - 12
misago/users/views/activation.py

@@ -17,14 +17,13 @@ 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({
-        'SEND_ACTIVATION_API': reverse('misago:api:send-activation')
-    })
+    request.frontend_context.update({'SEND_ACTIVATION_API': reverse('misago:api:send-activation')})
     return render(request, 'misago/activation/request.html')
     return render(request, 'misago/activation/request.html')
 
 
 
 
@@ -47,8 +46,10 @@ def activate_by_token(request, pk, token):
             raise ActivationStopped(message)
             raise ActivationStopped(message)
 
 
         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 = _(
+                "%(user)s, your activation link is invalid. "
+                "Try again or request new activation link."
+            )
             message = message % {'user': inactive_user.username}
             message = message % {'user': inactive_user.username}
             raise ActivationError(message)
             raise ActivationError(message)
 
 
@@ -57,18 +58,24 @@ def activate_by_token(request, pk, token):
             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 - 14
misago/users/views/admin/bans.py

@@ -20,23 +20,17 @@ class BanAdmin(generic.AdminBaseMixin):
 
 
 class BansList(BanAdmin, generic.ListView):
 class BansList(BanAdmin, generic.ListView):
     items_per_page = 30
     items_per_page = 30
-    ordering = (
-        ('-id', _("From newest")),
-        ('id', _("From oldest")),
-        ('banned_value', _("A to z")),
-        ('-banned_value', _("Z to a")),
-    )
+    ordering = (('-id', _("From newest")), ('id', _("From oldest")), ('banned_value', _("A to z")),
+                ('-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):

+ 50 - 66
misago/users/views/admin/users.py

@@ -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,43 +50,40 @@ class UserAdmin(generic.AdminBaseMixin):
 
 
 class UsersList(UserAdmin, generic.ListView):
 class UsersList(UserAdmin, generic.ListView):
     items_per_page = 24
     items_per_page = 24
-    ordering = (
-        ('-id', _("From newest")),
-        ('id', _("From oldest")),
-        ('slug', _("A to z")),
-        ('-slug', _("Z to a")),
-        ('posts', _("Biggest posters")),
-        ('-posts', _("Smallest posters")),
-    )
+    ordering = (('-id', _("From newest")), ('id', _("From oldest")), ('slug', _("A to z")),
+                ('-slug', _("Z to a")), ('posts', _("Biggest 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 = [
-        {
-            'action': 'activate',
-            'name': _("Activate accounts"),
-            'icon': 'fa fa-check-square-o',
-        },
-        {
-            'action': 'ban',
-            'name': _("Ban users"),
-            'icon': 'fa fa-lock',
-        },
-        {
-            'action': 'delete_accounts',
-            'name': _("Delete accounts"),
-            'icon': 'fa fa-times-circle',
-            'confirmation': _("Are you sure you want to delete selected users?"),
-        },
-        {
-            'action': 'delete_all',
-            'name': _("Delete all"),
-            'icon': 'fa fa-eraser',
-            'confirmation': _("Are you sure you want to delete selected "
-                              "users? This will also delete all content "
-                              "associated with their accounts."),
-            'is_atomic': False,
-        }
-    ]
+    mass_actions = [{
+        'action': 'activate',
+        'name': _("Activate accounts"),
+        'icon': 'fa fa-check-square-o',
+    }, {
+        'action': 'ban',
+        'name': _("Ban users"),
+        'icon': 'fa fa-lock',
+    }, {
+        'action': 'delete_accounts',
+        'name': _("Delete accounts"),
+        'icon': 'fa fa-times-circle',
+        'confirmation': _("Are you sure you want to delete selected users?"),
+    }, {
+        'action':
+            'delete_all',
+        'name':
+            _("Delete all"),
+        'icon':
+            'fa fa-eraser',
+        'confirmation':
+            _(
+                "Are you sure you want to delete selected "
+                "users? This will also delete all content "
+                "associated with their accounts."
+            ),
+        'is_atomic':
+            False,
+    }]
 
 
     def get_queryset(self):
     def get_queryset(self):
         qs = super(UsersList, self).get_queryset()
         qs = super(UsersList, self).get_queryset()
@@ -109,12 +107,9 @@ 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.")
             message = _("Selected users accounts have been activated.")
             messages.success(request, message)
             messages.success(request, message)
@@ -172,10 +167,7 @@ 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:
@@ -186,17 +178,19 @@ class UsersList(UserAdmin, generic.ListView):
                             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.")
                 message = _("Selected users have been banned.")
                 messages.success(request, message)
                 messages.success(request, message)
                 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:
@@ -225,9 +219,7 @@ class UsersList(UserAdmin, generic.ListView):
         messages.success(request, message)
         messages.success(request, message)
 
 
         return self.render(
         return self.render(
-            request,
-            template='misago/admin/users/delete.html',
-            context={
+            request, template='misago/admin/users/delete.html', context={
                 'users': users,
                 'users': users,
             }
             }
         )
         )
@@ -258,8 +250,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)
 
 
 
 
@@ -276,8 +267,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'])
@@ -313,8 +303,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):
@@ -329,7 +318,8 @@ 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))
@@ -355,10 +345,7 @@ class DeleteThreadsStep(DeletionStep):
         else:
         else:
             is_completed = True
             is_completed = True
 
 
-        return {
-            'deleted_count': deleted_threads,
-            'is_completed': is_completed
-        }
+        return {'deleted_count': deleted_threads, 'is_completed': is_completed}
 
 
 
 
 class DeletePostsStep(DeletionStep):
 class DeletePostsStep(DeletionStep):
@@ -388,10 +375,7 @@ class DeletePostsStep(DeletionStep):
         else:
         else:
             is_completed = True
             is_completed = True
 
 
-        return {
-            'deleted_count': deleted_posts,
-            'is_completed': is_completed
-        }
+        return {'deleted_count': deleted_posts, 'is_completed': is_completed}
 
 
 
 
 class DeleteAccountStep(DeletionStep):
 class DeleteAccountStep(DeletionStep):

+ 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 - 13
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,16 +34,16 @@ 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.")
+        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}
             message = message % {'user': requesting_user.username}
             raise ResetError(message)
             raise ResetError(message)
 
 
         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 = _("%(user)s, your link is invalid. " "Please try again or request new link.")
             message = message % {'user': requesting_user.username}
             message = message % {'user': requesting_user.username}
             raise ResetError(message)
             raise ResetError(message)
 
 
@@ -50,14 +51,18 @@ def reset_password_form(request, pk, token):
         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)
+        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,
-    })
+    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
             })
             })
 
 

+ 17 - 11
misago/users/views/options.py

@@ -19,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,8 +34,8 @@ def confirm_change_view(f):
         try:
         try:
             return f(request, token)
             return f(request, token)
         except ChangeError:
         except ChangeError:
-            return render(request, 'misago/options/credentials_error.html',
-                status=400)
+            return render(request, 'misago/options/credentials_error.html', status=400)
+
     return decorator
     return decorator
 
 
 
 
@@ -54,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
@@ -70,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'
+)