Browse Source

wip moderation actions backend for posts

Rafał Pitoń 8 years ago
parent
commit
a008b6b5ca

+ 5 - 10
misago/core/apipatch.py

@@ -72,21 +72,16 @@ class ApiPatch(object):
             raise InvalidAction(u"undefined op")
             raise InvalidAction(u"undefined op")
 
 
         if action.get('op') not in ALLOWED_OPS:
         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'):
         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:
         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):
     def dispatch_action(self, patch, request, target, action):
         for handler in self._actions:
         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():
                 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.posts import ThreadPosts
 from ..viewmodels.thread import ForumThread
 from ..viewmodels.thread import ForumThread
 from .postingendpoint import PostingEndpoint
 from .postingendpoint import PostingEndpoint
+from .postendpoints.patch_event import event_patch_endpoint
+from .postendpoints.patch_post import post_patch_endpoint
 
 
 
 
 class ViewSet(viewsets.ViewSet):
 class ViewSet(viewsets.ViewSet):
@@ -129,6 +131,16 @@ class ViewSet(viewsets.ViewSet):
         return Response({})
         return Response({})
 
 
     @transaction.atomic
     @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):
     def delete(self, request, thread_pk, pk):
         thread = self.get_thread_for_update(request, thread_pk)
         thread = self.get_thread_for_update(request, thread_pk)
         post = self.get_post_for_update(request, thread, pk).post
         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(
     can_hide_own_posts = forms.TypedChoiceField(
         label=_("Can hide own posts"),
         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,
         coerce=int,
         initial=0,
         initial=0,
         choices=(
         choices=(
@@ -565,7 +564,7 @@ def allow_edit_post(user, target):
             raise PermissionDenied(_("This thread is closed. You can't edit posts in it."))
             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']:
     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 category_acl['can_edit_posts'] == 1:
         if target.poster_id != user.pk:
         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']})
             raise PermissionDenied(message % {'minutes': category_acl['post_edit_time']})
 
 
     if target.is_first_post:
     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:
     if not target.is_hidden:
         raise PermissionDenied(_("Only hidden posts can be revealed."))
         raise PermissionDenied(_("Only hidden posts can be revealed."))
 can_unhide_post = return_boolean(allow_unhide_post)
 can_unhide_post = return_boolean(allow_unhide_post)
@@ -651,7 +650,7 @@ def allow_hide_post(user, target):
     if target.is_first_post:
     if target.is_first_post:
         raise PermissionDenied(_("You can't hide thread's first post."))
         raise PermissionDenied(_("You can't hide thread's first post."))
     if target.is_hidden:
     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)
 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):
 class ThreadAddAclApiTests(ThreadPatchApiTestCase):
-    def test_add_thread_acl(self):
+    def test_add_acl_true(self):
         """api adds current thread's acl to response"""
         """api adds current thread's acl to response"""
         response = self.patch(self.api_link, [
         response = self.patch(self.api_link, [
             {'op': 'add', 'path': 'acl', 'value': True}
             {'op': 'add', 'path': 'acl', 'value': True}
@@ -25,7 +25,7 @@ class ThreadAddAclApiTests(ThreadPatchApiTestCase):
         response_json = json.loads(smart_str(response.content))
         response_json = json.loads(smart_str(response.content))
         self.assertTrue(response_json['acl'])
         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"""
         """if value is false, api won't add acl to the response, but will set empty key"""
         response = self.patch(self.api_link, [
         response = self.patch(self.api_link, [
             {'op': 'add', 'path': 'acl', 'value': False}
             {'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 = get_object_or_404(queryset, pk=pk)
 
 
+        post.thread = thread.thread
         post.category = thread.category
         post.category = thread.category
 
 
         return post
         return post