Просмотр исходного кода

wip moderation actions backend for posts

Rafał Pitoń 8 лет назад
Родитель
Сommit
a008b6b5ca

+ 5 - 10
misago/core/apipatch.py

@@ -72,21 +72,16 @@ class ApiPatch(object):
             raise InvalidAction(u"undefined op")
 
         if action.get('op') not in ALLOWED_OPS:
-            raise InvalidAction(
-                u'"%s" op is unsupported' % action.get('op'))
+            raise InvalidAction(u'"%s" op is unsupported' % action.get('op'))
 
         if not action.get('path'):
-            raise InvalidAction(
-                u'"%s" op has to specify path' % action.get('op'))
+            raise InvalidAction(u'"%s" op has to specify path' % action.get('op'))
 
         if 'value' not in action:
-            raise InvalidAction(
-                u'"%s" op has to specify value' % action.get('op'))
+            raise InvalidAction(u'"%s" op has to specify value' % action.get('op'))
 
     def dispatch_action(self, patch, request, target, action):
         for handler in self._actions:
-            if (action['op'] == handler['op'] and
-                    action['path'] == handler['path']):
+            if action['op'] == handler['op'] and action['path'] == handler['path']:
                 with transaction.atomic():
-                    patch.update(
-                        handler['handler'](request, target, action['value']))
+                    patch.update(handler['handler'](request, target, action['value']))

+ 0 - 0
misago/threads/api/postendpoints/__init__.py


+ 46 - 0
misago/threads/api/postendpoints/patch_event.py

@@ -0,0 +1,46 @@
+from django.core.exceptions import PermissionDenied
+from django.utils.translation import gettext as _
+
+from misago.acl import add_acl
+from misago.core.apipatch import ApiPatch
+from ...moderation import posts as moderation
+
+
+event_patch_dispatcher = ApiPatch()
+
+
+def patch_acl(request, event, value):
+    """useful little op that updates event acl to current state"""
+    if value:
+        add_acl(request.user, event)
+        return {'acl': event.acl}
+    else:
+        return {'acl': None}
+event_patch_dispatcher.add('acl', patch_acl)
+
+
+def patch_is_hidden(request, event, value):
+    if event.acl.get('can_hide'):
+        if value:
+            moderation.hide_post(request.user, event)
+        else:
+            moderation.unhide_post(request.user, event)
+
+        return {'is_hidden': event.is_hidden}
+    else:
+        raise PermissionDenied(_("You don't have permission to hide this event."))
+event_patch_dispatcher.replace('is-hidden', patch_is_hidden)
+
+
+def event_patch_endpoint(request, event):
+    old_is_hidden = event.is_hidden
+
+    response = event_patch_dispatcher.dispatch(request, event)
+
+    if old_is_hidden != event.is_hidden:
+        event.thread.synchronize()
+        event.thread.save()
+
+        event.category.synchronize()
+        event.category.save()
+    return response

+ 63 - 0
misago/threads/api/postendpoints/patch_post.py

@@ -0,0 +1,63 @@
+from django.core.exceptions import PermissionDenied
+from django.utils.translation import gettext as _
+
+from misago.acl import add_acl
+from misago.core.apipatch import ApiPatch
+from ...moderation import posts as moderation
+from ...permissions.threads import allow_hide_post, allow_unhide_post
+
+
+post_patch_dispatcher = ApiPatch()
+
+
+def patch_acl(request, post, value):
+    """useful little op that updates post acl to current state"""
+    if value:
+        add_acl(request.user, post)
+        return {'acl': post.acl}
+    else:
+        return {'acl': None}
+post_patch_dispatcher.add('acl', patch_acl)
+
+
+def patch_is_hidden(request, post, value):
+    if value is True:
+        allow_hide_post(request.user, post)
+        moderation.hide_post(request.user, post)
+    elif value is False:
+        allow_unhide_post(request.user, post)
+        moderation.unhide_post(request.user, post)
+
+    return {'is_hidden': post.is_hidden}
+post_patch_dispatcher.replace('is-hidden', patch_is_hidden)
+
+
+def post_patch_endpoint(request, post):
+    old_is_hidden = post.is_hidden
+    old_is_unapproved = post.is_unapproved
+    old_thread = post.thread
+    old_category = post.category
+
+    response = post_patch_dispatcher.dispatch(request, post)
+
+    # diff posts's state against pre-patch and resync category if necessary
+    hidden_changed = old_is_hidden != post.is_hidden
+    unapproved_changed = old_is_unapproved != post.is_unapproved
+    thread_changed = old_thread != post.thread
+    category_changed = old_category != post.category
+
+    if hidden_changed or unapproved_changed or thread_changed or category_changed:
+        post.thread.synchronize()
+        post.thread.save()
+
+        post.category.synchronize()
+        post.category.save()
+
+        if thread_changed:
+            old_thread.synchronize()
+            old_thread.save()
+
+        if category_changed:
+            old_category.synchronize()
+            old_category.save()
+    return response

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

@@ -18,6 +18,8 @@ from ..viewmodels.post import ThreadPost
 from ..viewmodels.posts import ThreadPosts
 from ..viewmodels.thread import ForumThread
 from .postingendpoint import PostingEndpoint
+from .postendpoints.patch_event import event_patch_endpoint
+from .postendpoints.patch_post import post_patch_endpoint
 
 
 class ViewSet(viewsets.ViewSet):
@@ -129,6 +131,16 @@ class ViewSet(viewsets.ViewSet):
         return Response({})
 
     @transaction.atomic
+    def partial_update(self, request, thread_pk, pk):
+        thread = self.get_thread_for_update(request, thread_pk)
+        post = self.get_post_for_update(request, thread, pk).post
+
+        if post.is_event:
+            return event_patch_endpoint(request, post)
+        else:
+            return post_patch_endpoint(request, post)
+
+    @transaction.atomic
     def delete(self, request, thread_pk, pk):
         thread = self.get_thread_for_update(request, thread_pk)
         post = self.get_post_for_update(request, thread, pk).post

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

@@ -144,8 +144,7 @@ class CategoryPermissionsForm(forms.Form):
     )
     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."),
+        help_text=_("Only last posts to thread made within edit time limit can be hidden."),
         coerce=int,
         initial=0,
         choices=(
@@ -565,7 +564,7 @@ def allow_edit_post(user, target):
             raise PermissionDenied(_("This thread is closed. You can't edit posts in it."))
 
     if target.is_hidden and not target.is_first_post and not category_acl['can_hide_posts']:
-            raise PermissionDenied(_("This post is hidden, you can't edit it."))
+        raise PermissionDenied(_("This post is hidden, you can't edit it."))
 
     if category_acl['can_edit_posts'] == 1:
         if target.poster_id != user.pk:
@@ -613,7 +612,7 @@ def allow_unhide_post(user, target):
             raise PermissionDenied(message % {'minutes': category_acl['post_edit_time']})
 
     if target.is_first_post:
-        raise PermissionDenied(_("You can't hide thread's first post."))
+        raise PermissionDenied(_("You can't reveal thread's first post."))
     if not target.is_hidden:
         raise PermissionDenied(_("Only hidden posts can be revealed."))
 can_unhide_post = return_boolean(allow_unhide_post)
@@ -651,7 +650,7 @@ def allow_hide_post(user, target):
     if target.is_first_post:
         raise PermissionDenied(_("You can't hide thread's first post."))
     if target.is_hidden:
-        raise PermissionDenied(_("Only visible posts can be hidden."))
+        raise PermissionDenied(_("Only visible posts can be made hidden."))
 can_hide_post = return_boolean(allow_hide_post)
 
 

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

@@ -15,7 +15,7 @@ class ThreadPatchApiTestCase(ThreadsApiTestCase):
 
 
 class ThreadAddAclApiTests(ThreadPatchApiTestCase):
-    def test_add_thread_acl(self):
+    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}
@@ -25,7 +25,7 @@ class ThreadAddAclApiTests(ThreadPatchApiTestCase):
         response_json = json.loads(smart_str(response.content))
         self.assertTrue(response_json['acl'])
 
-    def test_add_thread_acl(self):
+    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}

+ 598 - 0
misago/threads/tests/test_threadpost_patch_api.py

@@ -0,0 +1,598 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+import json
+from datetime import timedelta
+
+from django.core.urlresolvers import reverse
+from django.test.client import BOUNDARY, MULTIPART_CONTENT, encode_multipart
+from django.utils import timezone
+from django.utils.encoding import smart_str
+
+from misago.acl.testutils import override_acl
+from misago.categories.models import Category
+from misago.users.testutils import AuthenticatedUserTestCase
+
+from .. import testutils
+from ..models import Thread
+
+
+class ThreadPostPatchApiTestCase(AuthenticatedUserTestCase):
+    def setUp(self):
+        super(ThreadPostPatchApiTestCase, self).setUp()
+
+        self.category = Category.objects.get(slug='first-category')
+        self.thread = testutils.post_thread(category=self.category)
+        self.post = testutils.reply_thread(self.thread, poster=self.user)
+
+        self.api_link = reverse('misago:api:thread-post-detail', kwargs={
+            'thread_pk': self.thread.pk,
+            'pk': self.post.pk
+        })
+
+    def patch(self, api_link, ops):
+        return self.client.patch(api_link, json.dumps(ops), content_type="application/json")
+
+    def refresh_post(self):
+        self.post = self.thread.post_set.get(pk=self.post.pk)
+
+    def override_acl(self, extra_acl=None):
+        new_acl = self.user.acl
+        new_acl['categories'][self.category.pk].update({
+            'can_see': 1,
+            'can_browse': 1,
+            'can_start_threads': 0,
+            'can_reply_threads': 0,
+            'can_edit_posts': 1
+        })
+
+        if extra_acl:
+            new_acl['categories'][self.category.pk].update(extra_acl)
+
+        override_acl(self.user, new_acl)
+
+
+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}
+        ])
+        self.assertEqual(response.status_code, 200)
+
+        response_json = json.loads(smart_str(response.content))
+        self.assertTrue(response_json['acl'])
+
+    def test_add_acl_false(self):
+        """if value is false, api won't add acl to the response, but will set empty key"""
+        response = self.patch(self.api_link, [
+            {'op': 'add', 'path': 'acl', 'value': False}
+        ])
+        self.assertEqual(response.status_code, 200)
+
+        response_json = json.loads(smart_str(response.content))
+        self.assertIsNone(response_json['acl'])
+
+        response = self.patch(self.api_link, [
+            {'op': 'add', 'path': 'acl', 'value': True}
+        ])
+        self.assertEqual(response.status_code, 200)
+
+
+class PostHideApiTests(ThreadPostPatchApiTestCase):
+    def test_hide_post(self):
+        """api makes it possible to hide post"""
+        self.override_acl({
+            'can_hide_posts': 1
+        })
+
+        response = self.patch(self.api_link, [
+            {'op': 'replace', 'path': 'is-hidden', 'value': True}
+        ])
+        self.assertEqual(response.status_code, 200)
+
+        self.refresh_post()
+        self.assertTrue(self.post.is_hidden)
+
+    def test_show_post(self):
+        """api makes it possible to unhide post"""
+        self.post.is_hidden = True
+        self.post.save()
+
+        self.refresh_post()
+        self.assertTrue(self.post.is_hidden)
+
+        self.override_acl({
+            'can_hide_posts': 1
+        })
+
+        response = self.patch(self.api_link, [
+            {'op': 'replace', 'path': 'is-hidden', 'value': False}
+        ])
+        self.assertEqual(response.status_code, 200)
+
+        self.refresh_post()
+        self.assertFalse(self.post.is_hidden)
+
+    def test_hide_own_post(self):
+        """api makes it possible to hide owned post"""
+        self.override_acl({
+            'can_hide_own_posts': 1
+        })
+
+        response = self.patch(self.api_link, [
+            {'op': 'replace', 'path': 'is-hidden', 'value': True}
+        ])
+        self.assertEqual(response.status_code, 200)
+
+        self.refresh_post()
+        self.assertTrue(self.post.is_hidden)
+
+    def test_show_own_post(self):
+        """api makes it possible to unhide owned post"""
+        self.post.is_hidden = True
+        self.post.save()
+
+        self.refresh_post()
+        self.assertTrue(self.post.is_hidden)
+
+        self.override_acl({
+            'can_hide_own_posts': 1
+        })
+
+        response = self.patch(self.api_link, [
+            {'op': 'replace', 'path': 'is-hidden', 'value': False}
+        ])
+        self.assertEqual(response.status_code, 200)
+
+        self.refresh_post()
+        self.assertFalse(self.post.is_hidden)
+
+    def test_hide_post_already_hidden(self):
+        """api hide hidden post fails"""
+        self.post.is_hidden = True
+        self.post.save()
+
+        self.refresh_post()
+        self.assertTrue(self.post.is_hidden)
+
+        self.override_acl({
+            'can_hide_posts': 1
+        })
+
+        response = self.patch(self.api_link, [
+            {'op': 'replace', 'path': 'is-hidden', 'value': True}
+        ])
+        self.assertEqual(response.status_code, 400)
+
+        response_json = json.loads(smart_str(response.content))
+        self.assertEqual(response_json['detail'][0], "Only visible posts can be made hidden.")
+
+        self.refresh_post()
+        self.assertTrue(self.post.is_hidden)
+
+    def test_show_post_already_visible(self):
+        """api unhide visible post fails"""
+        self.override_acl({
+            'can_hide_posts': 1
+        })
+
+        response = self.patch(self.api_link, [
+            {'op': 'replace', 'path': 'is-hidden', 'value': False}
+        ])
+        self.assertEqual(response.status_code, 400)
+
+        response_json = json.loads(smart_str(response.content))
+        self.assertEqual(response_json['detail'][0], "Only hidden posts can be revealed.")
+
+    def test_hide_post_no_permission(self):
+        """api hide post with no permission fails"""
+        self.override_acl({
+            'can_hide_posts': 0
+        })
+
+        response = self.patch(self.api_link, [
+            {'op': 'replace', 'path': 'is-hidden', 'value': True}
+        ])
+        self.assertEqual(response.status_code, 400)
+
+        response_json = json.loads(smart_str(response.content))
+        self.assertEqual(response_json['detail'][0], "You can't hide posts in this category.")
+
+        self.refresh_post()
+        self.assertFalse(self.post.is_hidden)
+
+    def test_show_post_no_permission(self):
+        """api unhide post with no permission fails"""
+        self.post.is_hidden = True
+        self.post.save()
+
+        self.refresh_post()
+        self.assertTrue(self.post.is_hidden)
+
+        self.override_acl({
+            'can_hide_posts': 0
+        })
+
+        response = self.patch(self.api_link, [
+            {'op': 'replace', 'path': 'is-hidden', 'value': False}
+        ])
+        self.assertEqual(response.status_code, 400)
+
+        response_json = json.loads(smart_str(response.content))
+        self.assertEqual(response_json['detail'][0], "You can't reveal posts in this category.")
+
+        self.refresh_post()
+        self.assertTrue(self.post.is_hidden)
+
+    def test_hide_own_protected_post(self):
+        """api validates if we are trying to hide protected post"""
+        self.post.is_protected = True
+        self.post.save()
+
+        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.assertEqual(response.status_code, 400)
+
+        response_json = json.loads(smart_str(response.content))
+        self.assertEqual(response_json['detail'][0], "This post is protected. You can't hide it.")
+
+        self.refresh_post()
+        self.assertFalse(self.post.is_hidden)
+
+    def test_show_own_protected_post(self):
+        """api validates if we are trying to reveal protected post"""
+        self.post.is_hidden = True
+        self.post.save()
+
+        self.override_acl({
+            'can_protect_posts': 0,
+            'can_hide_own_posts': 1
+        })
+
+        self.post.is_protected = True
+        self.post.save()
+
+        response = self.patch(self.api_link, [
+            {'op': 'replace', 'path': 'is-hidden', 'value': False}
+        ])
+        self.assertEqual(response.status_code, 400)
+
+        response_json = json.loads(smart_str(response.content))
+        self.assertEqual(response_json['detail'][0], "This post is protected. You can't reveal it.")
+
+        self.refresh_post()
+        self.assertTrue(self.post.is_hidden)
+
+    def test_hide_other_user_post(self):
+        """api validates post ownership when hiding"""
+        self.post.poster = None
+        self.post.save()
+
+        self.override_acl({
+            'can_hide_own_posts': 1
+        })
+
+        response = self.patch(self.api_link, [
+            {'op': 'replace', 'path': 'is-hidden', 'value': True}
+        ])
+        self.assertEqual(response.status_code, 400)
+
+        response_json = json.loads(smart_str(response.content))
+        self.assertEqual(response_json['detail'][0], "You can't hide other users posts in this category.")
+
+        self.refresh_post()
+        self.assertFalse(self.post.is_hidden)
+
+    def test_show_other_user_post(self):
+        """api validates post ownership when revealing"""
+        self.post.is_hidden = True
+        self.post.poster = None
+        self.post.save()
+
+        self.override_acl({
+            'can_hide_own_posts': 1
+        })
+
+        response = self.patch(self.api_link, [
+            {'op': 'replace', 'path': 'is-hidden', 'value': False}
+        ])
+        self.assertEqual(response.status_code, 400)
+
+        response_json = json.loads(smart_str(response.content))
+        self.assertEqual(response_json['detail'][0], "You can't reveal other users posts in this category.")
+
+        self.refresh_post()
+        self.assertTrue(self.post.is_hidden)
+
+    def test_hide_own_post_after_edit_time(self):
+        """api validates if we are trying to hide post after edit time"""
+        self.post.posted_on = timezone.now() - timedelta(minutes=10)
+        self.post.save()
+
+        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.assertEqual(response.status_code, 400)
+
+        response_json = json.loads(smart_str(response.content))
+        self.assertEqual(response_json['detail'][0], "You can't hide posts that are older than 1 minute.")
+
+        self.refresh_post()
+        self.assertFalse(self.post.is_hidden)
+
+    def test_show_own_post_after_edit_time(self):
+        """api validates if we are trying to reveal post after edit time"""
+        self.post.is_hidden = True
+        self.post.posted_on = timezone.now() - timedelta(minutes=10)
+        self.post.save()
+
+        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.assertEqual(response.status_code, 400)
+
+        response_json = json.loads(smart_str(response.content))
+        self.assertEqual(response_json['detail'][0], "You can't reveal posts that are older than 1 minute.")
+
+        self.refresh_post()
+        self.assertTrue(self.post.is_hidden)
+
+    def test_hide_post_in_closed_thread(self):
+        """api validates if we are trying to hide post in closed thread"""
+        self.thread.is_closed = True
+        self.thread.save()
+
+        self.override_acl({
+            'can_hide_own_posts': 1
+        })
+
+        response = self.patch(self.api_link, [
+            {'op': 'replace', 'path': 'is-hidden', 'value': True}
+        ])
+        self.assertEqual(response.status_code, 400)
+
+        response_json = json.loads(smart_str(response.content))
+        self.assertEqual(response_json['detail'][0], "This thread is closed. You can't hide posts in it.")
+
+        self.refresh_post()
+        self.assertFalse(self.post.is_hidden)
+
+    def test_show_post_in_closed_thread(self):
+        """api validates if we are trying to reveal post in closed thread"""
+        self.thread.is_closed = True
+        self.thread.save()
+
+        self.post.is_hidden = True
+        self.post.save()
+
+        self.override_acl({
+            'can_hide_own_posts': 1
+        })
+
+        response = self.patch(self.api_link, [
+            {'op': 'replace', 'path': 'is-hidden', 'value': False}
+        ])
+        self.assertEqual(response.status_code, 400)
+
+        response_json = json.loads(smart_str(response.content))
+        self.assertEqual(response_json['detail'][0], "This thread is closed. You can't reveal posts in it.")
+
+        self.refresh_post()
+        self.assertTrue(self.post.is_hidden)
+
+    def test_hide_post_in_closed_category(self):
+        """api validates if we are trying to hide post in closed category"""
+        self.category.is_closed = True
+        self.category.save()
+
+        self.override_acl({
+            'can_hide_own_posts': 1
+        })
+
+        response = self.patch(self.api_link, [
+            {'op': 'replace', 'path': 'is-hidden', 'value': True}
+        ])
+        self.assertEqual(response.status_code, 400)
+
+        response_json = json.loads(smart_str(response.content))
+        self.assertEqual(response_json['detail'][0], "This category is closed. You can't hide posts in it.")
+
+        self.refresh_post()
+        self.assertFalse(self.post.is_hidden)
+
+    def test_show_post_in_closed_category(self):
+        """api validates if we are trying to reveal post in closed category"""
+        self.category.is_closed = True
+        self.category.save()
+
+        self.post.is_hidden = True
+        self.post.save()
+
+        self.override_acl({
+            'can_hide_own_posts': 1
+        })
+
+        response = self.patch(self.api_link, [
+            {'op': 'replace', 'path': 'is-hidden', 'value': False}
+        ])
+        self.assertEqual(response.status_code, 400)
+
+        response_json = json.loads(smart_str(response.content))
+        self.assertEqual(response_json['detail'][0], "This category is closed. You can't reveal posts in it.")
+
+        self.refresh_post()
+        self.assertTrue(self.post.is_hidden)
+
+    def test_hide_first_post(self):
+        """api hide first post fails"""
+        self.thread.set_first_post(self.post)
+        self.thread.save()
+
+        self.override_acl({
+            'can_hide_posts': 1
+        })
+
+        response = self.patch(self.api_link, [
+            {'op': 'replace', 'path': 'is-hidden', 'value': True}
+        ])
+        self.assertEqual(response.status_code, 400)
+
+        response_json = json.loads(smart_str(response.content))
+        self.assertEqual(response_json['detail'][0], "You can't hide thread's first post.")
+
+    def test_show_first_post(self):
+        """api unhide first post fails"""
+        self.thread.set_first_post(self.post)
+        self.thread.save()
+
+        self.override_acl({
+            'can_hide_posts': 1
+        })
+
+        response = self.patch(self.api_link, [
+            {'op': 'replace', 'path': 'is-hidden', 'value': False}
+        ])
+        self.assertEqual(response.status_code, 400)
+
+        response_json = json.loads(smart_str(response.content))
+        self.assertEqual(response_json['detail'][0], "You can't reveal thread's first post.")
+
+
+class ThreadEventPatchApiTestCase(ThreadPostPatchApiTestCase):
+    def setUp(self):
+        super(ThreadEventPatchApiTestCase, self).setUp()
+
+        self.event = testutils.reply_thread(self.thread, poster=self.user, is_event=True)
+
+        self.api_link = reverse('misago:api:thread-post-detail', kwargs={
+            'thread_pk': self.thread.pk,
+            'pk': self.event.pk
+        })
+
+    def refresh_event(self):
+        self.event = self.thread.post_set.get(pk=self.event.pk)
+
+
+class EventAnonPatchApiTests(ThreadEventPatchApiTestCase):
+    def test_anonymous_user(self):
+        """anonymous users can't change event state"""
+        self.logout_user()
+
+        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}
+        ])
+        self.assertEqual(response.status_code, 200)
+
+        response_json = json.loads(smart_str(response.content))
+        self.assertTrue(response_json['acl'])
+
+    def test_add_acl_false(self):
+        """if value is false, api won't add acl to the response, but will set empty key"""
+        response = self.patch(self.api_link, [
+            {'op': 'add', 'path': 'acl', 'value': False}
+        ])
+        self.assertEqual(response.status_code, 200)
+
+        response_json = json.loads(smart_str(response.content))
+        self.assertIsNone(response_json['acl'])
+
+        response = self.patch(self.api_link, [
+            {'op': 'add', 'path': 'acl', 'value': True}
+        ])
+        self.assertEqual(response.status_code, 200)
+
+
+class EventHideApiTests(ThreadEventPatchApiTestCase):
+    def test_hide_event(self):
+        """api makes it possible to hide event"""
+        self.override_acl({
+            'can_hide_events': 1
+        })
+
+        response = self.patch(self.api_link, [
+            {'op': 'replace', 'path': 'is-hidden', 'value': True}
+        ])
+        self.assertEqual(response.status_code, 200)
+
+        self.refresh_event()
+        self.assertTrue(self.event.is_hidden)
+
+    def test_show_event(self):
+        """api makes it possible to unhide event"""
+        self.event.is_hidden = True
+        self.event.save()
+
+        self.refresh_event()
+        self.assertTrue(self.event.is_hidden)
+
+        self.override_acl({
+            'can_hide_events': 1
+        })
+
+        response = self.patch(self.api_link, [
+            {'op': 'replace', 'path': 'is-hidden', 'value': False}
+        ])
+        self.assertEqual(response.status_code, 200)
+
+        self.refresh_event()
+        self.assertFalse(self.event.is_hidden)
+
+    def test_hide_event_no_permission(self):
+        """api hide event with no permission fails"""
+        self.override_acl({
+            'can_hide_events': 0
+        })
+
+        response = self.patch(self.api_link, [
+            {'op': 'replace', 'path': 'is-hidden', 'value': True}
+        ])
+        self.assertEqual(response.status_code, 400)
+
+        response_json = json.loads(smart_str(response.content))
+        self.assertEqual(response_json['detail'][0], "You don't have permission to hide this event.")
+
+        self.refresh_event()
+        self.assertFalse(self.event.is_hidden)
+
+    def test_show_event_no_permission(self):
+        """api unhide event with no permission fails"""
+        self.event.is_hidden = True
+        self.event.save()
+
+        self.refresh_event()
+        self.assertTrue(self.event.is_hidden)
+
+        self.override_acl({
+            'can_hide_events': 0
+        })
+
+        response = self.patch(self.api_link, [
+            {'op': 'replace', 'path': 'is-hidden', 'value': False}
+        ])
+        self.assertEqual(response.status_code, 404)

+ 1 - 0
misago/threads/viewmodels/post.py

@@ -26,6 +26,7 @@ class ViewModel(object):
 
         post = get_object_or_404(queryset, pk=pk)
 
+        post.thread = thread.thread
         post.category = thread.category
 
         return post