Browse Source

#458: hide/delete individual post

Rafał Pitoń 10 years ago
parent
commit
9bc86c73bc

+ 12 - 0
misago/static/misago/js/misago-posts.js

@@ -79,6 +79,18 @@ $(function() {
 
 
     });
     });
 
 
+    this.$e.find('form').submit(function() {
+
+      var prompt = $(this).data('prompt');
+      if (prompt) {
+        var decision = confirm(prompt);
+        return decision;
+      } else {
+        return true;
+      }
+
+    });
+
   }
   }
 
 
   // Posts controller
   // Posts controller

+ 50 - 26
misago/templates/misago/thread/post.html

@@ -107,35 +107,59 @@
           </button>
           </button>
           {% endif %}
           {% endif %}
 
 
-          <button type="button" class="btn btn-warning btn-flat pull-right">
-            <span class="fa fa-eye-slash">
-            {% trans "Hide" %}
-          </button>
-
-          <button type="button" class="btn btn-default btn-flat pull-right">
-            <span class="fa fa-eye">
-            {% trans "Show" %}
-          </button>
-
-          <button type="button" class="btn btn-danger btn-flat pull-right">
-            <span class="fa fa-times">
-            {% trans "Delete" %}
-          </button>
+          {% if post.acl.can_unhide %}
+          <form action="{% url 'misago:unhide_post' post_id=post.id %}" method="post">
+            {% csrf_token %}
+            <button type="submit" class="btn btn-default btn-flat pull-right">
+              <span class="fa fa-eye">
+              {% trans "Reveal" %}
+            </button>
+          </form>
+          {% endif %}
 
 
-          <button type="button" class="btn btn-warning btn-flat pull-right">
-            <span class="fa fa-exclamation-triangle">
-            {% trans "Report" %}
-          </button>
+          {% if post.acl.can_hide %}
+          <form action="{% url 'misago:hide_post' post_id=post.id %}" method="post">
+            {% csrf_token %}
+            <button type="submit" class="btn btn-warning btn-flat pull-right">
+              <span class="fa fa-eye-slash">
+              {% trans "Hide" %}
+            </button>
+          </form>
+          {% endif %}
 
 
-          <button type="button" class="btn btn-success btn-flat pull-right">
-            <span class="fa fa-check">
-            {% trans "Approve" %}
-          </button>
+          {% if post.acl.can_delete %}
+          <form action="{% url 'misago:delete_post' post_id=post.id %}" method="post" data-prompt="{% trans "Are you sure you want to delete this post?" %}">
+            {% csrf_token %}
+            <button type="submit" class="btn btn-danger btn-flat pull-right">
+              <span class="fa fa-times">
+              {% trans "Delete" %}
+            </button>
+          </form>
+          {% endif %}
 
 
-          <button type="button" class="btn btn-success btn-flat pull-left">
-            <span class="fa fa-heart">
-            {% trans "Like" %}
-          </button>
+          <form action="" method="post">
+            {% csrf_token %}
+            <button type="submit" class="btn btn-warning btn-flat pull-right">
+              <span class="fa fa-exclamation-triangle">
+              {% trans "Report" %}
+            </button>
+          </form>
+
+          <form action="" method="post">
+            {% csrf_token %}
+            <button type="submit" class="btn btn-success btn-flat pull-right">
+              <span class="fa fa-check">
+              {% trans "Approve" %}
+            </button>
+          </form>
+
+          <form action="" method="post">
+            {% csrf_token %}
+            <button type="submit" class="btn btn-success btn-flat pull-left">
+              <span class="fa fa-heart">
+              {% trans "Like" %}
+            </button>
+          </form>
         </div>
         </div>
       {% else %}
       {% else %}
       <div class="panel-body">
       <div class="panel-body">

+ 125 - 4
misago/threads/permissions.py

@@ -313,9 +313,9 @@ def add_acl_to_post(user, post):
         'can_reply': can_reply_thread(user, post.thread),
         'can_reply': can_reply_thread(user, post.thread),
         'can_edit': can_edit_post(user, post),
         'can_edit': can_edit_post(user, post),
         'can_see_hidden': forum_acl.get('can_hide_posts'),
         'can_see_hidden': forum_acl.get('can_hide_posts'),
-        'can_unhide': post.is_hidden and forum_acl.get('can_hide_posts'),
-        'can_hide': forum_acl.get('can_hide_threads'),
-        'can_delete': forum_acl.get('can_hide_threads'),
+        'can_unhide': can_unhide_post(user, post),
+        'can_hide': can_hide_post(user, post),
+        'can_delete': can_delete_post(user, post),
         'can_protect': forum_acl.get('can_protect_posts'),
         'can_protect': forum_acl.get('can_protect_posts'),
         'can_report': forum_acl.get('can_report_content'),
         'can_report': forum_acl.get('can_report_content'),
         'can_see_reports': forum_acl.get('can_see_reports'),
         'can_see_reports': forum_acl.get('can_see_reports'),
@@ -435,6 +435,9 @@ def allow_edit_post(user, target):
         raise PermissionDenied(
         raise PermissionDenied(
             _("You can't edit posts in closed threads."))
             _("You can't edit posts in closed threads."))
 
 
+    if target.is_hidden and not can_unhide_post(user, target):
+        raise PermissionDenied(_("This post is hidden, you can't edit it."))
+
     if forum_acl['can_edit_posts'] == 1:
     if forum_acl['can_edit_posts'] == 1:
         if target.poster_id != user.pk:
         if target.poster_id != user.pk:
             raise PermissionDenied(
             raise PermissionDenied(
@@ -453,8 +456,126 @@ def allow_edit_post(user, target):
 can_edit_post = return_boolean(allow_edit_post)
 can_edit_post = return_boolean(allow_edit_post)
 
 
 
 
+def allow_unhide_post(user, target):
+    if target.forum.is_closed:
+        message = _("This forum is closed. You can't reveal posts in it.")
+        raise PermissionDenied(message)
+    if user.is_anonymous():
+        raise PermissionDenied(_("You have to sign in to reveal posts."))
+
+    if target.id == target.thread.first_post_id:
+        raise PermissionDenied(_("You can't reveal thread's first post."))
+    if not target.is_hidden:
+        raise PermissionDenied(_("Only hidden posts can be revealed."))
+
+    forum_acl = target.forum.acl
+
+    if target.thread.is_closed and not forum_acl['can_close_threads']:
+        raise PermissionDenied(
+            _("You can't reveal posts in closed threads."))
+    if target.is_protected and not forum_acl['can_protect_posts']:
+        message = _("This post is protected. You can't reveal it.")
+        raise PermissionDenied(message)
+
+    if not forum_acl['can_hide_posts']:
+        if not forum_acl['can_hide_own_posts']:
+            raise PermissionDenied(_("You can't reveal posts in this forum."))
+
+        if user.id != post.poster_id:
+            message = _("You can't reveal other users posts in this forum.")
+            raise PermissionDenied(message)
+
+        if has_time_to_edit_post(user, post):
+            message = ungettext("You can't reveal posts that are "
+                                "older than %(minutes)s minute.",
+                                "You can't reveal posts that are "
+                                "older than %(minutes)s minutes.",
+                                forum_acl['post_edit_time'])
+            raise PermissionDenied(
+                message % {'minutes': forum_acl['post_edit_time']})
+can_unhide_post = return_boolean(allow_unhide_post)
+
+
+def allow_hide_post(user, target):
+    if target.forum.is_closed:
+        message = _("This forum is closed. You can't hide posts in it.")
+        raise PermissionDenied(message)
+    if user.is_anonymous():
+        raise PermissionDenied(_("You have to sign in to hide posts."))
+
+    if target.id == target.thread.first_post_id:
+        raise PermissionDenied(_("You can't hide thread's first post."))
+    if target.is_hidden:
+        raise PermissionDenied(_("This post is already hidden."))
+
+    forum_acl = target.forum.acl
+
+    if target.thread.is_closed and not forum_acl['can_close_threads']:
+        raise PermissionDenied(
+            _("You can't hide posts in closed threads."))
+    if target.is_protected and not forum_acl['can_protect_posts']:
+        message = _("This post is protected. You can't hide it.")
+        raise PermissionDenied(message)
+
+    if not forum_acl['can_hide_posts']:
+        if not forum_acl['can_hide_own_posts']:
+            raise PermissionDenied(_("You can't hide posts in this forum."))
+
+        if user.id != post.poster_id:
+            message = _("You can't hide other users posts in this forum.")
+            raise PermissionDenied(message)
+
+        if has_time_to_edit_post(user, post):
+            message = ungettext("You can't hide posts that are "
+                                "older than %(minutes)s minute.",
+                                "You can't hide posts that are "
+                                "older than %(minutes)s minutes.",
+                                forum_acl['post_edit_time'])
+            raise PermissionDenied(
+                message % {'minutes': forum_acl['post_edit_time']})
+can_hide_post = return_boolean(allow_hide_post)
+
+
+def allow_delete_post(user, target):
+    if target.forum.is_closed:
+        message = _("This forum is closed. You can't delete posts in it.")
+        raise PermissionDenied(message)
+    if user.is_anonymous():
+        raise PermissionDenied(_("You have to sign in to delete posts."))
+
+    if target.id == target.thread.first_post_id:
+        raise PermissionDenied(_("You can't delete thread's first post."))
+
+    forum_acl = target.forum.acl
+
+    if target.thread.is_closed and not forum_acl['can_close_threads']:
+        raise PermissionDenied(
+            _("You can't delete posts in closed threads."))
+    if target.is_protected and not forum_acl['can_protect_posts']:
+        message = _("This post is protected. You can't delete it.")
+        raise PermissionDenied(message)
+
+    if forum_acl['can_hide_posts'] != 2:
+        if forum_acl['can_hide_own_posts'] != 2:
+            raise PermissionDenied(_("You can't delete posts in this forum."))
+
+        if user.id != post.poster_id:
+            message = _("You can't delete other users posts in this forum.")
+            raise PermissionDenied(message)
+
+        if has_time_to_edit_post(user, post):
+            message = ungettext("You can't delete posts that are "
+                                "older than %(minutes)s minute.",
+                                "You can't delete posts that are "
+                                "older than %(minutes)s minutes.",
+                                forum_acl['post_edit_time'])
+            raise PermissionDenied(
+                message % {'minutes': forum_acl['post_edit_time']})
+can_delete_post = return_boolean(allow_delete_post)
+
+
 """
 """
-Permission check helperts
+Permission check helpers
 """
 """
 def can_change_owned_thread(user, target):
 def can_change_owned_thread(user, target):
     forum_acl = user.acl['forums'].get(target.forum_id, {})
     forum_acl = user.acl['forums'].get(target.forum_id, {})

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

@@ -0,0 +1,136 @@
+from django.core.urlresolvers import reverse
+
+from misago.acl.testutils import override_acl
+from misago.forums.models import Forum
+from misago.users.testutils import AuthenticatedUserTestCase
+
+from misago.threads.models import Thread, Post
+from misago.threads.testutils import post_thread, reply_thread
+
+
+class PostViewTestCase(AuthenticatedUserTestCase):
+    def setUp(self):
+        super(PostViewTestCase, self).setUp()
+
+        self.forum = Forum.objects.all_forums().filter(role="forum")[:1][0]
+        self.forum.labels = []
+
+        self.thread = post_thread(self.forum)
+
+    def override_acl(self, new_acl, forum=None):
+        new_acl.update({
+            'can_see': True,
+            'can_browse': True,
+            'can_see_all_threads': True,
+            'can_see_own_threads': False
+        })
+
+        forum = forum or self.forum
+
+        forums_acl = self.user.acl
+        forums_acl['visible_forums'].append(forum.pk)
+        forums_acl['forums'][forum.pk] = new_acl
+        override_acl(self.user, forums_acl)
+
+
+class UnhidePostViewTests(PostViewTestCase):
+    def test_unhide_first_post(self):
+        """attempt to reveal first post in thread fails"""
+        post_link = reverse('misago:unhide_post', kwargs={
+            'post_id': self.thread.first_post_id
+        })
+
+        self.override_acl({'can_hide_posts': 2})
+        response = self.client.post(post_link)
+        self.assertEqual(response.status_code, 403)
+
+    def test_unhide_post_no_permission(self):
+        """view fails due to lack of permissions"""
+        post = reply_thread(self.thread, is_hidden=True)
+        post_link = reverse('misago:unhide_post', kwargs={'post_id': post.id})
+
+        self.override_acl({'can_hide_posts': 0, 'can_hide_own_posts': 0})
+        response = self.client.post(post_link)
+        self.assertEqual(response.status_code, 403)
+
+    def test_unhide_post(self):
+        """view reveals post"""
+        post = reply_thread(self.thread, is_hidden=True)
+        post_link = reverse('misago:unhide_post', kwargs={'post_id': post.id})
+
+        self.override_acl({'can_hide_posts': 2})
+        response = self.client.post(post_link)
+        self.assertEqual(response.status_code, 302)
+
+        post = Post.objects.get(id=post.id)
+        self.assertFalse(post.is_hidden)
+
+
+class HidePostViewTests(PostViewTestCase):
+    def test_hide_first_post(self):
+        """attempt to hide first post in thread fails"""
+        post_link = reverse('misago:hide_post', kwargs={
+            'post_id': self.thread.first_post_id
+        })
+
+        self.override_acl({'can_hide_posts': 2})
+        response = self.client.post(post_link)
+        self.assertEqual(response.status_code, 403)
+
+    def test_hide_post_no_permission(self):
+        """view fails due to lack of permissions"""
+        post = reply_thread(self.thread)
+        post_link = reverse('misago:hide_post', kwargs={'post_id': post.id})
+
+        self.override_acl({'can_hide_posts': 0, 'can_hide_own_posts': 0})
+        response = self.client.post(post_link)
+        self.assertEqual(response.status_code, 403)
+
+    def test_hide_post(self):
+        """view hides post"""
+        post = reply_thread(self.thread)
+        post_link = reverse('misago:hide_post', kwargs={'post_id': post.id})
+
+        self.override_acl({'can_hide_posts': 2})
+        response = self.client.post(post_link)
+        self.assertEqual(response.status_code, 302)
+
+        post = Post.objects.get(id=post.id)
+        self.assertTrue(post.is_hidden)
+
+
+class DeletePostViewTests(PostViewTestCase):
+    def test_delete_first_post(self):
+        """attempt to delete first post in thread fails"""
+        post_link = reverse('misago:delete_post', kwargs={
+            'post_id': self.thread.first_post_id
+        })
+
+        self.override_acl({'can_hide_posts': 2})
+        response = self.client.post(post_link)
+        self.assertEqual(response.status_code, 403)
+
+    def test_delete_post_no_permission(self):
+        """view fails due to lack of permissions"""
+        post = reply_thread(self.thread)
+        post_link = reverse('misago:delete_post', kwargs={'post_id': post.id})
+
+        self.override_acl({'can_hide_posts': 0, 'can_hide_own_posts': 0})
+        response = self.client.post(post_link)
+        self.assertEqual(response.status_code, 403)
+
+    def test_delete_post(self):
+        """view deletes post"""
+        post = reply_thread(self.thread)
+        post_link = reverse('misago:delete_post', kwargs={'post_id': post.id})
+
+        self.override_acl({'can_hide_posts': 2})
+        response = self.client.post(post_link)
+        self.assertEqual(response.status_code, 302)
+
+        thread = Thread.objects.get(id=self.thread.id)
+        self.assertEqual(thread.first_post_id, thread.last_post_id)
+        self.assertEqual(thread.replies, 0)
+
+        with self.assertRaises(Post.DoesNotExist):
+            Post.objects.get(id=post.id)

+ 6 - 2
misago/threads/urls.py

@@ -41,9 +41,13 @@ urlpatterns += patterns('',
 )
 )
 
 
 
 
-from misago.threads.views.post import QuotePostView
+from misago.threads.views.post import (QuotePostView, HidePostView,
+                                       UnhidePostView, DeletePostView)
 urlpatterns += patterns('',
 urlpatterns += patterns('',
-    url(r'^quote-post/(?P<post_id>\d+)/$', QuotePostView.as_view(), name='quote_post'),
+    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'),
 )
 )
 
 
 # new threads lists
 # new threads lists

+ 0 - 1
misago/threads/views/generic/__init__.py

@@ -2,7 +2,6 @@
 from misago.threads.views.generic.base import *
 from misago.threads.views.generic.base import *
 from misago.threads.views.generic.goto import *
 from misago.threads.views.generic.goto import *
 from misago.threads.views.generic.gotopostslist import *
 from misago.threads.views.generic.gotopostslist import *
-from misago.threads.views.generic.post import *
 from misago.threads.views.generic.thread import *
 from misago.threads.views.generic.thread import *
 from misago.threads.views.generic.threads import *
 from misago.threads.views.generic.threads import *
 from misago.threads.views.generic.forum import *
 from misago.threads.views.generic.forum import *

+ 0 - 15
misago/threads/views/generic/post.py

@@ -1,15 +0,0 @@
-from misago.threads.views.generic.base import ViewBase
-
-
-__all__ = ['PostView']
-
-
-class PostView(ViewBase):
-    """
-    Basic view for posts
-    """
-    def fetch_post(self, request, **kwargs):
-        pass
-
-    def dispatch(self, request, *args, **kwargs):
-        post = self.fetch_post(request, **kwargs)

+ 76 - 2
misago/threads/views/post.py

@@ -1,6 +1,13 @@
+from django.contrib import messages
 from django.db.transaction import atomic
 from django.db.transaction import atomic
 from django.http import JsonResponse
 from django.http import JsonResponse
+from django.shortcuts import redirect, render
+from django.utils.translation import ugettext as _
 
 
+from misago.acl import add_acl
+
+from misago.threads import permissions, moderation, goto
+from misago.threads.permissions import exclude_invisible_posts
 from misago.threads.views.generic.base import ViewBase
 from misago.threads.views.generic.base import ViewBase
 
 
 
 
@@ -9,25 +16,92 @@ __all__ = ['QuotePostView']
 
 
 class PostView(ViewBase):
 class PostView(ViewBase):
     is_atomic = True
     is_atomic = True
+    require_post = True
 
 
     def dispatch(self, request, *args, **kwargs):
     def dispatch(self, request, *args, **kwargs):
+        if request.method != "POST" and self.require_post:
+            response = render(request, 'misago/errorpages/wrong_way.html')
+            response.status_code = 405
+            return response
+
+        post = None
+        response = None
+
         if self.is_atomic:
         if self.is_atomic:
             with atomic():
             with atomic():
                 post = self.get_post(request, True, **kwargs)
                 post = self.get_post(request, True, **kwargs)
-                return self.real_dispatch(request, post)
+                response = self.real_dispatch(request, post)
         else:
         else:
             post = self.get_post(request, **kwargs)
             post = self.get_post(request, **kwargs)
-            return self.real_dispatch(request, post)
+            response = self.real_dispatch(request, post)
+
+        if response:
+            return response
+        else:
+            return self.redirect_to_post(request.user, post)
 
 
     def real_dispatch(self, request, post):
     def real_dispatch(self, request, post):
         raise NotImplementedError(
         raise NotImplementedError(
             "post views have to override real_dispatch method")
             "post views have to override real_dispatch method")
 
 
+    def redirect_to_post(self, user, post):
+        return redirect(goto.post(user, post.thread, post))
+
 
 
 class QuotePostView(PostView):
 class QuotePostView(PostView):
+    is_atomic = False
+    require_post = False
+
     def real_dispatch(self, request, post):
     def real_dispatch(self, request, post):
         quote_tpl = u'[quote="%s, post:%s, topic:%s"]\n%s\n[/quote]'
         quote_tpl = u'[quote="%s, post:%s, topic:%s"]\n%s\n[/quote]'
         formats = (post.poster_name, post.pk, post.thread_id, post.original)
         formats = (post.poster_name, post.pk, post.thread_id, post.original)
         return JsonResponse({
         return JsonResponse({
             'quote': quote_tpl % formats
             'quote': quote_tpl % formats
         })
         })
+
+
+class UnhidePostView(PostView):
+    def real_dispatch(self, request, post):
+        permissions.allow_unhide_post(request.user, post)
+        moderation.unhide_post(request.user, post)
+        messages.success(request, _("Post has been made visible."))
+
+
+class HidePostView(PostView):
+    def real_dispatch(self, request, post):
+        permissions.allow_hide_post(request.user, post)
+        moderation.hide_post(request.user, post)
+        messages.success(request, _("Post has been hidden."))
+
+
+class DeletePostView(PostView):
+    def real_dispatch(self, request, post):
+        post_id = post.id
+
+        permissions.allow_delete_post(request.user, post)
+        moderation.delete_post(request.user, post)
+
+        post.thread.synchronize()
+        post.thread.save()
+        post.forum.synchronize()
+        post.forum.save()
+
+        posts_queryset = exclude_invisible_posts(post.thread.post_set,
+                                                 request.user,
+                                                 post.forum)
+        posts_queryset = posts_queryset.select_related('thread', 'forum')
+
+        if post_id < post.thread.last_post_id:
+            target_post = posts_queryset.order_by('id').filter(id__gt=post_id)
+        else:
+            target_post = posts_queryset.order_by('-id').filter(id__lt=post_id)
+
+        target_post = target_post[:1][0]
+        target_post.thread.forum = target_post.forum
+
+        add_acl(request.user, target_post.forum)
+        add_acl(request.user, target_post.thread)
+        add_acl(request.user, target_post)
+
+        messages.success(request, _("Post has been deleted."))
+        return self.redirect_to_post(request.user, target_post)