Browse Source

Merge pull request #1165 from rafalp/fix-pylit-issues

Fix pylit issues
Rafał Pitoń 6 years ago
parent
commit
b16b5c6b72
197 changed files with 1056 additions and 1105 deletions
  1. 6 0
      .pylintrc
  2. 2 4
      .travis.yml
  3. 0 1
      misago/acl/tests/test_providers.py
  4. 0 2
      misago/admin/middleware.py
  5. 0 1
      misago/admin/views/__init__.py
  6. 0 1
      misago/admin/views/generic/list.py
  7. 0 1
      misago/cache/test.py
  8. 9 15
      misago/categories/forms.py
  9. 1 1
      misago/categories/management/commands/prunecategories.py
  10. 1 5
      misago/categories/permissions.py
  11. 0 2
      misago/categories/serializers.py
  12. 20 22
      misago/categories/tests/test_permissions_admin_views.py
  13. 3 1
      misago/categories/utils.py
  14. 18 22
      misago/categories/views/permsadmin.py
  15. 2 4
      misago/core/apipatch.py
  16. 1 2
      misago/core/context_processors.py
  17. 2 4
      misago/core/decorators.py
  18. 11 13
      misago/core/errorpages.py
  19. 1 4
      misago/core/exceptionhandler.py
  20. 1 4
      misago/core/exceptions.py
  21. 0 2
      misago/core/middleware.py
  22. 0 2
      misago/core/momentjs.py
  23. 12 11
      misago/core/page.py
  24. 13 12
      misago/core/pgutils.py
  25. 2 3
      misago/core/shortcuts.py
  26. 1 1
      misago/core/testproject/urlswitherrorhandlers.py
  27. 2 2
      misago/core/tests/test_errorpages.py
  28. 1 1
      misago/core/tests/test_jsi18n.py
  29. 7 1
      misago/core/tests/test_utils.py
  30. 4 5
      misago/core/utils.py
  31. 3 4
      misago/faker/management/commands/createfakebans.py
  32. 1 1
      misago/faker/management/commands/createfakecategories.py
  33. 3 1
      misago/faker/management/commands/createfakethreads.py
  34. 4 1
      misago/faker/management/commands/createfakeusers.py
  35. 4 4
      misago/legal/forms.py
  36. 1 1
      misago/legal/tests/test_admin_views.py
  37. 5 7
      misago/legal/utils.py
  38. 1 2
      misago/markup/bbcode/blocks.py
  39. 1 1
      misago/markup/bbcode/inline.py
  40. 3 3
      misago/markup/mentions.py
  41. 0 2
      misago/markup/parser.py
  42. 3 1
      misago/markup/tests/test_api.py
  43. 8 6
      misago/markup/tests/test_mentions.py
  44. 6 8
      misago/readtracker/dates.py
  45. 3 21
      misago/readtracker/tests/test_categoriestracker.py
  46. 1 1
      misago/readtracker/tests/test_dates.py
  47. 0 1
      misago/readtracker/tests/test_poststracker.py
  48. 0 1
      misago/readtracker/tests/test_threadstracker.py
  49. 0 1
      misago/search/context_processors.py
  50. 0 2
      misago/search/permissions.py
  51. 4 1
      misago/search/tests/test_searchproviders.py
  52. 0 1
      misago/search/views.py
  53. 2 1
      misago/threads/api/attachments.py
  54. 0 5
      misago/threads/api/postendpoints/delete.py
  55. 2 5
      misago/threads/api/postendpoints/patch_event.py
  56. 1 2
      misago/threads/api/postendpoints/patch_post.py
  57. 5 3
      misago/threads/api/postingendpoint/__init__.py
  58. 3 1
      misago/threads/api/postingendpoint/attachments.py
  59. 5 2
      misago/threads/api/postingendpoint/category.py
  60. 4 2
      misago/threads/api/postingendpoint/participants.py
  61. 8 6
      misago/threads/api/postingendpoint/reply.py
  62. 3 3
      misago/threads/api/threadendpoints/delete.py
  63. 4 2
      misago/threads/api/threadendpoints/editor.py
  64. 4 3
      misago/threads/api/threadendpoints/list.py
  65. 5 6
      misago/threads/api/threadendpoints/merge.py
  66. 16 11
      misago/threads/api/threadendpoints/patch.py
  67. 4 3
      misago/threads/api/threadpoll.py
  68. 42 45
      misago/threads/api/threadposts.py
  69. 15 24
      misago/threads/api/threads.py
  70. 2 4
      misago/threads/filtersearch.py
  71. 14 13
      misago/threads/forms.py
  72. 2 2
      misago/threads/mergeconflict.py
  73. 1 2
      misago/threads/models/attachment.py
  74. 0 1
      misago/threads/models/poll.py
  75. 3 4
      misago/threads/models/post.py
  76. 1 0
      misago/threads/moderation/exceptions.py
  77. 43 43
      misago/threads/moderation/posts.py
  78. 100 100
      misago/threads/moderation/threads.py
  79. 1 1
      misago/threads/paginator.py
  80. 2 5
      misago/threads/permissions/attachments.py
  81. 48 58
      misago/threads/permissions/bestanswers.py
  82. 2 5
      misago/threads/permissions/polls.py
  83. 0 2
      misago/threads/permissions/privatethreads.py
  84. 18 17
      misago/threads/permissions/threads.py
  85. 0 2
      misago/threads/serializers/attachment.py
  86. 4 2
      misago/threads/serializers/moderation.py
  87. 3 3
      misago/threads/serializers/poll.py
  88. 2 1
      misago/threads/serializers/pollvote.py
  89. 0 6
      misago/threads/serializers/post.py
  90. 0 2
      misago/threads/serializers/postedit.py
  91. 0 2
      misago/threads/serializers/postlike.py
  92. 0 4
      misago/threads/serializers/thread.py
  93. 6 8
      misago/threads/templatetags/misago_poststags.py
  94. 2 1
      misago/threads/tests/test_attachments_api.py
  95. 9 3
      misago/threads/tests/test_attachments_middleware.py
  96. 0 1
      misago/threads/tests/test_emailnotification_middleware.py
  97. 7 2
      misago/threads/tests/test_mergeconflict.py
  98. 4 1
      misago/threads/tests/test_privatethread_patch_api.py
  99. 4 2
      misago/threads/tests/test_privatethread_start_api.py
  100. 3 1
      misago/threads/tests/test_sync_unread_private_threads.py
  101. 61 40
      misago/threads/tests/test_thread_patch_api.py
  102. 8 4
      misago/threads/tests/test_thread_pollcreate_api.py
  103. 2 2
      misago/threads/tests/test_thread_polledit_api.py
  104. 3 1
      misago/threads/tests/test_thread_pollvotes_api.py
  105. 4 2
      misago/threads/tests/test_thread_postbulkdelete_api.py
  106. 0 2
      misago/threads/tests/test_thread_postbulkpatch_api.py
  107. 5 2
      misago/threads/tests/test_thread_postmerge_api.py
  108. 1 1
      misago/threads/tests/test_thread_postmove_api.py
  109. 7 3
      misago/threads/tests/test_thread_postpatch_api.py
  110. 3 2
      misago/threads/tests/test_thread_postsplit_api.py
  111. 0 1
      misago/threads/tests/test_threads_api.py
  112. 18 5
      misago/threads/tests/test_threads_editor_api.py
  113. 2 1
      misago/threads/tests/test_threads_merge_api.py
  114. 0 3
      misago/threads/tests/test_threads_moderation.py
  115. 0 1
      misago/threads/tests/test_threadslists.py
  116. 3 1
      misago/threads/tests/test_threadview.py
  117. 1 2
      misago/threads/tests/test_validators.py
  118. 4 4
      misago/threads/threadtypes/privatethread.py
  119. 5 8
      misago/threads/threadtypes/thread.py
  120. 13 15
      misago/threads/utils.py
  121. 4 0
      misago/threads/validators.py
  122. 1 1
      misago/threads/viewmodels/posts.py
  123. 8 16
      misago/threads/viewmodels/threads.py
  124. 2 1
      misago/threads/views/admin/attachmenttypes.py
  125. 5 7
      misago/threads/views/attachment.py
  126. 2 3
      misago/threads/views/goto.py
  127. 4 2
      misago/threads/views/list.py
  128. 1 1
      misago/threads/views/thread.py
  129. 48 52
      misago/users/api/auth.py
  130. 8 8
      misago/users/api/captcha.py
  131. 16 17
      misago/users/api/userendpoints/avatar.py
  132. 18 19
      misago/users/api/userendpoints/changeemail.py
  133. 19 19
      misago/users/api/userendpoints/changepassword.py
  134. 0 1
      misago/users/api/userendpoints/create.py
  135. 1 2
      misago/users/api/userendpoints/list.py
  136. 1 0
      misago/users/api/userendpoints/signature.py
  137. 40 40
      misago/users/api/userendpoints/username.py
  138. 1 1
      misago/users/api/usernamechanges.py
  139. 2 4
      misago/users/api/users.py
  140. 2 5
      misago/users/apps.py
  141. 2 3
      misago/users/avatars/uploaded.py
  142. 5 9
      misago/users/bans.py
  143. 1 1
      misago/users/datadownloads/__init__.py
  144. 2 2
      misago/users/datadownloads/dataarchive.py
  145. 2 2
      misago/users/djangoadmin.py
  146. 13 13
      misago/users/forms/admin.py
  147. 2 2
      misago/users/forms/auth.py
  148. 1 1
      misago/users/forms/register.py
  149. 4 2
      misago/users/management/commands/createsuperuser.py
  150. 1 2
      misago/users/management/commands/deletemarkedusers.py
  151. 1 1
      misago/users/management/commands/deleteprofilefield.py
  152. 3 3
      misago/users/management/commands/listusedprofilefields.py
  153. 2 1
      misago/users/management/commands/prepareuserdatadownloads.py
  154. 5 7
      misago/users/models/ban.py
  155. 1 1
      misago/users/models/online.py
  156. 2 4
      misago/users/models/user.py
  157. 1 3
      misago/users/namechanges.py
  158. 0 2
      misago/users/permissions/account.py
  159. 4 6
      misago/users/permissions/decorators.py
  160. 0 2
      misago/users/permissions/delete.py
  161. 0 2
      misago/users/permissions/moderation.py
  162. 1 4
      misago/users/permissions/profiles.py
  163. 5 6
      misago/users/profilefields/basefields.py
  164. 5 5
      misago/users/profilefields/default.py
  165. 2 2
      misago/users/profilefields/serializers.py
  166. 0 2
      misago/users/serializers/ban.py
  167. 5 7
      misago/users/serializers/moderation.py
  168. 2 2
      misago/users/serializers/options.py
  169. 1 2
      misago/users/serializers/rank.py
  170. 2 8
      misago/users/serializers/user.py
  171. 1 2
      misago/users/signatures.py
  172. 2 1
      misago/users/social/backendsnames.py
  173. 9 4
      misago/users/social/pipeline.py
  174. 2 3
      misago/users/test.py
  175. 1 1
      misago/users/tests/test_audittrail.py
  176. 3 2
      misago/users/tests/test_auth_api.py
  177. 0 1
      misago/users/tests/test_auth_backend.py
  178. 0 1
      misago/users/tests/test_bans.py
  179. 4 1
      misago/users/tests/test_bio_profilefield.py
  180. 14 5
      misago/users/tests/test_datadownloads.py
  181. 3 1
      misago/users/tests/test_forgottenpassword_views.py
  182. 0 1
      misago/users/tests/test_mention_api.py
  183. 1 5
      misago/users/tests/test_namechanges.py
  184. 1 0
      misago/users/tests/test_profilefields.py
  185. 0 1
      misago/users/tests/test_signatures.py
  186. 4 5
      misago/users/tests/test_social_pipeline.py
  187. 3 2
      misago/users/tests/test_twitter_profilefield.py
  188. 0 2
      misago/users/tests/test_user_avatar_api.py
  189. 0 2
      misago/users/tests/test_user_changeemail_api.py
  190. 4 1
      misago/users/tests/test_user_create_api.py
  191. 1 1
      misago/users/tests/test_user_getters.py
  192. 3 1
      misago/users/tests/test_user_requestdatadownload_api.py
  193. 3 1
      misago/users/tests/test_useradmin_views.py
  194. 16 6
      misago/users/tests/test_users_api.py
  195. 1 1
      misago/users/validators.py
  196. 5 5
      misago/users/views/admin/users.py
  197. 2 1
      misago/users/views/forgottenpassword.py

+ 6 - 0
.pylintrc

@@ -10,14 +10,20 @@ max-line-length=88
 disable=
 disable=
     abstract-method,
     abstract-method,
     arguments-differ,
     arguments-differ,
+    assignment-from-none,
+    attribute-defined-outside-init,
     bad-continuation,
     bad-continuation,
+    cyclic-import,
+    duplicate-code,
     expression-not-assigned,
     expression-not-assigned,
     fixme,
     fixme,
     inconsistent-return-statements,
     inconsistent-return-statements,
     invalid-name,
     invalid-name,
     missing-docstring,
     missing-docstring,
+    model-no-explicit-unicode,  # pylint-django
     no-member,
     no-member,
     no-self-use,
     no-self-use,
+    protected-access,
     redefined-outer-name,
     redefined-outer-name,
     too-few-public-methods,
     too-few-public-methods,
     too-many-ancestors,
     too-many-ancestors,

+ 2 - 4
.travis.yml

@@ -20,8 +20,6 @@ jobs:
       python: 3.6
       python: 3.6
       install:
       install:
         - pip install -U pip setuptools
         - pip install -U pip setuptools
-        - python setup.py install
-        - pip install black pylint pylint-django
+        - pip install black
       script:
       script:
-        - black --check devproject misago
-        - pylint misago/acl misago/admin misago/cache
+        - black --check devproject misago

+ 0 - 1
misago/acl/tests/test_providers.py

@@ -1,4 +1,3 @@
-# pylint: disable=protected-access
 import pytest
 import pytest
 
 
 from ...conf import settings
 from ...conf import settings

+ 0 - 2
misago/admin/middleware.py

@@ -16,5 +16,3 @@ class AdminAuthMiddleware(MiddlewareMixin):
                     return login(request)
                     return login(request)
                 return redirect("%s:index" % request.admin_namespace)
                 return redirect("%s:index" % request.admin_namespace)
             auth.update_admin_session(request)
             auth.update_admin_session(request)
-
-        return None

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

@@ -15,7 +15,6 @@ def get_protected_namespace(request):
                 return namespace
                 return namespace
         except NoReverseMatch:
         except NoReverseMatch:
             pass
             pass
-    return None
 
 
 
 
 def render(request, template, context=None, error_page=False):
 def render(request, template, context=None, error_page=False):

+ 0 - 1
misago/admin/views/generic/list.py

@@ -250,7 +250,6 @@ class ListView(AdminView):
         for order_by, _ in self.ordering:  # pylint: disable=not-an-iterable
         for order_by, _ in self.ordering:  # pylint: disable=not-an-iterable
             if order_by == new_ordering:
             if order_by == new_ordering:
                 return order_by
                 return order_by
-        return None
 
 
     def get_ordering_methods(self, request):
     def get_ordering_methods(self, request):
         return {
         return {

+ 0 - 1
misago/cache/test.py

@@ -4,7 +4,6 @@ from .versions import get_cache_versions_from_db
 class assert_invalidates_cache:
 class assert_invalidates_cache:
     def __init__(self, cache):
     def __init__(self, cache):
         self.cache = cache
         self.cache = cache
-        self.versions = None
 
 
     def __enter__(self):
     def __enter__(self):
         self.versions = get_cache_versions_from_db()
         self.versions = get_cache_versions_from_db()

+ 9 - 15
misago/categories/forms.py

@@ -29,8 +29,7 @@ class AdminCategoryFieldMixin:
         level = getattr(obj, obj._mptt_meta.level_attr) - self.base_level
         level = getattr(obj, obj._mptt_meta.level_attr) - self.base_level
         if level > 0:
         if level > 0:
             return mark_safe(conditional_escape(self.level_indicator) * level)
             return mark_safe(conditional_escape(self.level_indicator) * level)
-        else:
-            return ""
+        return ""
 
 
 
 
 class AdminCategoryChoiceField(AdminCategoryFieldMixin, TreeNodeChoiceField):
 class AdminCategoryChoiceField(AdminCategoryFieldMixin, TreeNodeChoiceField):
@@ -56,7 +55,8 @@ class CategoryFormBase(forms.ModelForm):
         label=_("CSS class"),
         label=_("CSS class"),
         required=False,
         required=False,
         help_text=_(
         help_text=_(
-            "Optional CSS class used to customize this category appearance from templates."
+            "Optional CSS class used to customize this "
+            "category's appearance from templates."
         ),
         ),
     )
     )
     is_closed = YesNoSwitch(
     is_closed = YesNoSwitch(
@@ -66,13 +66,6 @@ class CategoryFormBase(forms.ModelForm):
             "Only members with valid permissions can post in closed categories."
             "Only members with valid permissions can post in closed categories."
         ),
         ),
     )
     )
-    css_class = forms.CharField(
-        label=_("CSS class"),
-        required=False,
-        help_text=_(
-            "Optional CSS class used to customize this category appearance from templates."
-        ),
-    )
     require_threads_approval = YesNoSwitch(
     require_threads_approval = YesNoSwitch(
         label=_("Threads"),
         label=_("Threads"),
         required=False,
         required=False,
@@ -91,23 +84,24 @@ class CategoryFormBase(forms.ModelForm):
         label=_("Edits"),
         label=_("Edits"),
         required=False,
         required=False,
         help_text=_(
         help_text=_(
-            "Will make all edited replies return to unapproved state for moderator to review."
+            "Will make all edited replies return to unapproved state "
+            "for moderator to review."
         ),
         ),
     )
     )
     prune_started_after = forms.IntegerField(
     prune_started_after = forms.IntegerField(
         label=_("Thread age"),
         label=_("Thread age"),
         min_value=0,
         min_value=0,
         help_text=_(
         help_text=_(
-            "Prune thread if number of days since its creation is greater than specified. "
-            "Enter 0 to disable this pruning criteria."
+            "Prune thread if number of days since its creation is greater than "
+            "specified. Enter 0 to disable this pruning criteria."
         ),
         ),
     )
     )
     prune_replied_after = forms.IntegerField(
     prune_replied_after = forms.IntegerField(
         label=_("Last reply"),
         label=_("Last reply"),
         min_value=0,
         min_value=0,
         help_text=_(
         help_text=_(
-            "Prune thread if number of days since last reply is greater than specified. "
-            "Enter 0 to disable this pruning criteria."
+            "Prune thread if number of days since last reply is greater than "
+            "specified. Enter 0 to disable this pruning criteria."
         ),
         ),
     )
     )
 
 

+ 1 - 1
misago/categories/management/commands/prunecategories.py

@@ -14,7 +14,7 @@ class Command(BaseCommand):
 
 
     help = "Prunes categories"
     help = "Prunes categories"
 
 
-    def handle(self, *args, **options):
+    def handle(self, *args, **options):  # pylint: disable=too-many-branches
         now = timezone.now()
         now = timezone.now()
         synchronize_categories = []
         synchronize_categories = []
 
 

+ 1 - 5
misago/categories/permissions.py

@@ -1,5 +1,4 @@
 from django import forms
 from django import forms
-from django.contrib.auth import get_user_model
 from django.core.exceptions import PermissionDenied
 from django.core.exceptions import PermissionDenied
 from django.http import Http404
 from django.http import Http404
 from django.utils.translation import gettext_lazy as _
 from django.utils.translation import gettext_lazy as _
@@ -7,7 +6,6 @@ from django.utils.translation import gettext_lazy as _
 from ..acl import algebra
 from ..acl import algebra
 from ..acl.decorators import return_boolean
 from ..acl.decorators import return_boolean
 from ..admin.forms import YesNoSwitch
 from ..admin.forms import YesNoSwitch
-from ..users.models import AnonymousUser
 from .models import Category, CategoryRole, RoleCategoryACL
 from .models import Category, CategoryRole, RoleCategoryACL
 
 
 
 
@@ -21,8 +19,6 @@ class PermissionsForm(forms.Form):
 def change_permissions_form(role):
 def change_permissions_form(role):
     if isinstance(role, CategoryRole):
     if isinstance(role, CategoryRole):
         return PermissionsForm
         return PermissionsForm
-    else:
-        return None
 
 
 
 
 def build_acl(acl, roles, key_name):
 def build_acl(acl, roles, key_name):
@@ -53,7 +49,7 @@ def build_category_acl(acl, category, categories_roles, key_name):
         if category.parent_id not in acl["visible_categories"]:
         if category.parent_id not in acl["visible_categories"]:
             # dont bother with child categories of invisible parents
             # dont bother with child categories of invisible parents
             return
             return
-        elif not acl["categories"][category.parent_id]["can_browse"]:
+        if not acl["categories"][category.parent_id]["can_browse"]:
             # parent's visible, but its contents aint
             # parent's visible, but its contents aint
             return
             return
 
 

+ 0 - 2
misago/categories/serializers.py

@@ -69,7 +69,6 @@ class CategorySerializer(serializers.ModelSerializer, MutableFields):
                 "plain": obj.description,
                 "plain": obj.description,
                 "html": format_plaintext_for_html(obj.description),
                 "html": format_plaintext_for_html(obj.description),
             }
             }
-        return None
 
 
     def get_is_read(self, obj):
     def get_is_read(self, obj):
         try:
         try:
@@ -100,7 +99,6 @@ class CategorySerializer(serializers.ModelSerializer, MutableFields):
                     kwargs={"slug": obj.last_poster_slug, "pk": obj.last_poster_id},
                     kwargs={"slug": obj.last_poster_slug, "pk": obj.last_poster_id},
                 ),
                 ),
             }
             }
-        return None
 
 
     def get_url(self, obj):
     def get_url(self, obj):
         return {
         return {

+ 20 - 22
misago/categories/tests/test_permissions_admin_views.py

@@ -128,14 +128,14 @@ class CategoryRoleAdminViewsTests(AdminTestCase):
         root = Category.objects.root_category()
         root = Category.objects.root_category()
         for descendant in root.get_descendants():
         for descendant in root.get_descendants():
             descendant.delete()
             descendant.delete()
-        """
-        Create categories tree for test cases:
-
-        Category A
-          + Category B
-        Category C
-          + Category D
-        """
+
+        # Create categories tree for test cases:
+        #
+        # Category A
+        #   + Category B
+        # Category C
+        #   + Category D
+
         root = Category.objects.root_category()
         root = Category.objects.root_category()
         self.client.post(
         self.client.post(
             reverse("misago:admin:categories:nodes:new"),
             reverse("misago:admin:categories:nodes:new"),
@@ -149,9 +149,8 @@ class CategoryRoleAdminViewsTests(AdminTestCase):
         test_category = Category.objects.get(slug="category-a")
         test_category = Category.objects.get(slug="category-a")
 
 
         self.assertEqual(Category.objects.count(), 3)
         self.assertEqual(Category.objects.count(), 3)
-        """
-        Create test roles
-        """
+
+        # Create test roles
         self.client.post(
         self.client.post(
             reverse("misago:admin:permissions:users:new"),
             reverse("misago:admin:permissions:users:new"),
             data=mock_role_form_data(Role(), {"name": "Test Role A"}),
             data=mock_role_form_data(Role(), {"name": "Test Role A"}),
@@ -175,9 +174,8 @@ class CategoryRoleAdminViewsTests(AdminTestCase):
 
 
         role_comments = CategoryRole.objects.get(name="Test Comments")
         role_comments = CategoryRole.objects.get(name="Test Comments")
         role_full = CategoryRole.objects.get(name="Test Full")
         role_full = CategoryRole.objects.get(name="Test Full")
-        """
-        Test view itself
-        """
+
+        # Test view itself
         # See if form page is rendered
         # See if form page is rendered
         response = self.client.get(
         response = self.client.get(
             reverse(
             reverse(
@@ -246,14 +244,14 @@ class CategoryRoleAdminViewsTests(AdminTestCase):
             )
             )
         )
         )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
-        """
-        Create categories tree for test cases:
-
-        Category A
-          + Category B
-        Category C
-          + Category D
-        """
+
+        # Create categories tree for test cases:
+        #
+        # Category A
+        #   + Category B
+        # Category C
+        #   + Category D
+
         root = Category.objects.root_category()
         root = Category.objects.root_category()
         self.client.post(
         self.client.post(
             reverse("misago:admin:categories:nodes:new"),
             reverse("misago:admin:categories:nodes:new"),

+ 3 - 1
misago/categories/utils.py

@@ -3,7 +3,9 @@ from ..readtracker import categoriestracker
 from .models import Category
 from .models import Category
 
 
 
 
-def get_categories_tree(user, user_acl, parent=None, join_posters=False):
+def get_categories_tree(
+    user, user_acl, parent=None, join_posters=False
+):  # pylint: disable=too-many-branches
     if not user_acl["visible_categories"]:
     if not user_acl["visible_categories"]:
         return []
         return []
 
 

+ 18 - 22
misago/categories/views/permsadmin.py

@@ -41,22 +41,24 @@ class RoleFormMixin:
                     valid_forms += 1
                     valid_forms += 1
 
 
             form = CategoryRoleForm(request.POST, instance=target)
             form = CategoryRoleForm(request.POST, instance=target)
-            if form.is_valid() and len(perms_forms) == valid_forms:
-                new_permissions = {}
-                for permissions_form in perms_forms:
-                    cleaned_data = permissions_form.cleaned_data
-                    new_permissions[permissions_form.prefix] = cleaned_data
-
-                form.instance.permissions = new_permissions
-                form.instance.save()
-
-                messages.success(request, self.message_submit % {"name": target.name})
+            if form.is_valid():
+                if len(perms_forms) == valid_forms:
+                    new_permissions = {}
+                    for permissions_form in perms_forms:
+                        cleaned_data = permissions_form.cleaned_data
+                        new_permissions[permissions_form.prefix] = cleaned_data
+
+                    form.instance.permissions = new_permissions
+                    form.instance.save()
+
+                    messages.success(
+                        request, self.message_submit % {"name": target.name}
+                    )
 
 
-                if "stay" in request.POST:
-                    return redirect(request.path)
-                else:
+                    if "stay" in request.POST:
+                        return redirect(request.path)
                     return redirect(self.root_link)
                     return redirect(self.root_link)
-            elif form.is_valid() and len(perms_forms) != valid_forms:
+
                 form.add_error(None, _("Form contains errors."))
                 form.add_error(None, _("Form contains errors."))
 
 
         return self.render(
         return self.render(
@@ -85,8 +87,6 @@ class DeleteCategoryRole(CategoryRoleAdmin, generic.ButtonView):
 
 
 
 
 class CategoryPermissions(CategoryAdmin, generic.ModelFormView):
 class CategoryPermissions(CategoryAdmin, generic.ModelFormView):
-    """category roles view for assinging roles to category, add link to it in categories list"""
-
     templates_dir = "misago/admin/categoryroles"
     templates_dir = "misago/admin/categoryroles"
     template = "categoryroles.html"
     template = "categoryroles.html"
 
 
@@ -132,8 +132,7 @@ class CategoryPermissions(CategoryAdmin, generic.ModelFormView):
             messages.success(request, message % {"name": target.name})
             messages.success(request, message % {"name": target.name})
             if "stay" in request.POST:
             if "stay" in request.POST:
                 return redirect(request.path)
                 return redirect(request.path)
-            else:
-                return redirect(self.root_link)
+            return redirect(self.root_link)
 
 
         return self.render(request, {"forms": forms, "target": target})
         return self.render(request, {"forms": forms, "target": target})
 
 
@@ -147,8 +146,6 @@ CategoriesList.add_item_action(
 
 
 
 
 class RoleCategoriesACL(RoleAdmin, generic.ModelFormView):
 class RoleCategoriesACL(RoleAdmin, generic.ModelFormView):
-    """role categories view for assinging categories to role, add link to it in user roles list"""
-
     templates_dir = "misago/admin/categoryroles"
     templates_dir = "misago/admin/categoryroles"
     template = "rolecategories.html"
     template = "rolecategories.html"
 
 
@@ -200,8 +197,7 @@ class RoleCategoriesACL(RoleAdmin, generic.ModelFormView):
             messages.success(request, message % {"name": target.name})
             messages.success(request, message % {"name": target.name})
             if "stay" in request.POST:
             if "stay" in request.POST:
                 return redirect(request.path)
                 return redirect(request.path)
-            else:
-                return redirect(self.root_link)
+            return redirect(self.root_link)
 
 
         return self.render(request, {"forms": forms, "target": target})
         return self.render(request, {"forms": forms, "target": target})
 
 

+ 2 - 4
misago/core/apipatch.py

@@ -50,8 +50,7 @@ class ApiPatch:
         patch["detail"] = detail
         patch["detail"] = detail
         if is_errored:
         if is_errored:
             return Response(patch, status=400)
             return Response(patch, status=400)
-        else:
-            return Response(patch)
+        return Response(patch)
 
 
     def dispatch_bulk(self, request, targets):
     def dispatch_bulk(self, request, targets):
         is_errored = False
         is_errored = False
@@ -79,8 +78,7 @@ class ApiPatch:
 
 
         if is_errored:
         if is_errored:
             return Response(result, status=400)
             return Response(result, status=400)
-        else:
-            return Response(result)
+        return Response(result)
 
 
     def validate_action(self, action):
     def validate_action(self, action):
         if not action.get("op"):
         if not action.get("op"):

+ 1 - 2
misago/core/context_processors.py

@@ -44,5 +44,4 @@ def momentjs_locale(request):
 def frontend_context(request):
 def frontend_context(request):
     if request.include_frontend_context:
     if request.include_frontend_context:
         return {"frontend_context": request.frontend_context}
         return {"frontend_context": request.frontend_context}
-    else:
-        return {}
+    return {}

+ 2 - 4
misago/core/decorators.py

@@ -7,8 +7,7 @@ def ajax_only(f):
     def decorator(request, *args, **kwargs):
     def decorator(request, *args, **kwargs):
         if not request.is_ajax():
         if not request.is_ajax():
             return not_allowed(request)
             return not_allowed(request)
-        else:
-            return f(request, *args, **kwargs)
+        return f(request, *args, **kwargs)
 
 
     return decorator
     return decorator
 
 
@@ -17,8 +16,7 @@ def require_POST(f):
     def decorator(request, *args, **kwargs):
     def decorator(request, *args, **kwargs):
         if not request.method == "POST":
         if not request.method == "POST":
             return not_allowed(request)
             return not_allowed(request)
-        else:
-            return f(request, *args, **kwargs)
+        return f(request, *args, **kwargs)
 
 
     return decorator
     return decorator
 
 

+ 11 - 13
misago/core/errorpages.py

@@ -40,15 +40,13 @@ def banned(request, exception):
 def permission_denied(request, exception):
 def permission_denied(request, exception):
     if request.is_ajax():
     if request.is_ajax():
         return _ajax_error(403, exception, _("Permission denied."))
         return _ajax_error(403, exception, _("Permission denied."))
-    else:
-        return _error_page(request, 403, exception)
+    return _error_page(request, 403, exception)
 
 
 
 
 def page_not_found(request, exception):
 def page_not_found(request, exception):
     if request.is_ajax():
     if request.is_ajax():
         return _ajax_error(404, exception, "Not found.")
         return _ajax_error(404, exception, "Not found.")
-    else:
-        return _error_page(request, 404, exception)
+    return _error_page(request, 404, exception)
 
 
 
 
 def social_auth_failed(request, exception):
 def social_auth_failed(request, exception):
@@ -61,6 +59,7 @@ def social_auth_failed(request, exception):
         backend_name = exception.backend_name
         backend_name = exception.backend_name
     except AttributeError:
     except AttributeError:
         pass
         pass
+
     try:
     try:
         exception_backend = exception.backend
         exception_backend = exception.backend
         backend_name = get_social_auth_backend_name(exception_backend.name)
         backend_name = get_social_auth_backend_name(exception_backend.name)
@@ -69,7 +68,8 @@ def social_auth_failed(request, exception):
 
 
     if isinstance(exception, social_exceptions.NotAllowedToDisconnect):
     if isinstance(exception, social_exceptions.NotAllowedToDisconnect):
         message = _(
         message = _(
-            "A problem was encountered when disconnecting your account from the remote site."
+            "A problem was encountered when disconnecting your account "
+            "from the remote site."
         )
         )
         help_text = _(
         help_text = _(
             "You are not allowed to disconnect your account from the other site, "
             "You are not allowed to disconnect your account from the other site, "
@@ -108,10 +108,10 @@ def social_auth_failed(request, exception):
 def csrf_failure(request, reason=""):
 def csrf_failure(request, reason=""):
     if request.is_ajax():
     if request.is_ajax():
         return _ajax_error(403, _("Request authentication is invalid."))
         return _ajax_error(403, _("Request authentication is invalid."))
-    else:
-        response = render(request, "misago/errorpages/csrf_failure.html")
-        response.status_code = 403
-        return response
+
+    response = render(request, "misago/errorpages/csrf_failure.html")
+    response.status_code = 403
+    return response
 
 
 
 
 def not_allowed(request):
 def not_allowed(request):
@@ -123,8 +123,7 @@ def shared_403_exception_handler(f):
     def page_decorator(request, *args, **kwargs):
     def page_decorator(request, *args, **kwargs):
         if is_request_to_misago(request):
         if is_request_to_misago(request):
             return permission_denied(request, *args, **kwargs)
             return permission_denied(request, *args, **kwargs)
-        else:
-            return f(request, *args, **kwargs)
+        return f(request, *args, **kwargs)
 
 
     return page_decorator
     return page_decorator
 
 
@@ -133,7 +132,6 @@ def shared_404_exception_handler(f):
     def page_decorator(request, *args, **kwargs):
     def page_decorator(request, *args, **kwargs):
         if is_request_to_misago(request):
         if is_request_to_misago(request):
             return page_not_found(request, *args, **kwargs)
             return page_not_found(request, *args, **kwargs)
-        else:
-            return f(request, *args, **kwargs)
+        return f(request, *args, **kwargs)
 
 
     return page_decorator
     return page_decorator

+ 1 - 4
misago/core/exceptionhandler.py

@@ -83,8 +83,7 @@ def get_exception_handler(exception):
     for exception_type, handler in EXCEPTION_HANDLERS:
     for exception_type, handler in EXCEPTION_HANDLERS:
         if isinstance(exception, exception_type):
         if isinstance(exception, exception_type):
             return handler
             return handler
-    else:
-        raise ValueError("%s is not Misago exception" % exception.__class__.__name__)
+    raise ValueError("%s is not a Misago exception" % exception.__class__.__name__)
 
 
 
 
 def handle_misago_exception(request, exception):
 def handle_misago_exception(request, exception):
@@ -103,5 +102,3 @@ def handle_api_exception(exception, context):
             except IndexError:
             except IndexError:
                 pass
                 pass
         return response
         return response
-    else:
-        return None

+ 1 - 4
misago/core/exceptions.py

@@ -1,3 +1,4 @@
+# pylint: disable=super-init-not-called
 from django.core.exceptions import PermissionDenied
 from django.core.exceptions import PermissionDenied
 from social_core.exceptions import AuthException
 from social_core.exceptions import AuthException
 
 
@@ -35,10 +36,6 @@ class SocialAuthBanned(AuthException):
 class ExplicitFirstPage(Exception):
 class ExplicitFirstPage(Exception):
     """The url that was used to reach view contained explicit first page"""
     """The url that was used to reach view contained explicit first page"""
 
 
-    pass
-
 
 
 class OutdatedSlug(Exception):
 class OutdatedSlug(Exception):
     """The url that was used to reach view contained outdated slug"""
     """The url that was used to reach view contained outdated slug"""
-
-    pass

+ 0 - 2
misago/core/middleware.py

@@ -11,8 +11,6 @@ class ExceptionHandlerMiddleware(MiddlewareMixin):
 
 
         if request_is_to_misago and misago_can_handle_exception:
         if request_is_to_misago and misago_can_handle_exception:
             return exceptionhandler.handle_misago_exception(request, exception)
             return exceptionhandler.handle_misago_exception(request, exception)
-        else:
-            return None
 
 
 
 
 class FrontendContextMiddleware(MiddlewareMixin):
 class FrontendContextMiddleware(MiddlewareMixin):

+ 0 - 2
misago/core/momentjs.py

@@ -8,8 +8,6 @@ def get_locale_url(language):
     if clean_language:
     if clean_language:
         return MOMENT_STATIC_PATH % clean_language
         return MOMENT_STATIC_PATH % clean_language
 
 
-    return None
-
 
 
 def clean_language_name(language):
 def clean_language_name(language):
     # lowercase language
     # lowercase language

+ 12 - 11
misago/core/page.py

@@ -22,7 +22,10 @@ class Page:
         while self._unsorted_list:
         while self._unsorted_list:
             iterations += 1
             iterations += 1
             if iterations > 512:
             if iterations > 512:
-                message = "%s page hierarchy is invalid or too complex  to resolve. Sections left: %s"
+                message = (
+                    "%s page hierarchy is invalid or too complex to resolve. "
+                    "Sections left: %s"
+                )
                 raise ValueError(message % self._unsorted_list)
                 raise ValueError(message % self._unsorted_list)
 
 
             for index, section in enumerate(self._unsorted_list):
             for index, section in enumerate(self._unsorted_list):
@@ -50,9 +53,9 @@ class Page:
                     new_sorted_list.append(inserted_section)
                     new_sorted_list.append(inserted_section)
                     self._sorted_list = new_sorted_list
                     self._sorted_list = new_sorted_list
                     return True
                     return True
-            else:
-                return False
-        elif before:
+            return False
+
+        if before:
             new_sorted_list = []
             new_sorted_list = []
             for section in self._sorted_list:
             for section in self._sorted_list:
                 if section["link"] == before:
                 if section["link"] == before:
@@ -60,13 +63,11 @@ class Page:
                     new_sorted_list.append(section)
                     new_sorted_list.append(section)
                     self._sorted_list = new_sorted_list
                     self._sorted_list = new_sorted_list
                     return True
                     return True
-                else:
-                    new_sorted_list.append(section)
-            else:
-                return False
-        else:
-            self._sorted_list.append(inserted_section)
-            return True
+                new_sorted_list.append(section)
+            return False
+
+        self._sorted_list.append(inserted_section)
+        return True
 
 
     def add_section(
     def add_section(
         self,
         self,

+ 13 - 12
misago/core/pgutils.py

@@ -1,4 +1,3 @@
-from django.core.paginator import Paginator
 from django.db.models import Index
 from django.db.models import Index
 
 
 
 
@@ -6,11 +5,13 @@ class PgPartialIndex(Index):
     suffix = "part"
     suffix = "part"
     max_name_length = 31
     max_name_length = 31
 
 
-    def __init__(self, fields=[], name=None, where=None):
+    def __init__(self, fields=None, name=None, where=None):
         if not where:
         if not where:
             raise ValueError("partial index requires WHERE clause")
             raise ValueError("partial index requires WHERE clause")
         self.where = where
         self.where = where
 
 
+        fields = fields or []
+
         super().__init__(fields, name)
         super().__init__(fields, name)
 
 
     def set_name_with_model(self, model):
     def set_name_with_model(self, model):
@@ -37,18 +38,18 @@ class PgPartialIndex(Index):
         self.check_name()
         self.check_name()
 
 
     def __repr__(self):
     def __repr__(self):
-        if self.where is not None:
-            where_items = []
-            for key in sorted(self.where.keys()):
-                where_items.append("=".join([key, repr(self.where[key])]))
-            return "<%(name)s: fields=%(fields)s, where=%(where)s>" % {
-                "name": self.__class__.__name__,
-                "fields": "'%s'" % (", ".join(self.fields)),
-                "where": "'%s'" % (", ".join(where_items)),
-            }
-        else:
+        if self.where is None:
             return super().__repr__()
             return super().__repr__()
 
 
+        where_items = []
+        for key in sorted(self.where.keys()):
+            where_items.append("=".join([key, repr(self.where[key])]))
+        return "<%(name)s: fields=%(fields)s, where=%(where)s>" % {
+            "name": self.__class__.__name__,
+            "fields": "'%s'" % (", ".join(self.fields)),
+            "where": "'%s'" % (", ".join(where_items)),
+        }
+
     def deconstruct(self):
     def deconstruct(self):
         path, args, kwargs = super().deconstruct()
         path, args, kwargs = super().deconstruct()
         kwargs["where"] = self.where
         kwargs["where"] = self.where

+ 2 - 3
misago/core/shortcuts.py

@@ -16,7 +16,7 @@ def paginate(
 
 
     if page in (1, "1") and not allow_explicit_first_page:
     if page in (1, "1") and not allow_explicit_first_page:
         raise ExplicitFirstPage()
         raise ExplicitFirstPage()
-    elif not page:
+    if not page:
         page = 1
         page = 1
 
 
     paginator = paginator or Paginator
     paginator = paginator or Paginator
@@ -85,5 +85,4 @@ def validate_slug(model, slug):
 def get_int_or_404(value):
 def get_int_or_404(value):
     if str(value).isdigit():
     if str(value).isdigit():
         return int(value)
         return int(value)
-    else:
-        raise Http404()
+    raise Http404()

+ 1 - 1
misago/core/testproject/urlswitherrorhandlers.py

@@ -1,4 +1,4 @@
-from .urls import *
+from .urls import *  # pylint: disable=wildcard-import, unused-wildcard-import
 
 
 handler403 = "misago.core.testproject.views.mock_custom_403_error_page"
 handler403 = "misago.core.testproject.views.mock_custom_403_error_page"
 handler404 = "misago.core.testproject.views.mock_custom_404_error_page"
 handler404 = "misago.core.testproject.views.mock_custom_404_error_page"

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

@@ -66,7 +66,7 @@ class ErrorPageViewsTests(TestCase):
         self.assertContains(response, "page-error-social", status_code=403)
         self.assertContains(response, "page-error-social", status_code=403)
 
 
     def test_social_failed_message(self):
     def test_social_failed_message(self):
-        """misago-specific social auth failed exception error page returns 403 with message"""
+        """misago-specific social auth failed error page returns 403 with message"""
         response = self.client.get(reverse("raise-social-auth-failed-message"))
         response = self.client.get(reverse("raise-social-auth-failed-message"))
         self.assertContains(response, "page-error-social", status_code=403)
         self.assertContains(response, "page-error-social", status_code=403)
         self.assertContains(
         self.assertContains(
@@ -74,7 +74,7 @@ class ErrorPageViewsTests(TestCase):
         )
         )
 
 
     def test_social_auth_banned(self):
     def test_social_auth_banned(self):
-        """misago-specific social auth banned exception error page returns 403 with ban message"""
+        """misago-specific social auth banned error page returns 403 with ban message"""
         response = self.client.get(reverse("raise-social-auth-banned"))
         response = self.client.get(reverse("raise-social-auth-banned"))
         self.assertContains(response, "page-error-social", status_code=403)
         self.assertContains(response, "page-error-social", status_code=403)
         self.assertContains(response, "Banned in auth!", status_code=403)
         self.assertContains(response, "Banned in auth!", status_code=403)

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

@@ -30,7 +30,7 @@ class JsI18nUrlTests(TestCase):
                     response = self.client.get(reverse("django-i18n"))
                     response = self.client.get(reverse("django-i18n"))
                     if response.status_code != 200:
                     if response.status_code != 200:
                         failed_languages.append(language)
                         failed_languages.append(language)
-            except:
+            except Exception:  # pylint: disable=broad-except
                 failed_languages.append(language)
                 failed_languages.append(language)
 
 
         if failed_languages:
         if failed_languages:

+ 7 - 1
misago/core/tests/test_utils.py

@@ -95,7 +95,13 @@ PLAINTEXT_FORMAT_CASES = [
     ("Lorem ipsum.\n\nDolor met.", "<p>Lorem ipsum.</p>\n\n<p>Dolor met.</p>"),
     ("Lorem ipsum.\n\nDolor met.", "<p>Lorem ipsum.</p>\n\n<p>Dolor met.</p>"),
     (
     (
         "http://misago-project.org/login/",
         "http://misago-project.org/login/",
-        '<p><a href="http://misago-project.org/login/">http://misago-project.org/login/</a></p>',
+        (
+            "<p>"
+            '<a href="http://misago-project.org/login/">'
+            "http://misago-project.org/login/"
+            "</a>"
+            "</p>"
+        ),
     ),
     ),
 ]
 ]
 
 

+ 4 - 5
misago/core/utils.py

@@ -28,13 +28,13 @@ def parse_iso8601_string(value):
     """turns ISO 8601 string into datetime object"""
     """turns ISO 8601 string into datetime object"""
     value = force_text(value, strings_only=True).rstrip("Z")
     value = force_text(value, strings_only=True).rstrip("Z")
 
 
-    for format in ISO8601_FORMATS:
+    for format_str in ISO8601_FORMATS:
         try:
         try:
-            parsed_value = datetime.strptime(value, format)
+            parsed_value = datetime.strptime(value, format_str)
             break
             break
         except ValueError:
         except ValueError:
             try:
             try:
-                parsed_value = datetime.strptime(value[:-6], format)
+                parsed_value = datetime.strptime(value[:-6], format_str)
                 break
                 break
             except ValueError:
             except ValueError:
                 pass
                 pass
@@ -67,8 +67,7 @@ def clean_return_path(request):
     """return path utility that returns return path from referer or POST"""
     """return path utility that returns return path from referer or POST"""
     if request.method == "POST" and "return_path" in request.POST:
     if request.method == "POST" and "return_path" in request.POST:
         return _get_return_path_from_post(request)
         return _get_return_path_from_post(request)
-    else:
-        return _get_return_path_from_referer(request)
+    return _get_return_path_from_referer(request)
 
 
 
 
 def _get_return_path_from_post(request):
 def _get_return_path_from_post(request):

+ 3 - 4
misago/faker/management/commands/createfakebans.py

@@ -28,8 +28,7 @@ def fake_username_ban(fake):
 def fake_email_ban(fake):
 def fake_email_ban(fake):
     if random.randint(0, 100) < 35:
     if random.randint(0, 100) < 35:
         return "*@%s" % fake.domain_name()
         return "*@%s" % fake.domain_name()
-    else:
-        return fake.email()
+    return fake.email()
 
 
 
 
 def fake_ip_ban(fake):
 def fake_ip_ban(fake):
@@ -69,9 +68,9 @@ def fake_ip_ban(fake):
 def create_fake_test(fake, test_type):
 def create_fake_test(fake, test_type):
     if test_type == Ban.USERNAME:
     if test_type == Ban.USERNAME:
         return fake_username_ban(fake)
         return fake_username_ban(fake)
-    elif test_type == Ban.EMAIL:
+    if test_type == Ban.EMAIL:
         return fake_email_ban(fake)
         return fake_email_ban(fake)
-    elif test_type == Ban.IP:
+    if test_type == Ban.IP:
         return fake_ip_ban(fake)
         return fake_ip_ban(fake)
 
 
 
 

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

@@ -29,7 +29,7 @@ class Command(BaseCommand):
             default=0,
             default=0,
         )
         )
 
 
-    def handle(self, *args, **options):
+    def handle(self, *args, **options):  # pylint: disable=too-many-locals
         items_to_create = options["categories"]
         items_to_create = options["categories"]
         min_level = options["minlevel"]
         min_level = options["minlevel"]
 
 

+ 3 - 1
misago/faker/management/commands/createfakethreads.py

@@ -33,7 +33,9 @@ class Command(BaseCommand):
             default=5,
             default=5,
         )
         )
 
 
-    def handle(self, *args, **options):
+    def handle(
+        self, *args, **options
+    ):  # pylint: disable=too-many-branches, too-many-locals
         items_to_create = options["threads"]
         items_to_create = options["threads"]
 
 
         categories = list(Category.objects.all_categories())
         categories = list(Category.objects.all_categories())

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

@@ -53,7 +53,10 @@ class Command(BaseCommand):
                     rank=random.choice(ranks),
                     rank=random.choice(ranks),
                 )
                 )
 
 
-                dynamic.set_avatar(user)
+                if random.randint(0, 100) < 80:
+                    gallery.set_random_avatar(user)
+                else:
+                    dynamic.set_avatar(user)
                 user.save(update_fields=["avatars"])
                 user.save(update_fields=["avatars"])
             except (ValidationError, IntegrityError):
             except (ValidationError, IntegrityError):
                 pass
                 pass

+ 4 - 4
misago/legal/forms.py

@@ -16,10 +16,10 @@ class AgreementForm(forms.ModelForm):
     is_active = forms.BooleanField(
     is_active = forms.BooleanField(
         label=_("Set as active for its type"),
         label=_("Set as active for its type"),
         help_text=_(
         help_text=_(
-            "If other agreement is already active for this type, it will be unset and replaced "
-            "with this one. "
-            "Misago will ask users who didn't accept this agreement to do so before allowing them "
-            "to continue using the site's features."
+            "If other agreement is already active for this type, it will be unset "
+            "and replaced with this one. "
+            "Misago will ask users who didn't accept this agreement to do so "
+            "before allowing them to continue using the site's features."
         ),
         ),
         required=False,
         required=False,
     )
     )

+ 1 - 1
misago/legal/tests/test_admin_views.py

@@ -22,7 +22,7 @@ class AgreementAdminViewsTests(AdminTestCase):
 
 
     def test_mass_delete(self):
     def test_mass_delete(self):
         """adminview deletes multiple agreements"""
         """adminview deletes multiple agreements"""
-        for i in range(10):
+        for _ in range(10):
             response = self.client.post(
             response = self.client.post(
                 reverse("misago:admin:users:agreements:new"),
                 reverse("misago:admin:users:agreements:new"),
                 data={"type": Agreement.TYPE_TOS, "text": "test agreement!"},
                 data={"type": Agreement.TYPE_TOS, "text": "test agreement!"},

+ 5 - 7
misago/legal/utils.py

@@ -31,8 +31,6 @@ def get_required_user_agreement(user, agreements):
                 # possible stale cache
                 # possible stale cache
                 Agreement.invalidate_cache()
                 Agreement.invalidate_cache()
 
 
-    return None
-
 
 
 def get_parsed_agreement_text(request, agreement):
 def get_parsed_agreement_text(request, agreement):
     if not agreement.text:
     if not agreement.text:
@@ -48,11 +46,11 @@ def get_parsed_agreement_text(request, agreement):
 
 
     if cached_content and cached_content.get("checksum") == unparsed_checksum:
     if cached_content and cached_content.get("checksum") == unparsed_checksum:
         return cached_content["parsed"]
         return cached_content["parsed"]
-    else:
-        parsed = common_flavour(request, None, unparsed_content)["parsed_text"]
-        cached_content = {"checksum": unparsed_checksum, "parsed": parsed}
-        cache.set(cache_name, cached_content)
-        return cached_content["parsed"]
+
+    parsed = common_flavour(request, None, unparsed_content)["parsed_text"]
+    cached_content = {"checksum": unparsed_checksum, "parsed": parsed}
+    cache.set(cache_name, cached_content)
+    return cached_content["parsed"]
 
 
 
 
 def save_user_agreement_acceptance(user, agreement, commit=False):
 def save_user_agreement_acceptance(user, agreement, commit=False):

+ 1 - 2
misago/markup/bbcode/blocks.py

@@ -60,8 +60,7 @@ class QuotePreprocessor(Preprocessor):
 
 
         if title:
         if title:
             return "\n\n%s%s\n\n%s\n\n%s\n\n" % (QUOTE_START, title, text, QUOTE_END)
             return "\n\n%s%s\n\n%s\n\n%s\n\n" % (QUOTE_START, title, text, QUOTE_END)
-        else:
-            return "\n\n%s\n\n%s\n\n%s\n\n" % (QUOTE_START, text, QUOTE_END)
+        return "\n\n%s\n\n%s\n\n%s\n\n" % (QUOTE_START, text, QUOTE_END)
 
 
 
 
 class QuoteBlockProcessor(BlockProcessor):
 class QuoteBlockProcessor(BlockProcessor):

+ 1 - 1
misago/markup/bbcode/inline.py

@@ -18,7 +18,7 @@ class SimpleBBCodePattern(SimpleTagPattern):
     Case insensitive simple BBCode
     Case insensitive simple BBCode
     """
     """
 
 
-    def __init__(self, bbcode, tag=None):
+    def __init__(self, bbcode, tag=None):  # pylint: disable=super-init-not-called
         self.pattern = r"(\[%s\](.*?)\[/%s\])" % (bbcode, bbcode)
         self.pattern = r"(\[%s\](.*?)\[/%s\])" % (bbcode, bbcode)
         self.compiled_re = re.compile(
         self.compiled_re = re.compile(
             "^(.*?)%s(.*?)$" % self.pattern, re.DOTALL | re.UNICODE | re.IGNORECASE
             "^(.*?)%s(.*?)$" % self.pattern, re.DOTALL | re.UNICODE | re.IGNORECASE

+ 3 - 3
misago/markup/mentions.py

@@ -57,9 +57,9 @@ def parse_string(request, element, mentions_dict):
         if mentions_dict[username]:
         if mentions_dict[username]:
             user = mentions_dict[username]
             user = mentions_dict[username]
             return '<a href="%s">@%s</a>' % (user.get_absolute_url(), user.username)
             return '<a href="%s">@%s</a>' % (user.get_absolute_url(), user.username)
-        else:
-            # we've failed to resolve user for username
-            return matchobj.group(0)
+
+        # we've failed to resolve user for username
+        return matchobj.group(0)
 
 
     replaced_string = USERNAME_RE.sub(replace_mentions, element.string)
     replaced_string = USERNAME_RE.sub(replace_mentions, element.string)
     element.replace_with(BeautifulSoup(replaced_string, "html.parser"))
     element.replace_with(BeautifulSoup(replaced_string, "html.parser"))

+ 0 - 2
misago/markup/parser.py

@@ -1,5 +1,3 @@
-import warnings
-
 import bleach
 import bleach
 import markdown
 import markdown
 from bs4 import BeautifulSoup
 from bs4 import BeautifulSoup

+ 3 - 1
misago/markup/tests/test_api.py

@@ -81,7 +81,9 @@ class ParseMarkupApiTests(AuthenticatedUserTestCase):
         self.assertEqual(
         self.assertEqual(
             response.json(),
             response.json(),
             {
             {
-                "detail": "Posted message should be at least 5 characters long (it has 3)."
+                "detail": (
+                    "Posted message should be at least 5 characters long (it has 3)."
+                )
             },
             },
         )
         )
 
 

+ 8 - 6
misago/markup/tests/test_mentions.py

@@ -59,9 +59,10 @@ class MentionsTests(AuthenticatedUserTestCase):
             self.user.username
             self.user.username
         )
         )
 
 
-        after = '<p>Hello <a href="{0}">@{1}</a> and <a href="{0}">@{1}</a>, how is it going?</p>'.format(
-            self.user.get_absolute_url(), self.user.username
-        )
+        after = (
+            # pylint: disable=line-too-long
+            '<p>Hello <a href="{0}">@{1}</a> and <a href="{0}">@{1}</a>, how is it going?</p>'
+        ).format(self.user.get_absolute_url(), self.user.username)
 
 
         result = {"parsed_text": before, "mentions": []}
         result = {"parsed_text": before, "mentions": []}
 
 
@@ -75,9 +76,10 @@ class MentionsTests(AuthenticatedUserTestCase):
             self.user.username
             self.user.username
         )
         )
 
 
-        after = '<p>Hello <a href="{0}">@{1}</a></p><p><a href="{0}">@{1}</a>, how is it going?</p>'.format(
-            self.user.get_absolute_url(), self.user.username
-        )
+        after = (
+            # pylint: disable=line-too-long
+            '<p>Hello <a href="{0}">@{1}</a></p><p><a href="{0}">@{1}</a>, how is it going?</p>'
+        ).format(self.user.get_absolute_url(), self.user.username)
 
 
         result = {"parsed_text": before, "mentions": []}
         result = {"parsed_text": before, "mentions": []}
 
 

+ 6 - 8
misago/readtracker/dates.py

@@ -14,12 +14,10 @@ def get_cutoff_date(user=None):
 
 
 
 
 def is_date_tracked(date, user):
 def is_date_tracked(date, user):
-    if date:
-        cutoff_date = get_cutoff_date()
-
-        if cutoff_date < user.joined_on:
-            cutoff_date = user.joined_on
-
-        return date > cutoff_date
-    else:
+    if not date:
         return False
         return False
+
+    cutoff_date = get_cutoff_date()
+    if cutoff_date < user.joined_on:
+        cutoff_date = user.joined_on
+    return date > cutoff_date

+ 3 - 21
misago/readtracker/tests/test_categoriestracker.py

@@ -10,7 +10,6 @@ from ...conf import settings
 from ...conftest import get_cache_versions
 from ...conftest import get_cache_versions
 from ...threads import test
 from ...threads import test
 from ...users.test import create_test_user
 from ...users.test import create_test_user
-from ..models import PostRead
 
 
 cache_versions = get_cache_versions()
 cache_versions = get_cache_versions()
 
 
@@ -165,24 +164,9 @@ class CategoriesTrackerTests(TestCase):
         self.assertFalse(self.category.is_read)
         self.assertFalse(self.category.is_read)
         self.assertTrue(self.category.is_new)
         self.assertTrue(self.category.is_new)
 
 
-    def test_user_first_read_post_unapproved_own_post(self):
-        """tracked thread with read first post and unapproved own post"""
-        thread = test.post_thread(self.category, started_on=timezone.now())
-        poststracker.save_read(self.user, thread.first_post)
-
-        test.reply_thread(
-            thread, posted_on=timezone.now(), poster=self.user, is_unapproved=True
-        )
-
-        categoriestracker.make_read_aware(self.user, self.user_acl, self.category)
-        self.assertFalse(self.category.is_read)
-        self.assertTrue(self.category.is_new)
-
     def test_user_unapproved_thread_unread_post(self):
     def test_user_unapproved_thread_unread_post(self):
         """tracked unapproved thread"""
         """tracked unapproved thread"""
-        thread = test.post_thread(
-            self.category, started_on=timezone.now(), is_unapproved=True
-        )
+        test.post_thread(self.category, started_on=timezone.now(), is_unapproved=True)
 
 
         categoriestracker.make_read_aware(self.user, self.user_acl, self.category)
         categoriestracker.make_read_aware(self.user, self.user_acl, self.category)
         self.assertTrue(self.category.is_read)
         self.assertTrue(self.category.is_read)
@@ -190,7 +174,7 @@ class CategoriesTrackerTests(TestCase):
 
 
     def test_user_unapproved_own_thread_unread_post(self):
     def test_user_unapproved_own_thread_unread_post(self):
         """tracked unapproved but visible thread"""
         """tracked unapproved but visible thread"""
-        thread = test.post_thread(
+        test.post_thread(
             self.category,
             self.category,
             poster=self.user,
             poster=self.user,
             started_on=timezone.now(),
             started_on=timezone.now(),
@@ -203,9 +187,7 @@ class CategoriesTrackerTests(TestCase):
 
 
     def test_user_hidden_thread_unread_post(self):
     def test_user_hidden_thread_unread_post(self):
         """tracked hidden thread"""
         """tracked hidden thread"""
-        thread = test.post_thread(
-            self.category, started_on=timezone.now(), is_hidden=True
-        )
+        test.post_thread(self.category, started_on=timezone.now(), is_hidden=True)
 
 
         categoriestracker.make_read_aware(self.user, self.user_acl, self.category)
         categoriestracker.make_read_aware(self.user, self.user_acl, self.category)
         self.assertTrue(self.category.is_read)
         self.assertTrue(self.category.is_read)

+ 1 - 1
misago/readtracker/tests/test_dates.py

@@ -40,7 +40,7 @@ class ReadTrackerDatesTests(TestCase):
         self.assertTrue(returned_cutoff_date > valid_cutoff_date)
         self.assertTrue(returned_cutoff_date > valid_cutoff_date)
         self.assertEqual(returned_cutoff_date, user.joined_on)
         self.assertEqual(returned_cutoff_date, user.joined_on)
 
 
-    def test_get_cutoff_date_user(self):
+    def test_get_cutoff_date_anonymous_user(self):
         """passing anonymous user to get_cutoff_date has no effect"""
         """passing anonymous user to get_cutoff_date has no effect"""
         user = MockAnonymousUser()
         user = MockAnonymousUser()
 
 

+ 0 - 1
misago/readtracker/tests/test_poststracker.py

@@ -7,7 +7,6 @@ from ...categories.models import Category
 from ...conf import settings
 from ...conf import settings
 from ...threads import test
 from ...threads import test
 from ...users.test import create_test_user
 from ...users.test import create_test_user
-from ..models import PostRead
 from ..poststracker import make_read_aware, save_read
 from ..poststracker import make_read_aware, save_read
 
 
 
 

+ 0 - 1
misago/readtracker/tests/test_threadstracker.py

@@ -11,7 +11,6 @@ from ...conf import settings
 from ...conftest import get_cache_versions
 from ...conftest import get_cache_versions
 from ...threads import test
 from ...threads import test
 from ...users.test import create_test_user
 from ...users.test import create_test_user
-from ..models import PostRead
 
 
 cache_versions = get_cache_versions()
 cache_versions = get_cache_versions()
 
 

+ 0 - 1
misago/search/context_processors.py

@@ -1,4 +1,3 @@
-from django.core.exceptions import PermissionDenied
 from django.urls import reverse
 from django.urls import reverse
 
 
 from .searchproviders import searchproviders
 from .searchproviders import searchproviders

+ 0 - 2
misago/search/permissions.py

@@ -15,8 +15,6 @@ class PermissionsForm(forms.Form):
 def change_permissions_form(role):
 def change_permissions_form(role):
     if isinstance(role, Role):
     if isinstance(role, Role):
         return PermissionsForm
         return PermissionsForm
-    else:
-        return None
 
 
 
 
 def build_acl(acl, roles, key_name):
 def build_acl(acl, roles, key_name):

+ 4 - 1
misago/search/tests/test_searchproviders.py

@@ -53,7 +53,10 @@ class SearchProvidersTests(TestCase):
         self.assertEqual(searchproviders.get_providers("REQUEST")[0].request, "REQUEST")
         self.assertEqual(searchproviders.get_providers("REQUEST")[0].request, "REQUEST")
 
 
     def test_get_allowed_providers(self):
     def test_get_allowed_providers(self):
-        """get_allowed_providers returns only providers that didn't raise in allow_search"""
+        """
+        allowed providers getter returns only providers that didn't raise an exception
+        in allow_search
+        """
         searchproviders = SearchProviders([])
         searchproviders = SearchProviders([])
 
 
         searchproviders._initialized = True
         searchproviders._initialized = True

+ 0 - 1
misago/search/views.py

@@ -1,7 +1,6 @@
 from django.core.exceptions import PermissionDenied
 from django.core.exceptions import PermissionDenied
 from django.http import Http404
 from django.http import Http404
 from django.shortcuts import redirect, render
 from django.shortcuts import redirect, render
-from django.urls import reverse
 from django.utils.translation import gettext as _
 from django.utils.translation import gettext as _
 
 
 from .searchproviders import searchproviders
 from .searchproviders import searchproviders

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

@@ -101,7 +101,8 @@ def validate_filesize(upload, filetype, hard_limit):
 
 
     if filetype.size_limit and upload.size > filetype.size_limit * 1024:
     if filetype.size_limit and upload.size > filetype.size_limit * 1024:
         message = _(
         message = _(
-            "You can't upload files of this type larger than %(limit)s (your file has %(upload)s)."
+            "You can't upload files of this type larger "
+            "than %(limit)s (your file has %(upload)s)."
         )
         )
         raise ValidationError(
         raise ValidationError(
             message
             message

+ 0 - 5
misago/threads/api/postendpoints/delete.py

@@ -1,16 +1,11 @@
-from django.core.exceptions import PermissionDenied
-from django.utils.translation import gettext as _
-from django.utils.translation import ngettext
 from rest_framework.response import Response
 from rest_framework.response import Response
 
 
 from ....conf import settings
 from ....conf import settings
-from ....core.utils import clean_ids_list
 from ...moderation import posts as moderation
 from ...moderation import posts as moderation
 from ...permissions import (
 from ...permissions import (
     allow_delete_best_answer,
     allow_delete_best_answer,
     allow_delete_event,
     allow_delete_event,
     allow_delete_post,
     allow_delete_post,
-    exclude_invisible_posts,
 )
 )
 from ...serializers import DeletePostsSerializer
 from ...serializers import DeletePostsSerializer
 
 

+ 2 - 5
misago/threads/api/postendpoints/patch_event.py

@@ -1,6 +1,3 @@
-from django.core.exceptions import PermissionDenied
-from django.utils.translation import gettext as _
-
 from ....acl.objectacl import add_acl_to_obj
 from ....acl.objectacl import add_acl_to_obj
 from ....core.apipatch import ApiPatch
 from ....core.apipatch import ApiPatch
 from ...moderation import posts as moderation
 from ...moderation import posts as moderation
@@ -14,8 +11,7 @@ def patch_acl(request, event, value):
     if value:
     if value:
         add_acl_to_obj(request.user_acl, event)
         add_acl_to_obj(request.user_acl, event)
         return {"acl": event.acl}
         return {"acl": event.acl}
-    else:
-        return {"acl": None}
+    return {"acl": None}
 
 
 
 
 event_patch_dispatcher.add("acl", patch_acl)
 event_patch_dispatcher.add("acl", patch_acl)
@@ -46,4 +42,5 @@ def event_patch_endpoint(request, event):
 
 
         event.category.synchronize()
         event.category.synchronize()
         event.category.save()
         event.category.save()
+
     return response
     return response

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

@@ -27,8 +27,7 @@ def patch_acl(request, post, value):
     if value:
     if value:
         add_acl_to_obj(request.user_acl, post)
         add_acl_to_obj(request.user_acl, post)
         return {"acl": post.acl}
         return {"acl": post.acl}
-    else:
-        return {"acl": None}
+    return {"acl": None}
 
 
 
 
 post_patch_dispatcher.add("acl", patch_acl)
 post_patch_dispatcher.add("acl", patch_acl)

+ 5 - 3
misago/threads/api/postingendpoint/__init__.py

@@ -9,7 +9,7 @@ from ....conf import settings
 
 
 
 
 class PostingInterrupt(Exception):
 class PostingInterrupt(Exception):
-    def __init__(self, message):
+    def __init__(self, message):  # pylint: disable=super-init-not-called
         if not message:
         if not message:
             raise ValueError("You have to provide PostingInterrupt message.")
             raise ValueError("You have to provide PostingInterrupt message.")
         self.message = message
         self.message = message
@@ -118,7 +118,8 @@ class PostingEndpoint:
                 obj.pre_save(self._serializers.get(middleware))
                 obj.pre_save(self._serializers.get(middleware))
         except PostingInterrupt as e:
         except PostingInterrupt as e:
             raise ValueError(
             raise ValueError(
-                "Posting process can only be interrupted from within interrupt_posting method"
+                "Posting process can only be interrupted "
+                "from within interrupt_posting method"
             )
             )
 
 
         try:
         try:
@@ -134,7 +135,8 @@ class PostingEndpoint:
                 obj.post_save(self._serializers.get(middleware))
                 obj.post_save(self._serializers.get(middleware))
         except PostingInterrupt as e:
         except PostingInterrupt as e:
             raise ValueError(
             raise ValueError(
-                "Posting process can only be interrupted from within interrupt_posting method"
+                "Posting process can only be interrupted "
+                "from within interrupt_posting method"
             )
             )
 
 
 
 

+ 3 - 1
misago/threads/api/postingendpoint/attachments.py

@@ -59,7 +59,8 @@ class AttachmentsSerializer(serializers.Serializer):
                     self.removed_attachments.append(attachment)
                     self.removed_attachments.append(attachment)
                 else:
                 else:
                     message = _(
                     message = _(
-                        'You don\'t have permission to remove "%(attachment)s" attachment.'
+                        "You don't have permission to remove "
+                        '"%(attachment)s" attachment.'
                     )
                     )
                     raise serializers.ValidationError(
                     raise serializers.ValidationError(
                         message % {"attachment": attachment.filename}
                         message % {"attachment": attachment.filename}
@@ -123,6 +124,7 @@ class AttachmentsSerializer(serializers.Serializer):
 def validate_attachments_count(data):
 def validate_attachments_count(data):
     total_attachments = len(data)
     total_attachments = len(data)
     if total_attachments > settings.MISAGO_POST_ATTACHMENTS_LIMIT:
     if total_attachments > settings.MISAGO_POST_ATTACHMENTS_LIMIT:
+        # pylint: disable=line-too-long
         message = ngettext(
         message = ngettext(
             "You can't attach more than %(limit_value)s file to single post (added %(show_value)s).",
             "You can't attach more than %(limit_value)s file to single post (added %(show_value)s).",
             "You can't attach more than %(limit_value)s flies to single post (added %(show_value)s).",
             "You can't attach more than %(limit_value)s flies to single post (added %(show_value)s).",

+ 5 - 2
misago/threads/api/postingendpoint/category.py

@@ -13,7 +13,9 @@ from ...threadtypes import trees_map
 
 
 
 
 class CategoryMiddleware(PostingMiddleware):
 class CategoryMiddleware(PostingMiddleware):
-    """middleware that validates category id and sets category on thread and post instances"""
+    """
+    middleware that validates category id and sets category on thread and post instances
+    """
 
 
     def use_this_middleware(self):
     def use_this_middleware(self):
         if self.mode == PostingEndpoint.START:
         if self.mode == PostingEndpoint.START:
@@ -66,7 +68,8 @@ class CategorySerializer(serializers.Serializer):
         except Category.DoesNotExist:
         except Category.DoesNotExist:
             raise serializers.ValidationError(
             raise serializers.ValidationError(
                 _(
                 _(
-                    "Selected category doesn't exist or you don't have permission to browse it."
+                    "Selected category doesn't exist or "
+                    "you don't have permission to browse it."
                 )
                 )
             )
             )
         except PermissionDenied as e:
         except PermissionDenied as e:

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

@@ -49,7 +49,8 @@ class ParticipantsSerializer(serializers.Serializer):
             if clean_name == self.context["user"].slug:
             if clean_name == self.context["user"].slug:
                 raise serializers.ValidationError(
                 raise serializers.ValidationError(
                     _(
                     _(
-                        "You can't include yourself on the list of users to invite to new thread."
+                        "You can't include yourself on the "
+                        "list of users to invite to new thread."
                     )
                     )
                 )
                 )
 
 
@@ -61,6 +62,7 @@ class ParticipantsSerializer(serializers.Serializer):
 
 
         max_participants = self.context["user_acl"]["max_private_thread_participants"]
         max_participants = self.context["user_acl"]["max_private_thread_participants"]
         if max_participants and len(clean_usernames) > max_participants:
         if max_participants and len(clean_usernames) > max_participants:
+            # pylint: disable=line-too-long
             message = ngettext(
             message = ngettext(
                 "You can't add more than %(users)s user to private thread (you've added %(added)s).",
                 "You can't add more than %(users)s user to private thread (you've added %(added)s).",
                 "You can't add more than %(users)s users to private thread (you've added %(added)s).",
                 "You can't add more than %(users)s users to private thread (you've added %(added)s).",
@@ -85,7 +87,7 @@ class ParticipantsSerializer(serializers.Serializer):
             users.append(user)
             users.append(user)
 
 
         if len(usernames) != len(users):
         if len(usernames) != len(users):
-            invalid_usernames = set(usernames) - set([u.slug for u in users])
+            invalid_usernames = set(usernames) - {u.slug for u in users}
             sorted_usernames = sorted(invalid_usernames)
             sorted_usernames = sorted(invalid_usernames)
 
 
             message = _("One or more users could not be found: %(usernames)s")
             message = _("One or more users could not be found: %(usernames)s")

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

@@ -11,9 +11,11 @@ from ...validators import validate_post, validate_post_length, validate_thread_t
 class ReplyMiddleware(PostingMiddleware):
 class ReplyMiddleware(PostingMiddleware):
     def get_serializer(self):
     def get_serializer(self):
         if self.mode == PostingEndpoint.START:
         if self.mode == PostingEndpoint.START:
-            return ThreadSerializer(data=self.request.data, context=self.kwargs)
+            serializer = ThreadSerializer
         else:
         else:
-            return ReplySerializer(data=self.request.data, context=self.kwargs)
+            serializer = ReplySerializer
+
+        return serializer(data=self.request.data, context=self.kwargs)
 
 
     def save(self, serializer):
     def save(self, serializer):
         if self.mode == PostingEndpoint.START:
         if self.mode == PostingEndpoint.START:
@@ -93,10 +95,10 @@ class ReplySerializer(serializers.Serializer):
     def parse_post(self, post):
     def parse_post(self, post):
         if self.context["mode"] == PostingEndpoint.START:
         if self.context["mode"] == PostingEndpoint.START:
             return common_flavour(self.context["request"], self.context["user"], post)
             return common_flavour(self.context["request"], self.context["user"], post)
-        else:
-            return common_flavour(
-                self.context["request"], self.context["post"].poster, post
-            )
+
+        return common_flavour(
+            self.context["request"], self.context["post"].poster, post
+        )
 
 
 
 
 class ThreadSerializer(ReplySerializer):
 class ThreadSerializer(ReplySerializer):

+ 3 - 3
misago/threads/api/threadendpoints/delete.py

@@ -25,9 +25,9 @@ def delete_bulk(request, viewmodel):
             if "details" in errors:
             if "details" in errors:
                 return Response(hydrate_error_details(errors["details"]), status=400)
                 return Response(hydrate_error_details(errors["details"]), status=400)
             return Response({"detail": errors[0]}, status=403)
             return Response({"detail": errors[0]}, status=403)
-        else:
-            errors = list(serializer.errors)[0][0]
-            return Response({"detail": errors}, status=400)
+
+        errors = list(serializer.errors)[0][0]
+        return Response({"detail": errors}, status=400)
 
 
     for thread in serializer.validated_data["threads"]:
     for thread in serializer.validated_data["threads"]:
         with transaction.atomic():
         with transaction.atomic():

+ 4 - 2
misago/threads/api/threadendpoints/editor.py

@@ -47,7 +47,8 @@ def thread_start_editor(request):
             }
             }
         )
         )
 
 
-    # list only categories that allow new threads, or contains subcategory that allows one
+    # list only categories that allow new threads,
+    # or contains subcategory that allows one
     cleaned_categories = []
     cleaned_categories = []
     for category in reversed(categories):
     for category in reversed(categories):
         if category["id"] in available:
         if category["id"] in available:
@@ -56,7 +57,8 @@ def thread_start_editor(request):
     if not cleaned_categories:
     if not cleaned_categories:
         raise PermissionDenied(
         raise PermissionDenied(
             _(
             _(
-                "No categories that allow new threads are available to you at the moment."
+                "No categories that allow new threads "
+                "are available to you at the moment."
             )
             )
         )
         )
 
 

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

@@ -31,7 +31,9 @@ class ThreadsList:
         )
         )
 
 
     def get_threads(self, request, category, list_type, page):
     def get_threads(self, request, category, list_type, page):
-        return self.threads(request, category, list_type, page)
+        return self.threads(  # pylint: disable=not-callable
+            request, category, list_type, page
+        )
 
 
     def get_response_json(self, request, category, threads):
     def get_response_json(self, request, category, threads):
         return threads.get_frontend_context()
         return threads.get_frontend_context()
@@ -43,8 +45,7 @@ class ForumThreadsList(ThreadsList):
     def get_category(self, request, pk=None):
     def get_category(self, request, pk=None):
         if pk:
         if pk:
             return ThreadsCategory(request, pk=pk)
             return ThreadsCategory(request, pk=pk)
-        else:
-            return ThreadsRootCategory(request)
+        return ThreadsRootCategory(request)
 
 
 
 
 class PrivateThreadsList(ThreadsList):
 class PrivateThreadsList(ThreadsList):

+ 5 - 6
misago/threads/api/threadendpoints/merge.py

@@ -1,6 +1,4 @@
 from django.core.exceptions import PermissionDenied
 from django.core.exceptions import PermissionDenied
-from django.utils.translation import gettext as _
-from rest_framework.exceptions import ValidationError
 from rest_framework.response import Response
 from rest_framework.response import Response
 
 
 from ....acl.objectacl import add_acl_to_obj
 from ....acl.objectacl import add_acl_to_obj
@@ -16,7 +14,9 @@ from ...serializers import (
 )
 )
 
 
 
 
-def thread_merge_endpoint(request, thread, viewmodel):
+def thread_merge_endpoint(
+    request, thread, viewmodel
+):  # pylint: disable=too-many-branches
     allow_merge_thread(request.user_acl, thread)
     allow_merge_thread(request.user_acl, thread)
 
 
     serializer = MergeThreadSerializer(
     serializer = MergeThreadSerializer(
@@ -97,11 +97,10 @@ def threads_merge_endpoint(request):
         if "threads" in serializer.errors:
         if "threads" in serializer.errors:
             errors = {"detail": serializer.errors["threads"][0]}
             errors = {"detail": serializer.errors["threads"][0]}
             return Response(errors, status=403)
             return Response(errors, status=403)
-        elif "non_field_errors" in serializer.errors:
+        if "non_field_errors" in serializer.errors:
             errors = {"detail": serializer.errors["non_field_errors"][0]}
             errors = {"detail": serializer.errors["non_field_errors"][0]}
             return Response(errors, status=403)
             return Response(errors, status=403)
-        else:
-            return Response(serializer.errors, status=400)
+        return Response(serializer.errors, status=400)
 
 
     threads = serializer.validated_data["threads"]
     threads = serializer.validated_data["threads"]
     invalid_threads = []
     invalid_threads = []

+ 16 - 11
misago/threads/api/threadendpoints/patch.py

@@ -54,8 +54,7 @@ def patch_acl(request, thread, value):
     if value:
     if value:
         add_acl_to_obj(request.user_acl, thread)
         add_acl_to_obj(request.user_acl, thread)
         return {"acl": thread.acl}
         return {"acl": thread.acl}
-    else:
-        return {"acl": None}
+    return {"acl": None}
 
 
 
 
 thread_patch_dispatcher.add("acl", patch_acl)
 thread_patch_dispatcher.add("acl", patch_acl)
@@ -204,7 +203,8 @@ def patch_subscription(request, thread, value):
         )
         )
 
 
         return {"subscription": False}
         return {"subscription": False}
-    elif value == "email":
+
+    if value == "email":
         thread.subscription = request.user.subscription_set.create(
         thread.subscription = request.user.subscription_set.create(
             thread=thread,
             thread=thread,
             category=thread.category,
             category=thread.category,
@@ -213,8 +213,8 @@ def patch_subscription(request, thread, value):
         )
         )
 
 
         return {"subscription": True}
         return {"subscription": True}
-    else:
-        return {"subscription": None}
+
+    return {"subscription": None}
 
 
 
 
 thread_patch_dispatcher.replace("subscription", patch_subscription)
 thread_patch_dispatcher.replace("subscription", patch_subscription)
@@ -272,7 +272,8 @@ def patch_unmark_best_answer(request, thread, value):
     if not post.is_best_answer:
     if not post.is_best_answer:
         raise PermissionDenied(
         raise PermissionDenied(
             _(
             _(
-                "This post can't be unmarked because it's not currently marked as best answer."
+                "This post can't be unmarked because "
+                "it's not currently marked as best answer."
             )
             )
         )
         )
 
 
@@ -321,6 +322,7 @@ thread_patch_dispatcher.add("participants", patch_add_participant)
 
 
 
 
 def patch_remove_participant(request, thread, value):
 def patch_remove_participant(request, thread, value):
+    # pylint: disable=undefined-loop-variable
     try:
     try:
         user_id = int(value)
         user_id = int(value)
     except (ValueError, TypeError):
     except (ValueError, TypeError):
@@ -337,17 +339,18 @@ def patch_remove_participant(request, thread, value):
 
 
     if len(thread.participants_list) == 1:
     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)
 
 
-        return {"deleted": False, "participants": participants.data}
+    make_participants_aware(request.user, thread)
+    participants = ThreadParticipantSerializer(thread.participants_list, many=True)
+
+    return {"deleted": False, "participants": participants.data}
 
 
 
 
 thread_patch_dispatcher.remove("participants", patch_remove_participant)
 thread_patch_dispatcher.remove("participants", patch_remove_participant)
 
 
 
 
 def patch_replace_owner(request, thread, value):
 def patch_replace_owner(request, thread, value):
+    # pylint: disable=undefined-loop-variable
     try:
     try:
         user_id = int(value)
         user_id = int(value)
     except (ValueError, TypeError):
     except (ValueError, TypeError):
@@ -405,7 +408,9 @@ def thread_patch_endpoint(request, thread):
     return response
     return response
 
 
 
 
-def bulk_patch_endpoint(request, viewmodel):
+def bulk_patch_endpoint(
+    request, viewmodel
+):  # pylint: disable=too-many-branches, too-many-locals
     serializer = BulkPatchSerializer(data=request.data)
     serializer = BulkPatchSerializer(data=request.data)
     if not serializer.is_valid():
     if not serializer.is_valid():
         return Response(serializer.errors, status=400)
         return Response(serializer.errors, status=400)

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

@@ -31,7 +31,9 @@ class ViewSet(viewsets.ViewSet):
     thread = None
     thread = None
 
 
     def get_thread(self, request, thread_pk):
     def get_thread(self, request, thread_pk):
-        return self.thread(request, get_int_or_404(thread_pk)).unwrap()
+        return self.thread(  # pylint: disable=not-callable
+            request, get_int_or_404(thread_pk)
+        ).unwrap()
 
 
     def get_poll(self, thread, pk):
     def get_poll(self, thread, pk):
         try:
         try:
@@ -120,8 +122,7 @@ class ViewSet(viewsets.ViewSet):
     def votes(self, request, thread_pk, pk=None):
     def votes(self, request, thread_pk, pk=None):
         if request.method == "POST":
         if request.method == "POST":
             return self.post_votes(request, thread_pk, pk)
             return self.post_votes(request, thread_pk, pk)
-        else:
-            return self.get_votes(request, thread_pk, pk)
+        return self.get_votes(request, thread_pk, pk)
 
 
     @transaction.atomic
     @transaction.atomic
     def post_votes(self, request, thread_pk, pk=None):
     def post_votes(self, request, thread_pk, pk=None):

+ 42 - 45
misago/threads/api/threadposts.py

@@ -32,7 +32,7 @@ class ViewSet(viewsets.ViewSet):
     def get_thread(
     def get_thread(
         self, request, pk, path_aware=False, read_aware=False, subscription_aware=False
         self, request, pk, path_aware=False, read_aware=False, subscription_aware=False
     ):
     ):
-        return self.thread(
+        return self.thread(  # pylint: disable=not-callable
             request,
             request,
             get_int_or_404(pk),
             get_int_or_404(pk),
             path_aware=path_aware,
             path_aware=path_aware,
@@ -95,21 +95,21 @@ class ViewSet(viewsets.ViewSet):
             request, PostingEndpoint.REPLY, thread=thread, post=post
             request, PostingEndpoint.REPLY, thread=thread, post=post
         )
         )
 
 
-        if posting.is_valid():
-            user_posts = request.user.posts
+        if not posting.is_valid():
+            return Response(posting.errors, status=400)
 
 
-            posting.save()
+        user_posts = request.user.posts
 
 
-            # setup extra data for serialization
-            post.is_read = False
-            post.is_new = True
-            post.poster.posts = user_posts + 1
+        posting.save()
 
 
-            make_users_status_aware(request, [post.poster])
+        # setup extra data for serialization
+        post.is_read = False
+        post.is_new = True
+        post.poster.posts = user_posts + 1
 
 
-            return Response(PostSerializer(post, context={"user": request.user}).data)
-        else:
-            return Response(posting.errors, status=400)
+        make_users_status_aware(request, [post.poster])
+
+        return Response(PostSerializer(post, context={"user": request.user}).data)
 
 
     @transaction.atomic
     @transaction.atomic
     def update(self, request, thread_pk, pk=None):
     def update(self, request, thread_pk, pk=None):
@@ -122,21 +122,21 @@ class ViewSet(viewsets.ViewSet):
             request, PostingEndpoint.EDIT, thread=thread, post=post
             request, PostingEndpoint.EDIT, thread=thread, post=post
         )
         )
 
 
-        if posting.is_valid():
-            post_edits = post.edits
+        if not posting.is_valid():
+            return Response(posting.errors, status=400)
 
 
-            posting.save()
+        post_edits = post.edits
 
 
-            post.is_read = True
-            post.is_new = False
-            post.edits = post_edits + 1
+        posting.save()
 
 
-            if post.poster:
-                make_users_status_aware(request, [post.poster])
+        post.is_read = True
+        post.is_new = False
+        post.edits = post_edits + 1
 
 
-            return Response(PostSerializer(post, context={"user": request.user}).data)
-        else:
-            return Response(posting.errors, status=400)
+        if post.poster:
+            make_users_status_aware(request, [post.poster])
+
+        return Response(PostSerializer(post, context={"user": request.user}).data)
 
 
     def patch(self, request, thread_pk):
     def patch(self, request, thread_pk):
         thread = self.get_thread(request, thread_pk)
         thread = self.get_thread(request, thread_pk)
@@ -149,8 +149,7 @@ class ViewSet(viewsets.ViewSet):
 
 
         if post.is_event:
         if post.is_event:
             return event_patch_endpoint(request, post)
             return event_patch_endpoint(request, post)
-        else:
-            return post_patch_endpoint(request, post)
+        return post_patch_endpoint(request, post)
 
 
     @transaction.atomic
     @transaction.atomic
     def delete(self, request, thread_pk, pk=None):
     def delete(self, request, thread_pk, pk=None):
@@ -165,9 +164,7 @@ class ViewSet(viewsets.ViewSet):
     @detail_route(methods=["post"])
     @detail_route(methods=["post"])
     def read(self, request, thread_pk, pk=None):
     def read(self, request, thread_pk, pk=None):
         thread = self.get_thread(request, thread_pk, subscription_aware=True).unwrap()
         thread = self.get_thread(request, thread_pk, subscription_aware=True).unwrap()
-
         post = self.get_post(request, thread, pk).unwrap()
         post = self.get_post(request, thread, pk).unwrap()
-
         return post_read_endpoint(request, thread, post)
         return post_read_endpoint(request, thread, post)
 
 
     @detail_route(methods=["get"], url_path="editor")
     @detail_route(methods=["get"], url_path="editor")
@@ -202,26 +199,26 @@ class ViewSet(viewsets.ViewSet):
         thread = self.get_thread(request, thread_pk).unwrap()
         thread = self.get_thread(request, thread_pk).unwrap()
         allow_reply_thread(request.user_acl, thread)
         allow_reply_thread(request.user_acl, thread)
 
 
-        if "reply" in request.query_params:
-            reply_to = self.get_post(
-                request, thread, request.query_params["reply"]
-            ).unwrap()
-
-            if reply_to.is_event:
-                raise PermissionDenied(_("You can't reply to events."))
-            if reply_to.is_hidden and not reply_to.acl["can_see_hidden"]:
-                raise PermissionDenied(_("You can't reply to hidden posts."))
-
-            return Response(
-                {
-                    "id": reply_to.pk,
-                    "post": reply_to.original,
-                    "poster": reply_to.poster_name,
-                }
-            )
-        else:
+        if "reply" not in request.query_params:
             return Response({})
             return Response({})
 
 
+        reply_to = self.get_post(
+            request, thread, request.query_params["reply"]
+        ).unwrap()
+
+        if reply_to.is_event:
+            raise PermissionDenied(_("You can't reply to events."))
+        if reply_to.is_hidden and not reply_to.acl["can_see_hidden"]:
+            raise PermissionDenied(_("You can't reply to hidden posts."))
+
+        return Response(
+            {
+                "id": reply_to.pk,
+                "post": reply_to.original,
+                "poster": reply_to.poster_name,
+            }
+        )
+
     @detail_route(methods=["get", "post"])
     @detail_route(methods=["get", "post"])
     def edits(self, request, thread_pk, pk=None):
     def edits(self, request, thread_pk, pk=None):
         if request.method == "GET":
         if request.method == "GET":

+ 15 - 24
misago/threads/api/threads.py

@@ -8,7 +8,6 @@ from rest_framework.response import Response
 from ...categories import PRIVATE_THREADS_ROOT_NAME, THREADS_ROOT_NAME
 from ...categories import PRIVATE_THREADS_ROOT_NAME, THREADS_ROOT_NAME
 from ...core.shortcuts import get_int_or_404
 from ...core.shortcuts import get_int_or_404
 from ..models import Post, Thread
 from ..models import Post, Thread
-from ..moderation import threads as moderation
 from ..permissions import allow_use_private_threads
 from ..permissions import allow_use_private_threads
 from ..viewmodels import (
 from ..viewmodels import (
     ForumThread,
     ForumThread,
@@ -30,7 +29,7 @@ class ViewSet(viewsets.ViewSet):
     def get_thread(
     def get_thread(
         self, request, pk, path_aware=False, read_aware=False, subscription_aware=False
         self, request, pk, path_aware=False, read_aware=False, subscription_aware=False
     ):
     ):
-        return self.thread(
+        return self.thread(  # pylint: disable=not-callable
             request,
             request,
             get_int_or_404(pk),
             get_int_or_404(pk),
             path_aware=path_aware,
             path_aware=path_aware,
@@ -82,19 +81,15 @@ class ThreadViewSet(ViewSet):
             post=post,
             post=post,
         )
         )
 
 
-        if posting.is_valid():
-            posting.save()
-
-            return Response(
-                {
-                    "id": thread.pk,
-                    "title": thread.title,
-                    "url": thread.get_absolute_url(),
-                }
-            )
-        else:
+        if not posting.is_valid():
             return Response(posting.errors, status=400)
             return Response(posting.errors, status=400)
 
 
+        posting.save()
+
+        return Response(
+            {"id": thread.pk, "title": thread.title, "url": thread.get_absolute_url()}
+        )
+
     @detail_route(methods=["post"], url_path="merge")
     @detail_route(methods=["post"], url_path="merge")
     @transaction.atomic
     @transaction.atomic
     def thread_merge(self, request, pk=None):
     def thread_merge(self, request, pk=None):
@@ -139,15 +134,11 @@ class PrivateThreadViewSet(ViewSet):
             post=post,
             post=post,
         )
         )
 
 
-        if posting.is_valid():
-            posting.save()
-
-            return Response(
-                {
-                    "id": thread.pk,
-                    "title": thread.title,
-                    "url": thread.get_absolute_url(),
-                }
-            )
-        else:
+        if not posting.is_valid():
             return Response(posting.errors, status=400)
             return Response(posting.errors, status=400)
+
+        posting.save()
+
+        return Response(
+            {"id": thread.pk, "title": thread.title, "url": thread.get_absolute_url()}
+        )

+ 2 - 4
misago/threads/filtersearch.py

@@ -8,8 +8,6 @@ SEARCH_FILTERS = list(map(import_string, filters_list))
 
 
 def filter_search(search, filters=None):
 def filter_search(search, filters=None):
     filters = filters or SEARCH_FILTERS
     filters = filters or SEARCH_FILTERS
-
-    for filter in filters:
-        search = filter(search) or search
-
+    for search_filter in filters:
+        search = search_filter(search) or search
     return search
     return search

+ 14 - 13
misago/threads/forms.py

@@ -61,10 +61,12 @@ class AttachmentTypeForm(forms.ModelForm):
         }
         }
         help_texts = {
         help_texts = {
             "extensions": _(
             "extensions": _(
-                "List of comma separated file extensions associated with this attachment type."
+                "List of comma separated file extensions associated with this "
+                "attachment type."
             ),
             ),
             "mimetypes": _(
             "mimetypes": _(
-                "Optional list of comma separated mime types associated with this attachment type."
+                "Optional list of comma separated mime types associated with this "
+                "attachment type."
             ),
             ),
             "size_limit": _(
             "size_limit": _(
                 "Maximum allowed uploaded file size for this type, in kb. "
                 "Maximum allowed uploaded file size for this type, in kb. "
@@ -72,16 +74,16 @@ class AttachmentTypeForm(forms.ModelForm):
             ),
             ),
             "status": _("Controls this attachment type availability on your site."),
             "status": _("Controls this attachment type availability on your site."),
             "limit_uploads_to": _(
             "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."
+                "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": _(
             "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."
+                "If you wish to limit option to download files of this type to users "
+                "with specific roles, select them on this list. Otherwhise don't "
+                "select any roles to allow all users with permission to download "
+                "attachments to be able to download attachments of this type."
             ),
             ),
         }
         }
         widgets = {
         widgets = {
@@ -97,9 +99,8 @@ class AttachmentTypeForm(forms.ModelForm):
 
 
     def clean_mimetypes(self):
     def clean_mimetypes(self):
         data = self.cleaned_data["mimetypes"]
         data = self.cleaned_data["mimetypes"]
-        if not data:
-            return None
-        return self.clean_list(data)
+        if data:
+            return self.clean_list(data)
 
 
     def clean_list(self, value):
     def clean_list(self, value):
         items = [v.lstrip(".") for v in value.lower().replace(" ", "").split(",")]
         items = [v.lstrip(".") for v in value.lower().replace(" ", "").split(",")]

+ 2 - 2
misago/threads/mergeconflict.py

@@ -82,8 +82,8 @@ class PollMergeHandler(MergeConflictHandler):
 
 
 class MergeConflict:
 class MergeConflict:
     """
     """
-    Utility class single point of entry for detecting merge conflicts on different properties
-    and validating user resolutions.
+    Utility class single point of entry for detecting merge conflicts on different
+    properties and validating user resolutions.
     """
     """
 
 
     HANDLERS = (BestAnswerMergeHandler, PollMergeHandler)
     HANDLERS = (BestAnswerMergeHandler, PollMergeHandler)

+ 1 - 2
misago/threads/models/attachment.py

@@ -15,6 +15,7 @@ from ...core.utils import slugify
 
 
 
 
 def upload_to(instance, filename):
 def upload_to(instance, filename):
+    # pylint: disable=undefined-loop-variable
     spread_path = md5(str(instance.secret[:16]).encode()).hexdigest()
     spread_path = md5(str(instance.secret[:16]).encode()).hexdigest()
     secret = Attachment.generate_new_secret()
     secret = Attachment.generate_new_secret()
 
 
@@ -94,8 +95,6 @@ class Attachment(models.Model):
                 "misago:attachment-thumbnail",
                 "misago:attachment-thumbnail",
                 kwargs={"pk": self.pk, "secret": self.secret},
                 kwargs={"pk": self.pk, "secret": self.secret},
             )
             )
-        else:
-            return None
 
 
     def set_file(self, upload):
     def set_file(self, upload):
         self.file = File(upload, upload.name)
         self.file = File(upload, upload.name)

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

@@ -39,7 +39,6 @@ class Poll(models.Model):
     def ends_on(self):
     def ends_on(self):
         if self.length:
         if self.length:
             return self.posted_on + timedelta(days=self.length)
             return self.posted_on + timedelta(days=self.length)
-        return None
 
 
     @property
     @property
     def is_over(self):
     def is_over(self):

+ 3 - 4
misago/threads/models/post.py

@@ -149,6 +149,7 @@ class Post(models.Model):
 
 
     @property
     @property
     def attachments(self):
     def attachments(self):
+        # pylint: disable=access-member-before-definition
         if hasattr(self, "_hydrated_attachments_cache"):
         if hasattr(self, "_hydrated_attachments_cache"):
             return self._hydrated_attachments_cache
             return self._hydrated_attachments_cache
 
 
@@ -208,10 +209,8 @@ class Post(models.Model):
         if self.is_valid:
         if self.is_valid:
             if len(self.original) > 150:
             if len(self.original) > 150:
                 return str("%s...") % self.original[:150].strip()
                 return str("%s...") % self.original[:150].strip()
-            else:
-                return self.original
-        else:
-            return ""
+            return self.original
+        return ""
 
 
     @property
     @property
     def is_valid(self):
     def is_valid(self):

+ 1 - 0
misago/threads/moderation/exceptions.py

@@ -1,3 +1,4 @@
+# pylint: disable=super-init-not-called
 class ModerationError(Exception):
 class ModerationError(Exception):
     def __init__(self, message):
     def __init__(self, message):
         self.message = message
         self.message = message

+ 43 - 43
misago/threads/moderation/posts.py

@@ -15,37 +15,37 @@ __all__ = [
 
 
 
 
 def approve_post(user, post):
 def approve_post(user, post):
-    if post.is_unapproved:
-        post.is_unapproved = False
-        post.save(update_fields=["is_unapproved"])
-        return True
-    else:
+    if not post.is_unapproved:
         return False
         return False
 
 
+    post.is_unapproved = False
+    post.save(update_fields=["is_unapproved"])
+    return True
+
 
 
 def protect_post(user, post):
 def protect_post(user, post):
-    if not post.is_protected:
-        post.is_protected = True
-        post.save(update_fields=["is_protected"])
-        if post.is_best_answer:
-            post.thread.best_answer_is_protected = True
-            post.thread.save(update_fields=["best_answer_is_protected"])
-        return True
-    else:
+    if post.is_protected:
         return False
         return False
 
 
+    post.is_protected = True
+    post.save(update_fields=["is_protected"])
+    if post.is_best_answer:
+        post.thread.best_answer_is_protected = True
+        post.thread.save(update_fields=["best_answer_is_protected"])
+    return True
+
 
 
 def unprotect_post(user, post):
 def unprotect_post(user, post):
-    if post.is_protected:
-        post.is_protected = False
-        post.save(update_fields=["is_protected"])
-        if post.is_best_answer:
-            post.thread.best_answer_is_protected = False
-            post.thread.save(update_fields=["best_answer_is_protected"])
-        return True
-    else:
+    if not post.is_protected:
         return False
         return False
 
 
+    post.is_protected = False
+    post.save(update_fields=["is_protected"])
+    if post.is_best_answer:
+        post.thread.best_answer_is_protected = False
+        post.thread.save(update_fields=["best_answer_is_protected"])
+    return True
+
 
 
 def unhide_post(user, post):
 def unhide_post(user, post):
     if post.is_first_post:
     if post.is_first_post:
@@ -53,37 +53,37 @@ def unhide_post(user, post):
             _("You can't make original post visible without revealing thread.")
             _("You can't make original post visible without revealing thread.")
         )
         )
 
 
-    if post.is_hidden:
-        post.is_hidden = False
-        post.save(update_fields=["is_hidden"])
-        return True
-    else:
+    if not post.is_hidden:
         return False
         return False
 
 
+    post.is_hidden = False
+    post.save(update_fields=["is_hidden"])
+    return True
+
 
 
 def hide_post(user, post):
 def hide_post(user, post):
     if post.is_first_post:
     if post.is_first_post:
         raise ModerationError(_("You can't hide original post without hiding thread."))
         raise ModerationError(_("You can't hide original post without hiding thread."))
 
 
-    if not post.is_hidden:
-        post.is_hidden = True
-        post.hidden_by = user
-        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",
-            ]
-        )
-        return True
-    else:
+    if post.is_hidden:
         return False
         return False
 
 
+    post.is_hidden = True
+    post.hidden_by = user
+    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",
+        ]
+    )
+    return True
+
 
 
 @transaction.atomic
 @transaction.atomic
 def delete_post(user, post):
 def delete_post(user, post):

+ 100 - 100
misago/threads/moderation/threads.py

@@ -21,74 +21,74 @@ __all__ = [
 
 
 @transaction.atomic
 @transaction.atomic
 def change_thread_title(request, thread, new_title):
 def change_thread_title(request, thread, new_title):
-    if thread.title != new_title:
-        old_title = thread.title
-        thread.set_title(new_title)
-        thread.save(update_fields=["title", "slug"])
+    if thread.title == new_title:
+        return False
 
 
-        thread.first_post.set_search_document(thread.title)
-        thread.first_post.save(update_fields=["search_document"])
+    old_title = thread.title
+    thread.set_title(new_title)
+    thread.save(update_fields=["title", "slug"])
 
 
-        thread.first_post.update_search_vector()
-        thread.first_post.save(update_fields=["search_vector"])
+    thread.first_post.set_search_document(thread.title)
+    thread.first_post.save(update_fields=["search_document"])
 
 
-        record_event(request, thread, "changed_title", {"old_title": old_title})
-        return True
-    else:
-        return False
+    thread.first_post.update_search_vector()
+    thread.first_post.save(update_fields=["search_vector"])
+
+    record_event(request, thread, "changed_title", {"old_title": old_title})
+    return True
 
 
 
 
 @transaction.atomic
 @transaction.atomic
 def pin_thread_globally(request, thread):
 def pin_thread_globally(request, thread):
-    if thread.weight != 2:
-        thread.weight = 2
-        record_event(request, thread, "pinned_globally")
-        return True
-    else:
+    if thread.weight == 2:
         return False
         return False
 
 
+    thread.weight = 2
+    record_event(request, thread, "pinned_globally")
+    return True
+
 
 
 @transaction.atomic
 @transaction.atomic
 def pin_thread_locally(request, thread):
 def pin_thread_locally(request, thread):
-    if thread.weight != 1:
-        thread.weight = 1
-        record_event(request, thread, "pinned_locally")
-        return True
-    else:
+    if thread.weight == 1:
         return False
         return False
 
 
+    thread.weight = 1
+    record_event(request, thread, "pinned_locally")
+    return True
+
 
 
 @transaction.atomic
 @transaction.atomic
 def unpin_thread(request, thread):
 def unpin_thread(request, thread):
-    if thread.weight:
-        thread.weight = 0
-        record_event(request, thread, "unpinned")
-        return True
-    else:
+    if thread.weight == 0:
         return False
         return False
 
 
+    thread.weight = 0
+    record_event(request, thread, "unpinned")
+    return True
+
 
 
 @transaction.atomic
 @transaction.atomic
 def move_thread(request, thread, new_category):
 def move_thread(request, thread, new_category):
-    if thread.category_id != new_category.pk:
-        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(),
-                }
-            },
-        )
-        return True
-    else:
+    if thread.category_id == new_category.pk:
         return False
         return False
 
 
+    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(),
+            }
+        },
+    )
+    return True
+
 
 
 @transaction.atomic
 @transaction.atomic
 def merge_thread(request, thread, other_thread):
 def merge_thread(request, thread, other_thread):
@@ -101,88 +101,88 @@ def merge_thread(request, thread, other_thread):
 
 
 @transaction.atomic
 @transaction.atomic
 def approve_thread(request, thread):
 def approve_thread(request, thread):
-    if thread.is_unapproved:
-        thread.first_post.is_unapproved = False
-        thread.first_post.save(update_fields=["is_unapproved"])
+    if not thread.is_unapproved:
+        return False
 
 
-        thread.is_unapproved = False
+    thread.first_post.is_unapproved = False
+    thread.first_post.save(update_fields=["is_unapproved"])
 
 
-        unapproved_post_qs = thread.post_set.filter(is_unapproved=True)
-        thread.has_unapproved_posts = unapproved_post_qs.exists()
+    thread.is_unapproved = False
 
 
-        record_event(request, thread, "approved")
-        return True
-    else:
-        return False
+    unapproved_post_qs = thread.post_set.filter(is_unapproved=True)
+    thread.has_unapproved_posts = unapproved_post_qs.exists()
+
+    record_event(request, thread, "approved")
+    return True
 
 
 
 
 @transaction.atomic
 @transaction.atomic
 def open_thread(request, thread):
 def open_thread(request, thread):
-    if thread.is_closed:
-        thread.is_closed = False
-        record_event(request, thread, "opened")
-        return True
-    else:
+    if not thread.is_closed:
         return False
         return False
 
 
+    thread.is_closed = False
+    record_event(request, thread, "opened")
+    return True
+
 
 
 @transaction.atomic
 @transaction.atomic
 def close_thread(request, thread):
 def close_thread(request, thread):
-    if not thread.is_closed:
-        thread.is_closed = True
-        record_event(request, thread, "closed")
-        return True
-    else:
+    if thread.is_closed:
         return False
         return False
 
 
+    thread.is_closed = True
+    record_event(request, thread, "closed")
+    return True
+
 
 
 @transaction.atomic
 @transaction.atomic
 def unhide_thread(request, thread):
 def unhide_thread(request, thread):
-    if thread.is_hidden:
-        thread.first_post.is_hidden = False
-        thread.first_post.save(update_fields=["is_hidden"])
-        thread.is_hidden = False
+    if not thread.is_hidden:
+        return False
 
 
-        record_event(request, thread, "unhid")
+    thread.first_post.is_hidden = False
+    thread.first_post.save(update_fields=["is_hidden"])
+    thread.is_hidden = False
 
 
-        if thread.pk == thread.category.last_thread_id:
-            thread.category.synchronize()
-            thread.category.save()
+    record_event(request, thread, "unhid")
 
 
-        return True
-    else:
-        return False
+    if thread.pk == thread.category.last_thread_id:
+        thread.category.synchronize()
+        thread.category.save()
+
+    return True
 
 
 
 
 @transaction.atomic
 @transaction.atomic
 def hide_thread(request, thread):
 def hide_thread(request, thread):
-    if not thread.is_hidden:
-        thread.first_post.is_hidden = True
-        thread.first_post.hidden_by = request.user
-        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.is_hidden = True
-
-        record_event(request, thread, "hid")
-
-        if thread.pk == thread.category.last_thread_id:
-            thread.category.synchronize()
-            thread.category.save()
-
-        return True
-    else:
+    if thread.is_hidden:
         return False
         return False
 
 
+    thread.first_post.is_hidden = True
+    thread.first_post.hidden_by = request.user
+    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.is_hidden = True
+
+    record_event(request, thread, "hid")
+
+    if thread.pk == thread.category.last_thread_id:
+        thread.category.synchronize()
+        thread.category.save()
+
+    return True
+
 
 
 @transaction.atomic
 @transaction.atomic
 def delete_thread(request, thread):
 def delete_thread(request, thread):

+ 1 - 1
misago/threads/paginator.py

@@ -2,7 +2,7 @@ from django.core.paginator import Paginator
 
 
 
 
 class PostsPaginator(Paginator):
 class PostsPaginator(Paginator):
-    """paginator that returns that makes last item on page repeat as first item on next page."""
+    """paginator 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
         per_page = int(per_page) - 1

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

@@ -35,12 +35,9 @@ class AnonymousPermissionsForm(forms.Form):
 
 
 def change_permissions_form(role):
 def change_permissions_form(role):
     if isinstance(role, Role):
     if isinstance(role, Role):
-        if role.special_role != "anonymous":
-            return PermissionsForm
-        else:
+        if role.special_role == "anonymous":
             return AnonymousPermissionsForm
             return AnonymousPermissionsForm
-    else:
-        return None
+        return PermissionsForm
 
 
 
 
 def build_acl(acl, roles, key_name):
 def build_acl(acl, roles, key_name):

+ 48 - 58
misago/threads/permissions/bestanswers.py

@@ -6,7 +6,6 @@ from django.utils.translation import ngettext
 
 
 from ...acl import algebra
 from ...acl import algebra
 from ...acl.decorators import return_boolean
 from ...acl.decorators import return_boolean
-from ...admin.forms import YesNoSwitch
 from ...categories.models import Category, CategoryRole
 from ...categories.models import Category, CategoryRole
 from ...categories.permissions import get_categories_roles
 from ...categories.permissions import get_categories_roles
 from ..models import Post, Thread
 from ..models import Post, Thread
@@ -45,7 +44,8 @@ class CategoryPermissionsForm(forms.Form):
             "Time limit for changing marked best answer in owned thread, in minutes"
             "Time limit for changing marked best answer in owned thread, in minutes"
         ),
         ),
         help_text=_(
         help_text=_(
-            "Enter 0 to don't limit time for changing marked best answer in owned thread."
+            "Enter 0 to don't limit time for changing marked best answer in "
+            "owned thread."
         ),
         ),
         initial=0,
         initial=0,
         min_value=0,
         min_value=0,
@@ -55,8 +55,6 @@ class CategoryPermissionsForm(forms.Form):
 def change_permissions_form(role):
 def change_permissions_form(role):
     if isinstance(role, CategoryRole):
     if isinstance(role, CategoryRole):
         return CategoryPermissionsForm
         return CategoryPermissionsForm
-    else:
-        return None
 
 
 
 
 def build_acl(acl, roles, key_name):
 def build_acl(acl, roles, key_name):
@@ -140,7 +138,8 @@ def allow_mark_best_answer(user_acl, target):
     if not category_acl.get("can_mark_best_answers"):
     if not category_acl.get("can_mark_best_answers"):
         raise PermissionDenied(
         raise PermissionDenied(
             _(
             _(
-                'You don\'t have permission to mark best answers in the "%(category)s" category.'
+                "You don't have permission to mark best answers in the "
+                '"%(category)s" category.'
             )
             )
             % {"category": target.category}
             % {"category": target.category}
         )
         )
@@ -151,8 +150,8 @@ def allow_mark_best_answer(user_acl, target):
     ):
     ):
         raise PermissionDenied(
         raise PermissionDenied(
             _(
             _(
-                "You don't have permission to mark best answer in this thread because you didn't "
-                "start it."
+                "You don't have permission to mark best answer in this thread "
+                "because you didn't start it."
             )
             )
         )
         )
 
 
@@ -160,16 +159,16 @@ def allow_mark_best_answer(user_acl, target):
         if target.category.is_closed:
         if target.category.is_closed:
             raise PermissionDenied(
             raise PermissionDenied(
                 _(
                 _(
-                    "You don't have permission to mark best answer in this thread because its "
-                    'category "%(category)s" is closed.'
+                    "You don't have permission to mark best answer in this thread "
+                    'because its category "%(category)s" is closed.'
                 )
                 )
                 % {"category": target.category}
                 % {"category": target.category}
             )
             )
         if target.is_closed:
         if target.is_closed:
             raise PermissionDenied(
             raise PermissionDenied(
                 _(
                 _(
-                    "You can't mark best answer in this thread because it's closed and you don't "
-                    "have permission to open it."
+                    "You can't mark best answer in this thread because it's closed "
+                    "and you don't have permission to open it."
                 )
                 )
             )
             )
 
 
@@ -186,8 +185,8 @@ def allow_change_best_answer(user_acl, target):
     if not category_acl.get("can_change_marked_answers"):
     if not category_acl.get("can_change_marked_answers"):
         raise PermissionDenied(
         raise PermissionDenied(
             _(
             _(
-                "You don't have permission to change this thread's marked answer because it's "
-                'in the "%(category)s" category.'
+                "You don't have permission to change this thread's marked answer "
+                'because it\'s in the "%(category)s" category.'
             )
             )
             % {"category": target.category}
             % {"category": target.category}
         )
         )
@@ -196,31 +195,26 @@ def allow_change_best_answer(user_acl, target):
         if user_acl["user_id"] != target.starter_id:
         if user_acl["user_id"] != target.starter_id:
             raise PermissionDenied(
             raise PermissionDenied(
                 _(
                 _(
-                    "You don't have permission to change this thread's marked answer because you "
-                    "are not a thread starter."
+                    "You don't have permission to change this thread's marked answer "
+                    "because you are not a thread starter."
                 )
                 )
             )
             )
         if not has_time_to_change_answer(user_acl, target):
         if not has_time_to_change_answer(user_acl, target):
+            # pylint: disable=line-too-long
+            message = ngettext(
+                "You don't have permission to change best answer that was marked for more than %(minutes)s minute.",
+                "You don't have permission to change best answer that was marked for more than %(minutes)s minutes.",
+                category_acl["best_answer_change_time"],
+            )
             raise PermissionDenied(
             raise PermissionDenied(
-                ngettext(
-                    (
-                        "You don't have permission to change best answer that was marked for more "
-                        "than %(minutes)s minute."
-                    ),
-                    (
-                        "You don't have permission to change best answer that was marked for more "
-                        "than %(minutes)s minutes."
-                    ),
-                    category_acl["best_answer_change_time"],
-                )
-                % {"minutes": category_acl["best_answer_change_time"]}
+                message % {"minutes": category_acl["best_answer_change_time"]}
             )
             )
 
 
     if target.best_answer_is_protected and not category_acl["can_protect_posts"]:
     if target.best_answer_is_protected and not category_acl["can_protect_posts"]:
         raise PermissionDenied(
         raise PermissionDenied(
             _(
             _(
-                "You don't have permission to change this thread's best answer because "
-                "a moderator has protected it."
+                "You don't have permission to change this thread's best answer "
+                "because a moderator has protected it."
             )
             )
         )
         )
 
 
@@ -240,8 +234,8 @@ def allow_unmark_best_answer(user_acl, target):
     if not category_acl.get("can_change_marked_answers"):
     if not category_acl.get("can_change_marked_answers"):
         raise PermissionDenied(
         raise PermissionDenied(
             _(
             _(
-                'You don\'t have permission to unmark threads answers in the "%(category)s" '
-                "category."
+                "You don't have permission to unmark threads answers in "
+                'the "%(category)s" category.'
             )
             )
             % {"category": target.category}
             % {"category": target.category}
         )
         )
@@ -250,48 +244,43 @@ def allow_unmark_best_answer(user_acl, target):
         if user_acl["user_id"] != target.starter_id:
         if user_acl["user_id"] != target.starter_id:
             raise PermissionDenied(
             raise PermissionDenied(
                 _(
                 _(
-                    "You don't have permission to unmark this best answer because you are not a "
-                    "thread starter."
+                    "You don't have permission to unmark this best answer "
+                    "because you are not a thread starter."
                 )
                 )
             )
             )
         if not has_time_to_change_answer(user_acl, target):
         if not has_time_to_change_answer(user_acl, target):
+            # pylint: disable=line-too-long
+            message = ngettext(
+                "You don't have permission to unmark best answer that was marked for more than %(minutes)s minute.",
+                "You don't have permission to unmark best answer that was marked for more than %(minutes)s minutes.",
+                category_acl["best_answer_change_time"],
+            )
             raise PermissionDenied(
             raise PermissionDenied(
-                ngettext(
-                    (
-                        "You don't have permission to unmark best answer that was marked for more "
-                        "than %(minutes)s minute."
-                    ),
-                    (
-                        "You don't have permission to unmark best answer that was marked for more "
-                        "than %(minutes)s minutes."
-                    ),
-                    category_acl["best_answer_change_time"],
-                )
-                % {"minutes": category_acl["best_answer_change_time"]}
+                message % {"minutes": category_acl["best_answer_change_time"]}
             )
             )
 
 
     if not category_acl["can_close_threads"]:
     if not category_acl["can_close_threads"]:
         if target.category.is_closed:
         if target.category.is_closed:
             raise PermissionDenied(
             raise PermissionDenied(
                 _(
                 _(
-                    "You don't have permission to unmark this best answer because its category "
-                    '"%(category)s" is closed.'
+                    "You don't have permission to unmark this best answer "
+                    'because its category "%(category)s" is closed.'
                 )
                 )
                 % {"category": target.category}
                 % {"category": target.category}
             )
             )
         if target.is_closed:
         if target.is_closed:
             raise PermissionDenied(
             raise PermissionDenied(
                 _(
                 _(
-                    "You can't unmark this thread's best answer because it's closed and you "
-                    "don't have permission to open it."
+                    "You can't unmark this thread's best answer "
+                    "because it's closed and you don't have permission to open it."
                 )
                 )
             )
             )
 
 
     if target.best_answer_is_protected and not category_acl["can_protect_posts"]:
     if target.best_answer_is_protected and not category_acl["can_protect_posts"]:
         raise PermissionDenied(
         raise PermissionDenied(
             _(
             _(
-                "You don't have permission to unmark this thread's best answer because a "
-                "moderator has protected it."
+                "You don't have permission to unmark this thread's best answer "
+                "because a moderator has protected it."
             )
             )
         )
         )
 
 
@@ -311,7 +300,8 @@ def allow_mark_as_best_answer(user_acl, target):
     if not category_acl.get("can_mark_best_answers"):
     if not category_acl.get("can_mark_best_answers"):
         raise PermissionDenied(
         raise PermissionDenied(
             _(
             _(
-                'You don\'t have permission to mark best answers in the "%(category)s" category.'
+                "You don't have permission to mark best answers "
+                'in the "%(category)s" category.'
             )
             )
             % {"category": target.category}
             % {"category": target.category}
         )
         )
@@ -322,8 +312,8 @@ def allow_mark_as_best_answer(user_acl, target):
     ):
     ):
         raise PermissionDenied(
         raise PermissionDenied(
             _(
             _(
-                "You don't have permission to mark best answer in this thread because you "
-                "didn't start it."
+                "You don't have permission to mark best answer in this thread "
+                "because you didn't start it."
             )
             )
         )
         )
 
 
@@ -341,8 +331,8 @@ def allow_mark_as_best_answer(user_acl, target):
     if target.is_protected and not category_acl["can_protect_posts"]:
     if target.is_protected and not category_acl["can_protect_posts"]:
         raise PermissionDenied(
         raise PermissionDenied(
             _(
             _(
-                "You don't have permission to mark this post as best answer because a moderator "
-                "has protected it."
+                "You don't have permission to mark this post as best answer "
+                "because a moderator has protected it."
             )
             )
         )
         )
 
 
@@ -378,5 +368,5 @@ def has_time_to_change_answer(user_acl, target):
         diff = timezone.now() - target.best_answer_marked_on
         diff = timezone.now() - target.best_answer_marked_on
         diff_minutes = int(diff.total_seconds() / 60)
         diff_minutes = int(diff.total_seconds() / 60)
         return diff_minutes < change_time
         return diff_minutes < change_time
-    else:
-        return True
+
+    return True

+ 2 - 5
misago/threads/permissions/polls.py

@@ -62,8 +62,6 @@ class RolePermissionsForm(forms.Form):
 def change_permissions_form(role):
 def change_permissions_form(role):
     if isinstance(role, Role) and role.special_role != "anonymous":
     if isinstance(role, Role) and role.special_role != "anonymous":
         return RolePermissionsForm
         return RolePermissionsForm
-    else:
-        return None
 
 
 
 
 def build_acl(acl, roles, key_name):
 def build_acl(acl, roles, key_name):
@@ -253,7 +251,6 @@ def has_time_to_edit_poll(user_acl, target):
     if edit_time:
     if edit_time:
         diff = timezone.now() - target.posted_on
         diff = timezone.now() - target.posted_on
         diff_minutes = int(diff.total_seconds() / 60)
         diff_minutes = int(diff.total_seconds() / 60)
-
         return diff_minutes < edit_time
         return diff_minutes < edit_time
-    else:
-        return True
+
+    return True

+ 0 - 2
misago/threads/permissions/privatethreads.py

@@ -65,8 +65,6 @@ class PermissionsForm(forms.Form):
 def change_permissions_form(role):
 def change_permissions_form(role):
     if isinstance(role, Role) and role.special_role != "anonymous":
     if isinstance(role, Role) and role.special_role != "anonymous":
         return PermissionsForm
         return PermissionsForm
-    else:
-        return None
 
 
 
 
 def build_acl(acl, roles, key_name):
 def build_acl(acl, roles, key_name):

+ 18 - 17
misago/threads/permissions/threads.py

@@ -226,10 +226,8 @@ class CategoryPermissionsForm(forms.Form):
 def change_permissions_form(role):
 def change_permissions_form(role):
     if isinstance(role, Role) and role.special_role != "anonymous":
     if isinstance(role, Role) and role.special_role != "anonymous":
         return RolePermissionsForm
         return RolePermissionsForm
-    elif isinstance(role, CategoryRole):
+    if isinstance(role, CategoryRole):
         return CategoryPermissionsForm
         return CategoryPermissionsForm
-    else:
-        return None
 
 
 
 
 def build_acl(acl, roles, key_name):
 def build_acl(acl, roles, key_name):
@@ -1288,8 +1286,8 @@ def has_time_to_edit_thread(user_acl, target):
         diff = timezone.now() - target.started_on
         diff = timezone.now() - target.started_on
         diff_minutes = int(diff.total_seconds() / 60)
         diff_minutes = int(diff.total_seconds() / 60)
         return diff_minutes < edit_time
         return diff_minutes < edit_time
-    else:
-        return True
+
+    return True
 
 
 
 
 def has_time_to_edit_post(user_acl, target):
 def has_time_to_edit_post(user_acl, target):
@@ -1300,11 +1298,13 @@ def has_time_to_edit_post(user_acl, target):
         diff = timezone.now() - target.posted_on
         diff = timezone.now() - target.posted_on
         diff_minutes = int(diff.total_seconds() / 60)
         diff_minutes = int(diff.total_seconds() / 60)
         return diff_minutes < edit_time
         return diff_minutes < edit_time
-    else:
-        return True
+
+    return True
 
 
 
 
-def exclude_invisible_threads(user_acl, categories, queryset):
+def exclude_invisible_threads(
+    user_acl, categories, queryset
+):  # pylint: disable=too-many-branches
     show_all = []
     show_all = []
     show_accepted_visible = []
     show_accepted_visible = []
     show_accepted = []
     show_accepted = []
@@ -1399,20 +1399,21 @@ def exclude_invisible_threads(user_acl, categories, queryset):
         else:
         else:
             conditions = condition
             conditions = condition
 
 
-    if conditions:
-        return queryset.filter(conditions)
-    else:
+    if not conditions:
         return Thread.objects.none()
         return Thread.objects.none()
 
 
+    return queryset.filter(conditions)
+
 
 
 def exclude_invisible_posts(user_acl, categories, queryset):
 def exclude_invisible_posts(user_acl, categories, queryset):
     if hasattr(categories, "__iter__"):
     if hasattr(categories, "__iter__"):
         return exclude_invisible_posts_in_categories(user_acl, categories, queryset)
         return exclude_invisible_posts_in_categories(user_acl, categories, queryset)
-    else:
-        return exclude_invisible_posts_in_category(user_acl, categories, queryset)
+    return exclude_invisible_posts_in_category(user_acl, categories, queryset)
 
 
 
 
-def exclude_invisible_posts_in_categories(user_acl, categories, queryset):
+def exclude_invisible_posts_in_categories(
+    user_acl, categories, queryset
+):  # pylint: disable=too-many-branches
     show_all = []
     show_all = []
     show_approved = []
     show_approved = []
     show_approved_owned = []
     show_approved_owned = []
@@ -1461,11 +1462,11 @@ def exclude_invisible_posts_in_categories(user_acl, categories, queryset):
             category__in=hide_invisible_events, is_event=True, is_hidden=True
             category__in=hide_invisible_events, is_event=True, is_hidden=True
         )
         )
 
 
-    if conditions:
-        return queryset.filter(conditions)
-    else:
+    if not conditions:
         return Post.objects.none()
         return Post.objects.none()
 
 
+    return queryset.filter(conditions)
+
 
 
 def exclude_invisible_posts_in_category(user_acl, category, queryset):
 def exclude_invisible_posts_in_category(user_acl, category, queryset):
     add_acl_to_obj(user_acl, category)
     add_acl_to_obj(user_acl, category)

+ 0 - 2
misago/threads/serializers/attachment.py

@@ -54,5 +54,3 @@ class AttachmentSerializer(serializers.ModelSerializer):
             return reverse(
             return reverse(
                 "misago:user", kwargs={"slug": obj.uploader_slug, "pk": obj.uploader_id}
                 "misago:user", kwargs={"slug": obj.uploader_slug, "pk": obj.uploader_id}
             )
             )
-        else:
-            return None

+ 4 - 2
misago/threads/serializers/moderation.py

@@ -163,7 +163,8 @@ class MergePostsSerializer(serializers.Serializer):
             if posts[0].is_first_post and post.is_best_answer:
             if posts[0].is_first_post and post.is_best_answer:
                 raise serializers.ValidationError(
                 raise serializers.ValidationError(
                     _(
                     _(
-                        "Post marked as best answer can't be merged with thread's first post."
+                        "Post marked as best answer can't be merged with "
+                        "thread's first post."
                     )
                     )
                 )
                 )
 
 
@@ -311,7 +312,8 @@ class NewThreadSerializer(serializers.Serializer):
             if weight == 2:
             if weight == 2:
                 raise ValidationError(
                 raise ValidationError(
                     _(
                     _(
-                        "You don't have permission to pin threads globally in this category."
+                        "You don't have permission to pin threads globally "
+                        "in this category."
                     )
                     )
                 )
                 )
             else:
             else:

+ 3 - 3
misago/threads/serializers/poll.py

@@ -52,8 +52,6 @@ class PollSerializer(serializers.ModelSerializer):
             return reverse(
             return reverse(
                 "misago:user", kwargs={"slug": obj.poster_slug, "pk": obj.poster_id}
                 "misago:user", kwargs={"slug": obj.poster_slug, "pk": obj.poster_id}
             )
             )
-        else:
-            return None
 
 
     def get_acl(self, obj):
     def get_acl(self, obj):
         try:
         try:
@@ -119,6 +117,7 @@ class EditPollSerializer(serializers.ModelSerializer):
             )
             )
 
 
         if total_choices > MAX_POLL_OPTIONS:
         if total_choices > MAX_POLL_OPTIONS:
+            # pylint: disable=line-too-long
             message = ngettext(
             message = ngettext(
                 "You can't add more than %(limit_value)s option to a single poll (added %(show_value)s).",
                 "You can't add more than %(limit_value)s option to a single poll (added %(show_value)s).",
                 "You can't add more than %(limit_value)s options to a single poll (added %(show_value)s).",
                 "You can't add more than %(limit_value)s options to a single poll (added %(show_value)s).",
@@ -132,7 +131,8 @@ class EditPollSerializer(serializers.ModelSerializer):
         if data["allowed_choices"] > len(data["choices"]):
         if data["allowed_choices"] > len(data["choices"]):
             raise serializers.ValidationError(
             raise serializers.ValidationError(
                 _(
                 _(
-                    "Number of allowed choices can't be greater than number of all choices."
+                    "Number of allowed choices can't be "
+                    "greater than number of all choices."
                 )
                 )
             )
             )
         return data
         return data

+ 2 - 1
misago/threads/serializers/pollvote.py

@@ -31,7 +31,8 @@ class NewVoteSerializer(serializers.Serializer):
             raise serializers.ValidationError(
             raise serializers.ValidationError(
                 _("One or more of poll choices were invalid.")
                 _("One or more of poll choices were invalid.")
             )
             )
-        if not len(clean_choices):
+
+        if not clean_choices:
             raise serializers.ValidationError(_("You have to make a choice."))
             raise serializers.ValidationError(_("You have to make a choice."))
 
 
         return clean_choices
         return clean_choices

+ 0 - 6
misago/threads/serializers/post.py

@@ -79,8 +79,6 @@ class PostSerializer(serializers.ModelSerializer, MutableFields):
             and (not obj.is_hidden or obj.acl["can_see_hidden"])
             and (not obj.is_hidden or obj.acl["can_see_hidden"])
         ):
         ):
             return obj.content
             return obj.content
-        else:
-            return None
 
 
     def get_attachments(self, obj):
     def get_attachments(self, obj):
         return obj.attachments_cache
         return obj.attachments_cache
@@ -156,8 +154,6 @@ class PostSerializer(serializers.ModelSerializer, MutableFields):
                 "misago:user",
                 "misago:user",
                 kwargs={"pk": obj.last_editor_id, "slug": obj.last_editor_slug},
                 kwargs={"pk": obj.last_editor_id, "slug": obj.last_editor_slug},
             )
             )
-        else:
-            return None
 
 
     def get_hidden_by_url(self, obj):
     def get_hidden_by_url(self, obj):
         if obj.hidden_by_id:
         if obj.hidden_by_id:
@@ -165,5 +161,3 @@ class PostSerializer(serializers.ModelSerializer, MutableFields):
                 "misago:user",
                 "misago:user",
                 kwargs={"pk": obj.hidden_by_id, "slug": obj.hidden_by_slug},
                 kwargs={"pk": obj.hidden_by_id, "slug": obj.hidden_by_slug},
             )
             )
-        else:
-            return None

+ 0 - 2
misago/threads/serializers/postedit.py

@@ -26,5 +26,3 @@ class PostEditSerializer(serializers.ModelSerializer):
             return reverse(
             return reverse(
                 "misago:user", kwargs={"slug": obj.editor_slug, "pk": obj.editor_id}
                 "misago:user", kwargs={"slug": obj.editor_slug, "pk": obj.editor_id}
             )
             )
-        else:
-            return None

+ 0 - 2
misago/threads/serializers/postlike.py

@@ -31,5 +31,3 @@ class PostLikeSerializer(serializers.ModelSerializer):
             return reverse(
             return reverse(
                 "misago:user", kwargs={"slug": obj["liker_slug"], "pk": obj["liker_id"]}
                 "misago:user", kwargs={"slug": obj["liker_slug"], "pk": obj["liker_id"]}
             )
             )
-        else:
-            return None

+ 0 - 4
misago/threads/serializers/thread.py

@@ -138,7 +138,6 @@ class ThreadSerializer(serializers.ModelSerializer, MutableFields):
             return reverse(
             return reverse(
                 "misago:user", kwargs={"slug": obj.starter_slug, "pk": obj.starter_id}
                 "misago:user", kwargs={"slug": obj.starter_slug, "pk": obj.starter_id}
             )
             )
-        return None
 
 
     def get_last_poster_url(self, obj):
     def get_last_poster_url(self, obj):
         if obj.last_poster_id:
         if obj.last_poster_id:
@@ -146,7 +145,6 @@ class ThreadSerializer(serializers.ModelSerializer, MutableFields):
                 "misago:user",
                 "misago:user",
                 kwargs={"slug": obj.last_poster_slug, "pk": obj.last_poster_id},
                 kwargs={"slug": obj.last_poster_slug, "pk": obj.last_poster_id},
             )
             )
-        return None
 
 
 
 
 class PrivateThreadSerializer(ThreadSerializer):
 class PrivateThreadSerializer(ThreadSerializer):
@@ -175,7 +173,6 @@ class ThreadsListSerializer(ThreadSerializer):
                 "real_name": obj.starter.get_real_name(),
                 "real_name": obj.starter.get_real_name(),
                 "avatars": obj.starter.avatars,
                 "avatars": obj.starter.avatars,
             }
             }
-        return None
 
 
     def get_last_poster(self, obj):
     def get_last_poster(self, obj):
         if obj.last_poster_id:
         if obj.last_poster_id:
@@ -185,7 +182,6 @@ class ThreadsListSerializer(ThreadSerializer):
                 "real_name": obj.last_poster.get_real_name(),
                 "real_name": obj.last_poster.get_real_name(),
                 "avatars": obj.last_poster.avatars,
                 "avatars": obj.last_poster.avatars,
             }
             }
-        return None
 
 
 
 
 ThreadsListSerializer = ThreadsListSerializer.exclude_fields("path", "poll")
 ThreadsListSerializer = ThreadsListSerializer.exclude_fields("path", "poll")

+ 6 - 8
misago/threads/templatetags/misago_poststags.py

@@ -25,16 +25,14 @@ def likes_label(post):
     if not hidden_likes:
     if not hidden_likes:
         return _("%(users)s like this.") % {"users": usernames_string}
         return _("%(users)s like this.") % {"users": usernames_string}
 
 
+    label = ngettext(
+        "%(users)s and %(likes)s other user like this.",
+        "%(users)s and %(likes)s other users like this.",
+        hidden_likes,
+    )
     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,
-        )
-        % formats
-    )
+    return label % formats
 
 
 
 
 def humanize_usernames_list(usernames):
 def humanize_usernames_list(usernames):

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

@@ -158,7 +158,8 @@ class AttachmentsApiTestCase(AuthenticatedUserTestCase):
                 response.json(),
                 response.json(),
                 {
                 {
                     "detail": (
                     "detail": (
-                        "You can't upload files larger than 100.0\xa0KB (your file has 253.9\xa0KB)."
+                        "You can't upload files larger than 100.0\xa0KB "
+                        "(your file has 253.9\xa0KB)."
                     )
                     )
                 },
                 },
             )
             )

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

@@ -107,7 +107,9 @@ class AttachmentsMiddlewareTests(AuthenticatedUserTestCase):
 
 
     @patch_attachments_acl()
     @patch_attachments_acl()
     def test_get_initial_attachments(self):
     def test_get_initial_attachments(self):
-        """get_initial_attachments returns list of attachments already existing on post"""
+        """
+        get_initial_attachments returns list of attachments already existing on post
+        """
         user_acl = useracl.get_user_acl(self.user, cache_versions)
         user_acl = useracl.get_user_acl(self.user, cache_versions)
         middleware = AttachmentsMiddleware(
         middleware = AttachmentsMiddleware(
             request=Mock(data={}),
             request=Mock(data={}),
@@ -132,7 +134,9 @@ class AttachmentsMiddlewareTests(AuthenticatedUserTestCase):
 
 
     @patch_attachments_acl()
     @patch_attachments_acl()
     def test_get_new_attachments(self):
     def test_get_new_attachments(self):
-        """get_initial_attachments returns list of attachments already existing on post"""
+        """
+        get_initial_attachments returns list of attachments already existing on post
+        """
         user_acl = useracl.get_user_acl(self.user, cache_versions)
         user_acl = useracl.get_user_acl(self.user, cache_versions)
         middleware = AttachmentsMiddleware(
         middleware = AttachmentsMiddleware(
             request=Mock(data={}),
             request=Mock(data={}),
@@ -160,7 +164,9 @@ class AttachmentsMiddlewareTests(AuthenticatedUserTestCase):
 
 
     @patch_attachments_acl({"can_delete_other_users_attachments": False})
     @patch_attachments_acl({"can_delete_other_users_attachments": False})
     def test_cant_delete_attachment(self):
     def test_cant_delete_attachment(self):
-        """middleware validates if we have permission to delete other users attachments"""
+        """
+        middleware validates if we have permission to delete other users attachments
+        """
         attachment = self.mock_attachment(user=False, post=self.post)
         attachment = self.mock_attachment(user=False, post=self.post)
         self.assertIsNone(attachment.uploader)
         self.assertIsNone(attachment.uploader)
 
 

+ 0 - 1
misago/threads/tests/test_emailnotification_middleware.py

@@ -1,4 +1,3 @@
-from copy import deepcopy
 from datetime import timedelta
 from datetime import timedelta
 
 
 from django.core import mail
 from django.core import mail

+ 7 - 2
misago/threads/tests/test_mergeconflict.py

@@ -75,7 +75,9 @@ class MergeConflictTests(TestCase):
         self.assertFalse(merge_conflict.is_merge_conflict())
         self.assertFalse(merge_conflict.is_merge_conflict())
 
 
     def test_three_best_answers_one_poll_two_plain_conflict(self):
     def test_three_best_answers_one_poll_two_plain_conflict(self):
-        """three threads with best answer, thread with poll and two plain threads conflict"""
+        """
+        three threads with best answer, thread with poll and two plain threads conflict
+        """
         best_answers = [self.create_best_answer_thread() for i in range(3)]
         best_answers = [self.create_best_answer_thread() for i in range(3)]
         polls = [self.create_poll_thread()]
         polls = [self.create_poll_thread()]
         threads = (
         threads = (
@@ -133,7 +135,10 @@ class MergeConflictTests(TestCase):
         )
         )
 
 
     def test_one_best_answer_three_polls_two_plain_conflict(self):
     def test_one_best_answer_three_polls_two_plain_conflict(self):
-        """one thread with best answer, three threads with poll and two plain threads conflict"""
+        """
+        one thread with best answer, three threads with poll
+        and two plain threads conflict
+        """
         best_answers = [self.create_best_answer_thread()]
         best_answers = [self.create_best_answer_thread()]
         polls = [self.create_poll_thread() for i in range(3)]
         polls = [self.create_poll_thread() for i in range(3)]
         threads = (
         threads = (

+ 4 - 1
misago/threads/tests/test_privatethread_patch_api.py

@@ -168,7 +168,10 @@ class PrivateThreadAddParticipantApiTests(PrivateThreadPatchApiTestCase):
         )
         )
 
 
     def test_add_user(self):
     def test_add_user(self):
-        """adding user to thread add user to thread as participant, sets event and emails him"""
+        """
+        adding user to thread add user to thread as participant,
+        sets event and emails him
+        """
         ThreadParticipant.objects.set_owner(self.thread, self.user)
         ThreadParticipant.objects.set_owner(self.thread, self.user)
 
 
         self.patch(
         self.patch(

+ 4 - 2
misago/threads/tests/test_privatethread_start_api.py

@@ -112,7 +112,8 @@ class StartPrivateThreadTests(AuthenticatedUserTestCase):
             response.json(),
             response.json(),
             {
             {
                 "to": [
                 "to": [
-                    "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."
                 ]
                 ]
             },
             },
         )
         )
@@ -149,7 +150,8 @@ class StartPrivateThreadTests(AuthenticatedUserTestCase):
             response.json(),
             response.json(),
             {
             {
                 "to": [
                 "to": [
-                    "You can't add more than 3 users to private thread (you've added 50)."
+                    "You can't add more than 3 users to private thread "
+                    "(you've added 50)."
                 ]
                 ]
             },
             },
         )
         )

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

@@ -29,7 +29,9 @@ class SyncUnreadPrivateThreadsTestCase(PrivateThreadsTestCase):
         self.assertEqual(self.user.unread_private_threads, 1)
         self.assertEqual(self.user.unread_private_threads, 1)
 
 
     def test_middleware_counts_unread_thread(self):
     def test_middleware_counts_unread_thread(self):
-        """middleware counts thread with unread reply, post read flags user for recount"""
+        """
+        middleware counts thread with unread reply, post read flags user for recount
+        """
         self.user.sync_unread_private_threads = True
         self.user.sync_unread_private_threads = True
         self.user.save()
         self.user.save()
 
 

+ 61 - 40
misago/threads/tests/test_thread_patch_api.py

@@ -30,7 +30,9 @@ class ThreadAddAclApiTests(ThreadPatchApiTestCase):
         self.assertTrue(response_json["acl"])
         self.assertTrue(response_json["acl"])
 
 
     def test_add_acl_false(self):
     def test_add_acl_false(self):
-        """if value is false, api won't add acl to the response, but will set empty key"""
+        """
+        if value is false, api won't add acl to the response, but will set empty key
+        """
         response = self.patch(
         response = self.patch(
             self.api_link, [{"op": "add", "path": "acl", "value": False}]
             self.api_link, [{"op": "add", "path": "acl", "value": False}]
         )
         )
@@ -1048,7 +1050,8 @@ class ThreadMarkBestAnswerApiTests(ThreadPatchApiTestCase):
             {
             {
                 "id": self.thread.id,
                 "id": self.thread.id,
                 "detail": [
                 "detail": [
-                    'You don\'t have permission to mark best answers in the "First category" category.'
+                    "You don't have permission to mark best answers "
+                    'in the "First category" category.'
                 ],
                 ],
             },
             },
         )
         )
@@ -1071,8 +1074,8 @@ class ThreadMarkBestAnswerApiTests(ThreadPatchApiTestCase):
             {
             {
                 "id": self.thread.id,
                 "id": self.thread.id,
                 "detail": [
                 "detail": [
-                    "You don't have permission to mark best answer in this thread because you didn't "
-                    "start it."
+                    "You don't have permission to mark best answer in this thread "
+                    "because you didn't start it."
                 ],
                 ],
             },
             },
         )
         )
@@ -1108,8 +1111,8 @@ class ThreadMarkBestAnswerApiTests(ThreadPatchApiTestCase):
             {
             {
                 "id": self.thread.id,
                 "id": self.thread.id,
                 "detail": [
                 "detail": [
-                    "You don't have permission to mark best answer in this thread because its "
-                    'category "First category" is closed.'
+                    "You don't have permission to mark best answer in this thread "
+                    'because its category "First category" is closed.'
                 ],
                 ],
             },
             },
         )
         )
@@ -1149,8 +1152,8 @@ class ThreadMarkBestAnswerApiTests(ThreadPatchApiTestCase):
             {
             {
                 "id": self.thread.id,
                 "id": self.thread.id,
                 "detail": [
                 "detail": [
-                    "You can't mark best answer in this thread because it's closed and you don't have "
-                    "permission to open it."
+                    "You can't mark best answer in this thread because it's closed and "
+                    "you don't have permission to open it."
                 ],
                 ],
             },
             },
         )
         )
@@ -1356,8 +1359,8 @@ class ThreadMarkBestAnswerApiTests(ThreadPatchApiTestCase):
             {
             {
                 "id": self.thread.id,
                 "id": self.thread.id,
                 "detail": [
                 "detail": [
-                    "You don't have permission to mark this post as best answer because a moderator "
-                    "has protected it."
+                    "You don't have permission to mark this post as best answer "
+                    "because a moderator has protected it."
                 ],
                 ],
             },
             },
         )
         )
@@ -1441,7 +1444,9 @@ class ThreadChangeBestAnswerApiTests(ThreadPatchApiTestCase):
 
 
     @patch_category_acl({"can_mark_best_answers": 0, "can_change_marked_answers": 2})
     @patch_category_acl({"can_mark_best_answers": 0, "can_change_marked_answers": 2})
     def test_change_best_answer_no_permission_to_mark(self):
     def test_change_best_answer_no_permission_to_mark(self):
-        """api validates permission to mark best answers before allowing answer change"""
+        """
+        api validates permission to mark best answers before allowing answer change
+        """
         best_answer = test.reply_thread(self.thread)
         best_answer = test.reply_thread(self.thread)
 
 
         response = self.patch(
         response = self.patch(
@@ -1454,7 +1459,8 @@ class ThreadChangeBestAnswerApiTests(ThreadPatchApiTestCase):
             {
             {
                 "id": self.thread.id,
                 "id": self.thread.id,
                 "detail": [
                 "detail": [
-                    'You don\'t have permission to mark best answers in the "First category" category.'
+                    "You don't have permission to mark best answers in the "
+                    '"First category" category.'
                 ],
                 ],
             },
             },
         )
         )
@@ -1477,8 +1483,8 @@ class ThreadChangeBestAnswerApiTests(ThreadPatchApiTestCase):
             {
             {
                 "id": self.thread.id,
                 "id": self.thread.id,
                 "detail": [
                 "detail": [
-                    "You don't have permission to change this thread's marked answer because it's "
-                    'in the "First category" category.'
+                    "You don't have permission to change this thread's marked answer "
+                    'because it\'s in the "First category" category.'
                 ],
                 ],
             },
             },
         )
         )
@@ -1501,8 +1507,8 @@ class ThreadChangeBestAnswerApiTests(ThreadPatchApiTestCase):
             {
             {
                 "id": self.thread.id,
                 "id": self.thread.id,
                 "detail": [
                 "detail": [
-                    "You don't have permission to change this thread's marked answer because you are "
-                    "not a thread starter."
+                    "You don't have permission to change this thread's marked answer "
+                    "because you are not a thread starter."
                 ],
                 ],
             },
             },
         )
         )
@@ -1528,7 +1534,9 @@ class ThreadChangeBestAnswerApiTests(ThreadPatchApiTestCase):
         }
         }
     )
     )
     def test_change_best_answer_timelimit_out_of_time(self):
     def test_change_best_answer_timelimit_out_of_time(self):
-        """api validates permission for starter to change best answers within timelimit"""
+        """
+        api validates permission for starter to change best answers within timelimit
+        """
         best_answer = test.reply_thread(self.thread)
         best_answer = test.reply_thread(self.thread)
 
 
         self.thread.best_answer_marked_on = timezone.now() - timedelta(minutes=6)
         self.thread.best_answer_marked_on = timezone.now() - timedelta(minutes=6)
@@ -1545,8 +1553,8 @@ class ThreadChangeBestAnswerApiTests(ThreadPatchApiTestCase):
             {
             {
                 "id": self.thread.id,
                 "id": self.thread.id,
                 "detail": [
                 "detail": [
-                    "You don't have permission to change best answer that was marked for more than "
-                    "5 minutes."
+                    "You don't have permission to change best answer that was marked "
+                    "for more than 5 minutes."
                 ],
                 ],
             },
             },
         )
         )
@@ -1562,7 +1570,9 @@ class ThreadChangeBestAnswerApiTests(ThreadPatchApiTestCase):
         }
         }
     )
     )
     def test_change_best_answer_timelimit(self):
     def test_change_best_answer_timelimit(self):
-        """api validates permission for starter to change best answers within timelimit"""
+        """
+        api validates permission for starter to change best answers within timelimit
+        """
         best_answer = test.reply_thread(self.thread)
         best_answer = test.reply_thread(self.thread)
 
 
         self.thread.best_answer_marked_on = timezone.now() - timedelta(minutes=1)
         self.thread.best_answer_marked_on = timezone.now() - timedelta(minutes=1)
@@ -1599,8 +1609,8 @@ class ThreadChangeBestAnswerApiTests(ThreadPatchApiTestCase):
             {
             {
                 "id": self.thread.id,
                 "id": self.thread.id,
                 "detail": [
                 "detail": [
-                    "You don't have permission to change this thread's best answer because a "
-                    "moderator has protected it."
+                    "You don't have permission to change this thread's best answer "
+                    "because a moderator has protected it."
                 ],
                 ],
             },
             },
         )
         )
@@ -1727,7 +1737,8 @@ class ThreadUnmarkBestAnswerApiTests(ThreadPatchApiTestCase):
             {
             {
                 "id": self.thread.id,
                 "id": self.thread.id,
                 "detail": [
                 "detail": [
-                    "This post can't be unmarked because it's not currently marked as best answer."
+                    "This post can't be unmarked because it's not currently marked "
+                    "as best answer."
                 ],
                 ],
             },
             },
         )
         )
@@ -1748,8 +1759,8 @@ class ThreadUnmarkBestAnswerApiTests(ThreadPatchApiTestCase):
             {
             {
                 "id": self.thread.id,
                 "id": self.thread.id,
                 "detail": [
                 "detail": [
-                    'You don\'t have permission to unmark threads answers in the "First category" '
-                    "category."
+                    "You don't have permission to unmark threads answers in "
+                    'the "First category" category.'
                 ],
                 ],
             },
             },
         )
         )
@@ -1770,8 +1781,8 @@ class ThreadUnmarkBestAnswerApiTests(ThreadPatchApiTestCase):
             {
             {
                 "id": self.thread.id,
                 "id": self.thread.id,
                 "detail": [
                 "detail": [
-                    "You don't have permission to unmark this best answer because you are not a "
-                    "thread starter."
+                    "You don't have permission to unmark this best answer because "
+                    "you are not a thread starter."
                 ],
                 ],
             },
             },
         )
         )
@@ -1797,7 +1808,9 @@ class ThreadUnmarkBestAnswerApiTests(ThreadPatchApiTestCase):
         }
         }
     )
     )
     def test_unmark_best_answer_timelimit(self):
     def test_unmark_best_answer_timelimit(self):
-        """api validates if starter has permission to unmark best answer within time limit"""
+        """
+        api validates if starter has permission to unmark best answer within time limit
+        """
         self.thread.best_answer_marked_on = timezone.now() - timedelta(minutes=6)
         self.thread.best_answer_marked_on = timezone.now() - timedelta(minutes=6)
         self.thread.starter = self.user
         self.thread.starter = self.user
         self.thread.save()
         self.thread.save()
@@ -1812,8 +1825,8 @@ class ThreadUnmarkBestAnswerApiTests(ThreadPatchApiTestCase):
             {
             {
                 "id": self.thread.id,
                 "id": self.thread.id,
                 "detail": [
                 "detail": [
-                    "You don't have permission to unmark best answer that was marked for more than "
-                    "5 minutes."
+                    "You don't have permission to unmark best answer that was marked "
+                    "for more than 5 minutes."
                 ],
                 ],
             },
             },
         )
         )
@@ -1839,7 +1852,9 @@ class ThreadUnmarkBestAnswerApiTests(ThreadPatchApiTestCase):
         }
         }
     )
     )
     def test_unmark_best_answer_closed_category_no_permission(self):
     def test_unmark_best_answer_closed_category_no_permission(self):
-        """api validates if user has permission to unmark best answer in closed category"""
+        """
+        api validates if user has permission to unmark best answer in closed category
+        """
         self.category.is_closed = True
         self.category.is_closed = True
         self.category.save()
         self.category.save()
 
 
@@ -1853,8 +1868,8 @@ class ThreadUnmarkBestAnswerApiTests(ThreadPatchApiTestCase):
             {
             {
                 "id": self.thread.id,
                 "id": self.thread.id,
                 "detail": [
                 "detail": [
-                    "You don't have permission to unmark this best answer because its category "
-                    '"First category" is closed.'
+                    "You don't have permission to unmark this best answer because "
+                    'its category "First category" is closed.'
                 ],
                 ],
             },
             },
         )
         )
@@ -1870,7 +1885,9 @@ class ThreadUnmarkBestAnswerApiTests(ThreadPatchApiTestCase):
         }
         }
     )
     )
     def test_unmark_best_answer_closed_category(self):
     def test_unmark_best_answer_closed_category(self):
-        """api validates if user has permission to unmark best answer in closed category"""
+        """
+        api validates if user has permission to unmark best answer in closed category
+        """
         self.category.is_closed = True
         self.category.is_closed = True
         self.category.save()
         self.category.save()
 
 
@@ -1888,7 +1905,9 @@ class ThreadUnmarkBestAnswerApiTests(ThreadPatchApiTestCase):
         }
         }
     )
     )
     def test_unmark_best_answer_closed_thread_no_permission(self):
     def test_unmark_best_answer_closed_thread_no_permission(self):
-        """api validates if user has permission to unmark best answer in closed thread"""
+        """
+        api validates if user has permission to unmark best answer in closed thread
+        """
         self.thread.is_closed = True
         self.thread.is_closed = True
         self.thread.save()
         self.thread.save()
 
 
@@ -1902,8 +1921,8 @@ class ThreadUnmarkBestAnswerApiTests(ThreadPatchApiTestCase):
             {
             {
                 "id": self.thread.id,
                 "id": self.thread.id,
                 "detail": [
                 "detail": [
-                    "You can't unmark this thread's best answer because it's closed and you don't "
-                    "have permission to open it."
+                    "You can't unmark this thread's best answer because it's closed "
+                    "and you don't have permission to open it."
                 ],
                 ],
             },
             },
         )
         )
@@ -1919,7 +1938,9 @@ class ThreadUnmarkBestAnswerApiTests(ThreadPatchApiTestCase):
         }
         }
     )
     )
     def test_unmark_best_answer_closed_thread(self):
     def test_unmark_best_answer_closed_thread(self):
-        """api validates if user has permission to unmark best answer in closed thread"""
+        """
+        api validates if user has permission to unmark best answer in closed thread
+        """
         self.thread.is_closed = True
         self.thread.is_closed = True
         self.thread.save()
         self.thread.save()
 
 
@@ -1951,8 +1972,8 @@ class ThreadUnmarkBestAnswerApiTests(ThreadPatchApiTestCase):
             {
             {
                 "id": self.thread.id,
                 "id": self.thread.id,
                 "detail": [
                 "detail": [
-                    "You don't have permission to unmark this thread's best answer because a "
-                    "moderator has protected it."
+                    "You don't have permission to unmark this thread's best answer "
+                    "because a moderator has protected it."
                 ],
                 ],
             },
             },
         )
         )

+ 8 - 4
misago/threads/tests/test_thread_pollcreate_api.py

@@ -90,7 +90,9 @@ class ThreadPollCreateTests(ThreadPollApiTestCase):
 
 
     @patch_user_acl({"can_start_polls": 1})
     @patch_user_acl({"can_start_polls": 1})
     def test_other_user_thread_no_permission(self):
     def test_other_user_thread_no_permission(self):
-        """api validates that user has permission to start poll in other user's thread"""
+        """
+        api validates that user has permission to start poll in other user's thread
+        """
         self.thread.starter = None
         self.thread.starter = None
         self.thread.save()
         self.thread.save()
 
 
@@ -102,7 +104,9 @@ class ThreadPollCreateTests(ThreadPollApiTestCase):
 
 
     @patch_user_acl({"can_start_polls": 2})
     @patch_user_acl({"can_start_polls": 2})
     def test_other_user_thread(self):
     def test_other_user_thread(self):
-        """api validates that user has permission to start poll in other user's thread"""
+        """
+        api validates that user has permission to start poll in other user's thread
+        """
         self.thread.starter = None
         self.thread.starter = None
         self.thread.save()
         self.thread.save()
 
 
@@ -273,7 +277,7 @@ class ThreadPollCreateTests(ThreadPollApiTestCase):
         self.assertTrue(response_json["is_public"])
         self.assertTrue(response_json["is_public"])
 
 
         self.assertEqual(len(response_json["choices"]), 3)
         self.assertEqual(len(response_json["choices"]), 3)
-        self.assertEqual(len(set([c["hash"] for c in response_json["choices"]])), 3)
+        self.assertEqual(len({c["hash"] for c in response_json["choices"]}), 3)
         self.assertEqual(
         self.assertEqual(
             [c["label"] for c in response_json["choices"]], ["Red", "Green", "Blue"]
             [c["label"] for c in response_json["choices"]], ["Red", "Green", "Blue"]
         )
         )
@@ -296,6 +300,6 @@ class ThreadPollCreateTests(ThreadPollApiTestCase):
         self.assertTrue(poll.is_public)
         self.assertTrue(poll.is_public)
 
 
         self.assertEqual(len(poll.choices), 3)
         self.assertEqual(len(poll.choices), 3)
-        self.assertEqual(len(set([c["hash"] for c in poll.choices])), 3)
+        self.assertEqual(len({c["hash"] for c in poll.choices}), 3)
 
 
         self.assertEqual(self.user.audittrail_set.count(), 1)
         self.assertEqual(self.user.audittrail_set.count(), 1)

+ 2 - 2
misago/threads/tests/test_thread_polledit_api.py

@@ -307,7 +307,7 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
 
 
         # choices were updated
         # choices were updated
         self.assertEqual(len(response_json["choices"]), 3)
         self.assertEqual(len(response_json["choices"]), 3)
-        self.assertEqual(len(set([c["hash"] for c in response_json["choices"]])), 3)
+        self.assertEqual(len({c["hash"] for c in response_json["choices"]}), 3)
         self.assertEqual(
         self.assertEqual(
             [c["label"] for c in response_json["choices"]], ["Red", "Green", "Blue"]
             [c["label"] for c in response_json["choices"]], ["Red", "Green", "Blue"]
         )
         )
@@ -491,7 +491,7 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
 
 
         # choices were updated
         # choices were updated
         self.assertEqual(len(response_json["choices"]), 3)
         self.assertEqual(len(response_json["choices"]), 3)
-        self.assertEqual(len(set([c["hash"] for c in response_json["choices"]])), 3)
+        self.assertEqual(len({c["hash"] for c in response_json["choices"]}), 3)
         self.assertEqual(
         self.assertEqual(
             [c["label"] for c in response_json["choices"]], ["Red", "Green", "Blue"]
             [c["label"] for c in response_json["choices"]], ["Red", "Green", "Blue"]
         )
         )

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

@@ -243,7 +243,9 @@ class ThreadPostVotesTests(ThreadPollApiTestCase):
         )
         )
 
 
     def test_revote(self):
     def test_revote(self):
-        """api validates if user is trying to change vote in poll that disallows revoting"""
+        """
+        api validates if user is trying to change vote in poll that disallows revoting
+        """
         response = self.post(self.api_link, data=["lorem", "ipsum"])
         response = self.post(self.api_link, data=["lorem", "ipsum"])
         self.assertEqual(response.status_code, 403)
         self.assertEqual(response.status_code, 403)
         self.assertEqual(
         self.assertEqual(

+ 4 - 2
misago/threads/tests/test_thread_postbulkdelete_api.py

@@ -259,9 +259,11 @@ class PostBulkDeleteApiTests(ThreadsApiTestCase):
         ids = [self.posts[0].id, self.posts[-1].id]
         ids = [self.posts[0].id, self.posts[-1].id]
 
 
         response = self.delete(self.api_link, ids)
         response = self.delete(self.api_link, ids)
-        self.thread = Thread.objects.get(pk=self.thread.pk)
+        self.assertEqual(response.status_code, 200)
 
 
+        self.thread = Thread.objects.get(pk=self.thread.pk)
         self.assertNotEqual(self.thread.last_post_id, ids[-1])
         self.assertNotEqual(self.thread.last_post_id, ids[-1])
+
         for post in ids:
         for post in ids:
             with self.assertRaises(Post.DoesNotExist):
             with self.assertRaises(Post.DoesNotExist):
                 self.thread.post_set.get(pk=post)
                 self.thread.post_set.get(pk=post)
@@ -273,8 +275,8 @@ class PostBulkDeleteApiTests(ThreadsApiTestCase):
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         self.thread = Thread.objects.get(pk=self.thread.pk)
         self.thread = Thread.objects.get(pk=self.thread.pk)
-
         self.assertNotEqual(self.thread.last_post_id, self.posts[-1].pk)
         self.assertNotEqual(self.thread.last_post_id, self.posts[-1].pk)
+
         for post in self.posts:
         for post in self.posts:
             with self.assertRaises(Post.DoesNotExist):
             with self.assertRaises(Post.DoesNotExist):
                 self.thread.post_set.get(pk=post.pk)
                 self.thread.post_set.get(pk=post.pk)

+ 0 - 2
misago/threads/tests/test_thread_postbulkpatch_api.py

@@ -1,8 +1,6 @@
 import json
 import json
-from datetime import timedelta
 
 
 from django.urls import reverse
 from django.urls import reverse
-from django.utils import timezone
 
 
 from .. import test
 from .. import test
 from ...categories.models import Category
 from ...categories.models import Category

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

@@ -6,7 +6,7 @@ from .. import test
 from ...categories.models import Category
 from ...categories.models import Category
 from ...readtracker import poststracker
 from ...readtracker import poststracker
 from ...users.test import AuthenticatedUserTestCase
 from ...users.test import AuthenticatedUserTestCase
-from ..models import Post, Thread
+from ..models import Post
 from ..serializers.moderation import POSTS_LIMIT
 from ..serializers.moderation import POSTS_LIMIT
 from ..test import patch_category_acl
 from ..test import patch_category_acl
 
 
@@ -414,7 +414,10 @@ class ThreadPostMergeApiTestCase(AuthenticatedUserTestCase):
         self.assertEqual(
         self.assertEqual(
             response.json(),
             response.json(),
             {
             {
-                "detail": "Post marked as best answer can't be merged with thread's first post."
+                "detail": (
+                    "Post marked as best answer can't be "
+                    "merged with thread's first post."
+                )
             },
             },
         )
         )
 
 

+ 1 - 1
misago/threads/tests/test_thread_postmove_api.py

@@ -177,7 +177,7 @@ class ThreadPostMoveApiTestCase(AuthenticatedUserTestCase):
     @patch_category_acl({"can_move_posts": True})
     @patch_category_acl({"can_move_posts": True})
     def test_empty_data(self):
     def test_empty_data(self):
         """api handles empty data"""
         """api handles empty data"""
-        other_thread = test.post_thread(self.category)
+        test.post_thread(self.category)
 
 
         response = self.client.post(self.api_link)
         response = self.client.post(self.api_link)
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)

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

@@ -7,7 +7,7 @@ from django.utils import timezone
 from .. import test
 from .. import test
 from ...categories.models import Category
 from ...categories.models import Category
 from ...users.test import AuthenticatedUserTestCase
 from ...users.test import AuthenticatedUserTestCase
-from ..models import Post, Thread
+from ..models import Post
 from ..test import patch_category_acl
 from ..test import patch_category_acl
 
 
 
 
@@ -42,7 +42,9 @@ class PostAddAclApiTests(ThreadPostPatchApiTestCase):
         self.assertTrue(response_json["acl"])
         self.assertTrue(response_json["acl"])
 
 
     def test_add_acl_false(self):
     def test_add_acl_false(self):
-        """if value is false, api won't add acl to the response, but will set empty key"""
+        """
+        if value is false, api won't add acl to the response, but will set empty key
+        """
         response = self.patch(
         response = self.patch(
             self.api_link, [{"op": "add", "path": "acl", "value": False}]
             self.api_link, [{"op": "add", "path": "acl", "value": False}]
         )
         )
@@ -879,7 +881,9 @@ class EventAddAclApiTests(ThreadEventPatchApiTestCase):
         self.assertTrue(response_json["acl"])
         self.assertTrue(response_json["acl"])
 
 
     def test_add_acl_false(self):
     def test_add_acl_false(self):
-        """if value is false, api won't add acl to the response, but will set empty key"""
+        """
+        if value is false, api won't add acl to the response, but will set empty key
+        """
         response = self.patch(
         response = self.patch(
             self.api_link, [{"op": "add", "path": "acl", "value": False}]
             self.api_link, [{"op": "add", "path": "acl", "value": False}]
         )
         )

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

@@ -6,7 +6,7 @@ from .. import test
 from ...categories.models import Category
 from ...categories.models import Category
 from ...readtracker import poststracker
 from ...readtracker import poststracker
 from ...users.test import AuthenticatedUserTestCase
 from ...users.test import AuthenticatedUserTestCase
-from ..models import Post, Thread
+from ..models import Post
 from ..serializers.moderation import POSTS_LIMIT
 from ..serializers.moderation import POSTS_LIMIT
 from ..test import patch_category_acl, patch_other_category_acl
 from ..test import patch_category_acl, patch_other_category_acl
 
 
@@ -407,7 +407,8 @@ class ThreadPostSplitApiTestCase(AuthenticatedUserTestCase):
             response_json,
             response_json,
             {
             {
                 "weight": [
                 "weight": [
-                    "You don't have permission to pin threads globally in this category."
+                    "You don't have permission to pin threads "
+                    "globally in this category."
                 ]
                 ]
             },
             },
         )
         )

+ 0 - 1
misago/threads/tests/test_threads_api.py

@@ -1,6 +1,5 @@
 from datetime import timedelta
 from datetime import timedelta
 
 
-from django.urls import reverse
 from django.utils import timezone
 from django.utils import timezone
 
 
 from .. import test
 from .. import test

+ 18 - 5
misago/threads/tests/test_threads_editor_api.py

@@ -49,7 +49,10 @@ class ThreadPostEditorApiTests(EditorApiTestCase):
         self.assertEqual(
         self.assertEqual(
             response.json(),
             response.json(),
             {
             {
-                "detail": "No categories that allow new threads are available to you at the moment."
+                "detail": (
+                    "No categories that allow new threads are available "
+                    "to you at the moment."
+                )
             },
             },
         )
         )
 
 
@@ -61,7 +64,10 @@ class ThreadPostEditorApiTests(EditorApiTestCase):
         self.assertEqual(
         self.assertEqual(
             response.json(),
             response.json(),
             {
             {
-                "detail": "No categories that allow new threads are available to you at the moment."
+                "detail": (
+                    "No categories that allow new threads are available "
+                    "to you at the moment."
+                )
             },
             },
         )
         )
 
 
@@ -76,7 +82,10 @@ class ThreadPostEditorApiTests(EditorApiTestCase):
         self.assertEqual(
         self.assertEqual(
             response.json(),
             response.json(),
             {
             {
-                "detail": "No categories that allow new threads are available to you at the moment."
+                "detail": (
+                    "No categories that allow new threads are available "
+                    "to you at the moment."
+                )
             },
             },
         )
         )
 
 
@@ -258,7 +267,9 @@ class ThreadReplyEditorApiTests(EditorApiTestCase):
             self.assertEqual(
             self.assertEqual(
                 response.json(),
                 response.json(),
                 {
                 {
-                    "detail": "This category is closed. You can't reply to threads in it."
+                    "detail": (
+                        "This category is closed. You can't reply to threads in it."
+                    )
                 },
                 },
             )
             )
 
 
@@ -511,7 +522,9 @@ class EditReplyEditorApiTests(EditorApiTestCase):
 
 
     @patch_category_acl({"can_hide_threads": 1, "can_edit_posts": 2})
     @patch_category_acl({"can_hide_threads": 1, "can_edit_posts": 2})
     def test_edit_first_post_hidden(self):
     def test_edit_first_post_hidden(self):
-        """endpoint returns valid configuration for editor of hidden thread's first post"""
+        """
+        endpoint returns valid configuration for editor of hidden thread's first post
+        """
         self.thread.is_hidden = True
         self.thread.is_hidden = True
         self.thread.save()
         self.thread.save()
         self.thread.first_post.is_hidden = True
         self.thread.first_post.is_hidden = True

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

@@ -371,7 +371,8 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
             response.json(),
             response.json(),
             {
             {
                 "weight": [
                 "weight": [
-                    "You don't have permission to pin threads globally in this category."
+                    "You don't have permission to pin threads globally "
+                    "in this category."
                 ]
                 ]
             },
             },
         )
         )

+ 0 - 3
misago/threads/tests/test_threads_moderation.py

@@ -18,9 +18,6 @@ class ThreadsModerationTests(AuthenticatedUserTestCase):
         self.category = Category.objects.all_categories()[:1][0]
         self.category = Category.objects.all_categories()[:1][0]
         self.thread = test.post_thread(self.category)
         self.thread = test.post_thread(self.category)
 
 
-    def tearDown(self):
-        super().tearDown()
-
     def reload_thread(self):
     def reload_thread(self):
         self.thread = Thread.objects.get(pk=self.thread.pk)
         self.thread = Thread.objects.get(pk=self.thread.pk)
 
 

+ 0 - 1
misago/threads/tests/test_threadslists.py

@@ -9,7 +9,6 @@ from ...acl.test import patch_user_acl
 from ...categories.models import Category
 from ...categories.models import Category
 from ...conf import settings
 from ...conf import settings
 from ...readtracker import poststracker
 from ...readtracker import poststracker
-from ...users.models import AnonymousUser
 from ...users.test import AuthenticatedUserTestCase
 from ...users.test import AuthenticatedUserTestCase
 
 
 LISTS_URLS = ("", "my/", "new/", "unread/", "subscribed/")
 LISTS_URLS = ("", "my/", "new/", "unread/", "subscribed/")

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

@@ -191,7 +191,9 @@ class ThreadPostsVisibilityTests(ThreadViewTestCase):
             self.assertContains(response, post.parsed)
             self.assertContains(response, post.parsed)
 
 
     def test_unapproved_post_visibility(self):
     def test_unapproved_post_visibility(self):
-        """unapproved post renders for its author and users with perm to approve content"""
+        """
+        unapproved post renders for its author and users with perm to approve content
+        """
         post = test.reply_thread(self.thread, is_unapproved=True)
         post = test.reply_thread(self.thread, is_unapproved=True)
 
 
         # post is hdden because we aren't its author nor user with permission to approve
         # post is hdden because we aren't its author nor user with permission to approve

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

@@ -28,7 +28,6 @@ class ValidatePostLengthTests(TestCase):
         """too long post is rejected"""
         """too long post is rejected"""
         settings = Mock(post_length_min=1, post_length_max=2)
         settings = Mock(post_length_min=1, post_length_max=2)
         with self.assertRaises(ValidationError):
         with self.assertRaises(ValidationError):
-            post = "a" * settings.post_length_max
             validate_post_length(settings, "abc")
             validate_post_length(settings, "abc")
 
 
 
 
@@ -37,7 +36,7 @@ class ValidateThreadTitleTests(TestCase):
         """validate_thread_title is ok with valid titles"""
         """validate_thread_title is ok with valid titles"""
         settings = Mock(thread_title_length_min=1, thread_title_length_max=50)
         settings = Mock(thread_title_length_min=1, thread_title_length_max=50)
 
 
-        VALID_TITLES = ["Lorem ipsum dolor met", "123 456 789 112" "Ugabugagagagagaga"]
+        VALID_TITLES = ["Lorem ipsum dolor met", "123 456 789 112", "Ugabugagagagagaga"]
 
 
         for title in VALID_TITLES:
         for title in VALID_TITLES:
             validate_thread_title(settings, title)
             validate_thread_title(settings, title)

+ 4 - 4
misago/threads/threadtypes/privatethread.py

@@ -38,10 +38,10 @@ class PrivateThread(ThreadType):
                 "misago:private-thread",
                 "misago:private-thread",
                 kwargs={"slug": thread.slug, "pk": thread.pk, "page": page},
                 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):
     def get_thread_last_post_url(self, thread):
         return reverse(
         return reverse(

+ 5 - 8
misago/threads/threadtypes/thread.py

@@ -11,16 +11,15 @@ class Thread(ThreadType):
     def get_category_name(self, category):
     def get_category_name(self, category):
         if category.level:
         if category.level:
             return category.name
             return category.name
-        else:
-            return _("None (will become top level category)")
+        return _("None (will become top level category)")
 
 
     def get_category_absolute_url(self, category):
     def get_category_absolute_url(self, category):
         if category.level:
         if category.level:
             return reverse(
             return reverse(
                 "misago:category", kwargs={"pk": category.pk, "slug": category.slug}
                 "misago:category", kwargs={"pk": category.pk, "slug": category.slug}
             )
             )
-        else:
-            return reverse("misago:threads")
+
+        return reverse("misago:threads")
 
 
     def get_category_last_thread_url(self, category):
     def get_category_last_thread_url(self, category):
         return reverse(
         return reverse(
@@ -46,10 +45,8 @@ class Thread(ThreadType):
                 "misago:thread",
                 "misago:thread",
                 kwargs={"slug": thread.slug, "pk": thread.pk, "page": page},
                 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):
     def get_thread_last_post_url(self, thread):
         return reverse(
         return reverse(

+ 13 - 15
misago/threads/utils.py

@@ -1,6 +1,6 @@
 from urllib.parse import urlparse
 from urllib.parse import urlparse
 
 
-from django.urls import resolve
+from django.urls import Resolver404, resolve
 
 
 from .models import PostLike
 from .models import PostLike
 
 
@@ -38,27 +38,25 @@ SUPPORTED_THREAD_ROUTES = {
 }
 }
 
 
 
 
-def get_thread_id_from_url(request, url):
-    try:
-        clean_url = str(url).strip()
-        bits = urlparse(clean_url)
-    except:
-        return None
+def get_thread_id_from_url(request, url):  # pylint: disable=too-many-return-statements
+    clean_url = str(url).strip()
+    url_bits = urlparse(clean_url)
 
 
-    if bits.netloc and bits.netloc != request.get_host():
+    if url_bits.netloc and url_bits.netloc != request.get_host():
         return None
         return None
 
 
-    if bits.path.startswith(request.get_host()):
-        clean_path = bits.path.lstrip(request.get_host())
+    if url_bits.path.startswith(request.get_host()):
+        clean_path = url_bits.path.lstrip(request.get_host())
     else:
     else:
-        clean_path = bits.path
+        clean_path = url_bits.path
+
+    wsgi_alias = request.path[: len(request.path_info) * -1]
+    if wsgi_alias and not clean_path.startswith(wsgi_alias):
+        return None
 
 
     try:
     try:
-        wsgi_alias = request.path[: len(request.path_info) * -1]
-        if wsgi_alias and not clean_path.startswith(wsgi_alias):
-            return None
         resolution = resolve(clean_path[len(wsgi_alias) :])
         resolution = resolve(clean_path[len(wsgi_alias) :])
-    except:
+    except Resolver404:
         return None
         return None
 
 
     if not resolution.namespaces:
     if not resolution.namespaces:

+ 4 - 0
misago/threads/validators.py

@@ -45,6 +45,7 @@ def validate_thread_title_length(settings, value):
         raise ValidationError(_("You have to enter an thread title."))
         raise ValidationError(_("You have to enter an thread title."))
 
 
     if value_len < settings.thread_title_length_min:
     if value_len < settings.thread_title_length_min:
+        # pylint: disable=line-too-long
         message = ngettext(
         message = ngettext(
             "Thread title should be at least %(limit_value)s character long (it has %(show_value)s).",
             "Thread title should be at least %(limit_value)s character long (it has %(show_value)s).",
             "Thread title should be at least %(limit_value)s characters long (it has %(show_value)s).",
             "Thread title should be at least %(limit_value)s characters long (it has %(show_value)s).",
@@ -56,6 +57,7 @@ def validate_thread_title_length(settings, value):
         )
         )
 
 
     if value_len > settings.thread_title_length_max:
     if value_len > settings.thread_title_length_max:
+        # pylint: disable=line-too-long
         message = ngettext(
         message = ngettext(
             "Thread title cannot be longer than %(limit_value)s character (it has %(show_value)s).",
             "Thread title cannot be longer than %(limit_value)s character (it has %(show_value)s).",
             "Thread title cannot be longer than %(limit_value)s characters (it has %(show_value)s).",
             "Thread title cannot be longer than %(limit_value)s characters (it has %(show_value)s).",
@@ -74,6 +76,7 @@ def validate_post_length(settings, value):
         raise ValidationError(_("You have to enter a message."))
         raise ValidationError(_("You have to enter a message."))
 
 
     if value_len < settings.post_length_min:
     if value_len < settings.post_length_min:
+        # pylint: disable=line-too-long
         message = ngettext(
         message = ngettext(
             "Posted message should be at least %(limit_value)s character long (it has %(show_value)s).",
             "Posted message should be at least %(limit_value)s character long (it has %(show_value)s).",
             "Posted message should be at least %(limit_value)s characters long (it has %(show_value)s).",
             "Posted message should be at least %(limit_value)s characters long (it has %(show_value)s).",
@@ -84,6 +87,7 @@ def validate_post_length(settings, value):
         )
         )
 
 
     if settings.post_length_max and value_len > settings.post_length_max:
     if settings.post_length_max and value_len > settings.post_length_max:
+        # pylint: disable=line-too-long
         message = ngettext(
         message = ngettext(
             "Posted message cannot be longer than %(limit_value)s character (it has %(show_value)s).",
             "Posted message cannot be longer than %(limit_value)s character (it has %(show_value)s).",
             "Posted message cannot be longer than %(limit_value)s characters (it has %(show_value)s).",
             "Posted message cannot be longer than %(limit_value)s characters (it has %(show_value)s).",

+ 1 - 1
misago/threads/viewmodels/posts.py

@@ -12,7 +12,7 @@ __all__ = ["ThreadPosts"]
 
 
 
 
 class ViewModel:
 class ViewModel:
-    def __init__(self, request, thread, page):
+    def __init__(self, request, thread, page):  # pylint: disable=too-many-locals
         try:
         try:
             thread_model = thread.unwrap()
             thread_model = thread.unwrap()
         except AttributeError:
         except AttributeError:

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

@@ -1,9 +1,6 @@
-from datetime import timedelta
-
 from django.core.exceptions import PermissionDenied
 from django.core.exceptions import PermissionDenied
-from django.db.models import F, Q
+from django.db.models import Q
 from django.http import Http404
 from django.http import Http404
-from django.utils import timezone
 from django.utils.translation import gettext as _
 from django.utils.translation import gettext as _
 from django.utils.translation import gettext_lazy
 from django.utils.translation import gettext_lazy
 
 
@@ -157,14 +154,12 @@ class ForumThreads(ViewModel):
             return list(queryset.filter(weight=2)) + list(
             return list(queryset.filter(weight=2)) + list(
                 queryset.filter(weight=1, category__in=threads_categories)
                 queryset.filter(weight=1, category__in=threads_categories)
             )
             )
-        else:
-            return queryset.filter(weight=2)
+        return queryset.filter(weight=2)
 
 
     def get_remaining_threads_queryset(self, queryset, category, threads_categories):
     def get_remaining_threads_queryset(self, queryset, category, threads_categories):
         if category.level:
         if category.level:
             return queryset.filter(weight=0, category__in=threads_categories)
             return queryset.filter(weight=0, category__in=threads_categories)
-        else:
-            return queryset.filter(weight__lt=2, category__in=threads_categories)
+        return queryset.filter(weight__lt=2, category__in=threads_categories)
 
 
 
 
 class PrivateThreads(ViewModel):
 class PrivateThreads(ViewModel):
@@ -192,25 +187,22 @@ class PrivateThreads(ViewModel):
 
 
 def get_threads_queryset(request, categories, list_type):
 def get_threads_queryset(request, categories, list_type):
     queryset = exclude_invisible_threads(request.user_acl, categories, Thread.objects)
     queryset = exclude_invisible_threads(request.user_acl, categories, Thread.objects)
-
     if list_type == "all":
     if list_type == "all":
         return queryset
         return queryset
-    else:
-        return filter_threads_queryset(request, categories, list_type, queryset)
+    return filter_threads_queryset(request, categories, list_type, queryset)
 
 
 
 
 def filter_threads_queryset(request, categories, list_type, queryset):
 def filter_threads_queryset(request, categories, list_type, queryset):
     if list_type == "my":
     if list_type == "my":
         return queryset.filter(starter=request.user)
         return queryset.filter(starter=request.user)
-    elif list_type == "subscribed":
+    if list_type == "subscribed":
         subscribed_threads = request.user.subscription_set.values("thread_id")
         subscribed_threads = request.user.subscription_set.values("thread_id")
         return queryset.filter(id__in=subscribed_threads)
         return queryset.filter(id__in=subscribed_threads)
-    elif list_type == "unapproved":
+    if list_type == "unapproved":
         return queryset.filter(has_unapproved_posts=True)
         return queryset.filter(has_unapproved_posts=True)
-    elif list_type in ("new", "unread"):
+    if list_type in ("new", "unread"):
         return filter_read_threads_queryset(request, categories, list_type, queryset)
         return filter_read_threads_queryset(request, categories, list_type, queryset)
-    else:
-        return queryset
+    return queryset
 
 
 
 
 def filter_read_threads_queryset(request, categories, list_type, queryset):
 def filter_read_threads_queryset(request, categories, list_type, queryset):

+ 2 - 1
misago/threads/views/admin/attachmenttypes.py

@@ -44,7 +44,8 @@ class DeleteAttachmentType(AttachmentTypeAdmin, generic.ButtonView):
     def check_permissions(self, request, target):
     def check_permissions(self, request, target):
         if target.attachment_set.exists():
         if target.attachment_set.exists():
             message = _(
             message = _(
-                'Attachment type "%(name)s" has associated attachments and can\'t be deleted.'
+                'Attachment type "%(name)s" has '
+                "associated attachments and can't be deleted."
             )
             )
             return message % {"name": target.name}
             return message % {"name": target.name}
 
 

+ 5 - 7
misago/threads/views/attachment.py

@@ -34,13 +34,11 @@ def serve_file(request, pk, secret, thumbnail):
     if attachment.is_image:
     if attachment.is_image:
         if thumbnail:
         if thumbnail:
             return attachment.thumbnail.url
             return attachment.thumbnail.url
-        else:
-            return attachment.image.url
-    else:
-        if thumbnail:
-            raise Http404()
-        else:
-            return attachment.file.url
+        return attachment.image.url
+
+    if thumbnail:
+        raise Http404()
+    return attachment.file.url
 
 
 
 
 def allow_file_download(request, attachment):
 def allow_file_download(request, attachment):

+ 2 - 3
misago/threads/views/goto.py

@@ -31,7 +31,7 @@ class GotoView(View):
         return self.get_redirect(thread, target_post, target_page)
         return self.get_redirect(thread, target_post, target_page)
 
 
     def get_thread(self, request, pk, slug):
     def get_thread(self, request, pk, slug):
-        return self.thread(request, pk, slug)
+        return self.thread(request, pk, slug)  # pylint: disable=not-callable
 
 
     def test_permissions(self, request, thread):
     def test_permissions(self, request, thread):
         pass
         pass
@@ -137,8 +137,7 @@ class ThreadGotoUnapprovedView(GotoView):
         )
         )
         if unapproved_post:
         if unapproved_post:
             return unapproved_post
             return unapproved_post
-        else:
-            return posts_queryset.order_by("id").last()
+        return posts_queryset.order_by("id").last()
 
 
 
 
 class PrivateThreadGotoPostView(GotoView):
 class PrivateThreadGotoPostView(GotoView):

+ 4 - 2
misago/threads/views/list.py

@@ -32,10 +32,12 @@ class ThreadsList(View):
         return render(request, self.template_name, template_context)
         return render(request, self.template_name, template_context)
 
 
     def get_category(self, request, **kwargs):
     def get_category(self, request, **kwargs):
-        return self.category(request, **kwargs)
+        return self.category(request, **kwargs)  # pylint: disable=not-callable
 
 
     def get_threads(self, request, category, list_type, page):
     def get_threads(self, request, category, list_type, page):
-        return self.threads(request, category, list_type, page)
+        return self.threads(  # pylint: disable=not-callable
+            request, category, list_type, page
+        )
 
 
     def get_frontend_context(self, request, category, threads):
     def get_frontend_context(self, request, category, threads):
         context = self.get_default_frontend_context()
         context = self.get_default_frontend_context()

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

@@ -22,7 +22,7 @@ class ThreadBase(View):
         return render(request, self.template_name, template_context)
         return render(request, self.template_name, template_context)
 
 
     def get_thread(self, request, pk, slug):
     def get_thread(self, request, pk, slug):
-        return self.thread(
+        return self.thread(  # pylint: disable=not-callable
             request,
             request,
             pk,
             pk,
             slug,
             slug,

+ 48 - 52
misago/users/api/auth.py

@@ -26,8 +26,7 @@ User = auth.get_user_model()
 def gateway(request):
 def gateway(request):
     if request.method == "POST":
     if request.method == "POST":
         return login(request)
         return login(request)
-    else:
-        return session_user(request)
+    return session_user(request)
 
 
 
 
 @api_view(["POST"])
 @api_view(["POST"])
@@ -43,8 +42,7 @@ def login(request):
     if form.is_valid():
     if form.is_valid():
         auth.login(request, form.user_cache)
         auth.login(request, form.user_cache)
         return Response(AuthenticatedUserSerializer(form.user_cache).data)
         return Response(AuthenticatedUserSerializer(form.user_cache).data)
-    else:
-        return Response(form.get_errors_dict(), status=status.HTTP_400_BAD_REQUEST)
+    return Response(form.get_errors_dict(), status=status.HTTP_400_BAD_REQUEST)
 
 
 
 
 @api_view()
 @api_view()
@@ -72,9 +70,7 @@ def get_criteria(request):
 
 
     for validator in settings.AUTH_PASSWORD_VALIDATORS:
     for validator in settings.AUTH_PASSWORD_VALIDATORS:
         validator_dict = {"name": validator["NAME"].split(".")[-1]}
         validator_dict = {"name": validator["NAME"].split(".")[-1]}
-
         validator_dict.update(validator.get("OPTIONS", {}))
         validator_dict.update(validator.get("OPTIONS", {}))
-
         criteria["password"].append(validator_dict)
         criteria["password"].append(validator_dict)
 
 
     return Response(criteria)
     return Response(criteria)
@@ -90,30 +86,30 @@ def send_activation(request):
     will mail account activation link to requester
     will mail account activation link to requester
     """
     """
     form = ResendActivationForm(request.data)
     form = ResendActivationForm(request.data)
-    if form.is_valid():
-        requesting_user = form.user_cache
-
-        mail_subject = _("Activate %(user)s account on %(forum_name)s forums") % {
-            "user": requesting_user.username,
-            "forum_name": request.settings.forum_name,
-        }
-
-        mail_user(
-            requesting_user,
-            mail_subject,
-            "misago/emails/activation/by_user",
-            context={
-                "activation_token": make_activation_token(requesting_user),
-                "settings": request.settings,
-            },
-        )
-
-        return Response(
-            {"username": form.user_cache.username, "email": form.user_cache.email}
-        )
-    else:
+    if not form.is_valid():
         return Response(form.get_errors_dict(), status=status.HTTP_400_BAD_REQUEST)
         return Response(form.get_errors_dict(), status=status.HTTP_400_BAD_REQUEST)
 
 
+    requesting_user = form.user_cache
+
+    mail_subject = _("Activate %(user)s account on %(forum_name)s forums") % {
+        "user": requesting_user.username,
+        "forum_name": request.settings.forum_name,
+    }
+
+    mail_user(
+        requesting_user,
+        mail_subject,
+        "misago/emails/activation/by_user",
+        context={
+            "activation_token": make_activation_token(requesting_user),
+            "settings": request.settings,
+        },
+    )
+
+    return Response(
+        {"username": form.user_cache.username, "email": form.user_cache.email}
+    )
+
 
 
 @api_view(["POST"])
 @api_view(["POST"])
 @permission_classes((UnbannedOnly,))
 @permission_classes((UnbannedOnly,))
@@ -125,32 +121,32 @@ def send_password_form(request):
     will mail change password form link to requester
     will mail change password form link to requester
     """
     """
     form = ResetPasswordForm(request.data)
     form = ResetPasswordForm(request.data)
-    if form.is_valid():
-        requesting_user = form.user_cache
-
-        mail_subject = _("Change %(user)s password on %(forum_name)s forums") % {
-            "user": requesting_user.username,
-            "forum_name": request.settings.forum_name,
-        }
-
-        confirmation_token = make_password_change_token(requesting_user)
-
-        mail_user(
-            requesting_user,
-            mail_subject,
-            "misago/emails/change_password_form_link",
-            context={
-                "confirmation_token": confirmation_token,
-                "settings": request.settings,
-            },
-        )
-
-        return Response(
-            {"username": form.user_cache.username, "email": form.user_cache.email}
-        )
-    else:
+    if not form.is_valid():
         return Response(form.get_errors_dict(), status=status.HTTP_400_BAD_REQUEST)
         return Response(form.get_errors_dict(), status=status.HTTP_400_BAD_REQUEST)
 
 
+    requesting_user = form.user_cache
+
+    mail_subject = _("Change %(user)s password on %(forum_name)s forums") % {
+        "user": requesting_user.username,
+        "forum_name": request.settings.forum_name,
+    }
+
+    confirmation_token = make_password_change_token(requesting_user)
+
+    mail_user(
+        requesting_user,
+        mail_subject,
+        "misago/emails/change_password_form_link",
+        context={
+            "confirmation_token": confirmation_token,
+            "settings": request.settings,
+        },
+    )
+
+    return Response(
+        {"username": form.user_cache.username, "email": form.user_cache.email}
+    )
+
 
 
 class PasswordChangeFailed(Exception):
 class PasswordChangeFailed(Exception):
     pass
     pass

+ 8 - 8
misago/users/api/captcha.py

@@ -5,12 +5,12 @@ from rest_framework.response import Response
 
 
 @api_view()
 @api_view()
 def question(request):
 def question(request):
-    if request.settings.qa_question:
-        return Response(
-            {
-                "question": request.settings.qa_question,
-                "help_text": request.settings.qa_help_text,
-            }
-        )
-    else:
+    if not request.settings.qa_question:
         raise Http404()
         raise Http404()
+
+    return Response(
+        {
+            "question": request.settings.qa_question,
+            "help_text": request.settings.qa_help_text,
+        }
+    )

+ 16 - 17
misago/users/api/userendpoints/avatar.py

@@ -32,8 +32,7 @@ def avatar_endpoint(request, pk=None):
     avatar_options = get_avatar_options(request, request.user)
     avatar_options = get_avatar_options(request, request.user)
     if request.method == "POST":
     if request.method == "POST":
         return avatar_post(request, avatar_options)
         return avatar_post(request, avatar_options)
-    else:
-        return Response(avatar_options)
+    return Response(avatar_options)
 
 
 
 
 def get_avatar_options(request, user):
 def get_avatar_options(request, user):
@@ -206,26 +205,26 @@ def moderate_avatar_endpoint(request, profile):
     if request.method == "POST":
     if request.method == "POST":
         is_avatar_locked = profile.is_avatar_locked
         is_avatar_locked = profile.is_avatar_locked
         serializer = ModerateAvatarSerializer(profile, data=request.data)
         serializer = ModerateAvatarSerializer(profile, data=request.data)
-        if serializer.is_valid():
-            if serializer.validated_data["is_avatar_locked"] and not is_avatar_locked:
-                avatars.dynamic.set_avatar(profile)
-            serializer.save()
-
-            return Response(
-                {
-                    "avatars": profile.avatars,
-                    "is_avatar_locked": int(profile.is_avatar_locked),
-                    "avatar_lock_user_message": profile.avatar_lock_user_message,
-                    "avatar_lock_staff_message": profile.avatar_lock_staff_message,
-                }
-            )
-        else:
+        if not serializer.is_valid():
             return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
             return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
-    else:
+
+        if serializer.validated_data["is_avatar_locked"] and not is_avatar_locked:
+            avatars.dynamic.set_avatar(profile)
+        serializer.save()
+
         return Response(
         return Response(
             {
             {
+                "avatars": profile.avatars,
                 "is_avatar_locked": int(profile.is_avatar_locked),
                 "is_avatar_locked": int(profile.is_avatar_locked),
                 "avatar_lock_user_message": profile.avatar_lock_user_message,
                 "avatar_lock_user_message": profile.avatar_lock_user_message,
                 "avatar_lock_staff_message": profile.avatar_lock_staff_message,
                 "avatar_lock_staff_message": profile.avatar_lock_staff_message,
             }
             }
         )
         )
+
+    return Response(
+        {
+            "is_avatar_locked": int(profile.is_avatar_locked),
+            "avatar_lock_user_message": profile.avatar_lock_user_message,
+            "avatar_lock_staff_message": profile.avatar_lock_staff_message,
+        }
+    )

+ 18 - 19
misago/users/api/userendpoints/changeemail.py

@@ -2,7 +2,6 @@ from django.utils.translation import gettext as _
 from rest_framework import status
 from rest_framework import status
 from rest_framework.response import Response
 from rest_framework.response import Response
 
 
-from ....conf import settings
 from ....core.mail import mail_user
 from ....core.mail import mail_user
 from ...credentialchange import store_new_credential
 from ...credentialchange import store_new_credential
 from ...serializers import ChangeEmailSerializer
 from ...serializers import ChangeEmailSerializer
@@ -13,25 +12,25 @@ def change_email_endpoint(request, pk=None):
         data=request.data, context={"user": request.user}
         data=request.data, context={"user": request.user}
     )
     )
 
 
-    if serializer.is_valid():
-        token = store_new_credential(
-            request, "email", serializer.validated_data["new_email"]
-        )
+    if not serializer.is_valid():
+        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
 
 
-        mail_subject = _("Confirm e-mail change on %(forum_name)s forums")
-        mail_subject = mail_subject % {"forum_name": request.settings.forum_name}
+    token = store_new_credential(
+        request, "email", serializer.validated_data["new_email"]
+    )
 
 
-        # swap address with new one so email is sent to new address
-        request.user.email = serializer.validated_data["new_email"]
+    mail_subject = _("Confirm e-mail change on %(forum_name)s forums")
+    mail_subject = mail_subject % {"forum_name": request.settings.forum_name}
 
 
-        mail_user(
-            request.user,
-            mail_subject,
-            "misago/emails/change_email",
-            context={"settings": request.settings, "token": token},
-        )
+    # swap address with new one so email is sent to new address
+    request.user.email = serializer.validated_data["new_email"]
 
 
-        message = _("E-mail change confirmation link was sent to new address.")
-        return Response({"detail": message})
-    else:
-        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+    mail_user(
+        request.user,
+        mail_subject,
+        "misago/emails/change_email",
+        context={"settings": request.settings, "token": token},
+    )
+
+    message = _("E-mail change confirmation link was sent to new address.")
+    return Response({"detail": message})

+ 19 - 19
misago/users/api/userendpoints/changepassword.py

@@ -12,23 +12,23 @@ def change_password_endpoint(request, pk=None):
         data=request.data, context={"user": request.user}
         data=request.data, context={"user": request.user}
     )
     )
 
 
-    if serializer.is_valid():
-        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": request.settings.forum_name}
-
-        mail_user(
-            request.user,
-            mail_subject,
-            "misago/emails/change_password",
-            context={"settings": request.settings, "token": token},
-        )
-
-        return Response(
-            {"detail": _("Password change confirmation link was sent to your address.")}
-        )
-    else:
+    if not serializer.is_valid():
         return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
         return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+
+    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": request.settings.forum_name}
+
+    mail_user(
+        request.user,
+        mail_subject,
+        "misago/emails/change_password",
+        context={"settings": request.settings, "token": token},
+    )
+
+    return Response(
+        {"detail": _("Password change confirmation link was sent to your address.")}
+    )

+ 0 - 1
misago/users/api/userendpoints/create.py

@@ -7,7 +7,6 @@ from rest_framework import status
 from rest_framework.response import Response
 from rest_framework.response import Response
 
 
 from ... import captcha
 from ... import captcha
-from ....conf import settings
 from ....legal.models import Agreement
 from ....legal.models import Agreement
 from ...forms.register import RegisterForm
 from ...forms.register import RegisterForm
 from ...registration import (
 from ...registration import (

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

@@ -36,8 +36,7 @@ def list_endpoint(request):
 
 
     if list_handler:
     if list_handler:
         return list_handler(request)
         return list_handler(request)
-    else:
-        return rank_users(request)
+    return rank_users(request)
 
 
 
 
 ScoredUserSerializer = UserCardSerializer.extend_fields("meta")
 ScoredUserSerializer = UserCardSerializer.extend_fields("meta")

+ 1 - 0
misago/users/api/userendpoints/signature.py

@@ -41,6 +41,7 @@ def get_signature_options(settings, user):
         options["signature"] = {"plain": user.signature, "html": user.signature_parsed}
         options["signature"] = {"plain": user.signature, "html": user.signature_parsed}
 
 
         if not is_user_signature_valid(user):
         if not is_user_signature_valid(user):
+            # pylint: disable=unsupported-assignment-operation
             options["signature"]["html"] = None
             options["signature"]["html"] = None
 
 
     return Response(options)
     return Response(options)

+ 40 - 40
misago/users/api/userendpoints/username.py

@@ -10,9 +10,9 @@ from ...serializers import ChangeUsernameSerializer
 def username_endpoint(request):
 def username_endpoint(request):
     if request.method == "POST":
     if request.method == "POST":
         return change_username(request)
         return change_username(request)
-    else:
-        options = get_username_options_from_request(request)
-        return options_response(options)
+
+    options = get_username_options_from_request(request)
+    return options_response(options)
 
 
 
 
 def get_username_options_from_request(request):
 def get_username_options_from_request(request):
@@ -36,31 +36,31 @@ def change_username(request):
     serializer = ChangeUsernameSerializer(
     serializer = ChangeUsernameSerializer(
         data=request.data, context={"settings": request.settings, "user": request.user}
         data=request.data, context={"settings": request.settings, "user": request.user}
     )
     )
-    if serializer.is_valid():
-        try:
-            serializer.change_username(changed_by=request.user)
-            updated_options = get_username_options_from_request(request)
-            if updated_options["next_on"]:
-                updated_options["next_on"] = updated_options["next_on"].isoformat()
-
-            return Response(
-                {
-                    "username": request.user.username,
-                    "slug": request.user.slug,
-                    "options": updated_options,
-                }
-            )
-        except IntegrityError:
-            return Response(
-                {"detail": _("Error changing username. Please try again.")},
-                status=status.HTTP_400_BAD_REQUEST,
-            )
-    else:
+    if not serializer.is_valid():
         return Response(
         return Response(
             {"detail": serializer.errors["non_field_errors"][0]},
             {"detail": serializer.errors["non_field_errors"][0]},
             status=status.HTTP_400_BAD_REQUEST,
             status=status.HTTP_400_BAD_REQUEST,
         )
         )
 
 
+    try:
+        serializer.change_username(changed_by=request.user)
+        updated_options = get_username_options_from_request(request)
+        if updated_options["next_on"]:
+            updated_options["next_on"] = updated_options["next_on"].isoformat()
+
+        return Response(
+            {
+                "username": request.user.username,
+                "slug": request.user.slug,
+                "options": updated_options,
+            }
+        )
+    except IntegrityError:
+        return Response(
+            {"detail": _("Error changing username. Please try again.")},
+            status=status.HTTP_400_BAD_REQUEST,
+        )
+
 
 
 def moderate_username_endpoint(request, profile):
 def moderate_username_endpoint(request, profile):
     if request.method == "POST":
     if request.method == "POST":
@@ -68,24 +68,24 @@ def moderate_username_endpoint(request, profile):
             data=request.data, context={"settings": request.settings, "user": profile}
             data=request.data, context={"settings": request.settings, "user": profile}
         )
         )
 
 
-        if serializer.is_valid():
-            try:
-                serializer.change_username(changed_by=request.user)
-                return Response({"username": profile.username, "slug": profile.slug})
-            except IntegrityError:
-                return Response(
-                    {"detail": _("Error changing username. Please try again.")},
-                    status=status.HTTP_400_BAD_REQUEST,
-                )
-        else:
+        if not serializer.is_valid():
             return Response(
             return Response(
                 {"detail": serializer.errors["non_field_errors"][0]},
                 {"detail": serializer.errors["non_field_errors"][0]},
                 status=status.HTTP_400_BAD_REQUEST,
                 status=status.HTTP_400_BAD_REQUEST,
             )
             )
-    else:
-        return Response(
-            {
-                "length_min": request.settings.username_length_min,
-                "length_max": request.settings.username_length_max,
-            }
-        )
+
+        try:
+            serializer.change_username(changed_by=request.user)
+            return Response({"username": profile.username, "slug": profile.slug})
+        except IntegrityError:
+            return Response(
+                {"detail": _("Error changing username. Please try again.")},
+                status=status.HTTP_400_BAD_REQUEST,
+            )
+
+    return Response(
+        {
+            "length_min": request.settings.username_length_min,
+            "length_max": request.settings.username_length_max,
+        }
+    )

+ 1 - 1
misago/users/api/usernamechanges.py

@@ -23,7 +23,7 @@ class UsernameChangesViewSetPermission(BasePermission):
 
 
         if user_pk == request.user.pk:
         if user_pk == request.user.pk:
             return True
             return True
-        elif not request.user_acl.get("can_see_users_name_history"):
+        if not request.user_acl.get("can_see_users_name_history"):
             raise PermissionDenied(
             raise PermissionDenied(
                 _("You don't have permission to see other users name history.")
                 _("You don't have permission to see other users name history.")
             )
             )

+ 2 - 4
misago/users/api/users.py

@@ -120,8 +120,7 @@ class UserViewSet(viewsets.GenericViewSet):
         if serializer.is_valid():
         if serializer.is_valid():
             serializer.save()
             serializer.save()
             return Response({"detail": _("Your forum options have been changed.")})
             return Response({"detail": _("Your forum options have been changed.")})
-        else:
-            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
 
 
     @detail_route(methods=["get", "post"])
     @detail_route(methods=["get", "post"])
     def username(self, request, pk=None):
     def username(self, request, pk=None):
@@ -210,8 +209,7 @@ class UserViewSet(viewsets.GenericViewSet):
         ban = get_user_ban(profile, request.cache_versions)
         ban = get_user_ban(profile, request.cache_versions)
         if ban:
         if ban:
             return Response(BanDetailsSerializer(ban).data)
             return Response(BanDetailsSerializer(ban).data)
-        else:
-            return Response({})
+        return Response({})
 
 
     @detail_route(methods=["get", "post"])
     @detail_route(methods=["get", "post"])
     def moderate_avatar(self, request, pk=None):
     def moderate_avatar(self, request, pk=None):

+ 2 - 5
misago/users/apps.py

@@ -72,8 +72,7 @@ class MisagoUsersConfig(AppConfig):
                 is_account_owner = profile.pk == request.user.pk
                 is_account_owner = profile.pk == request.user.pk
                 has_permission = request.user_acl["can_see_users_name_history"]
                 has_permission = request.user_acl["can_see_users_name_history"]
                 return is_account_owner or has_permission
                 return is_account_owner or has_permission
-            else:
-                return False
+            return False
 
 
         def can_see_ban_details(request, profile):
         def can_see_ban_details(request, profile):
             if request.user.is_authenticated:
             if request.user.is_authenticated:
@@ -81,10 +80,8 @@ class MisagoUsersConfig(AppConfig):
                     from .bans import get_user_ban
                     from .bans import get_user_ban
 
 
                     return bool(get_user_ban(profile, request.cache_versions))
                     return bool(get_user_ban(profile, request.cache_versions))
-                else:
-                    return False
-            else:
                 return False
                 return False
+            return False
 
 
         user_profile.add_section(
         user_profile.add_section(
             link="misago:user-posts", name=_("Posts"), icon="message", component="posts"
             link="misago:user-posts", name=_("Posts"), icon="message", component="posts"

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

@@ -27,7 +27,7 @@ def validate_uploaded_file(settings, uploaded_file):
             temporary_file_path = Path(uploaded_file.temporary_file_path())
             temporary_file_path = Path(uploaded_file.temporary_file_path())
             if temporary_file_path.exists():
             if temporary_file_path.exists():
                 temporary_file_path.unlink()
                 temporary_file_path.unlink()
-        except Exception:
+        except Exception:  # pylint: disable=broad-except
             pass
             pass
         raise e
         raise e
 
 
@@ -43,8 +43,7 @@ def validate_extension(uploaded_file):
     for extension in ALLOWED_EXTENSIONS:
     for extension in ALLOWED_EXTENSIONS:
         if lowercased_name.endswith(extension):
         if lowercased_name.endswith(extension):
             return True
             return True
-    else:
-        raise ValidationError(_("Uploaded file type is not allowed."))
+    raise ValidationError(_("Uploaded file type is not allowed."))
 
 
 
 
 def validate_mime(uploaded_file):
 def validate_mime(uploaded_file):

+ 5 - 9
misago/users/bans.py

@@ -47,15 +47,13 @@ def get_user_ban(user, cache_versions):
     try:
     try:
         ban_cache = user.ban_cache
         ban_cache = user.ban_cache
         if not ban_cache.is_valid(cache_versions):
         if not ban_cache.is_valid(cache_versions):
-            _set_user_ban_cache(user)
+            _set_user_ban_cache(user, cache_versions)
     except BanCache.DoesNotExist:
     except BanCache.DoesNotExist:
         user.ban_cache = BanCache(user=user)
         user.ban_cache = BanCache(user=user)
         user.ban_cache = _set_user_ban_cache(user, cache_versions)
         user.ban_cache = _set_user_ban_cache(user, cache_versions)
 
 
     if user.ban_cache.ban:
     if user.ban_cache.ban:
         return user.ban_cache
         return user.ban_cache
-    else:
-        return None
 
 
 
 
 def _set_user_ban_cache(user, cache_versions):
 def _set_user_ban_cache(user, cache_versions):
@@ -92,8 +90,7 @@ def get_request_ip_ban(request):
     if session_ban_cache:
     if session_ban_cache:
         if session_ban_cache["is_banned"]:
         if session_ban_cache["is_banned"]:
             return session_ban_cache
             return session_ban_cache
-        else:
-            return False
+        return False
 
 
     found_ban = get_ip_ban(request.user_ip)
     found_ban = get_ip_ban(request.user_ip)
 
 
@@ -111,10 +108,9 @@ def get_request_ip_ban(request):
         ban_cache.update({"is_banned": True, "message": found_ban.user_message})
         ban_cache.update({"is_banned": True, "message": found_ban.user_message})
         request.session[CACHE_SESSION_KEY] = ban_cache
         request.session[CACHE_SESSION_KEY] = ban_cache
         return _hydrate_session_cache(request.session[CACHE_SESSION_KEY])
         return _hydrate_session_cache(request.session[CACHE_SESSION_KEY])
-    else:
-        ban_cache["is_banned"] = False
-        request.session[CACHE_SESSION_KEY] = ban_cache
-        return None
+
+    ban_cache["is_banned"] = False
+    request.session[CACHE_SESSION_KEY] = ban_cache
 
 
 
 
 def _get_session_bancache(request):
 def _get_session_bancache(request):

+ 1 - 1
misago/users/datadownloads/__init__.py

@@ -39,7 +39,7 @@ def prepare_user_data_download(download, logger=None):
             download.save()
             download.save()
             # todo: send an e-mail with download link
             # todo: send an e-mail with download link
             return True
             return True
-        except Exception as e:
+        except Exception as e:  # pylint: disable=broad-except
             if logger:
             if logger:
                 logger.exception(e)
                 logger.exception(e)
             return False
             return False

+ 2 - 2
misago/users/datadownloads/dataarchive.py

@@ -81,8 +81,8 @@ class DataArchive:
 
 
     def add_dict(self, name, value, date=None, directory=None):
     def add_dict(self, name, value, date=None, directory=None):
         text_lines = []
         text_lines = []
-        for key, value in value.items():
-            text_lines.append("%s: %s" % (key, value))
+        for key, item in value.items():
+            text_lines.append("%s: %s" % (key, item))
         text = "\n".join(text_lines)
         text = "\n".join(text_lines)
         return self.add_text(name, text, date=date, directory=directory)
         return self.add_text(name, text, date=date, directory=directory)
 
 

+ 2 - 2
misago/users/djangoadmin.py

@@ -8,9 +8,9 @@ class UserAdminModel(ModelAdmin):
     """
     """
     The model should be used for interaction of third party django apps with
     The model should be used for interaction of third party django apps with
     Misago's `User`.
     Misago's `User`.
-    
+
     Removes `new` and `delete` actions (use Misago admin for that).
     Removes `new` and `delete` actions (use Misago admin for that).
-    
+
     Registration call is placed in :mod:`misago.users.admin`.
     Registration call is placed in :mod:`misago.users.admin`.
     The tests are in :mod:`misago.users.tests.test_djangoadmin_user`.
     The tests are in :mod:`misago.users.tests.test_djangoadmin_user`.
     """
     """

+ 13 - 13
misago/users/forms/admin.py

@@ -215,14 +215,12 @@ class EditUserForm(UserBaseForm):
 
 
         length_limit = self.settings.signature_length_max
         length_limit = self.settings.signature_length_max
         if len(data) > length_limit:
         if len(data) > length_limit:
-            raise forms.ValidationError(
-                ngettext(
-                    "Signature can't be longer than %(limit)s character.",
-                    "Signature can't be longer than %(limit)s characters.",
-                    length_limit,
-                )
-                % {"limit": length_limit}
+            message = ngettext(
+                "Signature can't be longer than %(limit)s character.",
+                "Signature can't be longer than %(limit)s characters.",
+                length_limit,
             )
             )
+            raise forms.ValidationError(message % {"limit": length_limit})
 
 
         return data
         return data
 
 
@@ -504,9 +502,10 @@ class BanForm(forms.ModelForm):
     registration_only = YesNoSwitch(
     registration_only = YesNoSwitch(
         label=_("Restrict this ban to registrations"),
         label=_("Restrict this ban to registrations"),
         help_text=_(
         help_text=_(
-            "Changing this to yes will make this ban check be only performed on registration "
-            "step. This is good if you want to block certain registrations like ones from "
-            "recently comprimised e-mail providers, without harming existing users."
+            "Changing this to yes will make this ban check be only performed on "
+            "registration step. This is good if you want to block certain "
+            "registrations like ones from recently comprimised e-mail providers, "
+            "without harming existing users."
         ),
         ),
     )
     )
     banned_value = forms.CharField(
     banned_value = forms.CharField(
@@ -628,9 +627,10 @@ class RequestDataDownloadsForm(forms.Form):
         label=_("Usernames or emails"),
         label=_("Usernames or emails"),
         help_text=_(
         help_text=_(
             "Enter every item in new line. Duplicates will be ignored. "
             "Enter every item in new line. Duplicates will be ignored. "
-            "This field is case insensitive. Depending on site configuration and amount of data "
-            "to archive it may take up to few days for requests to complete. E-mail "
-            "will notification will be sent to every user once their download is ready."
+            "This field is case insensitive. Depending on site configuration and "
+            "amount of data to archive it may take up to few days for requests to "
+            "complete. E-mail will notification will be sent to every user once their "
+            "download is ready."
         ),
         ),
         widget=forms.Textarea,
         widget=forms.Textarea,
     )
     )

+ 2 - 2
misago/users/forms/auth.py

@@ -18,8 +18,8 @@ class MisagoAuthMixin:
             "You have to activate your account before you will be able to sign in."
             "You have to activate your account before you will be able to sign in."
         ),
         ),
         "inactive_admin": _(
         "inactive_admin": _(
-            "Your account has to be activated by site administrator before you will be able "
-            "to sign in."
+            "Your account has to be activated by site administrator "
+            "before you will be able to sign in."
         ),
         ),
     }
     }
 
 

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

@@ -69,7 +69,7 @@ class SocialAuthRegisterForm(BaseRegisterForm):
         self.clean_agreements(cleaned_data)
         self.clean_agreements(cleaned_data)
         self.raise_if_ip_banned()
         self.raise_if_ip_banned()
 
 
-        validate_new_registration(self.request, cleaned_data, self)
+        validate_new_registration(self.request, cleaned_data, self.add_error)
 
 
         return cleaned_data
         return cleaned_data
 
 

+ 4 - 2
misago/users/management/commands/createsuperuser.py

@@ -73,7 +73,9 @@ class Command(BaseCommand):
         self.stdin = options.get("stdin", sys.stdin)  # Used for testing
         self.stdin = options.get("stdin", sys.stdin)  # Used for testing
         return super().execute(*args, **options)
         return super().execute(*args, **options)
 
 
-    def handle(self, *args, **options):
+    def handle(
+        self, *args, **options
+    ):  # pylint: disable=too-many-branches, too-many-locals
         username = options.get("username")
         username = options.get("username")
         email = options.get("email")
         email = options.get("email")
         password = options.get("password")
         password = options.get("password")
@@ -121,7 +123,7 @@ class Command(BaseCommand):
                     try:
                     try:
                         message = force_str("Enter displayed username: ")
                         message = force_str("Enter displayed username: ")
                         raw_value = input(message).strip()
                         raw_value = input(message).strip()
-                        validate_username(raw_value)
+                        validate_username(settings, raw_value)
                         username = raw_value
                         username = raw_value
                     except ValidationError as e:
                     except ValidationError as e:
                         self.stderr.write("\n".join(e.messages))
                         self.stderr.write("\n".join(e.messages))

+ 1 - 2
misago/users/management/commands/deletemarkedusers.py

@@ -1,7 +1,6 @@
 from django.contrib.auth import get_user_model
 from django.contrib.auth import get_user_model
-from django.core.management.base import BaseCommand, CommandError
+from django.core.management.base import BaseCommand
 
 
-from ....conf import settings
 from ....core.pgutils import chunk_queryset
 from ....core.pgutils import chunk_queryset
 from ...permissions import can_delete_own_account
 from ...permissions import can_delete_own_account
 
 

+ 1 - 1
misago/users/management/commands/deleteprofilefield.py

@@ -1,5 +1,5 @@
 from django.contrib.auth import get_user_model
 from django.contrib.auth import get_user_model
-from django.core.management.base import BaseCommand, CommandError
+from django.core.management.base import BaseCommand
 
 
 from ....core.pgutils import chunk_queryset
 from ....core.pgutils import chunk_queryset
 
 

+ 3 - 3
misago/users/management/commands/listusedprofilefields.py

@@ -13,13 +13,13 @@ class Command(BaseCommand):
         keys = {}
         keys = {}
 
 
         for user in chunk_queryset(User.objects.all()):
         for user in chunk_queryset(User.objects.all()):
-            for key in user.profile_fields.keys():
+            for key in user.profile_fields:
                 keys.setdefault(key, 0)
                 keys.setdefault(key, 0)
                 keys[key] += 1
                 keys[key] += 1
 
 
         if keys:
         if keys:
-            max_len = max([len(k) for k in keys.keys()])
-            for key in sorted(keys.keys()):
+            max_len = max([len(k) for k in keys])
+            for key in sorted(keys):
                 space = " " * (max_len + 1 - len(key))
                 space = " " * (max_len + 1 - len(key))
                 self.stdout.write("%s:%s%s" % (key, space, keys[key]))
                 self.stdout.write("%s:%s%s" % (key, space, keys[key]))
         else:
         else:

+ 2 - 1
misago/users/management/commands/prepareuserdatadownloads.py

@@ -29,6 +29,7 @@ class Command(BaseCommand):
 
 
         cache_versions = get_cache_versions()
         cache_versions = get_cache_versions()
         dynamic_settings = DynamicSettings(cache_versions)
         dynamic_settings = DynamicSettings(cache_versions)
+        expires_in = settings.MISAGO_USER_DATA_DOWNLOADS_EXPIRE_IN_HOURS
 
 
         downloads_prepared = 0
         downloads_prepared = 0
         queryset = DataDownload.objects.select_related("user")
         queryset = DataDownload.objects.select_related("user")
@@ -45,7 +46,7 @@ class Command(BaseCommand):
                     "misago/emails/data_download",
                     "misago/emails/data_download",
                     context={
                     context={
                         "data_download": data_download,
                         "data_download": data_download,
-                        "expires_in": settings.MISAGO_USER_DATA_DOWNLOADS_EXPIRE_IN_HOURS,
+                        "expires_in": expires_in,
                         "settings": dynamic_settings,
                         "settings": dynamic_settings,
                     },
                     },
                 )
                 )

+ 5 - 7
misago/users/models/ban.py

@@ -58,8 +58,8 @@ class BansManager(models.Manager):
                 return ban
                 return ban
             elif ban.check_type == self.model.IP and ip and ban.check_value(ip):
             elif ban.check_type == self.model.IP and ip and ban.check_value(ip):
                 return ban
                 return ban
-        else:
-            raise Ban.DoesNotExist("specified values are not banned")
+
+        raise Ban.DoesNotExist("specified values are not banned")
 
 
 
 
 class Ban(models.Model):
 class Ban(models.Model):
@@ -104,15 +104,13 @@ class Ban(models.Model):
     def is_expired(self):
     def is_expired(self):
         if self.expires_on:
         if self.expires_on:
             return self.expires_on < timezone.now()
             return self.expires_on < timezone.now()
-        else:
-            return False
+        return False
 
 
     def check_value(self, value):
     def check_value(self, value):
         if "*" in self.banned_value:
         if "*" in self.banned_value:
-            regex = re.escape(self.banned_value).replace("\*", "(.*?)")
+            regex = re.escape(self.banned_value).replace(r"\*", r"(.*?)")
             return re.search("^%s$" % regex, value, re.IGNORECASE) is not None
             return re.search("^%s$" % regex, value, re.IGNORECASE) is not None
-        else:
-            return self.banned_value.lower() == value.lower()
+        return self.banned_value.lower() == value.lower()
 
 
     def lift(self):
     def lift(self):
         self.expires_on = timezone.now()
         self.expires_on = timezone.now()

+ 1 - 1
misago/users/models/online.py

@@ -1,5 +1,5 @@
 from django.conf import settings
 from django.conf import settings
-from django.db import models
+from django.db import IntegrityError, models
 from django.utils import timezone
 from django.utils import timezone
 
 
 
 

+ 2 - 4
misago/users/models/user.py

@@ -4,10 +4,9 @@ from django.contrib.auth.models import AbstractBaseUser
 from django.contrib.auth.models import AnonymousUser as DjangoAnonymousUser
 from django.contrib.auth.models import AnonymousUser as DjangoAnonymousUser
 from django.contrib.auth.models import PermissionsMixin
 from django.contrib.auth.models import PermissionsMixin
 from django.contrib.auth.models import UserManager as BaseUserManager
 from django.contrib.auth.models import UserManager as BaseUserManager
-from django.contrib.auth.password_validation import validate_password
 from django.contrib.postgres.fields import ArrayField, HStoreField, JSONField
 from django.contrib.postgres.fields import ArrayField, HStoreField, JSONField
 from django.core.mail import send_mail
 from django.core.mail import send_mail
-from django.db import IntegrityError, models, transaction
+from django.db import models
 from django.urls import reverse
 from django.urls import reverse
 from django.utils import timezone
 from django.utils import timezone
 from django.utils.translation import gettext_lazy as _
 from django.utils.translation import gettext_lazy as _
@@ -17,7 +16,6 @@ from ...acl.models import Role
 from ...conf import settings
 from ...conf import settings
 from ...core.pgutils import PgPartialIndex
 from ...core.pgutils import PgPartialIndex
 from ...core.utils import slugify
 from ...core.utils import slugify
-from ..audittrail import create_user_audit_trail
 from ..signatures import is_user_signature_valid
 from ..signatures import is_user_signature_valid
 from ..utils import hash_email
 from ..utils import hash_email
 from .online import Online
 from .online import Online
@@ -39,7 +37,7 @@ class UserManager(BaseUserManager):
         user.set_email(email)
         user.set_email(email)
         user.set_password(password)
         user.set_password(password)
 
 
-        if not "rank" in extra_fields:
+        if "rank" not in extra_fields:
             user.rank = Rank.objects.get_default()
             user.rank = Rank.objects.get_default()
 
 
         now = timezone.now()
         now = timezone.now()

+ 1 - 3
misago/users/namechanges.py

@@ -5,8 +5,6 @@ from datetime import timedelta
 
 
 from django.utils import timezone
 from django.utils import timezone
 
 
-from .models import UsernameChange
-
 
 
 def get_username_options(settings, user, user_acl):
 def get_username_options(settings, user, user_acl):
     changes_left = get_left_namechanges(user, user_acl)
     changes_left = get_left_namechanges(user, user_acl)
@@ -28,7 +26,7 @@ def get_left_namechanges(user, user_acl):
     valid_changes = get_valid_changes_queryset(user, user_acl)
     valid_changes = get_valid_changes_queryset(user, user_acl)
     used_changes = valid_changes.count()
     used_changes = valid_changes.count()
     if name_changes_allowed <= used_changes:
     if name_changes_allowed <= used_changes:
-        left = 0
+        return 0
     return name_changes_allowed - used_changes
     return name_changes_allowed - used_changes
 
 
 
 

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

@@ -37,8 +37,6 @@ class PermissionsForm(forms.Form):
 def change_permissions_form(role):
 def change_permissions_form(role):
     if isinstance(role, Role) and role.special_role != "anonymous":
     if isinstance(role, Role) and role.special_role != "anonymous":
         return PermissionsForm
         return PermissionsForm
-    else:
-        return None
 
 
 
 
 def build_acl(acl, roles, key_name):
 def build_acl(acl, roles, key_name):

+ 4 - 6
misago/users/permissions/decorators.py

@@ -6,19 +6,17 @@ __all__ = ["authenticated_only", "anonymous_only"]
 
 
 def authenticated_only(f):
 def authenticated_only(f):
     def perm_decorator(user_acl, target):
     def perm_decorator(user_acl, target):
-        if user_acl["is_authenticated"]:
-            return f(user_acl, target)
-        else:
+        if user_acl["is_anonymous"]:
             raise PermissionDenied(_("You have to sig in to perform this action."))
             raise PermissionDenied(_("You have to sig in to perform this action."))
+        return f(user_acl, target)
 
 
     return perm_decorator
     return perm_decorator
 
 
 
 
 def anonymous_only(f):
 def anonymous_only(f):
     def perm_decorator(user_acl, target):
     def perm_decorator(user_acl, target):
-        if user_acl["is_anonymous"]:
-            return f(user_acl, target)
-        else:
+        if user_acl["is_authenticated"]:
             raise PermissionDenied(_("Only guests can perform this action."))
             raise PermissionDenied(_("Only guests can perform this action."))
+        return f(user_acl, target)
 
 
     return perm_decorator
     return perm_decorator

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

@@ -40,8 +40,6 @@ class PermissionsForm(forms.Form):
 def change_permissions_form(role):
 def change_permissions_form(role):
     if isinstance(role, Role) and role.special_role != "anonymous":
     if isinstance(role, Role) and role.special_role != "anonymous":
         return PermissionsForm
         return PermissionsForm
-    else:
-        return None
 
 
 
 
 def build_acl(acl, roles, key_name):
 def build_acl(acl, roles, key_name):

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

@@ -55,8 +55,6 @@ class PermissionsForm(forms.Form):
 def change_permissions_form(role):
 def change_permissions_form(role):
     if isinstance(role, Role) and role.special_role != "anonymous":
     if isinstance(role, Role) and role.special_role != "anonymous":
         return PermissionsForm
         return PermissionsForm
-    else:
-        return None
 
 
 
 
 def build_acl(acl, roles, key_name):
 def build_acl(acl, roles, key_name):

+ 1 - 4
misago/users/permissions/profiles.py

@@ -58,10 +58,7 @@ def change_permissions_form(role):
     if isinstance(role, Role):
     if isinstance(role, Role):
         if role.special_role == "anonymous":
         if role.special_role == "anonymous":
             return LimitedPermissionsForm
             return LimitedPermissionsForm
-        else:
-            return PermissionsForm
-    else:
-        return None
+        return PermissionsForm
 
 
 
 
 def build_acl(acl, roles, key_name):
 def build_acl(acl, roles, key_name):

+ 5 - 6
misago/users/profilefields/basefields.py

@@ -56,8 +56,8 @@ class ProfileField:
         return data
         return data
 
 
     def get_display_data(self, request, user):
     def get_display_data(self, request, user):
-        value = user.profile_fields.get(self.fieldname, "")
-        if not self.readonly and not len(value):
+        value = user.profile_fields.get(self.fieldname, "").strip()
+        if not self.readonly and not value:
             return None
             return None
 
 
         data = self.get_value_display_data(request, user, value)
         data = self.get_value_display_data(request, user, value)
@@ -101,22 +101,21 @@ class ChoiceProfileField(ProfileField):
 
 
     def get_input_json(self, request, user):
     def get_input_json(self, request, user):
         choices = []
         choices = []
-        for key, choice in self.get_choices():
+        for key, choice in self.get_choices():  # pylint: disable=not-an-iterable
             choices.append({"value": key, "label": choice})
             choices.append({"value": key, "label": choice})
 
 
         return {"type": "select", "choices": choices}
         return {"type": "select", "choices": choices}
 
 
     def get_value_display_data(self, request, user, value):
     def get_value_display_data(self, request, user, value):
-        for key, name in self.get_choices():
+        for key, name in self.get_choices():  # pylint: disable=not-an-iterable
             if key == value:
             if key == value:
                 return {"text": str(name)}
                 return {"text": str(name)}
-        return None
 
 
     def search_users(self, criteria):
     def search_users(self, criteria):
         """custom search implementation for choice fields"""
         """custom search implementation for choice fields"""
         q_obj = Q(**{"profile_fields__%s__contains" % self.fieldname: criteria})
         q_obj = Q(**{"profile_fields__%s__contains" % self.fieldname: criteria})
 
 
-        for key, choice in self.get_choices():
+        for key, choice in self.get_choices():  # pylint: disable=not-an-iterable
             if key and criteria.lower() in str(choice).lower():
             if key and criteria.lower() in str(choice).lower():
                 q_obj = q_obj | Q(**{"profile_fields__%s" % self.fieldname: key})
                 q_obj = q_obj | Q(**{"profile_fields__%s" % self.fieldname: key})
 
 

+ 5 - 5
misago/users/profilefields/default.py

@@ -48,8 +48,8 @@ class SkypeIdField(basefields.TextProfileField):
     fieldname = "skype"
     fieldname = "skype"
     label = _("Skype ID")
     label = _("Skype ID")
     help_text = _(
     help_text = _(
-        "Entering your Skype ID in this field may invite other users to contact you over "
-        "the Skype instead of via private threads."
+        "Entering your Skype ID in this field may invite other users to "
+        "contact you over the Skype instead of via private threads."
     )
     )
 
 
 
 
@@ -59,9 +59,9 @@ class TwitterHandleField(basefields.TextProfileField):
 
 
     def get_help_text(self, user):
     def get_help_text(self, user):
         return _(
         return _(
-            "If you own Twitter account, here you may enter your Twitter handle for other users "
-            'to find you. Starting your handle with "@" sign is optional. Either "@%(slug)s" or '
-            '"%(slug)s" are valid values.'
+            "If you own Twitter account, here you may enter your Twitter handle for "
+            'other users to find you. Starting your handle with "@" sign is optional. '
+            'Either "@%(slug)s" or "%(slug)s" are valid values.'
         ) % {"slug": user.slug}
         ) % {"slug": user.slug}
 
 
     def get_value_display_data(self, request, user, value):
     def get_value_display_data(self, request, user, value):

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

@@ -13,8 +13,8 @@ def serialize_profilefields_data(request, profilefields, user):
             display_data = field.get_display_data(request, user)
             display_data = field.get_display_data(request, user)
             if display_data:
             if display_data:
                 group_fields.append(display_data)
                 group_fields.append(display_data)
-        if can_edit and field.is_editable(request, user):
-            has_editable_fields = True
+            if can_edit and field.is_editable(request, user):
+                has_editable_fields = True
         if group_fields:
         if group_fields:
             data["groups"].append({"name": group["name"], "fields": group_fields})
             data["groups"].append({"name": group["name"], "fields": group_fields})
 
 

+ 0 - 2
misago/users/serializers/ban.py

@@ -10,8 +10,6 @@ __all__ = ["BanMessageSerializer", "BanDetailsSerializer"]
 def serialize_message(message):
 def serialize_message(message):
     if message:
     if message:
         return {"plain": message, "html": format_plaintext_for_html(message)}
         return {"plain": message, "html": format_plaintext_for_html(message)}
-    else:
-        return None
 
 
 
 
 class BanMessageSerializer(serializers.ModelSerializer):
 class BanMessageSerializer(serializers.ModelSerializer):

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

@@ -32,13 +32,11 @@ class ModerateSignatureSerializer(serializers.ModelSerializer):
     def validate_signature(self, value):
     def validate_signature(self, value):
         length_limit = settings.signature_length_max
         length_limit = settings.signature_length_max
         if len(value) > length_limit:
         if len(value) > length_limit:
-            raise serializers.ValidationError(
-                ngettext(
-                    "Signature can't be longer than %(limit)s character.",
-                    "Signature can't be longer than %(limit)s characters.",
-                    length_limit,
-                )
-                % {"limit": length_limit}
+            message = ngettext(
+                "Signature can't be longer than %(limit)s character.",
+                "Signature can't be longer than %(limit)s characters.",
+                length_limit,
             )
             )
+            raise serializers.ValidationError(message % {"limit": length_limit})
 
 
         return value
         return value

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

@@ -118,8 +118,8 @@ class DeleteOwnAccountSerializer(serializers.Serializer):
 
 
     def mark_account_for_deletion(self, request):
     def mark_account_for_deletion(self, request):
         """
         """
-        Deleting user account can be costful, so just mark account for deletion, deactivate it
-        and sign user out.
+        Deleting user account can be costful, so just mark account for deletion,
+        deactivate it and sign user out.
         """
         """
         profile = self.context["user"]
         profile = self.context["user"]
         allow_delete_own_account(request.user, profile)
         allow_delete_own_account(request.user, profile)

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

@@ -27,8 +27,7 @@ class RankSerializer(serializers.ModelSerializer):
     def get_description(self, obj):
     def get_description(self, obj):
         if obj.description:
         if obj.description:
             return format_plaintext_for_html(obj.description)
             return format_plaintext_for_html(obj.description)
-        else:
-            return ""
+        return ""
 
 
     def get_url(self, obj):
     def get_url(self, obj):
         return obj.get_absolute_url()
         return obj.get_absolute_url()

+ 2 - 8
misago/users/serializers/user.py

@@ -72,22 +72,18 @@ class UserSerializer(serializers.ModelSerializer, MutableFields):
         request = self.context["request"]
         request = self.context["request"]
         if obj == request.user or request.user_acl["can_see_users_emails"]:
         if obj == request.user or request.user_acl["can_see_users_emails"]:
             return obj.email
             return obj.email
-        else:
-            return None
 
 
     def get_is_followed(self, obj):
     def get_is_followed(self, obj):
         request = self.context["request"]
         request = self.context["request"]
         if obj.acl["can_follow"]:
         if obj.acl["can_follow"]:
             return request.user.is_following(obj)
             return request.user.is_following(obj)
-        else:
-            return False
+        return False
 
 
     def get_is_blocked(self, obj):
     def get_is_blocked(self, obj):
         request = self.context["request"]
         request = self.context["request"]
         if obj.acl["can_block"]:
         if obj.acl["can_block"]:
             return request.user.is_blocking(obj)
             return request.user.is_blocking(obj)
-        else:
-            return False
+        return False
 
 
     def get_meta(self, obj):
     def get_meta(self, obj):
         return {"score": obj.score}
         return {"score": obj.score}
@@ -98,8 +94,6 @@ class UserSerializer(serializers.ModelSerializer, MutableFields):
     def get_signature(self, obj):
     def get_signature(self, obj):
         if obj.has_valid_signature:
         if obj.has_valid_signature:
             return obj.signature_parsed
             return obj.signature_parsed
-        else:
-            return None
 
 
     def get_status(self, obj):
     def get_status(self, obj):
         try:
         try:

+ 1 - 2
misago/users/signatures.py

@@ -16,8 +16,7 @@ def is_user_signature_valid(user):
     if user.signature:
     if user.signature:
         valid_checksum = make_signature_checksum(user.signature_parsed, user)
         valid_checksum = make_signature_checksum(user.signature_parsed, user)
         return user.signature_checksum == valid_checksum
         return user.signature_checksum == valid_checksum
-    else:
-        return False
+    return False
 
 
 
 
 def make_signature_checksum(parsed_signature, user):
 def make_signature_checksum(parsed_signature, user):

+ 2 - 1
misago/users/social/backendsnames.py

@@ -1,6 +1,7 @@
 """
 """
 Python Social Auth doesn't provide information about proper names for OAuth sites,
 Python Social Auth doesn't provide information about proper names for OAuth sites,
-so we are using this file for overrides whenever name is different than `provider.name.title`
+so we are using this file to define those for displaying the UI.
+If provider is not defined here, it's name is created with `provider.name.title()`
 """
 """
 
 
 BACKENDS_NAMES = {
 BACKENDS_NAMES = {

+ 9 - 4
misago/users/social/pipeline.py

@@ -1,3 +1,4 @@
+# pylint: disable=keyword-arg-before-vararg
 import json
 import json
 
 
 from django.contrib.auth import get_user_model
 from django.contrib.auth import get_user_model
@@ -31,7 +32,9 @@ User = get_user_model()
 
 
 
 
 def validate_ip_not_banned(strategy, details, backend, user=None, *args, **kwargs):
 def validate_ip_not_banned(strategy, details, backend, user=None, *args, **kwargs):
-    """Pipeline step that interrupts pipeline if found user is non-staff and IP banned"""
+    """
+    Pipeline step that interrupts pipeline if found user is non-staff and IP banned
+    """
     if not user or user.is_staff:
     if not user or user.is_staff:
         return None
         return None
 
 
@@ -85,8 +88,8 @@ def associate_by_email(strategy, details, backend, user=None, *args, **kwargs):
         raise SocialAuthFailed(
         raise SocialAuthFailed(
             backend,
             backend,
             _(
             _(
-                "Your account has to be activated by site administrator before you will be able to "
-                "sign in with %(backend)s."
+                "Your account has to be activated by site administrator "
+                "before you will be able to sign in with %(backend)s."
             )
             )
             % {"backend": backend_name},
             % {"backend": backend_name},
         )
         )
@@ -169,7 +172,9 @@ def create_user(strategy, details, backend, user=None, *args, **kwargs):
 
 
 @partial
 @partial
 def create_user_with_form(strategy, details, backend, user=None, *args, **kwargs):
 def create_user_with_form(strategy, details, backend, user=None, *args, **kwargs):
-    """Alternatively to create_user lets user confirm account creation before authenticating"""
+    """
+    create_user lets user confirm account creation by entering final username or email
+    """
     if user:
     if user:
         return None
         return None
 
 

+ 2 - 3
misago/users/test.py

@@ -2,7 +2,6 @@ from django.contrib.auth import get_user_model
 from django.test import TestCase
 from django.test import TestCase
 
 
 from .models import AnonymousUser, Online
 from .models import AnonymousUser, Online
-from .setupnewuser import setup_new_user
 
 
 User = get_user_model()
 User = get_user_model()
 
 
@@ -59,7 +58,7 @@ class SuperUserTestCase(AuthenticatedUserTestCase):
 
 
 
 
 def create_test_user(username, email, password=None, **extra_fields):
 def create_test_user(username, email, password=None, **extra_fields):
-    """Faster counterpart of regular `create_user` followed by `setup_new_user`"""
+    """Faster alternative to regular `create_user` followed by `setup_new_user`"""
     if "avatars" not in extra_fields:
     if "avatars" not in extra_fields:
         extra_fields["avatars"] = user_placeholder_avatars
         extra_fields["avatars"] = user_placeholder_avatars
 
 
@@ -67,7 +66,7 @@ def create_test_user(username, email, password=None, **extra_fields):
 
 
 
 
 def create_test_superuser(username, email, password=None, **extra_fields):
 def create_test_superuser(username, email, password=None, **extra_fields):
-    """Faster counterpart of regular `create_superuser` followed by `setup_new_user`"""
+    """Faster alternative to regular `create_superuser` followed by `setup_new_user`"""
     if "avatars" not in extra_fields:
     if "avatars" not in extra_fields:
         extra_fields["avatars"] = user_placeholder_avatars
         extra_fields["avatars"] = user_placeholder_avatars
 
 

+ 1 - 1
misago/users/tests/test_audittrail.py

@@ -167,7 +167,7 @@ class RemoveOldAuditTrailsTest(UserTestCase):
     def test_recent_audit_trail_is_kept(self):
     def test_recent_audit_trail_is_kept(self):
         """remove_old_ips keeps recent audit trails"""
         """remove_old_ips keeps recent audit trails"""
         user = self.get_authenticated_user()
         user = self.get_authenticated_user()
-        audit_trail = create_user_audit_trail(user, USER_IP, self.obj)
+        create_user_audit_trail(user, USER_IP, self.obj)
 
 
         remove_old_ips.send(None)
         remove_old_ips.send(None)
 
 

+ 3 - 2
misago/users/tests/test_auth_api.py

@@ -1,4 +1,3 @@
-from django.contrib.auth import get_user_model
 from django.core import mail
 from django.core import mail
 from django.test import TestCase
 from django.test import TestCase
 
 
@@ -582,6 +581,8 @@ class ChangePasswordApiTests(TestCase):
         self.assertEqual(
         self.assertEqual(
             response.json(),
             response.json(),
             {
             {
-                "detail": "This password is too short. It must contain at least 7 characters."
+                "detail": (
+                    "This password is too short. It must contain at least 7 characters."
+                )
             },
             },
         )
         )

+ 0 - 1
misago/users/tests/test_auth_backend.py

@@ -1,4 +1,3 @@
-from django.contrib.auth import get_user_model
 from django.test import TestCase
 from django.test import TestCase
 
 
 from ..authbackends import MisagoBackend
 from ..authbackends import MisagoBackend

+ 0 - 1
misago/users/tests/test_bans.py

@@ -4,7 +4,6 @@ from django.test import TestCase
 from django.utils import timezone
 from django.utils import timezone
 
 
 from ...conftest import get_cache_versions
 from ...conftest import get_cache_versions
-from ...users import BANS_CACHE
 from ..bans import (
 from ..bans import (
     ban_ip,
     ban_ip,
     ban_user,
     ban_user,

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

@@ -140,7 +140,10 @@ class BioProfileFieldTests(AdminTestCase):
                         {
                         {
                             "fieldname": "bio",
                             "fieldname": "bio",
                             "name": "Bio",
                             "name": "Bio",
-                            "html": "<p>I am Bob!</p>\n\n<p>This is &lt;b&gt;my&lt;/b&gt; bio!</p>",
+                            "html": (
+                                "<p>I am Bob!</p>\n\n"
+                                "<p>This is &lt;b&gt;my&lt;/b&gt; bio!</p>"
+                            ),
                         }
                         }
                     ],
                     ],
                 },
                 },

+ 14 - 5
misago/users/tests/test_datadownloads.py

@@ -3,7 +3,7 @@ import os
 from django.core.files import File
 from django.core.files import File
 
 
 from ...categories.models import Category
 from ...categories.models import Category
-from ...threads.models import Attachment, AttachmentType
+from ...threads.models import AttachmentType
 from ...threads.test import post_poll, post_thread
 from ...threads.test import post_poll, post_thread
 from ..audittrail import create_user_audit_trail
 from ..audittrail import create_user_audit_trail
 from ..datadownloads import (
 from ..datadownloads import (
@@ -176,7 +176,9 @@ class PrepareUserDataDownload(AuthenticatedUserTestCase):
         self.assert_download_is_valid()
         self.assert_download_is_valid()
 
 
     def test_prepare_download_with_username_changed_by_deleted_user(self):
     def test_prepare_download_with_username_changed_by_deleted_user(self):
-        """function creates data download for user with username changed by deleted user"""
+        """
+        function creates data download for user with username changed by deleted user
+        """
         self.user.record_name_change(self.user, "aerith", "alice")
         self.user.record_name_change(self.user, "aerith", "alice")
         self.user.namechanges.update(changed_by=None)
         self.user.namechanges.update(changed_by=None)
 
 
@@ -271,7 +273,10 @@ class RequestUserDataDownloadTests(AuthenticatedUserTestCase):
         self.assertEqual(data_download.status, DataDownload.STATUS_PENDING)
         self.assertEqual(data_download.status, DataDownload.STATUS_PENDING)
 
 
     def test_util_creates_data_download_for_user_explicit_requester(self):
     def test_util_creates_data_download_for_user_explicit_requester(self):
-        """request_user_data_download created valid data download for user with other requester"""
+        """
+        request_user_data_download created valid data download
+        for user with other requester
+        """
         requester = self.get_superuser()
         requester = self.get_superuser()
         data_download = request_user_data_download(self.user, requester)
         data_download = request_user_data_download(self.user, requester)
 
 
@@ -283,7 +288,9 @@ class RequestUserDataDownloadTests(AuthenticatedUserTestCase):
 
 
 class UserHasRequestedDataDownloadTests(AuthenticatedUserTestCase):
 class UserHasRequestedDataDownloadTests(AuthenticatedUserTestCase):
     def test_util_returns_false_for_no_download(self):
     def test_util_returns_false_for_no_download(self):
-        """user_has_data_download_request returns false if user has no requests in progress"""
+        """
+        user_has_data_download_request returns false if user has no requests in progress
+        """
         self.assertFalse(user_has_data_download_request(self.user))
         self.assertFalse(user_has_data_download_request(self.user))
 
 
     def test_util_returns_false_for_ready_download(self):
     def test_util_returns_false_for_ready_download(self):
@@ -311,7 +318,9 @@ class UserHasRequestedDataDownloadTests(AuthenticatedUserTestCase):
         self.assertTrue(user_has_data_download_request(self.user))
         self.assertTrue(user_has_data_download_request(self.user))
 
 
     def test_util_returns_true_for_processing_download(self):
     def test_util_returns_true_for_processing_download(self):
-        """user_has_data_download_request returns true if user has processing download"""
+        """
+        user_has_data_download_request returns true if user has processing download
+        """
         data_download = request_user_data_download(self.user)
         data_download = request_user_data_download(self.user)
         data_download.status = DataDownload.STATUS_PROCESSING
         data_download.status = DataDownload.STATUS_PROCESSING
         data_download.save()
         data_download.save()

+ 3 - 1
misago/users/tests/test_forgottenpassword_views.py

@@ -20,7 +20,9 @@ class ForgottenPasswordViewsTests(UserTestCase):
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
     def test_authenticated_request_unusable_password_view_returns_200(self):
     def test_authenticated_request_unusable_password_view_returns_200(self):
-        """request new password view returns 200 for authenticated with unusable password"""
+        """
+        request new password view returns 200 for authenticated with unusable password
+        """
         user = self.get_authenticated_user()
         user = self.get_authenticated_user()
         user.set_password(None)
         user.set_password(None)
         user.save()
         user.save()

+ 0 - 1
misago/users/tests/test_mention_api.py

@@ -1,7 +1,6 @@
 from django.test import TestCase
 from django.test import TestCase
 from django.urls import reverse
 from django.urls import reverse
 
 
-from ...conf import settings
 from ..test import create_test_user
 from ..test import create_test_user
 
 
 
 

+ 1 - 5
misago/users/tests/test_namechanges.py

@@ -1,10 +1,6 @@
 from datetime import timedelta
 from datetime import timedelta
 
 
-from ..namechanges import (
-    get_left_namechanges,
-    get_next_available_namechange,
-    get_username_options,
-)
+from ..namechanges import get_left_namechanges, get_next_available_namechange
 
 
 
 
 def test_user_without_permission_to_change_name_has_no_changes_left(user):
 def test_user_without_permission_to_change_name_has_no_changes_left(user):

+ 1 - 0
misago/users/tests/test_profilefields.py

@@ -89,6 +89,7 @@ class ProfileFieldsLoadTests(TestCase):
                 {
                 {
                     "name": "Other test",
                     "name": "Other test",
                     "fields": [
                     "fields": [
+                        # pylint: disable=line-too-long
                         "misago.users.tests.testfiles.profilefields.RepeatedFieldnameField"
                         "misago.users.tests.testfiles.profilefields.RepeatedFieldnameField"
                     ],
                     ],
                 },
                 },

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

@@ -2,7 +2,6 @@ from unittest.mock import Mock
 
 
 import pytest
 import pytest
 
 
-from ...acl.useracl import get_user_acl
 from ...users import signatures
 from ...users import signatures
 
 
 
 

+ 4 - 5
misago/users/tests/test_social_pipeline.py

@@ -6,7 +6,6 @@ from django.test import RequestFactory
 from social_core.backends.github import GithubOAuth2
 from social_core.backends.github import GithubOAuth2
 from social_django.utils import load_strategy
 from social_django.utils import load_strategy
 
 
-from ...acl import ACL_CACHE
 from ...acl.useracl import get_user_acl
 from ...acl.useracl import get_user_acl
 from ...conf.dynamicsettings import DynamicSettings
 from ...conf.dynamicsettings import DynamicSettings
 from ...conf.test import override_dynamic_settings
 from ...conf.test import override_dynamic_settings
@@ -123,8 +122,8 @@ class AssociateByEmailTests(PipelineTestCase):
             self.assertEqual(
             self.assertEqual(
                 e.message,
                 e.message,
                 (
                 (
-                    "The e-mail address associated with your GitHub account is not available for "
-                    "use on this site."
+                    "The e-mail address associated with your GitHub account "
+                    "is not available for use on this site."
                 ),
                 ),
             )
             )
 
 
@@ -140,8 +139,8 @@ class AssociateByEmailTests(PipelineTestCase):
             self.assertEqual(
             self.assertEqual(
                 e.message,
                 e.message,
                 (
                 (
-                    "Your account has to be activated by site administrator before you will be "
-                    "able to sign in with GitHub."
+                    "Your account has to be activated by site administrator "
+                    "before you will be able to sign in with GitHub."
                 ),
                 ),
             )
             )
 
 

+ 3 - 2
misago/users/tests/test_twitter_profilefield.py

@@ -196,8 +196,9 @@ class TwitterProfileFieldTests(AdminTestCase):
                 "fieldname": "twitter",
                 "fieldname": "twitter",
                 "label": "Twitter handle",
                 "label": "Twitter handle",
                 "help_text": (
                 "help_text": (
-                    "If you own Twitter account, here you may enter your Twitter handle for other users to find you. "
-                    'Starting your handle with "@" sign is optional. Either "@testsuperuser" or "testsuperuser" are '
+                    "If you own Twitter account, here you may enter your Twitter "
+                    'handle for other users to find you. Starting your handle with "@" '
+                    'sign is optional. Either "@testsuperuser" or "testsuperuser" are '
                     "valid values."
                     "valid values."
                 ),
                 ),
                 "input": {"type": "text"},
                 "input": {"type": "text"},

+ 0 - 2
misago/users/tests/test_user_avatar_api.py

@@ -2,8 +2,6 @@ import json
 import os
 import os
 from pathlib import Path
 from pathlib import Path
 
 
-from django.contrib.auth import get_user_model
-
 from ...acl.test import patch_user_acl
 from ...acl.test import patch_user_acl
 from ...conf import settings
 from ...conf import settings
 from ...conf.test import override_dynamic_settings
 from ...conf.test import override_dynamic_settings

+ 0 - 2
misago/users/tests/test_user_changeemail_api.py

@@ -101,8 +101,6 @@ class UserChangeEmailTests(AuthenticatedUserTestCase):
     def test_change_email_user_password_whitespace(self):
     def test_change_email_user_password_whitespace(self):
         """api supports users with whitespace around their passwords"""
         """api supports users with whitespace around their passwords"""
         user_password = " old password "
         user_password = " old password "
-        new_password = " N3wP@55w0rd "
-
         new_email = "new@email.com"
         new_email = "new@email.com"
 
 
         self.user.set_password(user_password)
         self.user.set_password(user_password)

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

@@ -261,7 +261,10 @@ class UserCreateTests(UserTestCase):
             {
             {
                 "email": ["This email is not allowed."],
                 "email": ["This email is not allowed."],
                 "password": [
                 "password": [
-                    "This password is too short. It must contain at least 7 characters.",
+                    (
+                        "This password is too short. "
+                        "It must contain at least 7 characters."
+                    ),
                     "This password is entirely numeric.",
                     "This password is entirely numeric.",
                 ],
                 ],
             },
             },

+ 1 - 1
misago/users/tests/test_user_getters.py

@@ -25,7 +25,7 @@ def test_getting_user_by_username_supports_diacritics(db):
 
 
 
 
 def test_getting_user_by_username_is_not_doing_fuzzy_matching(db):
 def test_getting_user_by_username_is_not_doing_fuzzy_matching(db):
-    user = User.objects.create_user("User", "test@example.com")
+    User.objects.create_user("User", "test@example.com")
     with pytest.raises(User.DoesNotExist):
     with pytest.raises(User.DoesNotExist):
         User.objects.get_by_username("usar")
         User.objects.get_by_username("usar")
 
 

+ 3 - 1
misago/users/tests/test_user_requestdatadownload_api.py

@@ -47,7 +47,9 @@ class UserRequestDataDownload(AuthenticatedUserTestCase):
         self.assertEqual(
         self.assertEqual(
             response.json(),
             response.json(),
             {
             {
-                "detail": "You can't have more than one data download request at single time."
+                "detail": (
+                    "You can't have more than one data download request at single time."
+                )
             },
             },
         )
         )
 
 

+ 3 - 1
misago/users/tests/test_useradmin_views.py

@@ -898,7 +898,9 @@ class UserAdminViewsTests(AdminTestCase):
         self.assertTrue(updated_user.has_usable_password())
         self.assertTrue(updated_user.has_usable_password())
 
 
     def test_edit_keep_unusable_password(self):
     def test_edit_keep_unusable_password(self):
-        """admin edit form handles unusable passwords and lets admin leave them unchanged"""
+        """
+        admin edit form handles unusable passwords and lets admin leave them unchanged
+        """
         test_user = create_test_user("User", "user@example.com")
         test_user = create_test_user("User", "user@example.com")
         self.assertFalse(test_user.has_usable_password())
         self.assertFalse(test_user.has_usable_password())
 
 

+ 16 - 6
misago/users/tests/test_users_api.py

@@ -4,7 +4,6 @@ from datetime import timedelta
 from django.contrib.auth import get_user_model
 from django.contrib.auth import get_user_model
 from django.test import override_settings
 from django.test import override_settings
 from django.urls import reverse
 from django.urls import reverse
-from django.utils.encoding import smart_str
 
 
 from ...acl.test import patch_user_acl
 from ...acl.test import patch_user_acl
 from ...categories.models import Category
 from ...categories.models import Category
@@ -455,7 +454,10 @@ class UserBanTests(AuthenticatedUserTestCase):
 
 
 
 
 class UserDeleteOwnAccountTests(AuthenticatedUserTestCase):
 class UserDeleteOwnAccountTests(AuthenticatedUserTestCase):
-    """tests for user request own account delete RPC (POST to /api/users/1/delete-own-account/)"""
+    """
+    tests for user request own account delete RPC
+    (POST to /api/users/1/delete-own-account/)
+    """
 
 
     def setUp(self):
     def setUp(self):
         super().setUp()
         super().setUp()
@@ -463,7 +465,9 @@ class UserDeleteOwnAccountTests(AuthenticatedUserTestCase):
 
 
     @override_settings(MISAGO_ENABLE_DELETE_OWN_ACCOUNT=False)
     @override_settings(MISAGO_ENABLE_DELETE_OWN_ACCOUNT=False)
     def test_delete_own_account_feature_disabled(self):
     def test_delete_own_account_feature_disabled(self):
-        """raises 403 error when attempting to delete own account but feature is disabled"""
+        """
+        raises 403 error when attempting to delete own account but feature is disabled
+        """
         response = self.client.post(self.api_link, {"password": self.USER_PASSWORD})
         response = self.client.post(self.api_link, {"password": self.USER_PASSWORD})
 
 
         self.assertEqual(response.status_code, 403)
         self.assertEqual(response.status_code, 403)
@@ -483,7 +487,9 @@ class UserDeleteOwnAccountTests(AuthenticatedUserTestCase):
         self.assertEqual(
         self.assertEqual(
             response.json(),
             response.json(),
             {
             {
-                "detail": "You can't delete your account because you are an administrator."
+                "detail": (
+                    "You can't delete your account because you are an administrator."
+                )
             },
             },
         )
         )
 
 
@@ -501,7 +507,9 @@ class UserDeleteOwnAccountTests(AuthenticatedUserTestCase):
         self.assertEqual(
         self.assertEqual(
             response.json(),
             response.json(),
             {
             {
-                "detail": "You can't delete your account because you are an administrator."
+                "detail": (
+                    "You can't delete your account because you are an administrator."
+                )
             },
             },
         )
         )
 
 
@@ -510,7 +518,9 @@ class UserDeleteOwnAccountTests(AuthenticatedUserTestCase):
         self.assertFalse(self.user.is_deleting_account)
         self.assertFalse(self.user.is_deleting_account)
 
 
     def test_delete_own_account_invalid_password(self):
     def test_delete_own_account_invalid_password(self):
-        """raises 400 error when attempting to delete own account with invalid password"""
+        """
+        raises 400 error when attempting to delete own account with invalid password
+        """
         response = self.client.post(self.api_link, {"password": "hello"})
         response = self.client.post(self.api_link, {"password": "hello"})
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
         self.assertEqual(
         self.assertEqual(

+ 1 - 1
misago/users/validators.py

@@ -145,7 +145,7 @@ REGISTRATION_VALIDATORS = list(map(import_string, validators_list))
 
 
 
 
 def raise_validation_error(*_):
 def raise_validation_error(*_):
-    raise ValidationError()
+    raise ValidationError("")  # Raised when message content can be discarded
 
 
 
 
 def validate_new_registration(request, cleaned_data, add_error=None, validators=None):
 def validate_new_registration(request, cleaned_data, add_error=None, validators=None):

+ 5 - 5
misago/users/views/admin/users.py

@@ -130,7 +130,9 @@ class UsersList(UserAdmin, generic.ListView):
 
 
             messages.success(request, _("Selected users accounts have been activated."))
             messages.success(request, _("Selected users accounts have been activated."))
 
 
-    def action_ban(self, request, users):
+    def action_ban(
+        self, request, users
+    ):  # pylint: disable=too-many-locals, too-many-nested-blocks, too-many-branches
         users = users.order_by("slug")
         users = users.order_by("slug")
         for user in users:
         for user in users:
             if user.is_superuser:
             if user.is_superuser:
@@ -252,8 +254,7 @@ class NewUser(UserAdmin, generic.ModelFormView):
     def initialize_form(self, form, request, target):
     def initialize_form(self, form, request, target):
         if request.method == "POST":
         if request.method == "POST":
             return form(request.POST, request.FILES, instance=target, request=request)
             return form(request.POST, request.FILES, instance=target, request=request)
-        else:
-            return form(instance=target, request=request)
+        return form(instance=target, request=request)
 
 
     def handle_form(self, form, request, target):
     def handle_form(self, form, request, target):
         new_user = User.objects.create_user(
         new_user = User.objects.create_user(
@@ -291,8 +292,7 @@ class EditUser(UserAdmin, generic.ModelFormView):
     def initialize_form(self, form, request, target):
     def initialize_form(self, form, request, target):
         if request.method == "POST":
         if request.method == "POST":
             return form(request.POST, request.FILES, instance=target, request=request)
             return form(request.POST, request.FILES, instance=target, request=request)
-        else:
-            return form(instance=target, request=request)
+        return form(instance=target, request=request)
 
 
     def handle_form(self, form, request, target):
     def handle_form(self, form, request, target):
         target.username = target.old_username
         target.username = target.old_username

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

@@ -36,7 +36,8 @@ def reset_password_form(request, pk, token):
     try:
     try:
         if request.user.is_authenticated and request.user.id != requesting_user.id:
         if request.user.is_authenticated and request.user.id != requesting_user.id:
             message = _(
             message = _(
-                "%(user)s, your link has expired. Please request new link and try again."
+                "%(user)s, your link has expired. "
+                "Please request new link and try again."
             )
             )
             raise ResetError(message % {"user": requesting_user.username})
             raise ResetError(message % {"user": requesting_user.username})