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
 indent_dictionary_value = true
 join_multiple_lines = false
-spaces_before_comment = 4
 split_arguments_when_comma_terminated = true
 split_before_first_argument = true
 split_before_logical_operator = true

+ 1 - 1
cleansource

@@ -1,5 +1,5 @@
 #!/bin/bash
 
+yapf -ir misago -e '*/project_template/**/*.py'
 isort -rc misago
-yapf -ir 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):
     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):
-        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 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)
 
     for permission, compare in permissions.items():

+ 1 - 0
misago/acl/decorators.py

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

+ 4 - 6
misago/acl/forms.py

@@ -25,8 +25,7 @@ def get_permissions_forms(role, data=None):
             module.change_permissions_form
         except AttributeError:
             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)
 
@@ -34,9 +33,8 @@ def get_permissions_forms(role, data=None):
             if data:
                 perms_forms.append(FormType(data, prefix=extension))
             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

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

@@ -9,14 +9,17 @@ from misago.acl.models import permissions_default
 
 class Migration(migrations.Migration):
 
-    dependencies = [
-    ]
+    dependencies = []
 
     operations = [
         migrations.CreateModel(
             name='Role',
             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)),
                 ('special_role', models.CharField(max_length=255, null=True, blank=True)),
                 ('permissions', JSONField(default=permissions_default)),
@@ -24,6 +27,6 @@ class Migration(migrations.Migration):
             options={
                 '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': {
                 'can_see_ban_details': 1,
             },
-
             'misago.users.permissions.moderation': {
                 'can_ban_users': 1,
                 '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(True, False), False)
 
-
     def test_lower_non_zero(self):
         """lower non-zero wins test"""
         self.assertEqual(algebra.lower_non_zero(1, 3), 1)
@@ -73,13 +72,14 @@ class SumACLTests(TestCase):
         }
 
         acl = algebra.sum_acls(
-            defaults, acls=test_acls,
+            defaults,
+            acls=test_acls,
             can_see=algebra.greater,
             can_hear=algebra.greater,
             max_speed=algebra.greater,
             min_age=algebra.lower,
             speed_limit=algebra.greater_or_zero
-            )
+        )
 
         self.assertEqual(acl['can_see'], 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):
     def test_get_authenticated_acl(self):
         """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)
 

+ 24 - 12
misago/acl/tests/test_roleadmin_views.py

@@ -26,8 +26,9 @@ class RoleAdminViewsTests(AdminTestCase):
         self.assertEqual(response.status_code, 200)
 
         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)
 
@@ -39,19 +40,24 @@ class RoleAdminViewsTests(AdminTestCase):
     def test_edit_view(self):
         """edit role view has no showstoppers"""
         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')
 
-        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.assertContains(response, 'Test Role')
 
         response = self.client.post(
             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)
 
@@ -63,23 +69,29 @@ class RoleAdminViewsTests(AdminTestCase):
     def test_users_view(self):
         """users with this role view has no showstoppers"""
         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')
 
-        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)
 
     def test_delete_view(self):
         """delete role view has no showstoppers"""
         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')
-        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)
 
         # 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):
-    ordering = (('name', None),)
+    ordering = (('name', None), )
 
 
 class RoleFormMixin(object):
@@ -43,8 +43,7 @@ class RoleFormMixin(object):
                 form.instance.permissions = new_permissions
                 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:
                     return redirect(request.path)
@@ -53,13 +52,11 @@ class RoleFormMixin(object):
             elif form.is_valid() and len(perms_forms) != valid_forms:
                 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):
@@ -73,8 +70,7 @@ class EditRole(RoleFormMixin, RoleAdmin, generic.ModelFormView):
 class DeleteRole(RoleAdmin, generic.ButtonView):
     def check_permissions(self, request, target):
         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}
 
     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 .discoverer import discover_misago_admin  # noqa
 
-
 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
     if admin_namespace and user.is_staff:
         start_admin_session(request, user)
+
+
 dj_auth.signals.user_logged_in.connect(django_login_handler)
 
 
 def django_logout_handler(sender, **kwargs):
     close_admin_session(kwargs['request'])
+
+
 dj_auth.signals.user_logged_out.connect(django_logout_handler)

+ 22 - 17
misago/admin/hierarchy.py

@@ -81,8 +81,7 @@ class Node(object):
         try:
             return self._children_dict[namespace]
         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):
         return False
@@ -100,25 +99,21 @@ class AdminHierarchyBuilder(object):
         while self.nodes_record:
             iterations += 1
             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)
 
             for index, node in enumerate(self.nodes_record):
                 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']]
                     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']:
-                        node_added = parent.add_node(
-                            node_obj, before=node['before'])
+                        node_added = parent.add_node(node_obj, before=node['before'])
                     else:
                         node_added = parent.add_node(node_obj)
 
@@ -133,11 +128,21 @@ class AdminHierarchyBuilder(object):
 
         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:
-            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:
             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):
         """get_protected_namespace returns true for protected links"""
         links_prefix = reverse('misago:admin:index')
-        TEST_CASES = (
-            '',
-            'somewhere/',
-            'ejksajdlksajldjskajdlksajlkdas',
-        )
+        TEST_CASES = ('', 'somewhere/', 'ejksajdlksajldjskajdlksajlkdas', )
 
         for case in TEST_CASES:
             request = FakeRequest(links_prefix + case)
@@ -34,11 +30,7 @@ class AdminProtectedNamespaceTests(TestCase):
 
     def test_invalid_cases(self):
         """get_protected_namespace returns none for other links"""
-        TEST_CASES = (
-            '/',
-            '/somewhere/',
-            '/ejksajdlksajldjskajdlksajlkdas',
-        )
+        TEST_CASES = ('/', '/somewhere/', '/ejksajdlksajldjskajdlksajlkdas', )
 
         for case in TEST_CASES:
             request = FakeRequest(case)
@@ -57,8 +49,9 @@ class AdminLoginViewTests(TestCase):
     def test_login_returns_200_on_invalid_post(self):
         """form handles invalid data gracefully"""
         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, "Sign in")
@@ -74,8 +67,9 @@ class AdminLoginViewTests(TestCase):
         user.save()
 
         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.")
 
@@ -88,8 +82,9 @@ class AdminLoginViewTests(TestCase):
         user.save()
 
         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.")
 
@@ -102,8 +97,9 @@ class AdminLoginViewTests(TestCase):
         user.save()
 
         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)
 
@@ -116,8 +112,9 @@ class AdminLoginViewTests(TestCase):
         user.save()
 
         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)
 
@@ -199,8 +196,7 @@ class Admin404ErrorTests(AdminTestCase):
 
         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):
@@ -214,13 +210,11 @@ class AdminGenericViewsTests(AdminTestCase):
         self.assertIn('redirected=1', response['location'])
 
         # 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)
 
     def test_list_search_unicode_handling(self):
         """querystring creation handles unicode strings"""
         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)

+ 5 - 4
misago/admin/testutils.py

@@ -9,8 +9,9 @@ class AdminTestCase(SuperUserTestCase):
         self.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'))

+ 0 - 1
misago/admin/urls.py

@@ -14,7 +14,6 @@ urlpatterns = [
     url(r'^logout/$', auth.logout, name='logout'),
 ]
 
-
 # Discover admin and register patterns
 admin.discover_misago_admin()
 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
 
 
-
 def get_protected_namespace(request):
     for namespace in settings.MISAGO_ADMIN_NAMESPACES:
         try:

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

@@ -28,8 +28,7 @@ def login(request):
             auth.login(request, form.user_cache)
             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
@@ -37,8 +36,7 @@ def login(request):
 def logout(request):
     if request.method == 'POST':
         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')
     else:
         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):
         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
         return response
     else:
@@ -27,6 +25,7 @@ def admin_error_page(f):
             return _error_page(request, *args, **kwargs)
         else:
             return f(request, *args, **kwargs)
+
     return decorator
 
 
@@ -35,8 +34,8 @@ def _csrf_failure(request, reason=""):
     if is_admin_session(request):
         update_admin_session(request)
         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:
         response = render(request, 'misago/admin/errorpages/csrf_failure.html')
 
@@ -50,4 +49,5 @@ def admin_csrf_failure(f):
             return _csrf_failure(request, *args, **kwargs)
         else:
             return f(request, *args, **kwargs)
+
     return decorator

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

@@ -1,5 +1,4 @@
 from .mixin import AdminBaseMixin
 from .base import AdminView
 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()
             # Does not work on Python 3:
             # return select_for_update.get(pk=kwargs[kwargs.keys()[0]])
-            (pk,) = kwargs.values()
+            (pk, ) = kwargs.values()
             return select_for_update.get(pk=pk)
         else:
             return self.get_model()()
@@ -37,17 +37,17 @@ class TargetedView(AdminView):
             return self.wrapped_dispatch(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):
         pass
@@ -69,7 +69,8 @@ class FormView(TargetedView):
     def handle_form(self, form, request):
         raise NotImplementedError(
             "You have to define your own handle_form method to handle "
-            "form submissions.")
+            "form submissions."
+        )
 
     def real_dispatch(self, request, target):
         FormType = self.create_form_type(request)

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

@@ -68,6 +68,7 @@ class ListView(AdminView):
     """
     Dispatch response
     """
+
     def dispatch(self, request, *args, **kwargs):
         mass_actions_list = self.mass_actions or []
         extra_actions_list = self.extra_actions or []
@@ -76,25 +77,19 @@ class ListView(AdminView):
 
         context = {
             'items': self.get_queryset(),
-
             'paginator': None,
             'page': None,
-
             'order_by': [],
             'order': None,
-
             'search_form': None,
             'active_filters': {},
-
             'querystring': '',
             'query_order': {},
             'query_filters': {},
-
             'selected_items': [],
             'selection_label': self.selection_label,
             'empty_selection_label': self.empty_selection_label,
             'mass_actions': mass_actions_list,
-
             'extra_actions': 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)
             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
                 session_key = self.ordering_session_key
                 request.session[session_key] = ordering_methods['GET']
@@ -128,14 +122,12 @@ class ListView(AdminView):
         SearchForm = self.get_search_form(request)
         if SearchForm:
             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'):
                 # Clear filters from querystring
                 request.session.pop(self.filters_session_key, None)
                 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
                     filtering_methods['GET'] != filtering_methods['session']):
@@ -159,8 +151,7 @@ class ListView(AdminView):
             try:
                 self.paginate_items(context, kwargs.get('page', 0))
             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'):
             return redirect('%s%s' % (request.path_info, context['querystring']))
@@ -178,7 +169,8 @@ class ListView(AdminView):
             page = 1
 
         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['items'] = context['page'].object_list
 
@@ -236,11 +228,13 @@ class ListView(AdminView):
 
         if context['active_filters']:
             context['items'] = context['search_form'].filter_queryset(
-                active_filters, context['items'])
+                active_filters, context['items']
+            )
 
     """
     Order list items
     """
+
     @property
     def ordering_session_key(self):
         return 'misago_admin_%s_order_by' % self.root_link
@@ -290,8 +284,7 @@ class ListView(AdminView):
 
             if order_by == method:
                 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']:
                 if order_as_dict['type'] == 'desc':
                     order_as_dict['order_by'] = order_as_dict['order_by'][1:]
@@ -300,6 +293,7 @@ class ListView(AdminView):
     """
     Mass actions
     """
+
     def handle_mass_action(self, request, context):
         limit = self.items_per_page or 64
         action = self.select_mass_action(request.POST.get('action'))
@@ -332,6 +326,7 @@ class ListView(AdminView):
     """
     Querystring builder
     """
+
     def make_querystring(self, context):
         values = {}
         filter_values = {}

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

@@ -21,18 +21,21 @@ UserModel = get_user_model()
 
 def admin_index(request):
     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):
@@ -54,15 +57,9 @@ def check_version(request):
             for i in range(3):
                 if latest[i] > current[i]:
                     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
             else:
                 formats = {'current': __version__}
@@ -74,8 +71,5 @@ def check_version(request):
             cache.set(VERSION_CHECK_CACHE_KEY, version, 180)
         except (RequestException, IndexError, KeyError, ValueError):
             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)

+ 0 - 1
misago/categories/__init__.py

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

+ 6 - 3
misago/categories/admin.py

@@ -15,7 +15,8 @@ class MisagoAdminExtension(object):
 
         # Nodes
         urlpatterns.namespace(r'^nodes/', 'nodes', 'categories')
-        urlpatterns.patterns('categories:nodes',
+        urlpatterns.patterns(
+            'categories:nodes',
             url(r'^$', CategoriesList.as_view(), name='index'),
             url(r'^new/$', NewCategory.as_view(), name='new'),
             url(r'^edit/(?P<pk>\d+)/$', EditCategory.as_view(), name='edit'),
@@ -27,7 +28,8 @@ class MisagoAdminExtension(object):
 
         # Category Roles
         urlpatterns.namespace(r'^categories/', 'categories', 'permissions')
-        urlpatterns.patterns('permissions:categories',
+        urlpatterns.patterns(
+            'permissions:categories',
             url(r'^$', CategoryRolesList.as_view(), name='index'),
             url(r'^new/$', NewCategoryRole.as_view(), name='new'),
             url(r'^edit/(?P<pk>\d+)/$', EditCategoryRole.as_view(), name='edit'),
@@ -35,7 +37,8 @@ class MisagoAdminExtension(object):
         )
 
         # Change Role Category Permissions
-        urlpatterns.patterns('permissions:users',
+        urlpatterns.patterns(
+            'permissions:users',
             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
 """
+
+
 class AdminCategoryFieldMixin(object):
     def __init__(self, *args, **kwargs):
         self.base_level = kwargs.pop('base_level', 1)
@@ -42,19 +44,17 @@ class AdminCategoryChoiceField(AdminCategoryFieldMixin, TreeNodeChoiceField):
     pass
 
 
-class AdminCategoryMultipleChoiceField(
-        AdminCategoryFieldMixin, TreeNodeMultipleChoiceField):
+class AdminCategoryMultipleChoiceField(AdminCategoryFieldMixin, TreeNodeMultipleChoiceField):
     pass
 
 
 """
 Forms
 """
+
+
 class CategoryFormBase(forms.ModelForm):
-    name = forms.CharField(
-        label=_("Name"),
-        validators=[validate_sluggable()]
-    )
+    name = forms.CharField(label=_("Name"), validators=[validate_sluggable()])
     description = forms.CharField(
         label=_("Description"),
         max_length=2048,
@@ -65,8 +65,10 @@ class CategoryFormBase(forms.ModelForm):
     css_class = forms.CharField(
         label=_("CSS class"),
         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(
         label=_("Closed category"),
@@ -77,22 +79,28 @@ class CategoryFormBase(forms.ModelForm):
     css_class = forms.CharField(
         label=_("CSS class"),
         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(
         label=_("Thread age"),
         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(
         label=_("Last reply"),
         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:
@@ -134,29 +142,39 @@ def CategoryFormFactory(instance):
         not_siblings = not_siblings | models.Q(rght__gt=instance.rght)
         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):
@@ -169,15 +187,16 @@ class DeleteCategoryFormBase(forms.ModelForm):
 
         if data.get('move_threads_to'):
             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)
 
             moving_to_child = self.instance.has_child(data['move_threads_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)
 
         return data
@@ -186,13 +205,14 @@ class DeleteCategoryFormBase(forms.ModelForm):
 def DeleteFormFactory(instance):
     content_queryset = Category.objects.all_categories().order_by('lft')
     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)
@@ -208,7 +228,7 @@ def DeleteFormFactory(instance):
             required=False
         )
 
-    return type('DeleteCategoryFormFinal', (DeleteCategoryFormBase,), fields)
+    return type('DeleteCategoryFormFinal', (DeleteCategoryFormBase, ), fields)
 
 
 class CategoryRoleForm(forms.ModelForm):
@@ -221,29 +241,33 @@ class CategoryRoleForm(forms.ModelForm):
 
 def RoleCategoryACLFormFactory(category, category_roles, selected_role):
     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):
     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(
             name='Category',
             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)),
                 ('name', 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)),
                 ('tree_id', 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={
                 'abstract': False,
             },
-            bases=(models.Model,),
+            bases=(models.Model, ),
         ),
         migrations.CreateModel(
             name='CategoryRole',
             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)),
                 ('special_role', models.CharField(max_length=255, null=True, blank=True)),
                 ('permissions', JSONField(default=permissions_default)),
@@ -62,18 +93,28 @@ class Migration(migrations.Migration):
             options={
                 'abstract': False,
             },
-            bases=(models.Model,),
+            bases=(models.Model, ),
         ),
         migrations.CreateModel(
             name='RoleCategoryACL',
             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')),
             ],
-            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)
 
     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(
@@ -174,9 +172,7 @@ def create_default_categories_roles(apps, schema_editor):
     )
 
     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(
             model_name='category',
             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,
         ),
     ]

+ 3 - 16
misago/categories/models.py

@@ -60,12 +60,7 @@ class CategoryManager(TreeManager):
 
 @python_2_unicode_compatible
 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)
     name = models.CharField(max_length=255)
     slug = models.CharField(max_length=255)
@@ -75,11 +70,7 @@ class Category(MPTTModel):
     posts = models.PositiveIntegerField(default=0)
     last_post_on = models.DateTimeField(null=True, blank=True)
     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_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_replied_after = models.PositiveIntegerField(default=0)
     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)
 

+ 16 - 1
misago/categories/permissions.py

@@ -15,6 +15,8 @@ from .models import Category, CategoryRole, RoleCategoryACL
 """
 Admin Permissions Form
 """
+
+
 class PermissionsForm(forms.Form):
     legend = _("Category access")
 
@@ -32,6 +34,8 @@ def change_permissions_form(role):
 """
 ACL Builder
 """
+
+
 def build_acl(acl, roles, key_name):
     new_acl = {
         'visible_categories': [],
@@ -75,7 +79,10 @@ def build_category_acl(acl, category, categories_roles, key_name):
         '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_browse=algebra.greater
     )
@@ -91,6 +98,8 @@ def build_category_acl(acl, category, categories_roles, key_name):
 """
 ACL's for targets
 """
+
+
 def add_acl_to_category(user, target):
     target.acl['can_see'] = can_see_category(user, target)
     target.acl['can_browse'] = can_browse_category(user, target)
@@ -121,6 +130,8 @@ def register_with(registry):
 """
 ACL tests
 """
+
+
 def allow_see_category(user, target):
     try:
         category_id = target.pk
@@ -129,6 +140,8 @@ def allow_see_category(user, target):
 
     if not category_id in user.acl_cache['visible_categories']:
         raise Http404()
+
+
 can_see_category = return_boolean(allow_see_category)
 
 
@@ -137,4 +150,6 @@ def allow_browse_category(user, target):
     if not target_acl['can_browse']:
         message = _('You don\'t have permission to browse "%(category)s" contents.')
         raise PermissionDenied(message % {'category': target.name})
+
+
 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):
     """util for serializing last activity details"""
+
     def decorator(self, obj):
         if not obj.last_thread_id:
             return None
 
         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 f(self, obj)
+
     return decorator
 
 
@@ -44,28 +42,10 @@ class CategorySerializer(serializers.ModelSerializer, MutableFields):
     class Meta:
         model = Category
         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):
@@ -103,10 +83,12 @@ class CategorySerializer(serializers.ModelSerializer, MutableFields):
     @last_activity_detail
     def get_last_poster_url(self, obj):
         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:
             return None
 

+ 3 - 4
misago/categories/signals.py

@@ -7,14 +7,13 @@ from .models import Category
 
 delete_category_content = Signal()
 move_category_content = Signal(providing_args=["new_category"])
-
-
 """
 Signal handlers
 """
+
+
 @receiver(username_changed)
 def update_usernames(sender, **kwargs):
     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 = []
         for category in queryset:
             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):
-            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):
             _category = current_tree[i]
             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]:
-                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]:
-                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]:
-                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):
     def test_link_registered(self):
         """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'))
 
     def test_list_view(self):
         """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')
 
@@ -68,8 +57,7 @@ class CategoryAdminViewsTests(CategoryAdminTestCate):
         for descendant in root.get_descendants():
             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.assertContains(response, 'No categories')
@@ -79,8 +67,7 @@ class CategoryAdminViewsTests(CategoryAdminTestCate):
         root = Category.objects.root_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)
 
         response = self.client.post(
@@ -91,11 +78,11 @@ class CategoryAdminViewsTests(CategoryAdminTestCate):
                 'new_parent': root.pk,
                 'prune_started_after': 0,
                 'prune_replied_after': 0,
-            })
+            }
+        )
         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')
 
         test_category = Category.objects.get(slug='test-category')
@@ -114,7 +101,8 @@ class CategoryAdminViewsTests(CategoryAdminTestCate):
                 'new_parent': root.pk,
                 'prune_started_after': 0,
                 'prune_replied_after': 0,
-            })
+            }
+        )
         self.assertEqual(response.status_code, 302)
 
         test_other_category = Category.objects.get(slug='test-other-category')
@@ -134,7 +122,8 @@ class CategoryAdminViewsTests(CategoryAdminTestCate):
                 'copy_permissions': test_category.pk,
                 'prune_started_after': 0,
                 'prune_replied_after': 0,
-            })
+            }
+        )
         self.assertEqual(response.status_code, 302)
 
         test_subcategory = Category.objects.get(slug='test-subcategory')
@@ -147,8 +136,7 @@ class CategoryAdminViewsTests(CategoryAdminTestCate):
             (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')
 
     def test_edit_view(self):
@@ -158,15 +146,13 @@ class CategoryAdminViewsTests(CategoryAdminTestCate):
         first_category = Category.objects.get(slug='first-category')
 
         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)
 
         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)
 
         response = self.client.post(
@@ -177,29 +163,28 @@ class CategoryAdminViewsTests(CategoryAdminTestCate):
                 'new_parent': root.pk,
                 'prune_started_after': 0,
                 'prune_replied_after': 0,
-            })
+            }
+        )
         self.assertEqual(response.status_code, 302)
 
         test_category = Category.objects.get(slug='test-category')
 
         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')
 
         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={
                 'name': 'Test Category Edited',
                 'new_parent': root.pk,
                 'role': 'category',
                 'prune_started_after': 0,
                 'prune_replied_after': 0,
-            })
+            }
+        )
         self.assertEqual(response.status_code, 302)
 
         self.assertValidTree([
@@ -208,21 +193,19 @@ class CategoryAdminViewsTests(CategoryAdminTestCate):
             (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')
 
         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={
                 'name': 'Test Category Edited',
                 'new_parent': first_category.pk,
                 'role': 'category',
                 'prune_started_after': 0,
                 'prune_replied_after': 0,
-            })
+            }
+        )
         self.assertEqual(response.status_code, 302)
 
         self.assertValidTree([
@@ -231,8 +214,7 @@ class CategoryAdminViewsTests(CategoryAdminTestCate):
             (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')
 
     def test_move_views(self):
@@ -240,26 +222,31 @@ class CategoryAdminViewsTests(CategoryAdminTestCate):
         root = Category.objects.root_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')
 
-        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')
 
         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.assertValidTree([
@@ -270,9 +257,8 @@ class CategoryAdminViewsTests(CategoryAdminTestCate):
         ])
 
         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.assertValidTree([
@@ -283,9 +269,8 @@ class CategoryAdminViewsTests(CategoryAdminTestCate):
         ])
 
         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.assertValidTree([
@@ -296,9 +281,8 @@ class CategoryAdminViewsTests(CategoryAdminTestCate):
         ])
 
         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.assertValidTree([
@@ -309,9 +293,8 @@ class CategoryAdminViewsTests(CategoryAdminTestCate):
         ])
 
         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.assertValidTree([
@@ -327,7 +310,6 @@ class CategoryAdminDeleteViewTests(CategoryAdminTestCate):
         super(CategoryAdminDeleteViewTests, self).setUp()
         self.root = Category.objects.root_category()
         self.first_category = Category.objects.get(slug='first-category')
-
         """
         Create categories tree for test cases:
 
@@ -341,53 +323,71 @@ class CategoryAdminDeleteViewTests(CategoryAdminTestCate):
         Category E
           + 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_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.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.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.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')
 
     def test_delete_category_move_contents(self):
@@ -397,19 +397,17 @@ class CategoryAdminDeleteViewTests(CategoryAdminTestCate):
         self.assertEqual(Thread.objects.count(), 10)
 
         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)
 
         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={
                 'move_children_to': self.category_e.pk,
                 'move_threads_to': self.category_d.pk,
-            })
+            }
+        )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(Category.objects.all_categories().count(), 6)
         self.assertEqual(Thread.objects.count(), 10)
@@ -430,19 +428,15 @@ class CategoryAdminDeleteViewTests(CategoryAdminTestCate):
             testutils.post_thread(self.category_b)
 
         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)
 
         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(Category.objects.all_categories().count(), 4)
@@ -463,19 +457,17 @@ class CategoryAdminDeleteViewTests(CategoryAdminTestCate):
         self.assertEqual(Thread.objects.count(), 10)
 
         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)
 
         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={
                 'move_children_to': '',
                 'move_threads_to': '',
-            })
+            }
+        )
         self.assertEqual(response.status_code, 302)
 
         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):
     def test_link_registered(self):
         """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'))
 
     def test_list_view(self):
         """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)
 
     def test_new_view(self):
         """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)
 
         response = self.client.post(
             reverse('misago:admin:permissions:categories:new'),
-            data=fake_data({'name': 'Test CategoryRole'}))
+            data=fake_data({
+                'name': 'Test CategoryRole'
+            })
+        )
         self.assertEqual(response.status_code, 302)
 
         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)
 
     def test_edit_view(self):
         """edit role view has no showstoppers"""
         self.client.post(
             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')
 
         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')
 
         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)
 
         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)
 
     def test_delete_view(self):
         """delete role view has no showstoppers"""
         self.client.post(
             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')
         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.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)
 
     def test_change_category_roles_view(self):
@@ -90,7 +92,6 @@ class CategoryRoleAdminViewsTests(AdminTestCase):
         root = Category.objects.root_category()
         for descendant in root.get_descendants():
             descendant.delete()
-
         """
         Create categories tree for test cases:
 
@@ -100,46 +101,55 @@ class CategoryRoleAdminViewsTests(AdminTestCase):
           + Category D
         """
         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')
 
         self.assertEqual(Category.objects.count(), 3)
-
         """
         Create test roles
         """
         self.client.post(
             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(
             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_b = Role.objects.get(name='Test Role B')
 
         self.client.post(
             reverse('misago:admin:permissions:categories:new'),
-            data=fake_data({'name': 'Test Comments'}))
+            data=fake_data({
+                'name': 'Test Comments'
+            })
+        )
         self.client.post(
             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_full = CategoryRole.objects.get(name='Test Full')
-
         """
         Test view itself
         """
         # See if form page is rendered
         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_role_a.name)
         self.assertContains(response, test_role_b.name)
@@ -148,28 +158,25 @@ class CategoryRoleAdminViewsTests(AdminTestCase):
 
         # Assign roles to categories
         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={
                 ('%s-category_role' % test_role_a.pk): role_full.pk,
                 ('%s-category_role' % test_role_b.pk): role_comments.pk,
-            })
+            }
+        )
         self.assertEqual(response.status_code, 302)
 
         # Check that roles were assigned
         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):
         """change role categories perms view works"""
         self.client.post(
             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')
 
@@ -179,11 +186,9 @@ class CategoryRoleAdminViewsTests(AdminTestCase):
 
         self.assertEqual(Category.objects.count(), 2)
         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)
-
         """
         Create categories tree for test cases:
 
@@ -193,45 +198,56 @@ class CategoryRoleAdminViewsTests(AdminTestCase):
           + Category D
         """
         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_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')
 
-        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')
 
         self.assertEqual(Category.objects.count(), 6)
 
         # See if form page is rendered
         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_b.name)
         self.assertContains(response, category_c.name)
@@ -240,46 +256,46 @@ class CategoryRoleAdminViewsTests(AdminTestCase):
         # Set test roles
         self.client.post(
             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')
 
         self.client.post(
             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')
 
         # See if form contains those roles
         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_full.name)
 
         # Assign roles to categories
         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={
                 ('%s-role' % category_a.pk): role_comments.pk,
                 ('%s-role' % category_b.pk): role_comments.pk,
                 ('%s-role' % category_c.pk): role_full.pk,
                 ('%s-role' % category_d.pk): role_full.pk,
-            })
+            }
+        )
         self.assertEqual(response.status_code, 302)
 
         # Check that roles were assigned
         categories_acls = test_role.categories_acls
         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(
-            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.first_category = Category.objects.get(slug='first-category')
-
         """
         Create categories tree for test cases:
 
@@ -29,43 +28,52 @@ class CategoriesUtilsTests(AuthenticatedUserTestCase):
         Category(
             name='Category A',
             slug='category-a',
-        ).insert_at(self.root, position='last-child', save=True)
+        ).insert_at(
+            self.root, position='last-child', save=True
+        )
         Category(
             name='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')
 
         Category(
             name='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')
 
         Category(
             name='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(
             name='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')
         Category(
             name='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': []}
         for category in Category.objects.all_categories():
             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)
 
     def test_root_categories_tree_no_parent(self):
@@ -73,24 +81,19 @@ class CategoriesUtilsTests(AuthenticatedUserTestCase):
         categories_tree = get_categories_tree(self.user)
         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):
         """get_categories_tree returns all children of given node"""
         categories_tree = get_categories_tree(self.user, self.category_a)
         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):
         """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)
 
     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
 
-
 if settings.MISAGO_THREADS_ON_INDEX:
     URL_PATH = r'^categories/$'
 else:
     URL_PATH = r'^$'
 
-
 urlpatterns = [
     url(URL_PATH, categories, name='categories'),
 

+ 1 - 3
misago/categories/utils.py

@@ -13,9 +13,7 @@ def get_categories_tree(user, parent=None):
     else:
         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)
 

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

@@ -59,13 +59,13 @@ class CategoryFormMixin(object):
     def handle_form(self, form, request, target):
         if form.instance.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()
             if form.instance.parent_id != form.cleaned_data['new_parent'].pk:
                 Category.objects.clear_cache()
         else:
-            form.instance.insert_at(form.cleaned_data['new_parent'],
+            form.instance.insert_at(
+                form.cleaned_data['new_parent'],
                 position='last-child',
                 save=True,
             )
@@ -77,11 +77,13 @@ class CategoryFormMixin(object):
 
             copied_acls = []
             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:
                 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):
-    ordering = (('name', None),)
+    ordering = (('name', None), )
 
 
 class RoleFormMixin(object):
@@ -48,8 +48,7 @@ class RoleFormMixin(object):
                 form.instance.permissions = new_permissions
                 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:
                     return redirect(request.path)
@@ -58,13 +57,11 @@ class RoleFormMixin(object):
             elif form.is_valid() and len(perms_forms) != valid_forms:
                 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):
@@ -78,8 +75,7 @@ class EditCategoryRole(RoleFormMixin, CategoryRoleAdmin, generic.ModelFormView):
 class DeleteCategoryRole(CategoryRoleAdmin, generic.ButtonView):
     def check_permissions(self, request, target):
         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}
 
     def button_action(self, request, target):
@@ -92,6 +88,8 @@ class DeleteCategoryRole(CategoryRoleAdmin, generic.ButtonView):
 Create category roles view for assinging roles to category,
 add link to it in categories list
 """
+
+
 class CategoryPermissions(CategoryAdmin, generic.ModelFormView):
     templates_dir = 'misago/admin/categoryroles'
     template = 'categoryroles.html'
@@ -107,7 +105,8 @@ class CategoryPermissions(CategoryAdmin, generic.ModelFormView):
         forms_are_valid = True
         for role in Role.objects.order_by('name'):
             FormType = CategoryRolesACLFormFactory(
-                role, category_roles, assigned_roles.get(role.pk))
+                role, category_roles, assigned_roles.get(role.pk)
+            )
 
             if request.method == 'POST':
                 forms.append(FormType(request.POST, prefix=role.pk))
@@ -126,7 +125,8 @@ class CategoryPermissions(CategoryAdmin, generic.ModelFormView):
                             role=form.role,
                             category=target,
                             category_role=form.cleaned_data['category_role']
-                        ))
+                        )
+                    )
             if new_permissions:
                 RoleCategoryACL.objects.bulk_create(new_permissions)
 
@@ -144,18 +144,19 @@ class CategoryPermissions(CategoryAdmin, generic.ModelFormView):
             'target': target,
         })
 
+
 CategoriesList.add_item_action(
     name=_("Category permissions"),
     icon='fa fa-adjust',
     link='misago:admin:categories:nodes:permissions',
     style='success'
 )
-
-
 """
 Create role categories view for assinging categories to role,
 add link to it in user roles list
 """
+
+
 class RoleCategoriesACL(RoleAdmin, generic.ModelFormView):
     templates_dir = 'misago/admin/categoryroles'
     template = 'rolecategories.html'
@@ -176,9 +177,7 @@ class RoleCategoriesACL(RoleAdmin, generic.ModelFormView):
         forms_are_valid = True
         for category in categories:
             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':
                 forms.append(FormType(request.POST, prefix=category.pk))
@@ -193,9 +192,12 @@ class RoleCategoriesACL(RoleAdmin, generic.ModelFormView):
             for form in forms:
                 if form.cleaned_data['role']:
                     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:
                 RoleCategoryACL.objects.bulk_create(new_permissions)
 
@@ -213,6 +215,7 @@ class RoleCategoriesACL(RoleAdmin, generic.ModelFormView):
             'target': target,
         })
 
+
 RolesList.add_item_action(
     name=_("Categories permissions"),
     icon='fa fa-comments-o',

+ 0 - 1
misago/conf/__init__.py

@@ -1,4 +1,3 @@
 from .gateway import settings, db_settings  # noqa
 
-
 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):
         urlpatterns.namespace(r'^settings/', 'settings', 'system')
 
-        urlpatterns.patterns('system:settings',
+        urlpatterns.patterns(
+            'system:settings',
             url(r'^$', views.index, name='index'),
             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):
     return {
         'DEBUG': misago_settings.DEBUG,
-
         'LANGUAGE_CODE_SHORT': get_language()[:2],
-
         'misago_settings': db_settings,
-
         'BLANK_AVATAR_URL': BLANK_AVATAR_URL,
-
         'THREADS_ON_INDEX': misago_settings.MISAGO_THREADS_ON_INDEX,
-
         'LOGIN_REDIRECT_URL': misago_settings.LOGIN_REDIRECT_URL,
         'LOGIN_URL': misago_settings.LOGIN_URL,
         'LOGOUT_URL': misago_settings.LOGOUT_URL,
@@ -32,23 +27,17 @@ def preload_settings_json(request):
 
     preloaded_settings.update({
         'LOGIN_API_URL': misago_settings.MISAGO_LOGIN_API_URL,
-
         'LOGIN_REDIRECT_URL': reverse(misago_settings.LOGIN_REDIRECT_URL),
         'LOGIN_URL': reverse(misago_settings.LOGIN_URL),
-
         'LOGOUT_URL': reverse(misago_settings.LOGOUT_URL),
     })
 
     request.frontend_context.update({
         'SETTINGS': preloaded_settings,
-
         'MISAGO_PATH': reverse('misago:index'),
-
         'BLANK_AVATAR_URL': BLANK_AVATAR_URL,
         'STATIC_URL': misago_settings.STATIC_URL,
-
         'CSRF_COOKIE_NAME': misago_settings.CSRF_COOKIE_NAME,
-
         '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
 
-
 # Permissions system extensions
 # https://misago.readthedocs.io/en/latest/developers/acls.html#extending-permissions-system
 
@@ -28,19 +27,16 @@ MISAGO_ACL_EXTENSIONS = [
     'misago.search.permissions',
 ]
 
-
 # Custom markup extensions
 
 MISAGO_MARKUP_EXTENSIONS = []
 
-
 # Posting middlewares
 # https://misago.readthedocs.io/en/latest/developers/posting_process.html
 
 MISAGO_POSTING_MIDDLEWARES = [
     # Always keep FloodProtectionMiddleware middleware first one
     'misago.threads.api.postingendpoint.floodprotection.FloodProtectionMiddleware',
-
     'misago.threads.api.postingendpoint.category.CategoryMiddleware',
     'misago.threads.api.postingendpoint.privatethread.PrivateThreadMiddleware',
     'misago.threads.api.postingendpoint.reply.ReplyMiddleware',
@@ -63,7 +59,6 @@ MISAGO_POSTING_MIDDLEWARES = [
     'misago.threads.api.postingendpoint.emailnotification.EmailNotificationMiddleware',
 ]
 
-
 # Configured thread types
 
 MISAGO_THREAD_TYPES = [
@@ -71,7 +66,6 @@ MISAGO_THREAD_TYPES = [
     'misago.threads.threadtypes.privatethread.PrivateThread',
 ]
 
-
 # Search extensions
 
 MISAGO_SEARCH_EXTENSIONS = [
@@ -79,13 +73,11 @@ MISAGO_SEARCH_EXTENSIONS = [
     'misago.users.search.SearchUsers',
 ]
 
-
 # Misago-admin specific date formats
 
 MISAGO_COMPACT_DATE_FORMAT_DAY_MONTH = 'j M'
 MISAGO_COMPACT_DATE_FORMAT_DAY_MONTH_YEAR = 'M \'y'
 
-
 # Additional registration validators
 # https://misago.readthedocs.io/en/latest/developers/validating_registrations.html
 
@@ -94,24 +86,20 @@ MISAGO_NEW_REGISTRATIONS_VALIDATORS = [
     'misago.users.validators.validate_with_sfs',
 ]
 
-
 # Stop Forum Spam settings
 
 MISAGO_USE_STOP_FORUM_SPAM = True
 MISAGO_STOP_FORUM_SPAM_MIN_CONFIDENCE = 80
 
-
 # Login API URL
 
 MISAGO_LOGIN_API_URL = 'auth'
 
-
 # Misago Admin Path
 # Omit starting and trailing slashes. To disable Misago admin, empty this value.
 
 MISAGO_ADMIN_PATH = 'admincp'
 
-
 # Admin urls namespaces that Misago's AdminAuthMiddleware should protect
 
 MISAGO_ADMIN_NAMESPACES = [
@@ -119,82 +107,68 @@ MISAGO_ADMIN_NAMESPACES = [
     'misago:admin',
 ]
 
-
 # How long (in minutes) since previous request to admin namespace should admin session last.
 
 MISAGO_ADMIN_SESSION_EXPIRATION = 60
 
-
 # Display threads on forum index
 # Change this to false to display categories list instead
 
 MISAGO_THREADS_ON_INDEX = True
 
-
 # Max age of notifications in days
 # Notifications older than this are deleted. On very active forums its better to keep this smaller.
 
 MISAGO_NOTIFICATIONS_MAX_AGE = 40
 
-
 # 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.
 
 MISAGO_DIALY_POST_LIMIT = 600
 MISAGO_HOURLY_POST_LIMIT = 100
 
-
 # Function used for generating individual avatar for user
 
 MISAGO_DYNAMIC_AVATAR_DRAWER = 'misago.users.avatars.dynamic.draw_default'
 
-
 # Path to directory containing avatar galleries
 # Those galleries can be loaded by running loadavatargallery command
 
 MISAGO_AVATAR_GALLERY = None
 
-
 # Save user avatars for sizes
 # Keep sizes ordered from greatest to smallest
 # Max size also controls min size of uploaded image as well as crop size
 
 MISAGO_AVATARS_SIZES = [400, 200, 150, 100, 64, 50, 30]
 
-
 # Path to blank avatar image used for guests and removed users.
 
 MISAGO_BLANK_AVATAR = 'blank-avatar.png'
 
-
 # Threads lists pagination settings
 
 MISAGO_THREADS_PER_PAGE = 25
 MISAGO_THREADS_TAIL = 15
 
-
 # Posts lists pagination settings
 
 MISAGO_POSTS_PER_PAGE = 18
 MISAGO_POSTS_TAIL = 6
 
-
 # Number of events displayed on single thread page
 # If there's more events than specified, oldest events will be trimmed
 
 MISAGO_EVENTS_PER_PAGE = 20
 
-
 # Number of attachments possible to assign to single post
 
 MISAGO_POST_ATTACHMENTS_LIMIT = 16
 
-
 # Max allowed size of image before Misago will generate thumbnail for it
 
 MISAGO_ATTACHMENT_IMAGE_SIZE_LIMIT = (500, 500)
 
-
 # Length of secret used for attachments url tokens and filenames
 
 MISAGO_ATTACHMENT_SECRET_LENGTH = 64
@@ -204,14 +178,12 @@ MISAGO_ATTACHMENT_SECRET_LENGTH = 64
 
 MISAGO_ATTACHMENT_ORPHANED_EXPIRE = 24 * 60
 
-
 # Names of files served when user requests file that doesn't exist or is unavailable
 # Those files will be sought within STATIC_ROOT directory
 
 MISAGO_404_IMAGE = 'misago/img/error-404.png'
 MISAGO_403_IMAGE = 'misago/img/error-403.png'
 
-
 # Controls max age in days of items that Misago has to process to make rankings
 # Used for active posters and most liked users lists
 # 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
 
-
 # Controls number of users displayed on single page
 
 MISAGO_USERS_PER_PAGE = 12
 
-
 # Controls amount of data used by readtracking system
 # 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
@@ -237,12 +207,14 @@ MISAGO_USERS_PER_PAGE = 12
 
 MISAGO_READTRACKER_CUTOFF = 40
 
-
 # Available Moment.js locales
 
 MISAGO_MOMENT_JS_LOCALES = [
     'af',
-    'ar-ma', 'ar-sa', 'ar-tn', 'ar',
+    'ar-ma',
+    'ar-sa',
+    'ar-tn',
+    'ar',
     'az',
     'be',
     'bg',
@@ -255,9 +227,12 @@ MISAGO_MOMENT_JS_LOCALES = [
     'cv',
     'cy',
     'da',
-    'de-at', 'de',
+    'de-at',
+    'de',
     'el',
-    'en-au', 'en-ca', 'en-gb',
+    'en-au',
+    'en-ca',
+    'en-gb',
     'eo',
     'es',
     'et',
@@ -272,7 +247,8 @@ MISAGO_MOMENT_JS_LOCALES = [
     'he',
     'hi',
     'hr',
-    'hu', 'hy-am',
+    'hu',
+    'hy-am',
     'id',
     'is',
     'it',
@@ -286,27 +262,32 @@ MISAGO_MOMENT_JS_LOCALES = [
     'mk',
     'ml',
     'mr',
-    'ms-my', 'my',
+    'ms-my',
+    'my',
     'nb',
     'ne',
     'nl',
     'nn',
     'pl',
-    'pt-br', 'pt',
+    'pt-br',
+    'pt',
     'ro',
     'ru',
     'sk',
     'sl',
     'sq',
-    'sr-cyrl', 'sr',
+    'sr-cyrl',
+    'sr',
     'sv',
     'ta',
     'th',
     'tl-ph',
     'tr',
-    'tzm-latn', 'tzm',
+    'tzm-latn',
+    'tzm',
     'uk',
     'uz',
     '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:
             message = ungettext(
                 '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}
             raise forms.ValidationError(message)
 
         if self.max_choices and self.max_choices < data_len:
             message = ungettext(
                 '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}
             raise forms.ValidationError(message)
 
@@ -69,9 +69,7 @@ def create_checkbox(setting, kwargs, extra):
     kwargs['choices'] = localise_choices(extra)
 
     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':
         return forms.TypedMultipleChoiceField(coerce='int', **kwargs)
@@ -130,12 +128,9 @@ def setting_field(FormType, setting):
     field_factory = FIELD_STYPES[setting.form_field]
     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
 
@@ -144,6 +139,7 @@ def ChangeSettingsForm(data=None, group=None):
     """
     Factory method that builds valid form for settings group
     """
+
     class FormType(forms.Form):
         pass
 
@@ -155,10 +151,7 @@ def ChangeSettingsForm(data=None, group=None):
     for setting in group.setting_set.order_by('order'):
         if setting.legend and setting.legend != fieldset_legend:
             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_form = FormType
             fieldset_fields = False
@@ -166,9 +159,6 @@ def ChangeSettingsForm(data=None, group=None):
         fieldset_form = setting_field(fieldset_form, setting)
 
     if fieldset_fields:
-        fieldsets.append({
-            'legend': fieldset_legend,
-            'form': fieldset_form(data)
-        })
+        fieldsets.append({'legend': fieldset_legend, 'form': fieldset_form(data)})
 
     return fieldsets

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

@@ -7,14 +7,17 @@ from django.db import migrations, models
 
 class Migration(migrations.Migration):
 
-    dependencies = [
-    ]
+    dependencies = []
 
     operations = [
         migrations.CreateModel(
             name='Setting',
             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)),
                 ('name', models.CharField(max_length=255)),
                 ('description', models.TextField(null=True, blank=True)),
@@ -28,21 +31,23 @@ class Migration(migrations.Migration):
                 ('form_field', models.CharField(default='text', max_length=255)),
                 ('field_extra', JSONField()),
             ],
-            options={
-            },
-            bases=(models.Model,),
+            options={},
+            bases=(models.Model, ),
         ),
         migrations.CreateModel(
             name='SettingsGroup',
             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)),
                 ('name', models.CharField(max_length=255)),
                 ('description', models.TextField(null=True, blank=True)),
             ],
-            options={
-            },
-            bases=(models.Model,),
+            options={},
+            bases=(models.Model, ),
         ),
         migrations.AddField(
             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'):
         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']
         translated_choices = []
         for val, name in untranslated_choices:
             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:
         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"):
         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 {}
 

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

@@ -17,9 +17,7 @@ class AdminSettingsViewsTests(AdminTestCase):
 
         self.assertEqual(response.status_code, 200)
         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_link)
 
@@ -27,9 +25,7 @@ class AdminSettingsViewsTests(AdminTestCase):
         """
         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)
         self.assertEqual(response.status_code, 302)
         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
         """
         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)
 
             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):
     def setUp(self):
         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)
@@ -42,16 +42,14 @@ class DBConfMigrationUtilsTests(TestCase):
     def test_get_custom_group_and_settings(self):
         """tests setup created settings 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.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.assertTrue('fish_license_no' not in custom_settings)
@@ -60,40 +58,39 @@ class DBConfMigrationUtilsTests(TestCase):
         """migrate_settings_group changed group key"""
 
         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(
-            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(db_group.key, new_group['key'])
         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']:
             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='')
         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):
         """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
         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):
         """forum_name is defined"""
         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):
             gateway.LoremIpsum
@@ -43,31 +41,31 @@ class GatewaySettingsTests(TestCase):
     def test_setting_public(self):
         """get_public_settings returns public settings"""
         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)
@@ -79,44 +77,42 @@ class GatewaySettingsTests(TestCase):
         self.assertIn('fish_name', public_settings)
         self.assertNotIn('private_fish_name', public_settings)
 
-
     def test_setting_lazy(self):
         """lazy settings work"""
         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)
@@ -125,11 +121,9 @@ class GatewaySettingsTests(TestCase):
         self.assertTrue(db_settings.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.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(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):
     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:
-        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):
     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:
         setting.dry_value = None
     return setting.value

+ 10 - 11
misago/conf/views.py

@@ -34,8 +34,7 @@ def group(request, key):
     fieldsets = ChangeSettingsForm(group=active_group)
     if request.method == 'POST':
         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:
             new_values = {}
             for fieldset in fieldsets:
@@ -47,15 +46,15 @@ def group(request, key):
 
             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)
 
-    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.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()
@@ -16,10 +12,7 @@ def check_db_engine(app_configs, **kwargs):
         if settings.DATABASES['default']['ENGINE'] not in SUPPORTED_ENGINES:
             raise 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
 

+ 1 - 3
misago/core/apipatch.py

@@ -39,9 +39,7 @@ class ApiPatch(object):
 
     def dispatch(self, request, target):
         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 = []
         is_errored = False

+ 2 - 4
misago/core/apirouter.py

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

+ 1 - 2
misago/core/cachebuster.py

@@ -65,8 +65,7 @@ class CacheBusterController(object):
         from .models import CacheVersion
 
         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)
 
     def invalidate_all(self):

+ 2 - 6
misago/core/context_processors.py

@@ -32,17 +32,13 @@ def current_link(request):
     else:
         link_name = url_name
 
-    request.frontend_context.update({
-        'CURRENT_LINK': link_name
-    })
+    request.frontend_context.update({'CURRENT_LINK': link_name})
 
     return {}
 
 
 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):

+ 2 - 0
misago/core/decorators.py

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

+ 5 - 9
misago/core/errorpages.py

@@ -13,13 +13,9 @@ def _ajax_error(code=406, message=None):
 
 @admin_error_page
 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):
@@ -28,9 +24,7 @@ def banned(request, ban):
         '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):
@@ -68,6 +62,7 @@ def shared_403_exception_handler(f):
             return permission_denied(request)
         else:
             return f(request, *args, **kwargs)
+
     return page_decorator
 
 
@@ -77,4 +72,5 @@ def shared_404_exception_handler(f):
             return page_not_found(request)
         else:
             return f(request, *args, **kwargs)
+
     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)
 
 
-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):
@@ -76,8 +74,7 @@ def get_exception_handler(exception):
         if isinstance(exception, exception_type):
             return handler
     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):

+ 1 - 0
misago/core/exceptions.py

@@ -3,6 +3,7 @@ from django.core.exceptions import PermissionDenied
 
 class AjaxError(Exception):
     """You've tried to do something over AJAX but misago blurped"""
+
     def __init__(self, message=None, code=406):
         self.message = message
         self.code = code

+ 4 - 2
misago/core/forms.py

@@ -45,6 +45,8 @@ def YesNoSwitch(**kwargs):
 
     return YesNoSwitchBase(
         coerce=int,
-        choices=((1, yes_label), (0, no_label)),
+        choices=((1, yes_label),
+                 (0, no_label)),
         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_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")
 
     return message

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

@@ -32,12 +32,12 @@ class Command(BaseCommand):
 
                         # Finally list 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):
         # Fancy title

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

@@ -24,7 +24,7 @@ class Command(BaseCommand):
             'Test Message',
             ("This message was sent to test if your "
              "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)

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

@@ -7,8 +7,7 @@ from misago.core.utils import is_request_to_misago
 class ExceptionHandlerMiddleware(MiddlewareMixin):
     def process_exception(self, request, exception):
         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:
             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):
 
-    dependencies = [
-    ]
+    dependencies = []
 
     operations = [
         migrations.CreateModel(
             name='CacheVersion',
             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)),
                 ('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):
-    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',
                 'name': _("Forum name"),
                 'legend': _("General"),
@@ -26,8 +31,7 @@ def create_basic_settings_group(apps, schema_editor):
                     'max_length': 255
                 },
                 'is_public': True,
-            },
-            {
+            }, {
                 'setting': 'forum_index_title',
                 'name': _("Index title"),
                 'description': _("You may set custon title on "
@@ -37,8 +41,7 @@ def create_basic_settings_group(apps, schema_editor):
                     'max_length': 255
                 },
                 'is_public': True,
-            },
-            {
+            }, {
                 'setting': 'forum_index_meta_description',
                 'name': _("Meta Description"),
                 'description': _("Short description of your forum "
@@ -46,8 +49,7 @@ def create_basic_settings_group(apps, schema_editor):
                 'field_extra': {
                     'max_length': 255
                 },
-            },
-            {
+            }, {
                 'setting': 'forum_branding_display',
                 'name': _("Display branding"),
                 'description': _("Switch branding in forum's navbar."),
@@ -56,8 +58,7 @@ def create_basic_settings_group(apps, schema_editor):
                 'python_type': 'bool',
                 'form_field': 'yesno',
                 'is_public': True,
-            },
-            {
+            }, {
                 'setting': 'forum_branding_text',
                 'name': _("Branding text"),
                 'description': _("Optional text displayed besides "
@@ -67,20 +68,23 @@ def create_basic_settings_group(apps, schema_editor):
                     'max_length': 255
                 },
                 'is_public': True,
-            },
-            {
-                'setting': 'email_footer',
-                'name': _("E-mails footer"),
-                'description': _("Optional short message included "
-                                 "at the end of e-mails sent by "
-                                 "forum"),
-                'legend': _("Forum e-mails"),
+            }, {
+                'setting':
+                    'email_footer',
+                'name':
+                    _("E-mails footer"),
+                'description':
+                    _("Optional short message included "
+                      "at the end of e-mails sent by "
+                      "forum"),
+                'legend':
+                    _("Forum e-mails"),
                 'field_extra': {
                     'max_length': 255
                 },
-            },
-        )
-    })
+            }, )
+        }
+    )
 
 
 class Migration(migrations.Migration):

+ 12 - 12
misago/core/page.py

@@ -5,6 +5,7 @@ class Page(object):
     Allows for adding custom views to "sectioned" pages like
     User Control Panel, Users List or Threads Lists
     """
+
     def __init__(self, name):
         self._finalized = False
         self.name = name
@@ -21,17 +22,17 @@ class Page(object):
         while self._unsorted_list:
             iterations += 1
             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)
 
             for index, section in enumerate(self._unsorted_list):
                 if section['after']:
-                    section_added = self._insert_section(
-                        section, after=section['after'])
+                    section_added = self._insert_section(section, after=section['after'])
                 elif section['before']:
-                    section_added = self._insert_section(
-                        section, before=section['before'])
+                    section_added = self._insert_section(section, before=section['before'])
                 else:
                     section_added = self._insert_section(section)
 
@@ -66,11 +67,11 @@ class Page(object):
             self._sorted_list.append(inserted_section)
             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:
-            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)
 
         if after and before:
@@ -110,8 +111,7 @@ class Page(object):
 
             if is_visible:
                 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'])
                 visible_sections.append(section)
         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):
         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)
 
         statement = self.CREATE_SQL % {
@@ -37,10 +36,8 @@ DROP INDEX %(index_name)s
 
         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):
         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.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)
 
         statement = self.CREATE_SQL % {
@@ -99,7 +95,6 @@ DROP INDEX %(index_name)s
         schema_editor.execute(statement)
 
     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)
         return message % formats

+ 3 - 9
misago/core/serializers.py

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

+ 12 - 9
misago/core/shortcuts.py

@@ -5,10 +5,15 @@ from django.http import Http404
 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 .exceptions import ExplicitFirstPage
 
@@ -21,8 +26,8 @@ def paginate(object_list, page, per_page, orphans=0,
 
     try:
         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):
         raise Http404()
 
@@ -62,9 +67,7 @@ def paginated_response(page, serializer=None, data=None, extra=None):
     if serializer:
         results = serializer(results, many=True).data
 
-    response_data.update({
-        'results': results
-    })
+    response_data.update({'results': results})
 
     if extra:
         response_data.update(extra)

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

@@ -31,7 +31,7 @@ def capture(parser, token):
     else:
         raise template.TemplateSyntaxError(SYNTAX_ERROR)
 
-    nodelist = parser.parse(('endcapture',))
+    nodelist = parser.parse(('endcapture', ))
     parser.delete_first_token()
     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()
-
-
 """
 Form row: renders single row in form
 
@@ -14,18 +12,20 @@ Syntax:
 {% form_row form.field %} # renders vertical field
 {% form_row form.field "col-md-3" "col-md-9" %} # renders horizontal field
 """
+
+
 @register.tag
 def form_row(parser, token):
     args = token.split_contents()
 
     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:
         raise template.TemplateSyntaxError(
             "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]
 
@@ -61,18 +61,22 @@ class FormRowNode(template.Node):
             field_class = None
 
         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
 """
+
+
 @register.tag
 def form_input(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()
 
     class Meta:
-        fields = (
-            'id',
-        )
+        fields = ('id', )
 
     def get_id(self, obj):
         return obj * 2

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

@@ -11,24 +11,48 @@ from . import views
 admin.autodiscover()
 admin.site.login_form = AdminAuthenticationForm
 
-
-
 urlpatterns = [
     url(r'^forum/', include('misago.urls', namespace='misago')),
     url(r'^django-admin/', include(admin.site.urls)),
-
     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-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/(?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-403/$', views.raise_misago_403, name='raise-misago-403'),
     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):
     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!")
 
 
 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!")
 
@@ -72,11 +66,7 @@ def test_paginated_response_data_serializer(request):
     data = [0, 1, 2, 3]
     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()
@@ -85,12 +75,8 @@ def test_paginated_response_data_extra(request):
     page = paginate(data, 0, 10)
 
     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():
             pass
+
         patch.add('test-add', mock_function)
 
         self.assertEqual(len(patch._actions), 1)
@@ -36,6 +37,7 @@ class ApiPatchTests(TestCase):
 
         def mock_function():
             pass
+
         patch.remove('test-remove', mock_function)
 
         self.assertEqual(len(patch._actions), 1)
@@ -49,6 +51,7 @@ class ApiPatchTests(TestCase):
 
         def mock_function():
             pass
+
         patch.replace('test-replace', mock_function)
 
         self.assertEqual(len(patch._actions), 1)
@@ -60,21 +63,25 @@ class ApiPatchTests(TestCase):
         """validate_action method validates action dict"""
         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:
             patch.validate_action(action)
 
         # undefined op
-        UNSUPPORTED_ACTIONS = (
-            {},
-            {'op': ''},
-            {'no': 'op'},
-        )
+        UNSUPPORTED_ACTIONS = ({}, {'op': ''}, {'no': 'op'}, )
 
         for action in UNSUPPORTED_ACTIONS:
             try:
@@ -116,12 +123,14 @@ class ApiPatchTests(TestCase):
             self.assertEqual(request, 'request')
             self.assertEqual(target, mock_target)
             return {'a': value * 2, 'b': 111}
+
         patch.replace('abc', action_a)
 
         def action_b(request, target, value):
             self.assertEqual(request, 'request')
             self.assertEqual(target, mock_target)
             return {'b': value * 10}
+
         patch.replace('abc', action_b)
 
         def action_fail(request, target, value):
@@ -131,15 +140,15 @@ class ApiPatchTests(TestCase):
         patch.remove('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(patch_dict['id'], 123)
@@ -155,26 +164,40 @@ class ApiPatchTests(TestCase):
                 raise Http404()
             if value == 'perm':
                 raise PermissionDenied("yo ain't doing that!")
+
         patch.replace('error', action_error)
 
         def action_mutate(request, target, value):
             return {'value': value * 2}
+
         patch.replace('mutate', action_mutate)
 
         # dispatch requires list as an argument
         response = patch.dispatch(MockRequest({}), {})
         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
-        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)
 
@@ -186,47 +209,97 @@ class ApiPatchTests(TestCase):
         self.assertEqual(response.data['value'], 14)
 
         # 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(len(response.data['detail']), 3)
         self.assertEqual(response.data['detail'][0], '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['value'], 12)
 
         # 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(len(response.data['detail']), 2)
         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['value'], 4)
 
         # 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)
 
@@ -234,7 +307,6 @@ class ApiPatchTests(TestCase):
         self.assertEqual(response.data['detail'][0], 'ok')
         self.assertEqual(response.data['detail'][1], '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['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 = (
-    '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])
         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):
         """momentjs_locale adds MOMENTJS_LOCALE_URL to context"""
         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'):
-            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'):
-            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'):
-            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):
     def test_site_address_for_http(self):
         """Correct SITE_ADDRESS set for HTTP request"""
         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):
         """Correct SITE_ADDRESS set for HTTPS request"""
         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):
@@ -76,11 +88,12 @@ class FrontendContextTests(TestCase):
         mock_request.include_frontend_context = True
         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'
-            }
-        })
+            }}
+        )
 
         mock_request.include_frontend_context = False
         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):
         """csrf_failure error page has no show-stoppers"""
         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)
 
 
-
 @override_settings(ROOT_URLCONF='misago.core.testproject.urls')
 class ErrorPageViewsTests(TestCase):
     def test_banned_returns_403(self):
         """banned error page has no show-stoppers"""
         response = self.client.get(reverse('raise-misago-banned'))
         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):
         """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 = (
-    django_exceptions.ObjectDoesNotExist,
-    django_exceptions.ViewDoesNotExist,
-    TypeError,
-    ValueError,
+    django_exceptions.ObjectDoesNotExist, django_exceptions.ViewDoesNotExist, TypeError, ValueError,
     KeyError,
 )
 
@@ -37,8 +34,9 @@ class IsMisagoExceptionTests(TestCase):
 class GetExceptionHandlerTests(TestCase):
     def test_exception_handlers_list(self):
         """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):
         """Exception handler has correct handler for every Misago exception"""
@@ -57,18 +55,14 @@ class HandleAPIExceptionTests(TestCase):
         """banned exception is correctly handled"""
         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.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):
         """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.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
         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)
 

+ 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):
     def test_clean_language_name(self):
         """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:
             self.assertEqual(clean_language_name(dirty), clean)
@@ -21,27 +16,13 @@ class MomentJSTests(TestCase):
     def test_get_locale_path(self):
         """get_locale_path returns path to locale or null if it doesnt exist"""
         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:
             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:
             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):
         """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(
-            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()
 

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

@@ -17,19 +17,18 @@ class MutableFieldsSerializerTests(TestCase):
         fields = ('id', 'title', 'replies', 'last_poster_name')
 
         serializer = TestSerializer.subset_fields(*fields)
-        self.assertEqual(
-            serializer.__name__,
-            'TestSerializerIdTitleRepliesLastPosterNameSubset'
-        )
+        self.assertEqual(serializer.__name__, 'TestSerializerIdTitleRepliesLastPosterNameSubset')
         self.assertEqual(serializer.Meta.fields, fields)
 
         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)
 
@@ -46,11 +45,13 @@ class MutableFieldsSerializerTests(TestCase):
         self.assertEqual(serializer.Meta.fields, kept_fields)
 
         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)
 
@@ -59,7 +60,7 @@ class MutableFieldsSerializerTests(TestCase):
         category = Category.objects.get(slug='first-category')
         thread = testutils.post_thread(category=category)
 
-        added_fields = ('category',)
+        added_fields = ('category', )
 
         serializer = TestSerializer.extend_fields(*added_fields)
 
@@ -73,17 +74,7 @@ class TestSerializer(serializers.ModelSerializer, MutableFields):
     class Meta:
         model = Thread
         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):
         """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')
 
-        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):
     def test_valid_page_handling(self):
         """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())
 
     def test_invalid_page_handling(self):
         """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)
 
     def test_implicit_page_handling(self):
         """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())
 
     def test_explicit_page_handling(self):
         """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/"
         self.assertEqual(response['Location'], valid_url)
 
@@ -37,18 +33,22 @@ class PaginateTests(TestCase):
 class ValidateSlugTests(TestCase):
     def test_valid_slug_handling(self):
         """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")
 
     def test_invalid_slug_handling(self):
         """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/"
         self.assertEqual(response['Location'], valid_url)
@@ -57,29 +57,14 @@ class ValidateSlugTests(TestCase):
 class GetIntOr404Tests(TestCase):
     def test_valid_inputs(self):
         """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:
             self.assertEqual(get_int_or_404(value), result)
 
     def test_invalid_inputs(self):
         """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:
             with self.assertRaises(Http404):
@@ -92,94 +77,120 @@ class PaginatedResponseTests(TestCase):
         """utility returns response for only page arg"""
         response = self.client.get(reverse('test-paginated-response'))
         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):
         """utility returns response with explicit data"""
         response = self.client.get(reverse('test-paginated-response-data'))
         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):
         """utility returns response with data serialized via serializer"""
         response = self.client.get(reverse('test-paginated-response-serializer'))
         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):
         """utility returns response with explicit data serialized via serializer"""
         response = self.client.get(reverse('test-paginated-response-data-serializer'))
         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):
         """utility returns response with explicit data and extra"""
         response = self.client.get(reverse('test-paginated-response-data-extra'))
         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):
         """standard batch yields valid results"""
         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)):
             self.assertEqual(test_yield, yields[i])
@@ -58,12 +53,7 @@ class BatchTests(TestCase):
     def test_batchnonefilled(self):
         """none-filled batch yields valid results"""
         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)):
             self.assertEqual(test_yield, yields[i])
@@ -171,10 +161,7 @@ class ShorthandsTests(TestCase):
 """
 
         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):
         """iftrue isnt rendering value for false"""
@@ -185,10 +172,7 @@ class ShorthandsTests(TestCase):
 """
 
         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):
         """iffalse isnt rendering value for true"""
@@ -199,10 +183,7 @@ class ShorthandsTests(TestCase):
 """
 
         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):
         """iffalse renders value for false"""
@@ -213,10 +194,7 @@ class ShorthandsTests(TestCase):
 """
 
         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):
@@ -229,9 +207,13 @@ class JSONTests(TestCase):
 """
 
         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):
@@ -244,9 +226,7 @@ class PageTitleTests(TestCase):
         """
 
         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):
         """tag builds full title from title and parent name"""
@@ -257,10 +237,12 @@ class PageTitleTests(TestCase):
         """
 
         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):
         """tag builds full title from title and page number"""
@@ -271,9 +253,11 @@ class PageTitleTests(TestCase):
         """
 
         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):
         """tag builds full title from all options"""
@@ -284,7 +268,9 @@ class PageTitleTests(TestCase):
         """
 
         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)
 
 
-VALID_PATHS = (
-    "/",
-    "/threads/",
-)
+VALID_PATHS = ("/", "/threads/", )
 
-INVALID_PATHS = (
-    "",
-    "somewhere/",
-)
+INVALID_PATHS = ("", "somewhere/", )
 
 
 class IsRequestToMisagoTests(TestCase):
@@ -34,14 +28,15 @@ class IsRequestToMisagoTests(TestCase):
             request.path_info = path
             self.assertTrue(
                 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:
             request = RequestFactory().get('/')
             request.path_info = path
             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):
@@ -65,8 +60,7 @@ class SlugifyTests(TestCase):
             resolve_slugify('misago.threads.invalidname')
         except ImportError as 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):
         """resolve_slugify resolves valid paths"""
@@ -75,15 +69,9 @@ class SlugifyTests(TestCase):
 
     def test_valid_slugify_output(self):
         """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:
             self.assertEqual(slugify(original), slug)
@@ -93,10 +81,8 @@ class ParseIso8601StringTests(TestCase):
     def test_valid_input(self):
         """util parses iso 8601 strings"""
         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:
@@ -105,9 +91,7 @@ class ParseIso8601StringTests(TestCase):
     def test_invalid_input(self):
         """util throws ValueError on invalid input"""
         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',
         )
 
@@ -117,27 +101,11 @@ class ParseIso8601StringTests(TestCase):
 
 
 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/',
         '<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):
     def test_get_request(self):
         """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))
 
-        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))
 
-        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))
 
-        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), '/')
 
-        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/')
 
     def test_post_request(self):
         """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))
 
-        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/')
 
 
 class IsRefererLocalTests(TestCase):
     def test_local_referers(self):
         """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))
 
-        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))
 
-        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))
 
     def test_foreign_referers(self):
         """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))
 
-        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))
 
-        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))

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

@@ -22,11 +22,13 @@ class ValidateSluggableTests(TestCase):
         with self.assertRaises(ValidationError):
             validator('!#@! !@#@')
         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):
         """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
     """
+
     def clear_state(self):
         cache.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
 """
-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):
     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
 that is incompatibile with Django's decorator
 """
+
+
 def hide_post_parameters(request):
     request.sensitive_post_parameters = '__ALL__'
 
@@ -83,6 +83,8 @@ def hide_post_parameters(request):
 """
 Return path utility
 """
+
+
 def clean_return_path(request):
     if request.method == 'POST' and 'return_path' in request.POST:
         return _get_return_path_from_post(request)
@@ -125,6 +127,8 @@ def _get_return_path_from_referer(request):
 """
 Utils for resolving requests destination
 """
+
+
 def _is_request_path_under_misago(request):
     # We are assuming that forum_index link is root of all Misago links
     forum_index = reverse('misago:index')

+ 1 - 2
misago/core/validators.py

@@ -6,8 +6,7 @@ from .utils import slugify
 
 class validate_sluggable(object):
     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.")
 
     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):
-    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):

+ 5 - 9
misago/datamover/attachments.py

@@ -13,11 +13,7 @@ from . import OLD_FORUM, fetch_assoc, localise_datetime, movedids
 
 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):
@@ -38,13 +34,13 @@ def move_attachments(stdout, style):
 
     for attachment in fetch_assoc(query):
         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
 
         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
 
         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)
                 except gravatar.GravatarError:
                     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:
                 try:
                     if not old_user['avatar_original'] or not old_user['avatar_crop']:
                         raise ValidationError("Invalid avatar upload data.")
 
                     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)
 
                     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
 
 
-CHECK_MAPPING = {
-  1: 0,
-  2: 1,
-  3: 2
-}
+CHECK_MAPPING = {1: 0, 2: 1, 3: 2}
 
 
 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'])
             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)
 
@@ -69,10 +73,12 @@ def move_labels():
             parent_id = movedids.get('category', parent_row['forum_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'])
             movedids.set('label', label_id, category.pk)

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

@@ -12,9 +12,7 @@ MAPPINGS = {
 
 
 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):
         self.stdout.write("Building moves index...")

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

@@ -10,10 +10,8 @@ class Command(BaseCommand):
 
         self.start_timer()
         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()
         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()
         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()
         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()
         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()
         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()
         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()
         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()
         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()
         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()
         threads.move_participants()
         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()
         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()
         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):
-    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):
         self.stdout.write("Moving users from Misago 0.5:")
 
         self.start_timer()
         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()
         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()
         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()
         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()
         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()
         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 = (
-    '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):
-    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):
         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
 """
+
+
 class FakeUser(object):
     slug = get_random_string(40)
 

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

@@ -9,14 +9,17 @@ class Migration(migrations.Migration):
 
     initial = True
 
-    dependencies = [
-    ]
+    dependencies = []
 
     operations = [
         migrations.CreateModel(
             name='MovedId',
             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)),
                 ('old_id', models.CharField(max_length=255)),
                 ('new_id', models.CharField(max_length=255)),
@@ -25,7 +28,11 @@ class Migration(migrations.Migration):
         migrations.CreateModel(
             name='OldIdRedirect',
             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()),
                 ('old_id', models.PositiveIntegerField()),
                 ('new_id', models.PositiveIntegerField()),

+ 1 - 0
misago/datamover/movedids.py

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

+ 75 - 44
misago/datamover/settings.py

@@ -12,6 +12,7 @@ def copy_value(setting):
         setting_obj.value = old_value
         setting_obj.save()
         return setting_obj
+
     return closure
 
 
@@ -21,6 +22,7 @@ def map_value(setting, translation):
         setting_obj.value = translation[old_value]
         setting_obj.save()
         return setting_obj
+
     return closure
 
 
@@ -32,50 +34,79 @@ def convert_allow_custom_avatars(old_value):
 
 
 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'):
         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
 
         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
 
         if special_categories.get(thread['forum_id']) == 'private_threads':
@@ -77,9 +75,7 @@ def move_posts():
         deleter_name = None
         deleter_slug = None
         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:
                 deleter_name = deleter.username
@@ -160,18 +156,20 @@ def move_post_edits(post, old_id):
         if changelog:
             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:
         PostEdit.objects.bulk_create(changelog)
@@ -215,10 +213,7 @@ def move_likes():
     for post in Post.objects.filter(id__in=posts).iterator():
         post.last_likes = []
         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'])
 
 
@@ -232,19 +227,14 @@ def move_participants():
 
         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):
     category = Category.objects.private_threads()
 
     # 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):
         thread.delete()
 
@@ -256,8 +246,7 @@ def clean_private_threads(stdout, style):
             thread.save()
         elif participants_count == 0:
             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():

+ 179 - 47
misago/datamover/urls.py

@@ -6,9 +6,18 @@ from . import views
 urlpatterns = [
     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+)/(?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),
 ]
 
@@ -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+)/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+)/(?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+)/find-(?P<post>\d+)/$', 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/email/$', views.thread_redirect),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/upvote/$', views.thread_redirect),
-    url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/downvote/$', views.thread_redirect),
+    url(
+        r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/downvote/$', views.thread_redirect
+    ),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/report/$', views.thread_redirect),
-    url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/show-report/$', views.thread_redirect),
+    url(
+        r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/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+)/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+)/(?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+)/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+)/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 += [
-    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+)/(?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+)/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 += [
     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<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()
 
-
-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):
@@ -31,15 +25,14 @@ def move_users(stdout, style):
         else:
             try:
                 new_user = UserModel.objects.create_user(
-                    user['username'], user['email'], 'Pass.123')
+                    user['username'], user['email'], 'Pass.123'
+                )
             except ValidationError:
                 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)
-                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']
 
@@ -64,7 +57,8 @@ def move_users(stdout, style):
             new_user.signature = user['signature']
             new_user.signature_parsed = user['signature_preparsed']
             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.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:
             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)

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

@@ -15,19 +15,11 @@ class Command(BaseCommand):
 
     def add_arguments(self, parser):
         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(
-            '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):
@@ -65,25 +57,27 @@ class Command(BaseCommand):
                 else:
                     new_category.description = fake.paragraph()
 
-            new_category.insert_at(parent,
+            new_category.insert_at(
+                parent,
                 position='last-child',
                 save=True,
             )
 
             copied_acls = []
             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:
                 RoleCategoryACL.objects.bulk_create(copied_acls)
 
             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()
 

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

@@ -32,7 +32,7 @@ class Command(BaseCommand):
             if random.randint(1, 100) > 10:
                 processed_count += 1
                 show_progress(self, processed_count, total_users)
-                continue # 10% active users
+                continue  # 10% active users
 
             users_to_add = random.randint(1, total_users / 5)
             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'
 
-
 UserModel = get_user_model()
 
-
 corpus = EnglishCorpus()
 corpus_short = EnglishCorpus(max_length=150)
 
@@ -32,11 +30,7 @@ class Command(BaseCommand):
 
     def add_arguments(self, parser):
         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):
@@ -160,8 +154,7 @@ class Command(BaseCommand):
                 thread.save()
 
                 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
         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):
         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):
@@ -51,8 +47,8 @@ class Command(BaseCommand):
                 }
 
                 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:
                     dynamic.set_avatar(user)
@@ -63,8 +59,7 @@ class Command(BaseCommand):
                 pass
             else:
                 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_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:
         legal_context['TERMS_OF_SERVICE_URL'] = settings.terms_of_service_link
     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:
         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):
-    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',
                 'name': _("Terms title"),
                 'legend': _("Terms of Service"),
@@ -28,37 +31,48 @@ def create_legal_settings_group(apps, schema_editor):
                     'required': False,
                 },
                 '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': {
                     'max_length': 255,
                     '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': {
                     'max_length': 128000,
                     'required': False,
                     'rows': 8,
                 },
-                'is_public': True,
-                'is_lazy': True,
-            },
-            {
+                'is_public':
+                    True,
+                'is_lazy':
+                    True,
+            }, {
                 'setting': 'privacy_policy_title',
                 'name': _("Policy title"),
                 'legend': _("Privacy policy"),
@@ -70,37 +84,48 @@ def create_legal_settings_group(apps, schema_editor):
                     'required': False,
                 },
                 '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': {
                     'max_length': 255,
                     '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': {
                     'max_length': 128000,
                     'required': False,
                     'rows': 8,
                 },
-                'is_public': True,
-                'is_lazy': True,
-            },
-            {
+                'is_public':
+                    True,
+                'is_lazy':
+                    True,
+            }, {
                 'setting': 'forum_footnote',
                 'name': _("Footnote"),
                 'description': _("Short message displayed "
@@ -110,9 +135,9 @@ def create_legal_settings_group(apps, schema_editor):
                     'max_length': 300
                 },
                 'is_public': True,
-            },
-        )
-    })
+            }, )
+        }
+    )
 
 
 class Migration(migrations.Migration):

+ 6 - 18
misago/legal/tests.py

@@ -56,26 +56,20 @@ class PrivacyPolicyTests(TestCase):
         settings.override_setting('privacy_policy', 'Lorem ipsum')
         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):
         """context processor has TOS link to remote url"""
         settings.override_setting('privacy_policy_link', 'http://test.com')
         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
         settings.override_setting('privacy_policy', 'Lorem ipsum')
         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):
@@ -123,23 +117,17 @@ class TermsOfServiceTests(TestCase):
         settings.override_setting('terms_of_service', 'Lorem ipsum')
         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):
         """context processor has TOS link to remote url"""
         settings.override_setting('terms_of_service_link', 'http://test.com')
         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
         settings.override_setting('terms_of_service', 'Lorem ipsum')
         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')
 
-    return render(request, 'misago/privacy_policy.html', {
+    return render(
+        request, 'misago/privacy_policy.html', {
             'id': 'privacy-policy',
             'title': settings.privacy_policy_title or _("Privacy policy"),
             'link': settings.privacy_policy_link,
             'body': parsed_content,
-        })
+        }
+    )
 
 
 def terms_of_service(request):
@@ -57,9 +59,11 @@ def terms_of_service(request):
 
     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',
             'title': settings.terms_of_service_title or _("Terms of service"),
             'link': settings.terms_of_service_link,
             '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 .parser import parse
 
-
 default_app_config = 'misago.markup.apps.MisagoMarkupConfig'

+ 2 - 6
misago/markup/api.py

@@ -17,13 +17,9 @@ def parse_markup(request):
     try:
         validate_post(post)
     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']
     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.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):
-    QUOTE_BLOCK_RE = re.compile(r'''
+    QUOTE_BLOCK_RE = re.compile(
+        r'''
 \[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\]
-'''.strip(), re.IGNORECASE | re.MULTILINE | re.DOTALL);
-
+'''.strip(), re.IGNORECASE | re.MULTILINE | re.DOTALL
+    )
 
     def run(self, lines):
         text = '\n'.join(lines)
@@ -106,12 +111,14 @@ class CodeBlockExtension(markdown.Extension):
     def extendMarkdown(self, md):
         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):
-        FENCED_BLOCK_RE = re.compile(r'''
+    FENCED_BLOCK_RE = re.compile(
+        r'''
 \[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
     """
+
     def __init__(self, bbcode, tag=None):
         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
         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):
-    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 {}

+ 4 - 2
misago/markup/finalise.py

@@ -5,9 +5,11 @@ import re
 from django.utils.translation import gettext as _
 
 
-HEADER_RE = re.compile(r'''
+HEADER_RE = re.compile(
+    r'''
 <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):

+ 18 - 6
misago/markup/flavours.py

@@ -24,16 +24,28 @@ def limited(request, 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']
 
 
 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']

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

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

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

@@ -9,4 +9,5 @@ class StriketroughExtension(markdown.Extension):
     def extendMarkdown(self, md):
         md.registerExtension(self)
         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')
 
 
-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
 
@@ -133,8 +142,7 @@ def md_factory(allow_links=True, allow_images=True, allow_blocks=True):
 
 
 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
     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
     """
+
     def extend_markdown(self, md):
         for extension in settings.MISAGO_MARKUP_EXTENSIONS:
             module = import_module(extension)
@@ -31,4 +32,5 @@ class MarkupPipeline(object):
         result['parsed_text'] = souped_text.strip()
         return result
 
+
 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):
     return _render_editor_template(context, editor, editor.body_template)
+
+
 register.simple_tag(takes_context=True)(editor_body)
 
 
 def editor_js(context, editor):
     return _render_editor_template(context, editor, editor.js_template)
+
+
 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):
         """api requires authentication"""
-        self.logout_user();
+        self.logout_user()
 
         response = self.client.post(self.api_link)
         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):
         """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)
 
     def test_invalid_post(self):
         """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(
-            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):
         """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>")

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

@@ -10,7 +10,5 @@ class ChecksumsTests(TestCase):
 
         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):
     def test_single_mention(self):
         """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:
-            result = {
-                'parsed_text': before.format(self.user.username),
-                'mentions': []
-            }
+            result = {'parsed_text': before.format(self.user.username), 'mentions': []}
 
             add_mentions(MockRequest(self.user), result)
 
@@ -48,17 +32,13 @@ class MentionsTests(AuthenticatedUserTestCase):
     def test_invalid_mentions(self):
         """markup extension leaves invalid mentions alone"""
         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="/"><b>@{}</b></a>!</p>'.format(self.user.username),
         )
 
         for markup in TEST_CASES:
-            result = {
-                'parsed_text': markup,
-                'mentions': []
-            }
+            result = {'parsed_text': markup, 'mentions': []}
 
             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)
 
         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)
         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)
 
         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)
         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
 
 
-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
 
     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:
             category = categories_dict[record.category_id]
@@ -61,8 +59,7 @@ def sync_record(user, category):
         category_record = None
 
     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(
         category=category,
@@ -87,10 +84,7 @@ def sync_record(user, category):
             last_read_on = timezone.now()
         else:
             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):
@@ -98,7 +92,9 @@ def read_category(user, category):
     if not category.is_leaf_node():
         categories += category.get_descendants().filter(
             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.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(
             name='CategoryRead',
             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()),
                 ('category', models.ForeignKey(to='misago_categories.Category')),
                 ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
             ],
-            options={
-            },
-            bases=(models.Model,),
+            options={},
+            bases=(models.Model, ),
         ),
         migrations.CreateModel(
             name='ThreadRead',
             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()),
                 ('category', models.ForeignKey(to='misago_categories.Category')),
                 ('thread', models.ForeignKey(to='misago_threads.Thread')),
                 ('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"])
 thread_tracked = Signal(providing_args=["thread"])
 thread_read = Signal(providing_args=["thread"])
-
-
 """
 Signal handlers
 """
+
+
 @receiver(delete_category_content)
 def delete_category_threads(sender, **kwargs):
     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)
 
         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)
-        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.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()
 
     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):

+ 6 - 7
misago/readtracker/threadstracker.py

@@ -88,8 +88,7 @@ def make_thread_read_aware(user, thread):
         thread.is_new = True
 
         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
 
             if thread.last_post_on > category_record.last_read_on:
@@ -113,8 +112,10 @@ def make_posts_read_aware(user, thread, posts):
     try:
         is_thread_read = thread.is_read
     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:
         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'])
     else:
         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)
         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
 """
+
+
 class PermissionsForm(forms.Form):
     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):
@@ -28,12 +27,10 @@ def change_permissions_form(role):
 """
 ACL Builder
 """
+
+
 def build_acl(acl, roles, key_name):
-    new_acl = {
-        'can_search': 0
-    }
+    new_acl = {'can_search': 0}
     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):
         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:
                 module = import_module(module_path)
             except ImportError:
-                raise ImportError(
-                    'search module %s could not be imported' % modulename)
+                raise ImportError('search module %s could not be imported' % modulename)
 
             try:
                 classdef = getattr(module, classname)
                 self._providers.append(classdef)
             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):
         if not self._initialized:

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

@@ -14,14 +14,11 @@ class SearchApiTests(AuthenticatedUserTestCase):
 
     def test_no_permission(self):
         """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)
 
-        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):
         """api handles no search query"""
@@ -30,9 +27,9 @@ class SearchApiTests(AuthenticatedUserTestCase):
 
         providers = searchproviders.get_providers(True)
         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(six.text_type(providers[i].name), provider['name'])
@@ -46,9 +43,9 @@ class SearchApiTests(AuthenticatedUserTestCase):
 
         providers = searchproviders.get_providers(True)
         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(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.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):
             classname = settings.MISAGO_SEARCH_EXTENSIONS[i].split('.')[-1]
@@ -37,9 +36,8 @@ class SearchProvidersTests(TestCase):
         searchproviders._initialized = True
         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):
         """providers constructor is provided with request"""
@@ -48,8 +46,7 @@ class SearchProvidersTests(TestCase):
         searchproviders._initialized = True
         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):
         """
@@ -60,6 +57,5 @@ class SearchProvidersTests(TestCase):
         searchproviders._initialized = True
         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):
         """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)
 
-        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):
         """view validates permission to search forum"""
@@ -33,38 +30,28 @@ class LandingTests(AuthenticatedUserTestCase):
 class SearchTests(AuthenticatedUserTestCase):
     def test_no_permission(self):
         """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):
         """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)
 
     def test_provider_no_permission(self):
         """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):
         """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...")

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

@@ -2,9 +2,7 @@ from django.conf.urls import url
 
 from misago.search.views import landing, search
 
-
 urlpatterns = [
     url(r'^search/$', landing, 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):
         # Attachment
         urlpatterns.namespace(r'^attachments/', 'attachments', 'system')
-        urlpatterns.patterns('system:attachments',
+        urlpatterns.patterns(
+            'system:attachments',
             url(r'^$', 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'),
@@ -18,7 +19,8 @@ class MisagoAdminExtension(object):
 
         # AttachmentType
         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'^new/$', NewAttachmentType.as_view(), name='new'),
             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:
             return self.create_attachment(request)
         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):
         upload = request.FILES.get('upload')
@@ -86,17 +84,23 @@ def validate_filetype(upload, user_roles):
 def validate_filesize(upload, filetype, hard_limit):
     if upload.size > hard_limit * 1024:
         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:
-        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):

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

@@ -46,7 +46,8 @@ def validate_votes(poll, votes):
             message = ungettext(
                 "This poll disallows voting for more than %(choices)s choice.",
                 "This poll disallows voting for more than %(choices)s choices.",
-                poll.allowed_choices)
+                poll.allowed_choices
+            )
             raise ValidationError(message % {'choices': poll.allowed_choices})
     except TypeError:
         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):
-    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 = []
     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:
         message = ungettext(
             "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})
 
     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:
         if post.is_event:
             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."))
 
         if not posts:
@@ -93,7 +95,9 @@ def clean_posts_for_merge(request, thread):
                     raise MergeError(authorship_error)
 
             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."))
 
             posts.append(post)

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

@@ -54,7 +54,11 @@ def clean_thread_for_move(request, thread, viewmodel):
     try:
         new_thread = viewmodel(request, new_thread_id, select_for_update=True).unwrap()
     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']:
         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:
         message = ungettext(
             "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})
 
     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}
     else:
         return {'acl': None}
+
+
 event_patch_dispatcher.add('acl', patch_acl)
 
 
@@ -29,6 +31,8 @@ def patch_is_hidden(request, event, value):
         return {'is_hidden': event.is_hidden}
     else:
         raise PermissionDenied(_("You don't have permission to hide this event."))
+
+
 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}
     else:
         return {'acl': None}
+
+
 post_patch_dispatcher.add('acl', patch_acl)
 
 
@@ -59,10 +61,7 @@ def patch_is_liked(request, post, value):
 
     post.last_likes = []
     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'])
 
@@ -71,6 +70,8 @@ def patch_is_liked(request, post, value):
         'last_likes': post.last_likes or [],
         'is_liked': value,
     }
+
+
 post_patch_dispatcher.replace('is-liked', patch_is_liked)
 
 
@@ -81,6 +82,8 @@ def patch_is_protected(request, post, value):
     else:
         moderation.unprotect_post(request.user, post)
     return {'is_protected': post.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)
         moderation.approve_post(request.user, post)
     return {'is_unapproved': post.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)
 
     return {'is_hidden': post.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:
             thread.subscription.last_read_on = post.posted_on
             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})

+ 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:
         message = ungettext(
             "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})
 
     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):
         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)
 
@@ -108,7 +104,9 @@ class PostingEndpoint(object):
             for middleware, obj in self.middlewares:
                 obj.pre_save(self._serializers.get(middleware))
         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:
             for middleware, obj in self.middlewares:
@@ -122,13 +120,16 @@ class PostingEndpoint(object):
             for middleware, obj in self.middlewares:
                 obj.post_save(self._serializers.get(middleware))
         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):
     """
     Abstract middleware class
     """
+
     def __init__(self, **kwargs):
         self.kwargs = 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'])
 
     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):
         serializer.save()
 
 
 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):
         self.update_attachments = False
@@ -41,11 +41,12 @@ class AttachmentsSerializer(serializers.Serializer):
         validate_attachments_count(ids)
 
         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)
 
         if not attachments and not new_attachments:
-            return [] # no attachments
+            return []  # no attachments
 
         # clean existing attachments
         for attachment in attachments:
@@ -56,7 +57,9 @@ class AttachmentsSerializer(serializers.Serializer):
                     self.update_attachments = True
                     self.removed_attachments.append(attachment)
                 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})
 
         if new_attachments:
@@ -77,8 +80,7 @@ class AttachmentsSerializer(serializers.Serializer):
             return []
 
         queryset = user.attachment_set.select_related('filetype').filter(
-            post__isnull=True,
-            id__in=ids
+            post__isnull=True, id__in=ids
         )
 
         return list(queryset)
@@ -122,8 +124,11 @@ def validate_attachments_count(data):
         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 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
     """
+
     def use_this_middleware(self):
         if self.mode == PostingEndpoint.START:
             return self.tree_name == THREADS_ROOT_NAME
@@ -41,10 +42,12 @@ class CategoryMiddleware(PostingMiddleware):
 
 
 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):
         self.user = user
@@ -55,8 +58,7 @@ class CategorySerializer(serializers.Serializer):
     def validate_category(self, value):
         try:
             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)
@@ -69,4 +71,5 @@ class CategorySerializer(serializers.Serializer):
             raise serializers.ValidationError(e.args[0])
         except Category.DoesNotExist:
             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):
         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')
 
         notifications = []
@@ -40,18 +39,10 @@ class EmailNotificationMiddleware(PostingMiddleware):
         else:
             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(
-            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):
     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):
         now = timezone.now()

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

@@ -23,9 +23,7 @@ class ParticipantsMiddleware(PostingMiddleware):
         return False
 
     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):
         set_owner(self.thread, self.user)
@@ -33,10 +31,7 @@ class ParticipantsMiddleware(PostingMiddleware):
 
 
 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):
         clean_usernames = self.clean_usernames(usernames)
@@ -49,7 +44,8 @@ class ParticipantsSerializer(serializers.Serializer):
 
             if clean_name == self.context['user'].slug:
                 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:
                 clean_usernames.append(clean_name)
@@ -62,11 +58,12 @@ class ParticipantsSerializer(serializers.Serializer):
             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 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))
 
@@ -84,7 +81,6 @@ class ParticipantsSerializer(serializers.Serializer):
             sorted_usernames = sorted(invalid_usernames)
 
             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

+ 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
     """
+
     def use_this_middleware(self):
         if self.mode == PostingEndpoint.START:
             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_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(
             category=self.post.category,

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

@@ -84,16 +84,12 @@ class ReplyMiddleware(PostingMiddleware):
 class ReplySerializer(serializers.Serializer):
     post = serializers.CharField(
         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):
     title = serializers.CharField(
         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
     """
+
     def use_this_middleware(self):
         if self.mode == PostingEndpoint.REPLY:
             return self.thread.thread_type.root_name == PRIVATE_THREADS_ROOT_NAME
@@ -15,6 +16,5 @@ class SyncPrivateThreadsMiddleware(PostingMiddleware):
 
     def post_save(self, serializer):
         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)
 
     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)

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

@@ -11,7 +11,7 @@ class ThreadsList(object):
     def __call__(self, request, **kwargs):
         page = get_int_or_404(request.query_params.get('page', 0))
         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')
 

+ 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
 
 
-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):
@@ -42,15 +42,19 @@ def thread_merge_endpoint(request, thread, viewmodel):
         if not can_reply_thread(request.user, other_thread):
             raise PermissionDenied(_("You can't merge this thread into thread you can't reply."))
         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:
-        return Response({
-            'detail': e.args[0]
-        }, status=400)
+        return Response({'detail': e.args[0]}, status=400)
     except Http404:
         return Response({
-            'detail': _("The thread you have entered link to doesn't exist or you don't have permission to see it.")
-        }, status=400)
+            '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])
     if len(polls_handler.polls) == 1:
@@ -67,13 +71,9 @@ def thread_merge_endpoint(request, thread, viewmodel):
                 elif not poll:
                     other_thread.poll.delete()
             else:
-                return Response({
-                    'detail': _("Invalid choice.")
-                }, status=400)
+                return Response({'detail': _("Invalid choice.")}, status=400)
         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)
 
@@ -106,9 +106,7 @@ def threads_merge_endpoint(request):
             invalid_threads.append({
                 'id': thread.pk,
                 '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:
@@ -125,13 +123,9 @@ def threads_merge_endpoint(request):
                 if polls_handler.is_valid():
                     poll = polls_handler.get_resolution()
                 else:
-                    return Response({
-                        'detail': _("Invalid choice.")
-                    }, status=400)
+                    return Response({'detail': _("Invalid choice.")}, status=400)
             else:
-                return Response({
-                    'polls': polls_handler.get_available_resolutions()
-                }, status=400)
+                return Response({'polls': polls_handler.get_available_resolutions()}, status=400)
         else:
             poll = None
 
@@ -152,8 +146,8 @@ def clean_threads_for_merge(request):
     elif len(threads_ids) > MERGE_LIMIT:
         message = ungettext(
             "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})
 
     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)
         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.save()
@@ -223,9 +219,10 @@ def merge_threads(request, validated_data, threads, poll):
 
     # add top category to thread
     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])
     else:
         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}
     else:
         return {'acl': None}
+
+
 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)
     return {'title': thread.title}
+
+
 thread_patch_dispatcher.replace('title', patch_title)
 
 
@@ -72,6 +76,8 @@ def patch_weight(request, thread, value):
         moderation.unpin_thread(request, thread)
 
     return {'weight': thread.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)
     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)
@@ -97,21 +102,24 @@ def patch_move(request, thread, value):
 
     return {'category': CategorySerializer(new_category).data}
 
+
 thread_patch_dispatcher.replace('category', patch_move)
 
 
 def patch_top_category(request, thread, value):
     category_pk = get_int_or_404(value)
     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])
     return {'top_category': CategorySerializer(thread.top_category).data}
+
+
 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,
         }
     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)
 
 
@@ -142,6 +149,8 @@ def patch_is_unapproved(request, thread, value):
         }
     else:
         raise PermissionDenied(_("You don't have permission to approve this thread."))
+
+
 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."))
         else:
             raise PermissionDenied(_("You don't have permission to open this thread."))
+
+
 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}
     else:
         raise PermissionDenied(_("You don't have permission to hide this thread."))
+
+
 thread_patch_dispatcher.replace('is-hidden', patch_is_hidden)
 
 
@@ -197,6 +210,8 @@ def patch_subscription(request, thread, value):
         return {'subscription': True}
     else:
         return {'subscription': None}
+
+
 thread_patch_dispatcher.replace('subscription', patch_subscription)
 
 
@@ -206,8 +221,7 @@ def patch_add_participant(request, thread, value):
     try:
         username = six.text_type(value).strip().lower()
         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)
     except UserModel.DoesNotExist:
         raise PermissionDenied(_("No user with such name exists."))
@@ -219,12 +233,11 @@ def patch_add_participant(request, thread, value):
     add_participant(request, thread, participant)
 
     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)
 
 
@@ -244,18 +257,14 @@ def patch_remove_participant(request, thread, value):
     remove_participant(request, thread, participant.user)
 
     if len(thread.participants_list) == 1:
-        return {
-            'deleted': True
-        }
+        return {'deleted': True}
     else:
         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)
 
 
@@ -279,9 +288,9 @@ def patch_replace_owner(request, thread, value):
 
     make_participants_aware(request.user, thread)
     participants = ThreadParticipantSerializer(thread.participants_list, many=True)
-    return {
-        'participants': participants.data
-    }
+    return {'participants': participants.data}
+
+
 thread_patch_dispatcher.replace('owner', patch_replace_owner)
 
 
@@ -300,7 +309,7 @@ def thread_patch_endpoint(request, thread):
 
     title_changed = old_title != thread.title
     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:
         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)
     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,
         tree_id=threads_tree_id,
     )
@@ -32,6 +33,4 @@ def read_private_threads(user):
 
     user.sync_unread_private_threads = False
     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.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'])
     def votes(self, request, thread_pk, pk):
@@ -152,7 +150,8 @@ class ViewSet(viewsets.ViewSet):
             choices.append(choice)
 
         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():
             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
     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(
-            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):
         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):
@@ -62,7 +56,7 @@ class ViewSet(viewsets.ViewSet):
     def list(self, request, thread_pk):
         page = get_int_or_404(request.query_params.get('page', 0))
         if page == 1:
-            page = 0 # api allows explicit first page
+            page = 0  # api allows explicit first page
 
         thread = self.get_thread(request, thread_pk)
         posts = self.get_posts(request, thread, page)
@@ -98,12 +92,7 @@ class ViewSet(viewsets.ViewSet):
         post = Post(thread=thread, category=thread.category)
 
         # 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():
             user_posts = request.user.posts
@@ -128,12 +117,7 @@ class ViewSet(viewsets.ViewSet):
 
         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():
             post_edits = post.edits
@@ -193,12 +177,7 @@ class ViewSet(viewsets.ViewSet):
 
     @detail_route(methods=['get'], url_path='editor')
     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()
 
         allow_edit_post(request.user, post)
@@ -208,7 +187,8 @@ class ViewSet(viewsets.ViewSet):
             add_acl(request.user, attachment)
             attachments.append(attachment)
         attachments_json = AttachmentSerializer(
-            attachments, many=True, context={'user': request.user}).data
+            attachments, many=True, context={'user': request.user}
+        ).data
 
         return Response({
             'id': post.pk,
@@ -223,10 +203,7 @@ class ViewSet(viewsets.ViewSet):
     @list_route(methods=['get'], url_path='editor')
     def reply_editor(self, request, thread_pk):
         thread = self.get_thread(
-            request,
-            thread_pk,
-            read_aware=False,
-            subscription_aware=False
+            request, thread_pk, read_aware=False, subscription_aware=False
         ).unwrap()
         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):
     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(
-            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):
         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):
@@ -76,11 +70,7 @@ class ThreadViewSet(ViewSet):
 
         # Put them through posting pipeline
         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():

+ 0 - 4
misago/threads/context_processors.py

@@ -4,13 +4,9 @@ from django.urls import reverse
 def preload_threads_urls(request):
     request.frontend_context.update({
         'ATTACHMENTS_API': reverse('misago:api:attachment-list'),
-
         'THREAD_EDITOR_API': reverse('misago:api:thread-editor'),
         'THREADS_API': reverse('misago:api:thread-list'),
-
         'PRIVATE_THREADS_API': reverse('misago:api:private-thread-list'),
-
-
         '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(
         label=_("State"),
         required=False,
-        choices=(
-            ('', _("All")),
-            ('yes', _("Only orphaned")),
-            ('no', _("Not orphaned")),
-        ),
+        choices=(('', _("All")), ('yes', _("Only orphaned")), ('no', _("Not orphaned")), ),
     )
 
     def filter_queryset(self, criteria, queryset):
@@ -58,19 +54,33 @@ class AttachmentTypeForm(forms.ModelForm):
             'limit_downloads_to': _("Limit downloads to"),
         }
         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 = {
             'limit_uploads_to': forms.CheckboxSelectMultiple,
@@ -78,7 +88,7 @@ class AttachmentTypeForm(forms.ModelForm):
         }
 
     def clean_extensions(self):
-        data =  self.clean_list(self.cleaned_data['extensions'])
+        data = self.clean_list(self.cleaned_data['extensions'])
         if not data:
             raise forms.ValidationError(_("This field is required."))
         return data

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

@@ -15,10 +15,7 @@ class Command(BaseCommand):
 
     def handle(self, *args, **options):
         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()
 

+ 7 - 8
misago/threads/middleware.py

@@ -20,10 +20,7 @@ class UnreadThreadsCountMiddleware(MiddlewareMixin):
         participated_threads = request.user.threadparticipant_set.values('thread_id')
 
         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)
         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.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(
             name='Post',
             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_ip', models.GenericIPAddressField()),
                 ('original', models.TextField()),
@@ -34,7 +38,15 @@ class Migration(migrations.Migration):
                 ('edits', models.PositiveIntegerField(default=0)),
                 ('last_editor_name', models.CharField(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_slug', models.SlugField(max_length=255, null=True, blank=True)),
                 ('hidden_on', models.DateTimeField(default=django.utils.timezone.now)),
@@ -44,9 +56,27 @@ class Migration(migrations.Migration):
                 ('is_hidden', models.BooleanField(default=False)),
                 ('is_protected', models.BooleanField(default=False)),
                 ('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)),
                 ('event_type', models.CharField(max_length=255, 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_vector', SearchVectorField()),
             ],
-            options={
-            },
-            bases=(models.Model,),
+            options={},
+            bases=(models.Model, ),
         ),
         CreatePartialIndex(
             field='Post.has_open_reports',
@@ -72,7 +101,11 @@ class Migration(migrations.Migration):
         migrations.CreateModel(
             name='Thread',
             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)),
                 ('slug', models.CharField(max_length=255)),
                 ('replies', models.PositiveIntegerField(default=0, db_index=True)),
@@ -94,9 +127,8 @@ class Migration(migrations.Migration):
                 ('is_hidden', models.BooleanField(default=False)),
                 ('is_closed', models.BooleanField(default=False)),
             ],
-            options={
-            },
-            bases=(models.Model,),
+            options={},
+            bases=(models.Model, ),
         ),
         CreatePartialIndex(
             field='Thread.weight',
@@ -111,19 +143,26 @@ class Migration(migrations.Migration):
         migrations.CreateModel(
             name='ThreadParticipant',
             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')),
                 ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
                 ('is_owner', models.BooleanField(default=False)),
             ],
-            options={
-            },
-            bases=(models.Model,),
+            options={},
+            bases=(models.Model, ),
         ),
         migrations.AddField(
             model_name='thread',
             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,
         ),
         CreatePartialIndex(
@@ -165,7 +204,13 @@ class Migration(migrations.Migration):
         migrations.AddField(
             model_name='thread',
             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,
         ),
         migrations.AddField(
@@ -177,19 +222,36 @@ class Migration(migrations.Migration):
         migrations.AddField(
             model_name='thread',
             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,
         ),
         migrations.AddField(
             model_name='thread',
             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,
         ),
         migrations.AddField(
             model_name='thread',
             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,
         ),
         migrations.AlterIndexTogether(
@@ -211,16 +273,19 @@ class Migration(migrations.Migration):
         migrations.CreateModel(
             name='Subscription',
             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)),
                 ('send_email', models.BooleanField(default=False)),
                 ('category', models.ForeignKey(to='misago_categories.Category')),
                 ('thread', models.ForeignKey(to='misago_threads.Thread')),
                 ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
             ],
-            options={
-            },
-            bases=(models.Model,),
+            options={},
+            bases=(models.Model, ),
         ),
         migrations.AlterIndexTogether(
             name='subscription',
@@ -231,17 +296,43 @@ class Migration(migrations.Migration):
         migrations.CreateModel(
             name='PostEdit',
             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)),
                 ('editor_name', models.CharField(max_length=255)),
                 ('editor_slug', models.CharField(max_length=255)),
                 ('editor_ip', models.GenericIPAddressField()),
                 ('edited_from', 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={
                 'ordering': ['-id'],
@@ -250,47 +341,115 @@ class Migration(migrations.Migration):
         migrations.CreateModel(
             name='Attachment',
             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)),
-                ('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_slug', models.CharField(max_length=255, db_index=True)),
                 ('uploader_ip', models.GenericIPAddressField()),
                 ('filename', models.CharField(max_length=255, 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(
             name='AttachmentType',
             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)),
                 ('extensions', models.CharField(max_length=255)),
                 ('mimetypes', models.CharField(blank=True, max_length=255, null=True)),
                 ('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(
             model_name='attachment',
             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(
             model_name='attachment',
             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(
             name='Poll',
             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_slug', models.CharField(max_length=255)),
                 ('poster_ip', models.GenericIPAddressField()),
@@ -302,24 +461,64 @@ class Migration(migrations.Migration):
                 ('allow_revotes', models.BooleanField(default=False)),
                 ('votes', models.PositiveIntegerField(default=0)),
                 ('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(
             name='PollVote',
             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_slug', models.CharField(max_length=255)),
                 ('voter_ip', models.GenericIPAddressField()),
                 ('voted_on', models.DateTimeField(default=django.utils.timezone.now)),
                 ('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(
@@ -331,12 +530,21 @@ class Migration(migrations.Migration):
         migrations.CreateModel(
             name='PostLike',
             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_slug', models.CharField(max_length=255)),
                 ('liker_ip', models.GenericIPAddressField()),
                 ('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={
                 'ordering': ['-id'],
@@ -345,21 +553,34 @@ class Migration(migrations.Migration):
         migrations.AddField(
             model_name='postlike',
             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(
             model_name='postlike',
             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(
             model_name='postlike',
             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(
             model_name='post',
             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):
-    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',
                 'name': _("Minimum length"),
                 'description': _("Minimum allowed thread title length."),
@@ -27,8 +30,7 @@ def create_threads_settings_group(apps, schema_editor):
                     'max_value': 255,
                 },
                 'is_public': True,
-            },
-            {
+            }, {
                 'setting': 'thread_title_length_max',
                 'name': _("Maximum length"),
                 'description': _("Maximum allowed thread length."),
@@ -39,8 +41,7 @@ def create_threads_settings_group(apps, schema_editor):
                     'max_value': 255,
                 },
                 'is_public': True,
-            },
-            {
+            }, {
                 'setting': 'post_length_min',
                 'name': _("Minimum length"),
                 'description': _("Minimum allowed user post length."),
@@ -51,23 +52,28 @@ def create_threads_settings_group(apps, schema_editor):
                     'min_value': 1,
                 },
                 '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': {
                     'min_value': 0,
                 },
-                'is_public': True,
-            },
-        )
-    })
+                'is_public':
+                    True,
+            }, )
+        }
+    )
 
 
 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
 
 
-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):

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

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

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

@@ -25,33 +25,21 @@ def upload_to(instance, filename):
         if filename_lowered.endswith(extension):
             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
 class Attachment(models.Model):
     secret = models.CharField(max_length=64)
     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)
 
     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_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)
     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):
         return self.filename
@@ -107,17 +80,21 @@ class Attachment(models.Model):
         return not self.is_image
 
     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):
         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:
             return None
 
@@ -131,8 +108,8 @@ class Attachment(models.Model):
 
         thumbnail = Image.open(upload)
         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'
 

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

@@ -17,11 +17,8 @@ class AttachmentType(models.Model):
     size_limit = models.PositiveIntegerField(default=1024)
     status = models.PositiveIntegerField(
         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)

+ 1 - 4
misago/threads/models/poll.py

@@ -37,10 +37,7 @@ class Poll(models.Model):
             self.category_id = thread.category_id
             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
     def ends_on(self):

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

@@ -82,7 +82,7 @@ class Post(models.Model):
 
     class Meta:
         index_together = [
-            ('thread', 'id'), # speed up threadview for team members
+            ('thread', 'id'),  # speed up threadview for team members
             ('is_event', 'is_hidden'),
             ('poster', 'posted_on')
         ]

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

@@ -13,6 +13,4 @@ class Subscription(models.Model):
     send_email = models.BooleanField(default=False)
 
     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_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')
     title = models.CharField(max_length=255)
@@ -35,27 +33,16 @@ class Thread(models.Model):
     last_post_on = models.DateTimeField(db_index=True)
 
     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(
-        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_slug = models.CharField(max_length=255)
 
     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_poster = models.ForeignKey(
@@ -119,10 +106,7 @@ class Thread(models.Model):
         except ObjectDoesNotExist:
             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:
             self.replies -= 1

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

@@ -5,35 +5,21 @@ from misago.conf import settings
 
 class ThreadParticipantManager(models.Manager):
     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)
 
-        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):
         bulk = []
         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)
 
     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):

+ 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_slug = user.slug
         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
     else:
         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.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
     else:
         return False
@@ -77,12 +75,14 @@ def move_thread(request, thread, new_category):
         from_category = thread.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
     else:
         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_slug = request.user.slug
         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
 
         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
     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
         if orphans:
             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):
         """

+ 24 - 38
misago/threads/participants.py

@@ -27,10 +27,7 @@ def make_threads_participants_aware(user, threads):
         thread.participant = None
         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:
         participant.user = user
@@ -51,8 +48,7 @@ def make_thread_participants_aware(user, thread):
     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 = []
     if users:
         users_ids += [u.pk for u in users]
@@ -64,9 +60,7 @@ def set_users_unread_private_threads_sync(
     if not users_ids:
         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):
@@ -82,17 +76,17 @@ def change_owner(request, thread, user):
     """
     ThreadParticipant.objects.set_owner(thread, user)
     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:
-        record_event(request, thread, 'changed_owner', {
-            'user': {
+        record_event(
+            request, thread, 'changed_owner',
+            {'user': {
                 'username': user.username,
                 'url': user.get_absolute_url(),
-            }
-        })
+            }}
+        )
     else:
         record_event(request, thread, 'tookover')
 
@@ -106,12 +100,13 @@ def add_participant(request, thread, user):
     if request.user == user:
         record_event(request, thread, 'entered_thread')
     else:
-        record_event(request, thread, 'added_participant', {
-            'user': {
+        record_event(
+            request, thread, 'added_participant',
+            {'user': {
                 'username': user.username,
                 'url': user.get_absolute_url(),
-            }
-        })
+            }}
+        )
 
 
 def add_participants(request, thread, users):
@@ -127,9 +122,7 @@ def add_participants(request, thread, users):
         thread_participants = []
 
     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 = []
@@ -142,19 +135,11 @@ def add_participants(request, thread, users):
 
 def build_noticiation_email(request, thread, user):
     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(
-        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()
 
         if removed_owner:
-            thread.is_closed = True # flag thread to close
+            thread.is_closed = True  # flag thread to close
 
             if request.user == user:
                 event_type = 'owner_left'
@@ -192,9 +177,10 @@ def remove_participant(request, thread, user):
             else:
                 event_type = 'removed_participant'
 
-        record_event(request, thread, event_type, {
-            'user': {
+        record_event(
+            request, thread, event_type,
+            {'user': {
                 'username': user.username,
                 '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
 """
+
+
 class PermissionsForm(forms.Form):
     legend = _("Attachments")
 
@@ -20,7 +22,9 @@ class PermissionsForm(forms.Form):
         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"))
 
 
@@ -43,6 +47,8 @@ def change_permissions_form(role):
 """
 ACL Builder
 """
+
+
 def build_acl(acl, roles, key_name):
     new_acl = {
         'max_attachment_size': 0,
@@ -51,7 +57,10 @@ def build_acl(acl, roles, key_name):
     }
     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,
         can_download_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
 """
+
+
 def add_acl_to_attachment(user, attachment):
     if user.is_authenticated and user.id == attachment.uploader_id:
         attachment.acl.update({

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

@@ -23,11 +23,11 @@ __all__ = [
     'allow_see_poll_votes',
     'can_see_poll_votes',
 ]
-
-
 """
 Admin Permissions Forms
 """
+
+
 class RolePermissionsForm(forms.Form):
     legend = _("Polls")
 
@@ -35,31 +35,19 @@ class RolePermissionsForm(forms.Form):
         label=_("Can start polls"),
         coerce=int,
         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(
         label=_("Can edit polls"),
         coerce=int,
         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(
         label=_("Can delete polls"),
         coerce=int,
         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(
         label=_("Time limit for own polls edits, in minutes"),
@@ -83,6 +71,8 @@ def change_permissions_form(role):
 """
 ACL Builder
 """
+
+
 def build_acl(acl, roles, key_name):
     acl.update({
         'can_start_polls': 0,
@@ -92,7 +82,10 @@ def build_acl(acl, roles, key_name):
         '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_edit_polls=algebra.greater,
         can_delete_polls=algebra.greater,
@@ -104,6 +97,8 @@ def build_acl(acl, roles, key_name):
 """
 ACL's for targets
 """
+
+
 def add_acl_to_poll(user, poll):
     poll.acl.update({
         '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):
-    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):
@@ -127,13 +120,17 @@ def register_with(registry):
 """
 ACL tests
 """
+
+
 def allow_start_poll(user, target):
     if user.is_anonymous:
         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'):
         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."))
         if target.is_closed:
             raise PermissionDenied(_("This thread is closed. You can't start polls in it."))
+
+
 can_start_poll = return_boolean(allow_start_poll)
 
 
@@ -152,9 +151,11 @@ def allow_edit_poll(user, target):
     if user.is_anonymous:
         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'):
         raise PermissionDenied(_("You can't edit polls."))
@@ -166,7 +167,8 @@ def allow_edit_poll(user, target):
             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 minutes.",
-                user.acl_cache['poll_edit_time'])
+                user.acl_cache['poll_edit_time']
+            )
             raise PermissionDenied(message % {'minutes': user.acl_cache['poll_edit_time']})
 
         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."))
         if target.thread.is_closed:
             raise PermissionDenied(_("This thread is closed. You can't edit polls in it."))
+
+
 can_edit_poll = return_boolean(allow_edit_poll)
 
 
@@ -184,9 +188,11 @@ def allow_delete_poll(user, target):
     if user.is_anonymous:
         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'):
         raise PermissionDenied(_("You can't delete polls."))
@@ -198,7 +204,8 @@ def allow_delete_poll(user, target):
             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 minutes.",
-                user.acl_cache['poll_edit_time'])
+                user.acl_cache['poll_edit_time']
+            )
             raise PermissionDenied(message % {'minutes': user.acl_cache['poll_edit_time']})
         if target.is_over:
             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."))
         if target.thread.is_closed:
             raise PermissionDenied(_("This thread is closed. You can't delete polls in it."))
+
+
 can_delete_poll = return_boolean(allow_delete_poll)
 
 
@@ -220,21 +229,27 @@ def allow_vote_poll(user, target):
     if target.is_over:
         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 target.category.is_closed:
             raise PermissionDenied(_("This category is closed. You can't vote in it."))
         if target.thread.is_closed:
             raise PermissionDenied(_("This thread is closed. You can't vote in it."))
+
+
 can_vote_poll = return_boolean(allow_vote_poll)
 
 
 def allow_see_poll_votes(user, target):
     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."))
+
+
 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',
     'can_message_user',
 ]
-
-
 """
 Admin Permissions Form
 """
+
+
 class PermissionsForm(forms.Form):
     legend = _("Private threads")
 
@@ -50,13 +50,17 @@ class PermissionsForm(forms.Form):
     )
     can_report_private_threads = YesNoSwitch(
         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(
         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
 """
+
+
 def build_acl(acl, roles, key_name):
     new_acl = {
         'can_use_private_threads': 0,
@@ -82,7 +88,10 @@ def build_acl(acl, roles, key_name):
 
     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_start_private_threads=algebra.greater,
         max_private_thread_participants=algebra.greater_or_zero,
@@ -172,11 +181,15 @@ def register_with(registry):
 """
 ACL tests
 """
+
+
 def allow_use_private_threads(user):
     if user.is_anonymous:
         raise PermissionDenied(_("You have to sign in to use private threads."))
     if not user.acl_cache['can_use_private_threads']:
         raise PermissionDenied(_("You can't 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):
         raise Http404()
+
+
 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
 
     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:
-        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)
 
 
@@ -212,19 +227,18 @@ def allow_add_participants(user, target):
 
     if not is_moderator:
         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:
-            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']
     current_participants = len(target.participants_list) - 1
 
     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)
 
 
@@ -233,15 +247,15 @@ def allow_remove_participant(user, thread, target):
         return
 
     if user == target:
-        return # we can always remove ourselves
+        return  # we can always remove ourselves
 
     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:
-        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)
 
 
@@ -249,8 +263,7 @@ def allow_add_participant(user, target):
     message_format = {'user': target.username}
 
     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']:
         return
@@ -260,15 +273,20 @@ def allow_add_participant(user, target):
 
     if target.can_be_messaged_by_nobody:
         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):
         message = _("%(user)s limits invitations to private threads to followed users.")
         raise PermissionDenied(message % message_format)
+
+
 can_add_participant = return_boolean(allow_add_participant)
 
 
 def allow_message_user(user, target):
     allow_use_private_threads(user)
     allow_add_participant(user, target)
+
+
 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_posts',
 ]
-
-
 """
 Admin Permissions Forms
 """
+
+
 class RolePermissionsForm(forms.Form):
     legend = _("Threads")
 
     can_see_unapproved_content_lists = YesNoSwitch(
         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(
         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(
         label=_("Can omit flood protection"),
@@ -102,11 +106,7 @@ class CategoryPermissionsForm(forms.Form):
                     "with no replies can be hidden."),
         coerce=int,
         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(
         label=_("Time limit for own threads edits, in minutes"),
@@ -118,22 +118,14 @@ class CategoryPermissionsForm(forms.Form):
         label=_("Can hide all threads"),
         coerce=int,
         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(
         label=_("Can pin threads"),
         coerce=int,
         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_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."),
         coerce=int,
         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(
         label=_("Time limit for own post edits, in minutes"),
@@ -166,22 +154,14 @@ class CategoryPermissionsForm(forms.Form):
         label=_("Can hide all posts"),
         coerce=int,
         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(
         label=_("Can see posts likes"),
         coerce=int,
         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(
         label=_("Can like posts"),
@@ -193,8 +173,7 @@ class CategoryPermissionsForm(forms.Form):
         help_text=_("Only users with this permission can edit protected posts.")
     )
     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_approve_content = YesNoSwitch(
@@ -208,11 +187,7 @@ class CategoryPermissionsForm(forms.Form):
         label=_("Can hide events"),
         coerce=int,
         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
 """
+
+
 def build_acl(acl, roles, key_name):
     acl.update({
         'can_see_unapproved_content_lists': False,
@@ -237,7 +214,10 @@ def build_acl(acl, roles, key_name):
         '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_reported_content_lists=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})
         if category_acl['can_browse']:
             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'):
                 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)
 
-    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_start_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
 """
+
+
 def add_acl_to_category(user, category):
     category_acl = user.acl_cache['categories'].get(category.pk, {})
 
@@ -355,13 +341,17 @@ def add_acl_to_category(user, category):
         '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_posts_likes=algebra.greater,
     )
 
     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_reply_threads=algebra.greater,
             can_edit_threads=algebra.greater,
@@ -480,11 +470,13 @@ def register_with(registry):
 """
 ACL tests
 """
+
+
 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']):
         raise Http404()
@@ -498,6 +490,8 @@ def allow_see_thread(user, target):
 
         if target.is_unapproved and not category_acl['can_approve_content']:
             raise Http404()
+
+
 can_see_thread = return_boolean(allow_see_thread)
 
 
@@ -505,16 +499,20 @@ def allow_start_thread(user, target):
     if user.is_anonymous:
         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']:
         raise PermissionDenied(_("This category is closed. You can't start new threads in it."))
 
     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)
 
 
@@ -522,10 +520,10 @@ def allow_reply_thread(user, target):
     if user.is_anonymous:
         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 target.category.is_closed:
@@ -535,6 +533,8 @@ def allow_reply_thread(user, target):
 
     if not category_acl['can_reply_threads']:
         raise PermissionDenied(_("You can't reply to threads in this category."))
+
+
 can_reply_thread = return_boolean(allow_reply_thread)
 
 
@@ -542,9 +542,7 @@ def allow_edit_thread(user, target):
     if user.is_anonymous:
         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']:
         raise PermissionDenied(_("You can't edit threads in this category."))
@@ -563,16 +561,19 @@ def allow_edit_thread(user, target):
             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 minutes.",
-                category_acl['thread_edit_time'])
+                category_acl['thread_edit_time']
+            )
             raise PermissionDenied(message % {'minutes': category_acl['thread_edit_time']})
+
+
 can_edit_thread = return_boolean(allow_edit_thread)
 
 
 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 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']:
         raise Http404()
+
+
 can_see_post = return_boolean(allow_see_post)
 
 
@@ -593,9 +596,7 @@ def allow_edit_post(user, target):
     if target.is_event:
         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']:
         raise PermissionDenied(_("You can't edit posts in this category."))
@@ -620,8 +621,11 @@ def allow_edit_post(user, target):
             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 minutes.",
-                category_acl['post_edit_time'])
+                category_acl['post_edit_time']
+            )
             raise PermissionDenied(message % {'minutes': category_acl['post_edit_time']})
+
+
 can_edit_post = return_boolean(allow_edit_post)
 
 
@@ -629,10 +633,10 @@ def allow_unhide_post(user, target):
     if user.is_anonymous:
         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_own_posts']:
@@ -654,11 +658,14 @@ def allow_unhide_post(user, target):
             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 minutes.",
-                category_acl['post_edit_time'])
+                category_acl['post_edit_time']
+            )
             raise PermissionDenied(message % {'minutes': category_acl['post_edit_time']})
 
     if target.is_first_post:
         raise PermissionDenied(_("You can't reveal thread's first post."))
+
+
 can_unhide_post = return_boolean(allow_unhide_post)
 
 
@@ -666,10 +673,10 @@ def allow_hide_post(user, target):
     if user.is_anonymous:
         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_own_posts']:
@@ -691,11 +698,14 @@ def allow_hide_post(user, target):
             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 minutes.",
-                category_acl['post_edit_time'])
+                category_acl['post_edit_time']
+            )
             raise PermissionDenied(message % {'minutes': category_acl['post_edit_time']})
 
     if target.is_first_post:
         raise PermissionDenied(_("You can't hide thread's first post."))
+
+
 can_hide_post = return_boolean(allow_hide_post)
 
 
@@ -703,10 +713,10 @@ def allow_delete_post(user, target):
     if user.is_anonymous:
         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_own_posts'] != 2:
@@ -728,11 +738,14 @@ def allow_delete_post(user, target):
             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 minutes.",
-                category_acl['post_edit_time'])
+                category_acl['post_edit_time']
+            )
             raise PermissionDenied(message % {'minutes': category_acl['post_edit_time']})
 
     if target.is_first_post:
         raise PermissionDenied(_("You can't delete thread's first post."))
+
+
 can_delete_post = return_boolean(allow_delete_post)
 
 
@@ -740,14 +753,16 @@ def allow_protect_post(user, target):
     if user.is_anonymous:
         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']:
         raise PermissionDenied(_("You can't protect posts in this category."))
     if not can_edit_post(user, target):
         raise PermissionDenied(_("You can't protect posts you can't edit."))
+
+
 can_protect_post = return_boolean(allow_protect_post)
 
 
@@ -755,9 +770,9 @@ def allow_approve_post(user, target):
     if user.is_anonymous:
         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']:
         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."))
     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."))
+
+
 can_approve_post = return_boolean(allow_approve_post)
 
 
@@ -772,9 +789,7 @@ def allow_move_post(user, target):
     if user.is_anonymous:
         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']:
         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."))
     if not category_acl['can_hide_posts'] and target.is_hidden:
         raise PermissionDenied(_("You can't move posts the content you can't see."))
+
+
 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:
         raise PermissionDenied(_("You can't delete events in this category."))
-can_delete_event = return_boolean(allow_delete_event)
 
 
+can_delete_event = return_boolean(allow_delete_event)
 """
 Permission check helpers
 """
+
+
 def can_change_owned_thread(user, target):
     if user.is_anonymous or user.pk != target.starter_id:
         return False
@@ -836,6 +855,8 @@ def has_time_to_edit_post(user, target):
 """
 Queryset helpers
 """
+
+
 def exclude_invisible_threads(user, categories, queryset):
     show_all = []
     show_accepted_visible = []

+ 10 - 7
misago/threads/search.py

@@ -22,14 +22,19 @@ class SearchThreads(SearchProvider):
 
         if len(query) > 2:
             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)
         else:
             results = []
 
         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)
 
         posts = list(list_page.object_list)
@@ -38,12 +43,10 @@ class SearchThreads(SearchProvider):
         for post in posts:
             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': FeedSerializer(
-                posts, many=True, context={'user': self.request.user}).data
+            'results': FeedSerializer(posts, many=True, context={'user': self.request.user}).data
         }
         results.update(paginator)
 

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

@@ -21,19 +21,8 @@ class AttachmentSerializer(serializers.ModelSerializer):
     class Meta:
         model = Attachment
         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):
@@ -63,9 +52,11 @@ class AttachmentSerializer(serializers.ModelSerializer):
 
     def get_uploader_url(self, obj):
         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:
             return None

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

@@ -12,14 +12,9 @@ __all__ = [
     '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):
@@ -31,18 +26,10 @@ class FeedSerializer(PostSerializer, MutableFields):
 
     class Meta:
         model = Post
-        fields = PostSerializer.Meta.fields + [
-            'category',
-
-            'thread',
-            'top_category'
-        ]
+        fields = PostSerializer.Meta.fields + ['category', 'thread', 'top_category']
 
     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):
         try:

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

@@ -43,20 +43,24 @@ class NewThreadSerializer(serializers.Serializer):
         try:
             add_acl(self.context, self.category)
         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 == 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:
-                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
 
     def validate_is_hidden(self, is_hidden):
         try:
             add_acl(self.context, self.category)
         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'):
             raise ValidationError(_("You don't have permission to hide threads in this category."))
@@ -66,7 +70,7 @@ class NewThreadSerializer(serializers.Serializer):
         try:
             add_acl(self.context, self.category)
         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'):
             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:
         model = Poll
         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):
@@ -58,10 +45,12 @@ class PollSerializer(serializers.ModelSerializer):
 
     def get_poster_url(self, obj):
         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:
             return None
 
@@ -80,19 +69,13 @@ class EditPollSerializer(serializers.ModelSerializer):
     question = serializers.CharField(required=True, max_length=255)
     allowed_choices = serializers.IntegerField(required=True, min_value=1)
     choices = serializers.ListField(
-       allow_empty=False,
-       child=serializers.DictField(),
+        allow_empty=False,
+        child=serializers.DictField(),
     )
 
     class Meta:
         model = Poll
-        fields = (
-            'length',
-            'question',
-            'allowed_choices',
-            'allow_revotes',
-            'choices',
-        )
+        fields = ('length', 'question', 'allowed_choices', 'allow_revotes', 'choices', )
 
     def validate_choices(self, choices):
         clean_choices = list(map(self.clean_choice, choices))
@@ -105,15 +88,10 @@ class EditPollSerializer(serializers.ModelSerializer):
         final_choices = []
         for choice in clean_choices:
             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']])
             else:
-                choice.update({
-                    'hash': get_random_string(12),
-                    'votes': 0
-                })
+                choice.update({'hash': get_random_string(12), 'votes': 0})
                 final_choices.append(choice)
 
         self.validate_choices_num(final_choices)
@@ -142,16 +120,18 @@ class EditPollSerializer(serializers.ModelSerializer):
             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 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):
         if data['allowed_choices'] > len(data['choices']):
             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
 
     def update(self, instance, validated_data):
@@ -177,12 +157,7 @@ class NewPollSerializer(EditPollSerializer):
     class Meta:
         model = Poll
         fields = (
-            'length',
-            'question',
-            'allowed_choices',
-            'allow_revotes',
-            'is_public',
-            'choices',
+            'length', 'question', 'allowed_choices', 'allow_revotes', 'is_public', 'choices',
         )
 
     def validate_choices(self, choices):
@@ -191,10 +166,7 @@ class NewPollSerializer(EditPollSerializer):
         self.validate_choices_num(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
 

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

@@ -13,20 +13,16 @@ class PollVoteSerializer(serializers.Serializer):
     url = serializers.SerializerMethodField()
 
     class Meta:
-        fields = (
-            'voted_on',
-
-            'username',
-
-            'url',
-        )
+        fields = ('voted_on', 'username', 'url', )
 
     def get_username(self, obj):
         return obj['voter_name']
 
     def get_url(self, obj):
         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']
 
-
 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):
@@ -57,14 +57,12 @@ class PostSerializer(serializers.ModelSerializer, MutableFields):
             'is_event',
             'event_type',
             'event_context',
-
             'acl',
             'is_liked',
             'is_new',
             'is_read',
             'last_likes',
             'likes',
-
             'api',
             'url',
         ]
@@ -151,18 +149,18 @@ class PostSerializer(serializers.ModelSerializer, MutableFields):
 
     def get_last_editor_url(self, obj):
         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:
             return None
 
     def get_hidden_by_url(self, obj):
         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:
             return None

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

@@ -17,16 +17,7 @@ class PostEditSerializer(serializers.ModelSerializer):
 
     class Meta:
         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):
         return obj.get_diff()
@@ -38,9 +29,11 @@ class PostEditSerializer(serializers.ModelSerializer):
 
     def get_editor_url(self, obj):
         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:
             return None

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

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

+ 17 - 37
misago/threads/serializers/thread.py

@@ -16,10 +16,10 @@ __all__ = [
     'ThreadsListSerializer',
 ]
 
-
 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):
@@ -38,30 +38,10 @@ class ThreadSerializer(serializers.ModelSerializer, MutableFields):
     class Meta:
         model = Thread
         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):
@@ -122,10 +102,12 @@ class ThreadSerializer(serializers.ModelSerializer, MutableFields):
 
     def get_last_poster_url(self, obj):
         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:
             return None
 
@@ -135,9 +117,7 @@ class PrivateThreadSerializer(ThreadSerializer):
 
     class Meta:
         model = Thread
-        fields = ThreadSerializer.Meta.fields + (
-            'participants',
-        )
+        fields = ThreadSerializer.Meta.fields + ('participants', )
 
 
 class ThreadsListSerializer(ThreadSerializer):
@@ -148,7 +128,7 @@ class ThreadsListSerializer(ThreadSerializer):
 
     class Meta:
         model = Thread
-        fields = ThreadSerializer.Meta.fields + (
-            'has_poll', 'top_category'
-        )
+        fields = ThreadSerializer.Meta.fields + ('has_poll', 'top_category')
+
+
 ThreadsListSerializer = ThreadsListSerializer.exclude_fields('path', 'poll')

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

@@ -15,13 +15,7 @@ class ThreadParticipantSerializer(serializers.ModelSerializer):
 
     class Meta:
         model = ThreadParticipant
-        fields = (
-            'id',
-            'username',
-            'avatars',
-            'url',
-            'is_owner'
-        )
+        fields = ('id', 'username', 'avatars', 'url', 'is_owner')
 
     def get_id(self, obj):
         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"])
 move_post = Signal()
 move_thread = Signal()
-
-
 """
 Signal handlers
 """
+
+
 @receiver(merge_thread)
 def merge_threads_posts(sender, **kwargs):
     other_thread = kwargs['other_thread']
@@ -101,46 +101,32 @@ def delete_user_threads(sender, **kwargs):
 @receiver(username_changed)
 def update_usernames(sender, **kwargs):
     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(
-        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(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(
-        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(
-        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())

+ 1 - 3
misago/threads/subscriptions.py

@@ -21,9 +21,7 @@ def make_threads_subscription_aware(user, threads):
             thread.subscription = None
             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():
             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:
         return _("%(users)s like this.") % {'users': usernames_string}
 
-    formats = {
-        'users': usernames_string,
-        'likes': hidden_likes
-    }
+    formats = {'users': usernames_string, 'likes': hidden_likes}
 
     return ngettext(
         "%(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
 
 
 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

+ 20 - 18
misago/threads/tests/test_attachmentadmin_views.py

@@ -11,9 +11,7 @@ class AttachmentAdminViewsTests(AdminTestCase):
         super(AttachmentAdminViewsTests, self).setUp()
 
         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()
 
@@ -57,7 +55,9 @@ class AttachmentAdminViewsTests(AdminTestCase):
         self.assertEqual(response.status_code, 200)
 
         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, delete_link)
             self.assertContains(response, attachment.get_absolute_url())
@@ -78,10 +78,11 @@ class AttachmentAdminViewsTests(AdminTestCase):
         self.post.attachments_cache = [{'id': attachments[-1].pk}]
         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(Attachment.objects.count(), 0)
@@ -93,14 +94,18 @@ class AttachmentAdminViewsTests(AdminTestCase):
     def test_delete_view(self):
         """delete attachment view has no showstoppers"""
         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()
 
-        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)
         self.assertEqual(response.status_code, 302)
@@ -114,7 +119,4 @@ class AttachmentAdminViewsTests(AdminTestCase):
 
         # assert it was removed from post's 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):
         """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)
         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):
         """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:
-            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)
 
     def test_invalid_mime(self):
         """uploaded file's mimetype is rejected as invalid"""
         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:
-            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)
 
     def test_no_perm_to_type(self):
         """user needs permission to upload files of this type"""
         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())
         attachment_type.limit_uploads_to.set(Role.objects.exclude(id__in=user_roles))
 
         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)
 
     def test_type_is_locked(self):
@@ -110,9 +94,7 @@ class AttachmentsApiTestCase(AuthenticatedUserTestCase):
         )
 
         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)
 
     def test_type_is_disabled(self):
@@ -125,70 +107,50 @@ class AttachmentsApiTestCase(AuthenticatedUserTestCase):
         )
 
         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)
 
     def test_upload_too_big_for_type(self):
         """too big uploads are rejected"""
         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:
-            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):
         """too big uploads are rejected"""
-        self.override_acl({
-            'max_attachment_size': 100
-        })
+        self.override_acl({'max_attachment_size': 100})
 
         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:
-            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)
 
     def test_corrupted_image_upload(self):
         """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:
-            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)
 
     def test_document_upload(self):
         """successful upload creates orphan attachment"""
         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:
-            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)
 
         response_json = response.json()
@@ -219,15 +181,11 @@ class AttachmentsApiTestCase(AuthenticatedUserTestCase):
     def test_small_image_upload(self):
         """successful small image upload creates orphan attachment without thumbnail"""
         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:
-            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)
 
         response_json = response.json()
@@ -251,20 +209,14 @@ class AttachmentsApiTestCase(AuthenticatedUserTestCase):
 
     def test_large_image_upload(self):
         """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(
-            name="Test extension",
-            extensions='png',
-            mimetypes='image/png'
+            name="Test extension", extensions='png', mimetypes='image/png'
         )
 
         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)
 
         response_json = response.json()
@@ -307,15 +259,11 @@ class AttachmentsApiTestCase(AuthenticatedUserTestCase):
     def test_animated_image_upload(self):
         """successful gif upload creates orphan attachment with thumbnail"""
         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:
-            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)
 
         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()
 
     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):
         return Attachment.objects.create(
@@ -51,24 +49,17 @@ class AttachmentsMiddlewareTests(AuthenticatedUserTestCase):
         """use_this_middleware returns False if we can't upload attachments"""
         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.override_acl({
-            'max_attachment_size': 1024
-        })
+        self.override_acl({'max_attachment_size': 1024})
 
         self.assertTrue(middleware.use_this_middleware())
 
     def test_middleware_is_optional(self):
         """middleware is optional"""
-        INPUTS = (
-            {},
-            {'attachments': []}
-        )
+        INPUTS = ({}, {'attachments': []})
 
         for test_input in INPUTS:
             middleware = AttachmentsMiddleware(
@@ -83,11 +74,7 @@ class AttachmentsMiddlewareTests(AuthenticatedUserTestCase):
 
     def test_middleware_validates_ids(self):
         """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:
             middleware = AttachmentsMiddleware(
@@ -105,30 +92,26 @@ class AttachmentsMiddlewareTests(AuthenticatedUserTestCase):
     def test_get_initial_attachments(self):
         """get_initial_attachments returns list of attachments already existing on post"""
         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()
 
         attachments = serializer.get_initial_attachments(
-            middleware.mode, middleware.user, middleware.post)
+            middleware.mode, middleware.user, middleware.post
+        )
         self.assertEqual(attachments, [])
 
         attachment = self.mock_attachment(post=self.post)
         attachments = serializer.get_initial_attachments(
-            middleware.mode, middleware.user, middleware.post)
+            middleware.mode, middleware.user, middleware.post
+        )
         self.assertEqual(attachments, [attachment])
 
     def test_get_new_attachments(self):
         """get_initial_attachments returns list of attachments already existing on post"""
         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()
@@ -156,7 +139,9 @@ class AttachmentsMiddlewareTests(AuthenticatedUserTestCase):
         self.assertIsNone(attachment.uploader)
 
         serializer = AttachmentsMiddleware(
-            request=RequestMock({'attachments': []}),
+            request=RequestMock({
+                'attachments': []
+            }),
             mode=PostingEndpoint.EDIT,
             user=self.user,
             post=self.post
@@ -189,7 +174,8 @@ class AttachmentsMiddlewareTests(AuthenticatedUserTestCase):
         self.assertEqual(self.post.attachment_set.count(), 2)
 
         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):
         """middleware removes attachment from post and db"""
@@ -218,7 +204,8 @@ class AttachmentsMiddlewareTests(AuthenticatedUserTestCase):
         self.assertEqual(Attachment.objects.count(), 1)
 
         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):
         """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)
 
         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):

+ 82 - 61
misago/threads/tests/test_attachmenttypeadmin_views.py

@@ -37,12 +37,15 @@ class AttachmentTypeAdminViewsTests(AdminTestCase):
         response = self.client.post(form_link, data={})
         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)
 
         # clean alert about new item created
@@ -55,17 +58,22 @@ class AttachmentTypeAdminViewsTests(AdminTestCase):
 
     def test_edit_view(self):
         """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()
         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)
         self.assertEqual(response.status_code, 200)
@@ -73,15 +81,18 @@ class AttachmentTypeAdminViewsTests(AdminTestCase):
         response = self.client.post(form_link, data={})
         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)
 
         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())
 
         # 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(test_type.limit_uploads_to.count(), 0)
@@ -116,24 +130,21 @@ class AttachmentTypeAdminViewsTests(AdminTestCase):
 
     def test_clean_params_view(self):
         """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:
-            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)
 
             test_type = AttachmentType.objects.order_by('id').last()
@@ -141,17 +152,22 @@ class AttachmentTypeAdminViewsTests(AdminTestCase):
 
     def test_delete_view(self):
         """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()
         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)
         self.assertEqual(response.status_code, 302)
@@ -165,12 +181,15 @@ class AttachmentTypeAdminViewsTests(AdminTestCase):
 
     def test_cant_delete_type_with_attachments_view(self):
         """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()
         self.assertEqual(test_type.name, 'Test type')
@@ -185,7 +204,9 @@ class AttachmentTypeAdminViewsTests(AdminTestCase):
             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)
         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.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()
 
@@ -48,9 +42,7 @@ class AttachmentViewTestCase(AuthenticatedUserTestCase):
 
     def upload_document(self, is_orphaned=False, by_other_user=False):
         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)
 
         attachment = Attachment.objects.order_by('id').last()
@@ -68,9 +60,7 @@ class AttachmentViewTestCase(AuthenticatedUserTestCase):
 
     def upload_image(self):
         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)
 
         attachment = Attachment.objects.order_by('id').last()
@@ -94,10 +84,10 @@ class AttachmentViewTestCase(AuthenticatedUserTestCase):
 
     def test_nonexistant_file(self):
         """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)
 
@@ -105,10 +95,10 @@ class AttachmentViewTestCase(AuthenticatedUserTestCase):
         """user tries to retrieve existing file using invalid secret"""
         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)
 
@@ -135,10 +125,13 @@ class AttachmentViewTestCase(AuthenticatedUserTestCase):
         """user tries to retrieve thumbnail from non-image attachment"""
         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)
 
     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.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.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):
         new_acl = deepcopy(self.user.acl_cache)
@@ -60,17 +56,13 @@ class EmailNotificationTests(AuthenticatedUserTestCase):
         })
 
         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)
 
     def test_no_subscriptions(self):
         """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(len(mail.outbox), 0)
@@ -84,9 +76,7 @@ class EmailNotificationTests(AuthenticatedUserTestCase):
             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(len(mail.outbox), 0)
@@ -100,9 +90,7 @@ class EmailNotificationTests(AuthenticatedUserTestCase):
             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(len(mail.outbox), 0)
@@ -117,9 +105,7 @@ class EmailNotificationTests(AuthenticatedUserTestCase):
         )
         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(len(mail.outbox), 0)
@@ -136,9 +122,7 @@ class EmailNotificationTests(AuthenticatedUserTestCase):
 
         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(len(mail.outbox), 0)
@@ -153,9 +137,7 @@ class EmailNotificationTests(AuthenticatedUserTestCase):
         )
         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(len(mail.outbox), 1)
@@ -183,9 +165,7 @@ class EmailNotificationTests(AuthenticatedUserTestCase):
         )
         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(len(mail.outbox), 1)

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

@@ -20,8 +20,7 @@ class MockRequest(object):
 
 class EventsAPITests(TestCase):
     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()
 

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

@@ -17,9 +17,9 @@ class PostMentionsTests(AuthenticatedUserTestCase):
         self.thread = testutils.post_thread(category=self.category)
         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):
         new_acl = self.user.acl_cache
@@ -34,12 +34,10 @@ class PostMentionsTests(AuthenticatedUserTestCase):
 
     def test_flood_has_no_showstoppers(self):
         """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)
 
-        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):
         """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)
         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"""
         response = self.client.get(self.thread.first_post.get_absolute_url())
         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'])
         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())
         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'])
         self.assertContains(response, post.get_absolute_url())
@@ -65,7 +70,9 @@ class GotoPostTests(GotoViewTestCase):
 
         response = self.client.get(post.get_absolute_url())
         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'])
         self.assertContains(response, post.get_absolute_url())
@@ -87,7 +94,9 @@ class GotoPostTests(GotoViewTestCase):
 
         response = self.client.get(post.get_absolute_url())
         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'])
         self.assertContains(response, post.get_absolute_url())
@@ -98,7 +107,10 @@ class GotoLastTests(GotoViewTestCase):
         """first post redirect url is valid"""
         response = self.client.get(self.thread.get_last_post_url())
         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'])
         self.assertContains(response, self.thread.last_post.get_absolute_url())
@@ -121,7 +133,10 @@ class GotoNewTests(GotoViewTestCase):
         """first unread post redirect url is valid"""
         response = self.client.get(self.thread.get_new_post_url())
         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):
         """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())
         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):
         """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())
         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):
         """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())
         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):
@@ -197,7 +218,10 @@ class GotoUnapprovedTests(GotoViewTestCase):
 
         response = self.client.get(self.thread.get_unapproved_post_url())
         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):
         """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())
         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)]
 
         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):
         """paginator handles orphans"""
         items = [i + 1 for i in range(16)]
 
         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)
-        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)
-        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)
-        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)
-        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)
-        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)
         self.assertEqual(self.get_paginator_items_list(paginator), [items])
@@ -84,9 +98,10 @@ class PostsPaginatorTests(TestCase):
                         continue
 
                     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):
         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.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):
         new_acl = self.user.acl_cache
@@ -45,9 +45,7 @@ class PostMentionsTests(AuthenticatedUserTestCase):
 
     def test_mention_noone(self):
         """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)
 
         post = self.user.post_set.order_by('id').last()
@@ -55,9 +53,9 @@ class PostMentionsTests(AuthenticatedUserTestCase):
 
     def test_mention_nonexistant(self):
         """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)
 
         post = self.user.post_set.order_by('id').last()
@@ -65,9 +63,9 @@ class PostMentionsTests(AuthenticatedUserTestCase):
 
     def test_mention_self(self):
         """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)
 
         post = self.user.post_set.order_by('id').last()
@@ -80,16 +78,15 @@ class PostMentionsTests(AuthenticatedUserTestCase):
         users = []
 
         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]
-        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)
 
         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_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)
 
         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)
 
         # 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()
-        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(post.mentions.count(), 2)
@@ -129,9 +126,7 @@ class PostMentionsTests(AuthenticatedUserTestCase):
 
         # remove first mention from post - should preserve mentions
         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(post.mentions.count(), 2)
@@ -139,9 +134,7 @@ class PostMentionsTests(AuthenticatedUserTestCase):
 
         # remove mentions from post - should preserve mentions
         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(post.mentions.count(), 2)
@@ -152,9 +145,9 @@ class PostMentionsTests(AuthenticatedUserTestCase):
         user_a = UserModel.objects.create_user('Mention', 'mention@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)
 
         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.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)
 
         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
         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
         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
         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):
         """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.other_user = UserModel.objects.create_user(
-            'BobBoberson', 'bob@boberson.com', 'pass123')
+            'BobBoberson', 'bob@boberson.com', 'pass123'
+        )
 
     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):
@@ -33,31 +33,39 @@ class PrivateThreadAddParticipantApiTests(PrivateThreadPatchApiTestCase):
         """non-owner can't add participant"""
         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(
-            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):
         """path validates username"""
         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(
-            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):
         """can't user two times"""
         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)
 
@@ -65,21 +73,28 @@ class PrivateThreadAddParticipantApiTests(PrivateThreadPatchApiTestCase):
         """can't add user that is already participant"""
         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):
         """can't add user that is already participant"""
         ThreadParticipant.objects.set_owner(self.thread, 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)
 
@@ -87,13 +102,15 @@ class PrivateThreadAddParticipantApiTests(PrivateThreadPatchApiTestCase):
         """can't add user that has no permission to use private threads"""
         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)
 
@@ -103,15 +120,21 @@ class PrivateThreadAddParticipantApiTests(PrivateThreadPatchApiTestCase):
 
         for i in range(self.user.acl_cache['max_private_thread_participants']):
             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])
 
-        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, "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):
         """adding user to closed thread fails for non-moderator"""
@@ -120,20 +143,29 @@ class PrivateThreadAddParticipantApiTests(PrivateThreadPatchApiTestCase):
         self.thread.is_closed = True
         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(
-            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):
         """adding user to thread add user to thread as participant, sets event and emails him"""
         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 = self.thread.post_set.order_by('id').last()
@@ -154,13 +186,15 @@ class PrivateThreadAddParticipantApiTests(PrivateThreadPatchApiTestCase):
         self.thread.has_reported_posts = True
         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 = self.thread.post_set.order_by('id').last()
@@ -177,13 +211,15 @@ class PrivateThreadAddParticipantApiTests(PrivateThreadPatchApiTestCase):
         self.thread.is_closed = True
         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 = self.thread.post_set.order_by('id').last()
@@ -203,9 +239,13 @@ class PrivateThreadRemoveParticipantApiTests(PrivateThreadPatchApiTestCase):
         """api handles empty user id"""
         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)
 
@@ -213,9 +253,13 @@ class PrivateThreadRemoveParticipantApiTests(PrivateThreadPatchApiTestCase):
         """api validates user id type"""
         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)
 
@@ -223,9 +267,13 @@ class PrivateThreadRemoveParticipantApiTests(PrivateThreadPatchApiTestCase):
         """removed user has to be participant"""
         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)
 
@@ -234,12 +282,17 @@ class PrivateThreadRemoveParticipantApiTests(PrivateThreadPatchApiTestCase):
         ThreadParticipant.objects.set_owner(self.thread, self.other_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(
-            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):
         """api disallows owner to remove other user from closed thread"""
@@ -249,12 +302,17 @@ class PrivateThreadRemoveParticipantApiTests(PrivateThreadPatchApiTestCase):
         self.thread.is_closed = True
         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(
-            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):
         """api allows user to remove himself from thread"""
@@ -266,9 +324,13 @@ class PrivateThreadRemoveParticipantApiTests(PrivateThreadPatchApiTestCase):
             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.assertFalse(response.json()['deleted'])
@@ -300,9 +362,13 @@ class PrivateThreadRemoveParticipantApiTests(PrivateThreadPatchApiTestCase):
         self.thread.is_closed = True
         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.assertFalse(response.json()['deleted'])
@@ -325,19 +391,20 @@ class PrivateThreadRemoveParticipantApiTests(PrivateThreadPatchApiTestCase):
 
     def test_moderator_remove_user(self):
         """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.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.assertFalse(response.json()['deleted'])
@@ -364,9 +431,13 @@ class PrivateThreadRemoveParticipantApiTests(PrivateThreadPatchApiTestCase):
         ThreadParticipant.objects.set_owner(self.thread, self.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.assertFalse(response.json()['deleted'])
@@ -392,9 +463,13 @@ class PrivateThreadRemoveParticipantApiTests(PrivateThreadPatchApiTestCase):
         ThreadParticipant.objects.set_owner(self.thread, self.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.assertFalse(response.json()['deleted'])
@@ -419,9 +494,13 @@ class PrivateThreadRemoveParticipantApiTests(PrivateThreadPatchApiTestCase):
         """api allows last user leave thread, causing thread to delete"""
         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.assertTrue(response.json()['deleted'])
@@ -439,9 +518,7 @@ class PrivateThreadTakeOverApiTests(PrivateThreadPatchApiTestCase):
         """api handles empty user id"""
         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)
 
@@ -449,9 +526,13 @@ class PrivateThreadTakeOverApiTests(PrivateThreadPatchApiTestCase):
         """api handles invalid user id"""
         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)
 
@@ -459,9 +540,13 @@ class PrivateThreadTakeOverApiTests(PrivateThreadPatchApiTestCase):
         """api handles nonexistant user id"""
         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)
 
@@ -470,21 +555,30 @@ class PrivateThreadTakeOverApiTests(PrivateThreadPatchApiTestCase):
         ThreadParticipant.objects.set_owner(self.thread, self.other_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(
-            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):
         """api validates that new owner id is same as current owner"""
         ThreadParticipant.objects.set_owner(self.thread, self.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)
 
@@ -496,21 +590,30 @@ class PrivateThreadTakeOverApiTests(PrivateThreadPatchApiTestCase):
         self.thread.is_closed = True
         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(
-            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):
         """owner can pass thread ownership to other participant"""
         ThreadParticipant.objects.set_owner(self.thread, self.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)
 
@@ -530,19 +633,20 @@ class PrivateThreadTakeOverApiTests(PrivateThreadPatchApiTestCase):
 
     def test_moderator_change_owner(self):
         """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.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)
 
@@ -567,13 +671,15 @@ class PrivateThreadTakeOverApiTests(PrivateThreadPatchApiTestCase):
         ThreadParticipant.objects.set_owner(self.thread, self.other_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)
 
@@ -599,13 +705,15 @@ class PrivateThreadTakeOverApiTests(PrivateThreadPatchApiTestCase):
         self.thread.is_closed = True
         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)
 

+ 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.other_user = UserModel.objects.create_user(
-            'BobBoberson', 'bob@boberson.com', 'pass123')
+            'BobBoberson', 'bob@boberson.com', 'pass123'
+        )
 
     def test_reply_private_thread(self):
         """api sets other private thread participants sync thread flag"""
         ThreadParticipant.objects.set_owner(self.thread, self.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)
 
         # 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.other_user = UserModel.objects.create_user(
-            'BobBoberson', 'bob@boberson.com', 'pass123')
+            'BobBoberson', 'bob@boberson.com', 'pass123'
+        )
 
     def test_cant_start_thread_as_guest(self):
         """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={})
 
         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):
         """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.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):
         """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.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):
         """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.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):
         """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.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):
         """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.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):
         """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.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):
         """api validates that you cant invite blocking user to thread"""
         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.json(), {
-            'to': [
-                "BobBoberson is blocking you."
-            ]
-        })
+        self.assertEqual(response.json(), {'to': ["BobBoberson is blocking you."]})
 
         # allow us to bypass blocked check
         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.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):
         """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.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.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
         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.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
         override_acl(self.user, {'can_add_everyone_to_private_threads': 0})
         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.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):
         """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.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.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
         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.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):
         """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)
 
         thread = self.user.thread_set.all()[:1][0]
@@ -320,18 +332,10 @@ class StartPrivateThreadTests(AuthenticatedUserTestCase):
         self.assertEqual(thread.participants.count(), 2)
 
         # 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
-        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
         user_to_sync = UserModel.objects.get(sync_unread_private_threads=True)
@@ -352,9 +356,12 @@ class StartPrivateThreadTests(AuthenticatedUserTestCase):
 
     def test_post_unicode(self):
         """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)

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

@@ -21,9 +21,7 @@ class PrivateThreadViewTests(PrivateThreadsTestCase):
 
     def test_no_permission(self):
         """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)
         self.assertContains(response, "t use private threads", status_code=403)
@@ -35,9 +33,7 @@ class PrivateThreadViewTests(PrivateThreadsTestCase):
 
     def test_mod_not_reported(self):
         """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)
         self.assertEqual(response.status_code, 404)
@@ -66,9 +62,7 @@ class PrivateThreadViewTests(PrivateThreadsTestCase):
 
     def test_mod_can_see_reported(self):
         """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.save()

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

@@ -8,10 +8,7 @@ class PrivateThreadsTestCase(AuthenticatedUserTestCase):
         super(PrivateThreadsTestCase, self).setUp()
         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()
 
     def override_acl(self, acl=None):
@@ -32,8 +29,4 @@ class PrivateThreadsTestCase(AuthenticatedUserTestCase):
         if 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):
         """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)
         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)
 
         # 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)
         self.assertEqual(response.status_code, 200)
@@ -87,9 +83,7 @@ class PrivateThreadRetrieveApiTests(PrivateThreadsTestCase):
 
     def test_no_permission(self):
         """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)
         self.assertContains(response, "t use private threads", status_code=403)
@@ -101,9 +95,7 @@ class PrivateThreadRetrieveApiTests(PrivateThreadsTestCase):
 
     def test_mod_not_reported(self):
         """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)
         self.assertEqual(response.status_code, 404)
@@ -125,15 +117,15 @@ class PrivateThreadRetrieveApiTests(PrivateThreadsTestCase):
 
         response_json = response.json()
         self.assertEqual(response_json['title'], self.thread.title)
-        self.assertEqual(response_json['participants'], [
-            {
+        self.assertEqual(
+            response_json['participants'], [{
                 'id': self.user.id,
                 'username': self.user.username,
                 'avatars': self.user.avatars,
                 'url': self.user.get_absolute_url(),
                 'is_owner': True
-            }
-        ])
+            }]
+        )
 
     def test_can_see_participant(self):
         """user can see thread he is participant of"""
@@ -144,21 +136,19 @@ class PrivateThreadRetrieveApiTests(PrivateThreadsTestCase):
 
         response_json = response.json()
         self.assertEqual(response_json['title'], self.thread.title)
-        self.assertEqual(response_json['participants'], [
-            {
+        self.assertEqual(
+            response_json['participants'], [{
                 'id': self.user.id,
                 'username': self.user.username,
                 'avatars': self.user.avatars,
                 'url': self.user.get_absolute_url(),
                 'is_owner': False
-            }
-        ])
+            }]
+        )
 
     def test_mod_can_see_reported(self):
         """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.save()
@@ -178,9 +168,7 @@ class PrivateThreadsReadApiTests(PrivateThreadsTestCase):
 
     def test_read_threads_no_permission(self):
         """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)
         self.assertEqual(response.status_code, 403)
@@ -212,29 +200,24 @@ class PrivateThreadDeleteApiTests(PrivateThreadsTestCase):
 
     def test_delete_thread_no_permission(self):
         """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)
         self.assertEqual(response.status_code, 403)
 
-        self.override_acl({
-            'can_hide_threads': 0
-        })
+        self.override_acl({'can_hide_threads': 0})
 
         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)
         self.assertEqual(response.status_code, 403)
 
     def test_delete_thread(self):
         """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)
         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):
         """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)
         self.assertContains(response, "use private threads", status_code=403)
@@ -53,9 +51,7 @@ class PrivateThreadsListTests(PrivateThreadsTestCase):
         self.assertContains(response, visible.get_absolute_url())
 
         # 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)
         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):
         """hidden posts are extempt from search"""
         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)
 
         response = self.client.get('%s?q=ipsum' % self.api_link)
@@ -99,8 +98,7 @@ class SearchApiTests(AuthenticatedUserTestCase):
     def test_unapproved_post(self):
         """unapproves posts are extempt from search"""
         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)
 
         response = self.client.get('%s?q=ipsum' % self.api_link)
@@ -174,6 +172,4 @@ class SearchProviderApiTests(SearchApiTests):
     def setUp(self):
         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.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)
 
         # user has no subscriptions
@@ -58,11 +61,14 @@ class SubscribeStartedThreadTests(SubscriptionMiddlewareTestCase):
         self.user.subscribe_to_started_threads = UserModel.SUBSCRIBE_NOTIFY
         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)
 
         # user has subscribed to thread
@@ -77,11 +83,14 @@ class SubscribeStartedThreadTests(SubscriptionMiddlewareTestCase):
         self.user.subscribe_to_started_threads = UserModel.SUBSCRIBE_ALL
         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)
 
         # user has subscribed to thread
@@ -96,9 +105,7 @@ class SubscribeRepliedThreadTests(SubscriptionMiddlewareTestCase):
     def setUp(self):
         super(SubscribeRepliedThreadTests, self).setUp()
         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):
         """middleware makes no subscription to thread"""
@@ -106,9 +113,7 @@ class SubscribeRepliedThreadTests(SubscriptionMiddlewareTestCase):
         self.user.subscribe_to_replied_threads = UserModel.SUBSCRIBE_NONE
         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)
 
         # user has no subscriptions
@@ -119,9 +124,7 @@ class SubscribeRepliedThreadTests(SubscriptionMiddlewareTestCase):
         self.user.subscribe_to_replied_threads = UserModel.SUBSCRIBE_NOTIFY
         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)
 
         # user has subscribed to thread
@@ -135,9 +138,7 @@ class SubscribeRepliedThreadTests(SubscriptionMiddlewareTestCase):
         self.user.subscribe_to_replied_threads = UserModel.SUBSCRIBE_ALL
         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)
 
         # user has subscribed to thread
@@ -151,17 +152,13 @@ class SubscribeRepliedThreadTests(SubscriptionMiddlewareTestCase):
         self.user.subscribe_to_replied_threads = UserModel.SUBSCRIBE_ALL
         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)
 
         # clear subscription
         self.user.subscription_set.all().delete()
         # 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)
 
         # 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.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()
 
     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):
         """make single thread sub aware for anon"""
@@ -37,8 +33,7 @@ class SubscriptionsTests(TestCase):
         """make multiple threads list sub aware for anon"""
         threads = []
         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)
 
@@ -55,7 +50,6 @@ class SubscriptionsTests(TestCase):
         self.user.subscription_set.create(
             thread=self.thread,
             category=self.category,
-
             last_read_on=timezone.now(),
             send_email=True,
         )
@@ -67,14 +61,12 @@ class SubscriptionsTests(TestCase):
         """make mulitple threads sub aware for authenticated"""
         threads = []
         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:
                 self.user.subscription_set.create(
                     thread=threads[-1],
                     category=self.category,
-
                     last_read_on=timezone.now(),
                     send_email=False,
                 )
@@ -82,7 +74,6 @@ class SubscriptionsTests(TestCase):
                 self.user.subscription_set.create(
                     thread=threads[-1],
                     category=self.category,
-
                     last_read_on=timezone.now(),
                     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()
 
         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)
 

+ 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.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):
         new_acl = self.user.acl_cache
@@ -65,70 +66,62 @@ class EditReplyTests(AuthenticatedUserTestCase):
 
     def test_cant_edit_reply(self):
         """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)
         self.assertContains(response, "You can't edit posts in this category.", status_code=403)
 
     def test_cant_edit_other_user_reply(self):
         """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.save()
 
         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):
         """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.save()
 
         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
-        self.override_acl({
-            'can_close_threads': 1
-        })
+        self.override_acl({'can_close_threads': 1})
 
         response = self.put(self.api_link)
         self.assertEqual(response.status_code, 400)
 
     def test_closed_thread(self):
         """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.save()
 
         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
-        self.override_acl({
-            'can_close_threads': 1
-        })
+        self.override_acl({'can_close_threads': 1})
 
         response = self.put(self.api_link)
         self.assertEqual(response.status_code, 400)
 
     def test_protected_post(self):
         """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.save()
@@ -137,9 +130,7 @@ class EditReplyTests(AuthenticatedUserTestCase):
         self.assertContains(response, "This post is protected. You can't edit it.", status_code=403)
 
         # 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)
         self.assertEqual(response.status_code, 400)
@@ -151,11 +142,7 @@ class EditReplyTests(AuthenticatedUserTestCase):
         response = self.put(self.api_link, data={})
 
         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):
         """events can't be edited"""
@@ -172,25 +159,24 @@ class EditReplyTests(AuthenticatedUserTestCase):
         """post is validated"""
         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.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):
         """endpoint isn't bumping edits count if no change was made to post's body"""
         self.override_acl()
         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.override_acl()
@@ -211,9 +197,7 @@ class EditReplyTests(AuthenticatedUserTestCase):
         self.override_acl()
         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.override_acl()
@@ -239,36 +223,27 @@ class EditReplyTests(AuthenticatedUserTestCase):
 
     def test_edit_first_post_hidden(self):
         """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.save()
         self.thread.first_post.is_hidden = True
         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)
 
     def test_protect_post(self):
         """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)
 
         post = self.user.post_set.order_by('id').last()
@@ -276,14 +251,9 @@ class EditReplyTests(AuthenticatedUserTestCase):
 
     def test_protect_post_no_permission(self):
         """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)
 
         post = self.user.post_set.order_by('id').last()
@@ -293,7 +263,5 @@ class EditReplyTests(AuthenticatedUserTestCase):
         """unicode characters can be posted"""
         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)

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

@@ -15,7 +15,9 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
         Category(
             name='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.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']:
             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):
         """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)
-        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):
         """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)
         self.assertContains(response, "This is not a valid thread link.", status_code=400)
 
     def test_invalid_url(self):
         """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)
 
     def test_current_thread_url(self):
         """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)
 
     def test_other_thread_exists(self):
         """api validates if other thread exists"""
-        self.override_acl({
-            'can_merge_threads': 1
-        })
+        self.override_acl({'can_merge_threads': 1})
 
         self.override_other_acl()
 
@@ -102,77 +96,59 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
         other_thread_url = other_thread.get_absolute_url()
         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):
         """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)
 
-        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):
         """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)
 
-        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):
         """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)
 
-        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):
         """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)
 
-        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)
 
         # other thread has two posts now
@@ -184,20 +160,14 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
 
     def test_merge_threads_kept_poll(self):
         """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)
         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)
 
         # other thread has two posts now
@@ -213,20 +183,14 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
 
     def test_merge_threads_moved_poll(self):
         """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)
         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)
 
         # other thread has two posts now
@@ -242,29 +206,23 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
 
     def test_threads_merge_conflict(self):
         """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)
         poll = testutils.post_poll(self.thread, self.user)
         other_poll = testutils.post_poll(other_thread, self.user)
 
-        response = self.client.post(self.api_link, {
-            'thread_url': other_thread.get_absolute_url()
-        })
+        response = self.client.post(self.api_link, {'thread_url': other_thread.get_absolute_url()})
         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
         self.assertEqual(Poll.objects.count(), 2)
@@ -272,27 +230,21 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
 
     def test_threads_merge_conflict_invalid_resolution(self):
         """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)
 
         testutils.post_poll(self.thread, self.user)
         testutils.post_poll(other_thread, self.user)
 
-        response = self.client.post(self.api_link, {
-            'thread_url': other_thread.get_absolute_url(),
-            'poll': 'jhdkajshdsak'
-        })
+        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.json(), {
-            'detail': "Invalid choice."
-        })
+        self.assertEqual(response.json(), {'detail': "Invalid choice."})
 
         # polls and votes were untouched
         self.assertEqual(Poll.objects.count(), 2)
@@ -300,22 +252,18 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
 
     def test_threads_merge_conflict_delete_all(self):
         """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)
         testutils.post_poll(self.thread, self.user)
         testutils.post_poll(other_thread, self.user)
 
-        response = self.client.post(self.api_link, {
-            'thread_url': other_thread.get_absolute_url(),
-            'poll': 0
-        })
+        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)
 
         # other thread has two posts now
@@ -331,22 +279,18 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
 
     def test_threads_merge_conflict_keep_first_poll(self):
         """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)
         poll = testutils.post_poll(self.thread, self.user)
         other_poll = testutils.post_poll(other_thread, self.user)
 
-        response = self.client.post(self.api_link, {
-            'thread_url': other_thread.get_absolute_url(),
-            'poll': 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)
 
         # other thread has two posts now
@@ -369,22 +313,18 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
 
     def test_threads_merge_conflict_keep_other_poll(self):
         """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)
         poll = testutils.post_poll(self.thread, self.user)
         other_poll = testutils.post_poll(other_thread, self.user)
 
-        response = self.client.post(self.api_link, {
-            'thread_url': other_thread.get_absolute_url(),
-            'poll': 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)
 
         # 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):
         """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)
 
@@ -163,7 +162,7 @@ class ThreadModelTests(TestCase):
         self.assertFalse(self.thread.has_hidden_posts)
         self.assertEqual(self.thread.replies, 3)
 
-         # add event post
+        # add event post
         event = Post.objects.create(
             category=self.category,
             thread=self.thread,
@@ -234,8 +233,7 @@ class ThreadModelTests(TestCase):
 
     def test_set_first_post(self):
         """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)
 
@@ -261,8 +259,7 @@ class ThreadModelTests(TestCase):
 
     def test_set_last_post(self):
         """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)
 
@@ -293,7 +290,9 @@ class ThreadModelTests(TestCase):
         Category(
             name='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')
 
         self.thread.move(new_category)
@@ -352,10 +351,8 @@ class ThreadModelTests(TestCase):
         private thread gets deleted automatically
         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])
         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):
     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):
     def test_add_acl_true(self):
         """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)
 
         response_json = response.json()
@@ -28,9 +25,7 @@ class ThreadAddAclApiTests(ThreadPatchApiTestCase):
 
     def test_add_acl_false(self):
         """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)
 
         response_json = response.json()
@@ -40,13 +35,15 @@ class ThreadAddAclApiTests(ThreadPatchApiTestCase):
 class ThreadChangeTitleApiTests(ThreadPatchApiTestCase):
     def test_change_thread_title(self):
         """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)
 
         thread_json = self.get_thread_json()
@@ -54,65 +51,62 @@ class ThreadChangeTitleApiTests(ThreadPatchApiTestCase):
 
     def test_change_thread_title_no_permission(self):
         """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)
 
         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):
         """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.started_on = timezone.now() - timedelta(minutes=10)
         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)
 
         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):
         """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)
 
         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):
     def test_pin_thread(self):
         """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)
 
         thread_json = self.get_thread_json()
@@ -126,13 +120,9 @@ class ThreadPinGloballyApiTests(ThreadPatchApiTestCase):
         thread_json = self.get_thread_json()
         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)
 
         thread_json = self.get_thread_json()
@@ -140,18 +130,15 @@ class ThreadPinGloballyApiTests(ThreadPatchApiTestCase):
 
     def test_pin_thread_no_permission(self):
         """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)
 
         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()
         self.assertEqual(thread_json['weight'], 0)
@@ -164,18 +151,15 @@ class ThreadPinGloballyApiTests(ThreadPatchApiTestCase):
         thread_json = self.get_thread_json()
         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)
 
         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()
         self.assertEqual(thread_json['weight'], 2)
@@ -184,13 +168,9 @@ class ThreadPinGloballyApiTests(ThreadPatchApiTestCase):
 class ThreadPinLocallyApiTests(ThreadPatchApiTestCase):
     def test_pin_thread(self):
         """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)
 
         thread_json = self.get_thread_json()
@@ -204,13 +184,9 @@ class ThreadPinLocallyApiTests(ThreadPatchApiTestCase):
         thread_json = self.get_thread_json()
         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)
 
         thread_json = self.get_thread_json()
@@ -218,18 +194,15 @@ class ThreadPinLocallyApiTests(ThreadPatchApiTestCase):
 
     def test_pin_thread_no_permission(self):
         """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)
 
         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()
         self.assertEqual(thread_json['weight'], 0)
@@ -242,18 +215,15 @@ class ThreadPinLocallyApiTests(ThreadPatchApiTestCase):
         thread_json = self.get_thread_json()
         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)
 
         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()
         self.assertEqual(thread_json['weight'], 1)
@@ -266,7 +236,9 @@ class ThreadMoveApiTests(ThreadPatchApiTestCase):
         Category(
             name='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')
 
     def override_other_acl(self, acl):
@@ -288,25 +260,37 @@ class ThreadMoveApiTests(ThreadPatchApiTestCase):
         if other_category_acl['can_see']:
             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):
         """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.override_other_acl({})
@@ -320,22 +304,28 @@ class ThreadMoveApiTests(ThreadPatchApiTestCase):
 
     def test_move_thread_with_top(self):
         """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.override_other_acl({})
@@ -349,19 +339,22 @@ class ThreadMoveApiTests(ThreadPatchApiTestCase):
 
     def test_move_thread_no_permission(self):
         """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({})
 
-        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)
 
         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({})
 
@@ -370,16 +363,16 @@ class ThreadMoveApiTests(ThreadPatchApiTestCase):
 
     def test_move_thread_no_category_access(self):
         """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)
 
         response_json = response.json()
@@ -392,21 +385,23 @@ class ThreadMoveApiTests(ThreadPatchApiTestCase):
 
     def test_move_thread_no_category_browse(self):
         """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)
 
         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({})
 
@@ -415,21 +410,22 @@ class ThreadMoveApiTests(ThreadPatchApiTestCase):
 
     def test_move_thread_same_category(self):
         """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)
 
         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({})
 
@@ -438,9 +434,13 @@ class ThreadMoveApiTests(ThreadPatchApiTestCase):
 
     def test_thread_flatten_categories(self):
         """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)
 
         response_json = response.json()
@@ -453,14 +453,20 @@ class ThreadMoveApiTests(ThreadPatchApiTestCase):
 
         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)
 
         response_json = response.json()
@@ -471,13 +477,15 @@ class ThreadMoveApiTests(ThreadPatchApiTestCase):
 class ThreadCloseApiTests(ThreadPatchApiTestCase):
     def test_close_thread(self):
         """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)
 
         thread_json = self.get_thread_json()
@@ -491,13 +499,15 @@ class ThreadCloseApiTests(ThreadPatchApiTestCase):
         thread_json = self.get_thread_json()
         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)
 
         thread_json = self.get_thread_json()
@@ -505,18 +515,21 @@ class ThreadCloseApiTests(ThreadPatchApiTestCase):
 
     def test_close_thread_no_permission(self):
         """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)
 
         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()
         self.assertFalse(thread_json['is_closed'])
@@ -529,18 +542,21 @@ class ThreadCloseApiTests(ThreadPatchApiTestCase):
         thread_json = self.get_thread_json()
         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)
 
         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()
         self.assertTrue(thread_json['is_closed'])
@@ -552,13 +568,15 @@ class ThreadApproveApiTests(ThreadPatchApiTestCase):
         self.thread.is_unapproved = True
         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)
 
         thread_json = self.get_thread_json()
@@ -566,35 +584,36 @@ class ThreadApproveApiTests(ThreadPatchApiTestCase):
 
     def test_unapprove_thread(self):
         """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)
 
         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):
     def test_hide_thread(self):
         """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.override_acl({
-            'can_hide_threads': 1
-        })
+        self.override_acl({'can_hide_threads': 1})
 
         thread_json = self.get_thread_json()
         self.assertTrue(thread_json['is_hidden'])
@@ -604,43 +623,44 @@ class ThreadHideApiTests(ThreadPatchApiTestCase):
         self.thread.is_hidden = True
         self.thread.save()
 
-        self.override_acl({
-            'can_hide_threads': 1
-        })
+        self.override_acl({'can_hide_threads': 1})
 
         thread_json = self.get_thread_json()
         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.override_acl({
-            'can_hide_threads': 1
-        })
+        self.override_acl({'can_hide_threads': 1})
 
         thread_json = self.get_thread_json()
         self.assertFalse(thread_json['is_hidden'])
 
     def test_hide_thread_no_permission(self):
         """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)
 
         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()
         self.assertFalse(thread_json['is_hidden'])
@@ -650,29 +670,33 @@ class ThreadHideApiTests(ThreadPatchApiTestCase):
         self.thread.is_hidden = True
         self.thread.save()
 
-        self.override_acl({
-            'can_hide_threads': 1
-        })
+        self.override_acl({'can_hide_threads': 1})
 
         thread_json = self.get_thread_json()
         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)
 
 
 class ThreadSubscribeApiTests(ThreadPatchApiTestCase):
     def test_subscribe_thread(self):
         """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)
 
@@ -684,9 +708,13 @@ class ThreadSubscribeApiTests(ThreadPatchApiTestCase):
 
     def test_subscribe_thread_with_email(self):
         """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)
 
@@ -698,9 +726,13 @@ class ThreadSubscribeApiTests(ThreadPatchApiTestCase):
 
     def test_unsubscribe_thread(self):
         """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)
 
@@ -713,19 +745,28 @@ class ThreadSubscribeApiTests(ThreadPatchApiTestCase):
         """api makes it impossible to subscribe thread"""
         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)
 
     def test_subscribe_nonexistant_thread(self):
         """api makes it impossible to subscribe nonexistant thread"""
         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)

+ 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.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):
         return self.client.post(url, json.dumps(data or {}), content_type='application/json')
@@ -52,7 +50,8 @@ class ThreadPollApiTestCase(AuthenticatedUserTestCase):
     def mock_poll(self):
         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):
         """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)
         self.assertEqual(response.status_code, 404)
 
     def test_nonexistant_thread_id(self):
         """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)
         self.assertEqual(response.status_code, 404)
 
     def test_no_permission(self):
         """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)
         self.assertContains(response, "can't start polls", status_code=403)
 
     def test_no_permission_closed_thread(self):
         """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.save()
@@ -53,18 +45,14 @@ class ThreadPollCreateTests(ThreadPollApiTestCase):
         response = self.post(self.api_link)
         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)
         self.assertEqual(response.status_code, 400)
 
     def test_no_permission_closed_category(self):
         """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.save()
@@ -72,18 +60,14 @@ class ThreadPollCreateTests(ThreadPollApiTestCase):
         response = self.post(self.api_link)
         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)
         self.assertEqual(response.status_code, 400)
 
     def test_no_permission_other_user_thread(self):
         """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.save()
@@ -91,9 +75,7 @@ class ThreadPollCreateTests(ThreadPollApiTestCase):
         response = self.post(self.api_link)
         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)
         self.assertEqual(response.status_code, 400)
@@ -108,7 +90,9 @@ class ThreadPollCreateTests(ThreadPollApiTestCase):
             poster_ip='127.0.0.1',
             length=30,
             question='Test',
-            choices=[{'hash': 't3st'}],
+            choices=[{
+                'hash': 't3st'
+            }],
             allowed_choices=1
         )
 
@@ -125,156 +109,133 @@ class ThreadPollCreateTests(ThreadPollApiTestCase):
 
     def test_length_validation(self):
         """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)
 
         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)
 
         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):
         """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)
 
         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):
         """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)
 
         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)
 
         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):
         """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)
 
         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):
         """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)
 
         error_formats = (MAX_POLL_OPTIONS, MAX_POLL_OPTIONS + 1)
 
         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):
         """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)
 
         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'
-                }
-            ]
-        })
+                }]
+            }
+        )
         self.assertEqual(response.status_code, 400)
 
         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):
         """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': 'Green'
-                },
-                {
+                }, {
                     'label': 'Blue'
-                }
-            ]
-        })
+                }]
+            }
+        )
         self.assertEqual(response.status_code, 200)
 
         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):
         """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)
         self.assertEqual(response.status_code, 404)
 
     def test_nonexistant_thread_id(self):
         """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)
         self.assertEqual(response.status_code, 404)
 
     def test_invalid_poll_id(self):
         """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)
         self.assertEqual(response.status_code, 404)
 
     def test_nonexistant_poll_id(self):
         """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)
         self.assertEqual(response.status_code, 404)
 
     def test_no_permission(self):
         """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)
         self.assertContains(response, "can't delete polls", status_code=403)
 
     def test_no_permission_timeout(self):
         """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.save()
 
         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):
         """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.length = 5
@@ -98,9 +97,7 @@ class ThreadPollDeleteTests(ThreadPollApiTestCase):
 
     def test_no_permission_other_user_poll(self):
         """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.save()
@@ -110,9 +107,7 @@ class ThreadPollDeleteTests(ThreadPollApiTestCase):
 
     def test_no_permission_closed_thread(self):
         """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.save()
@@ -120,18 +115,14 @@ class ThreadPollDeleteTests(ThreadPollApiTestCase):
         response = self.client.delete(self.api_link)
         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)
         self.assertEqual(response.status_code, 200)
 
     def test_no_permission_closed_category(self):
         """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.save()
@@ -139,9 +130,7 @@ class ThreadPollDeleteTests(ThreadPollApiTestCase):
         response = self.client.delete(self.api_link)
         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)
         self.assertEqual(response.status_code, 200)
@@ -162,10 +151,7 @@ class ThreadPollDeleteTests(ThreadPollApiTestCase):
 
     def test_other_user_poll_delete(self):
         """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.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):
         """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)
         self.assertEqual(response.status_code, 404)
 
     def test_nonexistant_thread_id(self):
         """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)
         self.assertEqual(response.status_code, 404)
 
     def test_invalid_poll_id(self):
         """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)
         self.assertEqual(response.status_code, 404)
 
     def test_nonexistant_poll_id(self):
         """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)
         self.assertEqual(response.status_code, 404)
 
     def test_no_permission(self):
         """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)
         self.assertContains(response, "can't edit polls", status_code=403)
 
     def test_no_permission_timeout(self):
         """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.save()
 
         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):
         """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.length = 5
@@ -98,9 +97,7 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
 
     def test_no_permission_other_user_poll(self):
         """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.save()
@@ -110,9 +107,7 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
 
     def test_no_permission_closed_thread(self):
         """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.save()
@@ -120,18 +115,14 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
         response = self.put(self.api_link)
         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)
         self.assertEqual(response.status_code, 400)
 
     def test_no_permission_closed_category(self):
         """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.save()
@@ -139,9 +130,7 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
         response = self.put(self.api_link)
         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)
         self.assertEqual(response.status_code, 400)
@@ -156,156 +145,133 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
 
     def test_length_validation(self):
         """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)
 
         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)
 
         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):
         """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)
 
         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):
         """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)
 
         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)
 
         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):
         """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)
 
         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):
         """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)
 
         error_formats = (MAX_POLL_OPTIONS, MAX_POLL_OPTIONS + 1)
 
         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):
         """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)
 
         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'
-                }
-            ]
-        })
+                }]
+            }
+        )
         self.assertEqual(response.status_code, 400)
 
         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):
         """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': 'Green'
-                },
-                {
+                }, {
                     'label': 'Blue'
-                }
-            ]
-        })
+                }]
+            }
+        )
         self.assertEqual(response.status_code, 200)
 
         response_json = response.json()
@@ -332,35 +298,38 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
 
     def test_poll_current_choices_edited(self):
         """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',
                     'label': '\nFirst  ',
                     'votes': 5555
-                },
-                {
+                }, {
                     'hash': 'bbbbbbbbbbbb',
                     'label': 'Second',
                     'votes': 5555
-                },
-                {
+                }, {
                     'hash': 'gggggggggggg',
                     'label': 'Third',
                     'votes': 5555
-                },
-                {
+                }, {
                     'hash': 'dddddddddddd',
                     'label': 'Fourth',
                     'votes': 5555
-                }
-            ]
-        })
+                }]
+            }
+        )
         self.assertEqual(response.status_code, 200)
 
         response_json = response.json()
@@ -376,32 +345,29 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
 
         # choices were updated
         self.assertEqual(len(response_json['choices']), 4)
-        self.assertEqual(response_json['choices'], [
-            {
+        self.assertEqual(
+            response_json['choices'], [{
                 'hash': 'aaaaaaaaaaaa',
                 'label': 'First',
                 'votes': 1,
                 'selected': False
-            },
-            {
+            }, {
                 'hash': 'bbbbbbbbbbbb',
                 'label': 'Second',
                 'votes': 0,
                 'selected': False
-            },
-            {
+            }, {
                 'hash': 'gggggggggggg',
                 'label': 'Third',
                 'votes': 2,
                 'selected': True
-            },
-            {
+            }, {
                 'hash': 'dddddddddddd',
                 'label': 'Fourth',
                 'votes': 1,
                 'selected': True
-            }
-        ])
+            }]
+        )
 
         # no votes were removed
         self.assertEqual(response_json['votes'], 4)
@@ -409,30 +375,34 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
 
     def test_poll_some_choices_edited(self):
         """api edits some poll choices"""
-        response = self.put(self.api_link, data={
-            'length': 40,
-            'question': "Select two best colors",
-            'allowed_choices': 2,
-            'allow_revotes': True,
-            'is_public': True,
-            'choices': [
-                {
+        response = self.put(
+            self.api_link,
+            data={
+                'length':
+                    40,
+                'question':
+                    "Select two best colors",
+                'allowed_choices':
+                    2,
+                'allow_revotes':
+                    True,
+                'is_public':
+                    True,
+                'choices': [{
                     'hash': 'aaaaaaaaaaaa',
                     'label': '\nFirst ',
                     'votes': 5555
-                },
-                {
+                }, {
                     'hash': 'bbbbbbbbbbbb',
                     'label': 'Second',
                     'votes': 5555
-                },
-                {
+                }, {
                     'hash': 'dsadsadsa788',
                     'label': 'New Option',
                     'votes': 5555
-                }
-            ]
-        })
+                }]
+            }
+        )
         self.assertEqual(response.status_code, 200)
 
         response_json = response.json()
@@ -448,26 +418,24 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
 
         # choices were updated
         self.assertEqual(len(response_json['choices']), 3)
-        self.assertEqual(response_json['choices'], [
-            {
+        self.assertEqual(
+            response_json['choices'], [{
                 'hash': 'aaaaaaaaaaaa',
                 'label': 'First',
                 'votes': 1,
                 'selected': False
-            },
-            {
+            }, {
                 'hash': 'bbbbbbbbbbbb',
                 'label': 'Second',
                 'votes': 0,
                 'selected': False
-            },
-            {
+            }, {
                 'hash': response_json['choices'][2]['hash'],
                 'label': 'New Option',
                 'votes': 0,
                 'selected': False
-            }
-        ])
+            }]
+        )
 
         # no votes were removed
         self.assertEqual(response_json['votes'], 1)
@@ -475,34 +443,30 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
 
     def test_moderate_user_poll(self):
         """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.posted_on = timezone.now() - timedelta(days=15)
         self.poll.length = 5
         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': 'Green'
-                },
-                {
+                }, {
                     'label': 'Blue'
-                }
-            ]
-        })
+                }]
+            }
+        )
         self.assertEqual(response.status_code, 200)
 
         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.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):
         """api allows guests to get poll votes"""
@@ -35,49 +36,51 @@ class ThreadGetVotesTests(ThreadPollApiTestCase):
 
     def test_invalid_thread_id(self):
         """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)
         self.assertEqual(response.status_code, 404)
 
     def test_nonexistant_thread_id(self):
         """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)
         self.assertEqual(response.status_code, 404)
 
     def test_invalid_poll_id(self):
         """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)
         self.assertEqual(response.status_code, 404)
 
     def test_nonexistant_poll_id(self):
         """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)
         self.assertEqual(response.status_code, 404)
 
     def test_no_permission(self):
         """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.save()
@@ -111,14 +114,12 @@ class ThreadGetVotesTests(ThreadPollApiTestCase):
 
         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):
         """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.save()
@@ -137,8 +138,8 @@ class ThreadGetVotesTests(ThreadPollApiTestCase):
 
         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):
@@ -147,10 +148,11 @@ class ThreadPostVotesTests(ThreadPollApiTestCase):
 
         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):
         self.poll.choices[2]['votes'] = 1
@@ -195,7 +197,9 @@ class ThreadPostVotesTests(ThreadPollApiTestCase):
         self.poll.save()
 
         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):
         """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):
         """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.save()
@@ -221,18 +223,14 @@ class ThreadPostVotesTests(ThreadPollApiTestCase):
         response = self.post(self.api_link)
         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)
         self.assertContains(response, "You have to make a choice.", status_code=400)
 
     def test_vote_in_closed_category(self):
         """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.save()
@@ -242,9 +240,7 @@ class ThreadPostVotesTests(ThreadPollApiTestCase):
         response = self.post(self.api_link)
         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)
         self.assertContains(response, "You have to make a choice.", status_code=400)
@@ -287,7 +283,8 @@ class ThreadPostVotesTests(ThreadPollApiTestCase):
         response_json = response.json()
         self.assertEqual(response_json['votes'], 4)
         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'])
 
@@ -313,6 +310,7 @@ class ThreadPostVotesTests(ThreadPollApiTestCase):
         response_json = response.json()
         self.assertEqual(response_json['votes'], 4)
         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'])

+ 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.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):
         """api validates if deleting user is authenticated"""
@@ -29,28 +30,22 @@ class PostDeleteApiTests(ThreadsApiTestCase):
 
     def test_no_permission(self):
         """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)
         self.assertContains(response, "You can't delete posts in this category.", status_code=403)
 
     def test_delete_other_user_post_no_permission(self):
         """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.save()
 
         response = self.client.delete(self.api_link)
         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):
         """api validates if user can delete protected post"""
@@ -65,7 +60,8 @@ class PostDeleteApiTests(ThreadsApiTestCase):
 
         response = self.client.delete(self.api_link)
         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):
         """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)
         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):
         """api valdiates if user can delete posts in closed threads"""
@@ -95,7 +92,8 @@ class PostDeleteApiTests(ThreadsApiTestCase):
 
         response = self.client.delete(self.api_link)
         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):
         """api valdiates if user can delete posts in closed categories"""
@@ -110,31 +108,25 @@ class PostDeleteApiTests(ThreadsApiTestCase):
 
         response = self.client.delete(self.api_link)
         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):
         """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)
         self.assertContains(response, "You can't delete thread's first post.", status_code=403)
 
     def test_delete_event(self):
         """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.save()
@@ -144,11 +136,7 @@ class PostDeleteApiTests(ThreadsApiTestCase):
 
     def test_delete_owned_post(self):
         """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)
         self.assertEqual(response.status_code, 200)
@@ -161,10 +149,7 @@ class PostDeleteApiTests(ThreadsApiTestCase):
 
     def test_delete_post(self):
         """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)
         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.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):
         """api validates if deleting user is authenticated"""
@@ -196,24 +182,14 @@ class EventDeleteApiTests(ThreadsApiTestCase):
 
     def test_no_permission(self):
         """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)
         self.assertContains(response, "You can't delete events in this category.", status_code=403)
 
     def test_delete_event(self):
         """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)
         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.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()
 
@@ -32,8 +33,7 @@ class ThreadPostEditsApiTestCase(ThreadsApiTestCase):
                 editor_ip='127.0.0.1',
                 edited_from="Original body",
                 edited_to="First Edit"
-            ),
-            self.post.edits_record.create(
+            ), self.post.edits_record.create(
                 category=self.category,
                 thread=self.thread,
                 editor_name='Deleted',
@@ -41,8 +41,7 @@ class ThreadPostEditsApiTestCase(ThreadsApiTestCase):
                 editor_ip='127.0.0.1',
                 edited_from="First Edit",
                 edited_to="Second Edit"
-            ),
-            self.post.edits_record.create(
+            ), self.post.edits_record.create(
                 category=self.category,
                 thread=self.thread,
                 editor=self.user,
@@ -132,9 +131,7 @@ class ThreadPostPostEditTests(ThreadPostEditsApiTestCase):
         super(ThreadPostPostEditTests, self).setUp()
         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):
         """api handles empty edit in querystring"""
@@ -160,9 +157,7 @@ class ThreadPostPostEditTests(ThreadPostEditsApiTestCase):
 
     def test_no_permission(self):
         """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))
         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.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):
         """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)
         self.assertContains(response, "You can't see who liked this post.", status_code=403)
 
     def test_no_permission_to_list(self):
         """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)
         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)
         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.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()
 
@@ -56,9 +56,7 @@ class ThreadPostMergeApiTestCase(AuthenticatedUserTestCase):
 
     def test_no_permission(self):
         """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")
         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)
 
         # allow closing threads
-        self.override_acl({
-            'can_close_threads': 1
-        })
+        self.override_acl({'can_close_threads': 1})
 
         response = self.client.post(self.api_link, json.dumps({}), content_type="application/json")
-        self.assertContains(response, "You have to select at least two posts to merge.", status_code=400)
+        self.assertContains(
+            response, "You have to select at least two posts to merge.", status_code=400
+        )
 
     def test_closed_category(self):
         """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)
 
         # allow closing threads
-        self.override_acl({
-            'can_close_threads': 1
-        })
+        self.override_acl({'can_close_threads': 1})
 
         response = self.client.post(self.api_link, json.dumps({}), content_type="application/json")
-        self.assertContains(response, "You have to select at least two posts to merge.", status_code=400)
+        self.assertContains(
+            response, "You have to select at least two posts to merge.", status_code=400
+        )
 
     def test_empty_data(self):
         """api handles empty data"""
         response = self.client.post(self.api_link, json.dumps({}), content_type="application/json")
-        self.assertContains(response, "You have to select at least two posts to merge.", status_code=400)
+        self.assertContains(
+            response, "You have to select at least two posts to merge.", status_code=400
+        )
 
     def test_no_posts_ids(self):
         """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):
         """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):
         """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):
         """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):
         """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):
         """api recjects events"""
         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)
 
     def test_merge_notfound_pk(self):
         """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):
         """api recjects attempt to merge with post made in other thread"""
         other_thread = testutils.post_thread(category=self.category)
         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):
         """api recjects attempt to merge with post made by deleted user"""
         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):
         """api recjects attempt to merge with post made by deleted user"""
         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):
         """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):
         """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):
         """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):
         """api merges two posts"""
@@ -224,9 +288,13 @@ class ThreadPostMergeApiTestCase(AuthenticatedUserTestCase):
 
         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.refresh_thread()
@@ -240,30 +308,34 @@ class ThreadPostMergeApiTestCase(AuthenticatedUserTestCase):
 
     def test_merge_hidden_posts(self):
         """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)
 
     def test_merge_unapproved_posts(self):
         """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)
 
     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)
 
-        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)

+ 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.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(
             name='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.override_acl()
@@ -75,10 +75,12 @@ class ThreadPostMoveApiTestCase(AuthenticatedUserTestCase):
         if other_category_acl['can_see']:
             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):
         """you need to authenticate to move posts"""
@@ -89,9 +91,7 @@ class ThreadPostMoveApiTestCase(AuthenticatedUserTestCase):
 
     def test_no_permission(self):
         """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")
         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):
         """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)
 
     def test_current_thread_url(self):
         """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):
         """api validates if other thread exists"""
@@ -123,168 +121,204 @@ class ThreadPostMoveApiTestCase(AuthenticatedUserTestCase):
         other_thread_url = other_thread.get_absolute_url()
         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):
         """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)
 
-        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):
         """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)
 
-        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):
         """api handles empty data"""
         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):
         """api rejects no posts ids"""
         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):
         """api handles invalid data"""
         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):
         """api handles invalid post id"""
         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):
         """api rejects more posts than move limit"""
         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):
         """api validates posts visibility"""
         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):
         """api recjects attempt to move other thread's post"""
         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):
         """api rejects events move"""
         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)
 
     def test_move_first_post(self):
         """api rejects first post move"""
         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)
 
     def test_move_hidden_posts(self):
         """api recjects attempt to move urneadable hidden post"""
         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):
         """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)
 
         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.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)
 
         # 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.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):
         return self.client.patch(api_link, json.dumps(ops), content_type="application/json")
@@ -52,9 +53,7 @@ class ThreadPostPatchApiTestCase(AuthenticatedUserTestCase):
 class PostAddAclApiTests(ThreadPostPatchApiTestCase):
     def test_add_acl_true(self):
         """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)
 
         response_json = response.json()
@@ -62,30 +61,28 @@ class PostAddAclApiTests(ThreadPostPatchApiTestCase):
 
     def test_add_acl_false(self):
         """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)
 
         response_json = response.json()
         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)
 
 
 class PostProtectApiTests(ThreadPostPatchApiTestCase):
     def test_protect_post(self):
         """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.refresh_post()
@@ -96,13 +93,15 @@ class PostProtectApiTests(ThreadPostPatchApiTestCase):
         self.post.is_protected = True
         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.refresh_post()
@@ -110,13 +109,15 @@ class PostProtectApiTests(ThreadPostPatchApiTestCase):
 
     def test_protect_post_no_permission(self):
         """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)
 
         response_json = response.json()
@@ -130,13 +131,15 @@ class PostProtectApiTests(ThreadPostPatchApiTestCase):
         self.post.is_protected = True
         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)
 
         response_json = response.json()
@@ -147,14 +150,15 @@ class PostProtectApiTests(ThreadPostPatchApiTestCase):
 
     def test_unprotect_post_not_editable(self):
         """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)
 
         response_json = response.json()
@@ -170,13 +174,15 @@ class PostApproveApiTests(ThreadPostPatchApiTestCase):
         self.post.is_unapproved = True
         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.refresh_post()
@@ -184,13 +190,15 @@ class PostApproveApiTests(ThreadPostPatchApiTestCase):
 
     def test_unapprove_post(self):
         """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.refresh_post()
@@ -201,13 +209,15 @@ class PostApproveApiTests(ThreadPostPatchApiTestCase):
         self.post.is_unapproved = True
         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)
 
         response_json = response.json()
@@ -224,13 +234,15 @@ class PostApproveApiTests(ThreadPostPatchApiTestCase):
         self.thread.set_first_post(self.post)
         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)
 
         response_json = response.json()
@@ -245,17 +257,21 @@ class PostApproveApiTests(ThreadPostPatchApiTestCase):
         self.post.is_hidden = True
         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)
 
         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.assertTrue(self.post.is_unapproved)
@@ -264,13 +280,15 @@ class PostApproveApiTests(ThreadPostPatchApiTestCase):
 class PostHideApiTests(ThreadPostPatchApiTestCase):
     def test_hide_post(self):
         """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.refresh_post()
@@ -284,13 +302,15 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
         self.refresh_post()
         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.refresh_post()
@@ -298,13 +318,15 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
 
     def test_hide_own_post(self):
         """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.refresh_post()
@@ -318,13 +340,15 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
         self.refresh_post()
         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.refresh_post()
@@ -332,13 +356,15 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
 
     def test_hide_post_no_permission(self):
         """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)
 
         response_json = response.json()
@@ -355,13 +381,15 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
         self.refresh_post()
         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)
 
         response_json = response.json()
@@ -375,14 +403,15 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
         self.post.is_protected = True
         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)
 
         response_json = response.json()
@@ -396,17 +425,18 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
         self.post.is_hidden = True
         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.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)
 
         response_json = response.json()
@@ -420,17 +450,21 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
         self.post.poster = None
         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)
 
         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.assertFalse(self.post.is_hidden)
@@ -441,17 +475,21 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
         self.post.poster = None
         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)
 
         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.assertTrue(self.post.is_hidden)
@@ -461,18 +499,21 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
         self.post.posted_on = timezone.now() - timedelta(minutes=10)
         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)
 
         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.assertFalse(self.post.is_hidden)
@@ -483,18 +524,21 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
         self.post.posted_on = timezone.now() - timedelta(minutes=10)
         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)
 
         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.assertTrue(self.post.is_hidden)
@@ -504,17 +548,21 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
         self.thread.is_closed = True
         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)
 
         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.assertFalse(self.post.is_hidden)
@@ -527,17 +575,21 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
         self.post.is_hidden = True
         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)
 
         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.assertTrue(self.post.is_hidden)
@@ -547,17 +599,21 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
         self.category.is_closed = True
         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)
 
         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.assertFalse(self.post.is_hidden)
@@ -570,17 +626,21 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
         self.post.is_hidden = True
         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)
 
         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.assertTrue(self.post.is_hidden)
@@ -590,13 +650,15 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
         self.thread.set_first_post(self.post)
         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)
 
         response_json = response.json()
@@ -607,13 +669,15 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
         self.thread.set_first_post(self.post)
         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)
 
         response_json = response.json()
@@ -623,42 +687,32 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
 class PostLikeApiTests(ThreadPostPatchApiTestCase):
     def test_like_no_see_permission(self):
         """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)
 
     def test_like_no_like_permission(self):
         """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)
 
     def test_like_post(self):
         """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)
 
         response_json = response.json()
         self.assertEqual(response_json['likes'], 1)
         self.assertEqual(response_json['is_liked'], True)
-        self.assertEqual(response_json['last_likes'], [
-            {
+        self.assertEqual(
+            response_json['last_likes'], [{
                 'id': self.user.id,
                 'username': self.user.username
-            }
-        ])
+            }]
+        )
 
         post = Post.objects.get(pk=self.post.pk)
         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='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)
 
         response_json = response.json()
         self.assertEqual(response_json['likes'], 5)
         self.assertEqual(response_json['is_liked'], True)
-        self.assertEqual(response_json['last_likes'], [
-            {
+        self.assertEqual(
+            response_json['last_likes'], [{
                 'id': self.user.id,
                 'username': self.user.username
-            },
-            {
+            }, {
                 'id': None,
                 'username': 'Miku'
-            },
-            {
+            }, {
                 'id': None,
                 'username': 'Bob'
-            },
-            {
+            }, {
                 'id': None,
                 'username': 'Mugi'
-            }
-        ])
+            }]
+        )
 
         post = Post.objects.get(pk=self.post.pk)
         self.assertEqual(post.likes, response_json['likes'])
@@ -706,9 +755,13 @@ class PostLikeApiTests(ThreadPostPatchApiTestCase):
         """api removes user like from post"""
         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)
 
         response_json = response.json()
@@ -724,20 +777,18 @@ class PostLikeApiTests(ThreadPostPatchApiTestCase):
         """api does no state change if we are linking liked post"""
         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)
 
         response_json = response.json()
         self.assertEqual(response_json['likes'], 1)
         self.assertEqual(response_json['is_liked'], True)
-        self.assertEqual(response_json['last_likes'], [
-            {
+        self.assertEqual(
+            response_json['last_likes'], [{
                 'id': self.user.id,
                 'username': self.user.username
-            }
-        ])
+            }]
+        )
 
         post = Post.objects.get(pk=self.post.pk)
         self.assertEqual(post.likes, response_json['likes'])
@@ -745,9 +796,13 @@ class PostLikeApiTests(ThreadPostPatchApiTestCase):
 
     def test_unlike_post_no_change(self):
         """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)
 
         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.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):
         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"""
         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)
 
 
 class EventAddAclApiTests(ThreadEventPatchApiTestCase):
     def test_add_acl_true(self):
         """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)
 
         response_json = response.json()
@@ -795,30 +847,28 @@ class EventAddAclApiTests(ThreadEventPatchApiTestCase):
 
     def test_add_acl_false(self):
         """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)
 
         response_json = response.json()
         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)
 
 
 class EventHideApiTests(ThreadEventPatchApiTestCase):
     def test_hide_event(self):
         """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.refresh_event()
@@ -832,13 +882,15 @@ class EventHideApiTests(ThreadEventPatchApiTestCase):
         self.refresh_event()
         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.refresh_event()
@@ -846,17 +898,21 @@ class EventHideApiTests(ThreadEventPatchApiTestCase):
 
     def test_hide_event_no_permission(self):
         """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)
 
         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.assertFalse(self.event.is_hidden)
@@ -869,11 +925,13 @@ class EventHideApiTests(ThreadEventPatchApiTestCase):
         self.refresh_event()
         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)

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

@@ -10,16 +10,12 @@ class PostReadApiTests(ThreadsApiTestCase):
     def setUp(self):
         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):
         """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.thread = testutils.post_thread(category=self.category)
         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(
             name='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.override_acl()
@@ -79,10 +80,12 @@ class ThreadPostSplitApiTestCase(AuthenticatedUserTestCase):
         if other_category_acl['can_see']:
             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):
         """you need to authenticate to split posts"""
@@ -93,9 +96,7 @@ class ThreadPostSplitApiTestCase(AuthenticatedUserTestCase):
 
     def test_no_permission(self):
         """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")
         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):
         """api handles empty data"""
         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):
         """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):
         """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):
         """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):
         """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):
         """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):
         """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)
 
     def test_split_first_post(self):
         """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)
 
     def test_split_hidden_posts(self):
         """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):
         """api recjects attempt to split other thread's post"""
         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):
         """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)
 
         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):
         """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)
 
         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):
         """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)
 
         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):
         """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)
 
         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):
         """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)
 
         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):
         """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)
 
         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):
         """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)
 
         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):
         """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)
 
         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):
         """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)
 
         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):
         """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)
 
         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):
         """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)
 
         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):
         """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)
 
         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):
         """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)
 
         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):
         """api splits posts to new thread"""
         self.refresh_thread()
         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)
 
         # thread was created
@@ -441,14 +519,18 @@ class ThreadPostSplitApiTestCase(AuthenticatedUserTestCase):
             '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)
 
         # 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.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):
         new_acl = self.user.acl_cache
@@ -58,49 +56,45 @@ class ReplyThreadTests(AuthenticatedUserTestCase):
 
     def test_cant_reply_thread(self):
         """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)
-        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):
         """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.save()
 
         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
-        self.override_acl({
-            'can_close_threads': 1
-        })
+        self.override_acl({'can_close_threads': 1})
 
         response = self.client.post(self.api_link)
         self.assertEqual(response.status_code, 400)
 
     def test_closed_thread(self):
         """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.save()
 
         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
-        self.override_acl({
-            'can_close_threads': 1
-        })
+        self.override_acl({'can_close_threads': 1})
 
         response = self.client.post(self.api_link)
         self.assertEqual(response.status_code, 400)
@@ -111,33 +105,28 @@ class ReplyThreadTests(AuthenticatedUserTestCase):
 
         response = self.client.post(self.api_link, data={})
         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):
         """post is validated"""
         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.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):
         """endpoint creates new reply"""
         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)
 
         thread = Thread.objects.get(pk=self.thread.pk)
@@ -175,7 +164,7 @@ class ReplyThreadTests(AuthenticatedUserTestCase):
         """unicode characters can be posted"""
         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)

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

@@ -50,9 +50,7 @@ class StartThreadTests(AuthenticatedUserTestCase):
         """has no permission to see selected category"""
         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)
 
@@ -60,9 +58,7 @@ class StartThreadTests(AuthenticatedUserTestCase):
         """has no permission to browse selected category"""
         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)
 
@@ -70,11 +66,11 @@ class StartThreadTests(AuthenticatedUserTestCase):
         """permission to start thread in category is validated"""
         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):
         """can't post in closed category"""
@@ -83,9 +79,7 @@ class StartThreadTests(AuthenticatedUserTestCase):
 
         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)
 
@@ -96,9 +90,7 @@ class StartThreadTests(AuthenticatedUserTestCase):
 
         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)
 
@@ -108,60 +100,62 @@ class StartThreadTests(AuthenticatedUserTestCase):
 
         response = self.client.post(self.api_link, data={})
         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):
         """title is validated"""
         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.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):
         """post is validated"""
         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.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):
         """endpoint creates new thread"""
         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)
 
         thread = self.user.thread_set.all()[:1][0]
@@ -210,12 +204,15 @@ class StartThreadTests(AuthenticatedUserTestCase):
         """permission is checked before thread is closed"""
         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)
 
         thread = self.user.thread_set.all()[:1][0]
@@ -225,12 +222,15 @@ class StartThreadTests(AuthenticatedUserTestCase):
         """can post closed thread"""
         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)
 
         thread = self.user.thread_set.all()[:1][0]
@@ -240,12 +240,15 @@ class StartThreadTests(AuthenticatedUserTestCase):
         """can post unpinned thread"""
         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)
 
         thread = self.user.thread_set.all()[:1][0]
@@ -255,12 +258,15 @@ class StartThreadTests(AuthenticatedUserTestCase):
         """can post locally pinned thread"""
         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)
 
         thread = self.user.thread_set.all()[:1][0]
@@ -270,12 +276,15 @@ class StartThreadTests(AuthenticatedUserTestCase):
         """can post globally pinned thread"""
         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)
 
         thread = self.user.thread_set.all()[:1][0]
@@ -285,12 +294,15 @@ class StartThreadTests(AuthenticatedUserTestCase):
         """cant post globally pinned thread without permission"""
         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)
 
         thread = self.user.thread_set.all()[:1][0]
@@ -300,12 +312,15 @@ class StartThreadTests(AuthenticatedUserTestCase):
         """cant post locally pinned thread without permission"""
         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)
 
         thread = self.user.thread_set.all()[:1][0]
@@ -315,12 +330,15 @@ class StartThreadTests(AuthenticatedUserTestCase):
         """can post hidden thread"""
         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)
 
         thread = self.user.thread_set.all()[:1][0]
@@ -333,12 +351,15 @@ class StartThreadTests(AuthenticatedUserTestCase):
         """cant post hidden thread without permission"""
         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)
 
         thread = self.user.thread_set.all()[:1][0]
@@ -348,9 +369,12 @@ class StartThreadTests(AuthenticatedUserTestCase):
         """unicode characters can be posted"""
         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)

+ 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:
             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):
         response = self.client.get(self.thread.get_api_url())
@@ -92,9 +94,7 @@ class ThreadRetrieveApiTests(ThreadsApiTestCase):
     def test_api_shows_owned_thread(self):
         """api handles "owned threads only"""
         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)
             self.assertEqual(response.status_code, 404)
@@ -103,9 +103,7 @@ class ThreadRetrieveApiTests(ThreadsApiTestCase):
         self.thread.save()
 
         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)
             self.assertEqual(response.status_code, 200)
@@ -113,9 +111,7 @@ class ThreadRetrieveApiTests(ThreadsApiTestCase):
     def test_api_validates_category_see_permission(self):
         """api validates category visiblity"""
         for link in self.tested_links:
-            self.override_acl({
-                'can_see': 0
-            })
+            self.override_acl({'can_see': 0})
 
             response = self.client.get(link)
             self.assertEqual(response.status_code, 404)
@@ -123,35 +119,31 @@ class ThreadRetrieveApiTests(ThreadsApiTestCase):
     def test_api_validates_category_browse_permission(self):
         """api validates category browsability"""
         for link in self.tested_links:
-            self.override_acl({
-                'can_browse': 0
-            })
+            self.override_acl({'can_browse': 0})
 
             response = self.client.get(link)
             self.assertEqual(response.status_code, 404)
 
     def test_api_validates_posts_visibility(self):
         """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])
-        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
-        self.override_acl({
-            'can_hide_posts': 1
-        })
+        self.override_acl({'can_hide_posts': 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_post = testutils.reply_thread(self.thread, is_unapproved=True)
@@ -160,9 +152,7 @@ class ThreadRetrieveApiTests(ThreadsApiTestCase):
         self.assertNotContains(response, unapproved_post.get_absolute_url())
 
         # 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])
         self.assertContains(response, unapproved_post.get_absolute_url())
@@ -189,18 +179,14 @@ class ThreadsReadApiTests(ThreadsApiTestCase):
 
     def test_read_category_no_see(self):
         """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)
         self.assertEqual(response.status_code, 404)
 
     def test_read_category_no_browse(self):
         """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)
         self.assertEqual(response.status_code, 403)
@@ -229,29 +215,24 @@ class ThreadsReadApiTests(ThreadsApiTestCase):
 class ThreadDeleteApiTests(ThreadsApiTestCase):
     def test_delete_thread_no_permission(self):
         """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)
         self.assertEqual(response.status_code, 403)
 
-        self.override_acl({
-            'can_hide_threads': 0
-        })
+        self.override_acl({'can_hide_threads': 0})
 
         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)
         self.assertEqual(response.status_code, 403)
 
     def test_delete_thread(self):
         """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)
         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']:
             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):
@@ -123,16 +125,18 @@ class ThreadPostEditorApiTests(EditorApiTestCase):
         self.assertEqual(response.status_code, 200)
 
         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):
         """endpoint adds category that allows new threads"""
@@ -144,16 +148,18 @@ class ThreadPostEditorApiTests(EditorApiTestCase):
         self.assertEqual(response.status_code, 200)
 
         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):
         """endpoint adds category that allows new closed threads"""
@@ -166,16 +172,18 @@ class ThreadPostEditorApiTests(EditorApiTestCase):
         self.assertEqual(response.status_code, 200)
 
         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):
         """endpoint adds category that allows locally pinned threads"""
@@ -188,16 +196,18 @@ class ThreadPostEditorApiTests(EditorApiTestCase):
         self.assertEqual(response.status_code, 200)
 
         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):
         """endpoint adds category that allows globally pinned threads"""
@@ -210,16 +220,18 @@ class ThreadPostEditorApiTests(EditorApiTestCase):
         self.assertEqual(response.status_code, 200)
 
         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):
         """endpoint adds category that allows globally pinned threads"""
@@ -232,16 +244,18 @@ class ThreadPostEditorApiTests(EditorApiTestCase):
         self.assertEqual(response.status_code, 200)
 
         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({
             'can_start_threads': 2,
@@ -252,16 +266,18 @@ class ThreadPostEditorApiTests(EditorApiTestCase):
         self.assertEqual(response.status_code, 200)
 
         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):
@@ -269,9 +285,9 @@ class ThreadReplyEditorApiTests(EditorApiTestCase):
         super(ThreadReplyEditorApiTests, self).setUp()
 
         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):
         """endpoint validates if user is authenticated"""
@@ -296,71 +312,59 @@ class ThreadReplyEditorApiTests(EditorApiTestCase):
 
     def test_no_reply_permission(self):
         """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)
-        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):
         """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.save()
 
         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
-        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)
         self.assertEqual(response.status_code, 200)
 
     def test_closed_thread(self):
         """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.save()
 
         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
-        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)
         self.assertEqual(response.status_code, 200)
 
     def test_allow_reply_thread(self):
         """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)
         self.assertEqual(response.status_code, 200)
 
     def test_reply_to_visibility(self):
         """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 = testutils.reply_thread(self.thread, is_unapproved=True)
@@ -369,9 +373,7 @@ class ThreadReplyEditorApiTests(EditorApiTestCase):
         self.assertEqual(response.status_code, 404)
 
         # 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)
 
@@ -388,9 +390,7 @@ class ThreadReplyEditorApiTests(EditorApiTestCase):
 
     def test_reply_to_event(self):
         """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)
 
@@ -400,20 +400,19 @@ class ThreadReplyEditorApiTests(EditorApiTestCase):
 
     def test_reply_to(self):
         """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)
 
         response = self.client.get('{}?reply={}'.format(self.api_link, reply_to.pk))
 
         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):
@@ -423,10 +422,11 @@ class EditReplyEditorApiTests(EditorApiTestCase):
         self.thread = testutils.post_thread(category=self.category)
         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):
         """endpoint validates if user is authenticated"""
@@ -451,63 +451,50 @@ class EditReplyEditorApiTests(EditorApiTestCase):
 
     def test_no_edit_permission(self):
         """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)
         self.assertContains(response, "You can't edit posts in this category.", status_code=403)
 
     def test_closed_category(self):
         """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.save()
 
         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
-        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)
         self.assertEqual(response.status_code, 200)
 
     def test_closed_thread(self):
         """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.save()
 
         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
-        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)
         self.assertEqual(response.status_code, 200)
 
     def test_protected_post(self):
         """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.save()
@@ -516,56 +503,42 @@ class EditReplyEditorApiTests(EditorApiTestCase):
         self.assertContains(response, "This post is protected. You can't edit it.", status_code=403)
 
         # 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)
         self.assertEqual(response.status_code, 200)
 
     def test_post_visibility(self):
         """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()
 
         response = self.client.get(self.api_link)
         self.assertContains(response, "This post is hidden, you can't edit it.", status_code=403)
 
         # 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)
         self.assertEqual(response.status_code, 200)
 
         # 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.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()
 
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 404)
 
         # 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)
         self.assertEqual(response.status_code, 200)
@@ -587,11 +560,13 @@ class EditReplyEditorApiTests(EditorApiTestCase):
             'can_edit_posts': 1,
         })
 
-        self.post.poster = None;
+        self.post.poster = None
         self.post.save()
 
         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
         self.override_acl({
@@ -603,20 +578,18 @@ class EditReplyEditorApiTests(EditorApiTestCase):
 
     def test_edit_first_post_hidden(self):
         """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.save()
         self.thread.first_post.is_hidden = True
         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)
         self.assertEqual(response.status_code, 200)
@@ -629,9 +602,9 @@ class EditReplyEditorApiTests(EditorApiTestCase):
             })
 
             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)
 
         attachments = list(Attachment.objects.order_by('id'))
@@ -652,15 +625,23 @@ class EditReplyEditorApiTests(EditorApiTestCase):
             add_acl(self.user, attachment)
 
         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(
             name='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')
 
     def test_merge_no_threads(self):
@@ -29,110 +31,128 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         self.assertEqual(response.status_code, 403)
 
         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):
         """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)
 
         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):
         """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)
 
         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)
 
         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):
         """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)
 
         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):
         """api validates if we are trying to merge with invalid thread"""
         testutils.post_thread(category=self.category_b)
 
-        response = self.client.post(self.api_link, json.dumps({
-            'threads': [self.thread.id, self.thread.id + 1000]
-        }), content_type="application/json")
+        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)
 
         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):
         """api validates if we are trying to merge with inaccesible thread"""
         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)
 
         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):
         """api validates permission to merge threads"""
         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)
 
         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):
         """api rejects too many threads to merge"""
@@ -147,15 +167,18 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
             '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)
 
         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):
         """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)
 
-        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)
 
         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):
         """api rejects merge because final thread title was invalid"""
@@ -190,17 +219,22 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
 
         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)
 
         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):
         """api rejects merge because final category was invalid"""
@@ -213,17 +247,19 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
 
         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)
 
         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):
         """api rejects merge because category isn't allowing starting threads"""
@@ -237,19 +273,21 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
 
         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)
 
         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):
         """api rejects merge because final weight was invalid"""
@@ -262,18 +300,22 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
 
         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)
 
         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):
         """api rejects merge because global weight was unallowed"""
@@ -286,20 +328,23 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
 
         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)
 
         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):
         """api rejects merge because local weight was unallowed"""
@@ -312,20 +357,23 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
 
         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)
 
         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):
         """api allows local weight"""
@@ -339,18 +387,23 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
 
         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)
 
         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):
         """api allows global weight"""
@@ -364,18 +417,23 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
 
         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)
 
         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):
         """api rejects merge because closing thread was unallowed"""
@@ -388,20 +446,23 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
 
         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)
 
         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):
         """api allows for closing thread"""
@@ -414,19 +475,24 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
 
         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)
 
         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):
         """api rejects merge because hidden thread was unallowed"""
@@ -440,20 +506,23 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
 
         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)
 
         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):
         """api allows for hiding thread"""
@@ -467,19 +536,24 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
 
         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)
 
         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):
         """api performs basic merge"""
@@ -494,11 +568,15 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
 
         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)
 
         # is response json with new thread?
@@ -534,14 +612,18 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
 
         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)
 
         # is response json with new thread?
@@ -581,12 +663,16 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
 
         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)
 
         # is response json with new thread?
@@ -618,11 +704,15 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         other_thread = testutils.post_thread(self.category)
         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)
 
         response_json = response.json()
@@ -644,11 +734,15 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         other_thread = testutils.post_thread(self.category)
         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)
 
         response_json = response.json()
@@ -671,20 +765,24 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         poll = testutils.post_poll(self.thread, self.user)
         other_poll = testutils.post_poll(other_thread, self.user)
 
-        response = self.client.post(self.api_link, json.dumps({
-            'threads': [self.thread.id, other_thread.id],
-            'title': 'Merged thread!',
-            'category': self.category.id,
-        }), 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.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
         self.assertEqual(Poll.objects.count(), 2)
@@ -701,17 +799,19 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         testutils.post_poll(self.thread, self.user)
         testutils.post_poll(other_thread, self.user)
 
-        response = self.client.post(self.api_link, json.dumps({
-            'threads': [self.thread.id, other_thread.id],
-            'title': 'Merged thread!',
-            'category': self.category.id,
-            'poll': 'dsa7dsadsa9789'
-        }), content_type="application/json")
+        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.json(), {
-            'detail': "Invalid choice."
-        })
+        self.assertEqual(response.json(), {'detail': "Invalid choice."})
 
         # polls and votes were untouched
         self.assertEqual(Poll.objects.count(), 2)
@@ -728,12 +828,16 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         testutils.post_poll(self.thread, self.user)
         testutils.post_poll(other_thread, self.user)
 
-        response = self.client.post(self.api_link, json.dumps({
-            'threads': [self.thread.id, other_thread.id],
-            'title': 'Merged thread!',
-            'category': self.category.id,
-            'poll': 0
-        }), content_type="application/json")
+        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)
 
         # polls and votes are gone
@@ -750,12 +854,16 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         poll = testutils.post_poll(self.thread, self.user)
         other_poll = testutils.post_poll(other_thread, self.user)
 
-        response = self.client.post(self.api_link, json.dumps({
-            'threads': [self.thread.id, other_thread.id],
-            'title': 'Merged thread!',
-            'category': self.category.id,
-            'poll': 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)
 
         # other poll and its votes are gone
@@ -776,12 +884,16 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         poll = testutils.post_poll(self.thread, self.user)
         other_poll = testutils.post_poll(other_thread, self.user)
 
-        response = self.client.post(self.api_link, json.dumps({
-            'threads': [self.thread.id, other_thread.id],
-            'title': 'Merged thread!',
-            'category': self.category.id,
-            'poll': 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)
 
         # 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):
         """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.assertEqual(self.thread.title, "New title is here!")
@@ -139,12 +141,13 @@ class ThreadsModerationTests(AuthenticatedUserTestCase):
         Category(
             name='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')
 
         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.assertEqual(self.thread.category, new_category)
@@ -157,8 +160,7 @@ class ThreadsModerationTests(AuthenticatedUserTestCase):
     def test_move_thread_to_same_category(self):
         """moves_thread does not move thread to same category it is in"""
         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.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
 
 
-LISTS_URLS = (
-    '',
-    'my/',
-    'new/',
-    'unread/',
-    'subscribed/',
-)
+LISTS_URLS = ('', 'my/', 'new/', 'unread/', 'subscribed/', )
 
 
 class ThreadsListTestCase(AuthenticatedUserTestCase):
@@ -30,7 +24,6 @@ class ThreadsListTestCase(AuthenticatedUserTestCase):
 
         self.root = Category.objects.root_category()
         self.first_category = Category.objects.get(slug='first-category')
-
         """
         Create categories tree for test cases:
 
@@ -48,12 +41,16 @@ class ThreadsListTestCase(AuthenticatedUserTestCase):
             name='Category A',
             slug='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(
             name='Category E',
             slug='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()
 
@@ -63,7 +60,9 @@ class ThreadsListTestCase(AuthenticatedUserTestCase):
             name='Category B',
             slug='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')
 
@@ -71,12 +70,16 @@ class ThreadsListTestCase(AuthenticatedUserTestCase):
             name='Category C',
             slug='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(
             name='Category D',
             slug='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_d = Category.objects.get(slug='category-d')
@@ -86,7 +89,9 @@ class ThreadsListTestCase(AuthenticatedUserTestCase):
             name='Category F',
             slug='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')
 
@@ -146,26 +151,17 @@ class ThreadsListTestCase(AuthenticatedUserTestCase):
 class ApiTests(ThreadsListTestCase):
     def test_root_category(self):
         """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)
 
     def test_explicit_first_page(self):
         """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)
 
     def test_invalid_list_type(self):
         """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)
 
 
@@ -211,11 +207,10 @@ class AllThreadsListTests(ThreadsListTestCase):
             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.logout_user()
@@ -231,11 +226,10 @@ class AllThreadsListTests(ThreadsListTestCase):
             self.assertEqual(response.status_code, 403)
 
             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)
 
     def test_list_renders_categories_picker(self):
@@ -243,7 +237,9 @@ class AllThreadsListTests(ThreadsListTestCase):
         Category(
             name='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')
 
         testutils.post_thread(
@@ -253,22 +249,16 @@ class AllThreadsListTests(ThreadsListTestCase):
         response = self.client.get('/')
         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
-        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
-        self.assertNotContains(response,
-            'subcategory-%s' % test_category.css_class)
+        self.assertNotContains(response, 'subcategory-%s' % test_category.css_class)
 
         self.access_all_categories()
         response = self.client.get(self.api_link)
@@ -284,24 +274,19 @@ class AllThreadsListTests(ThreadsListTestCase):
         response = self.client.get(self.category_a.get_absolute_url())
         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
-        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()
         response = self.client.get('%s?category=%s' % (self.api_link, self.category_a.pk))
         self.assertEqual(response.status_code, 200)
 
         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):
         """
@@ -318,9 +303,7 @@ class AllThreadsListTests(ThreadsListTestCase):
             is_pinned=True,
         )
 
-        standard = testutils.post_thread(
-            category=self.first_category
-        )
+        standard = testutils.post_thread(category=self.first_category)
 
         response = self.client.get('/')
         self.assertEqual(response.status_code, 200)
@@ -371,9 +354,7 @@ class AllThreadsListTests(ThreadsListTestCase):
         """threads list is paginated for users with js disabled"""
         threads = []
         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
         response = self.client.get('/?page=2')
@@ -381,7 +362,8 @@ class AllThreadsListTests(ThreadsListTestCase):
 
         for thread in threads[:settings.MISAGO_THREADS_PER_PAGE]:
             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())
         for thread in threads[settings.MISAGO_THREADS_PER_PAGE * 2:]:
             self.assertNotContains(response, thread.get_absolute_url())
@@ -412,7 +394,9 @@ class CategoryThreadsListTests(ThreadsListTestCase):
         Category(
             name='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')
 
         for url in LISTS_URLS:
@@ -427,40 +411,44 @@ class CategoryThreadsListTests(ThreadsListTestCase):
         Category(
             name='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')
 
         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)
             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)
 
     def test_display_pinned_threads(self):
@@ -478,9 +466,7 @@ class CategoryThreadsListTests(ThreadsListTestCase):
             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())
         self.assertEqual(response.status_code, 200)
@@ -540,14 +526,10 @@ class ThreadsVisibilityTests(ThreadsListTestCase):
 
         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
         self.access_all_categories()
@@ -567,10 +549,8 @@ class ThreadsVisibilityTests(ThreadsListTestCase):
         # thread displays
         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
         self.access_all_categories()
@@ -587,12 +567,12 @@ class ThreadsVisibilityTests(ThreadsListTestCase):
         Category(
             name='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_thread = testutils.post_thread(
-            category=test_category
-        )
+        test_thread = testutils.post_thread(category=test_category)
 
         response = self.client.get('/')
         self.assertEqual(response.status_code, 200)
@@ -604,7 +584,9 @@ class ThreadsVisibilityTests(ThreadsListTestCase):
         Category(
             name='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')
 
@@ -704,18 +686,14 @@ class ThreadsVisibilityTests(ThreadsListTestCase):
             is_hidden=True,
         )
 
-        self.access_all_categories({
-            'can_hide_threads': 1
-        })
+        self.access_all_categories({'can_hide_threads': 1})
 
         response = self.client.get('/')
         self.assertEqual(response.status_code, 200)
         self.assertContains(response, test_thread.get_absolute_url())
 
         # 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)
         self.assertEqual(response.status_code, 200)
@@ -732,18 +710,14 @@ class ThreadsVisibilityTests(ThreadsListTestCase):
             is_hidden=True,
         )
 
-        self.access_all_categories({
-            'can_hide_threads': 1
-        })
+        self.access_all_categories({'can_hide_threads': 1})
 
         response = self.client.get('/')
         self.assertEqual(response.status_code, 200)
         self.assertContains(response, test_thread.get_absolute_url())
 
         # 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)
         self.assertEqual(response.status_code, 200)
@@ -760,18 +734,14 @@ class ThreadsVisibilityTests(ThreadsListTestCase):
             is_unapproved=True,
         )
 
-        self.access_all_categories({
-            'can_approve_content': 1
-        })
+        self.access_all_categories({'can_approve_content': 1})
 
         response = self.client.get('/')
         self.assertEqual(response.status_code, 200)
         self.assertContains(response, test_thread.get_absolute_url())
 
         # 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)
         self.assertEqual(response.status_code, 200)
@@ -922,13 +892,10 @@ class NewThreadsListTests(ThreadsListTestCase):
         self.user.save()
 
         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()
 
@@ -966,9 +933,7 @@ class NewThreadsListTests(ThreadsListTestCase):
 
         test_thread = testutils.post_thread(
             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()
@@ -1004,8 +969,7 @@ class NewThreadsListTests(ThreadsListTestCase):
         self.user.save()
 
         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()
@@ -1040,9 +1004,7 @@ class NewThreadsListTests(ThreadsListTestCase):
         self.user.joined_on = timezone.now() - timedelta(days=5)
         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.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.save()
 
-        test_thread = testutils.post_thread(
-            category=self.category_a
-        )
+        test_thread = testutils.post_thread(category=self.category_a)
 
         self.user.categoryread_set.create(
             category=self.category_a,
@@ -1140,7 +1100,9 @@ class UnreadThreadsListTests(ThreadsListTestCase):
         self.assertEqual(len(response_json['results']), 0)
 
         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)
 
         response_json = response.json()
@@ -1151,9 +1113,7 @@ class UnreadThreadsListTests(ThreadsListTestCase):
         self.user.joined_on = timezone.now() - timedelta(days=5)
         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.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.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)
 
         response_json = response.json()
@@ -1194,9 +1156,7 @@ class UnreadThreadsListTests(ThreadsListTestCase):
         self.user.joined_on = timezone.now() - timedelta(days=5)
         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()
 
@@ -1219,7 +1179,9 @@ class UnreadThreadsListTests(ThreadsListTestCase):
         self.assertEqual(len(response_json['results']), 0)
 
         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)
 
         response_json = response.json()
@@ -1230,9 +1192,7 @@ class UnreadThreadsListTests(ThreadsListTestCase):
         self.user.joined_on = timezone.now() - timedelta(days=5)
         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.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.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)
 
         response_json = response.json()
@@ -1271,9 +1233,7 @@ class UnreadThreadsListTests(ThreadsListTestCase):
 
         test_thread = testutils.post_thread(
             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)
@@ -1302,7 +1262,9 @@ class UnreadThreadsListTests(ThreadsListTestCase):
         self.assertEqual(len(response_json['results']), 0)
 
         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)
 
         response_json = response.json()
@@ -1314,8 +1276,7 @@ class UnreadThreadsListTests(ThreadsListTestCase):
         self.user.save()
 
         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)
@@ -1344,7 +1305,9 @@ class UnreadThreadsListTests(ThreadsListTestCase):
         self.assertEqual(len(response_json['results']), 0)
 
         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)
 
         response_json = response.json()
@@ -1356,13 +1319,11 @@ class UnreadThreadsListTests(ThreadsListTestCase):
         self.user.save()
 
         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.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)
 
@@ -1392,7 +1353,9 @@ class UnreadThreadsListTests(ThreadsListTestCase):
         self.assertEqual(len(response_json['results']), 0)
 
         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)
 
         response_json = response.json()
@@ -1402,9 +1365,7 @@ class UnreadThreadsListTests(ThreadsListTestCase):
 class SubscribedThreadsListTests(ThreadsListTestCase):
     def test_list_shows_subscribed_thread(self):
         """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(
             thread=test_thread,
             category=self.category_a,
@@ -1433,7 +1394,9 @@ class SubscribedThreadsListTests(ThreadsListTestCase):
         self.assertContains(response, test_thread.get_absolute_url())
 
         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)
 
         response_json = response.json()
@@ -1442,9 +1405,7 @@ class SubscribedThreadsListTests(ThreadsListTestCase):
 
     def test_list_hides_unsubscribed_thread(self):
         """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()
 
@@ -1468,7 +1429,9 @@ class SubscribedThreadsListTests(ThreadsListTestCase):
         self.assertNotContains(response, test_thread.get_absolute_url())
 
         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)
 
         response_json = response.json()
@@ -1480,8 +1443,7 @@ class UnapprovedListTests(ThreadsListTestCase):
     def test_list_errors_without_permission(self):
         """list errors if user has no permission to access it"""
         TEST_URLS = (
-            '/unapproved/',
-            self.category_a.get_absolute_url() + 'unapproved/',
+            '/unapproved/', self.category_a.get_absolute_url() + 'unapproved/',
             '%s?list=unapproved' % self.api_link,
         )
 
@@ -1492,9 +1454,7 @@ class UnapprovedListTests(ThreadsListTestCase):
 
         # approval perm has no influence on visibility
         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()
             response = self.client.get(test_url)
@@ -1502,9 +1462,7 @@ class UnapprovedListTests(ThreadsListTestCase):
 
         # approval perm has no influence on visibility
         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()
             response = self.client.get(test_url)
@@ -1524,9 +1482,7 @@ class UnapprovedListTests(ThreadsListTestCase):
 
         self.access_all_categories({
             'can_approve_content': True
-        }, {
-            'can_see_unapproved_content_lists': True
-        })
+        }, {'can_see_unapproved_content_lists': True})
         response = self.client.get('/unapproved/')
         self.assertEqual(response.status_code, 200)
         self.assertContains(response, visible_thread.get_absolute_url())
@@ -1534,9 +1490,7 @@ class UnapprovedListTests(ThreadsListTestCase):
 
         self.access_all_categories({
             '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/')
         self.assertEqual(response.status_code, 200)
         self.assertContains(response, visible_thread.get_absolute_url())
@@ -1545,9 +1499,7 @@ class UnapprovedListTests(ThreadsListTestCase):
         # test api
         self.access_all_categories({
             '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)
         self.assertEqual(response.status_code, 200)
         self.assertContains(response, visible_thread.get_absolute_url())
@@ -1568,26 +1520,20 @@ class UnapprovedListTests(ThreadsListTestCase):
             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/')
         self.assertEqual(response.status_code, 200)
         self.assertContains(response, visible_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/')
         self.assertEqual(response.status_code, 200)
         self.assertContains(response, visible_thread.get_absolute_url())
         self.assertNotContains(response, hidden_thread.get_absolute_url())
 
         # 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)
         self.assertEqual(response.status_code, 200)
         self.assertContains(response, visible_thread.get_absolute_url())
@@ -1602,9 +1548,7 @@ class OwnerOnlyThreadsVisibilityTests(AuthenticatedUserTestCase):
 
     def override_acl(self, user):
         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
 
         override_acl(user, user.acl_cache)

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

@@ -41,11 +41,7 @@ class ThreadViewTestCase(AuthenticatedUserTestCase):
         if 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):
@@ -56,9 +52,7 @@ class ThreadVisibilityTests(ThreadViewTestCase):
 
     def test_view_shows_owner_thread(self):
         """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())
         self.assertEqual(response.status_code, 404)
@@ -66,34 +60,26 @@ class ThreadVisibilityTests(ThreadViewTestCase):
         self.thread.starter = self.user
         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())
         self.assertContains(response, self.thread.title)
 
     def test_view_validates_category_permissions(self):
         """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())
         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())
         self.assertEqual(response.status_code, 404)
 
     def test_view_shows_unapproved_thread(self):
         """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.save()
@@ -102,9 +88,7 @@ class ThreadVisibilityTests(ThreadViewTestCase):
         self.assertEqual(response.status_code, 404)
 
         # 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())
         self.assertContains(response, self.thread.title)
@@ -114,18 +98,14 @@ class ThreadVisibilityTests(ThreadViewTestCase):
         self.thread.starter = self.user
         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())
         self.assertContains(response, self.thread.title)
 
     def test_view_shows_hidden_thread(self):
         """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.save()
@@ -141,9 +121,7 @@ class ThreadVisibilityTests(ThreadViewTestCase):
         self.assertEqual(response.status_code, 404)
 
         # 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())
         self.assertContains(response, self.thread.title)
@@ -189,9 +167,7 @@ class ThreadPostsVisibilityTests(ThreadViewTestCase):
         self.assertNotContains(response, post.parsed)
 
         # 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())
         self.assertContains(response, post.get_absolute_url())
@@ -199,13 +175,13 @@ class ThreadPostsVisibilityTests(ThreadViewTestCase):
         self.assertNotContains(response, post.parsed)
 
         # 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())
         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.assertContains(response, post.parsed)
 
@@ -218,9 +194,7 @@ class ThreadPostsVisibilityTests(ThreadViewTestCase):
         self.assertNotContains(response, post.get_absolute_url())
 
         # 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())
         self.assertContains(response, post.get_absolute_url())
@@ -231,9 +205,7 @@ class ThreadPostsVisibilityTests(ThreadViewTestCase):
         post.poster = self.user
         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())
         self.assertContains(response, post.get_absolute_url())
@@ -375,7 +347,9 @@ class ThreadEventVisibilityTests(ThreadViewTestCase):
 
     def test_changed_thread_title_event_renders(self):
         """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]
         self.assertEqual(event.event_type, 'changed_title')
@@ -442,8 +416,7 @@ class ThreadAttachmentsViewTests(ThreadViewTestCase):
                     'uploader': '/user/bobboberson-123/'
                 },
                 'filename': 'Archiwum-1.zip',
-            }),
-            self.mock_attachment_cache({
+            }), self.mock_attachment_cache({
                 'url': {
                     'index': '/attachment/loremipsum-223/',
                     'thumb': '/attachment/thumb/loremipsum-223/',
@@ -451,8 +424,7 @@ class ThreadAttachmentsViewTests(ThreadViewTestCase):
                 },
                 'is_image': True,
                 'filename': 'Archiwum-2.zip'
-            }),
-            self.mock_attachment_cache({
+            }), self.mock_attachment_cache({
                 'url': {
                     'index': '/attachment/loremipsum-323/',
                     'thumb': None,
@@ -521,9 +493,7 @@ class ThreadLikedPostsViewTests(ThreadViewTestCase):
         """
         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())
         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
 
         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")
 
     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
         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:
             trees_map.get_type_for_tree_id(tree_id + 1000)
             self.fail("invalid tree id should cause KeyError being raised")
         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):
         """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')
             self.fail("invalid root name should cause KeyError being raised")
         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()
 
         self.root = Category.objects.root_category()
-
         """
         Create categories tree for test cases:
 
@@ -27,12 +26,16 @@ class AddCategoriesToItemsTests(MisagoTestCase):
             name='Category A',
             slug='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(
             name='Category E',
             slug='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()
 
@@ -41,19 +44,25 @@ class AddCategoriesToItemsTests(MisagoTestCase):
             name='Category B',
             slug='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')
         Category(
             name='Category C',
             slug='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(
             name='Category D',
             slug='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_d = Category.objects.get(slug='category-d')
@@ -63,7 +72,9 @@ class AddCategoriesToItemsTests(MisagoTestCase):
             name='Category F',
             slug='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()
 
@@ -77,8 +88,7 @@ class AddCategoriesToItemsTests(MisagoTestCase):
         self.category_e = Category.objects.get(slug='category-e')
         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):
         """thread in root category is handled"""
@@ -230,7 +240,9 @@ class GetThreadIdFromUrlTests(MisagoTestCase):
         for case in TEST_CASES:
             pk = get_thread_id_from_url(case['request'], case['url'])
             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):
         TEST_CASES = (

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

@@ -31,11 +31,7 @@ class ValidatePostTests(TestCase):
 class ValidateTitleTests(TestCase):
     def test_valid_titles(self):
         """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:
             validate_title(title)

+ 44 - 43
misago/threads/testutils.py

@@ -12,9 +12,17 @@ from .models import Poll, Post, Thread
 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()
 
     kwargs = {
@@ -41,7 +49,7 @@ def post_thread(category, title='Test thread', poster='Tester',
             'last_poster': poster,
             'last_poster_name': poster.username,
             'last_poster_slug': poster.slug,
-            })
+        })
     except AttributeError:
         kwargs.update({
             'starter_name': poster,
@@ -62,9 +70,18 @@ def post_thread(category, title='Test thread', poster='Tester',
     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)
 
     kwargs = {
@@ -110,28 +127,23 @@ def post_poll(thread, poster):
         poster_slug=poster.slug,
         poster_ip='127.0.0.1',
         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,
         votes=4
     )
@@ -140,8 +152,7 @@ def post_poll(thread, poster):
     try:
         user = UserModel.objects.get(slug='bob')
     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(
         category=thread.category,
@@ -200,12 +211,7 @@ def like_post(post, liker=None, username=None):
             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:
         like = post.postlike_set.create(
             category=post.category,
@@ -215,12 +221,7 @@ def like_post(post, liker=None, username=None):
             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.save()

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

@@ -16,98 +16,90 @@ class PrivateThread(ThreadType):
         return reverse('misago:private-threads')
 
     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):
-        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):
         return reverse('misago:api:private-thread-read')
 
     def get_thread_absolute_url(self, thread, 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:
-            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):
-        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):
-        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):
-        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):
-        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):
-        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):
-        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):
-        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):
-        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):
-        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):
-        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):
-        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):
-        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):
         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:
             return reverse('misago:threads')
 
     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):
-        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):
-        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):
         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:
-            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):
-        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):
-        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):
-        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):
-        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):
-        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):
-        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):
-        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):
-        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):
-        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):
-        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):
-        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):
-        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):
-        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):
-        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):
-        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):
-        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):
-        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):
-        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):
-        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):
     """Object that maps trees to strategies"""
+
     def __init__(self, types_modules):
         self.is_loaded = False
         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.goto import (
     ThreadGotoPostView, ThreadGotoLastView, ThreadGotoNewView, ThreadGotoUnapprovedView,
-    PrivateThreadGotoPostView, PrivateThreadGotoLastView, PrivateThreadGotoNewView)
+    PrivateThreadGotoPostView, PrivateThreadGotoLastView, PrivateThreadGotoNewView
+)
 from misago.threads.views.list import ForumThreadsList, CategoryThreadsList, PrivateThreadsList
 from misago.threads.views.thread import ThreadView, PrivateThreadView
 
-
-LISTS_TYPES = (
-    'all',
-    'my',
-    'new',
-    'unread',
-    'subscribed',
-    'unapproved',
-)
+LISTS_TYPES = ('all', 'my', 'new', 'unread', 'subscribed', 'unapproved', )
 
 
 def threads_list_patterns(prefix, view, patterns):
@@ -28,58 +21,56 @@ def threads_list_patterns(prefix, view, patterns):
         else:
             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
 
 
 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:
-    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):
     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+)/(?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
 
@@ -120,8 +111,12 @@ urlpatterns += goto_patterns(
     new=PrivateThreadGotoNewView,
 )
 
-
 urlpatterns += [
     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'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

+ 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):
             # item in subcategory resolution
             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
                     item.top_category = category
         else:
             # item from other category's scope
             for category in categories:
                 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
                     item.top_category = category
 
@@ -48,8 +47,7 @@ def add_likes_to_posts(user, posts):
         posts_map[post.id] = post
         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'):
         posts_map[like['post_id']].is_liked = True

+ 24 - 20
misago/threads/validators.py

@@ -43,21 +43,23 @@ def validate_post(post):
         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 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:
         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 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):
@@ -70,21 +72,23 @@ def validate_title(title):
         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 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:
         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 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_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]
 
     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):
-        return {
-            'category': self._model,
-            'subcategories': self._children
-        }
+        return {'category': self._model, 'subcategories': self._children}
 
 
 class ThreadsRootCategory(ViewModel):
     def get_categories(self, request):
         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):
@@ -91,5 +86,6 @@ class PrivateThreadsCategory(ViewModel):
 
 
 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:
             queryset = queryset.select_for_update()
         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)
 
@@ -40,8 +36,7 @@ class ViewModel(BaseViewModel):
         return post
 
     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):

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

@@ -23,8 +23,9 @@ class ViewModel(object):
 
         posts_limit = settings.MISAGO_POSTS_PER_PAGE
         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)
 
         posts = list(list_page.object_list)
@@ -53,7 +54,8 @@ class ViewModel(object):
 
             events_limit = settings.MISAGO_EVENTS_PER_PAGE
             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
             posts.sort(key=lambda p: p.pk)
@@ -69,10 +71,7 @@ class ViewModel(object):
 
     def get_posts_queryset(self, request, thread):
         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')
         return exclude_invisible_posts(request.user, thread.category, queryset)
 
@@ -97,10 +96,7 @@ class ViewModel(object):
         return context
 
     def get_template_context(self):
-        return {
-            'posts': self.posts,
-            'paginator': self.paginator
-        }
+        return {'posts': self.posts, 'paginator': self.paginator}
 
 
 class ThreadPosts(ViewModel):

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

@@ -18,20 +18,22 @@ from misago.threads.threadtypes import trees_map
 
 __all__ = ['ForumThread', 'PrivateThread']
 
-
 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):
-    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.path = self.get_thread_path(model.category)
@@ -60,16 +62,16 @@ class ViewModel(BaseViewModel):
         return self._poll
 
     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):
         thread_path = []
 
         if category.level:
             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')
             thread_path = list(categories)
         else:
@@ -101,9 +103,7 @@ class ForumThread(ViewModel):
             queryset = Thread.objects.select_related(*BASE_RELATIONS)
 
         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)

+ 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']
 
-
 LISTS_NAMES = {
     'all': None,
     'my': ugettext_lazy("Your threads"),
@@ -32,11 +31,16 @@ LISTS_NAMES = {
 }
 
 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)
         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)
 
         if list_page.number > 1:
             threads = list(list_page.object_list)
         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)
 
         if list_type in ('new', 'unread'):
@@ -90,13 +100,15 @@ class ViewModel(object):
             has_permission = request.user.acl_cache['can_see_unapproved_content_lists']
             if list_type == 'unapproved' and not has_permission:
                 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):
         return LISTS_NAMES[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):
         return []
@@ -105,7 +117,7 @@ class ViewModel(object):
         return []
 
     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):
         context = {
@@ -122,7 +134,6 @@ class ViewModel(object):
         return {
             'list_name': self.get_list_name(self.list_type),
             'list_type': self.list_type,
-
             'threads': self.threads,
             'paginator': self.paginator
         }
@@ -131,10 +142,8 @@ class ViewModel(object):
 class ForumThreads(ViewModel):
     def get_pinned_threads(self, queryset, category, threads_categories):
         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:
             return queryset.filter(weight=2)
 
@@ -153,14 +162,14 @@ class ForumThreads(ViewModel):
 
 class PrivateThreads(ViewModel):
     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
         participated_threads = request.user.threadparticipant_set.values('thread_id')
 
         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:
             queryset = queryset.filter(id__in=participated_threads)
 
@@ -176,6 +185,8 @@ class PrivateThreads(ViewModel):
 """
 Thread queryset utils
 """
+
+
 def get_threads_queryset(user, categories, list_type):
     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':
         # new threads have no entry in reads table
         # 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 = 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):
     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')
     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):
         return SearchAttachmentsForm
@@ -65,7 +58,7 @@ class AttachmentsList(AttachmentAdmin, generic.ListView):
 
     def delete_from_cache(self, post, attachments):
         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 = []
         for a in post.attachments_cache:
@@ -86,7 +79,7 @@ class DeleteAttachment(AttachmentAdmin, generic.ButtonView):
 
     def delete_from_cache(self, attachment):
         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 = []
         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):
-    ordering = (('name', None),)
+    ordering = (('name', None), )
 
     def get_queryset(self):
         queryset = super(AttachmentTypesList, self).get_queryset()
@@ -43,7 +43,9 @@ class EditAttachmentType(AttachmentTypeAdmin, generic.ModelFormView):
 class DeleteAttachmentType(AttachmentTypeAdmin, generic.ButtonView):
     def check_permissions(self, request, target):
         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}
 
     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):
     thread = None
-    read_aware=False
+    read_aware = False
 
     def get(self, request, pk, slug, **kwargs):
         thread = self.get_thread(request, pk, slug).unwrap()
@@ -40,7 +40,7 @@ class GotoView(View):
 
         thread_len = posts_queryset.count()
         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
         hits = max(1, thread_len - settings.MISAGO_POSTS_TAIL)
@@ -100,7 +100,10 @@ class ThreadGotoUnapprovedView(GotoView):
     def test_permissions(self, request, thread):
         if not thread.acl['can_approve']:
             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):
         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):
         category = super(CategoryThreadsList, self).get_category(request, **kwargs)
         if not category.level:
-            raise Http404() # disallow root category access
+            raise Http404()  # disallow root category access
         return category
 
 

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

@@ -23,10 +23,7 @@ class ThreadBase(View):
 
     def get_thread(self, request, pk, slug):
         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):
@@ -47,7 +44,8 @@ class ThreadBase(View):
 
     def get_template_context(self, request, thread, posts):
         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())
@@ -61,9 +59,7 @@ class ThreadView(ThreadBase):
     template_name = 'misago/thread/thread.html'
 
     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):

+ 4 - 7
misago/urls.py

@@ -14,10 +14,10 @@ urlpatterns = [
     url(r'^', include('misago.search.urls')),
 
     # 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
     # 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'),
 ]
 
-
 # Register API
 apipatterns = [
     url(r'^', include('misago.categories.urls.api')),
@@ -40,7 +39,6 @@ urlpatterns += [
     url(r'^api/', include(apipatterns, namespace='api')),
 ]
 
-
 # Register Misago ACP
 if settings.MISAGO_ADMIN_PATH:
     # Admin patterns recognised by Misago
@@ -53,7 +51,6 @@ if settings.MISAGO_ADMIN_PATH:
         url(admin_prefix, include(adminpatterns, namespace='admin')),
     ]
 
-
 # Make error pages accessible casually in DEBUG
 if settings.DEBUG:
     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)
 
     queryset = UserModel.objects.filter(
-        is_active=True,
-        posts__gt=0
+        is_active=True, posts__gt=0
     ).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'))
 
     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
         urlpatterns.namespace(r'^accounts/', 'accounts', 'users')
-        urlpatterns.patterns('users:accounts',
+        urlpatterns.patterns(
+            'users:accounts',
             url(r'^$', 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'^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-account/(?P<pk>\d+)/$', DeleteAccountStep.as_view(), name='delete-account'),
+            url(
+                r'^delete-account/(?P<pk>\d+)/$',
+                DeleteAccountStep.as_view(),
+                name='delete-account'
+            ),
         )
 
         # Ranks
         urlpatterns.namespace(r'^ranks/', 'ranks', 'users')
-        urlpatterns.patterns('users:ranks',
+        urlpatterns.patterns(
+            'users:ranks',
             url(r'^$', RanksList.as_view(), name='index'),
             url(r'^new/$', NewRank.as_view(), name='new'),
             url(r'^edit/(?P<pk>\d+)/$', EditRank.as_view(), name='edit'),
@@ -46,7 +56,8 @@ class MisagoAdminExtension(object):
 
         # Bans
         urlpatterns.namespace(r'^bans/', 'bans', 'users')
-        urlpatterns.patterns('users:bans',
+        urlpatterns.patterns(
+            'users:bans',
             url(r'^$', BansList.as_view(), name='index'),
             url(r'^(?P<page>\d+)/$', BansList.as_view(), name='index'),
             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
 will attempt to authenticate new user
 """
+
+
 @api_view(['POST'])
-@permission_classes((UnbannedAnonOnly,))
+@permission_classes((UnbannedAnonOnly, ))
 @csrf_protect
 def login(request):
     form = AuthenticationForm(request, data=request.data)
@@ -42,13 +44,14 @@ def login(request):
         auth.login(request, form.user_cache)
         return Response(AuthenticatedUserSerializer(form.user_cache).data)
     else:
-        return Response(form.get_errors_dict(),
-                        status=status.HTTP_400_BAD_REQUEST)
+        return Response(form.get_errors_dict(), status=status.HTTP_400_BAD_REQUEST)
 
 
 """
 GET /auth/ will return current auth user, either User or AnonymousUser
 """
+
+
 @api_view()
 def session_user(request):
     if request.user.is_authenticated:
@@ -62,6 +65,8 @@ def session_user(request):
 """
 GET /auth/criteria/ will return password and username criteria for accounts
 """
+
+
 @api_view(['GET'])
 def get_criteria(request):
     criteria = {
@@ -73,9 +78,7 @@ def get_criteria(request):
     }
 
     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', {}))
 
@@ -88,8 +91,10 @@ def get_criteria(request):
 POST /auth/send-activation/ with CSRF token and email
 will mail account activation link to requester
 """
+
+
 @api_view(['POST'])
-@permission_classes((UnbannedAnonOnly,))
+@permission_classes((UnbannedAnonOnly, ))
 @csrf_protect
 def send_activation(request):
     form = ResendActivationForm(request.data)
@@ -103,25 +108,24 @@ def send_activation(request):
         }
         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:
-        return Response(form.get_errors_dict(),
-                        status=status.HTTP_400_BAD_REQUEST)
+        return Response(form.get_errors_dict(), status=status.HTTP_400_BAD_REQUEST)
 
 
 """
 POST /auth/send-password-form/ with CSRF token and email
 will mail change password form link to requester
 """
+
+
 @api_view(['POST'])
-@permission_classes((UnbannedOnly,))
+@permission_classes((UnbannedOnly, ))
 @csrf_protect
 def send_password_form(request):
     form = ResetPasswordForm(request.data)
@@ -137,29 +141,28 @@ def send_password_form(request):
 
         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:
-        return Response(form.get_errors_dict(),
-                        status=status.HTTP_400_BAD_REQUEST)
+        return Response(form.get_errors_dict(), status=status.HTTP_400_BAD_REQUEST)
 
 
 """
 POST /auth/change-password/user/token/ with CSRF and new password
 will change forgotten password
 """
+
+
 class PasswordChangeFailed(Exception):
     pass
 
 
 @api_view(['POST'])
-@permission_classes((UnbannedOnly,))
+@permission_classes((UnbannedOnly, ))
 @csrf_protect
 def change_forgotten_password(request, pk, token):
     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):
             raise PasswordChangeFailed(expired_message)
     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:
         new_password = request.data.get('password', '').strip()
@@ -191,10 +192,6 @@ def change_forgotten_password(request, pk, token):
         user.set_password(new_password)
         user.save()
     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)
         if 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)
 
     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):
     if request.user.is_avatar_locked:
         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:
             reason = None
 
         return Response({
             'detail': _("Your avatar is locked. You can't change it."),
             'reason': reason
-        }, status=status.HTTP_403_FORBIDDEN)
+        },
+                        status=status.HTTP_403_FORBIDDEN)
 
     avatar_options = get_avatar_options(request.user)
     if request.method == 'POST':
@@ -50,14 +50,8 @@ def get_avatar_options(user):
         for gallery in avatars.gallery.get_available_galleries():
             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?
     if not settings.allow_custom_avatars:
@@ -104,20 +98,17 @@ def avatar_post(options, user, data):
         if not type_options:
             return Response({
                 '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')]
     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:
         response_dict = {'detail': rpc_handler(user, data)}
     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()
 
@@ -128,6 +119,8 @@ def avatar_post(options, user, data):
 """
 Avatar rpc handlers
 """
+
+
 def avatar_generate(user, data):
     avatars.dynamic.set_avatar(user)
     return _("New avatar based on your account was set.")
@@ -138,8 +131,7 @@ def avatar_gravatar(user, data):
         avatars.gravatar.set_avatar(user)
         return _("Gravatar was downloaded and set as new avatar.")
     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:
         raise AvatarError(_("Failed to connect to Gravatar servers."))
 
@@ -182,8 +174,7 @@ def avatar_crop_tmp(user, data):
 
 def avatar_crop(user, data, suffix):
     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)
     except ValidationError as e:
         raise AvatarError(e.args[0])
@@ -194,7 +185,6 @@ AVATAR_TYPES = {
     'gravatar': avatar_gravatar,
     'galleries': avatar_gallery,
     'upload': avatar_upload,
-
     'crop_src': avatar_crop_src,
     '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):
-    serializer = ChangeEmailSerializer(
-        data=request.data,
-        context={
-            'user': request.user
-        }
-    )
+    serializer = ChangeEmailSerializer(data=request.data, context={'user': request.user})
 
     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 = 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
         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.")
         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):
-    serializer = ChangePasswordSerializer(
-        data=request.data,
-        context={
-            'user': request.user
-        }
-    )
+    serializer = ChangePasswordSerializer(data=request.data, context={'user': request.user})
 
     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 = 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:
         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 = {}
     if settings.account_activation == 'user':
-        activation_kwargs = {
-            'requires_activation': UserModel.ACTIVATION_USER
-        }
+        activation_kwargs = {'requires_activation': UserModel.ACTIVATION_USER}
     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(
         form.cleaned_data['username'],
@@ -55,12 +51,11 @@ def create_endpoint(request):
 
     if settings.account_activation == 'none':
         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)
 
-        mail_user(request, new_user, mail_subject,
-                  'misago/emails/register/complete')
+        mail_user(request, new_user, mail_subject, 'misago/emails/register/complete')
 
         return Response({
             'activation': 'active',
@@ -74,13 +69,12 @@ def create_endpoint(request):
         activation_by_user = new_user.requires_activation_by_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_by_admin': activation_by_admin,
                 'activation_by_user': activation_by_user,
-            })
+            }
+        )
 
         if activation_by_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))
     if page == 1:
-        page = 0 # api allows explicit first page
+        page = 0  # api allows explicit first page
 
     users = RankUsers(request, rank, page)
     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.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.signatures import is_user_signature_valid, set_user_signature
 
 
 def signature_endpoint(request):
     user = request.user
 
     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.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:
             reason = None
 
         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':
         return edit_signature(request, user)
@@ -57,13 +55,11 @@ def get_signature_options(user):
 def edit_signature(request, user):
     serializer = EditSignatureSerializer(user, data=request.data)
     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)
     else:
         return Response({
             '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 misago.conf import settings
-from misago.users.serializers import ChangeUsernameSerializer
 from misago.users.namechanges import UsernameChanges
+from misago.users.serializers import ChangeUsernameSerializer
 
 
 def username_endpoint(request):
@@ -39,14 +39,9 @@ def change_username(request):
             'detail': _("You can't change your username now."),
             '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():
         try:
@@ -60,21 +55,17 @@ def change_username(request):
             return Response({
                 'detail': _("Error changing username. Please try again."),
             },
-            status=status.HTTP_400_BAD_REQUEST)
+                            status=status.HTTP_400_BAD_REQUEST)
     else:
         return Response({
             '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):
     if request.method == 'POST':
-        serializer = ChangeUsernameSerializer(
-            data=request.data,
-            context={
-                'user': profile
-            }
-        )
+        serializer = ChangeUsernameSerializer(data=request.data, context={'user': profile})
 
         if serializer.is_valid():
             try:
@@ -86,11 +77,13 @@ def moderate_username_endpoint(request, profile):
             except IntegrityError:
                 return Response({
                     'detail': _("Error changing username. Please try again."),
-                }, status=status.HTTP_400_BAD_REQUEST)
+                },
+                                status=status.HTTP_400_BAD_REQUEST)
         else:
             return Response({
                 'detail': serializer.errors['non_field_errors'][0]
-            }, status=status.HTTP_400_BAD_REQUEST)
+            },
+                            status=status.HTTP_400_BAD_REQUEST)
     else:
         return Response({
             '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:
             return True
         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
 
 
 class UsernameChangesViewSet(viewsets.GenericViewSet):
-    permission_classes = (UsernameChangesViewSetPermission,)
+    permission_classes = (UsernameChangesViewSetPermission, )
     serializer_class = UsernameChangeSerializer
 
     def get_queryset(self):
@@ -41,16 +40,15 @@ class UsernameChangesViewSet(viewsets.GenericViewSet):
 
         if 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'):
             search_phrase = self.request.query_params.get('search').strip()
             if search_phrase:
                 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')
@@ -58,15 +56,13 @@ class UsernameChangesViewSet(viewsets.GenericViewSet):
     def list(self, request):
         page = get_int_or_404(request.GET.get('page', 0))
         if page == 1:
-            page = 0 # api allows explicit first page
+            page = 0  # api allows explicit first page
 
         queryset = self.get_queryset()
 
         list_page = paginate(queryset, page, 12, 4)
 
         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)

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

@@ -54,8 +54,8 @@ def allow_self_only(user, pk, message):
 
 
 class UserViewSet(viewsets.GenericViewSet):
-    permission_classes = (UserViewSetPermission,)
-    parser_classes=(FormParser, JSONParser, MultiPartParser)
+    permission_classes = (UserViewSetPermission, )
+    parser_classes = (FormParser, JSONParser, MultiPartParser)
     queryset = UserModel.objects
 
     def get_queryset(self):
@@ -104,9 +104,7 @@ class UserViewSet(viewsets.GenericViewSet):
         serializer = ForumOptionsSerializer(request.user, data=request.data)
         if serializer.is_valid():
             serializer.save()
-            return Response({
-                'detail': _("Your forum options have been changed.")
-            })
+            return Response({'detail': _("Your forum options have been changed.")})
         else:
             return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
 
@@ -164,10 +162,7 @@ class UserViewSet(viewsets.GenericViewSet):
             profile.save(update_fields=['followers'])
             request.user.save(update_fields=['following'])
 
-            return Response({
-                'is_followed': followed,
-                'followers': profile_followers
-            })
+            return Response({'is_followed': followed, 'followers': profile_followers})
 
     @detail_route()
     def ban(self, request, pk=None):
@@ -213,7 +208,9 @@ class UserViewSet(viewsets.GenericViewSet):
                         categories_to_sync.add(thread.category_id)
                         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():
                         categories_to_sync.add(post.category_id)
                         hide_post(request.user, post)
@@ -235,7 +232,7 @@ class UserViewSet(viewsets.GenericViewSet):
 
         page = get_int_or_404(request.query_params.get('page', 0))
         if page == 1:
-            page = 0 # api allows explicit first page
+            page = 0  # api allows explicit first page
 
         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))
         if page == 1:
-            page = 0 # api allows explicit first page
+            page = 0  # api allows explicit first page
 
         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))
         if page == 1:
-            page = 0 # api allows explicit first page
+            page = 0  # api allows explicit first 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))
         if page == 1:
-            page = 0 # api allows explicit first page
+            page = 0  # api allows explicit first page
 
         feed = UserPosts(request, profile, page)
 
@@ -283,7 +280,7 @@ class UserViewSet(viewsets.GenericViewSet):
 
 
 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(
             link='misago:users-active-posters',
             component='active-posters',
-            name=_('Active posters'))
+            name=_('Active posters')
+        )
 
     def register_default_user_profile_pages(self):
         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
 
-
 AVATAR_TYPES = ('gravatar', 'dynamic', 'gallery', 'uploaded')
 
-
 SET_DEFAULT_AVATAR = {
     'gravatar': gravatar.set_avatar,
     'dynamic': dynamic.set_avatar,

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

@@ -22,6 +22,8 @@ def set_avatar(user):
 """
 Default drawer
 """
+
+
 def draw_default(user):
     image_size = max(settings.MISAGO_AVATARS_SIZES)
 
@@ -32,11 +34,11 @@ def draw_default(user):
     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)
 
 
@@ -66,8 +68,7 @@ def draw_avatar_flavour(user, image):
     font = ImageFont.truetype(FONT_FILE, size=size)
 
     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.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
 
         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])
 
@@ -61,10 +58,10 @@ def load_avatar_galleries():
 
         for image in images:
             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
 
 

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

@@ -39,11 +39,10 @@ def store_avatar(user, image):
         image = image.resize((size, size), Image.ANTIALIAS)
         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.save(update_fields=['avatars'])
@@ -80,5 +79,4 @@ def upload_to(instance, filename):
     secret = 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)
     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})
 
     if image.size[0] * image.size[1] > 2000 * 3000:
@@ -82,7 +81,6 @@ def clean_crop(image, crop):
         crop_dict = {
             'x': float(crop['offset']['x']),
             'y': float(crop['offset']['y']),
-
             'zoom': float(crop['zoom']),
         }
     except (KeyError, TypeError, ValueError):
@@ -131,8 +129,7 @@ def crop_source_image(user, source, crop):
     else:
         upscale = 1.0 / crop['zoom']
         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['y'] - min_size) * upscale * -1, 0)),
         ))

+ 8 - 13
misago/users/bans.py

@@ -1,4 +1,3 @@
-
 """
 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)
 
     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.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
 boilerplate that caches ban check result in session
 """
+
+
 def get_request_ip_ban(request):
     session_ban_cache = _get_session_bancache(request)
     if session_ban_cache:
@@ -113,10 +111,7 @@ def get_request_ip_ban(request):
         else:
             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
         return _hydrate_session_cache(request.session[CACHE_SESSION_KEY])
     else:
@@ -156,8 +151,9 @@ def _hydrate_session_cache(ban_cache):
 """
 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:
         expires_on = timezone.now() + timedelta(**length)
 
@@ -171,8 +167,7 @@ def ban_user(user, user_message=None, staff_message=None, length=None,
     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:
         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):
-    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:
         response_json = r.json()
@@ -32,7 +35,7 @@ def qacaptcha_test(request):
 
 
 def nocaptcha_test(request):
-    return # no captcha means no validation
+    return  # no captcha means no validation
 
 
 CAPTCHA_TESTS = {
@@ -41,5 +44,6 @@ CAPTCHA_TESTS = {
     'no': nocaptcha_test,
 }
 
+
 def test_request(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_ACTIVATION_URL': reverse('misago:request-activation'),
             'FORGOTTEN_PASSWORD_URL': reverse('misago:forgotten-password'),
-
             'BANNED_URL': reverse('misago:banned'),
-
             'USERCP_URL': reverse('misago:options'),
             'USERS_LIST_URL': reverse('misago:users'),
-
             'AUTH_API': reverse('misago:api:auth'),
             'AUTH_CRITERIA_API': reverse('misago:api:auth-criteria'),
             'USERS_API': reverse('misago:api:user-list'),
-
             'CAPTCHA_API': reverse('misago:api:captcha-question'),
             'USERNAME_CHANGES_API': reverse('misago:api:usernamechange-list'),
         })
@@ -39,12 +35,8 @@ def preload_user_json(request):
     })
 
     if request.user.is_authenticated:
-        request.frontend_context.update({
-            'user': AuthenticatedUserSerializer(request.user).data
-        })
+        request.frontend_context.update({'user': AuthenticatedUserSerializer(request.user).data})
     else:
-        request.frontend_context.update({
-            'user': AnonymousUserSerializer(request.user).data
-        })
+        request.frontend_context.update({'user': AnonymousUserSerializer(request.user).data})
 
     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):
     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 decorator(request, *args, **kwargs):
         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:
             return f(request, *args, **kwargs)
+
     return decorator
 
 
 def deny_guests(f):
     def decorator(request, *args, **kwargs):
         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:
             return f(request, *args, **kwargs)
+
     return decorator
 
 
@@ -32,11 +32,10 @@ def deny_banned_ips(f):
         ban = get_request_ip_ban(request)
         if 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)
         else:
             return f(request, *args, **kwargs)
+
     return decorator

+ 11 - 34
misago/users/djangoadmin.py

@@ -79,48 +79,25 @@ class UserAdmin(admin.ModelAdmin):
     that).
     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')
     list_filter = ('groups', 'is_staff', 'is_superuser')
 
     form = UserAdminForm
     actions = None
     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 permissions and groups'),
-            {'fields': (
-                'groups',
-                'user_permissions',
-            )},
-        ),
-    )
+            )
+        },
+    ), (_('Edit permissions and groups'), {
+        'fields': ('groups', 'user_permissions', )
+    }, ), )
 
     def has_add_permission(self, request):
         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()
-
-
 """
 Users
 """
+
+
 class UserBaseForm(forms.ModelForm):
     username = forms.CharField(label=_("Username"))
     title = forms.CharField(label=_("Custom title"), required=False)
@@ -58,10 +58,7 @@ class UserBaseForm(forms.ModelForm):
 
 
 class NewUserForm(UserBaseForm):
-    new_password = forms.CharField(
-        label=_("Password"),
-        widget=forms.PasswordInput
-    )
+    new_password = forms.CharField(label=_("Password"), widget=forms.PasswordInput)
 
     class Meta:
         model = UserModel
@@ -90,16 +87,14 @@ class EditUserForm(UserBaseForm):
         "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 "
         "why user's account has been disabled."
     )
 
     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(
@@ -130,9 +125,7 @@ class EditUserForm(UserBaseForm):
     )
 
     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(
         label=_("Lock signature"),
@@ -143,17 +136,13 @@ class EditUserForm(UserBaseForm):
     )
     signature_lock_user_message = forms.CharField(
         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}),
         required=False
     )
     signature_lock_staff_message = forms.CharField(
         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}),
         required=False
     )
@@ -167,14 +156,10 @@ class EditUserForm(UserBaseForm):
     )
 
     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(
-        label=_("Replid threads"),
-        coerce=int,
-        choices=UserModel.SUBSCRIBE_CHOICES
+        label=_("Replid threads"), coerce=int, choices=UserModel.SUBSCRIBE_CHOICES
     )
 
     class Meta:
@@ -201,11 +186,12 @@ class EditUserForm(UserBaseForm):
 
         length_limit = settings.signature_length_max
         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
 
@@ -227,55 +213,56 @@ def UserFormFactory(FormType, instance):
 
     extra_fields['roles'] = forms.ModelMultipleChoiceField(
         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,
         initial=instance.roles.all() if instance.pk else None,
         widget=forms.CheckboxSelectMultiple
     )
 
-    return type('UserFormFinal', (FormType,), extra_fields)
+    return type('UserFormFinal', (FormType, ), extra_fields)
 
 
 def StaffFlagUserFormFactory(FormType, instance):
     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):
     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)
 
     if add_is_active_fields:
@@ -296,12 +283,10 @@ class SearchUsersFormBase(forms.Form):
 
     def filter_queryset(self, criteria, queryset):
         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'):
-            queryset = queryset.filter(
-                email__istartswith=criteria.get('email'))
+            queryset = queryset.filter(email__istartswith=criteria.get('email'))
 
         if 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)
 
     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)
 
 
 """
 Ranks
 """
+
+
 class RankForm(forms.ModelForm):
     name = forms.CharField(
         label=_("Name"),
@@ -401,9 +383,7 @@ class RankForm(forms.ModelForm):
     css_class = forms.CharField(
         label=_("CSS class"),
         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(
         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)
 
         if unique_qs.exists():
-            raise forms.ValidationError(
-                _("This name collides with other rank."))
+            raise forms.ValidationError(_("This name collides with other rank."))
 
         return data
 
@@ -444,18 +423,16 @@ class RankForm(forms.ModelForm):
 """
 Bans
 """
+
+
 class BanUsersForm(forms.Form):
     ban_type = forms.MultipleChoiceField(
         label=_("Values to ban"),
         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(
         label=_("User message"),
@@ -463,9 +440,7 @@ class BanUsersForm(forms.Form):
         max_length=1000,
         help_text=_("Optional message displayed to users instead of default one."),
         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(
         label=_("Team message"),
@@ -473,9 +448,7 @@ class BanUsersForm(forms.Form):
         max_length=1000,
         help_text=_("Optional ban message for moderators and administrators."),
         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(
         label=_("Expires on"),
@@ -485,11 +458,7 @@ class BanUsersForm(forms.Form):
 
 
 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(
         label=_("Banned value"),
         max_length=250,
@@ -498,10 +467,8 @@ class BanForm(forms.ModelForm):
             'for rought matches. For example, making IP ban for value '
             '"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(
         label=_("User message"),
@@ -509,9 +476,7 @@ class BanForm(forms.ModelForm):
         max_length=1000,
         help_text=_("Optional message displayed to user instead of default one."),
         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(
         label=_("Team message"),
@@ -519,9 +484,7 @@ class BanForm(forms.ModelForm):
         max_length=1000,
         help_text=_("Optional ban message for moderators and administrators."),
         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(
         label=_("Expires on"),
@@ -551,30 +514,15 @@ class BanForm(forms.ModelForm):
 
 
 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(
         label=_("State"),
         required=False,
-        choices=(
-            ('', _('Any')),
-            ('used', _('Active')),
-            ('unused', _('Expired')),
-        )
+        choices=(('', _('Any')), ('used', _('Active')), ('unused', _('Expired')), )
     )
 
     def filter_queryset(self, search_criteria, queryset):
@@ -589,8 +537,7 @@ class SearchBansForm(forms.Form):
             queryset = queryset.filter(check_type=2)
 
         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':
             queryset = queryset.filter(is_checked=True)

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

@@ -13,23 +13,27 @@ UserModel = get_user_model()
 
 class MisagoAuthMixin(object):
     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):
         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:
-            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):
         if not user.is_staff:
@@ -44,10 +48,7 @@ class MisagoAuthMixin(object):
         else:
             error.message = error.messages[0]
 
-        return {
-            'detail': error.message,
-            'code': error.code
-        }
+        return {'detail': error.message, 'code': error.code}
 
 
 class AuthenticationForm(MisagoAuthMixin, BaseAuthenticationForm):
@@ -55,33 +56,22 @@ class AuthenticationForm(MisagoAuthMixin, BaseAuthenticationForm):
     Base class for authenticating users, Floppy-forms and
     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):
         username = self.cleaned_data.get('username')
         password = self.cleaned_data.get('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:
-                raise ValidationError(
-                    self.error_messages['invalid_login'], code='invalid_login')
+                raise ValidationError(self.error_messages['invalid_login'], code='invalid_login')
             else:
                 self.confirm_login_allowed(self.user_cache)
         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
 
@@ -94,16 +84,13 @@ class AdminAuthenticationForm(AuthenticationForm):
     required_css_class = 'required'
 
     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)
 
     def confirm_login_allowed(self, user):
         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):
@@ -114,14 +101,12 @@ class GetUserForm(MisagoAuthMixin, forms.Form):
 
         email = data.get('email')
         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:
             validate_email(email)
         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:
             user = UserModel.objects.get_by_email(data['email'])
@@ -129,8 +114,7 @@ class GetUserForm(MisagoAuthMixin, forms.Form):
                 raise UserModel.DoesNotExist()
             self.user_cache = user
         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)
 
@@ -146,22 +130,26 @@ class ResendActivationForm(GetUserForm):
 
         if not user.requires_activation:
             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:
             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):
     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):

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

@@ -27,24 +27,43 @@ class Command(BaseCommand):
     help = 'Used to create a superuser.'
 
     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):
         self.stdin = options.get('stdin', sys.stdin)  # Used for testing
@@ -114,15 +133,11 @@ class Command(BaseCommand):
                 while not password:
                     try:
                         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()
                         if raw_value != repeat_raw_value:
-                            raise ValidationError(
-                                "Entered passwords are different.")
+                            raise ValidationError("Entered passwords are different.")
                         password = raw_value
                     except ValidationError as e:
                         self.stderr.write(e.messages[0])
@@ -143,7 +158,8 @@ class Command(BaseCommand):
     def create_superuser(self, username, email, password, verbosity):
         try:
             user = UserModel.objects.create_superuser(
-                username, email, password, set_default_avatar=True)
+                username, email, password, set_default_avatar=True
+            )
 
             if verbosity >= 1:
                 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()
         for user in batch_update(UserModel.objects.all()):
             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()
             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()
             user.followers = user.followed_by.count()
             user.following = user.follows.count()

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

@@ -22,34 +22,94 @@ class Migration(migrations.Migration):
         migrations.CreateModel(
             name='User',
             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')),
-                ('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)),
                 ('slug', models.CharField(unique=True, max_length=30)),
                 ('email', models.EmailField(max_length=255, db_index=True)),
                 ('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()),
                 ('last_ip', models.GenericIPAddressField(null=True, blank=True)),
                 ('is_hiding_presence', models.BooleanField(default=False)),
                 ('title', models.CharField(max_length=255, null=True, blank=True)),
                 ('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)),
-                ('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)),
-                ('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')),
-                ('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)),
                 ('avatars', JSONField(null=True, blank=True)),
                 ('is_avatar_locked', models.BooleanField(default=False)),
@@ -75,7 +135,7 @@ class Migration(migrations.Migration):
             options={
                 'abstract': False,
             },
-            bases=(models.Model,),
+            bases=(models.Model, ),
         ),
         CreatePartialIndex(
             field='User.is_staff',
@@ -92,32 +152,57 @@ class Migration(migrations.Migration):
             fields=[
                 ('current_ip', models.GenericIPAddressField()),
                 ('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(
             name='UsernameChange',
             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_on', models.DateTimeField(default=django.utils.timezone.now)),
                 ('new_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={
                 'get_latest_by': b'changed_on',
             },
-            bases=(models.Model,),
+            bases=(models.Model, ),
         ),
         migrations.CreateModel(
             name='Rank',
             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)),
                 ('slug', models.CharField(unique=True, max_length=255)),
                 ('description', models.TextField(null=True, blank=True)),
@@ -131,12 +216,18 @@ class Migration(migrations.Migration):
             options={
                 'get_latest_by': b'order',
             },
-            bases=(models.Model,),
+            bases=(models.Model, ),
         ),
         migrations.AddField(
             model_name='user',
             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,
         ),
         migrations.AddField(
@@ -154,29 +245,52 @@ class Migration(migrations.Migration):
         migrations.CreateModel(
             name='ActivityRanking',
             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)),
                 ('score', models.PositiveIntegerField(default=0, db_index=True)),
             ],
-            options={
-            },
-            bases=(models.Model,),
+            options={},
+            bases=(models.Model, ),
         ),
         migrations.CreateModel(
             name='Avatar',
             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)),
-                ('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(
             name='AvatarGallery',
             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)),
-                ('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={
                 'ordering': ['gallery', 'pk'],
@@ -185,7 +299,11 @@ class Migration(migrations.Migration):
         migrations.CreateModel(
             name='Ban',
             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)),
                 ('banned_value', models.CharField(max_length=255, db_index=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)),
                 ('is_checked', models.BooleanField(default=True, db_index=True)),
             ],
-            bases=(models.Model,),
+            bases=(models.Model, ),
         ),
         migrations.CreateModel(
             name='BanCache',
@@ -202,11 +320,24 @@ class Migration(migrations.Migration):
                 ('staff_message', models.TextField(null=True, blank=True)),
                 ('bans_version', models.PositiveIntegerField(default=0)),
                 ('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):
-    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',
                 'name': _("New accounts activation"),
                 'legend': _("New accounts"),
                 'value': 'none',
                 'form_field': 'select',
                 'field_extra': {
-                    'choices': (
-                        ('none', _("No activation required")),
-                        ('user', _("Activation token sent to User")),
-                        ('admin', _("Activation by administrator")),
-                        ('closed', _("Don't allow new registrations"))
-                    )
+                    'choices': (('none', _("No activation required")),
+                                ('user', _("Activation token sent to User")),
+                                ('admin', _("Activation by administrator")),
+                                ('closed', _("Don't allow new registrations")))
                 },
                 'is_public': True,
-            },
-            {
+            }, {
                 'setting': 'username_length_min',
                 'name': _("Minimum length"),
                 'description': _("Minimum allowed username length."),
@@ -43,8 +45,7 @@ def create_users_settings_group(apps, schema_editor):
                     'max_value': 20,
                 },
                 'is_public': True,
-            },
-            {
+            }, {
                 'setting': 'username_length_max',
                 'name': _("Maximum length"),
                 'description': _("Maximum allowed username length."),
@@ -55,8 +56,7 @@ def create_users_settings_group(apps, schema_editor):
                     'max_value': 20,
                 },
                 'is_public': True,
-            },
-            {
+            }, {
                 'setting': 'password_length_min',
                 'name': _("Minimum length"),
                 'description': _("Minimum allowed user password length."),
@@ -68,48 +68,55 @@ def create_users_settings_group(apps, schema_editor):
                     'max_value': 255,
                 },
                 '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',
                 'name': _("Default avatar"),
                 'value': 'gravatar',
                 'form_field': 'select',
                 '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',
                 'name': _("Maximum size of uploaded avatar"),
                 'description': _("Enter maximum allowed file size "
@@ -120,8 +127,7 @@ def create_users_settings_group(apps, schema_editor):
                     'min_value': 0,
                 },
                 'is_public': True,
-            },
-            {
+            }, {
                 'setting': 'signature_length_max',
                 'name': _("Maximum length"),
                 'legend': _("Signatures"),
@@ -133,63 +139,62 @@ def create_users_settings_group(apps, schema_editor):
                     'max_value': 5000,
                 },
                 'is_public': True,
-            },
-            {
+            }, {
                 'setting': 'subscribe_start',
                 'name': _("Started threads"),
                 'legend': _("Default subscriptions settings"),
                 'value': 'watch_email',
                 'form_field': 'select',
                 'field_extra': {
-                    'choices': (
-                        ('no', _("Don't watch")),
-                        ('watch', _("Put on watched threads list")),
-                        ('watch_email', _("Put on watched threads "
-                                          "list and e-mail user when "
-                                          "somebody replies")),
-                    ),
+                    'choices': (('no', _("Don't watch")),
+                                ('watch', _("Put on watched threads list")), (
+                                    'watch_email', _(
+                                        "Put on watched threads "
+                                        "list and e-mail user when "
+                                        "somebody replies"
+                                    )
+                                ), ),
                 },
-            },
-            {
+            }, {
                 'setting': 'subscribe_reply',
                 'name': _("Replied threads"),
                 'value': 'watch_email',
                 'form_field': 'select',
                 'field_extra': {
-                    'choices': (
-                        ('no', _("Don't watch")),
-                        ('watch', _("Put on watched threads list")),
-                        ('watch_email', _("Put on watched threads "
-                                          "list and e-mail user when "
-                                          "somebody replies")),
-                    ),
+                    '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',
                 'name': _("Select CAPTCHA type"),
                 'legend': _("CAPTCHA type"),
                 'value': 'no',
                 'form_field': 'select',
                 '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,
-            },
-            {
+            }, {
                 'setting': 'recaptcha_site_key',
                 'name': _("Site key"),
                 'legend': _("reCAPTCHA"),
@@ -199,8 +204,7 @@ def create_users_settings_group(apps, schema_editor):
                     'max_length': 100,
                 },
                 'is_public': True,
-            },
-            {
+            }, {
                 'setting': 'recaptcha_secret_key',
                 'name': _("Secret key"),
                 'value': '',
@@ -208,8 +212,7 @@ def create_users_settings_group(apps, schema_editor):
                     'required': False,
                     'max_length': 100,
                 },
-            },
-            {
+            }, {
                 'setting': 'qa_question',
                 'name': _("Test question"),
                 'legend': _("Question and answer"),
@@ -218,8 +221,7 @@ def create_users_settings_group(apps, schema_editor):
                     'required': False,
                     'max_length': 250,
                 },
-            },
-            {
+            }, {
                 'setting': 'qa_help_text',
                 'name': _("Question help text"),
                 'value': '',
@@ -227,22 +229,26 @@ def create_users_settings_group(apps, schema_editor):
                     'required': False,
                     'max_length': 250,
                 },
-            },
-            {
-                'setting': 'qa_answers',
-                'name': _("Valid answers"),
-                'description': _("Enter each answer in new line. "
-                                 "Answers are case-insensitive."),
-                'value': '',
-                'form_field': 'textarea',
+            }, {
+                'setting':
+                    'qa_answers',
+                'name':
+                    _("Valid answers"),
+                'description':
+                    _("Enter each answer in new line. "
+                      "Answers are case-insensitive."),
+                'value':
+                    '',
+                'form_field':
+                    'textarea',
                 'field_extra': {
                     'rows': 4,
                     'required': False,
                     'max_length': 250,
                 },
-            },
-        )
-    })
+            }, )
+        }
+    )
 
 
 class Migration(migrations.Migration):

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

@@ -20,10 +20,7 @@ def create_default_ranks(apps, schema_editor):
     )
 
     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')

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

@@ -28,6 +28,13 @@ class Migration(migrations.Migration):
         migrations.AlterField(
             model_name='user',
             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):
-    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',
                 'name': _("New accounts activation"),
                 'legend': _("New accounts"),
                 'value': 'none',
                 'form_field': 'select',
                 'field_extra': {
-                    'choices': (
-                        ('none', _("No activation required")),
-                        ('user', _("Activation token sent to User")),
-                        ('admin', _("Activation by administrator")),
-                        ('closed', _("Don't allow new registrations"))
-                    )
+                    'choices': (('none', _("No activation required")),
+                                ('user', _("Activation token sent to User")),
+                                ('admin', _("Activation by administrator")),
+                                ('closed', _("Don't allow new registrations")))
                 },
                 'is_public': True,
-            },
-            {
+            }, {
                 'setting': 'username_length_min',
                 'name': _("Minimum length"),
                 'description': _("Minimum allowed username length."),
@@ -43,8 +45,7 @@ def update_users_settings(apps, schema_editor):
                     'min_value': 2,
                     'max_value': 20,
                 },
-            },
-            {
+            }, {
                 'setting': 'username_length_max',
                 'name': _("Maximum length"),
                 'description': _("Maximum allowed username length."),
@@ -54,48 +55,55 @@ def update_users_settings(apps, schema_editor):
                     'min_value': 2,
                     'max_value': 20,
                 },
-            },
-            {
-                'setting': 'allow_custom_avatars',
-                'name': _("Allow custom avatars"),
-                'legend': _("Avatars"),
-                'description': _("Turning this option off will forbid "
-                                 "forum users from using avatars from "
-                                 "outside forums. Good for forums "
-                                 "adressed at young users."),
-                'python_type': 'bool',
-                'value': True,
-                'form_field': 'yesno',
-            },
-            {
+            }, {
+                'setting':
+                    'allow_custom_avatars',
+                'name':
+                    _("Allow custom avatars"),
+                'legend':
+                    _("Avatars"),
+                'description':
+                    _(
+                        "Turning this option off will forbid "
+                        "forum users from using avatars from "
+                        "outside forums. Good for forums "
+                        "adressed at young users."
+                    ),
+                'python_type':
+                    'bool',
+                'value':
+                    True,
+                'form_field':
+                    'yesno',
+            }, {
                 'setting': 'default_avatar',
                 'name': _("Default avatar"),
                 'value': 'gravatar',
                 'form_field': 'select',
                 'field_extra': {
-                    'choices': (
-                        ('dynamic', _("Individual")),
-                        ('gravatar', _("Gravatar")),
-                        ('gallery', _("Random avatar from gallery")),
-                    ),
+                    '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',
                 'name': _("Maximum size of uploaded avatar"),
                 'description': _("Enter maximum allowed file size "
@@ -106,8 +114,7 @@ def update_users_settings(apps, schema_editor):
                     'min_value': 0,
                 },
                 'is_public': True,
-            },
-            {
+            }, {
                 'setting': 'signature_length_max',
                 'name': _("Maximum length"),
                 'legend': _("Signatures"),
@@ -119,40 +126,40 @@ def update_users_settings(apps, schema_editor):
                     'max_value': 5000,
                 },
                 'is_public': True,
-            },
-            {
+            }, {
                 'setting': 'subscribe_start',
                 'name': _("Started threads"),
                 'legend': _("Default subscriptions settings"),
                 'value': 'watch_email',
                 'form_field': 'select',
                 'field_extra': {
-                    'choices': (
-                        ('no', _("Don't watch")),
-                        ('watch', _("Put on watched threads list")),
-                        ('watch_email', _("Put on watched threads "
-                                          "list and e-mail user when "
-                                          "somebody replies")),
-                    ),
+                    'choices': (('no', _("Don't watch")),
+                                ('watch', _("Put on watched threads list")), (
+                                    'watch_email', _(
+                                        "Put on watched threads "
+                                        "list and e-mail user when "
+                                        "somebody replies"
+                                    )
+                                ), ),
                 },
-            },
-            {
+            }, {
                 'setting': 'subscribe_reply',
                 'name': _("Replied threads"),
                 'value': 'watch_email',
                 'form_field': 'select',
                 'field_extra': {
-                    'choices': (
-                        ('no', _("Don't watch")),
-                        ('watch', _("Put on watched threads list")),
-                        ('watch_email', _("Put on watched threads "
-                                          "list and e-mail user when "
-                                          "somebody replies")),
-                    ),
+                    '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()
 

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

@@ -15,16 +15,22 @@ class Migration(migrations.Migration):
         migrations.AlterField(
             model_name='user',
             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(
             model_name='user',
             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(
             model_name='user',
             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):
-    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)
     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():
             if ban.is_expired:
                 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
-            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
             elif ban.check_type == self.model.IP and ip and ban.check_value(ip):
                 return ban
@@ -60,11 +58,7 @@ class Ban(models.Model):
     EMAIL = 1
     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)
     banned_value = models.CharField(max_length=255, db_index=True)
@@ -112,7 +106,9 @@ class Ban(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)
     bans_version = models.PositiveIntegerField(default=0)
     user_message = models.TextField(null=True, blank=True)
@@ -123,7 +119,7 @@ class BanCache(models.Model):
         try:
             super(BanCache, self).save(*args, **kwargs)
         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):
         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'
 
         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:
@@ -52,17 +52,10 @@ class UserManager(BaseUserManager):
             new_value = WATCH_DICT[settings.subscribe_reply]
             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()
-        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_email(email)
@@ -78,8 +71,9 @@ class UserManager(BaseUserManager):
         user.save(using=self._db)
 
         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:
             # just for test purposes
             user.avatars = [{'size': 400, 'url': '/placekitten.com/400/400'}]
@@ -101,9 +95,10 @@ class UserManager(BaseUserManager):
         return user
 
     @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,
             set_default_avatar=set_default_avatar,
         )
@@ -142,22 +137,16 @@ class User(AbstractBaseUser, PermissionsMixin):
     SUBSCRIBE_NOTIFY = 1
     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_FOLLOWED = 1
     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.
     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)
     requires_activation = models.PositiveIntegerField(default=ACTIVATION_NONE)
 
-    is_staff = models.BooleanField(_('staff status'),
+    is_staff = models.BooleanField(
+        _('staff status'),
         default=False,
         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)
 
     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(
-        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)
     avatars = JSONField(null=True, blank=True)
@@ -231,11 +215,13 @@ class User(AbstractBaseUser, PermissionsMixin):
     followers = models.PositiveIntegerField(default=0)
     following = models.PositiveIntegerField(default=0)
 
-    follows = models.ManyToManyField('self',
+    follows = models.ManyToManyField(
+        'self',
         related_name='followed_by',
         symmetrical=False,
     )
-    blocks = models.ManyToManyField('self',
+    blocks = models.ManyToManyField(
+        'self',
         related_name='blocked_by',
         symmetrical=False,
     )
@@ -248,12 +234,10 @@ class User(AbstractBaseUser, PermissionsMixin):
     sync_unread_private_threads = models.BooleanField(default=False)
 
     subscribe_to_started_threads = models.PositiveIntegerField(
-        default=SUBSCRIBE_NONE,
-        choices=SUBSCRIBE_CHOICES
+        default=SUBSCRIBE_NONE, choices=SUBSCRIBE_CHOICES
     )
     subscribe_to_replied_threads = models.PositiveIntegerField(
-        default=SUBSCRIBE_NONE,
-        choices=SUBSCRIBE_CHOICES
+        default=SUBSCRIBE_NONE, choices=SUBSCRIBE_CHOICES
     )
 
     threads = models.PositiveIntegerField(default=0)
@@ -330,10 +314,12 @@ class User(AbstractBaseUser, PermissionsMixin):
         return is_user_signature_valid(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):
         """
@@ -356,8 +342,7 @@ class User(AbstractBaseUser, PermissionsMixin):
 
             if self.pk:
                 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
                 username_changed.send(sender=self)
@@ -439,14 +424,15 @@ class Online(models.Model):
         try:
             super(Online, self).save(*args, **kwargs)
         except IntegrityError:
-            pass # first come is first serve in online tracker
+            pass  # first come is first serve in online tracker
 
 
 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',
         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_online': False,
         'is_offline': False,
-
         'banned_until': None,
         '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
 """
+
+
 class PermissionsForm(forms.Form):
     legend = _("Account settings")
 
     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(
         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,
         initial=0
     )
@@ -30,8 +32,10 @@ class PermissionsForm(forms.Form):
     allow_signature_images = YesNoSwitch(label=_("Can put images in signature"))
     allow_signature_blocks = YesNoSwitch(
         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
 """
+
+
 def build_acl(acl, roles, key_name):
     new_acl = {
         'name_changes_allowed': 0,
@@ -56,7 +62,10 @@ def build_acl(acl, roles, key_name):
     }
     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_expire=algebra.lower_non_zero,
         can_have_signature=algebra.greater,

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

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

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

@@ -16,11 +16,11 @@ __all__ = [
     'allow_delete_user',
     'can_delete_user',
 ]
-
-
 """
 Admin Permissions Form
 """
+
+
 class PermissionsForm(forms.Form):
     legend = _("Deleting users")
 
@@ -48,6 +48,8 @@ def change_permissions_form(role):
 """
 ACL Builder
 """
+
+
 def build_acl(acl, roles, key_name):
     new_acl = {
         'can_delete_users_newer_than': 0,
@@ -55,7 +57,10 @@ def build_acl(acl, roles, key_name):
     }
     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_with_less_posts_than=algebra.greater
     )
@@ -64,6 +69,8 @@ def build_acl(acl, roles, key_name):
 """
 ACL's for targets
 """
+
+
 def add_acl_to_user(user, target):
     target.acl['can_delete'] = can_delete_user(user, target)
     if target.acl['can_delete']:
@@ -77,6 +84,8 @@ def register_with(registry):
 """
 ACL tests
 """
+
+
 def allow_delete_user(user, target):
     newer_than = user.acl_cache['can_delete_users_newer_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 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)
     if less_posts_than:
         if target.posts > less_posts_than:
             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 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)
+
+
 can_delete_user = return_boolean(allow_delete_user)

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

@@ -26,11 +26,11 @@ __all__ = [
     'allow_lift_ban',
     'can_lift_ban',
 ]
-
-
 """
 Admin Permissions Form
 """
+
+
 class PermissionsForm(forms.Form):
     legend = _("Users moderation")
 
@@ -63,6 +63,8 @@ def change_permissions_form(role):
 """
 ACL Builder
 """
+
+
 def build_acl(acl, roles, key_name):
     new_acl = {
         'can_rename_users': 0,
@@ -75,7 +77,10 @@ def build_acl(acl, roles, key_name):
     }
     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_moderate_avatars=algebra.greater,
         can_moderate_signatures=algebra.greater,
@@ -89,6 +94,8 @@ def build_acl(acl, roles, key_name):
 """
 ACL's for targets
 """
+
+
 def add_acl_to_user(user, target):
     target.acl['can_rename'] = can_rename_user(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['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:
         if target.acl[permission]:
@@ -117,11 +119,15 @@ def register_with(registry):
 """
 ACL tests
 """
+
+
 def allow_rename_user(user, target):
     if not user.acl_cache['can_rename_users']:
         raise PermissionDenied(_("You can't rename users."))
     if not user.is_superuser and (target.is_staff or target.is_superuser):
         raise PermissionDenied(_("You can't rename administrators."))
+
+
 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."))
     if not user.is_superuser and (target.is_staff or target.is_superuser):
         raise PermissionDenied(_("You can't moderate administrators avatars."))
+
+
 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):
         message = _("You can't moderate administrators signatures.")
         raise PermissionDenied(message)
+
+
 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."))
     if target.is_staff or target.is_superuser:
         raise PermissionDenied(_("You can't ban administrators."))
+
+
 can_ban_user = return_boolean(allow_ban_user)
 
 
@@ -162,8 +174,9 @@ def allow_lift_ban(user, target):
         if not ban.valid_until:
             raise PermissionDenied(_("You can't lift permanent bans."))
         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)}
             raise PermissionDenied(message)
+
+
 can_lift_ban = return_boolean(allow_lift_ban)

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

@@ -21,22 +21,12 @@ __all__ = [
     'allow_see_ban_details',
     'can_see_ban_details',
 ]
-
-
 """
 Admin Permissions Form
 """
-CAN_BROWSE_USERS_LIST = YesNoSwitch(
-    label=_("Can browse users list"),
-    initial=1
-)
-CAN_SEARCH_USERS = YesNoSwitch(
-    label=_("Can search user profiles"),
-    initial=1
-)
-CAN_SEE_USER_NAME_HISTORY = YesNoSwitch(
-    label=_("Can see other members name history")
-)
+CAN_BROWSE_USERS_LIST = YesNoSwitch(label=_("Can browse users list"), initial=1)
+CAN_SEARCH_USERS = YesNoSwitch(label=_("Can search user profiles"), initial=1)
+CAN_SEE_USER_NAME_HISTORY = YesNoSwitch(label=_("Can see other members name history"))
 CAN_SEE_DETAILS = YesNoSwitch(
     label=_("Can see members bans details"),
     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):
     can_browse_users_list = CAN_BROWSE_USERS_LIST
     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_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):
@@ -89,6 +67,8 @@ def change_permissions_form(role):
 """
 ACL Builder
 """
+
+
 def build_acl(acl, roles, key_name):
     new_acl = {
         'can_browse_users_list': 0,
@@ -103,7 +83,10 @@ def build_acl(acl, roles, key_name):
     }
     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_search_users=algebra.greater,
         can_follow_users=algebra.greater,
@@ -119,16 +102,14 @@ def build_acl(acl, roles, key_name):
 """
 ACL's for targets
 """
+
+
 def add_acl_to_user(user, target):
     target.acl['can_have_attitude'] = False
     target.acl['can_follow'] = can_follow_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:
         if target.acl[permission]:
@@ -143,9 +124,13 @@ def register_with(registry):
 """
 ACL tests
 """
+
+
 def allow_browse_users_list(user):
     if not user.acl_cache['can_browse_users_list']:
         raise PermissionDenied(_("You can't 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."))
     if user.pk == target.pk:
         raise PermissionDenied(_("You can't add yourself to followed."))
+
+
 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:
         message = _("%(user)s can't be blocked.") % {'user': target.username}
         raise PermissionDenied(message)
+
+
 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):
     if not user.acl_cache['can_see_ban_details']:
         raise PermissionDenied(_("You can't see users bans 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):
         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):
         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:
             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):
     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):
         queryset = queryset.filter(is_active=True)
@@ -51,8 +45,9 @@ def search_users(**filters):
 
     # lets grab head and tail 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

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

@@ -32,12 +32,8 @@ class AuthenticatedUserSerializer(UserSerializer, AuthFlags):
     class Meta:
         model = UserModel
         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',
         )
 
@@ -49,21 +45,22 @@ class AuthenticatedUserSerializer(UserSerializer, AuthFlags):
 
     def get_api_url(self, obj):
         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(
-    '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):
     if message:
-        return {
-            'plain': message,
-            'html': format_plaintext_for_html(message)
-        }
+        return {'plain': message, 'html': format_plaintext_for_html(message)}
     else:
         return None
 

+ 8 - 7
misago/users/serializers/moderation.py

@@ -13,6 +13,7 @@ __all__ = [
     'ModerateSignatureSerializer',
 ]
 
+
 class ModerateAvatarSerializer(serializers.ModelSerializer):
     class Meta:
         model = UserModel
@@ -27,18 +28,18 @@ class ModerateSignatureSerializer(serializers.ModelSerializer):
     class Meta:
         model = UserModel
         fields = [
-            'signature',
-            'is_signature_locked',
-            'signature_lock_user_message',
+            'signature', 'is_signature_locked', 'signature_lock_user_message',
             'signature_lock_staff_message'
         ]
 
     def validate_signature(self, value):
         length_limit = settings.signature_length_max
         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

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

@@ -23,10 +23,8 @@ class ForumOptionsSerializer(serializers.ModelSerializer):
     class Meta:
         model = UserModel
         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 = {
             'limits_private_thread_invites_to': {
@@ -63,16 +61,14 @@ class ChangeUsernameSerializer(serializers.Serializer):
             raise serializers.ValidationError(_("Enter new 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)
 
         return data
 
     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'])
 
 

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

@@ -14,14 +14,7 @@ class RankSerializer(serializers.ModelSerializer):
     class Meta:
         model = Rank
         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',
         )
 

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

@@ -42,38 +42,17 @@ class UserSerializer(serializers.ModelSerializer, MutableFields):
     class Meta:
         model = UserModel
         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):
         return obj.acl
 
     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
         else:
             return None
@@ -110,21 +89,30 @@ class UserSerializer(serializers.ModelSerializer, MutableFields):
 
     def get_api_url(self, obj):
         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(
-    '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']
 
-
-UserSerializer = BaseUserSerializer.subset_fields(
-    'id', 'username', 'avatars', 'absolute_url')
+UserSerializer = BaseUserSerializer.subset_fields('id', 'username', 'avatars', 'absolute_url')
 
 
 class UsernameChangeSerializer(serializers.ModelSerializer):
@@ -19,11 +17,6 @@ class UsernameChangeSerializer(serializers.ModelSerializer):
     class Meta:
         model = UsernameChange
         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'
         )

+ 3 - 5
misago/users/signals.py

@@ -3,13 +3,11 @@ from django.dispatch import Signal, receiver
 
 delete_user_content = Signal()
 username_changed = Signal()
-
-
 """
 Signal handlers
 """
+
+
 @receiver(username_changed)
 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:
         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:
         user.signature_parsed = ''
         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):
         """activate banned user shows error"""
         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(
             check_type=Ban.USERNAME,
             banned_value='bob',
@@ -28,12 +29,16 @@ class ActivationViewsTests(TestCase):
 
         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)
         self.assertEqual(test_user.requires_activation, 1)
@@ -41,14 +46,20 @@ class ActivationViewsTests(TestCase):
     def test_view_activate_invalid_token(self):
         """activate with invalid token shows error"""
         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)
 
-        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)
 
         test_user = UserModel.objects.get(pk=test_user.pk)
@@ -57,27 +68,37 @@ class ActivationViewsTests(TestCase):
     def test_view_activate_disabled(self):
         """activate disabled user shows error"""
         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)
 
-        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)
 
     def test_view_activate_active(self):
         """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)
 
-        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)
 
         test_user = UserModel.objects.get(pk=test_user.pk)
@@ -86,14 +107,20 @@ class ActivationViewsTests(TestCase):
     def test_view_activate_inactive(self):
         """activate inactive user passess"""
         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)
 
-        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.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)
 
         # 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.save()
@@ -67,8 +66,7 @@ class TestActivePostersRanking(AuthenticatedUserTestCase):
         self.assertEqual(ranking['users'][1].score, 1)
 
         # 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.save()

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

@@ -12,9 +12,7 @@ UserModel = get_user_model()
 class GatewayTests(TestCase):
     def test_api_invalid_credentials(self):
         """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)
 
@@ -28,10 +26,12 @@ class GatewayTests(TestCase):
         """api signs user in"""
         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)
 
@@ -57,18 +57,18 @@ class GatewayTests(TestCase):
             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)
 
         response_json = response.json()
         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/')
         self.assertEqual(response.status_code, 200)
@@ -89,10 +89,12 @@ class GatewayTests(TestCase):
             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)
 
         response = self.client.get('/api/auth/')
@@ -104,13 +106,14 @@ class GatewayTests(TestCase):
 
     def test_login_inactive_admin(self):
         """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)
 
         response_json = response.json()
@@ -124,13 +127,14 @@ class GatewayTests(TestCase):
 
     def test_login_inactive_user(self):
         """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)
 
         response_json = response.json()
@@ -144,16 +148,17 @@ class GatewayTests(TestCase):
 
     def test_login_disabled_user(self):
         """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.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)
 
         response = self.client.get('/api/auth/')
@@ -325,10 +330,10 @@ class ChangePasswordAPITests(TestCase):
 
     def test_submit_valid(self):
         """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)
 
         user = UserModel.objects.get(id=self.user.pk)
@@ -336,10 +341,7 @@ class ChangePasswordAPITests(TestCase):
 
     def test_invalid_token_link(self):
         """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)
 
@@ -351,10 +353,9 @@ class ChangePasswordAPITests(TestCase):
             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)
 
     def test_inactive_user(self):
@@ -362,19 +363,17 @@ class ChangePasswordAPITests(TestCase):
         self.user.requires_activation = 1
         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.user.requires_activation = 2
         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)
 
     def test_disabled_user(self):
@@ -382,16 +381,14 @@ class ChangePasswordAPITests(TestCase):
         self.user.is_active = False
         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)
 
     def test_submit_empty(self):
         """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)

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

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

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

@@ -20,25 +20,22 @@ class AuthViewsTests(TestCase):
     def test_login_view_redirect_to(self):
         """login view respects redirect_to POST"""
         # 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['location'], '/redirect/')
 
         # 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['location'], '/')
 
     def test_logout_view(self):
         """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)
 

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

@@ -17,8 +17,7 @@ UserModel = get_user_model()
 class AvatarsStoreTests(TestCase):
     def test_store(self):
         """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)
         store.store_new_avatar(user, test_image)
@@ -85,8 +84,7 @@ class AvatarsStoreTests(TestCase):
 
 class AvatarSetterTests(TestCase):
     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.save()
@@ -158,7 +156,8 @@ class AvatarSetterTests(TestCase):
     def test_default_avatar_gravatar_fallback_dynamic(self):
         """default gravatar fails but fallback dynamic works"""
         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.save()
 
@@ -169,7 +168,8 @@ class AvatarSetterTests(TestCase):
     def test_default_avatar_gravatar_fallback_empty_gallery(self):
         """default both gravatar and fallback fail set"""
         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.save()
 
@@ -198,22 +198,18 @@ class UploadedAvatarTests(TestCase):
             uploaded.clean_crop(image, {'offset': {'x': 'ugabuga'}})
 
         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):
-            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):
         """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):
         """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)
 
         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):
         """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)
 
         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):
         """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)
 
         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):
     def setUp(self):
         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):

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

@@ -27,13 +27,16 @@ class BanAdminViewsTests(AdminTestCase):
         test_date = datetime.now() + timedelta(days=180)
 
         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(Ban.objects.count(), 10)
@@ -42,10 +45,11 @@ class BanAdminViewsTests(AdminTestCase):
         for ban in Ban.objects.iterator():
             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(Ban.objects.count(), 0)
 
@@ -56,13 +60,16 @@ class BanAdminViewsTests(AdminTestCase):
 
         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)
 
         response = self.client.get(reverse('misago:admin:users:bans:index'))
@@ -72,21 +79,27 @@ class BanAdminViewsTests(AdminTestCase):
 
     def test_edit_view(self):
         """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')
         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)
 
         response = self.client.get(reverse('misago:admin:users:bans:index'))
@@ -96,16 +109,19 @@ class BanAdminViewsTests(AdminTestCase):
 
     def test_delete_view(self):
         """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')
 
-        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)
 
         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')
         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')
         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')
         self.assertIsNone(wrong_type_ban)
 
         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)
 
@@ -54,10 +47,7 @@ class GetBanTests(TestCase):
         expired_ban = get_email_ban('ex@pired.com')
         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')
         self.assertIsNone(wrong_type_ban)
@@ -83,10 +73,7 @@ class GetBanTests(TestCase):
         expired_ban = get_ip_ban('124.0.0.1')
         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')
         self.assertIsNone(wrong_type_ban)
@@ -101,8 +88,7 @@ class GetBanTests(TestCase):
 
 class UserBansTests(TestCase):
     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):
         """user is not caught by ban"""
@@ -112,9 +98,7 @@ class UserBansTests(TestCase):
     def test_permanent_ban(self):
         """user is caught by permanent ban"""
         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)
@@ -140,20 +124,14 @@ class UserBansTests(TestCase):
 
     def test_expired_ban(self):
         """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.assertFalse(self.user.ban_cache.is_banned)
 
     def test_expired_non_flagged_ban(self):
         """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)
 
         self.assertIsNone(get_user_ban(self.user))
@@ -174,11 +152,7 @@ class RequestIPBansTests(TestCase):
 
     def test_permanent_ban(self):
         """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())
         self.assertTrue(ip_ban['is_banned'])
@@ -224,8 +198,7 @@ class RequestIPBansTests(TestCase):
 class BanUserTests(TestCase):
     def test_ban_user(self):
         """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')
         self.assertEqual(ban.user_message, 'User reason')

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

@@ -24,7 +24,8 @@ class CreateSuperuserTests(TestCase):
 
         self.assertEqual(
             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.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):
         """credentialchange module allows for store and read of change token"""
         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)
         self.assertEqual(email, 'newbob@test.com')
@@ -29,8 +28,7 @@ class CredentialChangeTests(TestCase):
     def test_email_change_invalidated_token(self):
         """token is invalidated by email change"""
         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.save()
@@ -41,8 +39,7 @@ class CredentialChangeTests(TestCase):
     def test_password_change_invalidated_token(self):
         """token is invalidated by password change"""
         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.save()
@@ -53,8 +50,7 @@ class CredentialChangeTests(TestCase):
     def test_invalid_token_is_handled(self):
         """there are no explosions in invalid tokens handling"""
         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)
         self.assertIsNone(email)

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

@@ -36,23 +36,14 @@ class DenyGuestsTests(UserTestCase):
 class DenyBannedIPTests(UserTestCase):
     def test_success(self):
         """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'))
         self.assertEqual(response.status_code, 200)
 
     def test_fail(self):
         """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'))
-        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)
 
         # 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)
 
         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)
 
         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):
         """change other user password errors"""
@@ -52,10 +55,14 @@ class ForgottenPasswordViewsTests(UserTestCase):
         self.login_user(self.get_authenticated_user())
 
         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)
 
     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')
 
         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)
 
     def test_change_password_form(self):
@@ -76,8 +87,12 @@ class ForgottenPasswordViewsTests(UserTestCase):
         password_token = make_password_change_token(test_user)
 
         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)

+ 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"""
         response = self.client.get(reverse('misago:users'))
         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):
@@ -57,7 +56,8 @@ class ActivePostersTests(UsersListTestCase):
         # Create 50 test users and see if errors appeared
         for i in range(50):
             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)
 
         build_active_posters_ranking()
@@ -69,8 +69,7 @@ class ActivePostersTests(UsersListTestCase):
 class UsersRankTests(UsersListTestCase):
     def test_ranks(self):
         """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():
             rank_user.rank = rank
@@ -88,7 +87,8 @@ class UsersRankTests(UsersListTestCase):
     def test_disabled_users(self):
         """ranks lists excludes disabled accounts"""
         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():
             rank_user.rank = rank
@@ -109,7 +109,8 @@ class UsersRankTests(UsersListTestCase):
         self.user.save()
 
         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():
             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):
         """/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)
 
 
@@ -23,10 +23,10 @@ class ConfirmChangeEmailTests(AuthenticatedUserTestCase):
         super(ConfirmChangeEmailTests, self).setUp()
         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)
 
         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):
         """invalid token is rejected"""
         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)
 
@@ -58,10 +57,10 @@ class ConfirmChangePasswordTests(AuthenticatedUserTestCase):
         super(ConfirmChangePasswordTests, self).setUp()
         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)
 
         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):
         """invalid token is rejected"""
         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)
 

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

@@ -14,18 +14,14 @@ UserModel = get_user_model()
 class UserProfileViewsTests(AuthenticatedUserTestCase):
     def setUp(self):
         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')
 
     def test_outdated_slugs(self):
         """user profile view redirects to valid slig"""
         invalid_kwargs = {'slug': 'baww', 'pk': self.user.pk}
-        response = self.client.get(reverse('misago:user-posts',
-                                           kwargs=invalid_kwargs))
+        response = self.client.get(reverse('misago:user-posts', kwargs=invalid_kwargs))
 
         self.assertEqual(response.status_code, 301)
 
@@ -98,8 +94,7 @@ class UserProfileViewsTests(AuthenticatedUserTestCase):
 
     def test_user_followers(self):
         """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.assertContains(response, 'You have no followers.')
@@ -110,16 +105,14 @@ class UserProfileViewsTests(AuthenticatedUserTestCase):
             followers.append(UserModel.objects.create_user(*user_data))
             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)
         for i in range(10):
             self.assertContains(response, "Follower%s" % i)
 
     def test_user_follows(self):
         """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.assertContains(response, 'You are not following any users.')
@@ -130,16 +123,14 @@ class UserProfileViewsTests(AuthenticatedUserTestCase):
             followers.append(UserModel.objects.create_user(*user_data))
             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)
         for i in range(10):
             self.assertContains(response, "Follower%s" % i)
 
     def test_username_history_list(self):
         """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.assertContains(response, 'Your username was never changed.')
 
@@ -148,8 +139,7 @@ class UserProfileViewsTests(AuthenticatedUserTestCase):
         self.user.set_username('TestUser')
         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.assertContains(response, "TestUser")
@@ -164,16 +154,14 @@ class UserProfileViewsTests(AuthenticatedUserTestCase):
         test_user = UserModel.objects.create_user("Bob", "bob@bob.com", 'pass.123')
         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)
 
         override_acl(self.user, {
             '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)
 
         override_acl(self.user, {
@@ -188,8 +176,7 @@ class UserProfileViewsTests(AuthenticatedUserTestCase):
             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.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):
     def test_link_registered(self):
         """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'])
         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_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)
 
         response = self.client.post(
@@ -40,7 +38,8 @@ class RankAdminViewsTests(AdminTestCase):
                 'style': 'test',
                 'is_tab': '1',
                 'roles': [test_role_a.pk, test_role_c.pk],
-            })
+            }
+        )
         self.assertEqual(response.status_code, 302)
 
         response = self.client.get(reverse('misago:admin:users:ranks:index'))
@@ -68,24 +67,25 @@ class RankAdminViewsTests(AdminTestCase):
                 'style': 'test',
                 'is_tab': '1',
                 'roles': [test_role_a.pk, test_role_c.pk],
-            })
+            }
+        )
 
         test_rank = Rank.objects.get(slug='test-rank')
 
         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.assertContains(response, test_rank.name)
         self.assertContains(response, test_rank.title)
 
         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={
                 'name': 'Top Lel',
                 'roles': [test_role_b.pk],
-            })
+            }
+        )
         self.assertEqual(response.status_code, 302)
 
         test_rank = Rank.objects.get(slug='top-lel')
@@ -109,13 +109,14 @@ class RankAdminViewsTests(AdminTestCase):
                 'title': 'Test Title',
                 'style': 'test',
                 'is_tab': '1',
-            })
+            }
+        )
 
         test_rank = Rank.objects.get(slug='test-rank')
 
         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)
 
         test_rank = Rank.objects.get(slug='test-rank')
@@ -131,13 +132,14 @@ class RankAdminViewsTests(AdminTestCase):
                 'title': 'Test Title',
                 'style': 'test',
                 'is_tab': '1',
-            })
+            }
+        )
 
         test_rank = Rank.objects.get(slug='test-rank')
 
         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)
 
         changed_rank = Rank.objects.get(slug='test-rank')
@@ -153,18 +155,19 @@ class RankAdminViewsTests(AdminTestCase):
                 'title': 'Test Title',
                 'style': 'test',
                 'is_tab': '1',
-            })
+            }
+        )
 
         test_rank = Rank.objects.get(slug='test-rank')
 
         # Move rank up
         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(
-            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)
 
         # Test move down
@@ -181,12 +184,14 @@ class RankAdminViewsTests(AdminTestCase):
                 'title': 'Test Title',
                 'style': 'test',
                 'is_tab': '1',
-            })
+            }
+        )
 
         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)
 
     def test_delete_view(self):
@@ -199,13 +204,14 @@ class RankAdminViewsTests(AdminTestCase):
                 'title': 'Test Title',
                 'style': 'test',
                 'is_tab': '1',
-            })
+            }
+        )
 
         test_rank = Rank.objects.get(slug='test-rank')
 
         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.client.get(reverse('misago:admin:users:ranks:index'))
@@ -228,7 +234,8 @@ class RankAdminViewsTests(AdminTestCase):
                 'style': 'test',
                 'is_tab': '1',
                 'roles': [test_role_a.pk],
-            })
+            }
+        )
 
         self.assertEqual(response.status_code, 200)
         self.assertContains(response, "This name collides with other rank.")
@@ -242,16 +249,17 @@ class RankAdminViewsTests(AdminTestCase):
                 'style': 'test',
                 'is_tab': '1',
                 'roles': [test_role_a.pk],
-            })
+            }
+        )
 
         test_rank = Rank.objects.get(slug='test-rank')
 
         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={
                 'name': 'Members',
                 'roles': [test_role_a.pk],
-            })
+            }
+        )
         self.assertEqual(response.status_code, 200)
         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')
         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):
         """policy allows guests"""
         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)
 
     def test_api_allows_authenticated(self):
@@ -21,23 +20,17 @@ class UnbannedOnlyTests(UserTestCase):
         self.login_user(self.user)
 
         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)
 
     def test_api_blocks_banned(self):
         """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(
-            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)
 
 
@@ -51,9 +44,8 @@ class UnbannedAnonOnlyTests(UserTestCase):
         self.user.save()
 
         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)
 
     def test_api_allows_authenticated(self):
@@ -61,21 +53,15 @@ class UnbannedAnonOnlyTests(UserTestCase):
         self.login_user(self.user)
 
         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)
 
     def test_api_blocks_banned(self):
         """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(
-            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)

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

@@ -16,9 +16,7 @@ class SearchApiTests(AuthenticatedUserTestCase):
 
     def test_no_permission(self):
         """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)
         self.assertEqual(response.status_code, 200)
@@ -105,7 +103,8 @@ class SearchApiTests(AuthenticatedUserTestCase):
     def test_search_disabled(self):
         """api respects disabled users visibility"""
         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)
         self.assertEqual(response.status_code, 200)
@@ -138,6 +137,4 @@ class SearchProviderApiTests(SearchApiTests):
     def setUp(self):
         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_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_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/)
     """
+
     def setUp(self):
         super(UserAvatarTests, self).setUp()
         self.link = '/api/users/%s/avatar/' % self.user.pk
@@ -77,13 +78,12 @@ class UserAvatarTests(AuthenticatedUserTestCase):
 
     def test_other_user_avatar(self):
         """requests to api error if user tries to access other user"""
-        self.logout_user();
+        self.logout_user()
 
         response = self.client.get(self.link)
         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)
         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)
 
         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)
 
             response_json = response.json()
             self.assertTrue(response_json['crop_tmp'])
             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)
         self.assertTrue(avatar.exists())
         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()
         self.assertEqual(response.status_code, 200)
@@ -158,26 +161,36 @@ class UserAvatarTests(AuthenticatedUserTestCase):
         self.assertTrue(avatar.exists())
         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)
 
-        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.")
 
         # delete user avatars, test if it deletes src and tmp
@@ -194,13 +207,9 @@ class UserAvatarTests(AuthenticatedUserTestCase):
         response = self.client.get(self.link)
         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):
         """gallery validates image to set"""
@@ -210,16 +219,15 @@ class UserAvatarTests(AuthenticatedUserTestCase):
         self.assertEqual(response.status_code, 200)
 
         # 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)
 
         # 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)
 
         # nonexistant image is handled
@@ -230,21 +238,16 @@ class UserAvatarTests(AuthenticatedUserTestCase):
         self.assertTrue(options['galleries'])
 
         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)
 
         # 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)
 
     def test_gallery_set_valid_avatar(self):
@@ -258,10 +261,7 @@ class UserAvatarTests(AuthenticatedUserTestCase):
         self.assertTrue(options['galleries'])
 
         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.")
 
@@ -270,11 +270,11 @@ class UserAvatarModerationTests(AuthenticatedUserTestCase):
     """
     tests for moderate user avatar RPC (/api/users/1/moderate-avatar/)
     """
+
     def setUp(self):
         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
 
@@ -297,53 +297,54 @@ class UserAvatarModerationTests(AuthenticatedUserTestCase):
         self.assertEqual(response.status_code, 200)
 
         options = response.json()
+        self.assertEqual(options['is_avatar_locked'], self.other_user.is_avatar_locked)
         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(
-            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, {
             '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)
 
         other_user = UserModel.objects.get(pk=self.other_user.pk)
 
         options = response.json()
         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, {
             '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)
 
         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)
 
         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, {
             '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)
 
         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, '')
 
         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, {
             '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)
 
         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, '')
 
         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):
         """moderate own avatar"""
@@ -419,5 +412,5 @@ class UserAvatarModerationTests(AuthenticatedUserTestCase):
             '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)

+ 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/)
     """
+
     def setUp(self):
         super(UserChangeEmailTests, self).setUp()
         self.link = '/api/users/%s/change-email/' % self.user.pk
@@ -26,67 +27,61 @@ class UserChangeEmailTests(AuthenticatedUserTestCase):
         response = self.client.post(self.link, data={})
 
         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):
         """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)
 
     def test_invalid_input(self):
         """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.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.json(), {
-            'new_email': [
-                "Enter a valid email address."
-            ],
+            'new_email': ["Enter a valid email address."],
         })
 
     def test_email_taken(self):
         """api validates email usage"""
         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)
 
     def test_change_email(self):
         """api allows users to change their e-mail addresses"""
         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.assertIn('Confirm e-mail change', mail.outbox[0].subject)
@@ -97,9 +92,9 @@ class UserChangeEmailTests(AuthenticatedUserTestCase):
         else:
             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)
 

+ 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/)
     """
+
     def setUp(self):
         super(UserChangePasswordTests, self).setUp()
         self.link = '/api/users/%s/change-password/' % self.user.pk
@@ -22,65 +23,60 @@ class UserChangePasswordTests(AuthenticatedUserTestCase):
         response = self.client.post(self.link, data={})
 
         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):
         """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.json(), {
-            'password': [
-                "Entered password is invalid."
-            ],
+            'password': ["Entered password is invalid."],
         })
 
     def test_blank_input(self):
         """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.json(), {
-            'new_password': [
-                "This field may not be blank."
-            ],
+            'new_password': ["This field may not be blank."],
         })
 
     def test_short_new_pasword(self):
         """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.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):
         """api allows users to change their passwords"""
         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.assertIn('Confirm password change', mail.outbox[0].subject)
@@ -91,9 +87,9 @@ class UserChangePasswordTests(AuthenticatedUserTestCase):
         else:
             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)
 

+ 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/)
     """
+
     def setUp(self):
         super(UserCreateTests, self).setUp()
         self.api_link = '/api/users/'
@@ -40,43 +41,40 @@ class UserCreateTests(UserTestCase):
         """api validates usernames"""
         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.json(), {
-            'username': [
-                "This username is not available."
-            ]
-        })
+        self.assertEqual(response.json(), {'username': ["This username is not available."]})
 
     def test_registration_validates_email(self):
         """api validates usernames"""
         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.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):
         """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 entirely numeric", status_code=400)
@@ -84,21 +82,27 @@ class UserCreateTests(UserTestCase):
 
     def test_registration_validates_password_similiarity(self):
         """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)
 
     def test_registration_calls_validate_new_registration(self):
         """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)
 
@@ -106,11 +110,11 @@ class UserCreateTests(UserTestCase):
         """api creates active and signed in user on POST"""
         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, 'Bob')
@@ -130,11 +134,11 @@ class UserCreateTests(UserTestCase):
         """api creates inactive user on POST"""
         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, 'Bob')
@@ -149,11 +153,11 @@ class UserCreateTests(UserTestCase):
         """api creates admin activated user on POST"""
         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, 'Bob')

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

@@ -8,8 +8,7 @@ from misago.users.models import User
 class UserManagerTests(TestCase):
     def test_create_user(self):
         """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)
 

+ 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/)
     """
+
     def setUp(self):
         super(UserSignatureTests, self).setUp()
         self.link = '/api/users/%s/signature/' % self.user.pk
@@ -69,9 +70,7 @@ class UserSignatureTests(AuthenticatedUserTestCase):
         self.user.is_signature_locked = False
         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)
 
     def test_post_good_signature(self):
@@ -83,17 +82,14 @@ class UserSignatureTests(AuthenticatedUserTestCase):
         self.user.is_signature_locked = False
         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.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.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/)
     """
+
     def setUp(self):
         super(UserUsernameTests, self).setUp()
         self.link = '/api/users/%s/username/' % self.user.pk
@@ -26,10 +27,8 @@ class UserUsernameTests(AuthenticatedUserTestCase):
         response_json = response.json()
 
         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'])
 
         for i in range(response_json['changes_left']):
@@ -55,9 +54,7 @@ class UserUsernameTests(AuthenticatedUserTestCase):
         response_json = response.json()
         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.assertTrue(self.user.username != 'Pointless')
@@ -70,9 +67,7 @@ class UserUsernameTests(AuthenticatedUserTestCase):
 
     def test_change_username_invalid_name(self):
         """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)
 
@@ -84,9 +79,7 @@ class UserUsernameTests(AuthenticatedUserTestCase):
         old_username = self.user.username
         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)
         options = response.json()['options']
@@ -96,19 +89,18 @@ class UserUsernameTests(AuthenticatedUserTestCase):
         self.assertEqual(self.user.username, new_username)
         self.assertTrue(self.user.username != old_username)
 
-        self.assertEqual(self.user.namechanges.last().new_username,
-                         new_username)
+        self.assertEqual(self.user.namechanges.last().new_username, new_username)
 
 
 class UserUsernameModerationTests(AuthenticatedUserTestCase):
     """
     tests for moderate username RPC (/api/users/1/moderate-username/)
     """
+
     def setUp(self):
         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
 
@@ -138,19 +130,18 @@ class UserUsernameModerationTests(AuthenticatedUserTestCase):
         self.assertEqual(response.status_code, 200)
 
         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, {
             'can_rename_users': 1,
         })
 
-        response = self.client.post(self.link, json.dumps({
+        response = self.client.post(
+            self.link, json.dumps({
                 'username': '',
-            }),
-            content_type="application/json")
+            }), content_type="application/json"
+        )
 
         self.assertContains(response, "Enter new username", status_code=400)
 
@@ -158,37 +149,42 @@ class UserUsernameModerationTests(AuthenticatedUserTestCase):
             'can_rename_users': 1,
         })
 
-        response = self.client.post(self.link, json.dumps({
+        response = self.client.post(
+            self.link, json.dumps({
                 'username': '$$$',
-            }),
-            content_type="application/json")
+            }), content_type="application/json"
+        )
 
-        self.assertContains(response,
+        self.assertContains(
+            response,
             "Username can only contain latin alphabet letters and digits.",
-            status_code=400)
+            status_code=400
+        )
 
         override_acl(self.user, {
             'can_rename_users': 1,
         })
 
-        response = self.client.post(self.link, json.dumps({
+        response = self.client.post(
+            self.link, json.dumps({
                 'username': 'a',
-            }),
-            content_type="application/json")
+            }), content_type="application/json"
+        )
 
         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, {
             'can_rename_users': 1,
         })
 
-        response = self.client.post(self.link, json.dumps({
+        response = self.client.post(
+            self.link, json.dumps({
                 'username': 'BobBoberson',
-            }),
-            content_type="application/json")
+            }), content_type="application/json"
+        )
 
         self.assertEqual(response.status_code, 200)
 
@@ -207,6 +203,5 @@ class UserUsernameModerationTests(AuthenticatedUserTestCase):
             '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)

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

@@ -24,8 +24,7 @@ class UserAdminViewsTests(AdminTestCase):
 
     def test_list_view(self):
         """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)
 
         response = self.client.get(response['location'])
@@ -34,8 +33,7 @@ class UserAdminViewsTests(AdminTestCase):
 
     def test_list_search(self):
         """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)
 
         link_base = response['location']
@@ -79,20 +77,18 @@ class UserAdminViewsTests(AdminTestCase):
         user_pks = []
         for i in range(10):
             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)
 
         response = self.client.post(
             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)
 
-        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.assertIn("has been activated", mail.outbox[0].subject)
 
@@ -101,16 +97,15 @@ class UserAdminViewsTests(AdminTestCase):
         user_pks = []
         for i in range(10):
             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)
 
         response = self.client.post(
             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)
 
         response = self.client.post(
@@ -118,12 +113,10 @@ class UserAdminViewsTests(AdminTestCase):
             data={
                 'action': 'ban',
                 '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': ''
-            })
+            }
+        )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(Ban.objects.count(), 24)
 
@@ -132,16 +125,15 @@ class UserAdminViewsTests(AdminTestCase):
         user_pks = []
         for i in range(10):
             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)
 
         response = self.client.post(
             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(UserModel.objects.count(), 1)
 
@@ -150,29 +142,28 @@ class UserAdminViewsTests(AdminTestCase):
         user_pks = []
         for i in range(10):
             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)
 
         response = self.client.post(
             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(UserModel.objects.count(), 1)
 
     def test_new_view(self):
         """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)
 
         default_rank = Rank.objects.get_default()
         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={
                 'username': 'Bawww',
                 'rank': six.text_type(default_rank.pk),
@@ -180,7 +171,8 @@ class UserAdminViewsTests(AdminTestCase):
                 'email': 'reg@stered.com',
                 'new_password': 'pass123',
                 'staff_level': '0'
-            })
+            }
+        )
         self.assertEqual(response.status_code, 302)
 
         UserModel.objects.get_by_username('Bawww')
@@ -189,28 +181,30 @@ class UserAdminViewsTests(AdminTestCase):
     def test_edit_view(self):
         """edit user view changes account"""
         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)
         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)
 
         updated_user = UserModel.objects.get(pk=test_user.pk)
@@ -228,27 +222,29 @@ class UserAdminViewsTests(AdminTestCase):
         This is regression test for issue #640
         """
         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)
         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)
 
         updated_user = UserModel.objects.get(pk=test_user.pk)
@@ -259,30 +255,32 @@ class UserAdminViewsTests(AdminTestCase):
     def test_edit_make_admin(self):
         """edit user view allows super admin to make other user admin"""
         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)
         self.assertContains(response, 'id="id_is_staff_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)
 
         updated_user = UserModel.objects.get(pk=test_user.pk)
@@ -292,30 +290,32 @@ class UserAdminViewsTests(AdminTestCase):
     def test_edit_make_superadmin_admin(self):
         """edit user view allows super admin to make other user super admin"""
         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)
         self.assertContains(response, 'id="id_is_staff_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)
 
         updated_user = UserModel.objects.get(pk=test_user.pk)
@@ -328,30 +328,32 @@ class UserAdminViewsTests(AdminTestCase):
         self.user.save()
 
         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)
         self.assertNotContains(response, 'id="id_is_staff_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)
 
         updated_user = UserModel.objects.get(pk=test_user.pk)
@@ -364,32 +366,34 @@ class UserAdminViewsTests(AdminTestCase):
         self.user.save()
 
         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)
         self.assertContains(response, 'id="id_is_active_1"')
         self.assertContains(response, 'id="id_is_active_staff_message"')
 
-        response = self.client.post(test_link, data={
-            'username': 'Bawww',
-            'rank': six.text_type(test_user.rank_id),
-            'roles': six.text_type(test_user.roles.all()[0].pk),
-            'email': 'reg@stered.com',
-            'new_password': 'pass123',
-            'is_staff': '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)
 
         updated_user = UserModel.objects.get(pk=test_user.pk)
@@ -406,32 +410,34 @@ class UserAdminViewsTests(AdminTestCase):
         test_user.is_staff = True
         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)
         self.assertContains(response, 'id="id_is_active_1"')
         self.assertContains(response, 'id="id_is_active_staff_message"')
 
-        response = self.client.post(test_link, data={
-            'username': 'Bawww',
-            'rank': six.text_type(test_user.rank_id),
-            'roles': six.text_type(test_user.roles.all()[0].pk),
-            'email': 'reg@stered.com',
-            'new_password': 'pass123',
-            'is_staff': '1',
-            'is_superuser': '0',
-            'signature': 'Hello world!',
-            'is_signature_locked': '1',
-            'is_hiding_presence': '0',
-            'limits_private_thread_invites_to': '0',
-            'signature_lock_staff_message': 'Staff message',
-            'signature_lock_user_message': 'User message',
-            'subscribe_to_started_threads': '2',
-            'subscribe_to_replied_threads': '2',
-            'is_active': '0',
-            'is_active_staff_message': "Disabled in test!"
-        })
+        response = self.client.post(
+            test_link,
+            data={
+                'username': 'Bawww',
+                'rank': six.text_type(test_user.rank_id),
+                'roles': six.text_type(test_user.roles.all()[0].pk),
+                'email': 'reg@stered.com',
+                'new_password': 'pass123',
+                'is_staff': '1',
+                'is_superuser': '0',
+                'signature': 'Hello world!',
+                'is_signature_locked': '1',
+                'is_hiding_presence': '0',
+                'limits_private_thread_invites_to': '0',
+                'signature_lock_staff_message': 'Staff message',
+                'signature_lock_user_message': 'User message',
+                'subscribe_to_started_threads': '2',
+                'subscribe_to_replied_threads': '2',
+                'is_active': '0',
+                'is_active_staff_message': "Disabled in test!"
+            }
+        )
         self.assertEqual(response.status_code, 302)
 
         updated_user = UserModel.objects.get(pk=test_user.pk)
@@ -448,32 +454,34 @@ class UserAdminViewsTests(AdminTestCase):
         test_user.is_staff = True
         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)
         self.assertNotContains(response, 'id="id_is_active_1"')
         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)
 
         updated_user = UserModel.objects.get(pk=test_user.pk)
@@ -483,8 +491,9 @@ class UserAdminViewsTests(AdminTestCase):
     def test_delete_threads_view(self):
         """delete user threads view deletes threads"""
         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]
         [post_thread(category, poster=test_user) for _ in range(10)]
@@ -506,8 +515,7 @@ class UserAdminViewsTests(AdminTestCase):
     def test_delete_posts_view(self):
         """delete user posts view deletes posts"""
         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]
         thread = post_thread(category)
@@ -530,8 +538,9 @@ class UserAdminViewsTests(AdminTestCase):
     def test_delete_account_view(self):
         """delete user account view deletes user account"""
         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)
         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})
 
-        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.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.assertContains(response, '[]')
@@ -57,8 +55,7 @@ class UsernameChangesApiTests(AuthenticatedUserTestCase):
         """list denies permission for other user (or all) if no access"""
         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)
 
         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)
     """
+
     def setUp(self):
         super(ActivePostersListTests, self).setUp()
         self.link = '/api/users/?list=active'
@@ -71,6 +72,7 @@ class FollowersListTests(AuthenticatedUserTestCase):
     """
     tests for generic list (GET /users/) filtered by followers
     """
+
     def setUp(self):
         super(FollowersListTests, self).setUp()
         self.link = '/api/users/%s/followers/'
@@ -88,7 +90,8 @@ class FollowersListTests(AuthenticatedUserTestCase):
     def test_filled_list(self):
         """user with followers returns 200"""
         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)
 
         response = self.client.get(self.link % self.user.pk)
@@ -98,7 +101,8 @@ class FollowersListTests(AuthenticatedUserTestCase):
     def test_filled_list_search(self):
         """followers list is searchable"""
         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)
 
         api_link = self.link % self.user.pk
@@ -112,6 +116,7 @@ class FollowsListTests(AuthenticatedUserTestCase):
     """
     tests for generic list (GET /users/) filtered by follows
     """
+
     def setUp(self):
         super(FollowsListTests, self).setUp()
         self.link = '/api/users/%s/follows/'
@@ -129,7 +134,8 @@ class FollowsListTests(AuthenticatedUserTestCase):
     def test_filled_list(self):
         """user with follows returns 200"""
         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)
 
         response = self.client.get(self.link % self.user.pk)
@@ -139,7 +145,8 @@ class FollowsListTests(AuthenticatedUserTestCase):
     def test_filled_list_search(self):
         """follows list is searchable"""
         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)
 
         api_link = self.link % self.user.pk
@@ -153,6 +160,7 @@ class RankListTests(AuthenticatedUserTestCase):
     """
     tests for generic list (GET /users/) filtered by rank
     """
+
     def setUp(self):
         super(RankListTests, self).setUp()
         self.link = '/api/users/?rank=%s'
@@ -164,11 +172,7 @@ class RankListTests(AuthenticatedUserTestCase):
 
     def test_empty_list(self):
         """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)
         self.assertEqual(response.status_code, 200)
@@ -199,15 +203,10 @@ class RankListTests(AuthenticatedUserTestCase):
 
     def test_disabled_users(self):
         """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(
-            '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)
@@ -225,6 +224,7 @@ class SearchNamesListTests(AuthenticatedUserTestCase):
     """
     tests for generic list (GET /users/) filtered by username disallowing searches
     """
+
     def setUp(self):
         super(SearchNamesListTests, self).setUp()
         self.link = '/api/users/?&name='
@@ -245,9 +245,7 @@ class UserRetrieveTests(AuthenticatedUserTestCase):
         super(UserRetrieveTests, self).setUp()
 
         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):
         """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/)
     """
+
     def setUp(self):
         super(UserForumOptionsTests, self).setUp()
         self.link = '/api/users/%s/forum-options/' % self.user.pk
@@ -285,79 +284,95 @@ class UserForumOptionsTests(AuthenticatedUserTestCase):
         response = self.client.post(self.link)
 
         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):
         """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.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):
         """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.reload_user();
+        self.reload_user()
 
         self.assertFalse(self.user.is_hiding_presence)
         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_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.reload_user();
+        self.reload_user()
 
         self.assertTrue(self.user.is_hiding_presence)
         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_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.reload_user();
+        self.reload_user()
 
         self.assertFalse(self.user.is_hiding_presence)
         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/)
     """
+
     def setUp(self):
         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
 
@@ -403,7 +418,6 @@ class UserFollowTests(AuthenticatedUserTestCase):
         response = self.client.post(self.link)
         self.assertEqual(response.status_code, 200)
 
-
         user = UserModel.objects.get(pk=self.user.pk)
         self.assertEqual(user.followers, 0)
         self.assertEqual(user.following, 1)
@@ -436,28 +450,24 @@ class UserBanTests(AuthenticatedUserTestCase):
     """
     tests for ban endpoint (GET to /api/users/1/ban/)
     """
+
     def setUp(self):
         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
 
     def test_no_permission(self):
         """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)
         self.assertContains(response, "can't see users bans details", status_code=403)
 
     def test_no_ban(self):
         """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)
         self.assertEqual(response.status_code, 200)
@@ -465,14 +475,10 @@ class UserBanTests(AuthenticatedUserTestCase):
 
     def test_ban_details(self):
         """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(
-            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)
@@ -487,11 +493,11 @@ class UserDeleteTests(AuthenticatedUserTestCase):
     """
     tests for user delete RPC (POST to /api/users/1/delete/)
     """
+
     def setUp(self):
         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
 
@@ -507,10 +513,12 @@ class UserDeleteTests(AuthenticatedUserTestCase):
 
     def test_delete_no_permission(self):
         """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)
         self.assertEqual(response.status_code, 403)
@@ -518,10 +526,12 @@ class UserDeleteTests(AuthenticatedUserTestCase):
 
     def test_delete_too_many_posts(self):
         """raises 403 error when user has too many posts"""
-        override_acl(self.user, {
-            'can_delete_users_newer_than': 0,
-            'can_delete_users_with_less_posts_than': 5,
-        })
+        override_acl(
+            self.user, {
+                'can_delete_users_newer_than': 0,
+                'can_delete_users_with_less_posts_than': 5,
+            }
+        )
 
         self.other_user.posts = 6
         self.other_user.save()
@@ -533,10 +543,12 @@ class UserDeleteTests(AuthenticatedUserTestCase):
 
     def test_delete_too_old_member(self):
         """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.save()
@@ -548,20 +560,24 @@ class UserDeleteTests(AuthenticatedUserTestCase):
 
     def test_delete_self(self):
         """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)
         self.assertContains(response, "can't delete yourself", status_code=403)
 
     def test_delete_admin(self):
         """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.save()
@@ -571,10 +587,12 @@ class UserDeleteTests(AuthenticatedUserTestCase):
 
     def test_delete_superadmin(self):
         """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.save()
@@ -584,14 +602,18 @@ class UserDeleteTests(AuthenticatedUserTestCase):
 
     def test_delete_with_content(self):
         """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)
 
         with self.assertRaises(UserModel.DoesNotExist):
@@ -602,14 +624,18 @@ class UserDeleteTests(AuthenticatedUserTestCase):
 
     def test_delete_without_content(self):
         """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)
 
         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):
     def test_is_case_insensitive(self):
         """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):
         """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):
     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):
         """validate_email_available allows available emails"""
@@ -32,10 +31,7 @@ class ValidateEmailAvailableTests(TestCase):
 
 class ValidateEmailBannedTests(TestCase):
     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):
         """unbanned email passes validation"""
@@ -65,14 +61,12 @@ class ValidateUsernameTests(TestCase):
 
 class ValidateUsernameAvailableTests(TestCase):
     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):
         """validate_username_available allows available names"""
         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):
         """validate_username_available disallows unvailable names"""
@@ -82,10 +76,7 @@ class ValidateUsernameAvailableTests(TestCase):
 
 class ValidateUsernameBannedTests(TestCase):
     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):
         """unbanned name passes validation"""

+ 3 - 3
misago/users/testutils.py

@@ -22,12 +22,12 @@ class UserTestCase(MisagoTestCase):
         return AnonymousUser()
 
     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):
         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):
         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
 - token checksum for discovering manipulations
 """
+
+
 def make(user, token_type):
     user_hash = _make_hash(user, token_type)
     creation_day = _days_since_epoch()
@@ -46,16 +48,11 @@ def is_valid(user, token_type, token):
 
 def _make_hash(user, token_type):
     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():
@@ -63,8 +60,7 @@ def _days_since_epoch():
 
 
 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
 
-
 urlpatterns = [
     url(r'^banned/$', home_redirect, name='banned'),
-
     url(r'^login/$', auth.login, name='login'),
     url(r'^logout/$', auth.logout, name='logout'),
-
     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/(?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 += [
     url(r'^options/$', options.index, name='options'),
     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/change-username/$', options.index, name='usercp-change-username'),
     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 += [
-    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 += [
-    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 += [
     url(r'^avatar/$', avatarserver.blank_avatar, name='blank-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/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/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'),
 ]
 
-
 router = MisagoApiRouter()
 router.register(r'ranks', RanksViewSet)
 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)
 
 UserModel = get_user_model()
-
-
 """
 Email validators
 """
+
+
 def validate_email_available(value, exclude=None):
     try:
         user = UserModel.objects.get_by_email(value)
@@ -53,6 +53,8 @@ def validate_email(value, exclude=None):
 """
 Username validators
 """
+
+
 def validate_username_available(value, exclude=None):
     try:
         user = UserModel.objects.get_by_username(value)
@@ -74,8 +76,7 @@ def validate_username_banned(value):
 
 def validate_username_content(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):
@@ -83,7 +84,8 @@ def validate_username_length(value):
         message = ungettext(
             "Username must be at least %(limit_value)s character 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}
         raise ValidationError(message)
 
@@ -91,7 +93,8 @@ def validate_username_length(value):
         message = ungettext(
             "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}
         raise ValidationError(message)
 
@@ -117,10 +120,7 @@ def validate_with_sfs(request, form, cleaned_data):
 
 def _real_validate_with_sfs(ip, email):
     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()
 
@@ -133,7 +133,7 @@ def _real_validate_with_sfs(ip, email):
         if api_score > settings.MISAGO_STOP_FORUM_SPAM_MIN_CONFIDENCE:
             raise ValidationError(_("Data entered was found in spammers database."))
     except requests.exceptions.RequestException:
-        pass # todo: log those somewhere
+        pass  # todo: log those somewhere
 
 
 def validate_gmail_email(request, form, cleaned_data):
@@ -149,6 +149,8 @@ def validate_gmail_email(request, form, cleaned_data):
 """
 Registration validation
 """
+
+
 def load_registration_validators(validators):
     loaded_validators = []
     for path in validators:

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

@@ -27,4 +27,5 @@ class ActivePosters(object):
             'users_count': self.count
         }
 
+
 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.core.shortcuts import paginate, pagination_dict
-from django.http import Http404
 from misago.users.online.utils import make_users_status_aware
 from misago.users.serializers import UserCardSerializer
 
@@ -29,9 +30,7 @@ class Followers(object):
         return profile.followed_by
 
     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)
         return context
 

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

@@ -6,8 +6,7 @@ from .threads import UserThreads
 
 class UserPosts(UserThreads):
     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):
         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):
     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:
             queryset = queryset.filter(is_active=True)
@@ -19,9 +19,7 @@ class RankUsers(object):
         self.paginator = pagination_dict(list_page)
 
     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)
         return context
 

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

@@ -13,19 +13,15 @@ class UserThreads(object):
         root_category = ThreadsRootCategory(request)
         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')
 
         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)
 
         posts = list(list_page.object_list)
@@ -34,8 +30,7 @@ class UserThreads(object):
         for post in posts:
             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, posts)
@@ -52,8 +47,7 @@ class UserThreads(object):
         self.paginator = paginator
 
     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):
         return profile.post_set.select_related('thread', 'poster').filter(
@@ -62,8 +56,7 @@ class UserThreads(object):
 
     def get_frontend_context(self):
         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)
@@ -71,10 +64,7 @@ class UserThreads(object):
         return context
 
     def get_template_context(self):
-        return {
-            'posts': self.posts,
-            'paginator': self.paginator
-        }
+        return {'posts': self.posts, 'paginator': self.paginator}
 
 
 UserFeedSerializer = FeedSerializer.exclude_fields('poster')

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

@@ -17,14 +17,13 @@ def activation_view(f):
     @deny_banned_ips
     def decorator(*args, **kwargs):
         return f(*args, **kwargs)
+
     return decorator
 
 
 @activation_view
 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')
 
 
@@ -47,8 +46,10 @@ def activate_by_token(request, pk, token):
             raise ActivationStopped(message)
 
         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}
             raise ActivationError(message)
 
@@ -57,18 +58,24 @@ def activate_by_token(request, pk, token):
             raise Banned(ban)
     except ActivationStopped as e:
         return render(request, 'misago/activation/stopped.html', {
-                'message': e.args[0],
-            })
+            'message': e.args[0],
+        })
     except ActivationError as e:
-        return render(request, 'misago/activation/error.html', {
+        return render(
+            request, 'misago/activation/error.html', {
                 'message': e.args[0],
-            }, status=400)
+            }, status=400
+        )
 
     inactive_user.requires_activation = UserModel.ACTIVATION_NONE
     inactive_user.save(update_fields=['requires_activation'])
 
     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):
     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
     selection_label = _('With bans: 0')
     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):
         items.delete()

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

@@ -26,7 +26,7 @@ class RankAdmin(generic.AdminBaseMixin):
 
 
 class RanksList(RankAdmin, generic.ListView):
-    ordering = (('order', None),)
+    ordering = (('order', None), )
 
 
 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
 
         return EditUserFormFactory(
-            self.form, target,
+            self.form,
+            target,
             add_is_active_fields=add_is_active_fields,
             add_admin_fields=add_admin_fields,
         )
@@ -49,43 +50,40 @@ class UserAdmin(generic.AdminBaseMixin):
 
 class UsersList(UserAdmin, generic.ListView):
     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')
     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):
         qs = super(UsersList, self).get_queryset()
@@ -109,12 +107,9 @@ class UsersList(UserAdmin, generic.ListView):
             queryset.update(requires_activation=UserModel.ACTIVATION_NONE)
 
             subject = _("Your account on %(forum_name)s forums has been activated")
-            mail_subject = subject % {
-                'forum_name': settings.forum_name
-            }
+            mail_subject = subject % {'forum_name': settings.forum_name}
 
-            mail_users(request, inactive_users, mail_subject,
-                       'misago/emails/activation/by_admin')
+            mail_users(request, inactive_users, mail_subject, 'misago/emails/activation/by_admin')
 
             message = _("Selected users accounts have been activated.")
             messages.success(request, message)
@@ -172,10 +167,7 @@ class UsersList(UserAdmin, generic.ListView):
                             if ban == 'ip_first':
                                 formats = (bits[0], ip_separator)
                             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))
 
                         if banned_value not in banned_values:
@@ -186,17 +178,19 @@ class UsersList(UserAdmin, generic.ListView):
                             Ban.objects.create(**ban_kwargs)
                             banned_values.append(banned_value)
 
-
                 Ban.objects.invalidate_cache()
                 message = _("Selected users have been banned.")
                 messages.success(request, message)
                 return None
 
         return self.render(
-            request, template='misago/admin/users/ban.html', context={
+            request,
+            template='misago/admin/users/ban.html',
+            context={
                 'users': users,
                 'form': form,
-            })
+            }
+        )
 
     def action_delete_accounts(self, request, users):
         for user in users:
@@ -225,9 +219,7 @@ class UsersList(UserAdmin, generic.ListView):
         messages.success(request, message)
 
         return self.render(
-            request,
-            template='misago/admin/users/delete.html',
-            context={
+            request, template='misago/admin/users/delete.html', context={
                 'users': users,
             }
         )
@@ -258,8 +250,7 @@ class NewUser(UserAdmin, generic.ModelFormView):
         new_user.update_acl_key()
         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)
 
 
@@ -276,8 +267,7 @@ class EditUser(UserAdmin, generic.ModelFormView):
     def handle_form(self, form, request, target):
         target.username = target.old_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'):
             target.set_password(form.cleaned_data['new_password'])
@@ -313,8 +303,7 @@ class EditUser(UserAdmin, generic.ModelFormView):
         target.update_acl_key()
         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):
@@ -329,7 +318,8 @@ class DeletionStep(UserAdmin, generic.ButtonView):
 
     def execute_step(self, user):
         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):
         return JsonResponse(self.execute_step(target))
@@ -355,10 +345,7 @@ class DeleteThreadsStep(DeletionStep):
         else:
             is_completed = True
 
-        return {
-            'deleted_count': deleted_threads,
-            'is_completed': is_completed
-        }
+        return {'deleted_count': deleted_threads, 'is_completed': is_completed}
 
 
 class DeletePostsStep(DeletionStep):
@@ -388,10 +375,7 @@ class DeletePostsStep(DeletionStep):
         else:
             is_completed = True
 
-        return {
-            'deleted_count': deleted_posts,
-            'is_completed': is_completed
-        }
+        return {'deleted_count': deleted_posts, 'is_completed': is_completed}
 
 
 class DeleteAccountStep(DeletionStep):

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

@@ -15,8 +15,7 @@ def login(request):
     if request.method == 'POST':
         redirect_to = request.POST.get('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:
                 redirect_to_path = urlparse(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
     def decorator(*args, **kwargs):
         return f(*args, **kwargs)
+
     return decorator
 
 
@@ -33,16 +34,16 @@ def reset_password_form(request, pk, token):
     requesting_user = get_object_or_404(get_user_model(), pk=pk)
 
     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}
             raise ResetError(message)
 
         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}
             raise ResetError(message)
 
@@ -50,14 +51,18 @@ def reset_password_form(request, pk, token):
         if ban:
             raise Banned(ban)
     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
     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.utils import six
 from django.views import View
@@ -33,9 +32,7 @@ class ListView(View):
         for rank in Rank.objects.filter(is_tab=True).order_by('order'):
             context_data['pages'].append({
                 '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
             })
 

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

@@ -19,9 +19,7 @@ def index(request, *args, **kwargs):
             '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')
 
@@ -36,8 +34,8 @@ def confirm_change_view(f):
         try:
             return f(request, token)
         except ChangeError:
-            return render(request, 'misago/options/credentials_error.html',
-                status=400)
+            return render(request, 'misago/options/credentials_error.html', status=400)
+
     return decorator
 
 
@@ -54,9 +52,13 @@ def confirm_email_change(request, token):
         raise ChangeError()
 
     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
@@ -70,6 +72,10 @@ def confirm_password_change(request, token):
     request.user.save(update_fields=['password'])
 
     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.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.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.online.utils import get_user_status
 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
 
 
@@ -38,8 +36,7 @@ class ProfileView(View):
         return render(request, self.template_name, context_data)
 
     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)
 
@@ -70,7 +67,8 @@ class ProfileView(View):
             })
 
         request.frontend_context['PROFILE'] = UserProfileSerializer(
-            profile, context={'user': request.user}).data
+            profile, context={'user': request.user}
+        ).data
 
         if not profile.is_active:
             request.frontend_context['PROFILE']['is_active'] = False
@@ -104,11 +102,7 @@ class LandingView(ProfileView):
     def get(self, request, *args, **kwargs):
         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):
@@ -161,9 +155,7 @@ class UserUsernameHistoryView(ProfileView):
         page = paginate(queryset, None, 14, 4)
 
         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
 
@@ -187,7 +179,7 @@ class UserBanView(ProfileView):
 
 
 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'
+)