Browse Source

wip #887: post and event perm revamp (sans move/merge/split actions)

Rafał Pitoń 7 years ago
parent
commit
ec0442e5f5

+ 8 - 8
misago/threads/api/postendpoints/patch_event.py

@@ -4,6 +4,7 @@ from django.utils.translation import ugettext as _
 from misago.acl import add_acl
 from misago.acl import add_acl
 from misago.core.apipatch import ApiPatch
 from misago.core.apipatch import ApiPatch
 from misago.threads.moderation import posts as moderation
 from misago.threads.moderation import posts as moderation
+from misago.threads.permissions import allow_hide_event, allow_unhide_event
 
 
 
 
 event_patch_dispatcher = ApiPatch()
 event_patch_dispatcher = ApiPatch()
@@ -22,15 +23,14 @@ event_patch_dispatcher.add('acl', patch_acl)
 
 
 
 
 def patch_is_hidden(request, event, value):
 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}
+    if value:
+        allow_hide_event(request.user, event)
+        moderation.hide_post(request.user, event)
     else:
     else:
-        raise PermissionDenied(_("You don't have permission to hide this event."))
+        allow_unhide_event(request.user, event)
+        moderation.unhide_post(request.user, event)
+
+    return {'is_hidden': event.is_hidden}
 
 
 
 
 event_patch_dispatcher.replace('is-hidden', patch_is_hidden)
 event_patch_dispatcher.replace('is-hidden', patch_is_hidden)

+ 7 - 3
misago/threads/api/postendpoints/patch_post.py

@@ -94,9 +94,13 @@ post_patch_dispatcher.replace('is-protected', patch_is_protected)
 
 
 
 
 def patch_is_unapproved(request, post, value):
 def patch_is_unapproved(request, post, value):
-    if value is False:
-        allow_approve_post(request.user, post)
-        moderation.approve_post(request.user, post)
+    allow_approve_post(request.user, post)
+
+    if value:
+        raise PermissionDenied(_("Content approval can't be reversed."))
+
+    moderation.approve_post(request.user, post)
+
     return {'is_unapproved': post.is_unapproved}
     return {'is_unapproved': post.is_unapproved}
 
 
 
 

+ 116 - 48
misago/threads/permissions/threads.py

@@ -54,6 +54,10 @@ __all__ = [
     'can_approve_post',
     'can_approve_post',
     'allow_move_post',
     'allow_move_post',
     'can_move_post',
     'can_move_post',
+    'allow_unhide_event',
+    'can_unhide_event',
+    'allow_hide_event',
+    'can_hide_event',
     'allow_delete_event',
     'allow_delete_event',
     'can_delete_event',
     'can_delete_event',
     'exclude_invisible_threads',
     'exclude_invisible_threads',
@@ -458,8 +462,8 @@ def add_acl_to_thread(user, thread):
         'can_close': category_acl.get('can_close_threads', False),
         'can_close': category_acl.get('can_close_threads', False),
         'can_move': can_move_thread(user, thread),
         'can_move': can_move_thread(user, thread),
         'can_merge': can_merge_thread(user, thread),
         'can_merge': can_merge_thread(user, thread),
-        'can_move_posts': False,
-        'can_merge_posts': False,
+        'can_move_posts': category_acl.get('can_move_posts', False),
+        'can_merge_posts': category_acl.get('can_merge_posts', False),
         'can_approve': can_approve_thread(user, thread),
         'can_approve': can_approve_thread(user, thread),
         'can_see_reports': category_acl.get('can_see_reports', False),
         'can_see_reports': category_acl.get('can_see_reports', False),
     })
     })
@@ -467,19 +471,6 @@ def add_acl_to_thread(user, thread):
     if thread.acl['can_pin'] and category_acl.get('can_pin_threads') == 2:
     if thread.acl['can_pin'] and category_acl.get('can_pin_threads') == 2:
         thread.acl['can_pin_globally'] = True
         thread.acl['can_pin_globally'] = True
 
 
-    if not category_acl.get('can_close_threads'):
-        thread_is_protected = thread.is_closed or thread.category.is_closed
-    else:
-        thread_is_protected = False
-
-    if thread_is_protected:
-        return
-
-    thread.acl.update({
-        'can_move_posts': category_acl.get('can_move_posts', False),
-        'can_merge_posts': category_acl.get('can_merge_posts', False),
-    })
-
 
 
 def add_acl_to_post(user, post):
 def add_acl_to_post(user, post):
     if post.is_event:
     if post.is_event:
@@ -489,16 +480,21 @@ def add_acl_to_post(user, post):
 
 
 
 
 def add_acl_to_event(user, event):
 def add_acl_to_event(user, event):
+    can_hide_events = 0
+
     if user.is_authenticated:
     if user.is_authenticated:
-        category_acl = user.acl_cache['categories'].get(event.category_id, {})
-        can_hide_events = category_acl.get('can_hide_events', 0)
-    else:
-        can_hide_events = 0
+        category_acl = user.acl_cache['categories'].get(
+            event.category_id, {
+                'can_hide_events': 0,
+            }
+        )
+
+        can_hide_events = category_acl['can_hide_events']
 
 
     event.acl.update({
     event.acl.update({
         'can_see_hidden': can_hide_events > 0,
         'can_see_hidden': can_hide_events > 0,
-        'can_hide': can_hide_events > 0,
-        'can_delete': can_hide_events == 2,
+        'can_hide': can_hide_event(user, event),
+        'can_delete': can_delete_event(user, event),
     })
     })
 
 
 
 
@@ -862,12 +858,6 @@ def allow_edit_post(user, target):
     if not category_acl['can_edit_posts']:
     if not category_acl['can_edit_posts']:
         raise PermissionDenied(_("You can't edit posts in this category."))
         raise PermissionDenied(_("You can't edit posts in this category."))
 
 
-    if not category_acl['can_close_threads']:
-        if target.category.is_closed:
-            raise PermissionDenied(_("This category is closed. You can't edit posts in it."))
-        if target.thread.is_closed:
-            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."))
 
 
@@ -886,6 +876,12 @@ def allow_edit_post(user, target):
             )
             )
             raise PermissionDenied(message % {'minutes': category_acl['post_edit_time']})
             raise PermissionDenied(message % {'minutes': category_acl['post_edit_time']})
 
 
+    if not category_acl['can_close_threads']:
+        if target.category.is_closed:
+            raise PermissionDenied(_("This category is closed. You can't edit posts in it."))
+        if target.thread.is_closed:
+            raise PermissionDenied(_("This thread is closed. You can't edit posts in it."))
+
 
 
 can_edit_post = return_boolean(allow_edit_post)
 can_edit_post = return_boolean(allow_edit_post)
 
 
@@ -908,12 +904,6 @@ def allow_unhide_post(user, target):
         if user.id != target.poster_id:
         if user.id != target.poster_id:
             raise PermissionDenied(_("You can't reveal other users posts in this category."))
             raise PermissionDenied(_("You can't reveal other users posts in this category."))
 
 
-        if not category_acl['can_close_threads']:
-            if target.category.is_closed:
-                raise PermissionDenied(_("This category is closed. You can't reveal posts in it."))
-            if target.thread.is_closed:
-                raise PermissionDenied(_("This thread is closed. You can't reveal posts in it."))
-
         if target.is_protected and not category_acl['can_protect_posts']:
         if target.is_protected and not category_acl['can_protect_posts']:
             raise PermissionDenied(_("This post is protected. You can't reveal it."))
             raise PermissionDenied(_("This post is protected. You can't reveal it."))
 
 
@@ -928,6 +918,12 @@ def allow_unhide_post(user, target):
     if target.is_first_post:
     if target.is_first_post:
         raise PermissionDenied(_("You can't reveal thread's first post."))
         raise PermissionDenied(_("You can't reveal thread's first post."))
 
 
+    if not category_acl['can_close_threads']:
+        if target.category.is_closed:
+            raise PermissionDenied(_("This category is closed. You can't reveal posts in it."))
+        if target.thread.is_closed:
+            raise PermissionDenied(_("This thread is closed. You can't reveal posts in it."))
+
 
 
 can_unhide_post = return_boolean(allow_unhide_post)
 can_unhide_post = return_boolean(allow_unhide_post)
 
 
@@ -950,12 +946,6 @@ def allow_hide_post(user, target):
         if user.id != target.poster_id:
         if user.id != target.poster_id:
             raise PermissionDenied(_("You can't hide other users posts in this category."))
             raise PermissionDenied(_("You can't hide other users posts in this category."))
 
 
-        if not category_acl['can_close_threads']:
-            if target.category.is_closed:
-                raise PermissionDenied(_("This category is closed. You can't hide posts in it."))
-            if target.thread.is_closed:
-                raise PermissionDenied(_("This thread is closed. You can't hide posts in it."))
-
         if target.is_protected and not category_acl['can_protect_posts']:
         if target.is_protected and not category_acl['can_protect_posts']:
             raise PermissionDenied(_("This post is protected. You can't hide it."))
             raise PermissionDenied(_("This post is protected. You can't hide it."))
 
 
@@ -970,6 +960,12 @@ 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 not category_acl['can_close_threads']:
+        if target.category.is_closed:
+            raise PermissionDenied(_("This category is closed. You can't hide posts in it."))
+        if target.thread.is_closed:
+            raise PermissionDenied(_("This thread is closed. You can't hide posts in it."))
+
 
 
 can_hide_post = return_boolean(allow_hide_post)
 can_hide_post = return_boolean(allow_hide_post)
 
 
@@ -992,12 +988,6 @@ def allow_delete_post(user, target):
         if user.id != target.poster_id:
         if user.id != target.poster_id:
             raise PermissionDenied(_("You can't delete other users posts in this category."))
             raise PermissionDenied(_("You can't delete other users posts in this category."))
 
 
-        if not category_acl['can_close_threads']:
-            if target.category.is_closed:
-                raise PermissionDenied(_("This category is closed. You can't delete posts in it."))
-            if target.thread.is_closed:
-                raise PermissionDenied(_("This thread is closed. You can't delete posts in it."))
-
         if target.is_protected and not category_acl['can_protect_posts']:
         if target.is_protected and not category_acl['can_protect_posts']:
             raise PermissionDenied(_("This post is protected. You can't delete it."))
             raise PermissionDenied(_("This post is protected. You can't delete it."))
 
 
@@ -1012,6 +1002,12 @@ def allow_delete_post(user, target):
     if target.is_first_post:
     if target.is_first_post:
         raise PermissionDenied(_("You can't delete thread's first post."))
         raise PermissionDenied(_("You can't delete thread's first post."))
 
 
+    if not category_acl['can_close_threads']:
+        if target.category.is_closed:
+            raise PermissionDenied(_("This category is closed. You can't delete posts in it."))
+        if target.thread.is_closed:
+            raise PermissionDenied(_("This thread is closed. You can't delete posts in it."))
+
 
 
 can_delete_post = return_boolean(allow_delete_post)
 can_delete_post = return_boolean(allow_delete_post)
 
 
@@ -1048,6 +1044,12 @@ def allow_approve_post(user, target):
     if not target.is_first_post and not category_acl['can_hide_posts'] and target.is_hidden:
     if not target.is_first_post and not category_acl['can_hide_posts'] and target.is_hidden:
         raise PermissionDenied(_("You can't approve posts the content you can't see."))
         raise PermissionDenied(_("You can't approve posts the content you can't see."))
 
 
+    if not category_acl['can_close_threads']:
+        if target.category.is_closed:
+            raise PermissionDenied(_("This category is closed. You can't approve posts in it."))
+        if target.thread.is_closed:
+            raise PermissionDenied(_("This thread is closed. You can't approve posts in it."))
+
 
 
 can_approve_post = return_boolean(allow_approve_post)
 can_approve_post = return_boolean(allow_approve_post)
 
 
@@ -1056,7 +1058,11 @@ def allow_move_post(user, target):
     if user.is_anonymous:
     if user.is_anonymous:
         raise PermissionDenied(_("You have to sign in to move posts."))
         raise PermissionDenied(_("You have to sign in to move posts."))
 
 
-    category_acl = user.acl_cache['categories'].get(target.category_id, {'can_move_posts': False})
+    category_acl = user.acl_cache['categories'].get(
+        target.category_id, {
+            'can_move_posts': False,
+        }
+    )
 
 
     if not category_acl['can_move_posts']:
     if not category_acl['can_move_posts']:
         raise PermissionDenied(_("You can't move posts in this category."))
         raise PermissionDenied(_("You can't move posts in this category."))
@@ -1067,19 +1073,81 @@ def allow_move_post(user, target):
     if not category_acl['can_hide_posts'] and target.is_hidden:
     if not category_acl['can_hide_posts'] and target.is_hidden:
         raise PermissionDenied(_("You can't move posts the content you can't see."))
         raise PermissionDenied(_("You can't move posts the content you can't see."))
 
 
+    if not category_acl['can_close_threads']:
+        if target.category.is_closed:
+            raise PermissionDenied(_("This category is closed. You can't move posts in it."))
+        if target.thread.is_closed:
+            raise PermissionDenied(_("This thread is closed. You can't move posts in it."))
+
 
 
 can_move_post = return_boolean(allow_move_post)
 can_move_post = return_boolean(allow_move_post)
 
 
 
 
+def allow_unhide_event(user, target):
+    if user.is_anonymous:
+        raise PermissionDenied(_("You have to sign in to reveal events."))
+
+    category_acl = user.acl_cache['categories'].get(
+        target.category_id, {
+            'can_hide_events': 0,
+        }
+    )
+
+    if not category_acl['can_hide_events']:
+        raise PermissionDenied(_("You can't reveal events in this category."))
+
+    if not category_acl['can_close_threads']:
+        if target.category.is_closed:
+            raise PermissionDenied(_("This category is closed. You can't reveal events in it."))
+        if target.thread.is_closed:
+            raise PermissionDenied(_("This thread is closed. You can't reveal events in it."))
+
+
+can_unhide_event = return_boolean(allow_unhide_event)
+
+
+def allow_hide_event(user, target):
+    if user.is_anonymous:
+        raise PermissionDenied(_("You have to sign in to hide events."))
+
+    category_acl = user.acl_cache['categories'].get(
+        target.category_id, {
+            'can_hide_events': 0,
+        }
+    )
+
+    if not category_acl['can_hide_events']:
+        raise PermissionDenied(_("You can't hide events in this category."))
+
+    if not category_acl['can_close_threads']:
+        if target.category.is_closed:
+            raise PermissionDenied(_("This category is closed. You can't hide events in it."))
+        if target.thread.is_closed:
+            raise PermissionDenied(_("This thread is closed. You can't hide events in it."))
+
+
+can_hide_event = return_boolean(allow_hide_event)
+
+
 def allow_delete_event(user, target):
 def allow_delete_event(user, target):
     if user.is_anonymous:
     if user.is_anonymous:
         raise PermissionDenied(_("You have to sign in to delete events."))
         raise PermissionDenied(_("You have to sign in to delete events."))
 
 
-    category_acl = user.acl_cache['categories'].get(target.category_id)
+    category_acl = user.acl_cache['categories'].get(
+        target.category_id, {
+            'can_hide_events': 0,
+        }
+    )
 
 
-    if not category_acl or category_acl['can_hide_events'] != 2:
+    if category_acl['can_hide_events'] != 2:
         raise PermissionDenied(_("You can't delete events in this category."))
         raise PermissionDenied(_("You can't delete events in this category."))
 
 
+    if not category_acl['can_close_threads']:
+        if target.category.is_closed:
+            raise PermissionDenied(_("This category is closed. You can't delete events in it."))
+        if target.thread.is_closed:
+            raise PermissionDenied(_("This thread is closed. You can't delete events in it."))
+
 
 
 can_delete_event = return_boolean(allow_delete_event)
 can_delete_event = return_boolean(allow_delete_event)
 
 

+ 18 - 0
misago/threads/tests/test_thread_editreply_api.py

@@ -1,8 +1,11 @@
 # -*- coding: utf-8 -*-
 # -*- coding: utf-8 -*-
 from __future__ import unicode_literals
 from __future__ import unicode_literals
 
 
+from datetime import timedelta
+
 from django.test.client import BOUNDARY, MULTIPART_CONTENT, encode_multipart
 from django.test.client import BOUNDARY, MULTIPART_CONTENT, encode_multipart
 from django.urls import reverse
 from django.urls import reverse
+from django.utils import timezone
 
 
 from misago.acl.testutils import override_acl
 from misago.acl.testutils import override_acl
 from misago.categories.models import Category
 from misago.categories.models import Category
@@ -86,6 +89,21 @@ class EditReplyTests(AuthenticatedUserTestCase):
             response, "You can't edit other users posts in this category.", status_code=403
             response, "You can't edit other users posts in this category.", status_code=403
         )
         )
 
 
+    def test_edit_too_old(self):
+        """permission to edit reply within timelimit is validated"""
+        self.override_acl({
+            'can_edit_posts': 1,
+            'post_edit_time': 1,
+        })
+
+        self.post.posted_on = timezone.now() - timedelta(minutes=5)
+        self.post.save()
+
+        response = self.put(self.api_link)
+        self.assertContains(
+            response, "You can't edit posts that are older than 1 minute.", status_code=403
+        )
+
     def test_closed_category(self):
     def test_closed_category(self):
         """permssion to edit reply in closed category is validated"""
         """permssion to edit reply in closed category is validated"""
         self.override_acl({'can_close_threads': 0})
         self.override_acl({'can_close_threads': 0})

+ 30 - 0
misago/threads/tests/test_thread_postdelete_api.py

@@ -193,6 +193,36 @@ class EventDeleteApiTests(ThreadsApiTestCase):
         response = self.client.delete(self.api_link)
         response = self.client.delete(self.api_link)
         self.assertContains(response, "You can't delete events in this category.", status_code=403)
         self.assertContains(response, "You can't delete events in this category.", status_code=403)
 
 
+    def test_delete_event_closed_thread_no_permission(self):
+        """api valdiates if user can delete events in closed threads"""
+        self.override_acl({
+            'can_hide_events': 2,
+            'can_close_threads': 0,
+        })
+
+        self.thread.is_closed = True
+        self.thread.save()
+
+        response = self.client.delete(self.api_link)
+        self.assertContains(
+            response, "This thread is closed. You can't delete events in it.", status_code=403
+        )
+
+    def test_delete_event_closed_category_no_permission(self):
+        """api valdiates if user can delete events in closed categories"""
+        self.override_acl({
+            'can_hide_events': 2,
+            'can_close_threads': 0,
+        })
+
+        self.category.is_closed = True
+        self.category.save()
+
+        response = self.client.delete(self.api_link)
+        self.assertContains(
+            response, "This category is closed. You can't delete events in it.", status_code=403
+        )
+
     def test_delete_event(self):
     def test_delete_event(self):
         """api differs posts from events"""
         """api differs posts from events"""
         self.override_acl({
         self.override_acl({

+ 351 - 134
misago/threads/tests/test_thread_postpatch_api.py

@@ -182,7 +182,7 @@ class PostProtectApiTests(ThreadPostPatchApiTestCase):
         self.refresh_post()
         self.refresh_post()
         self.assertTrue(self.post.is_protected)
         self.assertTrue(self.post.is_protected)
 
 
-    def test_unprotect_post_not_editable(self):
+    def test_protect_post_not_editable(self):
         """api validates if we can edit post we want to protect"""
         """api validates if we can edit post we want to protect"""
         self.override_acl({'can_edit_posts': 0, 'can_protect_posts': 1})
         self.override_acl({'can_edit_posts': 0, 'can_protect_posts': 1})
 
 
@@ -203,6 +203,30 @@ class PostProtectApiTests(ThreadPostPatchApiTestCase):
         self.refresh_post()
         self.refresh_post()
         self.assertFalse(self.post.is_protected)
         self.assertFalse(self.post.is_protected)
 
 
+    def test_unprotect_post_not_editable(self):
+        """api validates if we can edit post we want to protect"""
+        self.post.is_protected = True
+        self.post.save()
+
+        self.override_acl({'can_edit_posts': 0, 'can_protect_posts': 1})
+
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-protected',
+                    'value': False,
+                },
+            ]
+        )
+        self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
+        self.assertEqual(response_json['detail'][0], "You can't protect posts you can't edit.")
+
+        self.refresh_post()
+        self.assertTrue(self.post.is_protected)
+
 
 
 class PostApproveApiTests(ThreadPostPatchApiTestCase):
 class PostApproveApiTests(ThreadPostPatchApiTestCase):
     def test_approve_post(self):
     def test_approve_post(self):
@@ -242,7 +266,10 @@ class PostApproveApiTests(ThreadPostPatchApiTestCase):
                 },
                 },
             ]
             ]
         )
         )
-        self.assertEqual(response.status_code, 200)
+        self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
+        self.assertEqual(response_json['detail'][0], "Content approval can't be reversed.")
 
 
         self.refresh_post()
         self.refresh_post()
         self.assertFalse(self.post.is_unapproved)
         self.assertFalse(self.post.is_unapproved)
@@ -271,15 +298,18 @@ class PostApproveApiTests(ThreadPostPatchApiTestCase):
         self.refresh_post()
         self.refresh_post()
         self.assertTrue(self.post.is_unapproved)
         self.assertTrue(self.post.is_unapproved)
 
 
-    def test_approve_first_post(self):
-        """api approve first post fails"""
+    def test_approve_post_closed_thread_no_permission(self):
+        """api validates approval permission in closed threads"""
         self.post.is_unapproved = True
         self.post.is_unapproved = True
         self.post.save()
         self.post.save()
 
 
-        self.thread.set_first_post(self.post)
+        self.thread.is_closed = True
         self.thread.save()
         self.thread.save()
 
 
-        self.override_acl({'can_approve_content': 1})
+        self.override_acl({
+            'can_approve_content': 1,
+            'can_close_threads': 0,
+        })
 
 
         response = self.patch(
         response = self.patch(
             self.api_link, [
             self.api_link, [
@@ -293,18 +323,26 @@ class PostApproveApiTests(ThreadPostPatchApiTestCase):
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         response_json = response.json()
         response_json = response.json()
-        self.assertEqual(response_json['detail'][0], "You can't approve thread's first post.")
+        self.assertEqual(
+            response_json['detail'][0],
+            "This thread is closed. You can't approve posts in it.",
+        )
 
 
         self.refresh_post()
         self.refresh_post()
         self.assertTrue(self.post.is_unapproved)
         self.assertTrue(self.post.is_unapproved)
 
 
-    def test_approve_hidden_post(self):
-        """api approve hidden post fails"""
+    def test_approve_post_closed_category_no_permission(self):
+        """api validates approval permission in closed categories"""
         self.post.is_unapproved = True
         self.post.is_unapproved = True
-        self.post.is_hidden = True
         self.post.save()
         self.post.save()
 
 
-        self.override_acl({'can_approve_content': 1})
+        self.category.is_closed = True
+        self.category.save()
+
+        self.override_acl({
+            'can_approve_content': 1,
+            'can_close_threads': 0,
+        })
 
 
         response = self.patch(
         response = self.patch(
             self.api_link, [
             self.api_link, [
@@ -319,65 +357,72 @@ class PostApproveApiTests(ThreadPostPatchApiTestCase):
 
 
         response_json = response.json()
         response_json = response.json()
         self.assertEqual(
         self.assertEqual(
-            response_json['detail'][0], "You can't approve posts the content you can't see."
+            response_json['detail'][0],
+            "This category is closed. You can't approve posts in it.",
         )
         )
 
 
         self.refresh_post()
         self.refresh_post()
         self.assertTrue(self.post.is_unapproved)
         self.assertTrue(self.post.is_unapproved)
 
 
+    def test_approve_first_post(self):
+        """api approve first post fails"""
+        self.post.is_unapproved = True
+        self.post.save()
 
 
-class PostHideApiTests(ThreadPostPatchApiTestCase):
-    def test_hide_post(self):
-        """api makes it possible to hide post"""
-        self.override_acl({'can_hide_posts': 1})
+        self.thread.set_first_post(self.post)
+        self.thread.save()
+
+        self.override_acl({'can_approve_content': 1})
 
 
         response = self.patch(
         response = self.patch(
             self.api_link, [
             self.api_link, [
                 {
                 {
                     'op': 'replace',
                     'op': 'replace',
-                    'path': 'is-hidden',
-                    'value': True,
+                    'path': 'is-unapproved',
+                    'value': False,
                 },
                 },
             ]
             ]
         )
         )
-        self.assertEqual(response.status_code, 200)
+        self.assertEqual(response.status_code, 400)
 
 
-        reponse_json = response.json()
-        self.assertTrue(reponse_json['is_hidden'])
+        response_json = response.json()
+        self.assertEqual(response_json['detail'][0], "You can't approve thread's first post.")
 
 
         self.refresh_post()
         self.refresh_post()
-        self.assertTrue(self.post.is_hidden)
+        self.assertTrue(self.post.is_unapproved)
 
 
-    def test_show_post(self):
-        """api makes it possible to unhide post"""
+    def test_approve_hidden_post(self):
+        """api approve hidden post fails"""
+        self.post.is_unapproved = True
         self.post.is_hidden = True
         self.post.is_hidden = True
         self.post.save()
         self.post.save()
 
 
-        self.refresh_post()
-        self.assertTrue(self.post.is_hidden)
-
-        self.override_acl({'can_hide_posts': 1})
+        self.override_acl({'can_approve_content': 1})
 
 
         response = self.patch(
         response = self.patch(
             self.api_link, [
             self.api_link, [
                 {
                 {
                     'op': 'replace',
                     'op': 'replace',
-                    'path': 'is-hidden',
+                    'path': 'is-unapproved',
                     'value': False,
                     'value': False,
                 },
                 },
             ]
             ]
         )
         )
-        self.assertEqual(response.status_code, 200)
+        self.assertEqual(response.status_code, 400)
 
 
-        reponse_json = response.json()
-        self.assertFalse(reponse_json['is_hidden'])
+        response_json = response.json()
+        self.assertEqual(
+            response_json['detail'][0], "You can't approve posts the content you can't see."
+        )
 
 
         self.refresh_post()
         self.refresh_post()
-        self.assertFalse(self.post.is_hidden)
+        self.assertTrue(self.post.is_unapproved)
 
 
-    def test_hide_own_post(self):
-        """api makes it possible to hide owned post"""
-        self.override_acl({'can_hide_own_posts': 1})
+
+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(
         response = self.patch(
             self.api_link, [
             self.api_link, [
@@ -396,14 +441,8 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
         self.refresh_post()
         self.refresh_post()
         self.assertTrue(self.post.is_hidden)
         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)
-
+    def test_hide_own_post(self):
+        """api makes it possible to hide owned post"""
         self.override_acl({'can_hide_own_posts': 1})
         self.override_acl({'can_hide_own_posts': 1})
 
 
         response = self.patch(
         response = self.patch(
@@ -411,17 +450,17 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
                 {
                 {
                     'op': 'replace',
                     'op': 'replace',
                     'path': 'is-hidden',
                     'path': 'is-hidden',
-                    'value': False,
+                    'value': True,
                 },
                 },
             ]
             ]
         )
         )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         reponse_json = response.json()
         reponse_json = response.json()
-        self.assertFalse(reponse_json['is_hidden'])
+        self.assertTrue(reponse_json['is_hidden'])
 
 
         self.refresh_post()
         self.refresh_post()
-        self.assertFalse(self.post.is_hidden)
+        self.assertTrue(self.post.is_hidden)
 
 
     def test_hide_post_no_permission(self):
     def test_hide_post_no_permission(self):
         """api hide post with no permission fails"""
         """api hide post with no permission fails"""
@@ -444,39 +483,36 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
         self.refresh_post()
         self.refresh_post()
         self.assertFalse(self.post.is_hidden)
         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
+    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.post.save()
 
 
-        self.refresh_post()
-        self.assertTrue(self.post.is_hidden)
-
-        self.override_acl({'can_hide_posts': 0})
+        self.override_acl({'can_protect_posts': 0, 'can_hide_own_posts': 1})
 
 
         response = self.patch(
         response = self.patch(
             self.api_link, [
             self.api_link, [
                 {
                 {
                     'op': 'replace',
                     'op': 'replace',
                     'path': 'is-hidden',
                     'path': 'is-hidden',
-                    'value': False,
+                    'value': True,
                 },
                 },
             ]
             ]
         )
         )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         response_json = response.json()
         response_json = response.json()
-        self.assertEqual(response_json['detail'][0], "You can't reveal posts in this category.")
+        self.assertEqual(response_json['detail'][0], "This post is protected. You can't hide it.")
 
 
         self.refresh_post()
         self.refresh_post()
-        self.assertTrue(self.post.is_hidden)
+        self.assertFalse(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
+    def test_hide_other_user_post(self):
+        """api validates post ownership when hiding"""
+        self.post.poster = None
         self.post.save()
         self.post.save()
 
 
-        self.override_acl({'can_protect_posts': 0, 'can_hide_own_posts': 1})
+        self.override_acl({'can_hide_own_posts': 1})
 
 
         response = self.patch(
         response = self.patch(
             self.api_link, [
             self.api_link, [
@@ -490,27 +526,26 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         response_json = response.json()
         response_json = response.json()
-        self.assertEqual(response_json['detail'][0], "This post is protected. You can't hide it.")
+        self.assertEqual(
+            response_json['detail'][0], "You can't hide other users posts in this category."
+        )
 
 
         self.refresh_post()
         self.refresh_post()
         self.assertFalse(self.post.is_hidden)
         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
+    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.post.save()
 
 
-        self.override_acl({'can_protect_posts': 0, 'can_hide_own_posts': 1})
-
-        self.post.is_protected = True
-        self.post.save()
+        self.override_acl({'post_edit_time': 1, 'can_hide_own_posts': 1})
 
 
         response = self.patch(
         response = self.patch(
             self.api_link, [
             self.api_link, [
                 {
                 {
                     'op': 'replace',
                     'op': 'replace',
                     'path': 'is-hidden',
                     'path': 'is-hidden',
-                    'value': False,
+                    'value': True,
                 },
                 },
             ]
             ]
         )
         )
@@ -518,16 +553,16 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
 
 
         response_json = response.json()
         response_json = response.json()
         self.assertEqual(
         self.assertEqual(
-            response_json['detail'][0], "This post is protected. You can't reveal it."
+            response_json['detail'][0], "You can't hide posts that are older than 1 minute."
         )
         )
 
 
         self.refresh_post()
         self.refresh_post()
-        self.assertTrue(self.post.is_hidden)
+        self.assertFalse(self.post.is_hidden)
 
 
-    def test_hide_other_user_post(self):
-        """api validates post ownership when hiding"""
-        self.post.poster = None
-        self.post.save()
+    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})
         self.override_acl({'can_hide_own_posts': 1})
 
 
@@ -544,17 +579,16 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
 
 
         response_json = response.json()
         response_json = response.json()
         self.assertEqual(
         self.assertEqual(
-            response_json['detail'][0], "You can't hide other users posts in this category."
+            response_json['detail'][0], "This thread is closed. You can't hide posts in it."
         )
         )
 
 
         self.refresh_post()
         self.refresh_post()
         self.assertFalse(self.post.is_hidden)
         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()
+    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})
         self.override_acl({'can_hide_own_posts': 1})
 
 
@@ -563,7 +597,7 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
                 {
                 {
                     'op': 'replace',
                     'op': 'replace',
                     'path': 'is-hidden',
                     'path': 'is-hidden',
-                    'value': False,
+                    'value': True,
                 },
                 },
             ]
             ]
         )
         )
@@ -571,18 +605,18 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
 
 
         response_json = response.json()
         response_json = response.json()
         self.assertEqual(
         self.assertEqual(
-            response_json['detail'][0], "You can't reveal other users posts in this category."
+            response_json['detail'][0], "This category is closed. You can't hide posts in it."
         )
         )
 
 
         self.refresh_post()
         self.refresh_post()
-        self.assertTrue(self.post.is_hidden)
+        self.assertFalse(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()
+    def test_hide_first_post(self):
+        """api hide first post fails"""
+        self.thread.set_first_post(self.post)
+        self.thread.save()
 
 
-        self.override_acl({'post_edit_time': 1, 'can_hide_own_posts': 1})
+        self.override_acl({'can_hide_posts': 1})
 
 
         response = self.patch(
         response = self.patch(
             self.api_link, [
             self.api_link, [
@@ -596,20 +630,73 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         response_json = response.json()
         response_json = response.json()
-        self.assertEqual(
-            response_json['detail'][0], "You can't hide posts that are older than 1 minute."
+        self.assertEqual(response_json['detail'][0], "You can't hide thread's first post.")
+
+
+class PostUnhideApiTests(ThreadPostPatchApiTestCase):
+    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)
+
+        reponse_json = response.json()
+        self.assertFalse(reponse_json['is_hidden'])
 
 
         self.refresh_post()
         self.refresh_post()
         self.assertFalse(self.post.is_hidden)
         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"""
+    def test_show_own_post(self):
+        """api makes it possible to unhide owned post"""
         self.post.is_hidden = True
         self.post.is_hidden = True
-        self.post.posted_on = timezone.now() - timedelta(minutes=10)
         self.post.save()
         self.post.save()
 
 
-        self.override_acl({'post_edit_time': 1, 'can_hide_own_posts': 1})
+        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)
+
+        reponse_json = response.json()
+        self.assertFalse(reponse_json['is_hidden'])
+
+        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(
         response = self.patch(
             self.api_link, [
             self.api_link, [
@@ -623,26 +710,27 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         response_json = response.json()
         response_json = response.json()
-        self.assertEqual(
-            response_json['detail'][0], "You can't reveal posts that are older than 1 minute."
-        )
+        self.assertEqual(response_json['detail'][0], "You can't reveal posts in this category.")
 
 
         self.refresh_post()
         self.refresh_post()
         self.assertTrue(self.post.is_hidden)
         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()
+    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_hide_own_posts': 1})
+        self.override_acl({'can_protect_posts': 0, 'can_hide_own_posts': 1})
+
+        self.post.is_protected = True
+        self.post.save()
 
 
         response = self.patch(
         response = self.patch(
             self.api_link, [
             self.api_link, [
                 {
                 {
                     'op': 'replace',
                     'op': 'replace',
                     'path': 'is-hidden',
                     'path': 'is-hidden',
-                    'value': True,
+                    'value': False,
                 },
                 },
             ]
             ]
         )
         )
@@ -650,18 +738,16 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
 
 
         response_json = response.json()
         response_json = response.json()
         self.assertEqual(
         self.assertEqual(
-            response_json['detail'][0], "This thread is closed. You can't hide posts in it."
+            response_json['detail'][0], "This post is protected. You can't reveal it."
         )
         )
 
 
         self.refresh_post()
         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.assertTrue(self.post.is_hidden)
 
 
+    def test_show_other_user_post(self):
+        """api validates post ownership when revealing"""
         self.post.is_hidden = True
         self.post.is_hidden = True
+        self.post.poster = None
         self.post.save()
         self.post.save()
 
 
         self.override_acl({'can_hide_own_posts': 1})
         self.override_acl({'can_hide_own_posts': 1})
@@ -679,25 +765,26 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
 
 
         response_json = response.json()
         response_json = response.json()
         self.assertEqual(
         self.assertEqual(
-            response_json['detail'][0], "This thread is closed. You can't reveal posts in it."
+            response_json['detail'][0], "You can't reveal other users posts in this category."
         )
         )
 
 
         self.refresh_post()
         self.refresh_post()
         self.assertTrue(self.post.is_hidden)
         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()
+    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({'can_hide_own_posts': 1})
+        self.override_acl({'post_edit_time': 1, 'can_hide_own_posts': 1})
 
 
         response = self.patch(
         response = self.patch(
             self.api_link, [
             self.api_link, [
                 {
                 {
                     'op': 'replace',
                     'op': 'replace',
                     'path': 'is-hidden',
                     'path': 'is-hidden',
-                    'value': True,
+                    'value': False,
                 },
                 },
             ]
             ]
         )
         )
@@ -705,16 +792,16 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
 
 
         response_json = response.json()
         response_json = response.json()
         self.assertEqual(
         self.assertEqual(
-            response_json['detail'][0], "This category is closed. You can't hide posts in it."
+            response_json['detail'][0], "You can't reveal posts that are older than 1 minute."
         )
         )
 
 
         self.refresh_post()
         self.refresh_post()
-        self.assertFalse(self.post.is_hidden)
+        self.assertTrue(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()
+    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.is_hidden = True
         self.post.save()
         self.post.save()
@@ -734,32 +821,40 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
 
 
         response_json = response.json()
         response_json = response.json()
         self.assertEqual(
         self.assertEqual(
-            response_json['detail'][0], "This category is closed. You can't reveal posts in it."
+            response_json['detail'][0], "This thread is closed. You can't reveal posts in it."
         )
         )
 
 
         self.refresh_post()
         self.refresh_post()
         self.assertTrue(self.post.is_hidden)
         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()
+    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.override_acl({'can_hide_posts': 1})
+        self.post.is_hidden = True
+        self.post.save()
+
+        self.override_acl({'can_hide_own_posts': 1})
 
 
         response = self.patch(
         response = self.patch(
             self.api_link, [
             self.api_link, [
                 {
                 {
                     'op': 'replace',
                     'op': 'replace',
                     'path': 'is-hidden',
                     'path': 'is-hidden',
-                    'value': True,
+                    'value': False,
                 },
                 },
             ]
             ]
         )
         )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
 
 
         response_json = response.json()
         response_json = response.json()
-        self.assertEqual(response_json['detail'][0], "You can't hide thread's first post.")
+        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_show_first_post(self):
     def test_show_first_post(self):
         """api unhide first post fails"""
         """api unhide first post fails"""
@@ -1095,7 +1190,65 @@ class EventHideApiTests(ThreadEventPatchApiTestCase):
 
 
         response_json = response.json()
         response_json = response.json()
         self.assertEqual(
         self.assertEqual(
-            response_json['detail'][0], "You don't have permission to hide this event."
+            response_json['detail'][0], "You can't hide events in this category."
+        )
+
+        self.refresh_event()
+        self.assertFalse(self.event.is_hidden)
+
+    def test_hide_event_closed_thread_no_permission(self):
+        """api hide event in closed thread with no permission fails"""
+        self.override_acl({
+            'can_hide_events': 1,
+            'can_close_threads': 0,
+        })
+
+        self.thread.is_closed = True
+        self.thread.save()
+
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-hidden',
+                    'value': True,
+                },
+            ]
+        )
+        self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
+        self.assertEqual(
+            response_json['detail'][0], "This thread is closed. You can't hide events in it."
+        )
+
+        self.refresh_event()
+        self.assertFalse(self.event.is_hidden)
+
+    def test_hide_event_closed_category_no_permission(self):
+        """api hide event in closed category with no permission fails"""
+        self.override_acl({
+            'can_hide_events': 1,
+            'can_close_threads': 0,
+        })
+
+        self.category.is_closed = True
+        self.category.save()
+
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-hidden',
+                    'value': True,
+                },
+            ]
+        )
+        self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
+        self.assertEqual(
+            response_json['detail'][0], "This category is closed. You can't hide events in it."
         )
         )
 
 
         self.refresh_event()
         self.refresh_event()
@@ -1121,3 +1274,67 @@ class EventHideApiTests(ThreadEventPatchApiTestCase):
             ]
             ]
         )
         )
         self.assertEqual(response.status_code, 404)
         self.assertEqual(response.status_code, 404)
+
+    def test_show_event_closed_thread_no_permission(self):
+        """api show event in closed thread with no permission fails"""
+        self.event.is_hidden = True
+        self.event.save()
+
+        self.override_acl({
+            'can_hide_events': 1,
+            'can_close_threads': 0,
+        })
+
+        self.thread.is_closed = True
+        self.thread.save()
+
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-hidden',
+                    'value': False,
+                },
+            ]
+        )
+        self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
+        self.assertEqual(
+            response_json['detail'][0], "This thread is closed. You can't reveal events in it."
+        )
+
+        self.refresh_event()
+        self.assertTrue(self.event.is_hidden)
+
+    def test_show_event_closed_category_no_permission(self):
+        """api show event in closed category with no permission fails"""
+        self.event.is_hidden = True
+        self.event.save()
+
+        self.override_acl({
+            'can_hide_events': 1,
+            'can_close_threads': 0,
+        })
+
+        self.category.is_closed = True
+        self.category.save()
+
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-hidden',
+                    'value': False,
+                },
+            ]
+        )
+        self.assertEqual(response.status_code, 400)
+
+        response_json = response.json()
+        self.assertEqual(
+            response_json['detail'][0], "This category is closed. You can't reveal events in it."
+        )
+
+        self.refresh_event()
+        self.assertTrue(self.event.is_hidden)