Browse Source

protect/unprotect posts

Rafał Pitoń 10 years ago
parent
commit
24098bf2eb

+ 4 - 0
misago/templates/misago/thread/post.html

@@ -33,6 +33,10 @@
           {{ post.posted_on|date }}
           {{ post.posted_on|date }}
         </a>
         </a>
 
 
+        {% if post.is_protected %}
+        <span class="fa fa-lock tooltip-top" title="{% trans "This post is protected from being changed by its author." %}"></span>
+        {% endif %}
+
         {% if not post.is_read %}
         {% if not post.is_read %}
         <span class="label label-success">
         <span class="label label-success">
           <span class="fa fa-plus-circle"></span>
           <span class="fa fa-plus-circle"></span>

+ 18 - 2
misago/threads/moderation/posts.py

@@ -5,7 +5,24 @@ from django.utils.translation import ugettext as _
 from misago.threads.moderation.exceptions import ModerationError
 from misago.threads.moderation.exceptions import ModerationError
 
 
 
 
-@atomic
+def protect_post(user, post):
+    if not post.is_protected:
+        post.is_protected = True
+        post.save(update_fields=['is_protected'])
+        return True
+    else:
+        return False
+
+
+def unprotect_post(user, post):
+    if post.is_protected:
+        post.is_protected = False
+        post.save(update_fields=['is_protected'])
+        return True
+    else:
+        return False
+
+
 def unhide_post(user, post):
 def unhide_post(user, post):
     if post.pk == post.thread.first_post_id:
     if post.pk == post.thread.first_post_id:
         raise ModerationError(_("You can't make original post "
         raise ModerationError(_("You can't make original post "
@@ -19,7 +36,6 @@ def unhide_post(user, post):
         return False
         return False
 
 
 
 
-@atomic
 def hide_post(user, post):
 def hide_post(user, post):
     if post.pk == post.thread.first_post_id:
     if post.pk == post.thread.first_post_id:
         raise ModerationError(_("You can't hide original "
         raise ModerationError(_("You can't hide original "

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

@@ -158,7 +158,7 @@ def unhide_thread(user, thread):
 @atomic
 @atomic
 def hide_thread(user, thread):
 def hide_thread(user, thread):
     if not thread.is_hidden:
     if not thread.is_hidden:
-        message = _("%(user)s hid thread.")
+        message = _("%(user)s hidden thread.")
         record_event(user, thread, "eye-slash", message, {'user': user})
         record_event(user, thread, "eye-slash", message, {'user': user})
 
 
         thread.first_post.is_hidden = True
         thread.first_post.is_hidden = True

+ 100 - 0
misago/threads/tests/test_posts_moderation.py

@@ -0,0 +1,100 @@
+from misago.forums.models import Forum
+from misago.users.testutils import AuthenticatedUserTestCase
+
+from misago.threads import moderation, testutils
+from misago.threads.models import Thread, Post
+
+
+class PostsModerationTests(AuthenticatedUserTestCase):
+    def setUp(self):
+        super(PostsModerationTests, self).setUp()
+
+        self.forum = Forum.objects.all_forums().filter(role="forum")[:1][0]
+        self.thread = testutils.post_thread(self.forum)
+        self.post = testutils.reply_thread(self.thread)
+
+    def reload_thread(self):
+        self.thread = Thread.objects.get(pk=self.thread.pk)
+
+    def reload_post(self):
+        self.post = Post.objects.get(pk=self.post.pk)
+
+    def test_hide_original_post(self):
+        """hide_post fails for first post in thread"""
+        with self.assertRaises(moderation.ModerationError):
+            moderation.hide_post(self.user, self.thread.first_post)
+
+    def test_protect_post(self):
+        """protect_post protects post"""
+        self.assertFalse(self.post.is_protected)
+        self.assertTrue(moderation.protect_post(self.user, self.post))
+
+        self.reload_post()
+        self.assertTrue(self.post.is_protected)
+
+    def test_protect_protected_post(self):
+        """protect_post fails to protect protected post gracefully"""
+        self.post.is_protected = True
+        self.assertFalse(moderation.protect_post(self.user, self.post))
+
+    def test_unprotect_post(self):
+        """unprotect_post releases post protection"""
+        self.post.is_protected = True
+        self.assertTrue(moderation.unprotect_post(self.user, self.post))
+
+        self.reload_post()
+        self.assertFalse(self.post.is_protected)
+
+    def test_unprotect_protected_post(self):
+        """unprotect_post fails to unprotect unprotected post gracefully"""
+        self.assertFalse(moderation.unprotect_post(self.user, self.post))
+
+    def test_hide_post(self):
+        """hide_post hides post"""
+        self.assertFalse(self.post.is_hidden)
+        self.assertTrue(moderation.hide_post(self.user, self.post))
+
+        self.reload_post()
+        self.assertTrue(self.post.is_hidden)
+        self.assertEqual(self.post.hidden_by, self.user)
+        self.assertEqual(self.post.hidden_by_name, self.user.username)
+        self.assertEqual(self.post.hidden_by_slug, self.user.slug)
+        self.assertIsNotNone(self.post.hidden_on)
+
+    def test_hide_hidden_post(self):
+        """hide_post fails to hide hidden post gracefully"""
+        self.post.is_hidden = True
+        self.assertFalse(moderation.hide_post(self.user, self.post))
+
+    def test_unhide_original_post(self):
+        """unhide_post fails for first post in thread"""
+        with self.assertRaises(moderation.ModerationError):
+            moderation.unhide_post(self.user, self.thread.first_post)
+
+    def test_unhide_post(self):
+        """unhide_post reveals post"""
+        self.post.is_hidden = True
+
+        self.assertTrue(self.post.is_hidden)
+        self.assertTrue(moderation.unhide_post(self.user, self.post))
+
+        self.reload_post()
+        self.assertFalse(self.post.is_hidden)
+
+    def test_unhide_visible_post(self):
+        """unhide_post fails to reveal visible post gracefully"""
+        self.assertFalse(moderation.unhide_post(self.user, self.post))
+
+    def test_delete_original_post(self):
+        """delete_post fails for first post in thread"""
+        with self.assertRaises(moderation.ModerationError):
+            moderation.delete_post(self.user, self.thread.first_post)
+
+    def test_delete_post(self):
+        """delete_post deletes thread post"""
+        self.assertTrue(moderation.delete_post(self.user, self.post))
+        with self.assertRaises(Post.DoesNotExist):
+            self.reload_post()
+
+        self.thread.synchronize()
+        self.assertEqual(self.thread.replies, 0)

+ 56 - 4
misago/threads/tests/test_thread_view.py

@@ -343,6 +343,62 @@ class ThreadViewModerationTests(ThreadViewTestCase):
         })
         })
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
+    def test_protect_unprotect_posts(self):
+        """moderation allows for protecting and unprotecting multiple posts"""
+        posts = [reply_thread(self.thread) for t in xrange(4)]
+
+        self.thread.synchronize()
+        self.assertEqual(self.thread.replies, 4)
+
+        test_acl = {
+            'can_protect_posts': 1
+        }
+
+        self.override_acl(test_acl)
+        response = self.client.get(self.thread.get_absolute_url())
+        self.assertEqual(response.status_code, 200)
+        self.assertIn("Protect posts", response.content)
+        self.assertIn("Release posts", response.content)
+
+        self.override_acl(test_acl)
+        response = self.client.post(self.thread.get_absolute_url(), data={
+            'action': 'protect', 'item': [p.pk for p in posts[:2]]
+        })
+        self.assertEqual(response.status_code, 302)
+
+        thread = Thread.objects.get(pk=self.thread.pk)
+        self.assertEqual(thread.replies, 4)
+
+        self.override_acl(test_acl)
+        response = self.client.post(self.thread.get_absolute_url(), data={
+            'action': 'protect', 'item': [p.pk for p in posts[:2]]
+        })
+        self.assertEqual(response.status_code, 302)
+
+        thread = Thread.objects.get(pk=self.thread.pk)
+        self.assertEqual(thread.replies, 4)
+
+        posts_queryset = self.thread.post_set
+        for post in posts_queryset.filter(id__in=[p.pk for p in posts[:2]]):
+            self.assertTrue(post.is_protected)
+
+        response = self.client.get(self.thread.get_absolute_url())
+        self.assertEqual(response.status_code, 200)
+        self.assertIn('This post is protected', response.content)
+
+        self.override_acl(test_acl)
+        response = self.client.post(self.thread.get_absolute_url(), data={
+            'action': 'unprotect', 'item': [p.pk for p in posts[:2]]
+        })
+        self.assertEqual(response.status_code, 302)
+
+        response = self.client.get(self.thread.get_absolute_url())
+        self.assertEqual(response.status_code, 200)
+        self.assertNotIn('This post is protected', response.content)
+
+        for post in posts_queryset.filter(id__in=[p.pk for p in posts[:2]]):
+            self.assertFalse(post.is_protected)
+
     def test_hide_unhide_posts(self):
     def test_hide_unhide_posts(self):
         """moderation allows for hiding and unhiding multiple posts"""
         """moderation allows for hiding and unhiding multiple posts"""
         posts = [reply_thread(self.thread) for t in xrange(4)]
         posts = [reply_thread(self.thread) for t in xrange(4)]
@@ -381,10 +437,6 @@ class ThreadViewModerationTests(ThreadViewTestCase):
         posts_queryset = self.thread.post_set
         posts_queryset = self.thread.post_set
         for post in posts_queryset.filter(id__in=[p.pk for p in posts[:2]]):
         for post in posts_queryset.filter(id__in=[p.pk for p in posts[:2]]):
             self.assertTrue(post.is_hidden)
             self.assertTrue(post.is_hidden)
-            self.assertIsNotNone(post.hidden_by)
-            self.assertIsNotNone(post.hidden_by_name)
-            self.assertIsNotNone(post.hidden_by_slug)
-            self.assertIsNotNone(post.hidden_on)
 
 
         self.override_acl(test_acl)
         self.override_acl(test_acl)
         response = self.client.post(self.thread.get_absolute_url(), data={
         response = self.client.post(self.thread.get_absolute_url(), data={

+ 44 - 1
misago/threads/views/generic/thread/postsactions.py

@@ -58,6 +58,18 @@ class PostsActions(ActionsBase):
 
 
         actions = []
         actions = []
 
 
+        if self.forum.acl['can_protect_posts']:
+            actions.append({
+                'action': 'unprotect',
+                'icon': 'unlock-alt',
+                'name': _("Release posts")
+            })
+            actions.append({
+                'action': 'protect',
+                'icon': 'lock',
+                'name': _("Protect posts")
+            })
+
         if self.forum.acl['can_hide_posts']:
         if self.forum.acl['can_hide_posts']:
             actions.append({
             actions.append({
                 'action': 'unhide',
                 'action': 'unhide',
@@ -80,6 +92,38 @@ class PostsActions(ActionsBase):
 
 
         return actions
         return actions
 
 
+    def action_unprotect(self, request, posts):
+        changed_posts = 0
+        for post in posts:
+            if moderation.unprotect_post(request.user, post):
+                changed_posts += 1
+
+        if changed_posts:
+            message = ungettext(
+                '%(changed)d post was released from protection.',
+                '%(changed)d posts were released from protection.',
+            changed_posts)
+            messages.success(request, message % {'changed': changed_posts})
+        else:
+            message = _("No posts were released from protection.")
+            messages.info(request, message)
+
+    def action_protect(self, request, posts):
+        changed_posts = 0
+        for post in posts:
+            if moderation.protect_post(request.user, post):
+                changed_posts += 1
+
+        if changed_posts:
+            message = ungettext(
+                '%(changed)d post was made protected.',
+                '%(changed)d posts were made protected.',
+            changed_posts)
+            messages.success(request, message % {'changed': changed_posts})
+        else:
+            message = _("No posts were made protected.")
+            messages.info(request, message)
+
     @atomic_post_action
     @atomic_post_action
     def action_unhide(self, request, posts):
     def action_unhide(self, request, posts):
         changed_posts = 0
         changed_posts = 0
@@ -113,7 +157,6 @@ class PostsActions(ActionsBase):
         else:
         else:
             message = _("No posts were hidden.")
             message = _("No posts were hidden.")
             messages.info(request, message)
             messages.info(request, message)
-        pass
 
 
     @atomic_post_action
     @atomic_post_action
     def action_delete(self, request, posts):
     def action_delete(self, request, posts):