Rafał Pitoń 8 years ago
parent
commit
4192bdfd1b
102 changed files with 3056 additions and 1650 deletions
  1. 3 3
      cleansource
  2. 2 2
      misago/threads/admin.py
  3. 2 2
      misago/threads/api/attachments.py
  4. 2 2
      misago/threads/api/pollvotecreateendpoint.py
  5. 1 1
      misago/threads/api/postendpoints/edits.py
  6. 2 3
      misago/threads/api/postendpoints/merge.py
  7. 5 2
      misago/threads/api/postendpoints/patch_post.py
  8. 3 1
      misago/threads/api/postendpoints/read.py
  9. 1 1
      misago/threads/api/postendpoints/split.py
  10. 4 3
      misago/threads/api/postingendpoint/attachments.py
  11. 1 1
      misago/threads/api/postingendpoint/category.py
  12. 10 4
      misago/threads/api/postingendpoint/emailnotification.py
  13. 5 3
      misago/threads/api/postingendpoint/participants.py
  14. 1 1
      misago/threads/api/postingendpoint/recordedit.py
  15. 6 2
      misago/threads/api/postingendpoint/reply.py
  16. 2 2
      misago/threads/api/postingendpoint/subscribe.py
  17. 2 1
      misago/threads/api/postingendpoint/syncprivatethreads.py
  18. 2 2
      misago/threads/api/threadendpoints/editor.py
  19. 11 9
      misago/threads/api/threadendpoints/merge.py
  20. 4 1
      misago/threads/api/threadendpoints/patch.py
  21. 3 1
      misago/threads/api/threadpoll.py
  22. 41 9
      misago/threads/api/threadposts.py
  23. 19 6
      misago/threads/api/threads.py
  24. 1 1
      misago/threads/forms.py
  25. 4 1
      misago/threads/management/commands/clearattachments.py
  26. 4 1
      misago/threads/models/attachment.py
  27. 5 2
      misago/threads/models/attachmenttype.py
  28. 1 1
      misago/threads/models/poll.py
  29. 1 1
      misago/threads/models/post.py
  30. 21 8
      misago/threads/models/thread.py
  31. 4 2
      misago/threads/moderation/threads.py
  32. 41 21
      misago/threads/participants.py
  33. 1 1
      misago/threads/permissions/attachments.py
  34. 9 7
      misago/threads/permissions/polls.py
  35. 4 4
      misago/threads/permissions/privatethreads.py
  36. 50 36
      misago/threads/permissions/threads.py
  37. 5 3
      misago/threads/search.py
  38. 4 1
      misago/threads/serializers/feed.py
  39. 13 5
      misago/threads/serializers/poll.py
  40. 8 4
      misago/threads/serializers/post.py
  41. 2 2
      misago/threads/serializers/thread.py
  42. 25 10
      misago/threads/signals.py
  43. 3 1
      misago/threads/subscriptions.py
  44. 2 1
      misago/threads/templatetags/misago_poststags.py
  45. 42 15
      misago/threads/tests/test_attachmentadmin_views.py
  46. 96 24
      misago/threads/tests/test_attachments_api.py
  47. 16 10
      misago/threads/tests/test_attachments_middleware.py
  48. 9 3
      misago/threads/tests/test_attachmenttypeadmin_views.py
  49. 31 11
      misago/threads/tests/test_attachmentview.py
  50. 51 18
      misago/threads/tests/test_emailnotification_middleware.py
  51. 1 1
      misago/threads/tests/test_events.py
  52. 14 4
      misago/threads/tests/test_floodprotection.py
  53. 26 8
      misago/threads/tests/test_participants.py
  54. 50 15
      misago/threads/tests/test_post_mentions.py
  55. 5 5
      misago/threads/tests/test_post_model.py
  56. 228 152
      misago/threads/tests/test_privatethread_patch_api.py
  57. 5 1
      misago/threads/tests/test_privatethread_reply_api.py
  58. 44 21
      misago/threads/tests/test_privatethread_start_api.py
  59. 11 3
      misago/threads/tests/test_privatethreads.py
  60. 18 14
      misago/threads/tests/test_privatethreads_api.py
  61. 15 3
      misago/threads/tests/test_search.py
  62. 32 10
      misago/threads/tests/test_subscription_middleware.py
  63. 4 1
      misago/threads/tests/test_subscriptions.py
  64. 32 11
      misago/threads/tests/test_thread_editreply_api.py
  65. 76 24
      misago/threads/tests/test_thread_merge_api.py
  66. 15 13
      misago/threads/tests/test_thread_model.py
  67. 265 133
      misago/threads/tests/test_thread_patch_api.py
  68. 8 4
      misago/threads/tests/test_thread_poll_api.py
  69. 74 32
      misago/threads/tests/test_thread_pollcreate_api.py
  70. 16 8
      misago/threads/tests/test_thread_polldelete_api.py
  71. 171 107
      misago/threads/tests/test_thread_polledit_api.py
  72. 20 10
      misago/threads/tests/test_thread_pollvotes_api.py
  73. 37 11
      misago/threads/tests/test_thread_postdelete_api.py
  74. 8 6
      misago/threads/tests/test_thread_postedits_api.py
  75. 4 2
      misago/threads/tests/test_thread_postlikes_api.py
  76. 70 34
      misago/threads/tests/test_thread_postmerge_api.py
  77. 54 30
      misago/threads/tests/test_thread_postmove_api.py
  78. 361 228
      misago/threads/tests/test_thread_postpatch_api.py
  79. 10 4
      misago/threads/tests/test_thread_postread_api.py
  80. 94 67
      misago/threads/tests/test_thread_postsplit_api.py
  81. 18 7
      misago/threads/tests/test_thread_reply_api.py
  82. 31 20
      misago/threads/tests/test_thread_start_api.py
  83. 2 2
      misago/threads/tests/test_threadparticipant_model.py
  84. 10 5
      misago/threads/tests/test_threads_api.py
  85. 53 74
      misago/threads/tests/test_threads_editor_api.py
  86. 139 109
      misago/threads/tests/test_threads_merge_api.py
  87. 3 1
      misago/threads/tests/test_threads_moderation.py
  88. 91 45
      misago/threads/tests/test_threadslists.py
  89. 18 18
      misago/threads/tests/test_threadview.py
  90. 33 21
      misago/threads/tests/test_utils.py
  91. 41 27
      misago/threads/testutils.py
  92. 76 27
      misago/threads/threadtypes/privatethread.py
  93. 111 37
      misago/threads/threadtypes/thread.py
  94. 2 3
      misago/threads/utils.py
  95. 20 12
      misago/threads/validators.py
  96. 13 3
      misago/threads/viewmodels/category.py
  97. 8 2
      misago/threads/viewmodels/posts.py
  98. 5 3
      misago/threads/viewmodels/thread.py
  99. 3 3
      misago/threads/viewmodels/threads.py
  100. 9 7
      misago/threads/views/admin/attachments.py
  101. 11 4
      misago/threads/views/goto.py
  102. 6 2
      misago/threads/views/thread.py

+ 3 - 3
cleansource

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

+ 2 - 2
misago/threads/admin.py

@@ -33,12 +33,12 @@ class MisagoAdminExtension(object):
             icon='fa fa-cubes',
             parent='misago:admin:system',
             after='misago:admin:system:settings:index',
-            link='misago:admin:system:attachments:index'
+            link='misago:admin:system:attachments:index',
         )
         site.add_node(
             name=_("Attachment types"),
             icon='fa fa-cube',
             parent='misago:admin:system',
             after='misago:admin:system:attachments:index',
-            link='misago:admin:system:attachment-types:index'
+            link='misago:admin:system:attachment-types:index',
         )

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

@@ -87,7 +87,7 @@ def validate_filesize(upload, filetype, hard_limit):
         raise ValidationError(
             message % {
                 'upload': filesizeformat(upload.size).rstrip('.0'),
-                'limit': filesizeformat(hard_limit * 1024).rstrip('.0')
+                'limit': filesizeformat(hard_limit * 1024).rstrip('.0'),
             }
         )
 
@@ -98,7 +98,7 @@ def validate_filesize(upload, filetype, hard_limit):
         raise ValidationError(
             message % {
                 'upload': filesizeformat(upload.size).rstrip('.0'),
-                'limit': filesizeformat(filetype.size_limit * 1024).rstrip('.0')
+                'limit': filesizeformat(filetype.size_limit * 1024).rstrip('.0'),
             }
         )
 

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

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

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

@@ -49,7 +49,7 @@ def revert_post_endpoint(request, post):
         editor_slug=request.user.slug,
         editor_ip=request.user_ip,
         edited_from=post.original,
-        edited_to=edit.edited_from
+        edited_to=edit.edited_from,
     )
 
     parsing_result = common_flavour(request, post.poster, edit.edited_from)

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

@@ -96,9 +96,8 @@ def clean_posts_for_merge(request, thread):
                     raise MergeError(authorship_error)
 
             if posts[0].pk != thread.first_post_id:
-                if posts[0].is_hidden != post.is_hidden or posts[
-                        0
-                ].is_unapproved != post.is_unapproved:
+                if (posts[0].is_hidden != post.is_hidden or
+                        posts[0].is_unapproved != post.is_unapproved):
                     raise MergeError(_("Posts with different visibility can't be merged."))
 
             posts.append(post)

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

@@ -50,7 +50,7 @@ def patch_is_liked(request, post, value):
             liker=request.user,
             liker_name=request.user.username,
             liker_slug=request.user.slug,
-            liker_ip=request.user_ip
+            liker_ip=request.user_ip,
         )
         post.likes += 1
 
@@ -61,7 +61,10 @@ def patch_is_liked(request, post, value):
 
     post.last_likes = []
     for like in post.postlike_set.all()[:4]:
-        post.last_likes.append({'id': like.liker_id, 'username': like.liker_name})
+        post.last_likes.append({
+            'id': like.liker_id,
+            'username': like.liker_name,
+        })
 
     post.save(update_fields=['likes', 'last_likes'])
 

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

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

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

@@ -76,7 +76,7 @@ def split_posts_to_new_thread(request, thread, validated_data, posts):
     new_thread = Thread(
         category=validated_data['category'],
         started_on=thread.started_on,
-        last_post_on=thread.last_post_on
+        last_post_on=thread.last_post_on,
     )
 
     new_thread.set_title(validated_data['title'])

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

@@ -82,7 +82,8 @@ class AttachmentsSerializer(serializers.Serializer):
             return []
 
         queryset = user.attachment_set.select_related('filetype').filter(
-            post__isnull=True, id__in=ids
+            post__isnull=True,
+            id__in=ids,
         )
 
         return list(queryset)
@@ -126,11 +127,11 @@ def validate_attachments_count(data):
         message = ungettext(
             "You can't attach more than %(limit_value)s file to single post (added %(show_value)s).",
             "You can't attach more than %(limit_value)s flies to single post (added %(show_value)s).",
-            settings.MISAGO_POST_ATTACHMENTS_LIMIT
+            settings.MISAGO_POST_ATTACHMENTS_LIMIT,
         )
         raise serializers.ValidationError(
             message % {
                 'limit_value': settings.MISAGO_POST_ATTACHMENTS_LIMIT,
-                'show_value': total_attachments
+                'show_value': total_attachments,
             }
         )

+ 1 - 1
misago/threads/api/postingendpoint/category.py

@@ -45,7 +45,7 @@ class CategorySerializer(serializers.Serializer):
     category = serializers.IntegerField(
         error_messages={
             'required': ugettext_lazy("You have to select category to post thread in."),
-            'invalid': ugettext_lazy("Selected category is invalid.")
+            'invalid': ugettext_lazy("Selected category is invalid."),
         }
     )
 

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

@@ -17,7 +17,8 @@ class EmailNotificationMiddleware(PostingMiddleware):
 
     def post_save(self, serializer):
         queryset = self.thread.subscription_set.filter(
-            send_email=True, last_read_on__gte=self.previous_last_post_on
+            send_email=True,
+            last_read_on__gte=self.previous_last_post_on,
         ).exclude(user=self.user).select_related('user')
 
         notifications = []
@@ -42,7 +43,12 @@ class EmailNotificationMiddleware(PostingMiddleware):
         subject_formats = {'user': self.user.username, 'thread': self.thread.title}
 
         return build_mail(
-            self.request, subscriber, subject % subject_formats, 'misago/emails/thread/reply',
-            {'thread': self.thread,
-             'post': self.post}
+            self.request,
+            subscriber,
+            subject % subject_formats,
+            'misago/emails/thread/reply',
+            {
+                'thread': self.thread,
+                'post': self.post,
+            },
         )

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

@@ -58,11 +58,13 @@ class ParticipantsSerializer(serializers.Serializer):
             message = ungettext(
                 "You can't add more than %(users)s user to private thread (you've added %(added)s).",
                 "You can't add more than %(users)s users to private thread (you've added %(added)s).",
-                max_participants
+                max_participants,
             )
             raise serializers.ValidationError(
-                message % {'users': max_participants,
-                           'added': len(clean_usernames)}
+                message % {
+                    'users': max_participants,
+                    'added': len(clean_usernames),
+                }
             )
 
         return list(set(clean_usernames))

+ 1 - 1
misago/threads/api/postingendpoint/recordedit.py

@@ -34,5 +34,5 @@ class RecordEditMiddleware(PostingMiddleware):
             editor_slug=self.user.slug,
             editor_ip=self.request.user_ip,
             edited_from=self.original_post,
-            edited_to=self.post.original
+            edited_to=self.post.original,
         )

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

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

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

@@ -26,7 +26,7 @@ class SubscribeMiddleware(PostingMiddleware):
         self.user.subscription_set.create(
             category=self.thread.category,
             thread=self.thread,
-            send_email=self.user.subscribe_to_started_threads == UserModel.SUBSCRIBE_ALL
+            send_email=self.user.subscribe_to_started_threads == UserModel.SUBSCRIBE_ALL,
         )
 
     def subscribe_replied_thread(self):
@@ -48,5 +48,5 @@ class SubscribeMiddleware(PostingMiddleware):
         self.user.subscription_set.create(
             category=self.thread.category,
             thread=self.thread,
-            send_email=self.user.subscribe_to_replied_threads == UserModel.SUBSCRIBE_ALL
+            send_email=self.user.subscribe_to_replied_threads == UserModel.SUBSCRIBE_ALL,
         )

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

@@ -16,5 +16,6 @@ class SyncPrivateThreadsMiddleware(PostingMiddleware):
 
     def post_save(self, serializer):
         set_users_unread_private_threads_sync(
-            participants=self.thread.participants_list, exclude_user=self.user
+            participants=self.thread.participants_list,
+            exclude_user=self.user,
         )

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

@@ -31,7 +31,7 @@ def thread_start_editor(request):
             post = {
                 'close': bool(category.acl['can_close_threads']),
                 'hide': bool(category.acl['can_hide_threads']),
-                'pin': category.acl['can_pin_threads']
+                'pin': category.acl['can_pin_threads'],
             }
 
             available.append(category.pk)
@@ -43,7 +43,7 @@ def thread_start_editor(request):
             'id': category.pk,
             'name': category.name,
             'level': category.level - 1,
-            'post': post
+            'post': post,
         })
 
     # list only categories that allow new threads, or contains subcategory that allows one

+ 11 - 9
misago/threads/api/threadendpoints/merge.py

@@ -48,14 +48,16 @@ def thread_merge_endpoint(request, thread, viewmodel):
     except PermissionDenied as e:
         return Response({'detail': e.args[0]}, status=400)
     except Http404:
-        return Response({
-            'detail':
-                _(
-                    "The thread you have entered link to doesn't "
-                    "exist or you don't have permission to see it."
-                )
-        },
-                        status=400)
+        return Response(
+            {
+                'detail':
+                    _(
+                        "The thread you have entered link to doesn't "
+                        "exist or you don't have permission to see it."
+                    )
+            },
+            status=400,
+        )
 
     polls_handler = PollMergeHandler([thread, other_thread])
     if len(polls_handler.polls) == 1:
@@ -91,7 +93,7 @@ def thread_merge_endpoint(request, thread, viewmodel):
     return Response({
         'id': other_thread.pk,
         'title': other_thread.title,
-        'url': other_thread.get_absolute_url()
+        'url': other_thread.get_absolute_url(),
     })
 
 

+ 4 - 1
misago/threads/api/threadendpoints/patch.py

@@ -262,7 +262,10 @@ def patch_remove_participant(request, thread, value):
         make_participants_aware(request.user, thread)
         participants = ThreadParticipantSerializer(thread.participants_list, many=True)
 
-        return {'deleted': False, 'participants': participants.data}
+        return {
+            'deleted': False,
+            'participants': participants.data,
+        }
 
 
 thread_patch_dispatcher.remove('participants', patch_remove_participant)

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

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

+ 41 - 9
misago/threads/api/threadposts.py

@@ -33,15 +33,29 @@ class ViewSet(viewsets.ViewSet):
     post_ = ThreadPost
 
     def get_thread(
-            self, request, pk, read_aware=True, subscription_aware=True, select_for_update=False
+            self,
+            request,
+            pk,
+            read_aware=True,
+            subscription_aware=True,
+            select_for_update=False,
     ):
         return self.thread(
-            request, get_int_or_404(pk), None, read_aware, subscription_aware, select_for_update
+            request,
+            get_int_or_404(pk),
+            None,
+            read_aware,
+            subscription_aware,
+            select_for_update,
         )
 
     def get_thread_for_update(self, request, pk):
         return self.get_thread(
-            request, pk, read_aware=False, subscription_aware=False, select_for_update=True
+            request,
+            pk,
+            read_aware=False,
+            subscription_aware=False,
+            select_for_update=True,
         )
 
     def get_posts(self, request, thread, page):
@@ -89,10 +103,18 @@ class ViewSet(viewsets.ViewSet):
         thread = self.get_thread_for_update(request, thread_pk).unwrap()
         allow_reply_thread(request.user, thread)
 
-        post = Post(thread=thread, category=thread.category)
+        post = Post(
+            thread=thread,
+            category=thread.category,
+        )
 
         # Put them through posting pipeline
-        posting = PostingEndpoint(request, PostingEndpoint.REPLY, thread=thread, post=post)
+        posting = PostingEndpoint(
+            request,
+            PostingEndpoint.REPLY,
+            thread=thread,
+            post=post,
+        )
 
         if posting.is_valid():
             user_posts = request.user.posts
@@ -117,7 +139,12 @@ class ViewSet(viewsets.ViewSet):
 
         allow_edit_post(request.user, post)
 
-        posting = PostingEndpoint(request, PostingEndpoint.EDIT, thread=thread, post=post)
+        posting = PostingEndpoint(
+            request,
+            PostingEndpoint.EDIT,
+            thread=thread,
+            post=post,
+        )
 
         if posting.is_valid():
             post_edits = post.edits
@@ -177,7 +204,12 @@ class ViewSet(viewsets.ViewSet):
 
     @detail_route(methods=['get'], url_path='editor')
     def post_editor(self, request, thread_pk, pk):
-        thread = self.get_thread(request, thread_pk, read_aware=False, subscription_aware=False)
+        thread = self.get_thread(
+            request,
+            thread_pk,
+            read_aware=False,
+            subscription_aware=False,
+        )
         post = self.get_post(request, thread, pk).unwrap()
 
         allow_edit_post(request.user, post)
@@ -197,7 +229,7 @@ class ViewSet(viewsets.ViewSet):
             'attachments': attachments_json,
             'can_protect': bool(thread.category.acl['can_protect_posts']),
             'is_protected': post.is_protected,
-            'poster': post.poster_name
+            'poster': post.poster_name,
         })
 
     @list_route(methods=['get'], url_path='editor')
@@ -218,7 +250,7 @@ class ViewSet(viewsets.ViewSet):
             return Response({
                 'id': reply_to.pk,
                 'post': reply_to.original,
-                'poster': reply_to.poster_name
+                'poster': reply_to.poster_name,
             })
         else:
             return Response({})

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

@@ -28,12 +28,21 @@ class ViewSet(viewsets.ViewSet):
             self, request, pk, read_aware=True, subscription_aware=True, select_for_update=False
     ):
         return self.thread(
-            request, get_int_or_404(pk), None, read_aware, subscription_aware, select_for_update
+            request,
+            get_int_or_404(pk),
+            None,
+            read_aware,
+            subscription_aware,
+            select_for_update,
         )
 
     def get_thread_for_update(self, request, pk):
         return self.get_thread(
-            request, pk, read_aware=False, subscription_aware=False, select_for_update=True
+            request,
+            pk,
+            read_aware=False,
+            subscription_aware=False,
+            select_for_update=True,
         )
 
     def retrieve(self, request, pk):
@@ -70,7 +79,11 @@ class ThreadViewSet(ViewSet):
 
         # Put them through posting pipeline
         posting = PostingEndpoint(
-            request, PostingEndpoint.START, tree_name=THREADS_ROOT_NAME, thread=thread, post=post
+            request,
+            PostingEndpoint.START,
+            tree_name=THREADS_ROOT_NAME,
+            thread=thread,
+            post=post,
         )
 
         if posting.is_valid():
@@ -79,7 +92,7 @@ class ThreadViewSet(ViewSet):
             return Response({
                 'id': thread.pk,
                 'title': thread.title,
-                'url': thread.get_absolute_url()
+                'url': thread.get_absolute_url(),
             })
         else:
             return Response(posting.errors, status=400)
@@ -128,7 +141,7 @@ class PrivateThreadViewSet(ViewSet):
             PostingEndpoint.START,
             tree_name=PRIVATE_THREADS_ROOT_NAME,
             thread=thread,
-            post=post
+            post=post,
         )
 
         if posting.is_valid():
@@ -137,7 +150,7 @@ class PrivateThreadViewSet(ViewSet):
             return Response({
                 'id': thread.pk,
                 'title': thread.title,
-                'url': thread.get_absolute_url()
+                'url': thread.get_absolute_url(),
             })
         else:
             return Response(posting.errors, status=400)

+ 1 - 1
misago/threads/forms.py

@@ -18,7 +18,7 @@ class SearchAttachmentsForm(forms.Form):
         coerce=int,
         choices=get_searchable_filetypes,
         empty_value=0,
-        required=False
+        required=False,
     )
     is_orphan = forms.ChoiceField(
         label=_("State"),

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

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

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

@@ -39,7 +39,10 @@ class Attachment(models.Model):
     uploaded_on = models.DateTimeField(default=timezone.now, db_index=True)
 
     uploader = models.ForeignKey(
-        settings.AUTH_USER_MODEL, blank=True, null=True, on_delete=models.SET_NULL
+        settings.AUTH_USER_MODEL,
+        blank=True,
+        null=True,
+        on_delete=models.SET_NULL,
     )
     uploader_name = models.CharField(max_length=255)
     uploader_slug = models.CharField(max_length=255, db_index=True)

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

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

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

@@ -93,6 +93,6 @@ class Poll(models.Model):
                 'label': choice['label'],
                 'votes': choice['votes'],
                 'selected': choice['selected'],
-                'proc': proc
+                'proc': proc,
             })
         return view_choices

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

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

+ 21 - 8
misago/threads/models/thread.py

@@ -13,9 +13,11 @@ class Thread(models.Model):
     WEIGHT_PINNED = 1
     WEIGHT_GLOBAL = 2
 
-    WEIGHT_CHOICES = ((WEIGHT_DEFAULT, _("Don't pin thread")),
-                      (WEIGHT_PINNED, _("Pin thread within category")),
-                      (WEIGHT_GLOBAL, _("Pin thread globally")))
+    WEIGHT_CHOICES = [
+        (WEIGHT_DEFAULT, _("Don't pin thread")),
+        (WEIGHT_PINNED, _("Pin thread within category")),
+        (WEIGHT_GLOBAL, _("Pin thread globally")),
+    ]
 
     category = models.ForeignKey('misago_categories.Category')
     title = models.CharField(max_length=255)
@@ -33,16 +35,27 @@ class Thread(models.Model):
     last_post_on = models.DateTimeField(db_index=True)
 
     first_post = models.ForeignKey(
-        'misago_threads.Post', related_name='+', null=True, blank=True, on_delete=models.SET_NULL
+        'misago_threads.Post',
+        related_name='+',
+        null=True,
+        blank=True,
+        on_delete=models.SET_NULL,
     )
     starter = models.ForeignKey(
-        settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.SET_NULL
+        settings.AUTH_USER_MODEL,
+        null=True,
+        blank=True,
+        on_delete=models.SET_NULL,
     )
     starter_name = models.CharField(max_length=255)
     starter_slug = models.CharField(max_length=255)
 
     last_post = models.ForeignKey(
-        'misago_threads.Post', related_name='+', null=True, blank=True, on_delete=models.SET_NULL
+        'misago_threads.Post',
+        related_name='+',
+        null=True,
+        blank=True,
+        on_delete=models.SET_NULL,
     )
     last_post_is_event = models.BooleanField(default=False)
     last_poster = models.ForeignKey(
@@ -50,7 +63,7 @@ class Thread(models.Model):
         related_name='last_poster_set',
         null=True,
         blank=True,
-        on_delete=models.SET_NULL
+        on_delete=models.SET_NULL,
     )
     last_poster_name = models.CharField(max_length=255, null=True, blank=True)
     last_poster_slug = models.CharField(max_length=255, null=True, blank=True)
@@ -65,7 +78,7 @@ class Thread(models.Model):
         settings.AUTH_USER_MODEL,
         related_name='privatethread_set',
         through='ThreadParticipant',
-        through_fields=('thread', 'user')
+        through_fields=('thread', 'user'),
     )
 
     class Meta:

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

@@ -33,7 +33,9 @@ def change_thread_title(request, thread, new_title):
         thread.first_post.update_search_vector()
         thread.first_post.save(update_fields=['search_vector'])
 
-        record_event(request, thread, 'changed_title', {'old_title': old_title})
+        record_event(request, thread, 'changed_title', {
+            'old_title': old_title,
+        })
         return True
     else:
         return False
@@ -80,7 +82,7 @@ def move_thread(request, thread, new_category):
                 'from_category': {
                     'name': from_category.name,
                     'url': from_category.get_absolute_url(),
-                }
+                },
             }
         )
         return True

+ 41 - 21
misago/threads/participants.py

@@ -28,7 +28,8 @@ def make_threads_participants_aware(user, threads):
         threads_dict[thread.pk] = thread
 
     participants_qs = ThreadParticipant.objects.filter(
-        user=user, thread_id__in=threads_dict.keys()
+        user=user,
+        thread_id__in=threads_dict.keys(),
     )
 
     for participant in participants_qs:
@@ -78,16 +79,21 @@ def change_owner(request, thread, user):
     """
     ThreadParticipant.objects.set_owner(thread, user)
     set_users_unread_private_threads_sync(
-        participants=thread.participants_list, exclude_user=request.user
+        participants=thread.participants_list,
+        exclude_user=request.user,
     )
 
     if thread.participant and thread.participant.is_owner:
         record_event(
-            request, thread, 'changed_owner',
-            {'user': {
-                'username': user.username,
-                'url': user.get_absolute_url(),
-            }}
+            request,
+            thread,
+            'changed_owner',
+            {
+                'user': {
+                    'username': user.username,
+                    'url': user.get_absolute_url(),
+                },
+            },
         )
     else:
         record_event(request, thread, 'tookover')
@@ -103,11 +109,15 @@ def add_participant(request, thread, user):
         record_event(request, thread, 'entered_thread')
     else:
         record_event(
-            request, thread, 'added_participant',
-            {'user': {
-                'username': user.username,
-                'url': user.get_absolute_url(),
-            }}
+            request,
+            thread,
+            'added_participant',
+            {
+                'user': {
+                    'username': user.username,
+                    'url': user.get_absolute_url(),
+                },
+            },
         )
 
 
@@ -124,7 +134,9 @@ def add_participants(request, thread, users):
         thread_participants = []
 
     set_users_unread_private_threads_sync(
-        users=users, participants=thread_participants, exclude_user=request.user
+        users=users,
+        participants=thread_participants,
+        exclude_user=request.user,
     )
 
     emails = []
@@ -137,11 +149,15 @@ def add_participants(request, thread, users):
 
 def build_noticiation_email(request, thread, user):
     subject = _('%(user)s has invited you to participate in private thread "%(thread)s"')
-    subject_formats = {'thread': thread.title, 'user': request.user.username}
+    subject_formats = {
+        'thread': thread.title,
+        'user': request.user.username,
+    }
 
     return build_mail(
-        request, user, subject % subject_formats, 'misago/emails/privatethread/added',
-        {'thread': thread}
+        request, user, subject % subject_formats, 'misago/emails/privatethread/added', {
+            'thread': thread,
+        }
     )
 
 
@@ -180,9 +196,13 @@ def remove_participant(request, thread, user):
                 event_type = 'removed_participant'
 
         record_event(
-            request, thread, event_type,
-            {'user': {
-                'username': user.username,
-                'url': user.get_absolute_url(),
-            }}
+            request,
+            thread,
+            event_type,
+            {
+                'user': {
+                    'username': user.username,
+                    'url': user.get_absolute_url(),
+                },
+            },
         )

+ 1 - 1
misago/threads/permissions/attachments.py

@@ -63,7 +63,7 @@ def build_acl(acl, roles, key_name):
         key=key_name,
         max_attachment_size=algebra.greater,
         can_download_other_users_attachments=algebra.greater,
-        can_delete_other_users_attachments=algebra.greater
+        can_delete_other_users_attachments=algebra.greater,
     )
 
 

+ 9 - 7
misago/threads/permissions/polls.py

@@ -35,29 +35,29 @@ class RolePermissionsForm(forms.Form):
         label=_("Can start polls"),
         coerce=int,
         initial=0,
-        choices=((0, _("No")), (1, _("Own threads")), (2, _("All threads")))
+        choices=((0, _("No")), (1, _("Own threads")), (2, _("All threads")), ),
     )
     can_edit_polls = forms.TypedChoiceField(
         label=_("Can edit polls"),
         coerce=int,
         initial=0,
-        choices=((0, _("No")), (1, _("Own polls")), (2, _("All polls")))
+        choices=((0, _("No")), (1, _("Own polls")), (2, _("All polls")), ),
     )
     can_delete_polls = forms.TypedChoiceField(
         label=_("Can delete polls"),
         coerce=int,
         initial=0,
-        choices=((0, _("No")), (1, _("Own polls")), (2, _("All polls")))
+        choices=((0, _("No")), (1, _("Own polls")), (2, _("All polls")), ),
     )
     poll_edit_time = forms.IntegerField(
         label=_("Time limit for own polls edits, in minutes"),
         help_text=_("Enter 0 to don't limit time for editing own polls."),
         initial=0,
-        min_value=0
+        min_value=0,
     )
     can_always_see_poll_voters = YesNoSwitch(
         label=_("Can always see polls voters"),
-        help_text=_("Allows users to see who voted in poll even if poll votes are secret.")
+        help_text=_("Allows users to see who voted in poll even if poll votes are secret."),
     )
 
 
@@ -79,7 +79,7 @@ def build_acl(acl, roles, key_name):
         'can_edit_polls': 0,
         'can_delete_polls': 0,
         'poll_edit_time': 0,
-        'can_always_see_poll_voters': 0
+        'can_always_see_poll_voters': 0,
     })
 
     return algebra.sum_acls(
@@ -109,7 +109,9 @@ def add_acl_to_poll(user, poll):
 
 
 def add_acl_to_thread(user, thread):
-    thread.acl.update({'can_start_poll': can_start_poll(user, thread)})
+    thread.acl.update({
+        'can_start_poll': can_start_poll(user, thread),
+    })
 
 
 def register_with(registry):

+ 4 - 4
misago/threads/permissions/privatethreads.py

@@ -42,25 +42,25 @@ class PermissionsForm(forms.Form):
         label=_("Max number of users invited to private thread"),
         help_text=_("Enter 0 to don't limit number of participants."),
         initial=3,
-        min_value=0
+        min_value=0,
     )
     can_add_everyone_to_private_threads = YesNoSwitch(
         label=_("Can add everyone to threads"),
-        help_text=_("Allows user to add users that are blocking him to private threads.")
+        help_text=_("Allows user to add users that are blocking him to private threads."),
     )
     can_report_private_threads = YesNoSwitch(
         label=_("Can report private threads"),
         help_text=_(
             "Allows user to report private threads they are "
             "participating in, making them accessible to moderators."
-        )
+        ),
     )
     can_moderate_private_threads = YesNoSwitch(
         label=_("Can moderate private threads"),
         help_text=_(
             "Allows user to read, reply, edit and delete content "
             "in reported private threads."
-        )
+        ),
     )
 
 

+ 50 - 36
misago/threads/permissions/threads.py

@@ -62,7 +62,7 @@ class RolePermissionsForm(forms.Form):
             "threads lists, it will only display threads belonging to "
             "categories in which the user has permission to approve "
             "content."
-        )
+        ),
     )
     can_see_reported_content_lists = YesNoSwitch(
         label=_("Can see reported content list"),
@@ -73,11 +73,11 @@ class RolePermissionsForm(forms.Form):
             "threads lists, it will only display threads belonging to "
             "categories in which the user has permission to see posts "
             "reports."
-        )
+        ),
     )
     can_omit_flood_protection = YesNoSwitch(
         label=_("Can omit flood protection"),
-        help_text=_("Allows posting more frequently than flood protection would.")
+        help_text=_("Allows posting more frequently than flood protection would."),
     )
 
 
@@ -88,7 +88,7 @@ class CategoryPermissionsForm(forms.Form):
         label=_("Can see threads"),
         coerce=int,
         initial=0,
-        choices=((0, _("Started threads")), (1, _("All threads")))
+        choices=((0, _("Started threads")), (1, _("All threads")), ),
     )
 
     can_start_threads = YesNoSwitch(label=_("Can start threads"))
@@ -98,7 +98,7 @@ class CategoryPermissionsForm(forms.Form):
         label=_("Can edit threads"),
         coerce=int,
         initial=0,
-        choices=((0, _("No")), (1, _("Own threads")), (2, _("All threads")))
+        choices=((0, _("No")), (1, _("Own threads")), (2, _("All threads")), ),
     )
     can_hide_own_threads = forms.TypedChoiceField(
         label=_("Can hide own threads"),
@@ -108,26 +108,26 @@ class CategoryPermissionsForm(forms.Form):
         ),
         coerce=int,
         initial=0,
-        choices=((0, _("No")), (1, _("Hide threads")), (2, _("Delete threads")))
+        choices=((0, _("No")), (1, _("Hide threads")), (2, _("Delete threads")), ),
     )
     thread_edit_time = forms.IntegerField(
         label=_("Time limit for own threads edits, in minutes"),
         help_text=_("Enter 0 to don't limit time for editing own threads."),
         initial=0,
-        min_value=0
+        min_value=0,
     )
     can_hide_threads = forms.TypedChoiceField(
         label=_("Can hide all threads"),
         coerce=int,
         initial=0,
-        choices=((0, _("No")), (1, _("Hide threads")), (2, _("Delete threads")))
+        choices=((0, _("No")), (1, _("Hide threads")), (2, _("Delete threads")), ),
     )
 
     can_pin_threads = forms.TypedChoiceField(
         label=_("Can pin threads"),
         coerce=int,
         initial=0,
-        choices=((0, _("No")), (1, _("Locally")), (2, _("Globally")))
+        choices=((0, _("No")), (1, _("Locally")), (2, _("Globally")), ),
     )
     can_close_threads = YesNoSwitch(label=_("Can close threads"))
     can_move_threads = YesNoSwitch(label=_("Can move threads"))
@@ -137,42 +137,42 @@ class CategoryPermissionsForm(forms.Form):
         label=_("Can edit posts"),
         coerce=int,
         initial=0,
-        choices=((0, _("No")), (1, _("Own posts")), (2, _("All posts")))
+        choices=((0, _("No")), (1, _("Own posts")), (2, _("All posts")), ),
     )
     can_hide_own_posts = forms.TypedChoiceField(
         label=_("Can hide own posts"),
         help_text=_("Only last posts to thread made within edit time limit can be hidden."),
         coerce=int,
         initial=0,
-        choices=((0, _("No")), (1, _("Hide posts")), (2, _("Delete posts")))
+        choices=((0, _("No")), (1, _("Hide posts")), (2, _("Delete posts")), ),
     )
     post_edit_time = forms.IntegerField(
         label=_("Time limit for own post edits, in minutes"),
         help_text=_("Enter 0 to don't limit time for editing own posts."),
         initial=0,
-        min_value=0
+        min_value=0,
     )
     can_hide_posts = forms.TypedChoiceField(
         label=_("Can hide all posts"),
         coerce=int,
         initial=0,
-        choices=((0, _("No")), (1, _("Hide posts")), (2, _("Delete posts")))
+        choices=((0, _("No")), (1, _("Hide posts")), (2, _("Delete posts")), ),
     )
 
     can_see_posts_likes = forms.TypedChoiceField(
         label=_("Can see posts likes"),
         coerce=int,
         initial=0,
-        choices=((0, _("No")), (1, _("Number only")), (2, _("Number and list of likers")))
+        choices=((0, _("No")), (1, _("Number only")), (2, _("Number and list of likers")), ),
     )
     can_like_posts = YesNoSwitch(
         label=_("Can like posts"),
-        help_text=_("Only users with this permission to see likes can like posts.")
+        help_text=_("Only users with this permission to see likes can like posts."),
     )
 
     can_protect_posts = YesNoSwitch(
         label=_("Can protect posts"),
-        help_text=_("Only users with this permission can edit protected posts.")
+        help_text=_("Only users with this permission can edit protected posts."),
     )
     can_move_posts = YesNoSwitch(
         label=_("Can move posts"), help_text=_("Will be able to move posts to other threads.")
@@ -180,7 +180,7 @@ class CategoryPermissionsForm(forms.Form):
     can_merge_posts = YesNoSwitch(label=_("Can merge posts"))
     can_approve_content = YesNoSwitch(
         label=_("Can approve content"),
-        help_text=_("Will be able to see and approve unapproved content.")
+        help_text=_("Will be able to see and approve unapproved content."),
     )
     can_report_content = YesNoSwitch(label=_("Can report posts"))
     can_see_reports = YesNoSwitch(label=_("Can see reports"))
@@ -189,7 +189,7 @@ class CategoryPermissionsForm(forms.Form):
         label=_("Can hide events"),
         coerce=int,
         initial=0,
-        choices=((0, _("No")), (1, _("Hide events")), (2, _("Delete events")))
+        choices=((0, _("No")), (1, _("Hide events")), (2, _("Delete events")), ),
     )
 
 
@@ -476,8 +476,10 @@ ACL tests
 
 def allow_see_thread(user, target):
     category_acl = user.acl_cache['categories'].get(
-        target.category_id, {'can_see': False,
-                             'can_browse': False}
+        target.category_id, {
+            'can_see': False,
+            'can_browse': False,
+        }
     )
 
     if not (category_acl['can_see'] and category_acl['can_browse']):
@@ -502,8 +504,10 @@ def allow_start_thread(user, target):
         raise PermissionDenied(_("You have to sign in to start threads."))
 
     category_acl = user.acl_cache['categories'].get(
-        target.pk, {'can_close_threads': False,
-                    'can_start_threads': False}
+        target.pk, {
+            'can_close_threads': False,
+            'can_start_threads': False,
+        }
     )
 
     if target.is_closed and not category_acl['can_close_threads']:
@@ -523,8 +527,10 @@ def allow_reply_thread(user, target):
         raise PermissionDenied(_("You have to sign in to reply threads."))
 
     category_acl = user.acl_cache['categories'].get(
-        target.category_id, {'can_close_threads': False,
-                             'can_reply_threads': False}
+        target.category_id, {
+            'can_close_threads': False,
+            'can_reply_threads': False,
+        }
     )
 
     if not category_acl['can_close_threads']:
@@ -575,8 +581,10 @@ can_edit_thread = return_boolean(allow_edit_thread)
 
 def allow_see_post(user, target):
     category_acl = user.acl_cache['categories'].get(
-        target.category_id, {'can_approve_content': False,
-                             'can_hide_events': False}
+        target.category_id, {
+            'can_approve_content': False,
+            'can_hide_events': False,
+        }
     )
 
     if not target.is_event and target.is_unapproved:
@@ -625,7 +633,7 @@ def allow_edit_post(user, target):
             message = ungettext(
                 "You can't edit posts that are older than %(minutes)s minute.",
                 "You can't edit posts that are older than %(minutes)s minutes.",
-                category_acl['post_edit_time']
+                category_acl['post_edit_time'],
             )
             raise PermissionDenied(message % {'minutes': category_acl['post_edit_time']})
 
@@ -638,8 +646,10 @@ def allow_unhide_post(user, target):
         raise PermissionDenied(_("You have to sign in to reveal posts."))
 
     category_acl = user.acl_cache['categories'].get(
-        target.category_id, {'can_hide_posts': 0,
-                             'can_hide_own_posts': 0}
+        target.category_id, {
+            'can_hide_posts': 0,
+            'can_hide_own_posts': 0,
+        }
     )
 
     if not category_acl['can_hide_posts']:
@@ -662,7 +672,7 @@ def allow_unhide_post(user, target):
             message = ungettext(
                 "You can't reveal posts that are older than %(minutes)s minute.",
                 "You can't reveal posts that are older than %(minutes)s minutes.",
-                category_acl['post_edit_time']
+                category_acl['post_edit_time'],
             )
             raise PermissionDenied(message % {'minutes': category_acl['post_edit_time']})
 
@@ -678,8 +688,10 @@ def allow_hide_post(user, target):
         raise PermissionDenied(_("You have to sign in to hide posts."))
 
     category_acl = user.acl_cache['categories'].get(
-        target.category_id, {'can_hide_posts': 0,
-                             'can_hide_own_posts': 0}
+        target.category_id, {
+            'can_hide_posts': 0,
+            'can_hide_own_posts': 0,
+        }
     )
 
     if not category_acl['can_hide_posts']:
@@ -702,7 +714,7 @@ def allow_hide_post(user, target):
             message = ungettext(
                 "You can't hide posts that are older than %(minutes)s minute.",
                 "You can't hide posts that are older than %(minutes)s minutes.",
-                category_acl['post_edit_time']
+                category_acl['post_edit_time'],
             )
             raise PermissionDenied(message % {'minutes': category_acl['post_edit_time']})
 
@@ -718,8 +730,10 @@ def allow_delete_post(user, target):
         raise PermissionDenied(_("You have to sign in to delete posts."))
 
     category_acl = user.acl_cache['categories'].get(
-        target.category_id, {'can_hide_posts': 0,
-                             'can_hide_own_posts': 0}
+        target.category_id, {
+            'can_hide_posts': 0,
+            'can_hide_own_posts': 0,
+        }
     )
 
     if category_acl['can_hide_posts'] != 2:
@@ -742,7 +756,7 @@ def allow_delete_post(user, target):
             message = ungettext(
                 "You can't delete posts that are older than %(minutes)s minute.",
                 "You can't delete posts that are older than %(minutes)s minutes.",
-                category_acl['post_edit_time']
+                category_acl['post_edit_time'],
             )
             raise PermissionDenied(message % {'minutes': category_acl['post_edit_time']})
 

+ 5 - 3
misago/threads/search.py

@@ -33,7 +33,7 @@ class SearchThreads(SearchProvider):
             page,
             settings.MISAGO_POSTS_PER_PAGE,
             settings.MISAGO_POSTS_TAIL,
-            allow_explicit_first_page=True
+            allow_explicit_first_page=True,
         )
         paginator = pagination_dict(list_page)
 
@@ -46,7 +46,9 @@ class SearchThreads(SearchProvider):
         add_categories_to_items(root_category.unwrap(), threads_categories, posts + threads)
 
         results = {
-            'results': FeedSerializer(posts, many=True, context={'user': self.request.user}).data
+            'results': FeedSerializer(posts, many=True, context={
+                'user': self.request.user,
+            }).data
         }
         results.update(paginator)
 
@@ -62,5 +64,5 @@ def search_threads(request, query, visible_threads):
         is_hidden=False,
         is_unapproved=False,
         thread_id__in=visible_threads.values('id'),
-        search_vector=search_query
+        search_vector=search_query,
     ).annotate(rank=SearchRank(search_vector, search_query)).order_by('-rank', '-id')

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

@@ -29,7 +29,10 @@ class FeedSerializer(PostSerializer, MutableFields):
         fields = PostSerializer.Meta.fields + ['category', 'thread', 'top_category']
 
     def get_thread(self, obj):
-        return {'title': obj.thread.title, 'url': obj.thread.get_absolute_url()}
+        return {
+            'title': obj.thread.title,
+            'url': obj.thread.get_absolute_url(),
+        }
 
     def get_top_category(self, obj):
         try:

+ 13 - 5
misago/threads/serializers/poll.py

@@ -91,7 +91,10 @@ class EditPollSerializer(serializers.ModelSerializer):
                 choices_map[choice['hash']].update({'label': choice['label']})
                 final_choices.append(choices_map[choice['hash']])
             else:
-                choice.update({'hash': get_random_string(12), 'votes': 0})
+                choice.update({
+                    'hash': get_random_string(12),
+                    'votes': 0,
+                })
                 final_choices.append(choice)
 
         self.validate_choices_num(final_choices)
@@ -120,11 +123,13 @@ class EditPollSerializer(serializers.ModelSerializer):
             message = ungettext(
                 "You can't add more than %(limit_value)s option to a single poll (added %(show_value)s).",
                 "You can't add more than %(limit_value)s options to a single poll (added %(show_value)s).",
-                MAX_POLL_OPTIONS
+                MAX_POLL_OPTIONS,
             )
             raise serializers.ValidationError(
-                message % {'limit_value': MAX_POLL_OPTIONS,
-                           'show_value': total_choices}
+                message % {
+                    'limit_value': MAX_POLL_OPTIONS,
+                    'show_value': total_choices,
+                }
             )
 
     def validate(self, data):
@@ -166,7 +171,10 @@ class NewPollSerializer(EditPollSerializer):
         self.validate_choices_num(clean_choices)
 
         for choice in clean_choices:
-            choice.update({'hash': get_random_string(12), 'votes': 0})
+            choice.update({
+                'hash': get_random_string(12),
+                'votes': 0,
+            })
 
         return clean_choices
 

+ 8 - 4
misago/threads/serializers/post.py

@@ -150,8 +150,10 @@ class PostSerializer(serializers.ModelSerializer, MutableFields):
     def get_last_editor_url(self, obj):
         if obj.last_editor_id:
             return reverse(
-                'misago:user', kwargs={'pk': obj.last_editor_id,
-                                       'slug': obj.last_editor_slug}
+                'misago:user', kwargs={
+                    'pk': obj.last_editor_id,
+                    'slug': obj.last_editor_slug,
+                }
             )
         else:
             return None
@@ -159,8 +161,10 @@ class PostSerializer(serializers.ModelSerializer, MutableFields):
     def get_hidden_by_url(self, obj):
         if obj.hidden_by_id:
             return reverse(
-                'misago:user', kwargs={'pk': obj.hidden_by_id,
-                                       'slug': obj.hidden_by_slug}
+                'misago:user', kwargs={
+                    'pk': obj.hidden_by_id,
+                    'slug': obj.hidden_by_slug,
+                }
             )
         else:
             return None

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

@@ -87,8 +87,8 @@ class ThreadSerializer(serializers.ModelSerializer, MutableFields):
                 'index': obj.get_posts_api_url(),
                 'merge': obj.get_post_merge_api_url(),
                 'move': obj.get_post_move_api_url(),
-                'split': obj.get_post_split_api_url()
-            }
+                'split': obj.get_post_split_api_url(),
+            },
         }
 
     def get_url(self, obj):

+ 25 - 10
misago/threads/signals.py

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

+ 3 - 1
misago/threads/subscriptions.py

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

+ 2 - 1
misago/threads/templatetags/misago_poststags.py

@@ -32,7 +32,8 @@ def likes_label(post):
 
     return ngettext(
         "%(users)s and %(likes)s other user like this.",
-        "%(users)s and %(likes)s other users like this.", hidden_likes
+        "%(users)s and %(likes)s other users like this.",
+        hidden_likes,
     ) % formats
 
 

+ 42 - 15
misago/threads/tests/test_attachmentadmin_views.py

@@ -30,7 +30,7 @@ class AttachmentAdminViewsTests(AdminTestCase):
             filename='testfile_{}.zip'.format(Attachment.objects.count() + 1),
             file=None,
             image=None,
-            thumbnail=None
+            thumbnail=None,
         )
 
     def test_link_registered(self):
@@ -48,7 +48,11 @@ class AttachmentAdminViewsTests(AdminTestCase):
         attachments = [
             self.mock_attachment(self.post, file='somefile.pdf'),
             self.mock_attachment(image='someimage.jpg'),
-            self.mock_attachment(self.post, image='somelargeimage.png', thumbnail='somethumb.png'),
+            self.mock_attachment(
+                self.post,
+                image='somelargeimage.png',
+                thumbnail='somethumb.png',
+            ),
         ]
 
         response = self.client.get(final_link)
@@ -56,7 +60,9 @@ class AttachmentAdminViewsTests(AdminTestCase):
 
         for attachment in attachments:
             delete_link = reverse(
-                'misago:admin:system:attachments:delete', kwargs={'pk': attachment.pk}
+                'misago:admin:system:attachments:delete', kwargs={
+                    'pk': attachment.pk,
+                }
             )
             self.assertContains(response, attachment.filename)
             self.assertContains(response, delete_link)
@@ -72,7 +78,11 @@ class AttachmentAdminViewsTests(AdminTestCase):
         attachments = [
             self.mock_attachment(self.post, file='somefile.pdf'),
             self.mock_attachment(image='someimage.jpg'),
-            self.mock_attachment(self.post, image='somelargeimage.png', thumbnail='somethumb.png'),
+            self.mock_attachment(
+                self.post,
+                image='somelargeimage.png',
+                thumbnail='somethumb.png',
+            ),
         ]
 
         self.post.attachments_cache = [{'id': attachments[-1].pk}]
@@ -80,8 +90,10 @@ class AttachmentAdminViewsTests(AdminTestCase):
 
         response = self.client.post(
             self.admin_link,
-            data={'action': 'delete',
-                  'selected_items': [a.pk for a in attachments]}
+            data={
+                'action': 'delete',
+                'selected_items': [a.pk for a in attachments],
+            }
         )
         self.assertEqual(response.status_code, 302)
 
@@ -94,17 +106,23 @@ class AttachmentAdminViewsTests(AdminTestCase):
     def test_delete_view(self):
         """delete attachment view has no showstoppers"""
         attachment = self.mock_attachment(self.post)
-        self.post.attachments_cache = [{
-            'id': attachment.pk + 1
-        }, {
-            'id': attachment.pk
-        }, {
-            'id': attachment.pk + 2
-        }]
+        self.post.attachments_cache = [
+            {
+                'id': attachment.pk + 1
+            },
+            {
+                'id': attachment.pk
+            },
+            {
+                'id': attachment.pk + 2
+            },
+        ]
         self.post.save()
 
         action_link = reverse(
-            'misago:admin:system:attachments:delete', kwargs={'pk': attachment.pk}
+            'misago:admin:system:attachments:delete', kwargs={
+                'pk': attachment.pk,
+            }
         )
 
         response = self.client.post(action_link)
@@ -119,4 +137,13 @@ class AttachmentAdminViewsTests(AdminTestCase):
 
         # assert it was removed from post's attachments cache
         attachments_cache = self.category.post_set.get(pk=self.post.pk).attachments_cache
-        self.assertEqual(attachments_cache, [{'id': attachment.pk + 1}, {'id': attachment.pk + 2}])
+        self.assertEqual(
+            attachments_cache, [
+                {
+                    'id': attachment.pk + 1,
+                },
+                {
+                    'id': attachment.pk + 2,
+                },
+            ]
+        )

+ 96 - 24
misago/threads/tests/test_attachments_api.py

@@ -55,33 +55,53 @@ class AttachmentsApiTestCase(AuthenticatedUserTestCase):
 
     def test_invalid_extension(self):
         """uploaded file's extension is rejected as invalid"""
-        AttachmentType.objects.create(name="Test extension", extensions='jpg,jpeg', mimetypes=None)
+        AttachmentType.objects.create(
+            name="Test extension",
+            extensions='jpg,jpeg',
+            mimetypes=None,
+        )
 
         with open(TEST_DOCUMENT_PATH, 'rb') as upload:
-            response = self.client.post(self.api_link, data={'upload': upload})
+            response = self.client.post(
+                self.api_link, data={
+                    'upload': upload,
+                }
+            )
         self.assertContains(response, "You can't upload files of this type.", status_code=400)
 
     def test_invalid_mime(self):
         """uploaded file's mimetype is rejected as invalid"""
         AttachmentType.objects.create(
-            name="Test extension", extensions='png', mimetypes='loremipsum'
+            name="Test extension",
+            extensions='png',
+            mimetypes='loremipsum',
         )
 
         with open(TEST_DOCUMENT_PATH, 'rb') as upload:
-            response = self.client.post(self.api_link, data={'upload': upload})
+            response = self.client.post(
+                self.api_link, data={
+                    'upload': upload,
+                }
+            )
         self.assertContains(response, "You can't upload files of this type.", status_code=400)
 
     def test_no_perm_to_type(self):
         """user needs permission to upload files of this type"""
         attachment_type = AttachmentType.objects.create(
-            name="Test extension", extensions='png', mimetypes='application/pdf'
+            name="Test extension",
+            extensions='png',
+            mimetypes='application/pdf',
         )
 
         user_roles = (r.pk for r in self.user.get_roles())
         attachment_type.limit_uploads_to.set(Role.objects.exclude(id__in=user_roles))
 
         with open(TEST_DOCUMENT_PATH, 'rb') as upload:
-            response = self.client.post(self.api_link, data={'upload': upload})
+            response = self.client.post(
+                self.api_link, data={
+                    'upload': upload,
+                }
+            )
         self.assertContains(response, "You can't upload files of this type.", status_code=400)
 
     def test_type_is_locked(self):
@@ -90,11 +110,15 @@ class AttachmentsApiTestCase(AuthenticatedUserTestCase):
             name="Test extension",
             extensions='png',
             mimetypes='application/pdf',
-            status=AttachmentType.LOCKED
+            status=AttachmentType.LOCKED,
         )
 
         with open(TEST_DOCUMENT_PATH, 'rb') as upload:
-            response = self.client.post(self.api_link, data={'upload': upload})
+            response = self.client.post(
+                self.api_link, data={
+                    'upload': upload,
+                }
+            )
         self.assertContains(response, "You can't upload files of this type.", status_code=400)
 
     def test_type_is_disabled(self):
@@ -103,21 +127,32 @@ class AttachmentsApiTestCase(AuthenticatedUserTestCase):
             name="Test extension",
             extensions='png',
             mimetypes='application/pdf',
-            status=AttachmentType.DISABLED
+            status=AttachmentType.DISABLED,
         )
 
         with open(TEST_DOCUMENT_PATH, 'rb') as upload:
-            response = self.client.post(self.api_link, data={'upload': upload})
+            response = self.client.post(
+                self.api_link, data={
+                    'upload': upload,
+                }
+            )
         self.assertContains(response, "You can't upload files of this type.", status_code=400)
 
     def test_upload_too_big_for_type(self):
         """too big uploads are rejected"""
         AttachmentType.objects.create(
-            name="Test extension", extensions='png', mimetypes='image/png', size_limit=100
+            name="Test extension",
+            extensions='png',
+            mimetypes='image/png',
+            size_limit=100,
         )
 
         with open(TEST_LARGEPNG_PATH, 'rb') as upload:
-            response = self.client.post(self.api_link, data={'upload': upload})
+            response = self.client.post(
+                self.api_link, data={
+                    'upload': upload,
+                }
+            )
 
         self.assertContains(
             response, "can't upload files of this type larger than", status_code=400
@@ -128,29 +163,48 @@ class AttachmentsApiTestCase(AuthenticatedUserTestCase):
         self.override_acl({'max_attachment_size': 100})
 
         AttachmentType.objects.create(
-            name="Test extension", extensions='png', mimetypes='image/png'
+            name="Test extension",
+            extensions='png',
+            mimetypes='image/png',
         )
 
         with open(TEST_LARGEPNG_PATH, 'rb') as upload:
-            response = self.client.post(self.api_link, data={'upload': upload})
+            response = self.client.post(
+                self.api_link, data={
+                    'upload': upload,
+                }
+            )
         self.assertContains(response, "can't upload files larger than", status_code=400)
 
     def test_corrupted_image_upload(self):
         """corrupted image upload is handled"""
-        AttachmentType.objects.create(name="Test extension", extensions='gif')
+        AttachmentType.objects.create(
+            name="Test extension",
+            extensions='gif',
+        )
 
         with open(TEST_CORRUPTEDIMG_PATH, 'rb') as upload:
-            response = self.client.post(self.api_link, data={'upload': upload})
+            response = self.client.post(
+                self.api_link, data={
+                    'upload': upload,
+                }
+            )
         self.assertContains(response, "Uploaded image was corrupted or invalid.", status_code=400)
 
     def test_document_upload(self):
         """successful upload creates orphan attachment"""
         AttachmentType.objects.create(
-            name="Test extension", extensions='pdf', mimetypes='application/pdf'
+            name="Test extension",
+            extensions='pdf',
+            mimetypes='application/pdf',
         )
 
         with open(TEST_DOCUMENT_PATH, 'rb') as upload:
-            response = self.client.post(self.api_link, data={'upload': upload})
+            response = self.client.post(
+                self.api_link, data={
+                    'upload': upload,
+                }
+            )
         self.assertEqual(response.status_code, 200)
 
         response_json = response.json()
@@ -181,11 +235,17 @@ class AttachmentsApiTestCase(AuthenticatedUserTestCase):
     def test_small_image_upload(self):
         """successful small image upload creates orphan attachment without thumbnail"""
         AttachmentType.objects.create(
-            name="Test extension", extensions='jpeg,jpg', mimetypes='image/jpeg'
+            name="Test extension",
+            extensions='jpeg,jpg',
+            mimetypes='image/jpeg',
         )
 
         with open(TEST_SMALLJPG_PATH, 'rb') as upload:
-            response = self.client.post(self.api_link, data={'upload': upload})
+            response = self.client.post(
+                self.api_link, data={
+                    'upload': upload,
+                }
+            )
         self.assertEqual(response.status_code, 200)
 
         response_json = response.json()
@@ -212,11 +272,17 @@ class AttachmentsApiTestCase(AuthenticatedUserTestCase):
         self.override_acl({'max_attachment_size': 10 * 1024})
 
         AttachmentType.objects.create(
-            name="Test extension", extensions='png', mimetypes='image/png'
+            name="Test extension",
+            extensions='png',
+            mimetypes='image/png',
         )
 
         with open(TEST_LARGEPNG_PATH, 'rb') as upload:
-            response = self.client.post(self.api_link, data={'upload': upload})
+            response = self.client.post(
+                self.api_link, data={
+                    'upload': upload,
+                }
+            )
         self.assertEqual(response.status_code, 200)
 
         response_json = response.json()
@@ -259,11 +325,17 @@ class AttachmentsApiTestCase(AuthenticatedUserTestCase):
     def test_animated_image_upload(self):
         """successful gif upload creates orphan attachment with thumbnail"""
         AttachmentType.objects.create(
-            name="Test extension", extensions='gif', mimetypes='image/gif'
+            name="Test extension",
+            extensions='gif',
+            mimetypes='image/gif',
         )
 
         with open(TEST_ANIMATEDGIF_PATH, 'rb') as upload:
-            response = self.client.post(self.api_link, data={'upload': upload})
+            response = self.client.post(
+                self.api_link, data={
+                    'upload': upload,
+                }
+            )
         self.assertEqual(response.status_code, 200)
 
         response_json = response.json()

+ 16 - 10
misago/threads/tests/test_attachments_middleware.py

@@ -66,7 +66,7 @@ class AttachmentsMiddlewareTests(AuthenticatedUserTestCase):
                 request=RequestMock(test_input),
                 mode=PostingEndpoint.START,
                 user=self.user,
-                post=self.post
+                post=self.post,
             )
 
             serializer = middleware.get_serializer()
@@ -83,7 +83,7 @@ class AttachmentsMiddlewareTests(AuthenticatedUserTestCase):
                 }),
                 mode=PostingEndpoint.START,
                 user=self.user,
-                post=self.post
+                post=self.post,
             )
 
             serializer = middleware.get_serializer()
@@ -92,7 +92,10 @@ class AttachmentsMiddlewareTests(AuthenticatedUserTestCase):
     def test_get_initial_attachments(self):
         """get_initial_attachments returns list of attachments already existing on post"""
         middleware = AttachmentsMiddleware(
-            request=RequestMock(), mode=PostingEndpoint.EDIT, user=self.user, post=self.post
+            request=RequestMock(),
+            mode=PostingEndpoint.EDIT,
+            user=self.user,
+            post=self.post,
         )
 
         serializer = middleware.get_serializer()
@@ -111,7 +114,10 @@ class AttachmentsMiddlewareTests(AuthenticatedUserTestCase):
     def test_get_new_attachments(self):
         """get_initial_attachments returns list of attachments already existing on post"""
         middleware = AttachmentsMiddleware(
-            request=RequestMock(), mode=PostingEndpoint.EDIT, user=self.user, post=self.post
+            request=RequestMock(),
+            mode=PostingEndpoint.EDIT,
+            user=self.user,
+            post=self.post,
         )
 
         serializer = middleware.get_serializer()
@@ -132,7 +138,7 @@ class AttachmentsMiddlewareTests(AuthenticatedUserTestCase):
         """middleware validates if we have permission to delete other users attachments"""
         self.override_acl({
             'max_attachment_size': 1024,
-            'can_delete_other_users_attachments': False
+            'can_delete_other_users_attachments': False,
         })
 
         attachment = self.mock_attachment(user=False, post=self.post)
@@ -144,7 +150,7 @@ class AttachmentsMiddlewareTests(AuthenticatedUserTestCase):
             }),
             mode=PostingEndpoint.EDIT,
             user=self.user,
-            post=self.post
+            post=self.post,
         ).get_serializer()
 
         self.assertFalse(serializer.is_valid())
@@ -162,7 +168,7 @@ class AttachmentsMiddlewareTests(AuthenticatedUserTestCase):
             }),
             mode=PostingEndpoint.EDIT,
             user=self.user,
-            post=self.post
+            post=self.post,
         )
 
         serializer = middleware.get_serializer()
@@ -190,7 +196,7 @@ class AttachmentsMiddlewareTests(AuthenticatedUserTestCase):
             }),
             mode=PostingEndpoint.EDIT,
             user=self.user,
-            post=self.post
+            post=self.post,
         )
 
         serializer = middleware.get_serializer()
@@ -222,7 +228,7 @@ class AttachmentsMiddlewareTests(AuthenticatedUserTestCase):
             }),
             mode=PostingEndpoint.EDIT,
             user=self.user,
-            post=self.post
+            post=self.post,
         )
 
         serializer = middleware.get_serializer()
@@ -250,7 +256,7 @@ class AttachmentsMiddlewareTests(AuthenticatedUserTestCase):
             }),
             mode=PostingEndpoint.EDIT,
             user=self.user,
-            post=self.post
+            post=self.post,
         )
 
         serializer = middleware.get_serializer()

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

@@ -72,7 +72,9 @@ class AttachmentTypeAdminViewsTests(AdminTestCase):
         self.assertEqual(test_type.name, 'Test type')
 
         form_link = reverse(
-            'misago:admin:system:attachment-types:edit', kwargs={'pk': test_type.pk}
+            'misago:admin:system:attachment-types:edit', kwargs={
+                'pk': test_type.pk,
+            }
         )
 
         response = self.client.get(form_link)
@@ -166,7 +168,9 @@ class AttachmentTypeAdminViewsTests(AdminTestCase):
         self.assertEqual(test_type.name, 'Test type')
 
         action_link = reverse(
-            'misago:admin:system:attachment-types:delete', kwargs={'pk': test_type.pk}
+            'misago:admin:system:attachment-types:delete', kwargs={
+                'pk': test_type.pk,
+            }
         )
 
         response = self.client.post(action_link)
@@ -205,7 +209,9 @@ class AttachmentTypeAdminViewsTests(AdminTestCase):
         )
 
         action_link = reverse(
-            'misago:admin:system:attachment-types:delete', kwargs={'pk': test_type.pk}
+            'misago:admin:system:attachment-types:delete', kwargs={
+                'pk': test_type.pk,
+            }
         )
 
         response = self.client.post(action_link)

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

@@ -27,8 +27,14 @@ class AttachmentViewTestCase(AuthenticatedUserTestCase):
 
         self.api_link = reverse('misago:api:attachment-list')
 
-        self.attachment_type_jpg = AttachmentType.objects.create(name="JPG", extensions='jpeg,jpg')
-        self.attachment_type_pdf = AttachmentType.objects.create(name="PDF", extensions='pdf')
+        self.attachment_type_jpg = AttachmentType.objects.create(
+            name="JPG",
+            extensions='jpeg,jpg',
+        )
+        self.attachment_type_pdf = AttachmentType.objects.create(
+            name="PDF",
+            extensions='pdf',
+        )
 
         self.override_acl()
 
@@ -36,13 +42,17 @@ class AttachmentViewTestCase(AuthenticatedUserTestCase):
         acl = self.user.acl_cache.copy()
         acl.update({
             'max_attachment_size': 1000,
-            'can_download_other_users_attachments': allow_download
+            'can_download_other_users_attachments': allow_download,
         })
         override_acl(self.user, acl)
 
     def upload_document(self, is_orphaned=False, by_other_user=False):
         with open(TEST_DOCUMENT_PATH, 'rb') as upload:
-            response = self.client.post(self.api_link, data={'upload': upload})
+            response = self.client.post(
+                self.api_link, data={
+                    'upload': upload,
+                }
+            )
         self.assertEqual(response.status_code, 200)
 
         attachment = Attachment.objects.order_by('id').last()
@@ -60,7 +70,11 @@ class AttachmentViewTestCase(AuthenticatedUserTestCase):
 
     def upload_image(self):
         with open(TEST_SMALLJPG_PATH, 'rb') as upload:
-            response = self.client.post(self.api_link, data={'upload': upload})
+            response = self.client.post(
+                self.api_link, data={
+                    'upload': upload,
+                }
+            )
         self.assertEqual(response.status_code, 200)
 
         attachment = Attachment.objects.order_by('id').last()
@@ -85,8 +99,10 @@ class AttachmentViewTestCase(AuthenticatedUserTestCase):
     def test_nonexistant_file(self):
         """user tries to retrieve nonexistant file"""
         response = self.client.get(
-            reverse('misago:attachment', kwargs={'pk': 123,
-                                                 'secret': 'qwertyuiop'})
+            reverse('misago:attachment', kwargs={
+                'pk': 123,
+                'secret': 'qwertyuiop',
+            })
         )
 
         self.assertIs404(response)
@@ -96,8 +112,10 @@ class AttachmentViewTestCase(AuthenticatedUserTestCase):
         attachment = self.upload_document()
 
         response = self.client.get(
-            reverse('misago:attachment', kwargs={'pk': attachment.pk,
-                                                 'secret': 'qwertyuiop'})
+            reverse('misago:attachment', kwargs={
+                'pk': attachment.pk,
+                'secret': 'qwertyuiop',
+            })
         )
 
         self.assertIs404(response)
@@ -128,8 +146,10 @@ class AttachmentViewTestCase(AuthenticatedUserTestCase):
         response = self.client.get(
             reverse(
                 'misago:attachment-thumbnail',
-                kwargs={'pk': attachment.pk,
-                        'secret': attachment.secret}
+                kwargs={
+                    'pk': attachment.pk,
+                    'secret': attachment.secret,
+                }
             )
         )
         self.assertIs404(response)

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

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

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

@@ -32,7 +32,7 @@ class EventsAPITests(TestCase):
             starter_slug='tester',
             last_post_on=datetime,
             last_poster_name='Tester',
-            last_poster_slug='tester'
+            last_poster_slug='tester',
         )
 
         self.thread.set_title("Test thread")

+ 14 - 4
misago/threads/tests/test_floodprotection.py

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

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

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

+ 50 - 15
misago/threads/tests/test_post_mentions.py

@@ -24,7 +24,9 @@ class PostMentionsTests(AuthenticatedUserTestCase):
         self.override_acl()
 
         self.post_link = reverse(
-            'misago:api:thread-post-list', kwargs={'thread_pk': self.thread.pk}
+            'misago:api:thread-post-list', kwargs={
+                'thread_pk': self.thread.pk,
+            }
         )
 
     def override_acl(self):
@@ -34,18 +36,26 @@ class PostMentionsTests(AuthenticatedUserTestCase):
             'can_browse': 1,
             'can_start_threads': 1,
             'can_reply_threads': 1,
-            'can_edit_posts': 1
+            'can_edit_posts': 1,
         })
 
         override_acl(self.user, new_acl)
 
-    def put(self, url, data=None):
+    def put(
+            self,
+            url,
+            data=None,
+    ):
         content = encode_multipart(BOUNDARY, data or {})
         return self.client.put(url, content, content_type=MULTIPART_CONTENT)
 
     def test_mention_noone(self):
         """endpoint handles no mentions in post"""
-        response = self.client.post(self.post_link, data={'post': "This is test response!"})
+        response = self.client.post(
+            self.post_link, data={
+                'post': "This is test response!",
+            }
+        )
         self.assertEqual(response.status_code, 200)
 
         post = self.user.post_set.order_by('id').last()
@@ -54,7 +64,9 @@ class PostMentionsTests(AuthenticatedUserTestCase):
     def test_mention_nonexistant(self):
         """endpoint handles nonexistant mention"""
         response = self.client.post(
-            self.post_link, data={'post': "This is test response, @InvalidUser!"}
+            self.post_link, data={
+                'post': "This is test response, @InvalidUser!",
+            }
         )
         self.assertEqual(response.status_code, 200)
 
@@ -64,7 +76,9 @@ class PostMentionsTests(AuthenticatedUserTestCase):
     def test_mention_self(self):
         """endpoint mentions author"""
         response = self.client.post(
-            self.post_link, data={'post': "This is test response, @{}!".format(self.user)}
+            self.post_link, data={
+                'post': "This is test response, @{}!".format(self.user),
+            }
         )
         self.assertEqual(response.status_code, 200)
 
@@ -86,7 +100,9 @@ class PostMentionsTests(AuthenticatedUserTestCase):
         mentions = ['@{}'.format(u) for u in users]
         response = self.client.post(
             self.post_link,
-            data={'post': "This is test response, {}!".format(', '.join(mentions))}
+            data={
+                'post': "This is test response, {}!".format(', '.join(mentions)),
+            }
         )
         self.assertEqual(response.status_code, 200)
 
@@ -101,7 +117,9 @@ class PostMentionsTests(AuthenticatedUserTestCase):
         user_b = UserModel.objects.create_user('MentionB', 'mentionb@test.com', 'pass123')
 
         response = self.client.post(
-            self.post_link, data={'post': "This is test response, @{}!".format(user_a)}
+            self.post_link, data={
+                'post': "This is test response, @{}!".format(user_a),
+            }
         )
         self.assertEqual(response.status_code, 200)
 
@@ -112,13 +130,18 @@ class PostMentionsTests(AuthenticatedUserTestCase):
 
         # add mention to post
         edit_link = reverse(
-            'misago:api:thread-post-detail', kwargs={'thread_pk': self.thread.pk,
-                                                     'pk': post.pk}
+            'misago:api:thread-post-detail', kwargs={
+                'thread_pk': self.thread.pk,
+                'pk': post.pk,
+            }
         )
 
         self.override_acl()
         response = self.put(
-            edit_link, data={'post': "This is test response, @{} and @{}!".format(user_a, user_b)}
+            edit_link,
+            data={
+                'post': "This is test response, @{} and @{}!".format(user_a, user_b),
+            }
         )
         self.assertEqual(response.status_code, 200)
 
@@ -127,7 +150,11 @@ class PostMentionsTests(AuthenticatedUserTestCase):
 
         # remove first mention from post - should preserve mentions
         self.override_acl()
-        response = self.put(edit_link, data={'post': "This is test response, @{}!".format(user_b)})
+        response = self.put(
+            edit_link, data={
+                'post': "This is test response, @{}!".format(user_b),
+            }
+        )
         self.assertEqual(response.status_code, 200)
 
         self.assertEqual(post.mentions.count(), 2)
@@ -135,7 +162,11 @@ class PostMentionsTests(AuthenticatedUserTestCase):
 
         # remove mentions from post - should preserve mentions
         self.override_acl()
-        response = self.put(edit_link, data={'post': "This is test response!"})
+        response = self.put(
+            edit_link, data={
+                'post': "This is test response!",
+            }
+        )
         self.assertEqual(response.status_code, 200)
 
         self.assertEqual(post.mentions.count(), 2)
@@ -147,7 +178,9 @@ class PostMentionsTests(AuthenticatedUserTestCase):
         user_b = UserModel.objects.create_user('MentionB', 'mentionb@test.com', 'pass123')
 
         response = self.client.post(
-            self.post_link, data={'post': "This is test response, @{}!".format(user_a)}
+            self.post_link, data={
+                'post': "This is test response, @{}!".format(user_a),
+            }
         )
         self.assertEqual(response.status_code, 200)
 
@@ -162,7 +195,9 @@ class PostMentionsTests(AuthenticatedUserTestCase):
 
         response = self.client.post(
             self.post_link,
-            data={'post': "This is test response, @{} and @{}!".format(user_a, user_b)}
+            data={
+                'post': "This is test response, @{} and @{}!".format(user_a, user_b),
+            }
         )
         self.assertEqual(response.status_code, 200)
 

+ 5 - 5
misago/threads/tests/test_post_model.py

@@ -26,7 +26,7 @@ class PostModelTests(TestCase):
             starter_slug='tester',
             last_post_on=datetime,
             last_poster_name='Tester',
-            last_poster_slug='tester'
+            last_poster_slug='tester',
         )
 
         self.thread.set_title("Test thread")
@@ -42,7 +42,7 @@ class PostModelTests(TestCase):
             parsed="<p>Hello! I am test message!</p>",
             checksum="nope",
             posted_on=datetime,
-            updated_on=datetime
+            updated_on=datetime,
         )
 
         update_post_checksum(self.post)
@@ -67,7 +67,7 @@ class PostModelTests(TestCase):
             starter_slug='tester',
             last_post_on=timezone.now(),
             last_poster_name='Tester',
-            last_poster_slug='tester'
+            last_poster_slug='tester',
         )
 
         # can't merge with other users posts
@@ -134,7 +134,7 @@ class PostModelTests(TestCase):
             parsed="<p>I am other message!</p>",
             checksum="nope",
             posted_on=timezone.now() + timedelta(minutes=5),
-            updated_on=timezone.now() + timedelta(minutes=5)
+            updated_on=timezone.now() + timedelta(minutes=5),
         )
 
         other_post.merge(self.post)
@@ -152,7 +152,7 @@ class PostModelTests(TestCase):
             starter_slug='tester',
             last_post_on=timezone.now(),
             last_poster_name='Tester',
-            last_poster_slug='tester'
+            last_poster_slug='tester',
         )
 
         self.post.move(new_thread)

+ 228 - 152
misago/threads/tests/test_privatethread_patch_api.py

@@ -34,11 +34,13 @@ class PrivateThreadAddParticipantApiTests(PrivateThreadPatchApiTestCase):
         ThreadParticipant.objects.add_participants(self.thread, [self.user])
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'add',
-                'path': 'participants',
-                'value': self.user.username
-            }]
+            self.api_link, [
+                {
+                    'op': 'add',
+                    'path': 'participants',
+                    'value': self.user.username,
+                },
+            ]
         )
 
         self.assertContains(
@@ -49,7 +51,15 @@ class PrivateThreadAddParticipantApiTests(PrivateThreadPatchApiTestCase):
         """path validates username"""
         ThreadParticipant.objects.set_owner(self.thread, self.user)
 
-        response = self.patch(self.api_link, [{'op': 'add', 'path': 'participants', 'value': ''}])
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'add',
+                    'path': 'participants',
+                    'value': '',
+                },
+            ]
+        )
 
         self.assertContains(
             response, "You have to enter new participant's username.", status_code=400
@@ -60,11 +70,13 @@ class PrivateThreadAddParticipantApiTests(PrivateThreadPatchApiTestCase):
         ThreadParticipant.objects.set_owner(self.thread, self.user)
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'add',
-                'path': 'participants',
-                'value': 'InvalidUser'
-            }]
+            self.api_link, [
+                {
+                    'op': 'add',
+                    'path': 'participants',
+                    'value': 'InvalidUser',
+                },
+            ]
         )
 
         self.assertContains(response, "No user with such name exists.", status_code=400)
@@ -74,11 +86,13 @@ class PrivateThreadAddParticipantApiTests(PrivateThreadPatchApiTestCase):
         ThreadParticipant.objects.set_owner(self.thread, self.user)
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'add',
-                'path': 'participants',
-                'value': self.user.username
-            }]
+            self.api_link, [
+                {
+                    'op': 'add',
+                    'path': 'participants',
+                    'value': self.user.username,
+                },
+            ]
         )
 
         self.assertContains(response, "This user is already thread participant", status_code=400)
@@ -89,11 +103,13 @@ class PrivateThreadAddParticipantApiTests(PrivateThreadPatchApiTestCase):
         self.other_user.blocks.add(self.user)
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'add',
-                'path': 'participants',
-                'value': self.other_user.username
-            }]
+            self.api_link, [
+                {
+                    'op': 'add',
+                    'path': 'participants',
+                    'value': self.other_user.username,
+                },
+            ]
         )
 
         self.assertContains(response, "BobBoberson is blocking you.", status_code=400)
@@ -105,11 +121,13 @@ class PrivateThreadAddParticipantApiTests(PrivateThreadPatchApiTestCase):
         override_acl(self.other_user, {'can_use_private_threads': 0})
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'add',
-                'path': 'participants',
-                'value': self.other_user.username
-            }]
+            self.api_link, [
+                {
+                    'op': 'add',
+                    'path': 'participants',
+                    'value': self.other_user.username,
+                },
+            ]
         )
 
         self.assertContains(response, "BobBoberson can't participate", status_code=400)
@@ -125,11 +143,13 @@ class PrivateThreadAddParticipantApiTests(PrivateThreadPatchApiTestCase):
             ThreadParticipant.objects.add_participants(self.thread, [user])
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'add',
-                'path': 'participants',
-                'value': self.other_user.username
-            }]
+            self.api_link, [
+                {
+                    'op': 'add',
+                    'path': 'participants',
+                    'value': self.other_user.username,
+                },
+            ]
         )
 
         self.assertContains(
@@ -144,11 +164,13 @@ class PrivateThreadAddParticipantApiTests(PrivateThreadPatchApiTestCase):
         self.thread.save()
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'add',
-                'path': 'participants',
-                'value': self.other_user.username
-            }]
+            self.api_link, [
+                {
+                    'op': 'add',
+                    'path': 'participants',
+                    'value': self.other_user.username,
+                },
+            ]
         )
 
         self.assertContains(
@@ -160,11 +182,13 @@ class PrivateThreadAddParticipantApiTests(PrivateThreadPatchApiTestCase):
         ThreadParticipant.objects.set_owner(self.thread, self.user)
 
         self.patch(
-            self.api_link, [{
-                'op': 'add',
-                'path': 'participants',
-                'value': self.other_user.username
-            }]
+            self.api_link, [
+                {
+                    'op': 'add',
+                    'path': 'participants',
+                    'value': self.other_user.username,
+                },
+            ]
         )
 
         # event was set on thread
@@ -189,11 +213,13 @@ class PrivateThreadAddParticipantApiTests(PrivateThreadPatchApiTestCase):
         override_acl(self.user, {'can_moderate_private_threads': 1})
 
         self.patch(
-            self.api_link, [{
-                'op': 'add',
-                'path': 'participants',
-                'value': self.user.username
-            }]
+            self.api_link, [
+                {
+                    'op': 'add',
+                    'path': 'participants',
+                    'value': self.user.username,
+                },
+            ]
         )
 
         # event was set on thread
@@ -214,11 +240,13 @@ class PrivateThreadAddParticipantApiTests(PrivateThreadPatchApiTestCase):
         override_acl(self.user, {'can_moderate_private_threads': 1})
 
         self.patch(
-            self.api_link, [{
-                'op': 'add',
-                'path': 'participants',
-                'value': self.other_user.username
-            }]
+            self.api_link, [
+                {
+                    'op': 'add',
+                    'path': 'participants',
+                    'value': self.other_user.username,
+                },
+            ]
         )
 
         # event was set on thread
@@ -240,11 +268,13 @@ class PrivateThreadRemoveParticipantApiTests(PrivateThreadPatchApiTestCase):
         ThreadParticipant.objects.set_owner(self.thread, self.user)
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'remove',
-                'path': 'participants',
-                'value': 'string'
-            }]
+            self.api_link, [
+                {
+                    'op': 'remove',
+                    'path': 'participants',
+                    'value': 'string',
+                },
+            ]
         )
 
         self.assertContains(response, "Participant doesn't exist.", status_code=400)
@@ -254,11 +284,13 @@ class PrivateThreadRemoveParticipantApiTests(PrivateThreadPatchApiTestCase):
         ThreadParticipant.objects.set_owner(self.thread, self.user)
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'remove',
-                'path': 'participants',
-                'value': 'string'
-            }]
+            self.api_link, [
+                {
+                    'op': 'remove',
+                    'path': 'participants',
+                    'value': 'string',
+                },
+            ]
         )
 
         self.assertContains(response, "Participant doesn't exist.", status_code=400)
@@ -268,11 +300,13 @@ class PrivateThreadRemoveParticipantApiTests(PrivateThreadPatchApiTestCase):
         ThreadParticipant.objects.set_owner(self.thread, self.user)
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'remove',
-                'path': 'participants',
-                'value': self.other_user.pk
-            }]
+            self.api_link, [
+                {
+                    'op': 'remove',
+                    'path': 'participants',
+                    'value': self.other_user.pk,
+                },
+            ]
         )
 
         self.assertContains(response, "Participant doesn't exist.", status_code=400)
@@ -283,11 +317,13 @@ class PrivateThreadRemoveParticipantApiTests(PrivateThreadPatchApiTestCase):
         ThreadParticipant.objects.add_participants(self.thread, [self.user])
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'remove',
-                'path': 'participants',
-                'value': self.other_user.pk
-            }]
+            self.api_link, [
+                {
+                    'op': 'remove',
+                    'path': 'participants',
+                    'value': self.other_user.pk,
+                },
+            ]
         )
 
         self.assertContains(
@@ -303,11 +339,13 @@ class PrivateThreadRemoveParticipantApiTests(PrivateThreadPatchApiTestCase):
         self.thread.save()
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'remove',
-                'path': 'participants',
-                'value': self.other_user.pk
-            }]
+            self.api_link, [
+                {
+                    'op': 'remove',
+                    'path': 'participants',
+                    'value': self.other_user.pk,
+                },
+            ]
         )
 
         self.assertContains(
@@ -325,11 +363,13 @@ class PrivateThreadRemoveParticipantApiTests(PrivateThreadPatchApiTestCase):
         )
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'remove',
-                'path': 'participants',
-                'value': self.user.pk
-            }]
+            self.api_link, [
+                {
+                    'op': 'remove',
+                    'path': 'participants',
+                    'value': self.user.pk,
+                },
+            ]
         )
 
         self.assertEqual(response.status_code, 200)
@@ -363,11 +403,13 @@ class PrivateThreadRemoveParticipantApiTests(PrivateThreadPatchApiTestCase):
         self.thread.save()
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'remove',
-                'path': 'participants',
-                'value': self.user.pk
-            }]
+            self.api_link, [
+                {
+                    'op': 'remove',
+                    'path': 'participants',
+                    'value': self.user.pk,
+                },
+            ]
         )
 
         self.assertEqual(response.status_code, 200)
@@ -399,11 +441,13 @@ class PrivateThreadRemoveParticipantApiTests(PrivateThreadPatchApiTestCase):
         override_acl(self.user, {'can_moderate_private_threads': True})
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'remove',
-                'path': 'participants',
-                'value': removed_user.pk
-            }]
+            self.api_link, [
+                {
+                    'op': 'remove',
+                    'path': 'participants',
+                    'value': removed_user.pk,
+                },
+            ]
         )
 
         self.assertEqual(response.status_code, 200)
@@ -432,11 +476,13 @@ class PrivateThreadRemoveParticipantApiTests(PrivateThreadPatchApiTestCase):
         ThreadParticipant.objects.add_participants(self.thread, [self.other_user])
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'remove',
-                'path': 'participants',
-                'value': self.other_user.pk
-            }]
+            self.api_link, [
+                {
+                    'op': 'remove',
+                    'path': 'participants',
+                    'value': self.other_user.pk,
+                },
+            ]
         )
 
         self.assertEqual(response.status_code, 200)
@@ -464,11 +510,13 @@ class PrivateThreadRemoveParticipantApiTests(PrivateThreadPatchApiTestCase):
         ThreadParticipant.objects.add_participants(self.thread, [self.other_user])
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'remove',
-                'path': 'participants',
-                'value': self.user.pk
-            }]
+            self.api_link, [
+                {
+                    'op': 'remove',
+                    'path': 'participants',
+                    'value': self.user.pk,
+                },
+            ]
         )
 
         self.assertEqual(response.status_code, 200)
@@ -495,11 +543,13 @@ class PrivateThreadRemoveParticipantApiTests(PrivateThreadPatchApiTestCase):
         ThreadParticipant.objects.set_owner(self.thread, self.user)
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'remove',
-                'path': 'participants',
-                'value': self.user.pk
-            }]
+            self.api_link, [
+                {
+                    'op': 'remove',
+                    'path': 'participants',
+                    'value': self.user.pk,
+                },
+            ]
         )
 
         self.assertEqual(response.status_code, 200)
@@ -518,7 +568,15 @@ class PrivateThreadTakeOverApiTests(PrivateThreadPatchApiTestCase):
         """api handles empty user id"""
         ThreadParticipant.objects.set_owner(self.thread, self.user)
 
-        response = self.patch(self.api_link, [{'op': 'replace', 'path': 'owner', 'value': ''}])
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'owner',
+                    'value': '',
+                },
+            ]
+        )
 
         self.assertContains(response, "Participant doesn't exist.", status_code=400)
 
@@ -527,11 +585,13 @@ class PrivateThreadTakeOverApiTests(PrivateThreadPatchApiTestCase):
         ThreadParticipant.objects.set_owner(self.thread, self.user)
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'replace',
-                'path': 'owner',
-                'value': 'dsadsa'
-            }]
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'owner',
+                    'value': 'dsadsa',
+                },
+            ]
         )
 
         self.assertContains(response, "Participant doesn't exist.", status_code=400)
@@ -541,11 +601,13 @@ class PrivateThreadTakeOverApiTests(PrivateThreadPatchApiTestCase):
         ThreadParticipant.objects.set_owner(self.thread, self.user)
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'replace',
-                'path': 'owner',
-                'value': self.other_user.pk
-            }]
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'owner',
+                    'value': self.other_user.pk,
+                },
+            ]
         )
 
         self.assertContains(response, "Participant doesn't exist.", status_code=400)
@@ -556,11 +618,13 @@ class PrivateThreadTakeOverApiTests(PrivateThreadPatchApiTestCase):
         ThreadParticipant.objects.add_participants(self.thread, [self.user])
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'replace',
-                'path': 'owner',
-                'value': self.user.pk
-            }]
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'owner',
+                    'value': self.user.pk,
+                },
+            ]
         )
 
         self.assertContains(
@@ -573,11 +637,13 @@ class PrivateThreadTakeOverApiTests(PrivateThreadPatchApiTestCase):
         ThreadParticipant.objects.add_participants(self.thread, [self.other_user])
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'replace',
-                'path': 'owner',
-                'value': self.user.pk
-            }]
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'owner',
+                    'value': self.user.pk,
+                },
+            ]
         )
 
         self.assertContains(response, "This user already is thread owner.", status_code=400)
@@ -591,11 +657,13 @@ class PrivateThreadTakeOverApiTests(PrivateThreadPatchApiTestCase):
         self.thread.save()
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'replace',
-                'path': 'owner',
-                'value': self.other_user.pk
-            }]
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'owner',
+                    'value': self.other_user.pk,
+                },
+            ]
         )
 
         self.assertContains(
@@ -608,11 +676,13 @@ class PrivateThreadTakeOverApiTests(PrivateThreadPatchApiTestCase):
         ThreadParticipant.objects.add_participants(self.thread, [self.other_user])
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'replace',
-                'path': 'owner',
-                'value': self.other_user.pk
-            }]
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'owner',
+                    'value': self.other_user.pk,
+                },
+            ]
         )
 
         self.assertEqual(response.status_code, 200)
@@ -641,11 +711,13 @@ class PrivateThreadTakeOverApiTests(PrivateThreadPatchApiTestCase):
         override_acl(self.user, {'can_moderate_private_threads': 1})
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'replace',
-                'path': 'owner',
-                'value': new_owner.pk
-            }]
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'owner',
+                    'value': new_owner.pk,
+                },
+            ]
         )
 
         self.assertEqual(response.status_code, 200)
@@ -674,11 +746,13 @@ class PrivateThreadTakeOverApiTests(PrivateThreadPatchApiTestCase):
         override_acl(self.user, {'can_moderate_private_threads': 1})
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'replace',
-                'path': 'owner',
-                'value': self.user.pk
-            }]
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'owner',
+                    'value': self.user.pk,
+                },
+            ]
         )
 
         self.assertEqual(response.status_code, 200)
@@ -708,11 +782,13 @@ class PrivateThreadTakeOverApiTests(PrivateThreadPatchApiTestCase):
         override_acl(self.user, {'can_moderate_private_threads': 1})
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'replace',
-                'path': 'owner',
-                'value': self.user.pk
-            }]
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'owner',
+                    'value': self.user.pk,
+                },
+            ]
         )
 
         self.assertEqual(response.status_code, 200)

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

@@ -25,7 +25,11 @@ class PrivateThreadReplyApiTestCase(PrivateThreadsTestCase):
         ThreadParticipant.objects.set_owner(self.thread, self.user)
         ThreadParticipant.objects.add_participants(self.thread, [self.other_user])
 
-        response = self.client.post(self.api_link, data={'post': "This is test response!"})
+        response = self.client.post(
+            self.api_link, data={
+                'post': "This is test response!",
+            }
+        )
         self.assertEqual(response.status_code, 200)
 
         # don't count private thread replies

+ 44 - 21
misago/threads/tests/test_privatethread_start_api.py

@@ -56,7 +56,7 @@ class StartPrivateThreadTests(AuthenticatedUserTestCase):
             response.json(), {
                 'to': ["You have to enter user names."],
                 'title': ["You have to enter thread title."],
-                'post': ["You have to enter a message."]
+                'post': ["You have to enter a message."],
             }
         )
 
@@ -73,7 +73,9 @@ class StartPrivateThreadTests(AuthenticatedUserTestCase):
 
         self.assertEqual(response.status_code, 400)
         self.assertEqual(
-            response.json(), {'title': ["Thread title should contain alpha-numeric characters."]}
+            response.json(), {
+                'title': ["Thread title should contain alpha-numeric characters."],
+            }
         )
 
     def test_post_is_validated(self):
@@ -89,8 +91,9 @@ class StartPrivateThreadTests(AuthenticatedUserTestCase):
 
         self.assertEqual(response.status_code, 400)
         self.assertEqual(
-            response.json(),
-            {'post': ["Posted message should be at least 5 characters long (it has 1)."]}
+            response.json(), {
+                'post': ["Posted message should be at least 5 characters long (it has 1)."],
+            }
         )
 
     def test_cant_invite_self(self):
@@ -106,8 +109,9 @@ class StartPrivateThreadTests(AuthenticatedUserTestCase):
 
         self.assertEqual(response.status_code, 400)
         self.assertEqual(
-            response.json(),
-            {'to': ["You can't include yourself on the list of users to invite to new thread."]}
+            response.json(), {
+                'to': ["You can't include yourself on the list of users to invite to new thread."],
+            }
         )
 
     def test_cant_invite_nonexisting(self):
@@ -122,7 +126,11 @@ class StartPrivateThreadTests(AuthenticatedUserTestCase):
         )
 
         self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {'to': ["One or more users could not be found: ab, cd"]})
+        self.assertEqual(
+            response.json(), {
+                'to': ["One or more users could not be found: ab, cd"],
+            }
+        )
 
     def test_cant_invite_too_many(self):
         """api validates that you cant invite too many users to thread"""
@@ -137,8 +145,9 @@ class StartPrivateThreadTests(AuthenticatedUserTestCase):
 
         self.assertEqual(response.status_code, 400)
         self.assertEqual(
-            response.json(),
-            {'to': ["You can't add more than 3 users to private thread (you've added 50)."]}
+            response.json(), {
+                'to': ["You can't add more than 3 users to private thread (you've added 50)."],
+            }
         )
 
     def test_cant_invite_no_permission(self):
@@ -156,7 +165,9 @@ class StartPrivateThreadTests(AuthenticatedUserTestCase):
 
         self.assertEqual(response.status_code, 400)
         self.assertEqual(
-            response.json(), {'to': ["BobBoberson can't participate in private threads."]}
+            response.json(), {
+                'to': ["BobBoberson can't participate in private threads."],
+            }
         )
 
     def test_cant_invite_blocking(self):
@@ -173,7 +184,9 @@ class StartPrivateThreadTests(AuthenticatedUserTestCase):
         )
 
         self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {'to': ["BobBoberson is blocking you."]})
+        self.assertEqual(response.json(), {
+            'to': ["BobBoberson is blocking you."],
+        })
 
         # allow us to bypass blocked check
         override_acl(self.user, {'can_add_everyone_to_private_threads': 1})
@@ -189,7 +202,9 @@ class StartPrivateThreadTests(AuthenticatedUserTestCase):
 
         self.assertEqual(response.status_code, 400)
         self.assertEqual(
-            response.json(), {'title': ["Thread title should contain alpha-numeric characters."]}
+            response.json(), {
+                'title': ["Thread title should contain alpha-numeric characters."],
+            }
         )
 
     def test_cant_invite_followers_only(self):
@@ -209,8 +224,9 @@ class StartPrivateThreadTests(AuthenticatedUserTestCase):
 
         self.assertEqual(response.status_code, 400)
         self.assertEqual(
-            response.json(),
-            {'to': ["BobBoberson limits invitations to private threads to followed users."]}
+            response.json(), {
+                'to': ["BobBoberson limits invitations to private threads to followed users."],
+            }
         )
 
         # allow us to bypass following check
@@ -227,7 +243,9 @@ class StartPrivateThreadTests(AuthenticatedUserTestCase):
 
         self.assertEqual(response.status_code, 400)
         self.assertEqual(
-            response.json(), {'title': ["Thread title should contain alpha-numeric characters."]}
+            response.json(), {
+                'title': ["Thread title should contain alpha-numeric characters."],
+            }
         )
 
         # make user follow us
@@ -245,7 +263,9 @@ class StartPrivateThreadTests(AuthenticatedUserTestCase):
 
         self.assertEqual(response.status_code, 400)
         self.assertEqual(
-            response.json(), {'title': ["Thread title should contain alpha-numeric characters."]}
+            response.json(), {
+                'title': ["Thread title should contain alpha-numeric characters."],
+            }
         )
 
     def test_cant_invite_anyone(self):
@@ -265,8 +285,9 @@ class StartPrivateThreadTests(AuthenticatedUserTestCase):
 
         self.assertEqual(response.status_code, 400)
         self.assertEqual(
-            response.json(),
-            {'to': ["BobBoberson is not allowing invitations to private threads."]}
+            response.json(), {
+                'to': ["BobBoberson is not allowing invitations to private threads."],
+            }
         )
 
         # allow us to bypass user preference check
@@ -283,7 +304,9 @@ class StartPrivateThreadTests(AuthenticatedUserTestCase):
 
         self.assertEqual(response.status_code, 400)
         self.assertEqual(
-            response.json(), {'title': ["Thread title should contain alpha-numeric characters."]}
+            response.json(), {
+                'title': ["Thread title should contain alpha-numeric characters."],
+            }
         )
 
     def test_can_start_thread(self):
@@ -293,7 +316,7 @@ class StartPrivateThreadTests(AuthenticatedUserTestCase):
             data={
                 'to': [self.other_user.username],
                 'title': "Hello, I am test thread!",
-                'post': "Lorem ipsum dolor met!"
+                'post': "Lorem ipsum dolor met!",
             }
         )
         self.assertEqual(response.status_code, 200)
@@ -361,7 +384,7 @@ class StartPrivateThreadTests(AuthenticatedUserTestCase):
             data={
                 'to': [self.other_user.username],
                 'title': "Brzęczyżczykiewicz",
-                'post': "Chrzążczyżewoszyce, powiat Łękółody."
+                'post': "Chrzążczyżewoszyce, powiat Łękółody.",
             }
         )
         self.assertEqual(response.status_code, 200)

+ 11 - 3
misago/threads/tests/test_privatethreads.py

@@ -8,7 +8,11 @@ class PrivateThreadsTestCase(AuthenticatedUserTestCase):
         super(PrivateThreadsTestCase, self).setUp()
         self.category = Category.objects.private_threads()
 
-        override_acl(self.user, {'can_use_private_threads': 1, 'can_start_private_threads': 1})
+        override_acl(self.user, {
+            'can_use_private_threads': 1,
+            'can_start_private_threads': 1,
+        })
+
         self.override_acl()
 
     def override_acl(self, acl=None):
@@ -23,10 +27,14 @@ class PrivateThreadsTestCase(AuthenticatedUserTestCase):
             'can_edit_posts': 0,
             'can_hide_posts': 0,
             'can_hide_own_posts': 0,
-            'can_merge_threads': 0
+            'can_merge_threads': 0,
         })
 
         if acl:
             final_acl.update(acl)
 
-        override_acl(self.user, {'categories': {self.category.pk: final_acl}})
+        override_acl(self.user, {
+            'categories': {
+                self.category.pk: final_acl,
+            },
+        })

+ 18 - 14
misago/threads/tests/test_privatethreads_api.py

@@ -118,13 +118,15 @@ class PrivateThreadRetrieveApiTests(PrivateThreadsTestCase):
         response_json = response.json()
         self.assertEqual(response_json['title'], self.thread.title)
         self.assertEqual(
-            response_json['participants'], [{
-                'id': self.user.id,
-                'username': self.user.username,
-                'avatars': self.user.avatars,
-                'url': self.user.get_absolute_url(),
-                'is_owner': True
-            }]
+            response_json['participants'], [
+                {
+                    'id': self.user.id,
+                    'username': self.user.username,
+                    'avatars': self.user.avatars,
+                    'url': self.user.get_absolute_url(),
+                    'is_owner': True,
+                },
+            ]
         )
 
     def test_can_see_participant(self):
@@ -137,13 +139,15 @@ class PrivateThreadRetrieveApiTests(PrivateThreadsTestCase):
         response_json = response.json()
         self.assertEqual(response_json['title'], self.thread.title)
         self.assertEqual(
-            response_json['participants'], [{
-                'id': self.user.id,
-                'username': self.user.username,
-                'avatars': self.user.avatars,
-                'url': self.user.get_absolute_url(),
-                'is_owner': False
-            }]
+            response_json['participants'], [
+                {
+                    'id': self.user.id,
+                    'username': self.user.username,
+                    'avatars': self.user.avatars,
+                    'url': self.user.get_absolute_url(),
+                    'is_owner': False,
+                },
+            ]
         )
 
     def test_mod_can_see_reported(self):

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

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

+ 32 - 10
misago/threads/tests/test_subscription_middleware.py

@@ -26,7 +26,7 @@ class SubscriptionMiddlewareTestCase(AuthenticatedUserTestCase):
             'can_see': 1,
             'can_browse': 1,
             'can_start_threads': 1,
-            'can_reply_threads': 1
+            'can_reply_threads': 1,
         })
 
         override_acl(self.user, new_acl)
@@ -48,7 +48,7 @@ class SubscribeStartedThreadTests(SubscriptionMiddlewareTestCase):
             data={
                 'category': self.category.id,
                 'title': "This is an test thread!",
-                'post': "This is test response!"
+                'post': "This is test response!",
             }
         )
         self.assertEqual(response.status_code, 200)
@@ -66,7 +66,7 @@ class SubscribeStartedThreadTests(SubscriptionMiddlewareTestCase):
             data={
                 'category': self.category.id,
                 'title': "This is an test thread!",
-                'post': "This is test response!"
+                'post': "This is test response!",
             }
         )
         self.assertEqual(response.status_code, 200)
@@ -88,7 +88,7 @@ class SubscribeStartedThreadTests(SubscriptionMiddlewareTestCase):
             data={
                 'category': self.category.id,
                 'title': "This is an test thread!",
-                'post': "This is test response!"
+                'post': "This is test response!",
             }
         )
         self.assertEqual(response.status_code, 200)
@@ -106,7 +106,9 @@ class SubscribeRepliedThreadTests(SubscriptionMiddlewareTestCase):
         super(SubscribeRepliedThreadTests, self).setUp()
         self.thread = testutils.post_thread(self.category)
         self.api_link = reverse(
-            'misago:api:thread-post-list', kwargs={'thread_pk': self.thread.pk}
+            'misago:api:thread-post-list', kwargs={
+                'thread_pk': self.thread.pk,
+            }
         )
 
     def test_dont_subscribe(self):
@@ -115,7 +117,11 @@ class SubscribeRepliedThreadTests(SubscriptionMiddlewareTestCase):
         self.user.subscribe_to_replied_threads = UserModel.SUBSCRIBE_NONE
         self.user.save()
 
-        response = self.client.post(self.api_link, data={'post': "This is test response!"})
+        response = self.client.post(
+            self.api_link, data={
+                'post': "This is test response!",
+            }
+        )
         self.assertEqual(response.status_code, 200)
 
         # user has no subscriptions
@@ -126,7 +132,11 @@ class SubscribeRepliedThreadTests(SubscriptionMiddlewareTestCase):
         self.user.subscribe_to_replied_threads = UserModel.SUBSCRIBE_NOTIFY
         self.user.save()
 
-        response = self.client.post(self.api_link, data={'post': "This is test response!"})
+        response = self.client.post(
+            self.api_link, data={
+                'post': "This is test response!",
+            }
+        )
         self.assertEqual(response.status_code, 200)
 
         # user has subscribed to thread
@@ -140,7 +150,11 @@ class SubscribeRepliedThreadTests(SubscriptionMiddlewareTestCase):
         self.user.subscribe_to_replied_threads = UserModel.SUBSCRIBE_ALL
         self.user.save()
 
-        response = self.client.post(self.api_link, data={'post': "This is test response!"})
+        response = self.client.post(
+            self.api_link, data={
+                'post': "This is test response!",
+            }
+        )
         self.assertEqual(response.status_code, 200)
 
         # user has subscribed to thread
@@ -154,13 +168,21 @@ class SubscribeRepliedThreadTests(SubscriptionMiddlewareTestCase):
         self.user.subscribe_to_replied_threads = UserModel.SUBSCRIBE_ALL
         self.user.save()
 
-        response = self.client.post(self.api_link, data={'post': "This is test response!"})
+        response = self.client.post(
+            self.api_link, data={
+                'post': "This is test response!",
+            }
+        )
         self.assertEqual(response.status_code, 200)
 
         # clear subscription
         self.user.subscription_set.all().delete()
         # reply again
-        response = self.client.post(self.api_link, data={'post': "This is test response!"})
+        response = self.client.post(
+            self.api_link, data={
+                'post': "This is test response!",
+            }
+        )
         self.assertEqual(response.status_code, 200)
 
         # user has no subscriptions

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

@@ -22,7 +22,10 @@ class SubscriptionsTests(TestCase):
         self.anon = AnonymousUser()
 
     def post_thread(self, datetime):
-        return testutils.post_thread(category=self.category, started_on=datetime)
+        return testutils.post_thread(
+            category=self.category,
+            started_on=datetime,
+        )
 
     def test_anon_subscription(self):
         """make single thread sub aware for anon"""

+ 32 - 11
misago/threads/tests/test_thread_editreply_api.py

@@ -20,8 +20,10 @@ class EditReplyTests(AuthenticatedUserTestCase):
 
         self.api_link = reverse(
             'misago:api:thread-post-detail',
-            kwargs={'thread_pk': self.thread.pk,
-                    'pk': self.post.pk}
+            kwargs={
+                'thread_pk': self.thread.pk,
+                'pk': self.post.pk,
+            }
         )
 
     def override_acl(self, extra_acl=None):
@@ -31,7 +33,7 @@ class EditReplyTests(AuthenticatedUserTestCase):
             'can_browse': 1,
             'can_start_threads': 0,
             'can_reply_threads': 0,
-            'can_edit_posts': 1
+            'can_edit_posts': 1,
         })
 
         if extra_acl:
@@ -144,7 +146,9 @@ class EditReplyTests(AuthenticatedUserTestCase):
         response = self.put(self.api_link, data={})
 
         self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {'post': ["You have to enter a message."]})
+        self.assertEqual(response.json(), {
+            'post': ["You have to enter a message."],
+        })
 
     def test_edit_event(self):
         """events can't be edited"""
@@ -169,8 +173,9 @@ class EditReplyTests(AuthenticatedUserTestCase):
 
         self.assertEqual(response.status_code, 400)
         self.assertEqual(
-            response.json(),
-            {'post': ["Posted message should be at least 5 characters long (it has 1)."]}
+            response.json(), {
+                'post': ["Posted message should be at least 5 characters long (it has 1)."],
+            }
         )
 
     def test_edit_reply_no_change(self):
@@ -234,8 +239,10 @@ class EditReplyTests(AuthenticatedUserTestCase):
 
         api_link = reverse(
             'misago:api:thread-post-detail',
-            kwargs={'thread_pk': self.thread.pk,
-                    'pk': self.thread.first_post.pk}
+            kwargs={
+                'thread_pk': self.thread.pk,
+                'pk': self.thread.first_post.pk,
+            }
         )
 
         response = self.put(api_link, data={'post': "This is test edit!"})
@@ -245,7 +252,12 @@ class EditReplyTests(AuthenticatedUserTestCase):
         """can protect post"""
         self.override_acl({'can_protect_posts': 1})
 
-        response = self.put(self.api_link, data={'post': "Lorem ipsum dolor met!", 'protect': 1})
+        response = self.put(
+            self.api_link, data={
+                'post': "Lorem ipsum dolor met!",
+                'protect': 1,
+            }
+        )
         self.assertEqual(response.status_code, 200)
 
         post = self.user.post_set.order_by('id').last()
@@ -255,7 +267,12 @@ class EditReplyTests(AuthenticatedUserTestCase):
         """cant protect post without permission"""
         self.override_acl({'can_protect_posts': 0})
 
-        response = self.put(self.api_link, data={'post': "Lorem ipsum dolor met!", 'protect': 1})
+        response = self.put(
+            self.api_link, data={
+                'post': "Lorem ipsum dolor met!",
+                'protect': 1,
+            }
+        )
         self.assertEqual(response.status_code, 200)
 
         post = self.user.post_set.order_by('id').last()
@@ -265,5 +282,9 @@ class EditReplyTests(AuthenticatedUserTestCase):
         """unicode characters can be posted"""
         self.override_acl()
 
-        response = self.put(self.api_link, data={'post': "Chrzążczyżewoszyce, powiat Łękółody."})
+        response = self.put(
+            self.api_link, data={
+                'post': "Chrzążczyżewoszyce, powiat Łękółody.",
+            }
+        )
         self.assertEqual(response.status_code, 200)

+ 76 - 24
misago/threads/tests/test_thread_merge_api.py

@@ -16,11 +16,17 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
             name='Category B',
             slug='category-b',
         ).insert_at(
-            self.category, position='last-child', save=True
+            self.category,
+            position='last-child',
+            save=True,
         )
         self.category_b = Category.objects.get(slug='category-b')
 
-        self.api_link = reverse('misago:api:thread-merge', kwargs={'pk': self.thread.pk})
+        self.api_link = reverse(
+            'misago:api:thread-merge', kwargs={
+                'pk': self.thread.pk,
+            }
+        )
 
     def override_other_acl(self, acl=None):
         other_category_acl = self.user.acl_cache['categories'][self.category.pk].copy()
@@ -34,7 +40,7 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
             'can_edit_posts': 0,
             'can_hide_posts': 0,
             'can_hide_own_posts': 0,
-            'can_merge_threads': 0
+            'can_merge_threads': 0,
         })
 
         if acl:
@@ -76,14 +82,20 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
         """api validates thread url"""
         self.override_acl({'can_merge_threads': 1})
 
-        response = self.client.post(self.api_link, {'thread_url': self.user.get_absolute_url()})
+        response = self.client.post(self.api_link, {
+            'thread_url': self.user.get_absolute_url(),
+        })
         self.assertContains(response, "This is not a valid thread link.", status_code=400)
 
     def test_current_thread_url(self):
         """api validates if thread url given is to current thread"""
         self.override_acl({'can_merge_threads': 1})
 
-        response = self.client.post(self.api_link, {'thread_url': self.thread.get_absolute_url()})
+        response = self.client.post(
+            self.api_link, {
+                'thread_url': self.thread.get_absolute_url(),
+            }
+        )
         self.assertContains(response, "You can't merge thread with itself.", status_code=400)
 
     def test_other_thread_exists(self):
@@ -96,7 +108,9 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
         other_thread_url = other_thread.get_absolute_url()
         other_thread.delete()
 
-        response = self.client.post(self.api_link, {'thread_url': other_thread_url})
+        response = self.client.post(self.api_link, {
+            'thread_url': other_thread_url,
+        })
         self.assertContains(
             response, "The thread you have entered link to doesn't exist", status_code=400
         )
@@ -109,7 +123,11 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
 
         other_thread = testutils.post_thread(self.category_b)
 
-        response = self.client.post(self.api_link, {'thread_url': other_thread.get_absolute_url()})
+        response = self.client.post(
+            self.api_link, {
+                'thread_url': other_thread.get_absolute_url(),
+            }
+        )
         self.assertContains(
             response, "The thread you have entered link to doesn't exist", status_code=400
         )
@@ -122,7 +140,11 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
 
         other_thread = testutils.post_thread(self.category_b)
 
-        response = self.client.post(self.api_link, {'thread_url': other_thread.get_absolute_url()})
+        response = self.client.post(
+            self.api_link, {
+                'thread_url': other_thread.get_absolute_url(),
+            }
+        )
         self.assertContains(
             response, "You don't have permission to merge this thread", status_code=400
         )
@@ -135,7 +157,11 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
 
         other_thread = testutils.post_thread(self.category_b)
 
-        response = self.client.post(self.api_link, {'thread_url': other_thread.get_absolute_url()})
+        response = self.client.post(
+            self.api_link, {
+                'thread_url': other_thread.get_absolute_url(),
+            }
+        )
         self.assertContains(
             response, "You can't merge this thread into thread you can't reply.", status_code=400
         )
@@ -148,7 +174,11 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
 
         other_thread = testutils.post_thread(self.category_b)
 
-        response = self.client.post(self.api_link, {'thread_url': other_thread.get_absolute_url()})
+        response = self.client.post(
+            self.api_link, {
+                'thread_url': other_thread.get_absolute_url(),
+            }
+        )
         self.assertContains(response, other_thread.get_absolute_url(), status_code=200)
 
         # other thread has two posts now
@@ -167,7 +197,11 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
         other_thread = testutils.post_thread(self.category_b)
         poll = testutils.post_poll(other_thread, self.user)
 
-        response = self.client.post(self.api_link, {'thread_url': other_thread.get_absolute_url()})
+        response = self.client.post(
+            self.api_link, {
+                'thread_url': other_thread.get_absolute_url(),
+            }
+        )
         self.assertContains(response, other_thread.get_absolute_url(), status_code=200)
 
         # other thread has two posts now
@@ -190,7 +224,11 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
         other_thread = testutils.post_thread(self.category_b)
         poll = testutils.post_poll(self.thread, self.user)
 
-        response = self.client.post(self.api_link, {'thread_url': other_thread.get_absolute_url()})
+        response = self.client.post(
+            self.api_link, {
+                'thread_url': other_thread.get_absolute_url(),
+            }
+        )
         self.assertContains(response, other_thread.get_absolute_url(), status_code=200)
 
         # other thread has two posts now
@@ -214,13 +252,19 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
         poll = testutils.post_poll(self.thread, self.user)
         other_poll = testutils.post_poll(other_thread, self.user)
 
-        response = self.client.post(self.api_link, {'thread_url': other_thread.get_absolute_url()})
+        response = self.client.post(
+            self.api_link, {
+                'thread_url': other_thread.get_absolute_url(),
+            }
+        )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(
             response.json(), {
-                'polls': [[0, "Delete all polls"],
-                          [poll.pk, poll.question],
-                          [other_poll.pk, other_poll.question]]
+                'polls': [
+                    [0, "Delete all polls"],
+                    [poll.pk, poll.question],
+                    [other_poll.pk, other_poll.question],
+                ]
             }
         )
 
@@ -240,8 +284,10 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
         testutils.post_poll(other_thread, self.user)
 
         response = self.client.post(
-            self.api_link, {'thread_url': other_thread.get_absolute_url(),
-                            'poll': 'jhdkajshdsak'}
+            self.api_link, {
+                'thread_url': other_thread.get_absolute_url(),
+                'poll': 'jhdkajshdsak',
+            }
         )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.json(), {'detail': "Invalid choice."})
@@ -261,8 +307,10 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
         testutils.post_poll(other_thread, self.user)
 
         response = self.client.post(
-            self.api_link, {'thread_url': other_thread.get_absolute_url(),
-                            'poll': 0}
+            self.api_link, {
+                'thread_url': other_thread.get_absolute_url(),
+                'poll': 0,
+            }
         )
         self.assertContains(response, other_thread.get_absolute_url(), status_code=200)
 
@@ -288,8 +336,10 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
         other_poll = testutils.post_poll(other_thread, self.user)
 
         response = self.client.post(
-            self.api_link, {'thread_url': other_thread.get_absolute_url(),
-                            'poll': poll.pk}
+            self.api_link, {
+                'thread_url': other_thread.get_absolute_url(),
+                'poll': poll.pk,
+            }
         )
         self.assertContains(response, other_thread.get_absolute_url(), status_code=200)
 
@@ -322,8 +372,10 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
         other_poll = testutils.post_poll(other_thread, self.user)
 
         response = self.client.post(
-            self.api_link, {'thread_url': other_thread.get_absolute_url(),
-                            'poll': other_poll.pk}
+            self.api_link, {
+                'thread_url': other_thread.get_absolute_url(),
+                'poll': other_poll.pk,
+            }
         )
         self.assertContains(response, other_thread.get_absolute_url(), status_code=200)
 

+ 15 - 13
misago/threads/tests/test_thread_model.py

@@ -23,7 +23,7 @@ class ThreadModelTests(TestCase):
             starter_slug='tester',
             last_post_on=datetime,
             last_poster_name='Tester',
-            last_poster_slug='tester'
+            last_poster_slug='tester',
         )
 
         self.thread.set_title("Test thread")
@@ -38,7 +38,7 @@ class ThreadModelTests(TestCase):
             parsed="<p>Hello! I am test message!</p>",
             checksum="nope",
             posted_on=datetime,
-            updated_on=datetime
+            updated_on=datetime,
         )
 
         self.thread.first_post = post
@@ -62,7 +62,7 @@ class ThreadModelTests(TestCase):
             parsed="<p>Hello! I am test message!</p>",
             checksum="nope",
             posted_on=datetime,
-            updated_on=datetime
+            updated_on=datetime,
         )
 
         # first sync call, updates last thread
@@ -90,7 +90,7 @@ class ThreadModelTests(TestCase):
             checksum="nope",
             posted_on=datetime + timedelta(5),
             updated_on=datetime + timedelta(5),
-            is_unapproved=True
+            is_unapproved=True,
         )
 
         self.thread.synchronize()
@@ -116,7 +116,7 @@ class ThreadModelTests(TestCase):
             checksum="nope",
             posted_on=datetime + timedelta(10),
             updated_on=datetime + timedelta(10),
-            is_hidden=True
+            is_hidden=True,
         )
 
         self.thread.synchronize()
@@ -174,7 +174,7 @@ class ThreadModelTests(TestCase):
             checksum="nope",
             posted_on=datetime + timedelta(10),
             updated_on=datetime + timedelta(10),
-            is_event=True
+            is_event=True,
         )
 
         self.thread.synchronize()
@@ -202,7 +202,7 @@ class ThreadModelTests(TestCase):
             parsed="<p>Hello! I am test message!</p>",
             checksum="nope",
             posted_on=datetime,
-            updated_on=datetime
+            updated_on=datetime,
         )
 
         self.thread.synchronize()
@@ -225,7 +225,7 @@ class ThreadModelTests(TestCase):
             poster_name='test',
             poster_slug='test',
             poster_ip='127.0.0.1',
-            choices=[]
+            choices=[],
         )
 
         self.thread.synchronize()
@@ -247,7 +247,7 @@ class ThreadModelTests(TestCase):
             parsed="<p>Hello! I am test message!</p>",
             checksum="nope",
             posted_on=datetime,
-            updated_on=datetime
+            updated_on=datetime,
         )
 
         self.thread.set_first_post(post)
@@ -273,7 +273,7 @@ class ThreadModelTests(TestCase):
             parsed="<p>Hello! I am test message!</p>",
             checksum="nope",
             posted_on=datetime,
-            updated_on=datetime
+            updated_on=datetime,
         )
 
         self.thread.set_last_post(post)
@@ -291,7 +291,9 @@ class ThreadModelTests(TestCase):
             name='New Category',
             slug='new-category',
         ).insert_at(
-            root_category, position='last-child', save=True
+            root_category,
+            position='last-child',
+            save=True,
         )
         new_category = Category.objects.get(slug='new-category')
 
@@ -315,7 +317,7 @@ class ThreadModelTests(TestCase):
             starter_slug='tester',
             last_post_on=datetime,
             last_poster_name='Tester',
-            last_poster_slug='tester'
+            last_poster_slug='tester',
         )
 
         other_thread.set_title("Other thread")
@@ -330,7 +332,7 @@ class ThreadModelTests(TestCase):
             parsed="<p>Hello! I am other message!</p>",
             checksum="nope",
             posted_on=datetime,
-            updated_on=datetime
+            updated_on=datetime,
         )
 
         other_thread.first_post = post

+ 265 - 133
misago/threads/tests/test_thread_patch_api.py

@@ -17,7 +17,13 @@ class ThreadPatchApiTestCase(ThreadsApiTestCase):
 class ThreadAddAclApiTests(ThreadPatchApiTestCase):
     def test_add_acl_true(self):
         """api adds current thread's acl to response"""
-        response = self.patch(self.api_link, [{'op': 'add', 'path': 'acl', 'value': True}])
+        response = self.patch(self.api_link, [
+            {
+                'op': 'add',
+                'path': 'acl',
+                'value': True,
+            },
+        ])
         self.assertEqual(response.status_code, 200)
 
         response_json = response.json()
@@ -25,7 +31,13 @@ class ThreadAddAclApiTests(ThreadPatchApiTestCase):
 
     def test_add_acl_false(self):
         """if value is false, api won't add acl to the response, but will set empty key"""
-        response = self.patch(self.api_link, [{'op': 'add', 'path': 'acl', 'value': False}])
+        response = self.patch(self.api_link, [
+            {
+                'op': 'add',
+                'path': 'acl',
+                'value': False,
+            },
+        ])
         self.assertEqual(response.status_code, 200)
 
         response_json = response.json()
@@ -38,11 +50,13 @@ class ThreadChangeTitleApiTests(ThreadPatchApiTestCase):
         self.override_acl({'can_edit_threads': 2})
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'replace',
-                'path': 'title',
-                'value': "Lorem ipsum change!"
-            }]
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'title',
+                    'value': "Lorem ipsum change!",
+                },
+            ]
         )
         self.assertEqual(response.status_code, 200)
 
@@ -54,11 +68,13 @@ class ThreadChangeTitleApiTests(ThreadPatchApiTestCase):
         self.override_acl({'can_edit_threads': 0})
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'replace',
-                'path': 'title',
-                'value': "Lorem ipsum change!"
-            }]
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'title',
+                    'value': "Lorem ipsum change!",
+                },
+            ]
         )
         self.assertEqual(response.status_code, 400)
 
@@ -74,11 +90,13 @@ class ThreadChangeTitleApiTests(ThreadPatchApiTestCase):
         self.thread.save()
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'replace',
-                'path': 'title',
-                'value': "Lorem ipsum change!"
-            }]
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'title',
+                    'value': "Lorem ipsum change!",
+                },
+            ]
         )
         self.assertEqual(response.status_code, 400)
 
@@ -91,7 +109,15 @@ class ThreadChangeTitleApiTests(ThreadPatchApiTestCase):
         """api cleans, validates and rejects too short title"""
         self.override_acl({'can_edit_threads': 2})
 
-        response = self.patch(self.api_link, [{'op': 'replace', 'path': 'title', 'value': 12}])
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'title',
+                    'value': 12,
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 400)
 
         response_json = response.json()
@@ -106,7 +132,15 @@ class ThreadPinGloballyApiTests(ThreadPatchApiTestCase):
         """api makes it possible to pin globally thread"""
         self.override_acl({'can_pin_threads': 2})
 
-        response = self.patch(self.api_link, [{'op': 'replace', 'path': 'weight', 'value': 2}])
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'weight',
+                    'value': 2,
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 200)
 
         thread_json = self.get_thread_json()
@@ -122,7 +156,15 @@ class ThreadPinGloballyApiTests(ThreadPatchApiTestCase):
 
         self.override_acl({'can_pin_threads': 2})
 
-        response = self.patch(self.api_link, [{'op': 'replace', 'path': 'weight', 'value': 0}])
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'weight',
+                    'value': 0,
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 200)
 
         thread_json = self.get_thread_json()
@@ -132,7 +174,15 @@ class ThreadPinGloballyApiTests(ThreadPatchApiTestCase):
         """api pin thread globally with no permission fails"""
         self.override_acl({'can_pin_threads': 1})
 
-        response = self.patch(self.api_link, [{'op': 'replace', 'path': 'weight', 'value': 2}])
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'weight',
+                    'value': 2,
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 400)
 
         response_json = response.json()
@@ -153,7 +203,15 @@ class ThreadPinGloballyApiTests(ThreadPatchApiTestCase):
 
         self.override_acl({'can_pin_threads': 1})
 
-        response = self.patch(self.api_link, [{'op': 'replace', 'path': 'weight', 'value': 1}])
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'weight',
+                    'value': 1,
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 400)
 
         response_json = response.json()
@@ -170,7 +228,15 @@ class ThreadPinLocallyApiTests(ThreadPatchApiTestCase):
         """api makes it possible to pin locally thread"""
         self.override_acl({'can_pin_threads': 1})
 
-        response = self.patch(self.api_link, [{'op': 'replace', 'path': 'weight', 'value': 1}])
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'weight',
+                    'value': 1,
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 200)
 
         thread_json = self.get_thread_json()
@@ -186,7 +252,15 @@ class ThreadPinLocallyApiTests(ThreadPatchApiTestCase):
 
         self.override_acl({'can_pin_threads': 1})
 
-        response = self.patch(self.api_link, [{'op': 'replace', 'path': 'weight', 'value': 0}])
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'weight',
+                    'value': 0,
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 200)
 
         thread_json = self.get_thread_json()
@@ -196,7 +270,15 @@ class ThreadPinLocallyApiTests(ThreadPatchApiTestCase):
         """api pin thread locally with no permission fails"""
         self.override_acl({'can_pin_threads': 0})
 
-        response = self.patch(self.api_link, [{'op': 'replace', 'path': 'weight', 'value': 1}])
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'weight',
+                    'value': 1,
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 400)
 
         response_json = response.json()
@@ -217,7 +299,15 @@ class ThreadPinLocallyApiTests(ThreadPatchApiTestCase):
 
         self.override_acl({'can_pin_threads': 0})
 
-        response = self.patch(self.api_link, [{'op': 'replace', 'path': 'weight', 'value': 0}])
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'weight',
+                    'value': 0,
+                },
+            ]
+        )
         self.assertEqual(response.status_code, 400)
 
         response_json = response.json()
@@ -237,7 +327,9 @@ class ThreadMoveApiTests(ThreadPatchApiTestCase):
             name='Category B',
             slug='category-b',
         ).insert_at(
-            self.category, position='last-child', save=True
+            self.category,
+            position='last-child',
+            save=True,
         )
         self.category_b = Category.objects.get(slug='category-b')
 
@@ -277,17 +369,17 @@ class ThreadMoveApiTests(ThreadPatchApiTestCase):
                 {
                     'op': 'replace',
                     'path': 'category',
-                    'value': self.category_b.pk
+                    'value': self.category_b.pk,
                 },
                 {
                     'op': 'add',
                     'path': 'top-category',
-                    'value': self.category_b.pk
+                    'value': self.category_b.pk,
                 },
                 {
                     'op': 'replace',
                     'path': 'flatten-categories',
-                    'value': None
+                    'value': None,
                 },
             ]
         )
@@ -312,7 +404,7 @@ class ThreadMoveApiTests(ThreadPatchApiTestCase):
                 {
                     'op': 'replace',
                     'path': 'category',
-                    'value': self.category_b.pk
+                    'value': self.category_b.pk,
                 },
                 {
                     'op': 'add',
@@ -322,7 +414,7 @@ class ThreadMoveApiTests(ThreadPatchApiTestCase):
                 {
                     'op': 'replace',
                     'path': 'flatten-categories',
-                    'value': None
+                    'value': None,
                 },
             ]
         )
@@ -343,11 +435,13 @@ class ThreadMoveApiTests(ThreadPatchApiTestCase):
         self.override_other_acl({})
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'replace',
-                'path': 'category',
-                'value': self.category_b.pk
-            }]
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'category',
+                    'value': self.category_b.pk,
+                },
+            ]
         )
         self.assertEqual(response.status_code, 400)
 
@@ -367,11 +461,13 @@ class ThreadMoveApiTests(ThreadPatchApiTestCase):
         self.override_other_acl({'can_see': False})
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'replace',
-                'path': 'category',
-                'value': self.category_b.pk
-            }]
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'category',
+                    'value': self.category_b.pk,
+                },
+            ]
         )
         self.assertEqual(response.status_code, 400)
 
@@ -389,11 +485,13 @@ class ThreadMoveApiTests(ThreadPatchApiTestCase):
         self.override_other_acl({'can_browse': False})
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'replace',
-                'path': 'category',
-                'value': self.category_b.pk
-            }]
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'category',
+                    'value': self.category_b.pk,
+                },
+            ]
         )
         self.assertEqual(response.status_code, 400)
 
@@ -414,11 +512,13 @@ class ThreadMoveApiTests(ThreadPatchApiTestCase):
         self.override_other_acl({'can_start_threads': 2})
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'replace',
-                'path': 'category',
-                'value': self.thread.category_id
-            }]
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'category',
+                    'value': self.thread.category_id,
+                },
+            ]
         )
         self.assertEqual(response.status_code, 400)
 
@@ -435,11 +535,13 @@ class ThreadMoveApiTests(ThreadPatchApiTestCase):
     def test_thread_flatten_categories(self):
         """api flatten thread categories"""
         response = self.patch(
-            self.api_link, [{
-                'op': 'replace',
-                'path': 'flatten-categories',
-                'value': None
-            }]
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'flatten-categories',
+                    'value': None,
+                },
+            ]
         )
         self.assertEqual(response.status_code, 200)
 
@@ -463,7 +565,7 @@ class ThreadMoveApiTests(ThreadPatchApiTestCase):
                 {
                     'op': 'replace',
                     'path': 'flatten-categories',
-                    'value': None
+                    'value': None,
                 },
             ]
         )
@@ -480,11 +582,13 @@ class ThreadCloseApiTests(ThreadPatchApiTestCase):
         self.override_acl({'can_close_threads': True})
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'replace',
-                'path': 'is-closed',
-                'value': True
-            }]
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-closed',
+                    'value': True,
+                },
+            ]
         )
         self.assertEqual(response.status_code, 200)
 
@@ -502,11 +606,13 @@ class ThreadCloseApiTests(ThreadPatchApiTestCase):
         self.override_acl({'can_close_threads': True})
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'replace',
-                'path': 'is-closed',
-                'value': False
-            }]
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-closed',
+                    'value': False,
+                },
+            ]
         )
         self.assertEqual(response.status_code, 200)
 
@@ -518,11 +624,13 @@ class ThreadCloseApiTests(ThreadPatchApiTestCase):
         self.override_acl({'can_close_threads': False})
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'replace',
-                'path': 'is-closed',
-                'value': True
-            }]
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-closed',
+                    'value': True,
+                },
+            ]
         )
         self.assertEqual(response.status_code, 400)
 
@@ -545,11 +653,13 @@ class ThreadCloseApiTests(ThreadPatchApiTestCase):
         self.override_acl({'can_close_threads': False})
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'replace',
-                'path': 'is-closed',
-                'value': False
-            }]
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-closed',
+                    'value': False,
+                },
+            ]
         )
         self.assertEqual(response.status_code, 400)
 
@@ -571,11 +681,13 @@ class ThreadApproveApiTests(ThreadPatchApiTestCase):
         self.override_acl({'can_approve_content': 1})
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'replace',
-                'path': 'is-unapproved',
-                'value': False
-            }]
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-unapproved',
+                    'value': False,
+                },
+            ]
         )
         self.assertEqual(response.status_code, 200)
 
@@ -587,11 +699,13 @@ class ThreadApproveApiTests(ThreadPatchApiTestCase):
         self.override_acl({'can_approve_content': 1})
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'replace',
-                'path': 'is-unapproved',
-                'value': True
-            }]
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-unapproved',
+                    'value': True,
+                },
+            ]
         )
         self.assertEqual(response.status_code, 400)
 
@@ -605,11 +719,13 @@ class ThreadHideApiTests(ThreadPatchApiTestCase):
         self.override_acl({'can_hide_threads': 1})
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'replace',
-                'path': 'is-hidden',
-                'value': True
-            }]
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-hidden',
+                    'value': True,
+                },
+            ]
         )
         self.assertEqual(response.status_code, 200)
 
@@ -631,11 +747,13 @@ class ThreadHideApiTests(ThreadPatchApiTestCase):
         self.override_acl({'can_hide_threads': 1})
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'replace',
-                'path': 'is-hidden',
-                'value': False
-            }]
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-hidden',
+                    'value': False,
+                },
+            ]
         )
         self.assertEqual(response.status_code, 200)
 
@@ -649,11 +767,13 @@ class ThreadHideApiTests(ThreadPatchApiTestCase):
         self.override_acl({'can_hide_threads': 0})
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'replace',
-                'path': 'is-hidden',
-                'value': True
-            }]
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-hidden',
+                    'value': True,
+                },
+            ]
         )
         self.assertEqual(response.status_code, 400)
 
@@ -678,11 +798,13 @@ class ThreadHideApiTests(ThreadPatchApiTestCase):
         self.override_acl({'can_hide_threads': 0})
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'replace',
-                'path': 'is-hidden',
-                'value': False
-            }]
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-hidden',
+                    'value': False,
+                },
+            ]
         )
         self.assertEqual(response.status_code, 404)
 
@@ -691,11 +813,13 @@ class ThreadSubscribeApiTests(ThreadPatchApiTestCase):
     def test_subscribe_thread(self):
         """api makes it possible to subscribe thread"""
         response = self.patch(
-            self.api_link, [{
-                'op': 'replace',
-                'path': 'subscription',
-                'value': 'notify'
-            }]
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'subscription',
+                    'value': 'notify',
+                },
+            ]
         )
 
         self.assertEqual(response.status_code, 200)
@@ -709,11 +833,13 @@ class ThreadSubscribeApiTests(ThreadPatchApiTestCase):
     def test_subscribe_thread_with_email(self):
         """api makes it possible to subscribe thread with emails"""
         response = self.patch(
-            self.api_link, [{
-                'op': 'replace',
-                'path': 'subscription',
-                'value': 'email'
-            }]
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'subscription',
+                    'value': 'email',
+                },
+            ]
         )
 
         self.assertEqual(response.status_code, 200)
@@ -727,11 +853,13 @@ class ThreadSubscribeApiTests(ThreadPatchApiTestCase):
     def test_unsubscribe_thread(self):
         """api makes it possible to unsubscribe thread"""
         response = self.patch(
-            self.api_link, [{
-                'op': 'replace',
-                'path': 'subscription',
-                'value': 'remove'
-            }]
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'subscription',
+                    'value': 'remove',
+                },
+            ]
         )
 
         self.assertEqual(response.status_code, 200)
@@ -746,11 +874,13 @@ class ThreadSubscribeApiTests(ThreadPatchApiTestCase):
         self.logout_user()
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'replace',
-                'path': 'subscription',
-                'value': 'email'
-            }]
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'subscription',
+                    'value': 'email',
+                },
+            ]
         )
 
         self.assertEqual(response.status_code, 403)
@@ -762,11 +892,13 @@ class ThreadSubscribeApiTests(ThreadPatchApiTestCase):
         )
 
         response = self.patch(
-            bad_api_link, [{
-                'op': 'replace',
-                'path': 'subscription',
-                'value': 'email'
-            }]
+            bad_api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'subscription',
+                    'value': 'email',
+                },
+            ]
         )
 
         self.assertEqual(response.status_code, 404)

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

@@ -17,7 +17,9 @@ class ThreadPollApiTestCase(AuthenticatedUserTestCase):
         self.override_acl()
 
         self.api_link = reverse(
-            'misago:api:thread-poll-list', kwargs={'thread_pk': self.thread.pk}
+            'misago:api:thread-poll-list', kwargs={
+                'thread_pk': self.thread.pk,
+            }
         )
 
     def post(self, url, data=None):
@@ -39,7 +41,7 @@ class ThreadPollApiTestCase(AuthenticatedUserTestCase):
             'can_edit_polls': 1,
             'can_delete_polls': 1,
             'poll_edit_time': 0,
-            'can_always_see_poll_voters': 0
+            'can_always_see_poll_voters': 0,
         })
 
         if user:
@@ -54,6 +56,8 @@ class ThreadPollApiTestCase(AuthenticatedUserTestCase):
 
         self.api_link = reverse(
             'misago:api:thread-poll-detail',
-            kwargs={'thread_pk': self.thread.pk,
-                    'pk': self.poll.pk}
+            kwargs={
+                'thread_pk': self.thread.pk,
+                'pk': self.poll.pk,
+            }
         )

+ 74 - 32
misago/threads/tests/test_thread_pollcreate_api.py

@@ -16,14 +16,22 @@ class ThreadPollCreateTests(ThreadPollApiTestCase):
 
     def test_invalid_thread_id(self):
         """api validates that thread id is integer"""
-        api_link = reverse('misago:api:thread-poll-list', kwargs={'thread_pk': 'kjha6dsa687sa'})
+        api_link = reverse(
+            'misago:api:thread-poll-list', kwargs={
+                'thread_pk': 'kjha6dsa687sa',
+            }
+        )
 
         response = self.post(api_link)
         self.assertEqual(response.status_code, 404)
 
     def test_nonexistant_thread_id(self):
         """api validates that thread exists"""
-        api_link = reverse('misago:api:thread-poll-list', kwargs={'thread_pk': self.thread.pk + 1})
+        api_link = reverse(
+            'misago:api:thread-poll-list', kwargs={
+                'thread_pk': self.thread.pk + 1,
+            }
+        )
 
         response = self.post(api_link)
         self.assertEqual(response.status_code, 404)
@@ -90,10 +98,12 @@ class ThreadPollCreateTests(ThreadPollApiTestCase):
             poster_ip='127.0.0.1',
             length=30,
             question='Test',
-            choices=[{
-                'hash': 't3st'
-            }],
-            allowed_choices=1
+            choices=[
+                {
+                    'hash': 't3st'
+                },
+            ],
+            allowed_choices=1,
         )
 
         response = self.post(self.api_link)
@@ -109,7 +119,11 @@ class ThreadPollCreateTests(ThreadPollApiTestCase):
 
     def test_length_validation(self):
         """api validates poll's length"""
-        response = self.post(self.api_link, data={'length': -1})
+        response = self.post(
+            self.api_link, data={
+                'length': -1,
+            }
+        )
         self.assertEqual(response.status_code, 400)
 
         response_json = response.json()
@@ -117,7 +131,11 @@ class ThreadPollCreateTests(ThreadPollApiTestCase):
             response_json['length'], ["Ensure this value is greater than or equal to 0."]
         )
 
-        response = self.post(self.api_link, data={'length': 200})
+        response = self.post(
+            self.api_link, data={
+                'length': 200,
+            }
+        )
         self.assertEqual(response.status_code, 400)
 
         response_json = response.json()
@@ -127,7 +145,11 @@ class ThreadPollCreateTests(ThreadPollApiTestCase):
 
     def test_question_validation(self):
         """api validates question length"""
-        response = self.post(self.api_link, data={'question': 'abcd' * 255})
+        response = self.post(
+            self.api_link, data={
+                'question': 'abcd' * 255,
+            }
+        )
         self.assertEqual(response.status_code, 400)
 
         response_json = response.json()
@@ -138,10 +160,14 @@ class ThreadPollCreateTests(ThreadPollApiTestCase):
     def test_validate_choice_length(self):
         """api validates single choice length"""
         response = self.post(
-            self.api_link, data={'choices': [{
-                'hash': 'qwertyuiopas',
-                'label': ''
-            }]}
+            self.api_link, data={
+                'choices': [
+                    {
+                        'hash': 'qwertyuiopas',
+                        'label': '',
+                    },
+                ],
+            }
         )
         self.assertEqual(response.status_code, 400)
 
@@ -149,10 +175,15 @@ class ThreadPollCreateTests(ThreadPollApiTestCase):
         self.assertEqual(response_json['choices'], ["One or more poll choices are invalid."])
 
         response = self.post(
-            self.api_link, data={'choices': [{
-                'hash': 'qwertyuiopas',
-                'label': 'abcd' * 255
-            }]}
+            self.api_link,
+            data={
+                'choices': [
+                    {
+                        'hash': 'qwertyuiopas',
+                        'label': 'abcd' * 255,
+                    },
+                ],
+            }
         )
         self.assertEqual(response.status_code, 400)
 
@@ -172,9 +203,13 @@ class ThreadPollCreateTests(ThreadPollApiTestCase):
     def test_validate_max_choices(self):
         """api validates that there are no more choices in poll than allowed number"""
         response = self.post(
-            self.api_link, data={'choices': [{
-                'label': 'Choice'
-            }] * (MAX_POLL_OPTIONS + 1)}
+            self.api_link, data={
+                'choices': [
+                    {
+                        'label': 'Choice',
+                    },
+                ] * (MAX_POLL_OPTIONS + 1),
+            }
         )
         self.assertEqual(response.status_code, 400)
 
@@ -202,11 +237,14 @@ class ThreadPollCreateTests(ThreadPollApiTestCase):
                 'length': 0,
                 'question': "Lorem ipsum",
                 'allowed_choices': 3,
-                'choices': [{
-                    'label': 'Choice'
-                }, {
-                    'label': 'Choice'
-                }]
+                'choices': [
+                    {
+                        'label': 'Choice',
+                    },
+                    {
+                        'label': 'Choice',
+                    },
+                ],
             }
         )
         self.assertEqual(response.status_code, 400)
@@ -227,13 +265,17 @@ class ThreadPollCreateTests(ThreadPollApiTestCase):
                 'allowed_choices': 2,
                 'allow_revotes': True,
                 'is_public': True,
-                'choices': [{
-                    'label': '\nRed '
-                }, {
-                    'label': 'Green'
-                }, {
-                    'label': 'Blue'
-                }]
+                'choices': [
+                    {
+                        'label': '\nRed ',
+                    },
+                    {
+                        'label': 'Green',
+                    },
+                    {
+                        'label': 'Blue',
+                    },
+                ],
             }
         )
         self.assertEqual(response.status_code, 200)

+ 16 - 8
misago/threads/tests/test_thread_polldelete_api.py

@@ -25,8 +25,10 @@ class ThreadPollDeleteTests(ThreadPollApiTestCase):
         """api validates that thread id is integer"""
         api_link = reverse(
             'misago:api:thread-poll-detail',
-            kwargs={'thread_pk': 'kjha6dsa687sa',
-                    'pk': self.poll.pk}
+            kwargs={
+                'thread_pk': 'kjha6dsa687sa',
+                'pk': self.poll.pk,
+            }
         )
 
         response = self.client.delete(api_link)
@@ -36,8 +38,10 @@ class ThreadPollDeleteTests(ThreadPollApiTestCase):
         """api validates that thread exists"""
         api_link = reverse(
             'misago:api:thread-poll-detail',
-            kwargs={'thread_pk': self.thread.pk + 1,
-                    'pk': self.poll.pk}
+            kwargs={
+                'thread_pk': self.thread.pk + 1,
+                'pk': self.poll.pk,
+            }
         )
 
         response = self.client.delete(api_link)
@@ -47,8 +51,10 @@ class ThreadPollDeleteTests(ThreadPollApiTestCase):
         """api validates that poll id is integer"""
         api_link = reverse(
             'misago:api:thread-poll-detail',
-            kwargs={'thread_pk': self.thread.pk,
-                    'pk': 'sad98as7d97sa98'}
+            kwargs={
+                'thread_pk': self.thread.pk,
+                'pk': 'sad98as7d97sa98',
+            }
         )
 
         response = self.client.delete(api_link)
@@ -58,8 +64,10 @@ class ThreadPollDeleteTests(ThreadPollApiTestCase):
         """api validates that poll exists"""
         api_link = reverse(
             'misago:api:thread-poll-detail',
-            kwargs={'thread_pk': self.thread.pk,
-                    'pk': self.poll.pk + 123}
+            kwargs={
+                'thread_pk': self.thread.pk,
+                'pk': self.poll.pk + 123,
+            }
         )
 
         response = self.client.delete(api_link)

+ 171 - 107
misago/threads/tests/test_thread_polledit_api.py

@@ -25,8 +25,10 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
         """api validates that thread id is integer"""
         api_link = reverse(
             'misago:api:thread-poll-detail',
-            kwargs={'thread_pk': 'kjha6dsa687sa',
-                    'pk': self.poll.pk}
+            kwargs={
+                'thread_pk': 'kjha6dsa687sa',
+                'pk': self.poll.pk,
+            }
         )
 
         response = self.put(api_link)
@@ -36,8 +38,10 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
         """api validates that thread exists"""
         api_link = reverse(
             'misago:api:thread-poll-detail',
-            kwargs={'thread_pk': self.thread.pk + 1,
-                    'pk': self.poll.pk}
+            kwargs={
+                'thread_pk': self.thread.pk + 1,
+                'pk': self.poll.pk,
+            }
         )
 
         response = self.put(api_link)
@@ -47,8 +51,10 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
         """api validates that poll id is integer"""
         api_link = reverse(
             'misago:api:thread-poll-detail',
-            kwargs={'thread_pk': self.thread.pk,
-                    'pk': 'sad98as7d97sa98'}
+            kwargs={
+                'thread_pk': self.thread.pk,
+                'pk': 'sad98as7d97sa98',
+            }
         )
 
         response = self.put(api_link)
@@ -58,8 +64,10 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
         """api validates that poll exists"""
         api_link = reverse(
             'misago:api:thread-poll-detail',
-            kwargs={'thread_pk': self.thread.pk,
-                    'pk': self.poll.pk + 123}
+            kwargs={
+                'thread_pk': self.thread.pk,
+                'pk': self.poll.pk + 123,
+            }
         )
 
         response = self.put(api_link)
@@ -145,7 +153,11 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
 
     def test_length_validation(self):
         """api validates poll's length"""
-        response = self.put(self.api_link, data={'length': -1})
+        response = self.put(
+            self.api_link, data={
+                'length': -1,
+            }
+        )
         self.assertEqual(response.status_code, 400)
 
         response_json = response.json()
@@ -174,10 +186,14 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
     def test_validate_choice_length(self):
         """api validates single choice length"""
         response = self.put(
-            self.api_link, data={'choices': [{
-                'hash': 'qwertyuiopas',
-                'label': ''
-            }]}
+            self.api_link, data={
+                'choices': [
+                    {
+                        'hash': 'qwertyuiopas',
+                        'label': '',
+                    },
+                ],
+            }
         )
         self.assertEqual(response.status_code, 400)
 
@@ -185,10 +201,15 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
         self.assertEqual(response_json['choices'], ["One or more poll choices are invalid."])
 
         response = self.put(
-            self.api_link, data={'choices': [{
-                'hash': 'qwertyuiopas',
-                'label': 'abcd' * 255
-            }]}
+            self.api_link,
+            data={
+                'choices': [
+                    {
+                        'hash': 'qwertyuiopas',
+                        'label': 'abcd' * 255,
+                    },
+                ],
+            }
         )
         self.assertEqual(response.status_code, 400)
 
@@ -197,7 +218,15 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
 
     def test_validate_two_choices(self):
         """api validates that there are at least two choices in poll"""
-        response = self.put(self.api_link, data={'choices': [{'label': 'Choice'}]})
+        response = self.put(
+            self.api_link, data={
+                'choices': [
+                    {
+                        'label': 'Choice',
+                    },
+                ],
+            }
+        )
         self.assertEqual(response.status_code, 400)
 
         response_json = response.json()
@@ -208,9 +237,13 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
     def test_validate_max_choices(self):
         """api validates that there are no more choices in poll than allowed number"""
         response = self.put(
-            self.api_link, data={'choices': [{
-                'label': 'Choice'
-            }] * (MAX_POLL_OPTIONS + 1)}
+            self.api_link, data={
+                'choices': [
+                    {
+                        'label': 'Choice',
+                    },
+                ] * (MAX_POLL_OPTIONS + 1),
+            }
         )
         self.assertEqual(response.status_code, 400)
 
@@ -238,11 +271,14 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
                 'length': 0,
                 'question': "Lorem ipsum",
                 'allowed_choices': 3,
-                'choices': [{
-                    'label': 'Choice'
-                }, {
-                    'label': 'Choice'
-                }]
+                'choices': [
+                    {
+                        'label': 'Choice',
+                    },
+                    {
+                        'label': 'Choice',
+                    },
+                ],
             }
         )
         self.assertEqual(response.status_code, 400)
@@ -263,13 +299,17 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
                 'allowed_choices': 2,
                 'allow_revotes': True,
                 'is_public': True,
-                'choices': [{
-                    'label': '\nRed  '
-                }, {
-                    'label': 'Green'
-                }, {
-                    'label': 'Blue'
-                }]
+                'choices': [
+                    {
+                        'label': '\nRed  ',
+                    },
+                    {
+                        'label': 'Green',
+                    },
+                    {
+                        'label': 'Blue',
+                    },
+                ],
             }
         )
         self.assertEqual(response.status_code, 200)
@@ -311,23 +351,28 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
                     True,
                 'is_public':
                     True,
-                'choices': [{
-                    'hash': 'aaaaaaaaaaaa',
-                    'label': '\nFirst  ',
-                    'votes': 5555
-                }, {
-                    'hash': 'bbbbbbbbbbbb',
-                    'label': 'Second',
-                    'votes': 5555
-                }, {
-                    'hash': 'gggggggggggg',
-                    'label': 'Third',
-                    'votes': 5555
-                }, {
-                    'hash': 'dddddddddddd',
-                    'label': 'Fourth',
-                    'votes': 5555
-                }]
+                'choices': [
+                    {
+                        'hash': 'aaaaaaaaaaaa',
+                        'label': '\nFirst  ',
+                        'votes': 5555,
+                    },
+                    {
+                        'hash': 'bbbbbbbbbbbb',
+                        'label': 'Second',
+                        'votes': 5555,
+                    },
+                    {
+                        'hash': 'gggggggggggg',
+                        'label': 'Third',
+                        'votes': 5555,
+                    },
+                    {
+                        'hash': 'dddddddddddd',
+                        'label': 'Fourth',
+                        'votes': 5555,
+                    },
+                ],
             }
         )
         self.assertEqual(response.status_code, 200)
@@ -346,27 +391,33 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
         # choices were updated
         self.assertEqual(len(response_json['choices']), 4)
         self.assertEqual(
-            response_json['choices'], [{
-                'hash': 'aaaaaaaaaaaa',
-                'label': 'First',
-                'votes': 1,
-                'selected': False
-            }, {
-                'hash': 'bbbbbbbbbbbb',
-                'label': 'Second',
-                'votes': 0,
-                'selected': False
-            }, {
-                'hash': 'gggggggggggg',
-                'label': 'Third',
-                'votes': 2,
-                'selected': True
-            }, {
-                'hash': 'dddddddddddd',
-                'label': 'Fourth',
-                'votes': 1,
-                'selected': True
-            }]
+            response_json['choices'],
+            [
+                {
+                    'hash': 'aaaaaaaaaaaa',
+                    'label': 'First',
+                    'votes': 1,
+                    'selected': False,
+                },
+                {
+                    'hash': 'bbbbbbbbbbbb',
+                    'label': 'Second',
+                    'votes': 0,
+                    'selected': False,
+                },
+                {
+                    'hash': 'gggggggggggg',
+                    'label': 'Third',
+                    'votes': 2,
+                    'selected': True,
+                },
+                {
+                    'hash': 'dddddddddddd',
+                    'label': 'Fourth',
+                    'votes': 1,
+                    'selected': True,
+                },
+            ],
         )
 
         # no votes were removed
@@ -388,19 +439,23 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
                     True,
                 'is_public':
                     True,
-                'choices': [{
-                    'hash': 'aaaaaaaaaaaa',
-                    'label': '\nFirst ',
-                    'votes': 5555
-                }, {
-                    'hash': 'bbbbbbbbbbbb',
-                    'label': 'Second',
-                    'votes': 5555
-                }, {
-                    'hash': 'dsadsadsa788',
-                    'label': 'New Option',
-                    'votes': 5555
-                }]
+                'choices': [
+                    {
+                        'hash': 'aaaaaaaaaaaa',
+                        'label': '\nFirst ',
+                        'votes': 5555,
+                    },
+                    {
+                        'hash': 'bbbbbbbbbbbb',
+                        'label': 'Second',
+                        'votes': 5555,
+                    },
+                    {
+                        'hash': 'dsadsadsa788',
+                        'label': 'New Option',
+                        'votes': 5555,
+                    },
+                ],
             }
         )
         self.assertEqual(response.status_code, 200)
@@ -419,22 +474,27 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
         # choices were updated
         self.assertEqual(len(response_json['choices']), 3)
         self.assertEqual(
-            response_json['choices'], [{
-                'hash': 'aaaaaaaaaaaa',
-                'label': 'First',
-                'votes': 1,
-                'selected': False
-            }, {
-                'hash': 'bbbbbbbbbbbb',
-                'label': 'Second',
-                'votes': 0,
-                'selected': False
-            }, {
-                'hash': response_json['choices'][2]['hash'],
-                'label': 'New Option',
-                'votes': 0,
-                'selected': False
-            }]
+            response_json['choices'],
+            [
+                {
+                    'hash': 'aaaaaaaaaaaa',
+                    'label': 'First',
+                    'votes': 1,
+                    'selected': False,
+                },
+                {
+                    'hash': 'bbbbbbbbbbbb',
+                    'label': 'Second',
+                    'votes': 0,
+                    'selected': False,
+                },
+                {
+                    'hash': response_json['choices'][2]['hash'],
+                    'label': 'New Option',
+                    'votes': 0,
+                    'selected': False,
+                },
+            ],
         )
 
         # no votes were removed
@@ -458,13 +518,17 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
                 'allowed_choices': 2,
                 'allow_revotes': True,
                 'is_public': True,
-                'choices': [{
-                    'label': '\nRed  '
-                }, {
-                    'label': 'Green'
-                }, {
-                    'label': 'Blue'
-                }]
+                'choices': [
+                    {
+                        'label': '\nRed  ',
+                    },
+                    {
+                        'label': 'Green',
+                    },
+                    {
+                        'label': 'Blue',
+                    },
+                ],
             }
         )
         self.assertEqual(response.status_code, 200)

+ 20 - 10
misago/threads/tests/test_thread_pollvotes_api.py

@@ -38,8 +38,10 @@ class ThreadGetVotesTests(ThreadPollApiTestCase):
         """api validates that thread id is integer"""
         api_link = reverse(
             'misago:api:thread-poll-votes',
-            kwargs={'thread_pk': 'kjha6dsa687sa',
-                    'pk': self.poll.pk}
+            kwargs={
+                'thread_pk': 'kjha6dsa687sa',
+                'pk': self.poll.pk,
+            }
         )
 
         response = self.client.get(api_link)
@@ -49,8 +51,10 @@ class ThreadGetVotesTests(ThreadPollApiTestCase):
         """api validates that thread exists"""
         api_link = reverse(
             'misago:api:thread-poll-votes',
-            kwargs={'thread_pk': self.thread.pk + 1,
-                    'pk': self.poll.pk}
+            kwargs={
+                'thread_pk': self.thread.pk + 1,
+                'pk': self.poll.pk,
+            }
         )
 
         response = self.client.get(api_link)
@@ -60,8 +64,10 @@ class ThreadGetVotesTests(ThreadPollApiTestCase):
         """api validates that poll id is integer"""
         api_link = reverse(
             'misago:api:thread-poll-votes',
-            kwargs={'thread_pk': self.thread.pk,
-                    'pk': 'sad98as7d97sa98'}
+            kwargs={
+                'thread_pk': self.thread.pk,
+                'pk': 'sad98as7d97sa98',
+            }
         )
 
         response = self.client.get(api_link)
@@ -71,8 +77,10 @@ class ThreadGetVotesTests(ThreadPollApiTestCase):
         """api validates that poll exists"""
         api_link = reverse(
             'misago:api:thread-poll-votes',
-            kwargs={'thread_pk': self.thread.pk,
-                    'pk': self.poll.pk + 123}
+            kwargs={
+                'thread_pk': self.thread.pk,
+                'pk': self.poll.pk + 123,
+            }
         )
 
         response = self.client.get(api_link)
@@ -152,8 +160,10 @@ class ThreadPostVotesTests(ThreadPollApiTestCase):
 
         self.api_link = reverse(
             'misago:api:thread-poll-votes',
-            kwargs={'thread_pk': self.thread.pk,
-                    'pk': self.poll.pk}
+            kwargs={
+                'thread_pk': self.thread.pk,
+                'pk': self.poll.pk,
+            }
         )
 
     def delete_user_votes(self):

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

@@ -17,8 +17,10 @@ class PostDeleteApiTests(ThreadsApiTestCase):
 
         self.api_link = reverse(
             'misago:api:thread-post-detail',
-            kwargs={'thread_pk': self.thread.pk,
-                    'pk': self.post.pk}
+            kwargs={
+                'thread_pk': self.thread.pk,
+                'pk': self.post.pk,
+            }
         )
 
     def test_delete_anonymous(self):
@@ -37,7 +39,11 @@ class PostDeleteApiTests(ThreadsApiTestCase):
 
     def test_delete_other_user_post_no_permission(self):
         """api valdiates if user can delete other users posts"""
-        self.override_acl({'post_edit_time': 0, 'can_hide_own_posts': 2, 'can_hide_posts': 0})
+        self.override_acl({
+            'post_edit_time': 0,
+            'can_hide_own_posts': 2,
+            'can_hide_posts': 0,
+        })
 
         self.post.poster = None
         self.post.save()
@@ -117,8 +123,10 @@ class PostDeleteApiTests(ThreadsApiTestCase):
 
         api_link = reverse(
             'misago:api:thread-post-detail',
-            kwargs={'thread_pk': self.thread.pk,
-                    'pk': self.thread.first_post_id}
+            kwargs={
+                'thread_pk': self.thread.pk,
+                'pk': self.thread.first_post_id,
+            }
         )
 
         response = self.client.delete(api_link)
@@ -126,7 +134,11 @@ class PostDeleteApiTests(ThreadsApiTestCase):
 
     def test_delete_event(self):
         """api differs posts from events"""
-        self.override_acl({'can_hide_own_posts': 2, 'can_hide_posts': 2, 'can_hide_events': 0})
+        self.override_acl({
+            'can_hide_own_posts': 2,
+            'can_hide_posts': 2,
+            'can_hide_events': 0,
+        })
 
         self.post.is_event = True
         self.post.save()
@@ -136,7 +148,11 @@ class PostDeleteApiTests(ThreadsApiTestCase):
 
     def test_delete_owned_post(self):
         """api deletes owned thread post"""
-        self.override_acl({'post_edit_time': 0, 'can_hide_own_posts': 2, 'can_hide_posts': 0})
+        self.override_acl({
+            'post_edit_time': 0,
+            'can_hide_own_posts': 2,
+            'can_hide_posts': 0,
+        })
 
         response = self.client.delete(self.api_link)
         self.assertEqual(response.status_code, 200)
@@ -169,8 +185,10 @@ class EventDeleteApiTests(ThreadsApiTestCase):
 
         self.api_link = reverse(
             'misago:api:thread-post-detail',
-            kwargs={'thread_pk': self.thread.pk,
-                    'pk': self.event.pk}
+            kwargs={
+                'thread_pk': self.thread.pk,
+                'pk': self.event.pk,
+            }
         )
 
     def test_delete_anonymous(self):
@@ -182,14 +200,22 @@ class EventDeleteApiTests(ThreadsApiTestCase):
 
     def test_no_permission(self):
         """api validates permission to delete event"""
-        self.override_acl({'can_hide_own_posts': 2, 'can_hide_posts': 2, 'can_hide_events': 0})
+        self.override_acl({
+            'can_hide_own_posts': 2,
+            'can_hide_posts': 2,
+            'can_hide_events': 0,
+        })
 
         response = self.client.delete(self.api_link)
         self.assertContains(response, "You can't delete events in this category.", status_code=403)
 
     def test_delete_event(self):
         """api differs posts from events"""
-        self.override_acl({'can_hide_own_posts': 0, 'can_hide_posts': 0, 'can_hide_events': 2})
+        self.override_acl({
+            'can_hide_own_posts': 0,
+            'can_hide_posts': 0,
+            'can_hide_events': 2,
+        })
 
         response = self.client.delete(self.api_link)
         self.assertEqual(response.status_code, 200)

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

@@ -32,16 +32,18 @@ class ThreadPostEditsApiTestCase(ThreadsApiTestCase):
                 editor_slug=self.user.slug,
                 editor_ip='127.0.0.1',
                 edited_from="Original body",
-                edited_to="First Edit"
-            ), self.post.edits_record.create(
+                edited_to="First Edit",
+            ),
+            self.post.edits_record.create(
                 category=self.category,
                 thread=self.thread,
                 editor_name='Deleted',
                 editor_slug='deleted',
                 editor_ip='127.0.0.1',
                 edited_from="First Edit",
-                edited_to="Second Edit"
-            ), self.post.edits_record.create(
+                edited_to="Second Edit",
+            ),
+            self.post.edits_record.create(
                 category=self.category,
                 thread=self.thread,
                 editor=self.user,
@@ -49,8 +51,8 @@ class ThreadPostEditsApiTestCase(ThreadsApiTestCase):
                 editor_slug=self.user.slug,
                 editor_ip='127.0.0.1',
                 edited_from="Second Edit",
-                edited_to="Last Edit"
-            )
+                edited_to="Last Edit",
+            ),
         ]
 
         self.post.original = 'Last Edit'

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

@@ -14,8 +14,10 @@ class ThreadPostLikesApiTestCase(ThreadsApiTestCase):
 
         self.api_link = reverse(
             'misago:api:thread-post-likes',
-            kwargs={'thread_pk': self.thread.pk,
-                    'pk': self.post.pk}
+            kwargs={
+                'thread_pk': self.thread.pk,
+                'pk': self.post.pk,
+            }
         )
 
     def test_no_permission(self):

+ 70 - 34
misago/threads/tests/test_thread_postmerge_api.py

@@ -22,7 +22,9 @@ class ThreadPostMergeApiTestCase(AuthenticatedUserTestCase):
         self.post = testutils.reply_thread(self.thread, poster=self.user)
 
         self.api_link = reverse(
-            'misago:api:thread-post-merge', kwargs={'thread_pk': self.thread.pk}
+            'misago:api:thread-post-merge', kwargs={
+                'thread_pk': self.thread.pk,
+            }
         )
 
         self.override_acl()
@@ -39,7 +41,7 @@ class ThreadPostMergeApiTestCase(AuthenticatedUserTestCase):
             'can_reply_threads': 0,
             'can_edit_posts': 1,
             'can_approve_content': 0,
-            'can_merge_posts': 1
+            'can_merge_posts': 1,
         })
 
         if extra_acl:
@@ -51,14 +53,22 @@ class ThreadPostMergeApiTestCase(AuthenticatedUserTestCase):
         """you need to authenticate to merge posts"""
         self.logout_user()
 
-        response = self.client.post(self.api_link, json.dumps({}), content_type="application/json")
+        response = self.client.post(
+            self.api_link,
+            json.dumps({}),
+            content_type="application/json",
+        )
         self.assertEqual(response.status_code, 403)
 
     def test_no_permission(self):
         """api validates permission to merge"""
         self.override_acl({'can_merge_posts': 0})
 
-        response = self.client.post(self.api_link, json.dumps({}), content_type="application/json")
+        response = self.client.post(
+            self.api_link,
+            json.dumps({}),
+            content_type="application/json",
+        )
         self.assertContains(response, "You can't merge posts in this thread.", status_code=403)
 
     def test_closed_thread(self):
@@ -66,13 +76,21 @@ class ThreadPostMergeApiTestCase(AuthenticatedUserTestCase):
         self.thread.is_closed = True
         self.thread.save()
 
-        response = self.client.post(self.api_link, json.dumps({}), content_type="application/json")
+        response = self.client.post(
+            self.api_link,
+            json.dumps({}),
+            content_type="application/json",
+        )
         self.assertContains(response, "You can't merge posts in this thread.", status_code=403)
 
         # allow closing threads
         self.override_acl({'can_close_threads': 1})
 
-        response = self.client.post(self.api_link, json.dumps({}), content_type="application/json")
+        response = self.client.post(
+            self.api_link,
+            json.dumps({}),
+            content_type="application/json",
+        )
         self.assertContains(
             response, "You have to select at least two posts to merge.", status_code=400
         )
@@ -82,20 +100,32 @@ class ThreadPostMergeApiTestCase(AuthenticatedUserTestCase):
         self.category.is_closed = True
         self.category.save()
 
-        response = self.client.post(self.api_link, json.dumps({}), content_type="application/json")
+        response = self.client.post(
+            self.api_link,
+            json.dumps({}),
+            content_type="application/json",
+        )
         self.assertContains(response, "You can't merge posts in this thread.", status_code=403)
 
         # allow closing threads
         self.override_acl({'can_close_threads': 1})
 
-        response = self.client.post(self.api_link, json.dumps({}), content_type="application/json")
+        response = self.client.post(
+            self.api_link,
+            json.dumps({}),
+            content_type="application/json",
+        )
         self.assertContains(
             response, "You have to select at least two posts to merge.", status_code=400
         )
 
     def test_empty_data(self):
         """api handles empty data"""
-        response = self.client.post(self.api_link, json.dumps({}), content_type="application/json")
+        response = self.client.post(
+            self.api_link,
+            json.dumps({}),
+            content_type="application/json",
+        )
         self.assertContains(
             response, "You have to select at least two posts to merge.", status_code=400
         )
@@ -103,9 +133,11 @@ class ThreadPostMergeApiTestCase(AuthenticatedUserTestCase):
     def test_no_posts_ids(self):
         """api rejects no posts ids"""
         response = self.client.post(
-            self.api_link, json.dumps({
+            self.api_link,
+            json.dumps({
                 'posts': []
-            }), content_type="application/json"
+            }),
+            content_type="application/json",
         )
         self.assertContains(
             response, "You have to select at least two posts to merge.", status_code=400
@@ -114,9 +146,11 @@ class ThreadPostMergeApiTestCase(AuthenticatedUserTestCase):
     def test_invalid_posts_data(self):
         """api handles invalid data"""
         response = self.client.post(
-            self.api_link, json.dumps({
+            self.api_link,
+            json.dumps({
                 'posts': 'string'
-            }), content_type="application/json"
+            }),
+            content_type="application/json",
         )
         self.assertContains(
             response, "One or more post ids received were invalid.", status_code=400
@@ -129,7 +163,7 @@ class ThreadPostMergeApiTestCase(AuthenticatedUserTestCase):
             json.dumps({
                 'posts': [1, 2, 'string']
             }),
-            content_type="application/json"
+            content_type="application/json",
         )
         self.assertContains(
             response, "One or more post ids received were invalid.", status_code=400
@@ -138,9 +172,11 @@ class ThreadPostMergeApiTestCase(AuthenticatedUserTestCase):
     def test_one_post_id(self):
         """api rejects one post id"""
         response = self.client.post(
-            self.api_link, json.dumps({
+            self.api_link,
+            json.dumps({
                 'posts': [1]
-            }), content_type="application/json"
+            }),
+            content_type="application/json",
         )
         self.assertContains(
             response, "You have to select at least two posts to merge.", status_code=400
@@ -153,7 +189,7 @@ class ThreadPostMergeApiTestCase(AuthenticatedUserTestCase):
             json.dumps({
                 'posts': list(range(MERGE_LIMIT + 1))
             }),
-            content_type="application/json"
+            content_type="application/json",
         )
         self.assertContains(
             response, "No more than {} posts can be merged".format(MERGE_LIMIT), status_code=400
@@ -168,7 +204,7 @@ class ThreadPostMergeApiTestCase(AuthenticatedUserTestCase):
             json.dumps({
                 'posts': [self.post.pk, event.pk]
             }),
-            content_type="application/json"
+            content_type="application/json",
         )
         self.assertContains(response, "Events can't be merged.", status_code=400)
 
@@ -179,7 +215,7 @@ class ThreadPostMergeApiTestCase(AuthenticatedUserTestCase):
             json.dumps({
                 'posts': [self.post.pk, self.post.pk * 1000]
             }),
-            content_type="application/json"
+            content_type="application/json",
         )
         self.assertContains(
             response, "One or more posts to merge could not be found.", status_code=400
@@ -195,7 +231,7 @@ class ThreadPostMergeApiTestCase(AuthenticatedUserTestCase):
             json.dumps({
                 'posts': [self.post.pk, other_post.pk]
             }),
-            content_type="application/json"
+            content_type="application/json",
         )
         self.assertContains(
             response, "One or more posts to merge could not be found.", status_code=400
@@ -210,7 +246,7 @@ class ThreadPostMergeApiTestCase(AuthenticatedUserTestCase):
             json.dumps({
                 'posts': [self.post.pk, other_post.pk]
             }),
-            content_type="application/json"
+            content_type="application/json",
         )
         self.assertContains(
             response, "Posts made by different users can't be merged.", status_code=400
@@ -225,7 +261,7 @@ class ThreadPostMergeApiTestCase(AuthenticatedUserTestCase):
             json.dumps({
                 'posts': [other_post.pk, self.post.pk]
             }),
-            content_type="application/json"
+            content_type="application/json",
         )
         self.assertContains(
             response, "Posts made by different users can't be merged.", status_code=400
@@ -238,10 +274,10 @@ class ThreadPostMergeApiTestCase(AuthenticatedUserTestCase):
             json.dumps({
                 'posts': [
                     testutils.reply_thread(self.thread, poster="Bob").pk,
-                    testutils.reply_thread(self.thread, poster="Miku").pk
+                    testutils.reply_thread(self.thread, poster="Miku").pk,
                 ]
             }),
-            content_type="application/json"
+            content_type="application/json",
         )
         self.assertContains(
             response, "Posts made by different users can't be merged.", status_code=400
@@ -256,10 +292,10 @@ class ThreadPostMergeApiTestCase(AuthenticatedUserTestCase):
             json.dumps({
                 'posts': [
                     testutils.reply_thread(self.thread, poster="Bob", is_hidden=True).pk,
-                    testutils.reply_thread(self.thread, poster="Bob", is_hidden=False).pk
+                    testutils.reply_thread(self.thread, poster="Bob", is_hidden=False).pk,
                 ]
             }),
-            content_type="application/json"
+            content_type="application/json",
         )
         self.assertContains(
             response, "Posts with different visibility can't be merged.", status_code=400
@@ -274,10 +310,10 @@ class ThreadPostMergeApiTestCase(AuthenticatedUserTestCase):
             json.dumps({
                 'posts': [
                     testutils.reply_thread(self.thread, poster="Bob", is_unapproved=True).pk,
-                    testutils.reply_thread(self.thread, poster="Bob", is_unapproved=False).pk
+                    testutils.reply_thread(self.thread, poster="Bob", is_unapproved=False).pk,
                 ]
             }),
-            content_type="application/json"
+            content_type="application/json",
         )
         self.assertContains(
             response, "Posts with different visibility can't be merged.", status_code=400
@@ -295,7 +331,7 @@ class ThreadPostMergeApiTestCase(AuthenticatedUserTestCase):
             json.dumps({
                 'posts': [post_a.pk, post_b.pk]
             }),
-            content_type="application/json"
+            content_type="application/json",
         )
         self.assertEqual(response.status_code, 200)
 
@@ -317,10 +353,10 @@ class ThreadPostMergeApiTestCase(AuthenticatedUserTestCase):
             json.dumps({
                 'posts': [
                     testutils.reply_thread(self.thread, poster=self.user, is_hidden=True).pk,
-                    testutils.reply_thread(self.thread, poster=self.user, is_hidden=True).pk
+                    testutils.reply_thread(self.thread, poster=self.user, is_hidden=True).pk,
                 ]
             }),
-            content_type="application/json"
+            content_type="application/json",
         )
         self.assertEqual(response.status_code, 200)
 
@@ -333,10 +369,10 @@ class ThreadPostMergeApiTestCase(AuthenticatedUserTestCase):
             json.dumps({
                 'posts': [
                     testutils.reply_thread(self.thread, poster=self.user, is_unapproved=True).pk,
-                    testutils.reply_thread(self.thread, poster=self.user, is_unapproved=True).pk
+                    testutils.reply_thread(self.thread, poster=self.user, is_unapproved=True).pk,
                 ]
             }),
-            content_type="application/json"
+            content_type="application/json",
         )
         self.assertEqual(response.status_code, 200)
 
@@ -355,6 +391,6 @@ class ThreadPostMergeApiTestCase(AuthenticatedUserTestCase):
             json.dumps({
                 'posts': [self.thread.first_post.pk, post_visible.pk]
             }),
-            content_type="application/json"
+            content_type="application/json",
         )
         self.assertEqual(response.status_code, 200)

+ 54 - 30
misago/threads/tests/test_thread_postmove_api.py

@@ -21,14 +21,18 @@ class ThreadPostMoveApiTestCase(AuthenticatedUserTestCase):
         self.thread = testutils.post_thread(category=self.category)
 
         self.api_link = reverse(
-            'misago:api:thread-post-move', kwargs={'thread_pk': self.thread.pk}
+            'misago:api:thread-post-move', kwargs={
+                'thread_pk': self.thread.pk,
+            }
         )
 
         Category(
             name='Category B',
             slug='category-b',
         ).insert_at(
-            self.category, position='last-child', save=True
+            self.category,
+            position='last-child',
+            save=True,
         )
         self.category_b = Category.objects.get(slug='category-b')
 
@@ -47,7 +51,7 @@ class ThreadPostMoveApiTestCase(AuthenticatedUserTestCase):
             'can_reply_threads': 1,
             'can_edit_posts': 1,
             'can_approve_content': 0,
-            'can_move_posts': 1
+            'can_move_posts': 1,
         })
 
         if extra_acl:
@@ -64,7 +68,7 @@ class ThreadPostMoveApiTestCase(AuthenticatedUserTestCase):
             'can_reply_threads': 0,
             'can_edit_posts': 1,
             'can_approve_content': 0,
-            'can_move_posts': 1
+            'can_move_posts': 1,
         })
 
         if acl:
@@ -105,12 +109,18 @@ class ThreadPostMoveApiTestCase(AuthenticatedUserTestCase):
 
     def test_invalid_url(self):
         """api validates thread url"""
-        response = self.client.post(self.api_link, {'thread_url': self.user.get_absolute_url()})
+        response = self.client.post(self.api_link, {
+            'thread_url': self.user.get_absolute_url(),
+        })
         self.assertContains(response, "This is not a valid thread link.", status_code=400)
 
     def test_current_thread_url(self):
         """api validates if thread url given is to current thread"""
-        response = self.client.post(self.api_link, {'thread_url': self.thread.get_absolute_url()})
+        response = self.client.post(
+            self.api_link, {
+                'thread_url': self.thread.get_absolute_url(),
+            }
+        )
         self.assertContains(
             response, "Thread to move posts to is same as current one.", status_code=400
         )
@@ -123,7 +133,9 @@ class ThreadPostMoveApiTestCase(AuthenticatedUserTestCase):
         other_thread_url = other_thread.get_absolute_url()
         other_thread.delete()
 
-        response = self.client.post(self.api_link, {'thread_url': other_thread_url})
+        response = self.client.post(self.api_link, {
+            'thread_url': other_thread_url,
+        })
         self.assertContains(
             response, "The thread you have entered link to doesn't exist", status_code=400
         )
@@ -134,7 +146,11 @@ class ThreadPostMoveApiTestCase(AuthenticatedUserTestCase):
 
         other_thread = testutils.post_thread(self.category_b)
 
-        response = self.client.post(self.api_link, {'thread_url': other_thread.get_absolute_url()})
+        response = self.client.post(
+            self.api_link, {
+                'thread_url': other_thread.get_absolute_url(),
+            }
+        )
         self.assertContains(
             response, "The thread you have entered link to doesn't exist", status_code=400
         )
@@ -145,7 +161,11 @@ class ThreadPostMoveApiTestCase(AuthenticatedUserTestCase):
 
         other_thread = testutils.post_thread(self.category_b)
 
-        response = self.client.post(self.api_link, {'thread_url': other_thread.get_absolute_url()})
+        response = self.client.post(
+            self.api_link, {
+                'thread_url': other_thread.get_absolute_url(),
+            }
+        )
         self.assertContains(
             response, "You can't move posts to threads you can't reply.", status_code=400
         )
@@ -154,7 +174,11 @@ class ThreadPostMoveApiTestCase(AuthenticatedUserTestCase):
         """api handles empty data"""
         other_thread = testutils.post_thread(self.category)
 
-        response = self.client.post(self.api_link, {'thread_url': other_thread.get_absolute_url()})
+        response = self.client.post(
+            self.api_link, {
+                'thread_url': other_thread.get_absolute_url(),
+            }
+        )
         self.assertContains(
             response, "You have to specify at least one post to move.", status_code=400
         )
@@ -167,9 +191,9 @@ class ThreadPostMoveApiTestCase(AuthenticatedUserTestCase):
             self.api_link,
             json.dumps({
                 'thread_url': other_thread.get_absolute_url(),
-                'posts': []
+                'posts': [],
             }),
-            content_type="application/json"
+            content_type="application/json",
         )
         self.assertContains(
             response, "You have to specify at least one post to move.", status_code=400
@@ -183,9 +207,9 @@ class ThreadPostMoveApiTestCase(AuthenticatedUserTestCase):
             self.api_link,
             json.dumps({
                 'thread_url': other_thread.get_absolute_url(),
-                'posts': 'string'
+                'posts': 'string',
             }),
-            content_type="application/json"
+            content_type="application/json",
         )
         self.assertContains(
             response, "One or more post ids received were invalid.", status_code=400
@@ -199,9 +223,9 @@ class ThreadPostMoveApiTestCase(AuthenticatedUserTestCase):
             self.api_link,
             json.dumps({
                 'thread_url': other_thread.get_absolute_url(),
-                'posts': [1, 2, 'string']
+                'posts': [1, 2, 'string'],
             }),
-            content_type="application/json"
+            content_type="application/json",
         )
         self.assertContains(
             response, "One or more post ids received were invalid.", status_code=400
@@ -215,9 +239,9 @@ class ThreadPostMoveApiTestCase(AuthenticatedUserTestCase):
             self.api_link,
             json.dumps({
                 'thread_url': other_thread.get_absolute_url(),
-                'posts': list(range(MOVE_LIMIT + 1))
+                'posts': list(range(MOVE_LIMIT + 1)),
             }),
-            content_type="application/json"
+            content_type="application/json",
         )
         self.assertContains(
             response, "No more than {} posts can be moved".format(MOVE_LIMIT), status_code=400
@@ -231,9 +255,9 @@ class ThreadPostMoveApiTestCase(AuthenticatedUserTestCase):
             self.api_link,
             json.dumps({
                 'thread_url': other_thread.get_absolute_url(),
-                'posts': [testutils.reply_thread(self.thread, is_unapproved=True).pk]
+                'posts': [testutils.reply_thread(self.thread, is_unapproved=True).pk],
             }),
-            content_type="application/json"
+            content_type="application/json",
         )
         self.assertContains(
             response, "One or more posts to move could not be found.", status_code=400
@@ -247,9 +271,9 @@ class ThreadPostMoveApiTestCase(AuthenticatedUserTestCase):
             self.api_link,
             json.dumps({
                 'thread_url': other_thread.get_absolute_url(),
-                'posts': [testutils.reply_thread(other_thread, is_hidden=True).pk]
+                'posts': [testutils.reply_thread(other_thread, is_hidden=True).pk],
             }),
-            content_type="application/json"
+            content_type="application/json",
         )
         self.assertContains(
             response, "One or more posts to move could not be found.", status_code=400
@@ -263,9 +287,9 @@ class ThreadPostMoveApiTestCase(AuthenticatedUserTestCase):
             self.api_link,
             json.dumps({
                 'thread_url': other_thread.get_absolute_url(),
-                'posts': [testutils.reply_thread(self.thread, is_event=True).pk]
+                'posts': [testutils.reply_thread(self.thread, is_event=True).pk],
             }),
-            content_type="application/json"
+            content_type="application/json",
         )
         self.assertContains(response, "Events can't be moved.", status_code=400)
 
@@ -277,9 +301,9 @@ class ThreadPostMoveApiTestCase(AuthenticatedUserTestCase):
             self.api_link,
             json.dumps({
                 'thread_url': other_thread.get_absolute_url(),
-                'posts': [self.thread.first_post_id]
+                'posts': [self.thread.first_post_id],
             }),
-            content_type="application/json"
+            content_type="application/json",
         )
         self.assertContains(response, "You can't move thread's first post.", status_code=400)
 
@@ -291,9 +315,9 @@ class ThreadPostMoveApiTestCase(AuthenticatedUserTestCase):
             self.api_link,
             json.dumps({
                 'thread_url': other_thread.get_absolute_url(),
-                'posts': [testutils.reply_thread(self.thread, is_hidden=True).pk]
+                'posts': [testutils.reply_thread(self.thread, is_hidden=True).pk],
             }),
-            content_type="application/json"
+            content_type="application/json",
         )
         self.assertContains(
             response, "You can't move posts the content you can't see.", status_code=400
@@ -317,9 +341,9 @@ class ThreadPostMoveApiTestCase(AuthenticatedUserTestCase):
             self.api_link,
             json.dumps({
                 'thread_url': other_thread.get_absolute_url(),
-                'posts': posts
+                'posts': posts,
             }),
-            content_type="application/json"
+            content_type="application/json",
         )
         self.assertEqual(response.status_code, 200)
 

+ 361 - 228
misago/threads/tests/test_thread_postpatch_api.py

@@ -24,8 +24,10 @@ class ThreadPostPatchApiTestCase(AuthenticatedUserTestCase):
 
         self.api_link = reverse(
             'misago:api:thread-post-detail',
-            kwargs={'thread_pk': self.thread.pk,
-                    'pk': self.post.pk}
+            kwargs={
+                'thread_pk': self.thread.pk,
+                'pk': self.post.pk,
+            }
         )
 
     def patch(self, api_link, ops):
@@ -41,7 +43,7 @@ class ThreadPostPatchApiTestCase(AuthenticatedUserTestCase):
             'can_browse': 1,
             'can_start_threads': 0,
             'can_reply_threads': 0,
-            'can_edit_posts': 1
+            'can_edit_posts': 1,
         })
 
         if extra_acl:
@@ -53,7 +55,13 @@ class ThreadPostPatchApiTestCase(AuthenticatedUserTestCase):
 class PostAddAclApiTests(ThreadPostPatchApiTestCase):
     def test_add_acl_true(self):
         """api adds current event's acl to response"""
-        response = self.patch(self.api_link, [{'op': 'add', 'path': 'acl', 'value': True}])
+        response = self.patch(self.api_link, [
+            {
+                'op': 'add',
+                'path': 'acl',
+                'value': True,
+            },
+        ])
         self.assertEqual(response.status_code, 200)
 
         response_json = response.json()
@@ -61,13 +69,25 @@ class PostAddAclApiTests(ThreadPostPatchApiTestCase):
 
     def test_add_acl_false(self):
         """if value is false, api won't add acl to the response, but will set empty key"""
-        response = self.patch(self.api_link, [{'op': 'add', 'path': 'acl', 'value': False}])
+        response = self.patch(self.api_link, [
+            {
+                'op': 'add',
+                'path': 'acl',
+                'value': False,
+            },
+        ])
         self.assertEqual(response.status_code, 200)
 
         response_json = response.json()
         self.assertIsNone(response_json['acl'])
 
-        response = self.patch(self.api_link, [{'op': 'add', 'path': 'acl', 'value': True}])
+        response = self.patch(self.api_link, [
+            {
+                'op': 'add',
+                'path': 'acl',
+                'value': True,
+            },
+        ])
         self.assertEqual(response.status_code, 200)
 
 
@@ -77,11 +97,13 @@ class PostProtectApiTests(ThreadPostPatchApiTestCase):
         self.override_acl({'can_protect_posts': 1})
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'replace',
-                'path': 'is-protected',
-                'value': True
-            }]
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-protected',
+                    'value': True,
+                },
+            ]
         )
         self.assertEqual(response.status_code, 200)
 
@@ -96,11 +118,13 @@ class PostProtectApiTests(ThreadPostPatchApiTestCase):
         self.override_acl({'can_protect_posts': 1})
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'replace',
-                'path': 'is-protected',
-                'value': False
-            }]
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-protected',
+                    'value': False,
+                },
+            ]
         )
         self.assertEqual(response.status_code, 200)
 
@@ -112,11 +136,13 @@ class PostProtectApiTests(ThreadPostPatchApiTestCase):
         self.override_acl({'can_protect_posts': 0})
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'replace',
-                'path': 'is-protected',
-                'value': True
-            }]
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-protected',
+                    'value': True,
+                },
+            ]
         )
         self.assertEqual(response.status_code, 400)
 
@@ -134,11 +160,13 @@ class PostProtectApiTests(ThreadPostPatchApiTestCase):
         self.override_acl({'can_protect_posts': 0})
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'replace',
-                'path': 'is-protected',
-                'value': False
-            }]
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-protected',
+                    'value': False,
+                },
+            ]
         )
         self.assertEqual(response.status_code, 400)
 
@@ -153,11 +181,13 @@ class PostProtectApiTests(ThreadPostPatchApiTestCase):
         self.override_acl({'can_edit_posts': 0, 'can_protect_posts': 1})
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'replace',
-                'path': 'is-protected',
-                'value': True
-            }]
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-protected',
+                    'value': True,
+                },
+            ]
         )
         self.assertEqual(response.status_code, 400)
 
@@ -177,11 +207,13 @@ class PostApproveApiTests(ThreadPostPatchApiTestCase):
         self.override_acl({'can_approve_content': 1})
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'replace',
-                'path': 'is-unapproved',
-                'value': False
-            }]
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-unapproved',
+                    'value': False,
+                },
+            ]
         )
         self.assertEqual(response.status_code, 200)
 
@@ -193,11 +225,13 @@ class PostApproveApiTests(ThreadPostPatchApiTestCase):
         self.override_acl({'can_approve_content': 1})
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'replace',
-                'path': 'is-unapproved',
-                'value': True
-            }]
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-unapproved',
+                    'value': True,
+                },
+            ]
         )
         self.assertEqual(response.status_code, 200)
 
@@ -212,11 +246,13 @@ class PostApproveApiTests(ThreadPostPatchApiTestCase):
         self.override_acl({'can_approve_content': 0})
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'replace',
-                'path': 'is-unapproved',
-                'value': False
-            }]
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-unapproved',
+                    'value': False,
+                },
+            ]
         )
         self.assertEqual(response.status_code, 400)
 
@@ -237,11 +273,13 @@ class PostApproveApiTests(ThreadPostPatchApiTestCase):
         self.override_acl({'can_approve_content': 1})
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'replace',
-                'path': 'is-unapproved',
-                'value': False
-            }]
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-unapproved',
+                    'value': False,
+                },
+            ]
         )
         self.assertEqual(response.status_code, 400)
 
@@ -260,11 +298,13 @@ class PostApproveApiTests(ThreadPostPatchApiTestCase):
         self.override_acl({'can_approve_content': 1})
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'replace',
-                'path': 'is-unapproved',
-                'value': False
-            }]
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-unapproved',
+                    'value': False,
+                },
+            ]
         )
         self.assertEqual(response.status_code, 400)
 
@@ -283,11 +323,13 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
         self.override_acl({'can_hide_posts': 1})
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'replace',
-                'path': 'is-hidden',
-                'value': True
-            }]
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-hidden',
+                    'value': True,
+                },
+            ]
         )
         self.assertEqual(response.status_code, 200)
 
@@ -305,11 +347,13 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
         self.override_acl({'can_hide_posts': 1})
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'replace',
-                'path': 'is-hidden',
-                'value': False
-            }]
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-hidden',
+                    'value': False,
+                },
+            ]
         )
         self.assertEqual(response.status_code, 200)
 
@@ -321,11 +365,13 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
         self.override_acl({'can_hide_own_posts': 1})
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'replace',
-                'path': 'is-hidden',
-                'value': True
-            }]
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-hidden',
+                    'value': True,
+                },
+            ]
         )
         self.assertEqual(response.status_code, 200)
 
@@ -343,11 +389,13 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
         self.override_acl({'can_hide_own_posts': 1})
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'replace',
-                'path': 'is-hidden',
-                'value': False
-            }]
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-hidden',
+                    'value': False,
+                },
+            ]
         )
         self.assertEqual(response.status_code, 200)
 
@@ -359,11 +407,13 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
         self.override_acl({'can_hide_posts': 0})
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'replace',
-                'path': 'is-hidden',
-                'value': True
-            }]
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-hidden',
+                    'value': True,
+                },
+            ]
         )
         self.assertEqual(response.status_code, 400)
 
@@ -384,11 +434,13 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
         self.override_acl({'can_hide_posts': 0})
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'replace',
-                'path': 'is-hidden',
-                'value': False
-            }]
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-hidden',
+                    'value': False,
+                },
+            ]
         )
         self.assertEqual(response.status_code, 400)
 
@@ -406,11 +458,13 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
         self.override_acl({'can_protect_posts': 0, 'can_hide_own_posts': 1})
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'replace',
-                'path': 'is-hidden',
-                'value': True
-            }]
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-hidden',
+                    'value': True,
+                },
+            ]
         )
         self.assertEqual(response.status_code, 400)
 
@@ -431,11 +485,13 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
         self.post.save()
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'replace',
-                'path': 'is-hidden',
-                'value': False
-            }]
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-hidden',
+                    'value': False,
+                },
+            ]
         )
         self.assertEqual(response.status_code, 400)
 
@@ -455,11 +511,13 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
         self.override_acl({'can_hide_own_posts': 1})
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'replace',
-                'path': 'is-hidden',
-                'value': True
-            }]
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-hidden',
+                    'value': True,
+                },
+            ]
         )
         self.assertEqual(response.status_code, 400)
 
@@ -480,11 +538,13 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
         self.override_acl({'can_hide_own_posts': 1})
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'replace',
-                'path': 'is-hidden',
-                'value': False
-            }]
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-hidden',
+                    'value': False,
+                },
+            ]
         )
         self.assertEqual(response.status_code, 400)
 
@@ -504,11 +564,13 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
         self.override_acl({'post_edit_time': 1, 'can_hide_own_posts': 1})
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'replace',
-                'path': 'is-hidden',
-                'value': True
-            }]
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-hidden',
+                    'value': True,
+                },
+            ]
         )
         self.assertEqual(response.status_code, 400)
 
@@ -529,11 +591,13 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
         self.override_acl({'post_edit_time': 1, 'can_hide_own_posts': 1})
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'replace',
-                'path': 'is-hidden',
-                'value': False
-            }]
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-hidden',
+                    'value': False,
+                },
+            ]
         )
         self.assertEqual(response.status_code, 400)
 
@@ -553,11 +617,13 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
         self.override_acl({'can_hide_own_posts': 1})
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'replace',
-                'path': 'is-hidden',
-                'value': True
-            }]
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-hidden',
+                    'value': True,
+                },
+            ]
         )
         self.assertEqual(response.status_code, 400)
 
@@ -580,11 +646,13 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
         self.override_acl({'can_hide_own_posts': 1})
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'replace',
-                'path': 'is-hidden',
-                'value': False
-            }]
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-hidden',
+                    'value': False,
+                },
+            ]
         )
         self.assertEqual(response.status_code, 400)
 
@@ -604,11 +672,13 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
         self.override_acl({'can_hide_own_posts': 1})
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'replace',
-                'path': 'is-hidden',
-                'value': True
-            }]
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-hidden',
+                    'value': True,
+                },
+            ]
         )
         self.assertEqual(response.status_code, 400)
 
@@ -631,11 +701,13 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
         self.override_acl({'can_hide_own_posts': 1})
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'replace',
-                'path': 'is-hidden',
-                'value': False
-            }]
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-hidden',
+                    'value': False,
+                },
+            ]
         )
         self.assertEqual(response.status_code, 400)
 
@@ -655,11 +727,13 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
         self.override_acl({'can_hide_posts': 1})
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'replace',
-                'path': 'is-hidden',
-                'value': True
-            }]
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-hidden',
+                    'value': True,
+                },
+            ]
         )
         self.assertEqual(response.status_code, 400)
 
@@ -674,11 +748,13 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
         self.override_acl({'can_hide_posts': 1})
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'replace',
-                'path': 'is-hidden',
-                'value': False
-            }]
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-hidden',
+                    'value': False,
+                },
+            ]
         )
         self.assertEqual(response.status_code, 400)
 
@@ -692,11 +768,13 @@ class PostLikeApiTests(ThreadPostPatchApiTestCase):
         self.override_acl({'can_see_posts_likes': 0})
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'replace',
-                'path': 'is-liked',
-                'value': True
-            }]
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-liked',
+                    'value': True,
+                },
+            ]
         )
         self.assertContains(response, "You can't like posts in this category.", status_code=400)
 
@@ -705,22 +783,26 @@ class PostLikeApiTests(ThreadPostPatchApiTestCase):
         self.override_acl({'can_like_posts': False})
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'replace',
-                'path': 'is-liked',
-                'value': True
-            }]
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-liked',
+                    'value': True,
+                },
+            ]
         )
         self.assertContains(response, "You can't like posts in this category.", status_code=400)
 
     def test_like_post(self):
         """api adds user like to post"""
         response = self.patch(
-            self.api_link, [{
-                'op': 'replace',
-                'path': 'is-liked',
-                'value': True
-            }]
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-liked',
+                    'value': True,
+                },
+            ]
         )
         self.assertEqual(response.status_code, 200)
 
@@ -728,10 +810,12 @@ class PostLikeApiTests(ThreadPostPatchApiTestCase):
         self.assertEqual(response_json['likes'], 1)
         self.assertEqual(response_json['is_liked'], True)
         self.assertEqual(
-            response_json['last_likes'], [{
-                'id': self.user.id,
-                'username': self.user.username
-            }]
+            response_json['last_likes'], [
+                {
+                    'id': self.user.id,
+                    'username': self.user.username,
+                },
+            ]
         )
 
         post = Post.objects.get(pk=self.post.pk)
@@ -746,11 +830,13 @@ class PostLikeApiTests(ThreadPostPatchApiTestCase):
         testutils.like_post(self.post, username='Miku')
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'replace',
-                'path': 'is-liked',
-                'value': True
-            }]
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-liked',
+                    'value': True,
+                },
+            ]
         )
         self.assertEqual(response.status_code, 200)
 
@@ -758,19 +844,24 @@ class PostLikeApiTests(ThreadPostPatchApiTestCase):
         self.assertEqual(response_json['likes'], 5)
         self.assertEqual(response_json['is_liked'], True)
         self.assertEqual(
-            response_json['last_likes'], [{
-                'id': self.user.id,
-                'username': self.user.username
-            }, {
-                'id': None,
-                'username': 'Miku'
-            }, {
-                'id': None,
-                'username': 'Bob'
-            }, {
-                'id': None,
-                'username': 'Mugi'
-            }]
+            response_json['last_likes'], [
+                {
+                    'id': self.user.id,
+                    'username': self.user.username
+                },
+                {
+                    'id': None,
+                    'username': 'Miku',
+                },
+                {
+                    'id': None,
+                    'username': 'Bob',
+                },
+                {
+                    'id': None,
+                    'username': 'Mugi',
+                },
+            ]
         )
 
         post = Post.objects.get(pk=self.post.pk)
@@ -782,11 +873,13 @@ class PostLikeApiTests(ThreadPostPatchApiTestCase):
         testutils.like_post(self.post, self.user)
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'replace',
-                'path': 'is-liked',
-                'value': False
-            }]
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-liked',
+                    'value': False,
+                },
+            ]
         )
         self.assertEqual(response.status_code, 200)
 
@@ -804,11 +897,13 @@ class PostLikeApiTests(ThreadPostPatchApiTestCase):
         testutils.like_post(self.post, self.user)
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'replace',
-                'path': 'is-liked',
-                'value': True
-            }]
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-liked',
+                    'value': True,
+                },
+            ]
         )
         self.assertEqual(response.status_code, 200)
 
@@ -816,10 +911,12 @@ class PostLikeApiTests(ThreadPostPatchApiTestCase):
         self.assertEqual(response_json['likes'], 1)
         self.assertEqual(response_json['is_liked'], True)
         self.assertEqual(
-            response_json['last_likes'], [{
-                'id': self.user.id,
-                'username': self.user.username
-            }]
+            response_json['last_likes'], [
+                {
+                    'id': self.user.id,
+                    'username': self.user.username,
+                },
+            ]
         )
 
         post = Post.objects.get(pk=self.post.pk)
@@ -829,11 +926,13 @@ class PostLikeApiTests(ThreadPostPatchApiTestCase):
     def test_unlike_post_no_change(self):
         """api does no state change if we are unlinking unliked post"""
         response = self.patch(
-            self.api_link, [{
-                'op': 'replace',
-                'path': 'is-liked',
-                'value': False
-            }]
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-liked',
+                    'value': False,
+                },
+            ]
         )
         self.assertEqual(response.status_code, 200)
 
@@ -851,8 +950,10 @@ class ThreadEventPatchApiTestCase(ThreadPostPatchApiTestCase):
 
         self.api_link = reverse(
             'misago:api:thread-post-detail',
-            kwargs={'thread_pk': self.thread.pk,
-                    'pk': self.event.pk}
+            kwargs={
+                'thread_pk': self.thread.pk,
+                'pk': self.event.pk,
+            }
         )
 
     def refresh_event(self):
@@ -864,14 +965,26 @@ class EventAnonPatchApiTests(ThreadEventPatchApiTestCase):
         """anonymous users can't change event state"""
         self.logout_user()
 
-        response = self.patch(self.api_link, [{'op': 'add', 'path': 'acl', 'value': True}])
+        response = self.patch(self.api_link, [
+            {
+                'op': 'add',
+                'path': 'acl',
+                'value': True,
+            },
+        ])
         self.assertEqual(response.status_code, 403)
 
 
 class EventAddAclApiTests(ThreadEventPatchApiTestCase):
     def test_add_acl_true(self):
         """api adds current event's acl to response"""
-        response = self.patch(self.api_link, [{'op': 'add', 'path': 'acl', 'value': True}])
+        response = self.patch(self.api_link, [
+            {
+                'op': 'add',
+                'path': 'acl',
+                'value': True,
+            },
+        ])
         self.assertEqual(response.status_code, 200)
 
         response_json = response.json()
@@ -879,13 +992,25 @@ class EventAddAclApiTests(ThreadEventPatchApiTestCase):
 
     def test_add_acl_false(self):
         """if value is false, api won't add acl to the response, but will set empty key"""
-        response = self.patch(self.api_link, [{'op': 'add', 'path': 'acl', 'value': False}])
+        response = self.patch(self.api_link, [
+            {
+                'op': 'add',
+                'path': 'acl',
+                'value': False,
+            },
+        ])
         self.assertEqual(response.status_code, 200)
 
         response_json = response.json()
         self.assertIsNone(response_json['acl'])
 
-        response = self.patch(self.api_link, [{'op': 'add', 'path': 'acl', 'value': True}])
+        response = self.patch(self.api_link, [
+            {
+                'op': 'add',
+                'path': 'acl',
+                'value': True,
+            },
+        ])
         self.assertEqual(response.status_code, 200)
 
 
@@ -895,11 +1020,13 @@ class EventHideApiTests(ThreadEventPatchApiTestCase):
         self.override_acl({'can_hide_events': 1})
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'replace',
-                'path': 'is-hidden',
-                'value': True
-            }]
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-hidden',
+                    'value': True,
+                },
+            ]
         )
         self.assertEqual(response.status_code, 200)
 
@@ -917,11 +1044,13 @@ class EventHideApiTests(ThreadEventPatchApiTestCase):
         self.override_acl({'can_hide_events': 1})
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'replace',
-                'path': 'is-hidden',
-                'value': False
-            }]
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-hidden',
+                    'value': False,
+                },
+            ]
         )
         self.assertEqual(response.status_code, 200)
 
@@ -933,11 +1062,13 @@ class EventHideApiTests(ThreadEventPatchApiTestCase):
         self.override_acl({'can_hide_events': 0})
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'replace',
-                'path': 'is-hidden',
-                'value': True
-            }]
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-hidden',
+                    'value': True,
+                },
+            ]
         )
         self.assertEqual(response.status_code, 400)
 
@@ -960,10 +1091,12 @@ class EventHideApiTests(ThreadEventPatchApiTestCase):
         self.override_acl({'can_hide_events': 0})
 
         response = self.patch(
-            self.api_link, [{
-                'op': 'replace',
-                'path': 'is-hidden',
-                'value': False
-            }]
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-hidden',
+                    'value': False,
+                },
+            ]
         )
         self.assertEqual(response.status_code, 404)

+ 10 - 4
misago/threads/tests/test_thread_postread_api.py

@@ -10,12 +10,18 @@ class PostReadApiTests(ThreadsApiTestCase):
     def setUp(self):
         super(PostReadApiTests, self).setUp()
 
-        self.post = testutils.reply_thread(self.thread, poster=self.user, posted_on=timezone.now())
+        self.post = testutils.reply_thread(
+            self.thread,
+            poster=self.user,
+            posted_on=timezone.now(),
+        )
 
         self.api_link = reverse(
             'misago:api:thread-post-read',
-            kwargs={'thread_pk': self.thread.pk,
-                    'pk': self.post.pk}
+            kwargs={
+                'thread_pk': self.thread.pk,
+                'pk': self.post.pk,
+            }
         )
 
     def test_read_anonymous(self):
@@ -43,7 +49,7 @@ class PostReadApiTests(ThreadsApiTestCase):
             user=self.user,
             thread=self.thread,
             category=self.thread.category,
-            last_read_on=self.thread.post_set.order_by('id').first().posted_on
+            last_read_on=self.thread.post_set.order_by('id').first().posted_on,
         )
 
         response = self.client.post(self.api_link)

+ 94 - 67
misago/threads/tests/test_thread_postsplit_api.py

@@ -24,14 +24,18 @@ class ThreadPostSplitApiTestCase(AuthenticatedUserTestCase):
         ]
 
         self.api_link = reverse(
-            'misago:api:thread-post-split', kwargs={'thread_pk': self.thread.pk}
+            'misago:api:thread-post-split', kwargs={
+                'thread_pk': self.thread.pk,
+            }
         )
 
         Category(
             name='Category B',
             slug='category-b',
         ).insert_at(
-            self.category, position='last-child', save=True
+            self.category,
+            position='last-child',
+            save=True,
         )
         self.category_b = Category.objects.get(slug='category-b')
 
@@ -50,7 +54,7 @@ class ThreadPostSplitApiTestCase(AuthenticatedUserTestCase):
             'can_reply_threads': 1,
             'can_edit_posts': 1,
             'can_approve_content': 0,
-            'can_move_posts': 1
+            'can_move_posts': 1,
         })
 
         if extra_acl:
@@ -67,7 +71,7 @@ class ThreadPostSplitApiTestCase(AuthenticatedUserTestCase):
             'can_reply_threads': 0,
             'can_edit_posts': 1,
             'can_approve_content': 0,
-            'can_move_posts': 1
+            'can_move_posts': 1,
         })
 
         if acl:
@@ -111,9 +115,11 @@ class ThreadPostSplitApiTestCase(AuthenticatedUserTestCase):
     def test_no_posts_ids(self):
         """api rejects no posts ids"""
         response = self.client.post(
-            self.api_link, json.dumps({
-                'posts': []
-            }), content_type="application/json"
+            self.api_link,
+            json.dumps({
+                'posts': [],
+            }),
+            content_type="application/json",
         )
         self.assertContains(
             response, "You have to specify at least one post to split.", status_code=400
@@ -122,9 +128,11 @@ class ThreadPostSplitApiTestCase(AuthenticatedUserTestCase):
     def test_invalid_posts_data(self):
         """api handles invalid data"""
         response = self.client.post(
-            self.api_link, json.dumps({
-                'posts': 'string'
-            }), content_type="application/json"
+            self.api_link,
+            json.dumps({
+                'posts': 'string',
+            }),
+            content_type="application/json",
         )
         self.assertContains(
             response, "One or more post ids received were invalid.", status_code=400
@@ -135,9 +143,9 @@ class ThreadPostSplitApiTestCase(AuthenticatedUserTestCase):
         response = self.client.post(
             self.api_link,
             json.dumps({
-                'posts': [1, 2, 'string']
+                'posts': [1, 2, 'string'],
             }),
-            content_type="application/json"
+            content_type="application/json",
         )
         self.assertContains(
             response, "One or more post ids received were invalid.", status_code=400
@@ -148,9 +156,9 @@ class ThreadPostSplitApiTestCase(AuthenticatedUserTestCase):
         response = self.client.post(
             self.api_link,
             json.dumps({
-                'posts': list(range(SPLIT_LIMIT + 1))
+                'posts': list(range(SPLIT_LIMIT + 1)),
             }),
-            content_type="application/json"
+            content_type="application/json",
         )
         self.assertContains(
             response, "No more than {} posts can be split".format(SPLIT_LIMIT), status_code=400
@@ -161,9 +169,9 @@ class ThreadPostSplitApiTestCase(AuthenticatedUserTestCase):
         response = self.client.post(
             self.api_link,
             json.dumps({
-                'posts': [testutils.reply_thread(self.thread, is_unapproved=True).pk]
+                'posts': [testutils.reply_thread(self.thread, is_unapproved=True).pk],
             }),
-            content_type="application/json"
+            content_type="application/json",
         )
         self.assertContains(
             response, "One or more posts to split could not be found.", status_code=400
@@ -174,9 +182,9 @@ class ThreadPostSplitApiTestCase(AuthenticatedUserTestCase):
         response = self.client.post(
             self.api_link,
             json.dumps({
-                'posts': [testutils.reply_thread(self.thread, is_event=True).pk]
+                'posts': [testutils.reply_thread(self.thread, is_event=True).pk],
             }),
-            content_type="application/json"
+            content_type="application/json",
         )
         self.assertContains(response, "Events can't be split.", status_code=400)
 
@@ -185,9 +193,9 @@ class ThreadPostSplitApiTestCase(AuthenticatedUserTestCase):
         response = self.client.post(
             self.api_link,
             json.dumps({
-                'posts': [self.thread.first_post_id]
+                'posts': [self.thread.first_post_id],
             }),
-            content_type="application/json"
+            content_type="application/json",
         )
         self.assertContains(response, "You can't split thread's first post.", status_code=400)
 
@@ -196,9 +204,9 @@ class ThreadPostSplitApiTestCase(AuthenticatedUserTestCase):
         response = self.client.post(
             self.api_link,
             json.dumps({
-                'posts': [testutils.reply_thread(self.thread, is_hidden=True).pk]
+                'posts': [testutils.reply_thread(self.thread, is_hidden=True).pk],
             }),
-            content_type="application/json"
+            content_type="application/json",
         )
         self.assertContains(
             response, "You can't split posts the content you can't see.", status_code=400
@@ -211,9 +219,9 @@ class ThreadPostSplitApiTestCase(AuthenticatedUserTestCase):
         response = self.client.post(
             self.api_link,
             json.dumps({
-                'posts': [testutils.reply_thread(other_thread, is_hidden=True).pk]
+                'posts': [testutils.reply_thread(other_thread, is_hidden=True).pk],
             }),
-            content_type="application/json"
+            content_type="application/json",
         )
         self.assertContains(
             response, "One or more posts to split could not be found.", status_code=400
@@ -222,9 +230,11 @@ class ThreadPostSplitApiTestCase(AuthenticatedUserTestCase):
     def test_split_empty_new_thread_data(self):
         """api handles empty form data"""
         response = self.client.post(
-            self.api_link, json.dumps({
-                'posts': self.posts
-            }), content_type="application/json"
+            self.api_link,
+            json.dumps({
+                'posts': self.posts,
+            }),
+            content_type="application/json",
         )
         self.assertEqual(response.status_code, 400)
 
@@ -243,16 +253,17 @@ class ThreadPostSplitApiTestCase(AuthenticatedUserTestCase):
             json.dumps({
                 'posts': self.posts,
                 'title': '$$$',
-                'category': self.category.id
+                'category': self.category.id,
             }),
-            content_type="application/json"
+            content_type="application/json",
         )
         self.assertEqual(response.status_code, 400)
 
         response_json = response.json()
         self.assertEqual(
-            response_json,
-            {'title': ["Thread title should be at least 5 characters long (it has 3)."]}
+            response_json, {
+                'title': ["Thread title should be at least 5 characters long (it has 3)."],
+            }
         )
 
     def test_split_invalid_category(self):
@@ -264,14 +275,18 @@ class ThreadPostSplitApiTestCase(AuthenticatedUserTestCase):
             json.dumps({
                 'posts': self.posts,
                 'title': 'Valid thread title',
-                'category': self.category_b.id
+                'category': self.category_b.id,
             }),
-            content_type="application/json"
+            content_type="application/json",
         )
         self.assertEqual(response.status_code, 400)
 
         response_json = response.json()
-        self.assertEqual(response_json, {'category': ["Requested category could not be found."]})
+        self.assertEqual(
+            response_json, {
+                'category': ["Requested category could not be found."],
+            }
+        )
 
     def test_split_unallowed_start_thread(self):
         """api rejects split because category isn't allowing starting threads"""
@@ -282,15 +297,17 @@ class ThreadPostSplitApiTestCase(AuthenticatedUserTestCase):
             json.dumps({
                 'posts': self.posts,
                 'title': 'Valid thread title',
-                'category': self.category.id
+                'category': self.category.id,
             }),
-            content_type="application/json"
+            content_type="application/json",
         )
         self.assertEqual(response.status_code, 400)
 
         response_json = response.json()
         self.assertEqual(
-            response_json, {'category': ["You can't create new threads in selected category."]}
+            response_json, {
+                'category': ["You can't create new threads in selected category."],
+            }
         )
 
     def test_split_invalid_weight(self):
@@ -303,13 +320,15 @@ class ThreadPostSplitApiTestCase(AuthenticatedUserTestCase):
                 'category': self.category.id,
                 'weight': 4,
             }),
-            content_type="application/json"
+            content_type="application/json",
         )
         self.assertEqual(response.status_code, 400)
 
         response_json = response.json()
         self.assertEqual(
-            response_json, {'weight': ["Ensure this value is less than or equal to 2."]}
+            response_json, {
+                'weight': ["Ensure this value is less than or equal to 2."],
+            }
         )
 
     def test_split_unallowed_global_weight(self):
@@ -322,14 +341,15 @@ class ThreadPostSplitApiTestCase(AuthenticatedUserTestCase):
                 'category': self.category.id,
                 'weight': 2,
             }),
-            content_type="application/json"
+            content_type="application/json",
         )
         self.assertEqual(response.status_code, 400)
 
         response_json = response.json()
         self.assertEqual(
-            response_json,
-            {'weight': ["You don't have permission to pin threads globally in this category."]}
+            response_json, {
+                'weight': ["You don't have permission to pin threads globally in this category."],
+            }
         )
 
     def test_split_unallowed_local_weight(self):
@@ -348,8 +368,9 @@ class ThreadPostSplitApiTestCase(AuthenticatedUserTestCase):
 
         response_json = response.json()
         self.assertEqual(
-            response_json,
-            {'weight': ["You don't have permission to pin threads in this category."]}
+            response_json, {
+                'weight': ["You don't have permission to pin threads in this category."],
+            }
         )
 
     def test_split_allowed_local_weight(self):
@@ -364,14 +385,15 @@ class ThreadPostSplitApiTestCase(AuthenticatedUserTestCase):
                 'category': self.category.id,
                 'weight': 1,
             }),
-            content_type="application/json"
+            content_type="application/json",
         )
         self.assertEqual(response.status_code, 400)
 
         response_json = response.json()
         self.assertEqual(
-            response_json,
-            {'title': ["Thread title should be at least 5 characters long (it has 3)."]}
+            response_json, {
+                'title': ["Thread title should be at least 5 characters long (it has 3)."],
+            }
         )
 
     def test_split_allowed_global_weight(self):
@@ -386,14 +408,15 @@ class ThreadPostSplitApiTestCase(AuthenticatedUserTestCase):
                 'category': self.category.id,
                 'weight': 2,
             }),
-            content_type="application/json"
+            content_type="application/json",
         )
         self.assertEqual(response.status_code, 400)
 
         response_json = response.json()
         self.assertEqual(
-            response_json,
-            {'title': ["Thread title should be at least 5 characters long (it has 3)."]}
+            response_json, {
+                'title': ["Thread title should be at least 5 characters long (it has 3)."],
+            }
         )
 
     def test_split_unallowed_close(self):
@@ -406,14 +429,15 @@ class ThreadPostSplitApiTestCase(AuthenticatedUserTestCase):
                 'category': self.category.id,
                 'is_closed': True,
             }),
-            content_type="application/json"
+            content_type="application/json",
         )
         self.assertEqual(response.status_code, 400)
 
         response_json = response.json()
         self.assertEqual(
-            response_json,
-            {'is_closed': ["You don't have permission to close threads in this category."]}
+            response_json, {
+                'is_closed': ["You don't have permission to close threads in this category."],
+            }
         )
 
     def test_split_with_close(self):
@@ -429,14 +453,15 @@ class ThreadPostSplitApiTestCase(AuthenticatedUserTestCase):
                 'weight': 0,
                 'is_closed': True,
             }),
-            content_type="application/json"
+            content_type="application/json",
         )
         self.assertEqual(response.status_code, 400)
 
         response_json = response.json()
         self.assertEqual(
-            response_json,
-            {'title': ["Thread title should be at least 5 characters long (it has 3)."]}
+            response_json, {
+                'title': ["Thread title should be at least 5 characters long (it has 3)."],
+            }
         )
 
     def test_split_unallowed_hidden(self):
@@ -449,14 +474,15 @@ class ThreadPostSplitApiTestCase(AuthenticatedUserTestCase):
                 'category': self.category.id,
                 'is_hidden': True,
             }),
-            content_type="application/json"
+            content_type="application/json",
         )
         self.assertEqual(response.status_code, 400)
 
         response_json = response.json()
         self.assertEqual(
-            response_json,
-            {'is_hidden': ["You don't have permission to hide threads in this category."]}
+            response_json, {
+                'is_hidden': ["You don't have permission to hide threads in this category."],
+            }
         )
 
     def test_split_with_hide(self):
@@ -472,14 +498,15 @@ class ThreadPostSplitApiTestCase(AuthenticatedUserTestCase):
                 'weight': 0,
                 'is_hidden': True,
             }),
-            content_type="application/json"
+            content_type="application/json",
         )
         self.assertEqual(response.status_code, 400)
 
         response_json = response.json()
         self.assertEqual(
-            response_json,
-            {'title': ["Thread title should be at least 5 characters long (it has 3)."]}
+            response_json, {
+                'title': ["Thread title should be at least 5 characters long (it has 3)."],
+            }
         )
 
     def test_split(self):
@@ -492,9 +519,9 @@ class ThreadPostSplitApiTestCase(AuthenticatedUserTestCase):
             json.dumps({
                 'posts': self.posts,
                 'title': 'Split thread.',
-                'category': self.category.id
+                'category': self.category.id,
             }),
-            content_type="application/json"
+            content_type="application/json",
         )
         self.assertEqual(response.status_code, 200)
 
@@ -518,7 +545,7 @@ class ThreadPostSplitApiTestCase(AuthenticatedUserTestCase):
             'can_start_threads': 2,
             'can_close_threads': True,
             'can_hide_threads': True,
-            'can_pin_threads': 2
+            'can_pin_threads': 2,
         })
 
         response = self.client.post(
@@ -529,9 +556,9 @@ class ThreadPostSplitApiTestCase(AuthenticatedUserTestCase):
                 'category': self.category_b.id,
                 'weight': 2,
                 'is_closed': 1,
-                'is_hidden': 1
+                'is_hidden': 1,
             }),
-            content_type="application/json"
+            content_type="application/json",
         )
         self.assertEqual(response.status_code, 200)
 

+ 18 - 7
misago/threads/tests/test_thread_reply_api.py

@@ -18,7 +18,9 @@ class ReplyThreadTests(AuthenticatedUserTestCase):
         self.thread = testutils.post_thread(category=self.category)
 
         self.api_link = reverse(
-            'misago:api:thread-post-list', kwargs={'thread_pk': self.thread.pk}
+            'misago:api:thread-post-list', kwargs={
+                'thread_pk': self.thread.pk,
+            }
         )
 
     def override_acl(self, extra_acl=None):
@@ -27,7 +29,7 @@ class ReplyThreadTests(AuthenticatedUserTestCase):
             'can_see': 1,
             'can_browse': 1,
             'can_start_threads': 0,
-            'can_reply_threads': 1
+            'can_reply_threads': 1,
         })
 
         if extra_acl:
@@ -109,7 +111,9 @@ class ReplyThreadTests(AuthenticatedUserTestCase):
 
         response = self.client.post(self.api_link, data={})
         self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {'post': ["You have to enter a message."]})
+        self.assertEqual(response.json(), {
+            'post': ["You have to enter a message."],
+        })
 
     def test_post_is_validated(self):
         """post is validated"""
@@ -123,14 +127,19 @@ class ReplyThreadTests(AuthenticatedUserTestCase):
 
         self.assertEqual(response.status_code, 400)
         self.assertEqual(
-            response.json(),
-            {'post': ["Posted message should be at least 5 characters long (it has 1)."]}
+            response.json(), {
+                'post': ["Posted message should be at least 5 characters long (it has 1)."],
+            }
         )
 
     def test_can_reply_thread(self):
         """endpoint creates new reply"""
         self.override_acl()
-        response = self.client.post(self.api_link, data={'post': "This is test response!"})
+        response = self.client.post(
+            self.api_link, data={
+                'post': "This is test response!",
+            }
+        )
         self.assertEqual(response.status_code, 200)
 
         thread = Thread.objects.get(pk=self.thread.pk)
@@ -169,6 +178,8 @@ class ReplyThreadTests(AuthenticatedUserTestCase):
         self.override_acl()
 
         response = self.client.post(
-            self.api_link, data={'post': "Chrzążczyżewoszyce, powiat Łękółody."}
+            self.api_link, data={
+                'post': "Chrzążczyżewoszyce, powiat Łękółody.",
+            }
         )
         self.assertEqual(response.status_code, 200)

+ 31 - 20
misago/threads/tests/test_thread_start_api.py

@@ -24,7 +24,7 @@ class StartThreadTests(AuthenticatedUserTestCase):
             'can_pin_threads': 0,
             'can_close_threads': 0,
             'can_hide_threads': 0,
-            'can_hide_own_threads': 0
+            'can_hide_own_threads': 0,
         })
 
         if extra_acl:
@@ -50,7 +50,9 @@ class StartThreadTests(AuthenticatedUserTestCase):
         """has no permission to see selected category"""
         self.override_acl({'can_see': 0})
 
-        response = self.client.post(self.api_link, {'category': self.category.pk})
+        response = self.client.post(self.api_link, {
+            'category': self.category.pk,
+        })
 
         self.assertContains(response, "Selected category is invalid.", status_code=400)
 
@@ -58,7 +60,9 @@ class StartThreadTests(AuthenticatedUserTestCase):
         """has no permission to browse selected category"""
         self.override_acl({'can_browse': 0})
 
-        response = self.client.post(self.api_link, {'category': self.category.pk})
+        response = self.client.post(self.api_link, {
+            'category': self.category.pk,
+        })
 
         self.assertContains(response, "Selected category is invalid.", status_code=400)
 
@@ -66,7 +70,9 @@ class StartThreadTests(AuthenticatedUserTestCase):
         """permission to start thread in category is validated"""
         self.override_acl({'can_start_threads': 0})
 
-        response = self.client.post(self.api_link, {'category': self.category.pk})
+        response = self.client.post(self.api_link, {
+            'category': self.category.pk,
+        })
 
         self.assertContains(
             response, "You don't have permission to start new threads", status_code=400
@@ -79,7 +85,9 @@ class StartThreadTests(AuthenticatedUserTestCase):
 
         self.override_acl({'can_close_threads': 0})
 
-        response = self.client.post(self.api_link, {'category': self.category.pk})
+        response = self.client.post(self.api_link, {
+            'category': self.category.pk,
+        })
 
         self.assertContains(response, "This category is closed.", status_code=400)
 
@@ -104,7 +112,7 @@ class StartThreadTests(AuthenticatedUserTestCase):
             response.json(), {
                 'category': ["You have to select category to post thread in."],
                 'title': ["You have to enter thread title."],
-                'post': ["You have to enter a message."]
+                'post': ["You have to enter a message."],
             }
         )
 
@@ -123,7 +131,9 @@ class StartThreadTests(AuthenticatedUserTestCase):
 
         self.assertEqual(response.status_code, 400)
         self.assertEqual(
-            response.json(), {'title': ["Thread title should contain alpha-numeric characters."]}
+            response.json(), {
+                'title': ["Thread title should contain alpha-numeric characters."],
+            }
         )
 
     def test_post_is_validated(self):
@@ -141,8 +151,9 @@ class StartThreadTests(AuthenticatedUserTestCase):
 
         self.assertEqual(response.status_code, 400)
         self.assertEqual(
-            response.json(),
-            {'post': ["Posted message should be at least 5 characters long (it has 1)."]}
+            response.json(), {
+                'post': ["Posted message should be at least 5 characters long (it has 1)."],
+            }
         )
 
     def test_can_start_thread(self):
@@ -153,7 +164,7 @@ class StartThreadTests(AuthenticatedUserTestCase):
             data={
                 'category': self.category.pk,
                 'title': "Hello, I am test thread!",
-                'post': "Lorem ipsum dolor met!"
+                'post': "Lorem ipsum dolor met!",
             }
         )
         self.assertEqual(response.status_code, 200)
@@ -210,7 +221,7 @@ class StartThreadTests(AuthenticatedUserTestCase):
                 'category': self.category.pk,
                 'title': "Hello, I am test thread!",
                 'post': "Lorem ipsum dolor met!",
-                'close': True
+                'close': True,
             }
         )
         self.assertEqual(response.status_code, 200)
@@ -228,7 +239,7 @@ class StartThreadTests(AuthenticatedUserTestCase):
                 'category': self.category.pk,
                 'title': "Hello, I am test thread!",
                 'post': "Lorem ipsum dolor met!",
-                'close': True
+                'close': True,
             }
         )
         self.assertEqual(response.status_code, 200)
@@ -246,7 +257,7 @@ class StartThreadTests(AuthenticatedUserTestCase):
                 'category': self.category.pk,
                 'title': "Hello, I am test thread!",
                 'post': "Lorem ipsum dolor met!",
-                'pin': 0
+                'pin': 0,
             }
         )
         self.assertEqual(response.status_code, 200)
@@ -264,7 +275,7 @@ class StartThreadTests(AuthenticatedUserTestCase):
                 'category': self.category.pk,
                 'title': "Hello, I am test thread!",
                 'post': "Lorem ipsum dolor met!",
-                'pin': 1
+                'pin': 1,
             }
         )
         self.assertEqual(response.status_code, 200)
@@ -282,7 +293,7 @@ class StartThreadTests(AuthenticatedUserTestCase):
                 'category': self.category.pk,
                 'title': "Hello, I am test thread!",
                 'post': "Lorem ipsum dolor met!",
-                'pin': 2
+                'pin': 2,
             }
         )
         self.assertEqual(response.status_code, 200)
@@ -300,7 +311,7 @@ class StartThreadTests(AuthenticatedUserTestCase):
                 'category': self.category.pk,
                 'title': "Hello, I am test thread!",
                 'post': "Lorem ipsum dolor met!",
-                'pin': 2
+                'pin': 2,
             }
         )
         self.assertEqual(response.status_code, 200)
@@ -318,7 +329,7 @@ class StartThreadTests(AuthenticatedUserTestCase):
                 'category': self.category.pk,
                 'title': "Hello, I am test thread!",
                 'post': "Lorem ipsum dolor met!",
-                'pin': 1
+                'pin': 1,
             }
         )
         self.assertEqual(response.status_code, 200)
@@ -336,7 +347,7 @@ class StartThreadTests(AuthenticatedUserTestCase):
                 'category': self.category.pk,
                 'title': "Hello, I am test thread!",
                 'post': "Lorem ipsum dolor met!",
-                'hide': 1
+                'hide': 1,
             }
         )
         self.assertEqual(response.status_code, 200)
@@ -357,7 +368,7 @@ class StartThreadTests(AuthenticatedUserTestCase):
                 'category': self.category.pk,
                 'title': "Hello, I am test thread!",
                 'post': "Lorem ipsum dolor met!",
-                'hide': 1
+                'hide': 1,
             }
         )
         self.assertEqual(response.status_code, 200)
@@ -374,7 +385,7 @@ class StartThreadTests(AuthenticatedUserTestCase):
             data={
                 'category': self.category.pk,
                 'title': "Brzęczyżczykiewicz",
-                'post': "Chrzążczyżewoszyce, powiat Łękółody."
+                'post': "Chrzążczyżewoszyce, powiat Łękółody.",
             }
         )
         self.assertEqual(response.status_code, 200)

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

@@ -21,7 +21,7 @@ class ThreadParticipantTests(TestCase):
             starter_slug='tester',
             last_post_on=datetime,
             last_poster_name='Tester',
-            last_poster_slug='tester'
+            last_poster_slug='tester',
         )
 
         self.thread.set_title("Test thread")
@@ -36,7 +36,7 @@ class ThreadParticipantTests(TestCase):
             parsed="<p>Hello! I am test message!</p>",
             checksum="nope",
             posted_on=datetime,
-            updated_on=datetime
+            updated_on=datetime,
         )
 
         self.thread.first_post = post

+ 10 - 5
misago/threads/tests/test_threads_api.py

@@ -33,7 +33,7 @@ class ThreadsApiTestCase(AuthenticatedUserTestCase):
             'can_edit_posts': 0,
             'can_hide_posts': 0,
             'can_hide_own_posts': 0,
-            'can_merge_threads': 0
+            'can_merge_threads': 0,
         })
 
         if acl:
@@ -54,8 +54,8 @@ class ThreadsApiTestCase(AuthenticatedUserTestCase):
                 'visible_categories': visible_categories,
                 'browseable_categories': browseable_categories,
                 'categories': {
-                    self.category.pk: final_acl
-                }
+                    self.category.pk: final_acl,
+                },
             }
         )
 
@@ -129,7 +129,9 @@ class ThreadRetrieveApiTests(ThreadsApiTestCase):
         self.override_acl({'can_hide_posts': 0})
 
         hidden_post = testutils.reply_thread(
-            self.thread, is_hidden=True, message="I'am hidden test message!"
+            self.thread,
+            is_hidden=True,
+            message="I'am hidden test message!",
         )
 
         response = self.client.get(self.tested_links[1])
@@ -146,7 +148,10 @@ class ThreadRetrieveApiTests(ThreadsApiTestCase):
         self.override_acl({'can_approve_content': 0})
 
         # unapproved posts shouldn't show at all
-        unapproved_post = testutils.reply_thread(self.thread, is_unapproved=True)
+        unapproved_post = testutils.reply_thread(
+            self.thread,
+            is_unapproved=True,
+        )
 
         response = self.client.get(self.tested_links[1])
         self.assertNotContains(response, unapproved_post.get_absolute_url())

+ 53 - 74
misago/threads/tests/test_threads_editor_api.py

@@ -63,8 +63,8 @@ class EditorApiTestCase(AuthenticatedUserTestCase):
             self.user, {
                 'browseable_categories': browseable_categories,
                 'categories': {
-                    self.category.pk: final_acl
-                }
+                    self.category.pk: final_acl,
+                },
             }
         )
 
@@ -91,19 +91,14 @@ class ThreadPostEditorApiTests(EditorApiTestCase):
 
     def test_category_disallowing_new_threads(self):
         """endpoint omits category disallowing starting threads"""
-        self.override_acl({
-            'can_start_threads': 0,
-        })
+        self.override_acl({'can_start_threads': 0})
 
         response = self.client.get(self.api_link)
         self.assertContains(response, "No categories that allow new threads", status_code=403)
 
     def test_category_closed_disallowing_new_threads(self):
         """endpoint omits closed category"""
-        self.override_acl({
-            'can_start_threads': 2,
-            'can_close_threads': 0,
-        })
+        self.override_acl({'can_start_threads': 2, 'can_close_threads': 0})
 
         self.category.is_closed = True
         self.category.save()
@@ -113,10 +108,7 @@ class ThreadPostEditorApiTests(EditorApiTestCase):
 
     def test_category_closed_allowing_new_threads(self):
         """endpoint adds closed category that allows new threads"""
-        self.override_acl({
-            'can_start_threads': 2,
-            'can_close_threads': 1,
-        })
+        self.override_acl({'can_start_threads': 2, 'can_close_threads': 1})
 
         self.category.is_closed = True
         self.category.save()
@@ -133,16 +125,14 @@ class ThreadPostEditorApiTests(EditorApiTestCase):
                 'post': {
                     'close': True,
                     'hide': False,
-                    'pin': 0
-                }
+                    'pin': 0,
+                },
             }
         )
 
     def test_category_allowing_new_threads(self):
         """endpoint adds category that allows new threads"""
-        self.override_acl({
-            'can_start_threads': 2,
-        })
+        self.override_acl({'can_start_threads': 2})
 
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 200)
@@ -156,17 +146,14 @@ class ThreadPostEditorApiTests(EditorApiTestCase):
                 'post': {
                     'close': False,
                     'hide': False,
-                    'pin': 0
-                }
+                    'pin': 0,
+                },
             }
         )
 
     def test_category_allowing_closing_threads(self):
         """endpoint adds category that allows new closed threads"""
-        self.override_acl({
-            'can_start_threads': 2,
-            'can_close_threads': 1,
-        })
+        self.override_acl({'can_start_threads': 2, 'can_close_threads': 1})
 
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 200)
@@ -180,17 +167,14 @@ class ThreadPostEditorApiTests(EditorApiTestCase):
                 'post': {
                     'close': True,
                     'hide': False,
-                    'pin': 0
-                }
+                    'pin': 0,
+                },
             }
         )
 
     def test_category_allowing_locally_pinned_threads(self):
         """endpoint adds category that allows locally pinned threads"""
-        self.override_acl({
-            'can_start_threads': 2,
-            'can_pin_threads': 1,
-        })
+        self.override_acl({'can_start_threads': 2, 'can_pin_threads': 1})
 
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 200)
@@ -204,17 +188,14 @@ class ThreadPostEditorApiTests(EditorApiTestCase):
                 'post': {
                     'close': False,
                     'hide': False,
-                    'pin': 1
-                }
+                    'pin': 1,
+                },
             }
         )
 
     def test_category_allowing_globally_pinned_threads(self):
         """endpoint adds category that allows globally pinned threads"""
-        self.override_acl({
-            'can_start_threads': 2,
-            'can_pin_threads': 2,
-        })
+        self.override_acl({'can_start_threads': 2, 'can_pin_threads': 2})
 
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 200)
@@ -228,17 +209,14 @@ class ThreadPostEditorApiTests(EditorApiTestCase):
                 'post': {
                     'close': False,
                     'hide': False,
-                    'pin': 2
-                }
+                    'pin': 2,
+                },
             }
         )
 
     def test_category_allowing_hidden_threads(self):
         """endpoint adds category that allows globally pinned threads"""
-        self.override_acl({
-            'can_start_threads': 2,
-            'can_hide_threads': 1,
-        })
+        self.override_acl({'can_start_threads': 2, 'can_hide_threads': 1})
 
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 200)
@@ -252,15 +230,12 @@ class ThreadPostEditorApiTests(EditorApiTestCase):
                 'post': {
                     'close': 0,
                     'hide': 1,
-                    'pin': 0
-                }
+                    'pin': 0,
+                },
             }
         )
 
-        self.override_acl({
-            'can_start_threads': 2,
-            'can_hide_threads': 2,
-        })
+        self.override_acl({'can_start_threads': 2, 'can_hide_threads': 2})
 
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 200)
@@ -274,8 +249,8 @@ class ThreadPostEditorApiTests(EditorApiTestCase):
                 'post': {
                     'close': False,
                     'hide': True,
-                    'pin': 0
-                }
+                    'pin': 0,
+                },
             }
         )
 
@@ -286,7 +261,9 @@ class ThreadReplyEditorApiTests(EditorApiTestCase):
 
         self.thread = testutils.post_thread(category=self.category)
         self.api_link = reverse(
-            'misago:api:thread-post-editor', kwargs={'thread_pk': self.thread.pk}
+            'misago:api:thread-post-editor', kwargs={
+                'thread_pk': self.thread.pk,
+            }
         )
 
     def test_anonymous_user_request(self):
@@ -369,7 +346,10 @@ class ThreadReplyEditorApiTests(EditorApiTestCase):
         self.override_acl({'can_reply_threads': 1})
 
         # unapproved reply can't be replied to
-        unapproved_reply = testutils.reply_thread(self.thread, is_unapproved=True)
+        unapproved_reply = testutils.reply_thread(
+            self.thread,
+            is_unapproved=True,
+        )
 
         response = self.client.get('{}?reply={}'.format(self.api_link, unapproved_reply.pk))
         self.assertEqual(response.status_code, 404)
@@ -410,10 +390,11 @@ class ThreadReplyEditorApiTests(EditorApiTestCase):
 
         self.assertEqual(response.status_code, 200)
         self.assertEqual(
-            response.json(),
-            {'id': reply_to.pk,
-             'post': reply_to.original,
-             'poster': reply_to.poster_name}
+            response.json(), {
+                'id': reply_to.pk,
+                'post': reply_to.original,
+                'poster': reply_to.poster_name,
+            }
         )
 
 
@@ -426,8 +407,10 @@ class EditReplyEditorApiTests(EditorApiTestCase):
 
         self.api_link = reverse(
             'misago:api:thread-post-editor',
-            kwargs={'thread_pk': self.thread.pk,
-                    'pk': self.post.pk}
+            kwargs={
+                'thread_pk': self.thread.pk,
+                'pk': self.post.pk,
+            }
         )
 
     def test_anonymous_user_request(self):
@@ -560,9 +543,7 @@ class EditReplyEditorApiTests(EditorApiTestCase):
 
     def test_other_user_post(self):
         """api validates if other user's post can be edited"""
-        self.override_acl({
-            'can_edit_posts': 1,
-        })
+        self.override_acl({'can_edit_posts': 1})
 
         self.post.poster = None
         self.post.save()
@@ -573,9 +554,7 @@ class EditReplyEditorApiTests(EditorApiTestCase):
         )
 
         # allow other users post edition
-        self.override_acl({
-            'can_edit_posts': 2,
-        })
+        self.override_acl({'can_edit_posts': 2})
 
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 200)
@@ -591,8 +570,10 @@ class EditReplyEditorApiTests(EditorApiTestCase):
 
         api_link = reverse(
             'misago:api:thread-post-editor',
-            kwargs={'thread_pk': self.thread.pk,
-                    'pk': self.thread.first_post.pk}
+            kwargs={
+                'thread_pk': self.thread.pk,
+                'pk': self.thread.first_post.pk,
+            }
         )
 
         response = self.client.get(api_link)
@@ -601,13 +582,13 @@ class EditReplyEditorApiTests(EditorApiTestCase):
     def test_edit(self):
         """endpoint returns valid configuration for editor"""
         for _ in range(3):
-            self.override_acl({
-                'max_attachment_size': 1000,
-            })
+            self.override_acl({'max_attachment_size': 1000})
 
             with open(TEST_DOCUMENT_PATH, 'rb') as upload:
                 response = self.client.post(
-                    reverse('misago:api:attachment-list'), data={'upload': upload}
+                    reverse('misago:api:attachment-list'), data={
+                        'upload': upload,
+                    }
                 )
             self.assertEqual(response.status_code, 200)
 
@@ -620,9 +601,7 @@ class EditReplyEditorApiTests(EditorApiTestCase):
             attachment.post = self.post
             attachment.save()
 
-        self.override_acl({
-            'can_edit_posts': 1,
-        })
+        self.override_acl({'can_edit_posts': 1})
         response = self.client.get(self.api_link)
 
         for attachment in attachments:
@@ -646,6 +625,6 @@ class EditReplyEditorApiTests(EditorApiTestCase):
                 'attachments': [
                     AttachmentSerializer(attachments[1], context={'user': self.user}).data,
                     AttachmentSerializer(attachments[0], context={'user': self.user}).data,
-                ]
+                ],
             }
         )

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

@@ -21,7 +21,9 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
             name='Category B',
             slug='category-b',
         ).insert_at(
-            self.category, position='last-child', save=True
+            self.category,
+            position='last-child',
+            save=True,
         )
         self.category_b = Category.objects.get(slug='category-b')
 
@@ -32,49 +34,61 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
 
         response_json = response.json()
         self.assertEqual(
-            response_json, {'detail': "You have to select at least two threads to merge."}
+            response_json, {
+                'detail': "You have to select at least two threads to merge.",
+            }
         )
 
     def test_merge_empty_threads(self):
         """api validates if we are trying to empty threads list"""
         response = self.client.post(
-            self.api_link, json.dumps({
-                'threads': []
-            }), content_type="application/json"
+            self.api_link,
+            json.dumps({
+                'threads': [],
+            }),
+            content_type="application/json",
         )
         self.assertEqual(response.status_code, 403)
 
         response_json = response.json()
         self.assertEqual(
-            response_json, {'detail': "You have to select at least two threads to merge."}
+            response_json, {
+                'detail': "You have to select at least two threads to merge.",
+            }
         )
 
     def test_merge_invalid_threads(self):
         """api validates if we are trying to merge invalid thread ids"""
         response = self.client.post(
-            self.api_link, json.dumps({
-                'threads': 'abcd'
-            }), content_type="application/json"
+            self.api_link,
+            json.dumps({
+                'threads': 'abcd',
+            }),
+            content_type="application/json",
         )
         self.assertEqual(response.status_code, 403)
 
         response_json = response.json()
         self.assertEqual(
-            response_json, {'detail': "One or more thread ids received were invalid."}
+            response_json, {
+                'detail': "One or more thread ids received were invalid.",
+            }
         )
 
         response = self.client.post(
             self.api_link,
             json.dumps({
-                'threads': ['a', '-', 'c']
+                'threads': ['a', '-', 'c'],
             }),
-            content_type="application/json"
+            content_type="application/json",
         )
         self.assertEqual(response.status_code, 403)
 
         response_json = response.json()
         self.assertEqual(
-            response_json, {'detail': "One or more thread ids received were invalid."}
+            response_json, {
+                'detail': "One or more thread ids received were invalid.",
+            }
         )
 
     def test_merge_single_thread(self):
@@ -82,15 +96,17 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         response = self.client.post(
             self.api_link,
             json.dumps({
-                'threads': [self.thread.id]
+                'threads': [self.thread.id],
             }),
-            content_type="application/json"
+            content_type="application/json",
         )
         self.assertEqual(response.status_code, 403)
 
         response_json = response.json()
         self.assertEqual(
-            response_json, {'detail': "You have to select at least two threads to merge."}
+            response_json, {
+                'detail': "You have to select at least two threads to merge.",
+            }
         )
 
     def test_merge_with_nonexisting_thread(self):
@@ -100,15 +116,17 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         response = self.client.post(
             self.api_link,
             json.dumps({
-                'threads': [self.thread.id, self.thread.id + 1000]
+                'threads': [self.thread.id, self.thread.id + 1000],
             }),
-            content_type="application/json"
+            content_type="application/json",
         )
         self.assertEqual(response.status_code, 403)
 
         response_json = response.json()
         self.assertEqual(
-            response_json, {'detail': "One or more threads to merge could not be found."}
+            response_json, {
+                'detail': "One or more threads to merge could not be found.",
+            }
         )
 
     def test_merge_with_invisible_thread(self):
@@ -118,15 +136,17 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         response = self.client.post(
             self.api_link,
             json.dumps({
-                'threads': [self.thread.id, unaccesible_thread.id]
+                'threads': [self.thread.id, unaccesible_thread.id],
             }),
-            content_type="application/json"
+            content_type="application/json",
         )
         self.assertEqual(response.status_code, 403)
 
         response_json = response.json()
         self.assertEqual(
-            response_json, {'detail': "One or more threads to merge could not be found."}
+            response_json, {
+                'detail': "One or more threads to merge could not be found.",
+            }
         )
 
     def test_merge_no_permission(self):
@@ -136,9 +156,9 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         response = self.client.post(
             self.api_link,
             json.dumps({
-                'threads': [self.thread.id, thread.id]
+                'threads': [self.thread.id, thread.id],
             }),
-            content_type="application/json"
+            content_type="application/json",
         )
         self.assertEqual(response.status_code, 403)
 
@@ -148,12 +168,12 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
                 {
                     'id': thread.pk,
                     'title': thread.title,
-                    'errors': ["You don't have permission to merge this thread with others."]
+                    'errors': ["You don't have permission to merge this thread with others."],
                 },
                 {
                     'id': self.thread.pk,
                     'title': self.thread.title,
-                    'errors': ["You don't have permission to merge this thread with others."]
+                    'errors': ["You don't have permission to merge this thread with others."],
                 },
             ]
         )
@@ -172,16 +192,19 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         })
 
         response = self.client.post(
-            self.api_link, json.dumps({
-                'threads': threads
-            }), content_type="application/json"
+            self.api_link,
+            json.dumps({
+                'threads': threads,
+            }),
+            content_type="application/json",
         )
         self.assertEqual(response.status_code, 403)
 
         response_json = response.json()
         self.assertEqual(
-            response_json,
-            {'detail': "No more than %s threads can be merged at single time." % MERGE_LIMIT}
+            response_json, {
+                'detail': "No more than %s threads can be merged at single time." % MERGE_LIMIT,
+            }
         )
 
     def test_merge_no_final_thread(self):
@@ -198,9 +221,9 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         response = self.client.post(
             self.api_link,
             json.dumps({
-                'threads': [self.thread.id, thread.id]
+                'threads': [self.thread.id, thread.id],
             }),
-            content_type="application/json"
+            content_type="application/json",
         )
         self.assertEqual(response.status_code, 400)
 
@@ -230,14 +253,15 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
                 'title': '$$$',
                 'category': self.category.id,
             }),
-            content_type="application/json"
+            content_type="application/json",
         )
         self.assertEqual(response.status_code, 400)
 
         response_json = response.json()
         self.assertEqual(
-            response_json,
-            {'title': ["Thread title should be at least 5 characters long (it has 3)."]}
+            response_json, {
+                'title': ["Thread title should be at least 5 characters long (it has 3)."],
+            }
         )
 
     def test_merge_invalid_category(self):
@@ -258,12 +282,16 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
                 'title': 'Valid thread title',
                 'category': self.category_b.id,
             }),
-            content_type="application/json"
+            content_type="application/json",
         )
         self.assertEqual(response.status_code, 400)
 
         response_json = response.json()
-        self.assertEqual(response_json, {'category': ["Requested category could not be found."]})
+        self.assertEqual(
+            response_json, {
+                'category': ["Requested category could not be found."],
+            }
+        )
 
     def test_merge_unallowed_start_thread(self):
         """api rejects merge because category isn't allowing starting threads"""
@@ -272,7 +300,7 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
             'can_close_threads': False,
             'can_edit_threads': False,
             'can_reply_threads': False,
-            'can_start_threads': 0
+            'can_start_threads': 0,
         })
 
         thread = testutils.post_thread(category=self.category)
@@ -282,15 +310,17 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
             json.dumps({
                 'threads': [self.thread.id, thread.id],
                 'title': 'Valid thread title',
-                'category': self.category.id
+                'category': self.category.id,
             }),
-            content_type="application/json"
+            content_type="application/json",
         )
         self.assertEqual(response.status_code, 400)
 
         response_json = response.json()
         self.assertEqual(
-            response_json, {'category': ["You can't create new threads in selected category."]}
+            response_json, {
+                'category': ["You can't create new threads in selected category."],
+            }
         )
 
     def test_merge_invalid_weight(self):
@@ -312,13 +342,15 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
                 'category': self.category.id,
                 'weight': 4,
             }),
-            content_type="application/json"
+            content_type="application/json",
         )
         self.assertEqual(response.status_code, 400)
 
         response_json = response.json()
         self.assertEqual(
-            response_json, {'weight': ["Ensure this value is less than or equal to 2."]}
+            response_json, {
+                'weight': ["Ensure this value is less than or equal to 2."],
+            }
         )
 
     def test_merge_unallowed_global_weight(self):
@@ -340,14 +372,15 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
                 'category': self.category.id,
                 'weight': 2,
             }),
-            content_type="application/json"
+            content_type="application/json",
         )
         self.assertEqual(response.status_code, 400)
 
         response_json = response.json()
         self.assertEqual(
-            response_json,
-            {'weight': ["You don't have permission to pin threads globally in this category."]}
+            response_json, {
+                'weight': ["You don't have permission to pin threads globally in this category."],
+            }
         )
 
     def test_merge_unallowed_local_weight(self):
@@ -369,14 +402,15 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
                 'category': self.category.id,
                 'weight': 1,
             }),
-            content_type="application/json"
+            content_type="application/json",
         )
         self.assertEqual(response.status_code, 400)
 
         response_json = response.json()
         self.assertEqual(
-            response_json,
-            {'weight': ["You don't have permission to pin threads in this category."]}
+            response_json, {
+                'weight': ["You don't have permission to pin threads in this category."],
+            }
         )
 
     def test_merge_allowed_local_weight(self):
@@ -399,14 +433,15 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
                 'category': self.category.id,
                 'weight': 1,
             }),
-            content_type="application/json"
+            content_type="application/json",
         )
         self.assertEqual(response.status_code, 400)
 
         response_json = response.json()
         self.assertEqual(
-            response_json,
-            {'title': ["Thread title should be at least 5 characters long (it has 3)."]}
+            response_json, {
+                'title': ["Thread title should be at least 5 characters long (it has 3)."],
+            }
         )
 
     def test_merge_allowed_global_weight(self):
@@ -429,14 +464,15 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
                 'category': self.category.id,
                 'weight': 2,
             }),
-            content_type="application/json"
+            content_type="application/json",
         )
         self.assertEqual(response.status_code, 400)
 
         response_json = response.json()
         self.assertEqual(
-            response_json,
-            {'title': ["Thread title should be at least 5 characters long (it has 3)."]}
+            response_json, {
+                'title': ["Thread title should be at least 5 characters long (it has 3)."],
+            }
         )
 
     def test_merge_unallowed_close(self):
@@ -458,14 +494,15 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
                 'category': self.category.id,
                 'is_closed': True,
             }),
-            content_type="application/json"
+            content_type="application/json",
         )
         self.assertEqual(response.status_code, 400)
 
         response_json = response.json()
         self.assertEqual(
-            response_json,
-            {'is_closed': ["You don't have permission to close threads in this category."]}
+            response_json, {
+                'is_closed': ["You don't have permission to close threads in this category."],
+            }
         )
 
     def test_merge_with_close(self):
@@ -488,14 +525,15 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
                 'weight': 0,
                 'is_closed': True,
             }),
-            content_type="application/json"
+            content_type="application/json",
         )
         self.assertEqual(response.status_code, 400)
 
         response_json = response.json()
         self.assertEqual(
-            response_json,
-            {'title': ["Thread title should be at least 5 characters long (it has 3)."]}
+            response_json, {
+                'title': ["Thread title should be at least 5 characters long (it has 3)."],
+            }
         )
 
     def test_merge_unallowed_hidden(self):
@@ -518,14 +556,15 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
                 'category': self.category.id,
                 'is_hidden': True,
             }),
-            content_type="application/json"
+            content_type="application/json",
         )
         self.assertEqual(response.status_code, 400)
 
         response_json = response.json()
         self.assertEqual(
-            response_json,
-            {'is_hidden': ["You don't have permission to hide threads in this category."]}
+            response_json, {
+                'is_hidden': ["You don't have permission to hide threads in this category."],
+            }
         )
 
     def test_merge_with_hide(self):
@@ -549,14 +588,15 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
                 'weight': 0,
                 'is_hidden': True,
             }),
-            content_type="application/json"
+            content_type="application/json",
         )
         self.assertEqual(response.status_code, 400)
 
         response_json = response.json()
         self.assertEqual(
-            response_json,
-            {'title': ["Thread title should be at least 5 characters long (it has 3)."]}
+            response_json, {
+                'title': ["Thread title should be at least 5 characters long (it has 3)."],
+            }
         )
 
     def test_merge(self):
@@ -579,7 +619,7 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
                 'title': 'Merged thread!',
                 'category': self.category.id,
             }),
-            content_type="application/json"
+            content_type="application/json",
         )
         self.assertEqual(response.status_code, 200)
 
@@ -611,7 +651,7 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
             'can_merge_threads': True,
             'can_close_threads': True,
             'can_hide_threads': 1,
-            'can_pin_threads': 2
+            'can_pin_threads': 2,
         })
 
         thread = testutils.post_thread(category=self.category)
@@ -624,9 +664,9 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
                 'category': self.category.id,
                 'is_closed': 1,
                 'is_hidden': 1,
-                'weight': 2
+                'weight': 2,
             }),
-            content_type="application/json"
+            content_type="application/json",
         )
         self.assertEqual(response.status_code, 200)
 
@@ -675,7 +715,7 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
                 'title': 'Merged thread!',
                 'category': self.category.id,
             }),
-            content_type="application/json"
+            content_type="application/json",
         )
         self.assertEqual(response.status_code, 200)
 
@@ -701,9 +741,7 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
 
     def test_merge_threads_kept_poll(self):
         """api merges two threads successfully, keeping poll from old thread"""
-        self.override_acl({
-            'can_merge_threads': True,
-        })
+        self.override_acl({'can_merge_threads': True})
 
         other_thread = testutils.post_thread(self.category)
         poll = testutils.post_poll(other_thread, self.user)
@@ -715,7 +753,7 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
                 'title': 'Merged thread!',
                 'category': self.category.id,
             }),
-            content_type="application/json"
+            content_type="application/json",
         )
         self.assertEqual(response.status_code, 200)
 
@@ -731,9 +769,7 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
 
     def test_merge_threads_moved_poll(self):
         """api merges two threads successfully, moving poll from other thread"""
-        self.override_acl({
-            'can_merge_threads': True,
-        })
+        self.override_acl({'can_merge_threads': True})
 
         other_thread = testutils.post_thread(self.category)
         poll = testutils.post_poll(self.thread, self.user)
@@ -745,7 +781,7 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
                 'title': 'Merged thread!',
                 'category': self.category.id,
             }),
-            content_type="application/json"
+            content_type="application/json",
         )
         self.assertEqual(response.status_code, 200)
 
@@ -761,9 +797,7 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
 
     def test_threads_merge_conflict(self):
         """api errors on merge conflict, returning list of available polls"""
-        self.override_acl({
-            'can_merge_threads': True,
-        })
+        self.override_acl({'can_merge_threads': True})
 
         other_thread = testutils.post_thread(self.category)
         poll = testutils.post_poll(self.thread, self.user)
@@ -776,15 +810,17 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
                 'title': 'Merged thread!',
                 'category': self.category.id,
             }),
-            content_type="application/json"
+            content_type="application/json",
         )
 
         self.assertEqual(response.status_code, 400)
         self.assertEqual(
             response.json(), {
-                'polls': [[0, "Delete all polls"],
-                          [poll.pk, poll.question],
-                          [other_poll.pk, other_poll.question]]
+                'polls': [
+                    [0, "Delete all polls"],
+                    [poll.pk, poll.question],
+                    [other_poll.pk, other_poll.question],
+                ],
             }
         )
 
@@ -794,9 +830,7 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
 
     def test_threads_merge_conflict_invalid_resolution(self):
         """api errors on invalid merge conflict resolution"""
-        self.override_acl({
-            'can_merge_threads': True,
-        })
+        self.override_acl({'can_merge_threads': True})
 
         other_thread = testutils.post_thread(self.category)
 
@@ -809,13 +843,15 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
                 'threads': [self.thread.id, other_thread.id],
                 'title': 'Merged thread!',
                 'category': self.category.id,
-                'poll': 'dsa7dsadsa9789'
+                'poll': 'dsa7dsadsa9789',
             }),
-            content_type="application/json"
+            content_type="application/json",
         )
 
         self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {'detail': "Invalid choice."})
+        self.assertEqual(response.json(), {
+            'detail': "Invalid choice.",
+        })
 
         # polls and votes were untouched
         self.assertEqual(Poll.objects.count(), 2)
@@ -823,9 +859,7 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
 
     def test_threads_merge_conflict_delete_all(self):
         """api deletes all polls when delete all choice is selected"""
-        self.override_acl({
-            'can_merge_threads': True,
-        })
+        self.override_acl({'can_merge_threads': True})
 
         other_thread = testutils.post_thread(self.category)
 
@@ -838,9 +872,9 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
                 'threads': [self.thread.id, other_thread.id],
                 'title': 'Merged thread!',
                 'category': self.category.id,
-                'poll': 0
+                'poll': 0,
             }),
-            content_type="application/json"
+            content_type="application/json",
         )
         self.assertEqual(response.status_code, 200)
 
@@ -850,9 +884,7 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
 
     def test_threads_merge_conflict_keep_first_poll(self):
         """api deletes other poll on merge"""
-        self.override_acl({
-            'can_merge_threads': True,
-        })
+        self.override_acl({'can_merge_threads': True})
 
         other_thread = testutils.post_thread(self.category)
         poll = testutils.post_poll(self.thread, self.user)
@@ -864,9 +896,9 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
                 'threads': [self.thread.id, other_thread.id],
                 'title': 'Merged thread!',
                 'category': self.category.id,
-                'poll': poll.pk
+                'poll': poll.pk,
             }),
-            content_type="application/json"
+            content_type="application/json",
         )
         self.assertEqual(response.status_code, 200)
 
@@ -880,9 +912,7 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
 
     def test_threads_merge_conflict_keep_other_poll(self):
         """api deletes first poll on merge"""
-        self.override_acl({
-            'can_merge_threads': True,
-        })
+        self.override_acl({'can_merge_threads': True})
 
         other_thread = testutils.post_thread(self.category)
         poll = testutils.post_poll(self.thread, self.user)
@@ -894,9 +924,9 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
                 'threads': [self.thread.id, other_thread.id],
                 'title': 'Merged thread!',
                 'category': self.category.id,
-                'poll': other_poll.pk
+                'poll': other_poll.pk,
             }),
-            content_type="application/json"
+            content_type="application/json",
         )
         self.assertEqual(response.status_code, 200)
 

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

@@ -142,7 +142,9 @@ class ThreadsModerationTests(AuthenticatedUserTestCase):
             name='New Category',
             slug='new-category',
         ).insert_at(
-            root_category, position='last-child', save=True
+            root_category,
+            position='last-child',
+            save=True,
         )
         new_category = Category.objects.get(slug='new-category')
 

+ 91 - 45
misago/threads/tests/test_threadslists.py

@@ -42,14 +42,18 @@ class ThreadsListTestCase(AuthenticatedUserTestCase):
             slug='category-a',
             css_class='showing-category-a',
         ).insert_at(
-            self.root, position='last-child', save=True
+            self.root,
+            position='last-child',
+            save=True,
         )
         Category(
             name='Category E',
             slug='category-e',
             css_class='showing-category-e',
         ).insert_at(
-            self.root, position='last-child', save=True
+            self.root,
+            position='last-child',
+            save=True,
         )
 
         self.root = Category.objects.root_category()
@@ -61,7 +65,9 @@ class ThreadsListTestCase(AuthenticatedUserTestCase):
             slug='category-b',
             css_class='showing-category-b',
         ).insert_at(
-            self.category_a, position='last-child', save=True
+            self.category_a,
+            position='last-child',
+            save=True,
         )
 
         self.category_b = Category.objects.get(slug='category-b')
@@ -71,14 +77,18 @@ class ThreadsListTestCase(AuthenticatedUserTestCase):
             slug='category-c',
             css_class='showing-category-c',
         ).insert_at(
-            self.category_b, position='last-child', save=True
+            self.category_b,
+            position='last-child',
+            save=True,
         )
         Category(
             name='Category D',
             slug='category-d',
             css_class='showing-category-d',
         ).insert_at(
-            self.category_b, position='last-child', save=True
+            self.category_b,
+            position='last-child',
+            save=True,
         )
 
         self.category_c = Category.objects.get(slug='category-c')
@@ -90,7 +100,9 @@ class ThreadsListTestCase(AuthenticatedUserTestCase):
             slug='category-f',
             css_class='showing-category-f',
         ).insert_at(
-            self.category_e, position='last-child', save=True
+            self.category_e,
+            position='last-child',
+            save=True,
         )
 
         self.category_f = Category.objects.get(slug='category-f')
@@ -116,7 +128,7 @@ class ThreadsListTestCase(AuthenticatedUserTestCase):
             'categories': {},
             'visible_categories': [],
             'browseable_categories': [],
-            'can_approve_content': []
+            'can_approve_content': [],
         }
 
         # copy first category's acl to other categories to make base for overrides
@@ -136,7 +148,7 @@ class ThreadsListTestCase(AuthenticatedUserTestCase):
                 'can_see_all_threads': 1,
                 'can_see_own_threads': 0,
                 'can_hide_threads': 0,
-                'can_approve_content': 0
+                'can_approve_content': 0,
             })
 
             if category_acl:
@@ -238,7 +250,9 @@ class AllThreadsListTests(ThreadsListTestCase):
             name='Hidden Category',
             slug='hidden-category',
         ).insert_at(
-            self.root, position='last-child', save=True
+            self.root,
+            position='last-child',
+            save=True,
         )
         test_category = Category.objects.get(slug='hidden-category')
 
@@ -352,6 +366,8 @@ class AllThreadsListTests(ThreadsListTestCase):
 
     def test_noscript_pagination(self):
         """threads list is paginated for users with js disabled"""
+        threads_per_page = settings.MISAGO_THREADS_PER_PAGE
+
         threads = []
         for _ in range(settings.MISAGO_THREADS_PER_PAGE * 3):
             threads.append(testutils.post_thread(category=self.first_category))
@@ -360,12 +376,11 @@ class AllThreadsListTests(ThreadsListTestCase):
         response = self.client.get('/?page=2')
         self.assertEqual(response.status_code, 200)
 
-        for thread in threads[:settings.MISAGO_THREADS_PER_PAGE]:
+        for thread in threads[:threads_per_page]:
             self.assertNotContains(response, thread.get_absolute_url())
-        for thread in threads[settings.MISAGO_THREADS_PER_PAGE:settings.MISAGO_THREADS_PER_PAGE * 2
-                              ]:
+        for thread in threads[threads_per_page:threads_per_page * 2]:
             self.assertContains(response, thread.get_absolute_url())
-        for thread in threads[settings.MISAGO_THREADS_PER_PAGE * 2:]:
+        for thread in threads[threads_per_page * 2:]:
             self.assertNotContains(response, thread.get_absolute_url())
 
         self.assertNotContains(response, '/?page=1')
@@ -375,9 +390,9 @@ class AllThreadsListTests(ThreadsListTestCase):
         response = self.client.get('/?page=3')
         self.assertEqual(response.status_code, 200)
 
-        for thread in threads[settings.MISAGO_THREADS_PER_PAGE:]:
+        for thread in threads[threads_per_page:]:
             self.assertNotContains(response, thread.get_absolute_url())
-        for thread in threads[:settings.MISAGO_THREADS_PER_PAGE]:
+        for thread in threads[:threads_per_page]:
             self.assertContains(response, thread.get_absolute_url())
 
         self.assertContains(response, '/?page=2')
@@ -395,7 +410,9 @@ class CategoryThreadsListTests(ThreadsListTestCase):
             name='Hidden Category',
             slug='hidden-category',
         ).insert_at(
-            self.root, position='last-child', save=True
+            self.root,
+            position='last-child',
+            save=True,
         )
         test_category = Category.objects.get(slug='hidden-category')
 
@@ -412,7 +429,9 @@ class CategoryThreadsListTests(ThreadsListTestCase):
             name='Hidden Category',
             slug='hidden-category',
         ).insert_at(
-            self.root, position='last-child', save=True
+            self.root,
+            position='last-child',
+            save=True,
         )
         test_category = Category.objects.get(slug='hidden-category')
 
@@ -425,8 +444,8 @@ class CategoryThreadsListTests(ThreadsListTestCase):
                         test_category.pk: {
                             'can_see': 1,
                             'can_browse': 0,
-                        }
-                    }
+                        },
+                    },
                 }
             )
 
@@ -441,8 +460,8 @@ class CategoryThreadsListTests(ThreadsListTestCase):
                         test_category.pk: {
                             'can_see': 1,
                             'can_browse': 0,
-                        }
-                    }
+                        },
+                    },
                 }
             )
 
@@ -568,7 +587,9 @@ class ThreadsVisibilityTests(ThreadsListTestCase):
             name='Hidden Category',
             slug='hidden-category',
         ).insert_at(
-            self.root, position='last-child', save=True
+            self.root,
+            position='last-child',
+            save=True,
         )
 
         test_category = Category.objects.get(slug='hidden-category')
@@ -585,7 +606,9 @@ class ThreadsVisibilityTests(ThreadsListTestCase):
             name='Hidden Category',
             slug='hidden-category',
         ).insert_at(
-            self.root, position='last-child', save=True
+            self.root,
+            position='last-child',
+            save=True,
         )
 
         test_category = Category.objects.get(slug='hidden-category')
@@ -786,9 +809,7 @@ class MyThreadsListTests(ThreadsListTestCase):
             poster=self.user,
         )
 
-        other_thread = testutils.post_thread(
-            category=self.category_a,
-        )
+        other_thread = testutils.post_thread(category=self.category_a)
 
         self.access_all_categories()
 
@@ -853,9 +874,7 @@ class NewThreadsListTests(ThreadsListTestCase):
 
     def test_list_renders_new_thread(self):
         """list renders new thread"""
-        test_thread = testutils.post_thread(
-            category=self.category_a,
-        )
+        test_thread = testutils.post_thread(category=self.category_a)
 
         self.access_all_categories()
 
@@ -892,10 +911,14 @@ class NewThreadsListTests(ThreadsListTestCase):
         self.user.save()
 
         test_thread = testutils.post_thread(
-            category=self.category_a, started_on=self.user.joined_on - timedelta(days=2)
+            category=self.category_a,
+            started_on=self.user.joined_on - timedelta(days=2),
         )
 
-        testutils.reply_thread(test_thread, posted_on=self.user.joined_on + timedelta(days=4))
+        testutils.reply_thread(
+            test_thread,
+            posted_on=self.user.joined_on + timedelta(days=4),
+        )
 
         self.access_all_categories()
 
@@ -933,7 +956,7 @@ class NewThreadsListTests(ThreadsListTestCase):
 
         test_thread = testutils.post_thread(
             category=self.category_a,
-            started_on=timezone.now() - timedelta(days=settings.MISAGO_READTRACKER_CUTOFF + 1)
+            started_on=timezone.now() - timedelta(days=settings.MISAGO_READTRACKER_CUTOFF + 1),
         )
 
         self.access_all_categories()
@@ -969,7 +992,8 @@ class NewThreadsListTests(ThreadsListTestCase):
         self.user.save()
 
         test_thread = testutils.post_thread(
-            category=self.category_a, started_on=self.user.joined_on - timedelta(minutes=1)
+            category=self.category_a,
+            started_on=self.user.joined_on - timedelta(minutes=1),
         )
 
         self.access_all_categories()
@@ -1233,7 +1257,7 @@ class UnreadThreadsListTests(ThreadsListTestCase):
 
         test_thread = testutils.post_thread(
             category=self.category_a,
-            started_on=timezone.now() - timedelta(days=settings.MISAGO_READTRACKER_CUTOFF + 5)
+            started_on=timezone.now() - timedelta(days=settings.MISAGO_READTRACKER_CUTOFF + 5),
         )
 
         threadstracker.make_thread_read_aware(self.user, test_thread)
@@ -1276,13 +1300,17 @@ class UnreadThreadsListTests(ThreadsListTestCase):
         self.user.save()
 
         test_thread = testutils.post_thread(
-            category=self.category_a, started_on=self.user.joined_on - timedelta(days=2)
+            category=self.category_a,
+            started_on=self.user.joined_on - timedelta(days=2),
         )
 
         threadstracker.make_thread_read_aware(self.user, test_thread)
         threadstracker.read_thread(self.user, test_thread, test_thread.last_post)
 
-        testutils.reply_thread(test_thread, posted_on=test_thread.started_on + timedelta(days=1))
+        testutils.reply_thread(
+            test_thread,
+            posted_on=test_thread.started_on + timedelta(days=1),
+        )
 
         self.access_all_categories()
 
@@ -1319,7 +1347,8 @@ class UnreadThreadsListTests(ThreadsListTestCase):
         self.user.save()
 
         test_thread = testutils.post_thread(
-            category=self.category_a, started_on=self.user.joined_on - timedelta(days=2)
+            category=self.category_a,
+            started_on=self.user.joined_on - timedelta(days=2),
         )
 
         threadstracker.make_thread_read_aware(self.user, test_thread)
@@ -1462,7 +1491,9 @@ class UnapprovedListTests(ThreadsListTestCase):
 
         # approval perm has no influence on visibility
         for test_url in TEST_URLS:
-            self.access_all_categories(base_acl={'can_see_unapproved_content_lists': True})
+            self.access_all_categories(base_acl={
+                'can_see_unapproved_content_lists': True,
+            })
 
             self.access_all_categories()
             response = self.client.get(test_url)
@@ -1481,8 +1512,11 @@ class UnapprovedListTests(ThreadsListTestCase):
         )
 
         self.access_all_categories({
-            'can_approve_content': True
-        }, {'can_see_unapproved_content_lists': True})
+            'can_approve_content': True,
+        }, {
+            'can_see_unapproved_content_lists': True,
+        })
+
         response = self.client.get('/unapproved/')
         self.assertEqual(response.status_code, 200)
         self.assertContains(response, visible_thread.get_absolute_url())
@@ -1490,7 +1524,10 @@ class UnapprovedListTests(ThreadsListTestCase):
 
         self.access_all_categories({
             'can_approve_content': True
-        }, {'can_see_unapproved_content_lists': True})
+        }, {
+            'can_see_unapproved_content_lists': True,
+        })
+
         response = self.client.get(self.category_a.get_absolute_url() + 'unapproved/')
         self.assertEqual(response.status_code, 200)
         self.assertContains(response, visible_thread.get_absolute_url())
@@ -1499,7 +1536,10 @@ class UnapprovedListTests(ThreadsListTestCase):
         # test api
         self.access_all_categories({
             'can_approve_content': True
-        }, {'can_see_unapproved_content_lists': True})
+        }, {
+            'can_see_unapproved_content_lists': True,
+        })
+
         response = self.client.get('%s?list=unapproved' % self.api_link)
         self.assertEqual(response.status_code, 200)
         self.assertContains(response, visible_thread.get_absolute_url())
@@ -1520,20 +1560,26 @@ class UnapprovedListTests(ThreadsListTestCase):
             is_unapproved=True,
         )
 
-        self.access_all_categories(base_acl={'can_see_unapproved_content_lists': True})
+        self.access_all_categories(base_acl={
+            'can_see_unapproved_content_lists': True,
+        })
         response = self.client.get('/unapproved/')
         self.assertEqual(response.status_code, 200)
         self.assertContains(response, visible_thread.get_absolute_url())
         self.assertNotContains(response, hidden_thread.get_absolute_url())
 
-        self.access_all_categories(base_acl={'can_see_unapproved_content_lists': True})
+        self.access_all_categories(base_acl={
+            'can_see_unapproved_content_lists': True,
+        })
         response = self.client.get(self.category_a.get_absolute_url() + 'unapproved/')
         self.assertEqual(response.status_code, 200)
         self.assertContains(response, visible_thread.get_absolute_url())
         self.assertNotContains(response, hidden_thread.get_absolute_url())
 
         # test api
-        self.access_all_categories(base_acl={'can_see_unapproved_content_lists': True})
+        self.access_all_categories(base_acl={
+            'can_see_unapproved_content_lists': True,
+        })
         response = self.client.get('%s?list=unapproved' % self.api_link)
         self.assertEqual(response.status_code, 200)
         self.assertContains(response, visible_thread.get_absolute_url())

+ 18 - 18
misago/threads/tests/test_threadview.py

@@ -41,7 +41,11 @@ class ThreadViewTestCase(AuthenticatedUserTestCase):
         if acl:
             category_acl.update(acl)
 
-        override_acl(self.user, {'categories': {self.category.pk: category_acl}})
+        override_acl(self.user, {
+            'categories': {
+                self.category.pk: category_acl,
+            },
+        })
 
 
 class ThreadVisibilityTests(ThreadViewTestCase):
@@ -231,10 +235,7 @@ class ThreadEventVisibilityTests(ThreadViewTestCase):
         self.thread.save()
 
         for action, message in TEST_ACTIONS:
-            self.override_acl({
-                'can_approve_content': 1,
-                'can_hide_threads': 1,
-            })
+            self.override_acl({'can_approve_content': 1, 'can_hide_threads': 1})
 
             self.thread.post_set.filter(is_event=True).delete()
             action(MockRequest(self.user), self.thread)
@@ -248,10 +249,7 @@ class ThreadEventVisibilityTests(ThreadViewTestCase):
 
             # hidden events don't render without permission
             hide_post(self.user, event)
-            self.override_acl({
-                'can_approve_content': 1,
-                'can_hide_threads': 1,
-            })
+            self.override_acl({'can_approve_content': 1, 'can_hide_threads': 1})
 
             response = self.client.get(self.thread.get_absolute_url())
             self.assertNotContains(response, event.get_absolute_url())
@@ -398,7 +396,7 @@ class ThreadAttachmentsViewTests(ThreadViewTestCase):
             'filetype': 'ZIP',
             'is_image': False,
             'uploaded_on': '2016-10-22T21:17:40.408710Z',
-            'uploader_name': 'BobBoberson'
+            'uploader_name': 'BobBoberson',
         }
 
         json.update(data)
@@ -413,25 +411,27 @@ class ThreadAttachmentsViewTests(ThreadViewTestCase):
                 'url': {
                     'index': '/attachment/loremipsum-123/',
                     'thumb': None,
-                    'uploader': '/user/bobboberson-123/'
+                    'uploader': '/user/bobboberson-123/',
                 },
                 'filename': 'Archiwum-1.zip',
-            }), self.mock_attachment_cache({
+            }),
+            self.mock_attachment_cache({
                 'url': {
                     'index': '/attachment/loremipsum-223/',
                     'thumb': '/attachment/thumb/loremipsum-223/',
-                    'uploader': '/user/bobboberson-223/'
+                    'uploader': '/user/bobboberson-223/',
                 },
                 'is_image': True,
-                'filename': 'Archiwum-2.zip'
-            }), self.mock_attachment_cache({
+                'filename': 'Archiwum-2.zip',
+            }),
+            self.mock_attachment_cache({
                 'url': {
                     'index': '/attachment/loremipsum-323/',
                     'thumb': None,
-                    'uploader': '/user/bobboberson-323/'
+                    'uploader': '/user/bobboberson-323/',
                 },
-                'filename': 'Archiwum-3.zip'
-            })
+                'filename': 'Archiwum-3.zip',
+            }),
         ]
         post.save()
 

+ 33 - 21
misago/threads/tests/test_utils.py

@@ -27,14 +27,18 @@ class AddCategoriesToItemsTests(MisagoTestCase):
             slug='category-a',
             css_class='showing-category-a',
         ).insert_at(
-            self.root, position='last-child', save=True
+            self.root,
+            position='last-child',
+            save=True,
         )
         Category(
             name='Category E',
             slug='category-e',
             css_class='showing-category-e',
         ).insert_at(
-            self.root, position='last-child', save=True
+            self.root,
+            position='last-child',
+            save=True,
         )
 
         self.root = Category.objects.root_category()
@@ -45,7 +49,9 @@ class AddCategoriesToItemsTests(MisagoTestCase):
             slug='category-b',
             css_class='showing-category-b',
         ).insert_at(
-            self.category_a, position='last-child', save=True
+            self.category_a,
+            position='last-child',
+            save=True,
         )
 
         self.category_b = Category.objects.get(slug='category-b')
@@ -54,14 +60,18 @@ class AddCategoriesToItemsTests(MisagoTestCase):
             slug='category-c',
             css_class='showing-category-c',
         ).insert_at(
-            self.category_b, position='last-child', save=True
+            self.category_b,
+            position='last-child',
+            save=True,
         )
         Category(
             name='Category D',
             slug='category-d',
             css_class='showing-category-d',
         ).insert_at(
-            self.category_b, position='last-child', save=True
+            self.category_b,
+            position='last-child',
+            save=True,
         )
 
         self.category_c = Category.objects.get(slug='category-c')
@@ -73,7 +83,9 @@ class AddCategoriesToItemsTests(MisagoTestCase):
             slug='category-f',
             css_class='showing-category-f',
         ).insert_at(
-            self.category_e, position='last-child', save=True
+            self.category_e,
+            position='last-child',
+            save=True,
         )
 
         self.clear_state()
@@ -178,62 +190,62 @@ class GetThreadIdFromUrlTests(MisagoTestCase):
                 # perfect match
                 'request': MockRequest('https', 'testforum.com', '/discuss/'),
                 'url': 'https://testforum.com/discuss/t/test-thread/123/',
-                'pk': 123
+                'pk': 123,
             },
             {
                 # we don't validate scheme in case site recently moved to https
                 # but user still has old url's saved somewhere
                 'request': MockRequest('http', 'testforum.com', '/discuss/'),
                 'url': 'http://testforum.com/discuss/t/test-thread/432/post/12321/',
-                'pk': 432
+                'pk': 432,
             },
             {
                 # extract thread id from other thread urls
                 'request': MockRequest('https', 'testforum.com', '/discuss/'),
                 'url': 'http://testforum.com/discuss/t/test-thread/432/post/12321/',
-                'pk': 432
+                'pk': 432,
             },
             {
                 # extract thread id from thread page url
                 'request': MockRequest('http', 'testforum.com', '/discuss/'),
                 'url': 'http://testforum.com/discuss/t/test-thread/432/123/',
-                'pk': 432
+                'pk': 432,
             },
             {
                 # extract thread id from thread last post url with relative schema
                 'request': MockRequest('http', 'testforum.com', '/discuss/'),
                 'url': '//testforum.com/discuss/t/test-thread/18/last/',
-                'pk': 18
+                'pk': 18,
             },
             {
                 # extract thread id from url that lacks scheme
                 'request': MockRequest('http', 'testforum.com', ''),
                 'url': 'testforum.com/t/test-thread/12/last/',
-                'pk': 12
+                'pk': 12,
             },
             {
                 # extract thread id from schemaless thread last post url
                 'request': MockRequest('http', 'testforum.com', '/discuss/'),
                 'url': 'testforum.com/discuss/t/test-thread/18/last/',
-                'pk': 18
+                'pk': 18,
             },
             {
                 # extract thread id from url that lacks scheme and hostname
                 'request': MockRequest('http', 'testforum.com', ''),
                 'url': '/t/test-thread/13/',
-                'pk': 13
+                'pk': 13,
             },
             {
                 # extract thread id from url that has port name
                 'request': MockRequest('http', '127.0.0.1:8000', ''),
                 'url': 'https://127.0.0.1:8000/t/test-thread/13/',
-                'pk': 13
+                'pk': 13,
             },
             {
                 # extract thread id from url that isn't trimmed
                 'request': MockRequest('http', '127.0.0.1:8000', ''),
                 'url': '   /t/test-thread/13/   ',
-                'pk': 13
+                'pk': 13,
             }
         )
 
@@ -249,27 +261,27 @@ class GetThreadIdFromUrlTests(MisagoTestCase):
             {
                 # invalid wsgi alias
                 'request': MockRequest('https', 'testforum.com'),
-                'url': 'http://testforum.com/discuss/t/test-thread-123/'
+                'url': 'http://testforum.com/discuss/t/test-thread-123/',
             },
             {
                 # invalid hostname
                 'request': MockRequest('http', 'misago-project.org', '/discuss/'),
-                'url': 'https://testforum.com/discuss/t/test-thread-432/post/12321/'
+                'url': 'https://testforum.com/discuss/t/test-thread-432/post/12321/',
             },
             {
                 # old thread url
                 'request': MockRequest('http', 'testforum.com'),
-                'url': 'https://testforum.com/thread/bobboberson-123/'
+                'url': 'https://testforum.com/thread/bobboberson-123/',
             },
             {
                 # dashed thread url
                 'request': MockRequest('http', 'testforum.com'),
-                'url': 'https://testforum.com/t/bobboberson-123/'
+                'url': 'https://testforum.com/t/bobboberson-123/',
             },
             {
                 # non-thread url
                 'request': MockRequest('http', 'testforum.com'),
-                'url': 'https://testforum.com/user/bobboberson-123/'
+                'url': 'https://testforum.com/user/bobboberson-123/',
             },
             {
                 # rubbish url

+ 41 - 27
misago/threads/testutils.py

@@ -33,7 +33,7 @@ def post_thread(
         'last_post_on': started_on,
         'is_unapproved': is_unapproved,
         'is_hidden': is_hidden,
-        'is_closed': is_closed
+        'is_closed': is_closed,
     }
 
     if is_global:
@@ -101,7 +101,10 @@ def reply_thread(
     }
 
     try:
-        kwargs.update({'poster': poster, 'poster_name': poster.username})
+        kwargs.update({
+            'poster': poster,
+            'poster_name': poster.username,
+        })
     except AttributeError:
         kwargs.update({'poster_name': poster})
 
@@ -127,23 +130,28 @@ def post_poll(thread, poster):
         poster_slug=poster.slug,
         poster_ip='127.0.0.1',
         question="Lorem ipsum dolor met?",
-        choices=[{
-            'hash': 'aaaaaaaaaaaa',
-            'label': 'Alpha',
-            'votes': 1
-        }, {
-            'hash': 'bbbbbbbbbbbb',
-            'label': 'Beta',
-            'votes': 0
-        }, {
-            'hash': 'gggggggggggg',
-            'label': 'Gamma',
-            'votes': 2
-        }, {
-            'hash': 'dddddddddddd',
-            'label': 'Delta',
-            'votes': 1
-        }],
+        choices=[
+            {
+                'hash': 'aaaaaaaaaaaa',
+                'label': 'Alpha',
+                'votes': 1
+            },
+            {
+                'hash': 'bbbbbbbbbbbb',
+                'label': 'Beta',
+                'votes': 0
+            },
+            {
+                'hash': 'gggggggggggg',
+                'label': 'Gamma',
+                'votes': 2
+            },
+            {
+                'hash': 'dddddddddddd',
+                'label': 'Delta',
+                'votes': 1
+            },
+        ],
         allowed_choices=2,
         votes=4
     )
@@ -161,7 +169,7 @@ def post_poll(thread, poster):
         voter_name=user.username,
         voter_slug=user.slug,
         voter_ip='127.0.0.1',
-        choice_hash='aaaaaaaaaaaa'
+        choice_hash='aaaaaaaaaaaa',
     )
 
     # test user voted on third and last choices
@@ -172,7 +180,7 @@ def post_poll(thread, poster):
         voter_name=poster.username,
         voter_slug=poster.slug,
         voter_ip='127.0.0.1',
-        choice_hash='gggggggggggg'
+        choice_hash='gggggggggggg',
     )
     poll.pollvote_set.create(
         category=thread.category,
@@ -181,7 +189,7 @@ def post_poll(thread, poster):
         voter_name=poster.username,
         voter_slug=poster.slug,
         voter_ip='127.0.0.1',
-        choice_hash='dddddddddddd'
+        choice_hash='dddddddddddd',
     )
 
     # somebody else voted on third option before being deleted
@@ -191,7 +199,7 @@ def post_poll(thread, poster):
         voter_name='deleted',
         voter_slug='deleted',
         voter_ip='127.0.0.1',
-        choice_hash='gggggggggggg'
+        choice_hash='gggggggggggg',
     )
 
     return poll
@@ -208,20 +216,26 @@ def like_post(post, liker=None, username=None):
             liker=liker,
             liker_name=liker.username,
             liker_slug=liker.slug,
-            liker_ip='127.0.0.1'
+            liker_ip='127.0.0.1',
         )
 
-        post.last_likes = [{'id': liker.id, 'username': liker.username}] + post.last_likes
+        post.last_likes = [{
+            'id': liker.id,
+            'username': liker.username,
+        }] + post.last_likes
     else:
         like = post.postlike_set.create(
             category=post.category,
             thread=post.thread,
             liker_name=username,
             liker_slug=slugify(username),
-            liker_ip='127.0.0.1'
+            liker_ip='127.0.0.1',
         )
 
-        post.last_likes = [{'id': None, 'username': username}] + post.last_likes
+        post.last_likes = [{
+            'id': None,
+            'username': username,
+        }] + post.last_likes
 
     post.likes += 1
     post.save()

+ 76 - 27
misago/threads/threadtypes/privatethread.py

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

+ 111 - 37
misago/threads/threadtypes/thread.py

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

+ 2 - 3
misago/threads/utils.py

@@ -31,9 +31,8 @@ def add_categories_to_items(root_category, categories, items):
         else:
             # item from other category's scope
             for category in categories:
-                if category.level == 1 and (
-                        category == item.category or category.has_child(item.category)
-                ):
+                category_is_parent = category.has_child(item.category)
+                if category.level == 1 and (category == item.category or category_is_parent):
                     top_categories_map[item.category_id] = category
                     item.top_category = category
 

+ 20 - 12
misago/threads/validators.py

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

+ 13 - 3
misago/threads/viewmodels/category.py

@@ -51,7 +51,7 @@ class ThreadsRootCategory(ViewModel):
     def get_categories(self, request):
         return [Category.objects.root_category()] + list(
             Category.objects.all_categories().filter(
-                id__in=request.user.acl_cache['browseable_categories']
+                id__in=request.user.acl_cache['browseable_categories'],
             ).select_related('parent')
         )
 
@@ -87,6 +87,16 @@ class PrivateThreadsCategory(ViewModel):
 
 
 BasicCategorySerializer = CategorySerializer.subset_fields(
-    'id', 'parent', 'name', 'description', 'is_closed', 'css_class', 'absolute_url', 'api_url',
-    'level', 'lft', 'rght', 'is_read'
+    'id',
+    'parent',
+    'name',
+    'description',
+    'is_closed',
+    'css_class',
+    'absolute_url',
+    'api_url',
+    'level',
+    'lft',
+    'rght',
+    'is_read',
 )

+ 8 - 2
misago/threads/viewmodels/posts.py

@@ -71,7 +71,10 @@ class ViewModel(object):
 
     def get_posts_queryset(self, request, thread):
         queryset = thread.post_set.select_related(
-            'poster', 'poster__rank', 'poster__ban_cache', 'poster__online_tracker'
+            'poster',
+            'poster__rank',
+            'poster__ban_cache',
+            'poster__online_tracker',
         ).filter(is_event=False).order_by('id')
         return exclude_invisible_posts(request.user, thread.category, queryset)
 
@@ -96,7 +99,10 @@ class ViewModel(object):
         return context
 
     def get_template_context(self):
-        return {'posts': self.posts, 'paginator': self.paginator}
+        return {
+            'posts': self.posts,
+            'paginator': self.paginator,
+        }
 
 
 class ThreadPosts(ViewModel):

+ 5 - 3
misago/threads/viewmodels/thread.py

@@ -91,7 +91,7 @@ class ViewModel(BaseViewModel):
             'thread': self._model,
             'poll': self._poll,
             'category': self._model.category,
-            'breadcrumbs': self._model.path
+            'breadcrumbs': self._model.path,
         }
 
 
@@ -103,7 +103,9 @@ class ForumThread(ViewModel):
             queryset = Thread.objects.select_related(*BASE_RELATIONS)
 
         thread = get_object_or_404(
-            queryset, pk=pk, category__tree_id=trees_map.get_tree_id_for_root(THREADS_ROOT_NAME)
+            queryset,
+            pk=pk,
+            category__tree_id=trees_map.get_tree_id_for_root(THREADS_ROOT_NAME),
         )
 
         allow_see_thread(request.user, thread)
@@ -130,7 +132,7 @@ class PrivateThread(ViewModel):
         thread = get_object_or_404(
             queryset,
             pk=pk,
-            category__tree_id=trees_map.get_tree_id_for_root(PRIVATE_THREADS_ROOT_NAME)
+            category__tree_id=trees_map.get_tree_id_for_root(PRIVATE_THREADS_ROOT_NAME),
         )
 
         make_participants_aware(request.user, thread)

+ 3 - 3
misago/threads/viewmodels/threads.py

@@ -123,7 +123,7 @@ class ViewModel(object):
         context = {
             'THREADS': {
                 'results': ThreadsListSerializer(self.threads, many=True).data,
-                'subcategories': [c.pk for c in self.category.children]
+                'subcategories': [c.pk for c in self.category.children],
             },
         }
 
@@ -135,7 +135,7 @@ class ViewModel(object):
             'list_name': self.get_list_name(self.list_type),
             'list_type': self.list_type,
             'threads': self.threads,
-            'paginator': self.paginator
+            'paginator': self.paginator,
         }
 
 
@@ -244,7 +244,7 @@ def filter_read_threads_queryset(user, categories, list_type, queryset):
         read_threads = user.threadread_set.filter(
             category__in=categories,
             thread__last_post_on__gt=cutoff_date,
-            last_read_on__lt=F('thread__last_post_on')
+            last_read_on__lt=F('thread__last_post_on'),
         ).values('thread_id')
 
         queryset = queryset.filter(id__in=read_threads)

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

@@ -25,13 +25,15 @@ class AttachmentsList(AttachmentAdmin, generic.ListView):
                 ('-size', _("Largest files")), )
     selection_label = _('With attachments: 0')
     empty_selection_label = _('Select attachments')
-    mass_actions = [{
-        'action': 'delete',
-        'name': _("Delete attachments"),
-        'icon': 'fa fa-times-circle',
-        'confirmation': _("Are you sure you want to delete selected attachments?"),
-        'is_atomic': False
-    }]
+    mass_actions = [
+        {
+            'action': 'delete',
+            'name': _("Delete attachments"),
+            'icon': 'fa fa-times-circle',
+            'confirmation': _("Are you sure you want to delete selected attachments?"),
+            'is_atomic': False,
+        },
+    ]
 
     def get_search_form(self, request):
         return SearchAttachmentsForm

+ 11 - 4
misago/threads/views/goto.py

@@ -89,7 +89,9 @@ class ThreadGotoNewView(GotoView):
 
     def get_target_post(self, thread, posts_queryset, **kwargs):
         if thread.is_new:
-            return posts_queryset.filter(posted_on__gt=thread.last_read_on).order_by('id').first()
+            return posts_queryset.filter(
+                posted_on__gt=thread.last_read_on,
+            ).order_by('id').first()
         else:
             return posts_queryset.order_by('id').last()
 
@@ -101,12 +103,15 @@ class ThreadGotoUnapprovedView(GotoView):
         if not thread.acl['can_approve']:
             raise PermissionDenied(
                 _(
-                    "You need permission to approve content to be able to go to first unapproved post."
+                    "You need permission to approve content to "
+                    "be able to go to first unapproved post."
                 )
             )
 
     def get_target_post(self, thread, posts_queryset, **kwargs):
-        unapproved_post = posts_queryset.filter(is_unapproved=True).order_by('id').first()
+        unapproved_post = posts_queryset.filter(
+            is_unapproved=True,
+        ).order_by('id').first()
         if unapproved_post:
             return unapproved_post
         else:
@@ -133,6 +138,8 @@ class PrivateThreadGotoNewView(GotoView):
 
     def get_target_post(self, thread, posts_queryset, **kwargs):
         if thread.is_new:
-            return posts_queryset.filter(posted_on__gt=thread.last_read_on).order_by('id').first()
+            return posts_queryset.filter(
+                posted_on__gt=thread.last_read_on,
+            ).order_by('id').first()
         else:
             return posts_queryset.order_by('id').last()

+ 6 - 2
misago/threads/views/thread.py

@@ -45,7 +45,9 @@ class ThreadBase(View):
     def get_template_context(self, request, thread, posts):
         context = {
             'url_name':
-                ':'.join(request.resolver_match.namespaces + [request.resolver_match.url_name])
+                ':'.join(request.resolver_match.namespaces + [
+                    request.resolver_match.url_name,
+                ])
         }
 
         context.update(thread.get_template_context())
@@ -59,7 +61,9 @@ class ThreadView(ThreadBase):
     template_name = 'misago/thread/thread.html'
 
     def get_default_frontend_context(self):
-        return {'THREADS_API': reverse('misago:api:thread-list')}
+        return {
+            'THREADS_API': reverse('misago:api:thread-list'),
+        }
 
 
 class PrivateThreadView(ThreadBase):