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=
     abstract-method,
     arguments-differ,
+    assignment-from-none,
+    attribute-defined-outside-init,
     bad-continuation,
+    cyclic-import,
+    duplicate-code,
     expression-not-assigned,
     fixme,
     inconsistent-return-statements,
     invalid-name,
     missing-docstring,
+    model-no-explicit-unicode,  # pylint-django
     no-member,
     no-self-use,
+    protected-access,
     redefined-outer-name,
     too-few-public-methods,
     too-many-ancestors,

+ 2 - 4
.travis.yml

@@ -20,8 +20,6 @@ jobs:
       python: 3.6
       install:
         - pip install -U pip setuptools
-        - python setup.py install
-        - pip install black pylint pylint-django
+        - pip install black
       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
 
 from ...conf import settings

+ 0 - 2
misago/admin/middleware.py

@@ -16,5 +16,3 @@ class AdminAuthMiddleware(MiddlewareMixin):
                     return login(request)
                 return redirect("%s:index" % request.admin_namespace)
             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
         except NoReverseMatch:
             pass
-    return None
 
 
 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
             if order_by == new_ordering:
                 return order_by
-        return None
 
     def get_ordering_methods(self, request):
         return {

+ 0 - 1
misago/cache/test.py

@@ -4,7 +4,6 @@ from .versions import get_cache_versions_from_db
 class assert_invalidates_cache:
     def __init__(self, cache):
         self.cache = cache
-        self.versions = None
 
     def __enter__(self):
         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
         if level > 0:
             return mark_safe(conditional_escape(self.level_indicator) * level)
-        else:
-            return ""
+        return ""
 
 
 class AdminCategoryChoiceField(AdminCategoryFieldMixin, TreeNodeChoiceField):
@@ -56,7 +55,8 @@ class CategoryFormBase(forms.ModelForm):
         label=_("CSS class"),
         required=False,
         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(
@@ -66,13 +66,6 @@ class CategoryFormBase(forms.ModelForm):
             "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(
         label=_("Threads"),
         required=False,
@@ -91,23 +84,24 @@ class CategoryFormBase(forms.ModelForm):
         label=_("Edits"),
         required=False,
         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(
         label=_("Thread age"),
         min_value=0,
         help_text=_(
-            "Prune thread if number of days since its creation is greater than specified. "
-            "Enter 0 to disable this pruning criteria."
+            "Prune thread if number of days since its creation is greater than "
+            "specified. Enter 0 to disable this pruning criteria."
         ),
     )
     prune_replied_after = forms.IntegerField(
         label=_("Last reply"),
         min_value=0,
         help_text=_(
-            "Prune thread if number of days since last reply is greater than specified. "
-            "Enter 0 to disable this pruning criteria."
+            "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"
 
-    def handle(self, *args, **options):
+    def handle(self, *args, **options):  # pylint: disable=too-many-branches
         now = timezone.now()
         synchronize_categories = []
 

+ 1 - 5
misago/categories/permissions.py

@@ -1,5 +1,4 @@
 from django import forms
-from django.contrib.auth import get_user_model
 from django.core.exceptions import PermissionDenied
 from django.http import Http404
 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.decorators import return_boolean
 from ..admin.forms import YesNoSwitch
-from ..users.models import AnonymousUser
 from .models import Category, CategoryRole, RoleCategoryACL
 
 
@@ -21,8 +19,6 @@ class PermissionsForm(forms.Form):
 def change_permissions_form(role):
     if isinstance(role, CategoryRole):
         return PermissionsForm
-    else:
-        return None
 
 
 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"]:
             # dont bother with child categories of invisible parents
             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
             return
 

+ 0 - 2
misago/categories/serializers.py

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

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

@@ -128,14 +128,14 @@ class CategoryRoleAdminViewsTests(AdminTestCase):
         root = Category.objects.root_category()
         for descendant in root.get_descendants():
             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()
         self.client.post(
             reverse("misago:admin:categories:nodes:new"),
@@ -149,9 +149,8 @@ class CategoryRoleAdminViewsTests(AdminTestCase):
         test_category = Category.objects.get(slug="category-a")
 
         self.assertEqual(Category.objects.count(), 3)
-        """
-        Create test roles
-        """
+
+        # Create test roles
         self.client.post(
             reverse("misago:admin:permissions:users:new"),
             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_full = CategoryRole.objects.get(name="Test Full")
-        """
-        Test view itself
-        """
+
+        # Test view itself
         # See if form page is rendered
         response = self.client.get(
             reverse(
@@ -246,14 +244,14 @@ class CategoryRoleAdminViewsTests(AdminTestCase):
             )
         )
         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()
         self.client.post(
             reverse("misago:admin:categories:nodes:new"),

+ 3 - 1
misago/categories/utils.py

@@ -3,7 +3,9 @@ from ..readtracker import categoriestracker
 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"]:
         return []
 

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

@@ -41,22 +41,24 @@ class RoleFormMixin:
                     valid_forms += 1
 
             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)
-            elif form.is_valid() and len(perms_forms) != valid_forms:
+
                 form.add_error(None, _("Form contains errors."))
 
         return self.render(
@@ -85,8 +87,6 @@ class DeleteCategoryRole(CategoryRoleAdmin, generic.ButtonView):
 
 
 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"
     template = "categoryroles.html"
 
@@ -132,8 +132,7 @@ class CategoryPermissions(CategoryAdmin, generic.ModelFormView):
             messages.success(request, message % {"name": target.name})
             if "stay" in request.POST:
                 return redirect(request.path)
-            else:
-                return redirect(self.root_link)
+            return redirect(self.root_link)
 
         return self.render(request, {"forms": forms, "target": target})
 
@@ -147,8 +146,6 @@ CategoriesList.add_item_action(
 
 
 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"
     template = "rolecategories.html"
 
@@ -200,8 +197,7 @@ class RoleCategoriesACL(RoleAdmin, generic.ModelFormView):
             messages.success(request, message % {"name": target.name})
             if "stay" in request.POST:
                 return redirect(request.path)
-            else:
-                return redirect(self.root_link)
+            return redirect(self.root_link)
 
         return self.render(request, {"forms": forms, "target": target})
 

+ 2 - 4
misago/core/apipatch.py

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

+ 1 - 2
misago/core/context_processors.py

@@ -44,5 +44,4 @@ def momentjs_locale(request):
 def frontend_context(request):
     if request.include_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):
         if not request.is_ajax():
             return not_allowed(request)
-        else:
-            return f(request, *args, **kwargs)
+        return f(request, *args, **kwargs)
 
     return decorator
 
@@ -17,8 +16,7 @@ def require_POST(f):
     def decorator(request, *args, **kwargs):
         if not request.method == "POST":
             return not_allowed(request)
-        else:
-            return f(request, *args, **kwargs)
+        return f(request, *args, **kwargs)
 
     return decorator
 

+ 11 - 13
misago/core/errorpages.py

@@ -40,15 +40,13 @@ def banned(request, exception):
 def permission_denied(request, exception):
     if request.is_ajax():
         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):
     if request.is_ajax():
         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):
@@ -61,6 +59,7 @@ def social_auth_failed(request, exception):
         backend_name = exception.backend_name
     except AttributeError:
         pass
+
     try:
         exception_backend = exception.backend
         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):
         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 = _(
             "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=""):
     if request.is_ajax():
         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):
@@ -123,8 +123,7 @@ def shared_403_exception_handler(f):
     def page_decorator(request, *args, **kwargs):
         if is_request_to_misago(request):
             return permission_denied(request, *args, **kwargs)
-        else:
-            return f(request, *args, **kwargs)
+        return f(request, *args, **kwargs)
 
     return page_decorator
 
@@ -133,7 +132,6 @@ def shared_404_exception_handler(f):
     def page_decorator(request, *args, **kwargs):
         if is_request_to_misago(request):
             return page_not_found(request, *args, **kwargs)
-        else:
-            return f(request, *args, **kwargs)
+        return f(request, *args, **kwargs)
 
     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:
         if isinstance(exception, exception_type):
             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):
@@ -103,5 +102,3 @@ def handle_api_exception(exception, context):
             except IndexError:
                 pass
         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 social_core.exceptions import AuthException
 
@@ -35,10 +36,6 @@ class SocialAuthBanned(AuthException):
 class ExplicitFirstPage(Exception):
     """The url that was used to reach view contained explicit first page"""
 
-    pass
-
 
 class OutdatedSlug(Exception):
     """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:
             return exceptionhandler.handle_misago_exception(request, exception)
-        else:
-            return None
 
 
 class FrontendContextMiddleware(MiddlewareMixin):

+ 0 - 2
misago/core/momentjs.py

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

+ 12 - 11
misago/core/page.py

@@ -22,7 +22,10 @@ class Page:
         while self._unsorted_list:
             iterations += 1
             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)
 
             for index, section in enumerate(self._unsorted_list):
@@ -50,9 +53,9 @@ class Page:
                     new_sorted_list.append(inserted_section)
                     self._sorted_list = new_sorted_list
                     return True
-            else:
-                return False
-        elif before:
+            return False
+
+        if before:
             new_sorted_list = []
             for section in self._sorted_list:
                 if section["link"] == before:
@@ -60,13 +63,11 @@ class Page:
                     new_sorted_list.append(section)
                     self._sorted_list = new_sorted_list
                     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(
         self,

+ 13 - 12
misago/core/pgutils.py

@@ -1,4 +1,3 @@
-from django.core.paginator import Paginator
 from django.db.models import Index
 
 
@@ -6,11 +5,13 @@ class PgPartialIndex(Index):
     suffix = "part"
     max_name_length = 31
 
-    def __init__(self, fields=[], name=None, where=None):
+    def __init__(self, fields=None, name=None, where=None):
         if not where:
             raise ValueError("partial index requires WHERE clause")
         self.where = where
 
+        fields = fields or []
+
         super().__init__(fields, name)
 
     def set_name_with_model(self, model):
@@ -37,18 +38,18 @@ class PgPartialIndex(Index):
         self.check_name()
 
     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__()
 
+        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):
         path, args, kwargs = super().deconstruct()
         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:
         raise ExplicitFirstPage()
-    elif not page:
+    if not page:
         page = 1
 
     paginator = paginator or Paginator
@@ -85,5 +85,4 @@ def validate_slug(model, slug):
 def get_int_or_404(value):
     if str(value).isdigit():
         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"
 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)
 
     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"))
         self.assertContains(response, "page-error-social", status_code=403)
         self.assertContains(
@@ -74,7 +74,7 @@ class ErrorPageViewsTests(TestCase):
         )
 
     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"))
         self.assertContains(response, "page-error-social", 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"))
                     if response.status_code != 200:
                         failed_languages.append(language)
-            except:
+            except Exception:  # pylint: disable=broad-except
                 failed_languages.append(language)
 
         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>"),
     (
         "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"""
     value = force_text(value, strings_only=True).rstrip("Z")
 
-    for format in ISO8601_FORMATS:
+    for format_str in ISO8601_FORMATS:
         try:
-            parsed_value = datetime.strptime(value, format)
+            parsed_value = datetime.strptime(value, format_str)
             break
         except ValueError:
             try:
-                parsed_value = datetime.strptime(value[:-6], format)
+                parsed_value = datetime.strptime(value[:-6], format_str)
                 break
             except ValueError:
                 pass
@@ -67,8 +67,7 @@ def clean_return_path(request):
     """return path utility that returns return path from referer or POST"""
     if request.method == "POST" and "return_path" in request.POST:
         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):

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

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

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

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

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

@@ -33,7 +33,9 @@ class Command(BaseCommand):
             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"]
 
         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),
                 )
 
-                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"])
             except (ValidationError, IntegrityError):
                 pass

+ 4 - 4
misago/legal/forms.py

@@ -16,10 +16,10 @@ class AgreementForm(forms.ModelForm):
     is_active = forms.BooleanField(
         label=_("Set as active for its type"),
         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,
     )

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

@@ -22,7 +22,7 @@ class AgreementAdminViewsTests(AdminTestCase):
 
     def test_mass_delete(self):
         """adminview deletes multiple agreements"""
-        for i in range(10):
+        for _ in range(10):
             response = self.client.post(
                 reverse("misago:admin:users:agreements:new"),
                 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
                 Agreement.invalidate_cache()
 
-    return None
-
 
 def get_parsed_agreement_text(request, agreement):
     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:
         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):

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

@@ -60,8 +60,7 @@ class QuotePreprocessor(Preprocessor):
 
         if title:
             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):

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

@@ -18,7 +18,7 @@ class SimpleBBCodePattern(SimpleTagPattern):
     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.compiled_re = re.compile(
             "^(.*?)%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]:
             user = mentions_dict[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)
     element.replace_with(BeautifulSoup(replaced_string, "html.parser"))

+ 0 - 2
misago/markup/parser.py

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

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

@@ -81,7 +81,9 @@ class ParseMarkupApiTests(AuthenticatedUserTestCase):
         self.assertEqual(
             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
         )
 
-        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": []}
 
@@ -75,9 +76,10 @@ class MentionsTests(AuthenticatedUserTestCase):
             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": []}
 

+ 6 - 8
misago/readtracker/dates.py

@@ -14,12 +14,10 @@ def get_cutoff_date(user=None):
 
 
 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
+
+    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 ...threads import test
 from ...users.test import create_test_user
-from ..models import PostRead
 
 cache_versions = get_cache_versions()
 
@@ -165,24 +164,9 @@ class CategoriesTrackerTests(TestCase):
         self.assertFalse(self.category.is_read)
         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):
         """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)
         self.assertTrue(self.category.is_read)
@@ -190,7 +174,7 @@ class CategoriesTrackerTests(TestCase):
 
     def test_user_unapproved_own_thread_unread_post(self):
         """tracked unapproved but visible thread"""
-        thread = test.post_thread(
+        test.post_thread(
             self.category,
             poster=self.user,
             started_on=timezone.now(),
@@ -203,9 +187,7 @@ class CategoriesTrackerTests(TestCase):
 
     def test_user_hidden_thread_unread_post(self):
         """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)
         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.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"""
         user = MockAnonymousUser()
 

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

@@ -7,7 +7,6 @@ from ...categories.models import Category
 from ...conf import settings
 from ...threads import test
 from ...users.test import create_test_user
-from ..models import PostRead
 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 ...threads import test
 from ...users.test import create_test_user
-from ..models import PostRead
 
 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 .searchproviders import searchproviders

+ 0 - 2
misago/search/permissions.py

@@ -15,8 +15,6 @@ class PermissionsForm(forms.Form):
 def change_permissions_form(role):
     if isinstance(role, Role):
         return PermissionsForm
-    else:
-        return None
 
 
 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")
 
     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._initialized = True

+ 0 - 1
misago/search/views.py

@@ -1,7 +1,6 @@
 from django.core.exceptions import PermissionDenied
 from django.http import Http404
 from django.shortcuts import redirect, render
-from django.urls import reverse
 from django.utils.translation import gettext as _
 
 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:
         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(
             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 ....conf import settings
-from ....core.utils import clean_ids_list
 from ...moderation import posts as moderation
 from ...permissions import (
     allow_delete_best_answer,
     allow_delete_event,
     allow_delete_post,
-    exclude_invisible_posts,
 )
 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 ....core.apipatch import ApiPatch
 from ...moderation import posts as moderation
@@ -14,8 +11,7 @@ def patch_acl(request, event, value):
     if value:
         add_acl_to_obj(request.user_acl, event)
         return {"acl": event.acl}
-    else:
-        return {"acl": None}
+    return {"acl": None}
 
 
 event_patch_dispatcher.add("acl", patch_acl)
@@ -46,4 +42,5 @@ def event_patch_endpoint(request, event):
 
         event.category.synchronize()
         event.category.save()
+
     return response

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

@@ -27,8 +27,7 @@ def patch_acl(request, post, value):
     if value:
         add_acl_to_obj(request.user_acl, post)
         return {"acl": post.acl}
-    else:
-        return {"acl": None}
+    return {"acl": None}
 
 
 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):
-    def __init__(self, message):
+    def __init__(self, message):  # pylint: disable=super-init-not-called
         if not message:
             raise ValueError("You have to provide PostingInterrupt message.")
         self.message = message
@@ -118,7 +118,8 @@ class PostingEndpoint:
                 obj.pre_save(self._serializers.get(middleware))
         except PostingInterrupt as e:
             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:
@@ -134,7 +135,8 @@ class PostingEndpoint:
                 obj.post_save(self._serializers.get(middleware))
         except PostingInterrupt as e:
             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)
                 else:
                     message = _(
-                        'You don\'t have permission to remove "%(attachment)s" attachment.'
+                        "You don't have permission to remove "
+                        '"%(attachment)s" attachment.'
                     )
                     raise serializers.ValidationError(
                         message % {"attachment": attachment.filename}
@@ -123,6 +124,7 @@ class AttachmentsSerializer(serializers.Serializer):
 def validate_attachments_count(data):
     total_attachments = len(data)
     if total_attachments > settings.MISAGO_POST_ATTACHMENTS_LIMIT:
+        # pylint: disable=line-too-long
         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 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):
-    """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):
         if self.mode == PostingEndpoint.START:
@@ -66,7 +68,8 @@ class CategorySerializer(serializers.Serializer):
         except Category.DoesNotExist:
             raise serializers.ValidationError(
                 _(
-                    "Selected category doesn't exist or you don't have permission to browse it."
+                    "Selected category doesn't exist or "
+                    "you don't have permission to browse it."
                 )
             )
         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:
                 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"]
         if max_participants and len(clean_usernames) > max_participants:
+            # pylint: disable=line-too-long
             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 users to private thread (you've added %(added)s).",
@@ -85,7 +87,7 @@ class ParticipantsSerializer(serializers.Serializer):
             users.append(user)
 
         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)
 
             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):
     def get_serializer(self):
         if self.mode == PostingEndpoint.START:
-            return ThreadSerializer(data=self.request.data, context=self.kwargs)
+            serializer = ThreadSerializer
         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):
         if self.mode == PostingEndpoint.START:
@@ -93,10 +95,10 @@ class ReplySerializer(serializers.Serializer):
     def parse_post(self, post):
         if self.context["mode"] == PostingEndpoint.START:
             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):

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

@@ -25,9 +25,9 @@ def delete_bulk(request, viewmodel):
             if "details" in errors:
                 return Response(hydrate_error_details(errors["details"]), status=400)
             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"]:
         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 = []
     for category in reversed(categories):
         if category["id"] in available:
@@ -56,7 +57,8 @@ def thread_start_editor(request):
     if not cleaned_categories:
         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):
-        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):
         return threads.get_frontend_context()
@@ -43,8 +45,7 @@ class ForumThreadsList(ThreadsList):
     def get_category(self, request, pk=None):
         if pk:
             return ThreadsCategory(request, pk=pk)
-        else:
-            return ThreadsRootCategory(request)
+        return ThreadsRootCategory(request)
 
 
 class PrivateThreadsList(ThreadsList):

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

@@ -1,6 +1,4 @@
 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 ....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)
 
     serializer = MergeThreadSerializer(
@@ -97,11 +97,10 @@ def threads_merge_endpoint(request):
         if "threads" in serializer.errors:
             errors = {"detail": serializer.errors["threads"][0]}
             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]}
             return Response(errors, status=403)
-        else:
-            return Response(serializer.errors, status=400)
+        return Response(serializer.errors, status=400)
 
     threads = serializer.validated_data["threads"]
     invalid_threads = []

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

@@ -54,8 +54,7 @@ def patch_acl(request, thread, value):
     if value:
         add_acl_to_obj(request.user_acl, thread)
         return {"acl": thread.acl}
-    else:
-        return {"acl": None}
+    return {"acl": None}
 
 
 thread_patch_dispatcher.add("acl", patch_acl)
@@ -204,7 +203,8 @@ def patch_subscription(request, thread, value):
         )
 
         return {"subscription": False}
-    elif value == "email":
+
+    if value == "email":
         thread.subscription = request.user.subscription_set.create(
             thread=thread,
             category=thread.category,
@@ -213,8 +213,8 @@ def patch_subscription(request, thread, value):
         )
 
         return {"subscription": True}
-    else:
-        return {"subscription": None}
+
+    return {"subscription": None}
 
 
 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:
         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):
+    # pylint: disable=undefined-loop-variable
     try:
         user_id = int(value)
     except (ValueError, TypeError):
@@ -337,17 +339,18 @@ def patch_remove_participant(request, thread, value):
 
     if len(thread.participants_list) == 1:
         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)
 
 
 def patch_replace_owner(request, thread, value):
+    # pylint: disable=undefined-loop-variable
     try:
         user_id = int(value)
     except (ValueError, TypeError):
@@ -405,7 +408,9 @@ def thread_patch_endpoint(request, thread):
     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)
     if not serializer.is_valid():
         return Response(serializer.errors, status=400)

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

@@ -31,7 +31,9 @@ class ViewSet(viewsets.ViewSet):
     thread = None
 
     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):
         try:
@@ -120,8 +122,7 @@ class ViewSet(viewsets.ViewSet):
     def votes(self, request, thread_pk, pk=None):
         if request.method == "POST":
             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
     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(
         self, request, pk, path_aware=False, read_aware=False, subscription_aware=False
     ):
-        return self.thread(
+        return self.thread(  # pylint: disable=not-callable
             request,
             get_int_or_404(pk),
             path_aware=path_aware,
@@ -95,21 +95,21 @@ class ViewSet(viewsets.ViewSet):
             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
     def update(self, request, thread_pk, pk=None):
@@ -122,21 +122,21 @@ class ViewSet(viewsets.ViewSet):
             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):
         thread = self.get_thread(request, thread_pk)
@@ -149,8 +149,7 @@ class ViewSet(viewsets.ViewSet):
 
         if post.is_event:
             return event_patch_endpoint(request, post)
-        else:
-            return post_patch_endpoint(request, post)
+        return post_patch_endpoint(request, post)
 
     @transaction.atomic
     def delete(self, request, thread_pk, pk=None):
@@ -165,9 +164,7 @@ class ViewSet(viewsets.ViewSet):
     @detail_route(methods=["post"])
     def read(self, request, thread_pk, pk=None):
         thread = self.get_thread(request, thread_pk, subscription_aware=True).unwrap()
-
         post = self.get_post(request, thread, pk).unwrap()
-
         return post_read_endpoint(request, thread, post)
 
     @detail_route(methods=["get"], url_path="editor")
@@ -202,26 +199,26 @@ class ViewSet(viewsets.ViewSet):
         thread = self.get_thread(request, thread_pk).unwrap()
         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({})
 
+        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"])
     def edits(self, request, thread_pk, pk=None):
         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 ...core.shortcuts import get_int_or_404
 from ..models import Post, Thread
-from ..moderation import threads as moderation
 from ..permissions import allow_use_private_threads
 from ..viewmodels import (
     ForumThread,
@@ -30,7 +29,7 @@ class ViewSet(viewsets.ViewSet):
     def get_thread(
         self, request, pk, path_aware=False, read_aware=False, subscription_aware=False
     ):
-        return self.thread(
+        return self.thread(  # pylint: disable=not-callable
             request,
             get_int_or_404(pk),
             path_aware=path_aware,
@@ -82,19 +81,15 @@ class ThreadViewSet(ViewSet):
             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)
 
+        posting.save()
+
+        return Response(
+            {"id": thread.pk, "title": thread.title, "url": thread.get_absolute_url()}
+        )
+
     @detail_route(methods=["post"], url_path="merge")
     @transaction.atomic
     def thread_merge(self, request, pk=None):
@@ -139,15 +134,11 @@ class PrivateThreadViewSet(ViewSet):
             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)
+
+        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):
     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

+ 14 - 13
misago/threads/forms.py

@@ -61,10 +61,12 @@ class AttachmentTypeForm(forms.ModelForm):
         }
         help_texts = {
             "extensions": _(
-                "List of comma separated file extensions associated with this attachment type."
+                "List of comma separated file extensions associated with this "
+                "attachment type."
             ),
             "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": _(
                 "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."),
             "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": _(
-                "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 = {
@@ -97,9 +99,8 @@ class AttachmentTypeForm(forms.ModelForm):
 
     def clean_mimetypes(self):
         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):
         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:
     """
-    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)

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

@@ -15,6 +15,7 @@ from ...core.utils import slugify
 
 
 def upload_to(instance, filename):
+    # pylint: disable=undefined-loop-variable
     spread_path = md5(str(instance.secret[:16]).encode()).hexdigest()
     secret = Attachment.generate_new_secret()
 
@@ -94,8 +95,6 @@ class Attachment(models.Model):
                 "misago:attachment-thumbnail",
                 kwargs={"pk": self.pk, "secret": self.secret},
             )
-        else:
-            return None
 
     def set_file(self, upload):
         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):
         if self.length:
             return self.posted_on + timedelta(days=self.length)
-        return None
 
     @property
     def is_over(self):

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

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

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

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

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

@@ -15,37 +15,37 @@ __all__ = [
 
 
 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
 
+    post.is_unapproved = False
+    post.save(update_fields=["is_unapproved"])
+    return True
+
 
 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
 
+    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):
-    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
 
+    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):
     if post.is_first_post:
@@ -53,37 +53,37 @@ def unhide_post(user, post):
             _("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
 
+    post.is_hidden = False
+    post.save(update_fields=["is_hidden"])
+    return True
+
 
 def hide_post(user, post):
     if post.is_first_post:
         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
 
+    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
 def delete_post(user, post):

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

@@ -21,74 +21,74 @@ __all__ = [
 
 @transaction.atomic
 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
 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
 
+    thread.weight = 2
+    record_event(request, thread, "pinned_globally")
+    return True
+
 
 @transaction.atomic
 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
 
+    thread.weight = 1
+    record_event(request, thread, "pinned_locally")
+    return True
+
 
 @transaction.atomic
 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
 
+    thread.weight = 0
+    record_event(request, thread, "unpinned")
+    return True
+
 
 @transaction.atomic
 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
 
+    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
 def merge_thread(request, thread, other_thread):
@@ -101,88 +101,88 @@ def merge_thread(request, thread, other_thread):
 
 @transaction.atomic
 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
 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
 
+    thread.is_closed = False
+    record_event(request, thread, "opened")
+    return True
+
 
 @transaction.atomic
 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
 
+    thread.is_closed = True
+    record_event(request, thread, "closed")
+    return True
+
 
 @transaction.atomic
 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
 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
 
+    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
 def delete_thread(request, thread):

+ 1 - 1
misago/threads/paginator.py

@@ -2,7 +2,7 @@ from django.core.paginator import 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):
         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):
     if isinstance(role, Role):
-        if role.special_role != "anonymous":
-            return PermissionsForm
-        else:
+        if role.special_role == "anonymous":
             return AnonymousPermissionsForm
-    else:
-        return None
+        return PermissionsForm
 
 
 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.decorators import return_boolean
-from ...admin.forms import YesNoSwitch
 from ...categories.models import Category, CategoryRole
 from ...categories.permissions import get_categories_roles
 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"
         ),
         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,
         min_value=0,
@@ -55,8 +55,6 @@ class CategoryPermissionsForm(forms.Form):
 def change_permissions_form(role):
     if isinstance(role, CategoryRole):
         return CategoryPermissionsForm
-    else:
-        return None
 
 
 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"):
         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}
         )
@@ -151,8 +150,8 @@ def allow_mark_best_answer(user_acl, target):
     ):
         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:
             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}
             )
         if target.is_closed:
             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"):
         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}
         )
@@ -196,31 +195,26 @@ def allow_change_best_answer(user_acl, target):
         if user_acl["user_id"] != target.starter_id:
             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):
+            # 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(
-                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"]:
         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"):
         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}
         )
@@ -250,48 +244,43 @@ def allow_unmark_best_answer(user_acl, target):
         if user_acl["user_id"] != target.starter_id:
             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):
+            # 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(
-                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 target.category.is_closed:
             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}
             )
         if target.is_closed:
             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"]:
         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"):
         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}
         )
@@ -322,8 +312,8 @@ def allow_mark_as_best_answer(user_acl, target):
     ):
         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"]:
         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_minutes = int(diff.total_seconds() / 60)
         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):
     if isinstance(role, Role) and role.special_role != "anonymous":
         return RolePermissionsForm
-    else:
-        return None
 
 
 def build_acl(acl, roles, key_name):
@@ -253,7 +251,6 @@ def has_time_to_edit_poll(user_acl, target):
     if edit_time:
         diff = timezone.now() - target.posted_on
         diff_minutes = int(diff.total_seconds() / 60)
-
         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):
     if isinstance(role, Role) and role.special_role != "anonymous":
         return PermissionsForm
-    else:
-        return None
 
 
 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):
     if isinstance(role, Role) and role.special_role != "anonymous":
         return RolePermissionsForm
-    elif isinstance(role, CategoryRole):
+    if isinstance(role, CategoryRole):
         return CategoryPermissionsForm
-    else:
-        return None
 
 
 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_minutes = int(diff.total_seconds() / 60)
         return diff_minutes < edit_time
-    else:
-        return True
+
+    return True
 
 
 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_minutes = int(diff.total_seconds() / 60)
         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_accepted_visible = []
     show_accepted = []
@@ -1399,20 +1399,21 @@ def exclude_invisible_threads(user_acl, categories, queryset):
         else:
             conditions = condition
 
-    if conditions:
-        return queryset.filter(conditions)
-    else:
+    if not conditions:
         return Thread.objects.none()
 
+    return queryset.filter(conditions)
+
 
 def exclude_invisible_posts(user_acl, categories, queryset):
     if hasattr(categories, "__iter__"):
         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_approved = []
     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
         )
 
-    if conditions:
-        return queryset.filter(conditions)
-    else:
+    if not conditions:
         return Post.objects.none()
 
+    return queryset.filter(conditions)
+
 
 def exclude_invisible_posts_in_category(user_acl, category, queryset):
     add_acl_to_obj(user_acl, category)

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

@@ -54,5 +54,3 @@ class AttachmentSerializer(serializers.ModelSerializer):
             return reverse(
                 "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:
                 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:
                 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:

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

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

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

@@ -31,7 +31,8 @@ class NewVoteSerializer(serializers.Serializer):
             raise serializers.ValidationError(
                 _("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."))
 
         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"])
         ):
             return obj.content
-        else:
-            return None
 
     def get_attachments(self, obj):
         return obj.attachments_cache
@@ -156,8 +154,6 @@ class PostSerializer(serializers.ModelSerializer, MutableFields):
                 "misago:user",
                 kwargs={"pk": obj.last_editor_id, "slug": obj.last_editor_slug},
             )
-        else:
-            return None
 
     def get_hidden_by_url(self, obj):
         if obj.hidden_by_id:
@@ -165,5 +161,3 @@ class PostSerializer(serializers.ModelSerializer, MutableFields):
                 "misago:user",
                 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(
                 "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(
                 "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(
                 "misago:user", kwargs={"slug": obj.starter_slug, "pk": obj.starter_id}
             )
-        return None
 
     def get_last_poster_url(self, obj):
         if obj.last_poster_id:
@@ -146,7 +145,6 @@ class ThreadSerializer(serializers.ModelSerializer, MutableFields):
                 "misago:user",
                 kwargs={"slug": obj.last_poster_slug, "pk": obj.last_poster_id},
             )
-        return None
 
 
 class PrivateThreadSerializer(ThreadSerializer):
@@ -175,7 +173,6 @@ class ThreadsListSerializer(ThreadSerializer):
                 "real_name": obj.starter.get_real_name(),
                 "avatars": obj.starter.avatars,
             }
-        return None
 
     def get_last_poster(self, obj):
         if obj.last_poster_id:
@@ -185,7 +182,6 @@ class ThreadsListSerializer(ThreadSerializer):
                 "real_name": obj.last_poster.get_real_name(),
                 "avatars": obj.last_poster.avatars,
             }
-        return None
 
 
 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:
         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}
 
-    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):

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

@@ -158,7 +158,8 @@ class AttachmentsApiTestCase(AuthenticatedUserTestCase):
                 response.json(),
                 {
                     "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()
     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)
         middleware = AttachmentsMiddleware(
             request=Mock(data={}),
@@ -132,7 +134,9 @@ class AttachmentsMiddlewareTests(AuthenticatedUserTestCase):
 
     @patch_attachments_acl()
     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)
         middleware = AttachmentsMiddleware(
             request=Mock(data={}),
@@ -160,7 +164,9 @@ class AttachmentsMiddlewareTests(AuthenticatedUserTestCase):
 
     @patch_attachments_acl({"can_delete_other_users_attachments": False})
     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)
         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 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())
 
     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)]
         polls = [self.create_poll_thread()]
         threads = (
@@ -133,7 +135,10 @@ class MergeConflictTests(TestCase):
         )
 
     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()]
         polls = [self.create_poll_thread() for i in range(3)]
         threads = (

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

@@ -168,7 +168,10 @@ class PrivateThreadAddParticipantApiTests(PrivateThreadPatchApiTestCase):
         )
 
     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)
 
         self.patch(

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

@@ -112,7 +112,8 @@ class StartPrivateThreadTests(AuthenticatedUserTestCase):
             response.json(),
             {
                 "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(),
             {
                 "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)
 
     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.save()
 

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

@@ -30,7 +30,9 @@ class ThreadAddAclApiTests(ThreadPatchApiTestCase):
         self.assertTrue(response_json["acl"])
 
     def test_add_acl_false(self):
-        """if value is false, api won't add acl to the response, but will set empty key"""
+        """
+        if value is false, api won't add acl to the response, but will set empty key
+        """
         response = self.patch(
             self.api_link, [{"op": "add", "path": "acl", "value": False}]
         )
@@ -1048,7 +1050,8 @@ class ThreadMarkBestAnswerApiTests(ThreadPatchApiTestCase):
             {
                 "id": self.thread.id,
                 "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,
                 "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,
                 "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,
                 "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,
                 "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})
     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)
 
         response = self.patch(
@@ -1454,7 +1459,8 @@ class ThreadChangeBestAnswerApiTests(ThreadPatchApiTestCase):
             {
                 "id": self.thread.id,
                 "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,
                 "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,
                 "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):
-        """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)
 
         self.thread.best_answer_marked_on = timezone.now() - timedelta(minutes=6)
@@ -1545,8 +1553,8 @@ class ThreadChangeBestAnswerApiTests(ThreadPatchApiTestCase):
             {
                 "id": self.thread.id,
                 "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):
-        """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)
 
         self.thread.best_answer_marked_on = timezone.now() - timedelta(minutes=1)
@@ -1599,8 +1609,8 @@ class ThreadChangeBestAnswerApiTests(ThreadPatchApiTestCase):
             {
                 "id": self.thread.id,
                 "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,
                 "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,
                 "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,
                 "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):
-        """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.starter = self.user
         self.thread.save()
@@ -1812,8 +1825,8 @@ class ThreadUnmarkBestAnswerApiTests(ThreadPatchApiTestCase):
             {
                 "id": self.thread.id,
                 "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):
-        """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.save()
 
@@ -1853,8 +1868,8 @@ class ThreadUnmarkBestAnswerApiTests(ThreadPatchApiTestCase):
             {
                 "id": self.thread.id,
                 "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):
-        """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.save()
 
@@ -1888,7 +1905,9 @@ class ThreadUnmarkBestAnswerApiTests(ThreadPatchApiTestCase):
         }
     )
     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.save()
 
@@ -1902,8 +1921,8 @@ class ThreadUnmarkBestAnswerApiTests(ThreadPatchApiTestCase):
             {
                 "id": self.thread.id,
                 "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):
-        """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.save()
 
@@ -1951,8 +1972,8 @@ class ThreadUnmarkBestAnswerApiTests(ThreadPatchApiTestCase):
             {
                 "id": self.thread.id,
                 "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})
     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.save()
 
@@ -102,7 +104,9 @@ class ThreadPollCreateTests(ThreadPollApiTestCase):
 
     @patch_user_acl({"can_start_polls": 2})
     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.save()
 
@@ -273,7 +277,7 @@ class ThreadPollCreateTests(ThreadPollApiTestCase):
         self.assertTrue(response_json["is_public"])
 
         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(
             [c["label"] for c in response_json["choices"]], ["Red", "Green", "Blue"]
         )
@@ -296,6 +300,6 @@ class ThreadPollCreateTests(ThreadPollApiTestCase):
         self.assertTrue(poll.is_public)
 
         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)

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

@@ -307,7 +307,7 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
 
         # choices were updated
         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(
             [c["label"] for c in response_json["choices"]], ["Red", "Green", "Blue"]
         )
@@ -491,7 +491,7 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
 
         # choices were updated
         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(
             [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):
-        """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"])
         self.assertEqual(response.status_code, 403)
         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]
 
         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])
+
         for post in ids:
             with self.assertRaises(Post.DoesNotExist):
                 self.thread.post_set.get(pk=post)
@@ -273,8 +275,8 @@ class PostBulkDeleteApiTests(ThreadsApiTestCase):
         self.assertEqual(response.status_code, 200)
 
         self.thread = Thread.objects.get(pk=self.thread.pk)
-
         self.assertNotEqual(self.thread.last_post_id, self.posts[-1].pk)
+
         for post in self.posts:
             with self.assertRaises(Post.DoesNotExist):
                 self.thread.post_set.get(pk=post.pk)

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

@@ -1,8 +1,6 @@
 import json
-from datetime import timedelta
 
 from django.urls import reverse
-from django.utils import timezone
 
 from .. import test
 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 ...readtracker import poststracker
 from ...users.test import AuthenticatedUserTestCase
-from ..models import Post, Thread
+from ..models import Post
 from ..serializers.moderation import POSTS_LIMIT
 from ..test import patch_category_acl
 
@@ -414,7 +414,10 @@ class ThreadPostMergeApiTestCase(AuthenticatedUserTestCase):
         self.assertEqual(
             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})
     def test_empty_data(self):
         """api handles empty data"""
-        other_thread = test.post_thread(self.category)
+        test.post_thread(self.category)
 
         response = self.client.post(self.api_link)
         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 ...categories.models import Category
 from ...users.test import AuthenticatedUserTestCase
-from ..models import Post, Thread
+from ..models import Post
 from ..test import patch_category_acl
 
 
@@ -42,7 +42,9 @@ class PostAddAclApiTests(ThreadPostPatchApiTestCase):
         self.assertTrue(response_json["acl"])
 
     def test_add_acl_false(self):
-        """if value is false, api won't add acl to the response, but will set empty key"""
+        """
+        if value is false, api won't add acl to the response, but will set empty key
+        """
         response = self.patch(
             self.api_link, [{"op": "add", "path": "acl", "value": False}]
         )
@@ -879,7 +881,9 @@ class EventAddAclApiTests(ThreadEventPatchApiTestCase):
         self.assertTrue(response_json["acl"])
 
     def test_add_acl_false(self):
-        """if value is false, api won't add acl to the response, but will set empty key"""
+        """
+        if value is false, api won't add acl to the response, but will set empty key
+        """
         response = self.patch(
             self.api_link, [{"op": "add", "path": "acl", "value": False}]
         )

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

@@ -6,7 +6,7 @@ from .. import test
 from ...categories.models import Category
 from ...readtracker import poststracker
 from ...users.test import AuthenticatedUserTestCase
-from ..models import Post, Thread
+from ..models import Post
 from ..serializers.moderation import POSTS_LIMIT
 from ..test import patch_category_acl, patch_other_category_acl
 
@@ -407,7 +407,8 @@ class ThreadPostSplitApiTestCase(AuthenticatedUserTestCase):
             response_json,
             {
                 "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 django.urls import reverse
 from django.utils import timezone
 
 from .. import test

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

@@ -49,7 +49,10 @@ class ThreadPostEditorApiTests(EditorApiTestCase):
         self.assertEqual(
             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(
             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(
             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(
                 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})
     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.save()
         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(),
             {
                 "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.thread = test.post_thread(self.category)
 
-    def tearDown(self):
-        super().tearDown()
-
     def reload_thread(self):
         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 ...conf import settings
 from ...readtracker import poststracker
-from ...users.models import AnonymousUser
 from ...users.test import AuthenticatedUserTestCase
 
 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)
 
     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 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"""
         settings = Mock(post_length_min=1, post_length_max=2)
         with self.assertRaises(ValidationError):
-            post = "a" * settings.post_length_max
             validate_post_length(settings, "abc")
 
 
@@ -37,7 +36,7 @@ class ValidateThreadTitleTests(TestCase):
         """validate_thread_title is ok with valid titles"""
         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:
             validate_thread_title(settings, title)

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

@@ -38,10 +38,10 @@ class PrivateThread(ThreadType):
                 "misago:private-thread",
                 kwargs={"slug": thread.slug, "pk": thread.pk, "page": page},
             )
-        else:
-            return reverse(
-                "misago:private-thread", kwargs={"slug": thread.slug, "pk": thread.pk}
-            )
+
+        return reverse(
+            "misago:private-thread", kwargs={"slug": thread.slug, "pk": thread.pk}
+        )
 
     def get_thread_last_post_url(self, thread):
         return reverse(

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

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

+ 13 - 15
misago/threads/utils.py

@@ -1,6 +1,6 @@
 from urllib.parse import urlparse
 
-from django.urls import resolve
+from django.urls import Resolver404, resolve
 
 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
 
-    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:
-        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:
-        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) :])
-    except:
+    except Resolver404:
         return None
 
     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."))
 
     if value_len < settings.thread_title_length_min:
+        # pylint: disable=line-too-long
         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 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:
+        # pylint: disable=line-too-long
         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 characters (it has %(show_value)s).",
@@ -74,6 +76,7 @@ def validate_post_length(settings, value):
         raise ValidationError(_("You have to enter a message."))
 
     if value_len < settings.post_length_min:
+        # pylint: disable=line-too-long
         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 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:
+        # pylint: disable=line-too-long
         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 characters (it has %(show_value)s).",

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

@@ -12,7 +12,7 @@ __all__ = ["ThreadPosts"]
 
 
 class ViewModel:
-    def __init__(self, request, thread, page):
+    def __init__(self, request, thread, page):  # pylint: disable=too-many-locals
         try:
             thread_model = thread.unwrap()
         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.db.models import F, Q
+from django.db.models import Q
 from django.http import Http404
-from django.utils import timezone
 from django.utils.translation import gettext as _
 from django.utils.translation import gettext_lazy
 
@@ -157,14 +154,12 @@ class ForumThreads(ViewModel):
             return list(queryset.filter(weight=2)) + list(
                 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):
         if category.level:
             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):
@@ -192,25 +187,22 @@ class PrivateThreads(ViewModel):
 
 def get_threads_queryset(request, categories, list_type):
     queryset = exclude_invisible_threads(request.user_acl, categories, Thread.objects)
-
     if list_type == "all":
         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):
     if list_type == "my":
         return queryset.filter(starter=request.user)
-    elif list_type == "subscribed":
+    if list_type == "subscribed":
         subscribed_threads = request.user.subscription_set.values("thread_id")
         return queryset.filter(id__in=subscribed_threads)
-    elif list_type == "unapproved":
+    if list_type == "unapproved":
         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)
-    else:
-        return queryset
+    return 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):
         if target.attachment_set.exists():
             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}
 

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

@@ -34,13 +34,11 @@ def serve_file(request, pk, secret, thumbnail):
     if attachment.is_image:
         if thumbnail:
             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):

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

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

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

@@ -26,8 +26,7 @@ User = auth.get_user_model()
 def gateway(request):
     if request.method == "POST":
         return login(request)
-    else:
-        return session_user(request)
+    return session_user(request)
 
 
 @api_view(["POST"])
@@ -43,8 +42,7 @@ def login(request):
     if form.is_valid():
         auth.login(request, form.user_cache)
         return Response(AuthenticatedUserSerializer(form.user_cache).data)
-    else:
-        return Response(form.get_errors_dict(), status=status.HTTP_400_BAD_REQUEST)
+    return Response(form.get_errors_dict(), status=status.HTTP_400_BAD_REQUEST)
 
 
 @api_view()
@@ -72,9 +70,7 @@ def get_criteria(request):
 
     for validator in settings.AUTH_PASSWORD_VALIDATORS:
         validator_dict = {"name": validator["NAME"].split(".")[-1]}
-
         validator_dict.update(validator.get("OPTIONS", {}))
-
         criteria["password"].append(validator_dict)
 
     return Response(criteria)
@@ -90,30 +86,30 @@ def send_activation(request):
     will mail account activation link to requester
     """
     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)
 
+    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"])
 @permission_classes((UnbannedOnly,))
@@ -125,32 +121,32 @@ def send_password_form(request):
     will mail change password form link to requester
     """
     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)
 
+    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):
     pass

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

@@ -5,12 +5,12 @@ from rest_framework.response import Response
 
 @api_view()
 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()
+
+    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)
     if request.method == "POST":
         return avatar_post(request, avatar_options)
-    else:
-        return Response(avatar_options)
+    return Response(avatar_options)
 
 
 def get_avatar_options(request, user):
@@ -206,26 +205,26 @@ def moderate_avatar_endpoint(request, profile):
     if request.method == "POST":
         is_avatar_locked = profile.is_avatar_locked
         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)
-    else:
+
+        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,
             }
         )
+
+    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.response import Response
 
-from ....conf import settings
 from ....core.mail import mail_user
 from ...credentialchange import store_new_credential
 from ...serializers import ChangeEmailSerializer
@@ -13,25 +12,25 @@ def change_email_endpoint(request, pk=None):
         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}
     )
 
-    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)
+
+    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 ... import captcha
-from ....conf import settings
 from ....legal.models import Agreement
 from ...forms.register import RegisterForm
 from ...registration import (

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

@@ -36,8 +36,7 @@ def list_endpoint(request):
 
     if list_handler:
         return list_handler(request)
-    else:
-        return rank_users(request)
+    return rank_users(request)
 
 
 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}
 
         if not is_user_signature_valid(user):
+            # pylint: disable=unsupported-assignment-operation
             options["signature"]["html"] = None
 
     return Response(options)

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

@@ -10,9 +10,9 @@ from ...serializers import ChangeUsernameSerializer
 def username_endpoint(request):
     if request.method == "POST":
         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):
@@ -36,31 +36,31 @@ def change_username(request):
     serializer = ChangeUsernameSerializer(
         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(
             {"detail": serializer.errors["non_field_errors"][0]},
             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):
     if request.method == "POST":
@@ -68,24 +68,24 @@ def moderate_username_endpoint(request, 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(
                 {"detail": serializer.errors["non_field_errors"][0]},
                 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:
             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(
                 _("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():
             serializer.save()
             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"])
     def username(self, request, pk=None):
@@ -210,8 +209,7 @@ class UserViewSet(viewsets.GenericViewSet):
         ban = get_user_ban(profile, request.cache_versions)
         if ban:
             return Response(BanDetailsSerializer(ban).data)
-        else:
-            return Response({})
+        return Response({})
 
     @detail_route(methods=["get", "post"])
     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
                 has_permission = request.user_acl["can_see_users_name_history"]
                 return is_account_owner or has_permission
-            else:
-                return False
+            return False
 
         def can_see_ban_details(request, profile):
             if request.user.is_authenticated:
@@ -81,10 +80,8 @@ class MisagoUsersConfig(AppConfig):
                     from .bans import get_user_ban
 
                     return bool(get_user_ban(profile, request.cache_versions))
-                else:
-                    return False
-            else:
                 return False
+            return False
 
         user_profile.add_section(
             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())
             if temporary_file_path.exists():
                 temporary_file_path.unlink()
-        except Exception:
+        except Exception:  # pylint: disable=broad-except
             pass
         raise e
 
@@ -43,8 +43,7 @@ def validate_extension(uploaded_file):
     for extension in ALLOWED_EXTENSIONS:
         if lowercased_name.endswith(extension):
             return True
-    else:
-        raise ValidationError(_("Uploaded file type is not allowed."))
+    raise ValidationError(_("Uploaded file type is not allowed."))
 
 
 def validate_mime(uploaded_file):

+ 5 - 9
misago/users/bans.py

@@ -47,15 +47,13 @@ def get_user_ban(user, cache_versions):
     try:
         ban_cache = user.ban_cache
         if not ban_cache.is_valid(cache_versions):
-            _set_user_ban_cache(user)
+            _set_user_ban_cache(user, cache_versions)
     except BanCache.DoesNotExist:
         user.ban_cache = BanCache(user=user)
         user.ban_cache = _set_user_ban_cache(user, cache_versions)
 
     if user.ban_cache.ban:
         return user.ban_cache
-    else:
-        return None
 
 
 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["is_banned"]:
             return session_ban_cache
-        else:
-            return False
+        return False
 
     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})
         request.session[CACHE_SESSION_KEY] = ban_cache
         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):

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

@@ -39,7 +39,7 @@ def prepare_user_data_download(download, logger=None):
             download.save()
             # todo: send an e-mail with download link
             return True
-        except Exception as e:
+        except Exception as e:  # pylint: disable=broad-except
             if logger:
                 logger.exception(e)
             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):
         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)
         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
     Misago's `User`.
-    
+
     Removes `new` and `delete` actions (use Misago admin for that).
-    
+
     Registration call is placed in :mod:`misago.users.admin`.
     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
         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
 
@@ -504,9 +502,10 @@ class BanForm(forms.ModelForm):
     registration_only = YesNoSwitch(
         label=_("Restrict this ban to registrations"),
         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(
@@ -628,9 +627,10 @@ class RequestDataDownloadsForm(forms.Form):
         label=_("Usernames or emails"),
         help_text=_(
             "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,
     )

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

+ 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
         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")
         email = options.get("email")
         password = options.get("password")
@@ -121,7 +123,7 @@ class Command(BaseCommand):
                     try:
                         message = force_str("Enter displayed username: ")
                         raw_value = input(message).strip()
-                        validate_username(raw_value)
+                        validate_username(settings, raw_value)
                         username = raw_value
                     except ValidationError as e:
                         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.core.management.base import BaseCommand, CommandError
+from django.core.management.base import BaseCommand
 
-from ....conf import settings
 from ....core.pgutils import chunk_queryset
 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.core.management.base import BaseCommand, CommandError
+from django.core.management.base import BaseCommand
 
 from ....core.pgutils import chunk_queryset
 

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

@@ -13,13 +13,13 @@ class Command(BaseCommand):
         keys = {}
 
         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[key] += 1
 
         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))
                 self.stdout.write("%s:%s%s" % (key, space, keys[key]))
         else:

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

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

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

@@ -58,8 +58,8 @@ class BansManager(models.Manager):
                 return ban
             elif ban.check_type == self.model.IP and ip and ban.check_value(ip):
                 return ban
-        else:
-            raise Ban.DoesNotExist("specified values are not banned")
+
+        raise Ban.DoesNotExist("specified values are not banned")
 
 
 class Ban(models.Model):
@@ -104,15 +104,13 @@ class Ban(models.Model):
     def is_expired(self):
         if self.expires_on:
             return self.expires_on < timezone.now()
-        else:
-            return False
+        return False
 
     def check_value(self, 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
-        else:
-            return self.banned_value.lower() == value.lower()
+        return self.banned_value.lower() == value.lower()
 
     def lift(self):
         self.expires_on = timezone.now()

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

@@ -1,5 +1,5 @@
 from django.conf import settings
-from django.db import models
+from django.db import IntegrityError, models
 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 PermissionsMixin
 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.core.mail import send_mail
-from django.db import IntegrityError, models, transaction
+from django.db import models
 from django.urls import reverse
 from django.utils import timezone
 from django.utils.translation import gettext_lazy as _
@@ -17,7 +16,6 @@ from ...acl.models import Role
 from ...conf import settings
 from ...core.pgutils import PgPartialIndex
 from ...core.utils import slugify
-from ..audittrail import create_user_audit_trail
 from ..signatures import is_user_signature_valid
 from ..utils import hash_email
 from .online import Online
@@ -39,7 +37,7 @@ class UserManager(BaseUserManager):
         user.set_email(email)
         user.set_password(password)
 
-        if not "rank" in extra_fields:
+        if "rank" not in extra_fields:
             user.rank = Rank.objects.get_default()
 
         now = timezone.now()

+ 1 - 3
misago/users/namechanges.py

@@ -5,8 +5,6 @@ from datetime import timedelta
 
 from django.utils import timezone
 
-from .models import UsernameChange
-
 
 def get_username_options(settings, 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)
     used_changes = valid_changes.count()
     if name_changes_allowed <= used_changes:
-        left = 0
+        return 0
     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):
     if isinstance(role, Role) and role.special_role != "anonymous":
         return PermissionsForm
-    else:
-        return None
 
 
 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 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."))
+        return f(user_acl, target)
 
     return perm_decorator
 
 
 def anonymous_only(f):
     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."))
+        return f(user_acl, target)
 
     return perm_decorator

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

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

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

@@ -56,8 +56,8 @@ class ProfileField:
         return data
 
     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
 
         data = self.get_value_display_data(request, user, value)
@@ -101,22 +101,21 @@ class ChoiceProfileField(ProfileField):
 
     def get_input_json(self, request, user):
         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})
 
         return {"type": "select", "choices": choices}
 
     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:
                 return {"text": str(name)}
-        return None
 
     def search_users(self, criteria):
         """custom search implementation for choice fields"""
         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():
                 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"
     label = _("Skype ID")
     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):
         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}
 
     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)
             if 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:
             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):
     if message:
         return {"plain": message, "html": format_plaintext_for_html(message)}
-    else:
-        return None
 
 
 class BanMessageSerializer(serializers.ModelSerializer):

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

@@ -32,13 +32,11 @@ class ModerateSignatureSerializer(serializers.ModelSerializer):
     def validate_signature(self, value):
         length_limit = settings.signature_length_max
         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

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

@@ -118,8 +118,8 @@ class DeleteOwnAccountSerializer(serializers.Serializer):
 
     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"]
         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):
         if obj.description:
             return format_plaintext_for_html(obj.description)
-        else:
-            return ""
+        return ""
 
     def get_url(self, obj):
         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"]
         if obj == request.user or request.user_acl["can_see_users_emails"]:
             return obj.email
-        else:
-            return None
 
     def get_is_followed(self, obj):
         request = self.context["request"]
         if obj.acl["can_follow"]:
             return request.user.is_following(obj)
-        else:
-            return False
+        return False
 
     def get_is_blocked(self, obj):
         request = self.context["request"]
         if obj.acl["can_block"]:
             return request.user.is_blocking(obj)
-        else:
-            return False
+        return False
 
     def get_meta(self, obj):
         return {"score": obj.score}
@@ -98,8 +94,6 @@ class UserSerializer(serializers.ModelSerializer, MutableFields):
     def get_signature(self, obj):
         if obj.has_valid_signature:
             return obj.signature_parsed
-        else:
-            return None
 
     def get_status(self, obj):
         try:

+ 1 - 2
misago/users/signatures.py

@@ -16,8 +16,7 @@ def is_user_signature_valid(user):
     if user.signature:
         valid_checksum = make_signature_checksum(user.signature_parsed, user)
         return user.signature_checksum == valid_checksum
-    else:
-        return False
+    return False
 
 
 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,
-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 = {

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

@@ -1,3 +1,4 @@
+# pylint: disable=keyword-arg-before-vararg
 import json
 
 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):
-    """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:
         return None
 
@@ -85,8 +88,8 @@ def associate_by_email(strategy, details, backend, user=None, *args, **kwargs):
         raise SocialAuthFailed(
             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},
         )
@@ -169,7 +172,9 @@ def create_user(strategy, details, backend, user=None, *args, **kwargs):
 
 @partial
 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:
         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 .models import AnonymousUser, Online
-from .setupnewuser import setup_new_user
 
 User = get_user_model()
 
@@ -59,7 +58,7 @@ class SuperUserTestCase(AuthenticatedUserTestCase):
 
 
 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:
         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):
-    """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:
         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):
         """remove_old_ips keeps recent audit trails"""
         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)
 

+ 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.test import TestCase
 
@@ -582,6 +581,8 @@ class ChangePasswordApiTests(TestCase):
         self.assertEqual(
             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 ..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 ...conftest import get_cache_versions
-from ...users import BANS_CACHE
 from ..bans import (
     ban_ip,
     ban_user,

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

@@ -140,7 +140,10 @@ class BioProfileFieldTests(AdminTestCase):
                         {
                             "fieldname": "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 ...categories.models import Category
-from ...threads.models import Attachment, AttachmentType
+from ...threads.models import AttachmentType
 from ...threads.test import post_poll, post_thread
 from ..audittrail import create_user_audit_trail
 from ..datadownloads import (
@@ -176,7 +176,9 @@ class PrepareUserDataDownload(AuthenticatedUserTestCase):
         self.assert_download_is_valid()
 
     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.namechanges.update(changed_by=None)
 
@@ -271,7 +273,10 @@ class RequestUserDataDownloadTests(AuthenticatedUserTestCase):
         self.assertEqual(data_download.status, DataDownload.STATUS_PENDING)
 
     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()
         data_download = request_user_data_download(self.user, requester)
 
@@ -283,7 +288,9 @@ class RequestUserDataDownloadTests(AuthenticatedUserTestCase):
 
 class UserHasRequestedDataDownloadTests(AuthenticatedUserTestCase):
     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))
 
     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))
 
     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.status = DataDownload.STATUS_PROCESSING
         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)
 
     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.set_password(None)
         user.save()

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

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

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

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

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

@@ -89,6 +89,7 @@ class ProfileFieldsLoadTests(TestCase):
                 {
                     "name": "Other test",
                     "fields": [
+                        # pylint: disable=line-too-long
                         "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
 
-from ...acl.useracl import get_user_acl
 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_django.utils import load_strategy
 
-from ...acl import ACL_CACHE
 from ...acl.useracl import get_user_acl
 from ...conf.dynamicsettings import DynamicSettings
 from ...conf.test import override_dynamic_settings
@@ -123,8 +122,8 @@ class AssociateByEmailTests(PipelineTestCase):
             self.assertEqual(
                 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(
                 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",
                 "label": "Twitter handle",
                 "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."
                 ),
                 "input": {"type": "text"},

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

@@ -2,8 +2,6 @@ import json
 import os
 from pathlib import Path
 
-from django.contrib.auth import get_user_model
-
 from ...acl.test import patch_user_acl
 from ...conf import 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):
         """api supports users with whitespace around their passwords"""
         user_password = " old password "
-        new_password = " N3wP@55w0rd "
-
         new_email = "new@email.com"
 
         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."],
                 "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.",
                 ],
             },

+ 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):
-    user = User.objects.create_user("User", "test@example.com")
+    User.objects.create_user("User", "test@example.com")
     with pytest.raises(User.DoesNotExist):
         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(
             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())
 
     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")
         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.test import override_settings
 from django.urls import reverse
-from django.utils.encoding import smart_str
 
 from ...acl.test import patch_user_acl
 from ...categories.models import Category
@@ -455,7 +454,10 @@ class UserBanTests(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):
         super().setUp()
@@ -463,7 +465,9 @@ class UserDeleteOwnAccountTests(AuthenticatedUserTestCase):
 
     @override_settings(MISAGO_ENABLE_DELETE_OWN_ACCOUNT=False)
     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})
 
         self.assertEqual(response.status_code, 403)
@@ -483,7 +487,9 @@ class UserDeleteOwnAccountTests(AuthenticatedUserTestCase):
         self.assertEqual(
             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(
             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)
 
     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"})
         self.assertEqual(response.status_code, 400)
         self.assertEqual(

+ 1 - 1
misago/users/validators.py

@@ -145,7 +145,7 @@ REGISTRATION_VALIDATORS = list(map(import_string, validators_list))
 
 
 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):

+ 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."))
 
-    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")
         for user in users:
             if user.is_superuser:
@@ -252,8 +254,7 @@ class NewUser(UserAdmin, generic.ModelFormView):
     def initialize_form(self, form, request, target):
         if request.method == "POST":
             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):
         new_user = User.objects.create_user(
@@ -291,8 +292,7 @@ class EditUser(UserAdmin, generic.ModelFormView):
     def initialize_form(self, form, request, target):
         if request.method == "POST":
             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):
         target.username = target.old_username

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

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