Browse Source

#458: approving posts

Rafał Pitoń 10 years ago
parent
commit
c89c26a008

+ 3 - 1
misago/templates/misago/thread/post.html

@@ -149,13 +149,15 @@
             </button>
           </form>
 
-          <form action="" method="post">
+          {% if post.acl.can_approve %}
+          <form action="{% url 'misago:approve_post' post_id=post.id %}" method="post">
             {% csrf_token %}
             <button type="submit" class="btn btn-success btn-flat pull-right">
               <span class="fa fa-check">
               {% trans "Approve" %}
             </button>
           </form>
+          {% endif %}
 
           <form action="" method="post">
             {% csrf_token %}

+ 9 - 0
misago/threads/moderation/posts.py

@@ -5,6 +5,15 @@ from django.utils.translation import ugettext as _
 from misago.threads.moderation.exceptions import ModerationError
 
 
+def approve_post(user, post):
+    if post.is_moderated:
+        post.is_moderated = False
+        post.save(update_fields=['is_moderated'])
+        return True
+    else:
+        return False
+
+
 def protect_post(user, post):
     if not post.is_protected:
         post.is_protected = True

+ 12 - 3
misago/threads/permissions.py

@@ -331,6 +331,9 @@ def add_acl_to_post(user, post):
         'can_approve': forum_acl.get('can_review_moderated_content'),
     })
 
+    if not post.is_moderated:
+        post.acl['can_approve'] = False
+
     if not post.acl['can_see_hidden']:
         if user.is_authenticated() and user.id == post.poster_id:
             post.acl['can_see_hidden'] = True
@@ -353,8 +356,14 @@ def allow_see_thread(user, target):
     forum_acl = user.acl['forums'].get(target.forum_id, {})
     if not forum_acl.get('can_browse'):
         raise Http404()
-    if not forum_acl.get('can_see_all_threads'):
-        if user.is_anonymous() or user.pk != target.starter_id:
+
+    if user.is_anonymous() or user.pk != target.starter_id:
+        if not forum_acl.get('can_see_all_threads'):
+            raise Http404()
+        if target.is_moderated:
+            if not forum_acl.get('can_review_moderated_content'):
+                raise Http404()
+        if target.is_hidden and not forum_acl.get('can_hide_threads'):
             raise Http404()
 can_see_thread = return_boolean(allow_see_thread)
 
@@ -723,7 +732,7 @@ def exclude_all_invisible_threads(queryset, user):
 def exclude_invisible_posts(queryset, user, forum):
     if not forum.acl['can_review_moderated_content']:
         if user.is_authenticated():
-            condition_author = Q(poster=user.id)
+            condition_author = Q(poster_id=user.id)
             condition = Q(is_moderated=False)
             queryset = queryset.filter(condition_author | condition)
         else:

+ 37 - 0
misago/threads/tests/test_post_views.py

@@ -33,6 +33,43 @@ class PostViewTestCase(AuthenticatedUserTestCase):
         override_acl(self.user, forums_acl)
 
 
+class ApprovePostViewTests(PostViewTestCase):
+    def test_approve_thread(self):
+        """view approves thread"""
+        self.thread.first_post.is_moderated = True
+        self.thread.first_post.save()
+
+        self.thread.synchronize()
+        self.thread.save()
+
+        post_link = reverse('misago:approve_post', kwargs={
+            'post_id': self.thread.first_post_id
+        })
+
+        self.override_acl({'can_review_moderated_content': 1})
+        response = self.client.post(post_link)
+        self.assertEqual(response.status_code, 302)
+
+        thread = Thread.objects.get(id=self.thread.id)
+        self.assertFalse(thread.is_moderated)
+        self.assertFalse(thread.has_moderated_posts)
+
+    def test_approve_post(self):
+        """view approves post"""
+        post = reply_thread(self.thread, is_moderated=True)
+
+        post_link = reverse('misago:approve_post', kwargs={
+            'post_id': post.id
+        })
+
+        self.override_acl({'can_review_moderated_content': 1})
+        response = self.client.post(post_link)
+        self.assertEqual(response.status_code, 302)
+
+        thread = Thread.objects.get(id=self.thread.id)
+        self.assertFalse(thread.has_moderated_posts)
+
+
 class UnhidePostViewTests(PostViewTestCase):
     def test_unhide_first_post(self):
         """attempt to reveal first post in thread fails"""

+ 59 - 7
misago/threads/tests/test_thread_view.py

@@ -204,6 +204,29 @@ class ThreadViewModerationTests(ThreadViewTestCase):
         self.assertEqual(response.status_code, 302)
         self.assertFalse(self.reload_thread().is_pinned)
 
+    def test_approve_thread(self):
+        """its possible to approve moderated thread"""
+        self.thread.first_post.poster = self.user
+        self.thread.first_post.poster_name = self.user.username
+        self.thread.first_post.is_moderated = True
+        self.thread.first_post.save()
+
+        self.thread.synchronize()
+        self.thread.save()
+
+        self.override_acl({'can_review_moderated_content': 1})
+        response = self.client.post(self.thread.get_absolute_url(),
+                                    data={'thread_action': 'approve'})
+        self.assertEqual(response.status_code, 302)
+
+        self.assertFalse(self.reload_thread().is_moderated)
+        self.assertFalse(self.reload_thread().first_post.is_moderated)
+
+        self.override_acl({'can_review_moderated_content': 1})
+        response = self.client.post(self.thread.get_absolute_url(),
+                                    data={'thread_action': 'approve'})
+        self.assertEqual(response.status_code, 200)
+
     def test_close_thread(self):
         """its possible to close thread"""
         self.override_acl({'can_close_threads': 0})
@@ -283,14 +306,11 @@ class ThreadViewModerationTests(ThreadViewTestCase):
         self.assertEqual(response.status_code, 200)
 
     def test_unhide_thread(self):
-        """its possible to hide thread"""
-        self.thread.is_hidden = True
-        self.thread.save()
-
-        self.override_acl({'can_hide_threads': 0})
+        """its possible to unhide thread"""
+        self.override_acl({'can_hide_threads': 2})
         response = self.client.post(self.thread.get_absolute_url(),
-                                    data={'thread_action': 'unhide'})
-        self.assertEqual(response.status_code, 200)
+                                    data={'thread_action': 'hide'})
+        self.assertEqual(response.status_code, 302)
 
         self.override_acl({'can_hide_threads': 2})
         response = self.client.post(self.thread.get_absolute_url(),
@@ -314,6 +334,38 @@ class ThreadViewModerationTests(ThreadViewTestCase):
         response = self.client.get(reverse('misago:index'))
         self.assertEqual(response.status_code, 200)
 
+    def test_approve_posts(self):
+        """moderation allows for approving multiple posts"""
+        posts = []
+        for p in xrange(4):
+            posts.append(reply_thread(self.thread, is_moderated=True))
+        for p in xrange(4):
+            posts.append(reply_thread(self.thread))
+
+        self.assertTrue(self.reload_thread().has_moderated_posts)
+        self.assertEqual(self.thread.replies, 4)
+
+        test_acl = {
+            'can_review_moderated_content': 1
+        }
+
+        self.override_acl(test_acl)
+        response = self.client.get(self.thread.get_absolute_url())
+        self.assertEqual(response.status_code, 200)
+        self.assertIn("Approve posts", response.content)
+
+        self.override_acl(test_acl)
+        response = self.client.post(self.thread.get_absolute_url(), data={
+            'action': 'approve', 'item': [p.pk for p in posts]
+        })
+        self.assertEqual(response.status_code, 302)
+
+        self.assertFalse(self.reload_thread().has_moderated_posts)
+        self.assertEqual(self.reload_thread().replies, 8)
+
+        for post in posts:
+            self.assertFalse(self.thread.post_set.get(id=post.id).is_moderated)
+
     def test_merge_posts(self):
         """moderation allows for merging multiple posts into one"""
         posts = []

+ 9 - 6
misago/threads/urls.py

@@ -41,15 +41,18 @@ urlpatterns += patterns('',
 )
 
 
-from misago.threads.views.post import (QuotePostView, HidePostView,
-                                       UnhidePostView, DeletePostView)
+from misago.threads.views.post import (QuotePostView, ApprovePostView,
+                                       HidePostView, UnhidePostView,
+                                       DeletePostView)
 urlpatterns += patterns('',
-    url(r'^post/(?P<post_id>\d+)/quote$', QuotePostView.as_view(), name='quote_post'),
-    url(r'^post/(?P<post_id>\d+)/unhide$', UnhidePostView.as_view(), name='unhide_post'),
-    url(r'^post/(?P<post_id>\d+)/hide$', HidePostView.as_view(), name='hide_post'),
-    url(r'^post/(?P<post_id>\d+)/delete$', DeletePostView.as_view(), name='delete_post'),
+    url(r'^post/(?P<post_id>\d+)/quote/$', QuotePostView.as_view(), name='quote_post'),
+    url(r'^post/(?P<post_id>\d+)/approve/$', ApprovePostView.as_view(), name='approve_post'),
+    url(r'^post/(?P<post_id>\d+)/unhide/$', UnhidePostView.as_view(), name='unhide_post'),
+    url(r'^post/(?P<post_id>\d+)/hide/$', HidePostView.as_view(), name='hide_post'),
+    url(r'^post/(?P<post_id>\d+)/delete/$', DeletePostView.as_view(), name='delete_post'),
 )
 
+
 # new threads list
 from misago.threads.views.newthreads import NewThreadsView, clear_new_threads
 urlpatterns += patterns('',

+ 25 - 0
misago/threads/views/generic/thread/postsactions.py

@@ -71,6 +71,14 @@ class PostsActions(ActionsBase):
 
         actions = []
 
+        if self.thread.acl['can_review']:
+            if self.thread.has_moderated_posts:
+                actions.append({
+                    'action': 'approve',
+                    'icon': 'check',
+                    'name': _("Approve posts")
+                })
+
         if self.forum.acl['can_merge_posts']:
             actions.append({
                 'action': 'merge',
@@ -127,6 +135,23 @@ class PostsActions(ActionsBase):
         return actions
 
     @changes_thread_state
+    def action_approve(self, request, posts):
+        changed_posts = 0
+        for post in posts:
+            if moderation.approve_post(request.user, post):
+                changed_posts += 1
+
+        if changed_posts:
+            message = ungettext(
+                '%(changed)d post was approved.',
+                '%(changed)d posts were approved.',
+            changed_posts)
+            messages.success(request, message % {'changed': changed_posts})
+        else:
+            message = _("No posts were approved.")
+            messages.info(request, message)
+
+    @changes_thread_state
     def action_merge(self, request, posts):
         first_post = posts[0]
 

+ 4 - 0
misago/threads/views/generic/thread/threadactions.py

@@ -133,6 +133,10 @@ class ThreadActions(ActionsBase):
         moderation.unpin_thread(request.user, thread)
         messages.success(request, _("Thread was unpinned."))
 
+    def action_approve(self, request, thread):
+        moderation.approve_thread(request.user, thread)
+        messages.success(request, _("Thread was approved."))
+
     move_thread_full_template = 'misago/thread/move/full.html'
     move_thread_modal_template = 'misago/thread/move/modal.html'
 

+ 19 - 0
misago/threads/views/post.py

@@ -1,4 +1,5 @@
 from django.contrib import messages
+from django.core.exceptions import PermissionDenied
 from django.db.transaction import atomic
 from django.http import JsonResponse
 from django.shortcuts import redirect, render
@@ -60,6 +61,24 @@ class QuotePostView(PostView):
         })
 
 
+class ApprovePostView(PostView):
+    def real_dispatch(self, request, post):
+        if not post.acl['can_approve']:
+            raise PermissionDenied(_("You can't approve this post."))
+
+        if post.id == post.thread.first_post_id:
+            moderation.approve_thread(request.user, post.thread)
+            messages.success(request, _("Thread has been approved."))
+        else:
+            moderation.approve_post(request.user, post)
+            messages.success(request, _("Post has been approved."))
+
+        post.thread.synchronize()
+        post.thread.save()
+        post.forum.synchronize()
+        post.forum.save()
+
+
 class UnhidePostView(PostView):
     is_atomic = False