Browse Source

moderation actions in thread patch endpoint

Rafał Pitoń 9 years ago
parent
commit
318dbcc0a6

+ 43 - 7
frontend/src/components/threads/moderation-menu.js

@@ -47,6 +47,38 @@ export default class extends React.Component {
       value: 0
     }, gettext("Selected threads were unpinned."));
   };
+
+  open = () => {
+    this.callApi({
+      op: 'replace',
+      path: 'is-closed',
+      value: false
+    }, gettext("Selected threads were opened."));
+  };
+
+  close = () => {
+    this.callApi({
+      op: 'replace',
+      path: 'is-closed',
+      value: true
+    }, gettext("Selected threads were closed."));
+  };
+
+  unhide = () => {
+    this.callApi({
+      op: 'replace',
+      path: 'is-hidden',
+      value: false
+    }, gettext("Selected threads were unhidden."));
+  };
+
+  hide = () => {
+    this.callApi({
+      op: 'replace',
+      path: 'is-hidden',
+      value: true
+    }, gettext("Selected threads were hidden."));
+  };
   /* jshint ignore:end */
 
   getPinGloballyButton() {
@@ -117,7 +149,8 @@ export default class extends React.Component {
       /* jshint ignore:start */
       return <li>
         <button type="button"
-                className="btn btn-link">
+                className="btn btn-link"
+                onClick={this.open}>
           {gettext("Open threads")}
         </button>
       </li>;
@@ -132,7 +165,8 @@ export default class extends React.Component {
       /* jshint ignore:start */
       return <li>
         <button type="button"
-                className="btn btn-link">
+                className="btn btn-link"
+                onClick={this.close}>
           {gettext("Close threads")}
         </button>
       </li>;
@@ -142,13 +176,14 @@ export default class extends React.Component {
     }
   }
 
-  getShowButton() {
+  getUnhideButton() {
     if (this.props.moderation.can_hide) {
       /* jshint ignore:start */
       return <li>
         <button type="button"
-                className="btn btn-link">
-          {gettext("Show threads")}
+                className="btn btn-link"
+                onClick={this.unhide}>
+          {gettext("Unhide threads")}
         </button>
       </li>;
       /* jshint ignore:end */
@@ -162,7 +197,8 @@ export default class extends React.Component {
       /* jshint ignore:start */
       return <li>
         <button type="button"
-                className="btn btn-link">
+                className="btn btn-link"
+                onClick={this.hide}>
           {gettext("Hide threads")}
         </button>
       </li>;
@@ -196,7 +232,7 @@ export default class extends React.Component {
       {this.getMoveButton()}
       {this.getOpenButton()}
       {this.getCloseButton()}
-      {this.getShowButton()}
+      {this.getUnhideButton()}
       {this.getHideButton()}
       {this.getDeleteButton()}
     </ul>;

+ 107 - 3
misago/threads/api/threadendpoints/patch.py

@@ -1,21 +1,125 @@
+from django.core.exceptions import PermissionDenied
+from django.utils.translation import gettext as _
+
+from misago.acl import add_acl
+from misago.categories.models import Category
+from misago.categories.permissions import (
+    allow_see_category, allow_browse_category)
+from misago.categories.serializers import CategorySerializer
 from misago.core.apipatch import ApiPatch
+from misago.core.shortcuts import get_int_or_404, get_object_or_404
+
 from misago.threads.moderation import threads as moderation
+from misago.threads.utils import add_categories_to_threads
+
 
 thread_patch_endpoint = ApiPatch()
 
 
 def patch_weight(request, thread, value):
+    message = _("You don't have permission to change this thread's weight.")
+    if not thread.acl.get('can_pin'):
+        raise PermissionDenied(message)
+    elif thread.weight > thread.acl.get('can_pin'):
+        raise PermissionDenied(message)
+
     if value == 2:
-        moderation.pin_thread_globally(request.user, thread)
-    if value == 1:
+        if thread.acl.get('can_pin') == 2:
+            moderation.pin_thread_globally(request.user, thread)
+        else:
+            raise PermissionDenied(
+                _("You don't have permission to pin this thread globally."))
+    elif value == 1:
         moderation.pin_thread_locally(request.user, thread)
-    if value == 0:
+    elif value == 0:
         moderation.unpin_thread(request.user, thread)
 
     return {'weight': thread.weight}
 thread_patch_endpoint.replace('weight', patch_weight)
 
 
+def patch_move(request, thread, value):
+    if thread.acl.get('can_move'):
+        category_pk = get_int_or_404(value)
+        new_category = get_object_or_404(
+            Category.objects.all_categories().select_related('parent'),
+            pk=category_pk
+        )
+
+        add_acl(request.user, new_category)
+        allow_see_category(request.user, new_category)
+        allow_browse_category(request.user, new_category)
+
+        moderation.move_thread(request.user, thread, new_category)
+
+        return {'category': CategorySerializer(new_category).data}
+    else:
+        raise PermissionDenied(
+            _("You don't have permission to move this thread."))
+thread_patch_endpoint.replace('category', patch_move)
+
+
+def patch_top_category(request, thread, value):
+    category_pk = get_int_or_404(value)
+    root_category = get_object_or_404(
+        Category.objects.all_categories(include_root=True),
+        pk=category_pk
+    )
+
+    categories = list(Category.objects.all_categories().filter(
+        id__in=request.user.acl['visible_categories']
+    ))
+    add_categories_to_threads(root_category, categories, [thread])
+
+    return {'top_category': CategorySerializer(thread.top_category).data}
+thread_patch_endpoint.add('top-category', patch_top_category)
+
+
+def patch_flatten_categories(request, thread, value):
+    try:
+        return {
+            'category': thread.category_id,
+            'top_category': thread.top_category.pk,
+        }
+    except AttributeError:
+        return {
+            'category': thread.category_id,
+        }
+thread_patch_endpoint.replace('flatten-categories', patch_flatten_categories)
+
+
+def patch_is_closed(request, thread, value):
+    if thread.acl.get('can_close'):
+        if value:
+            moderation.close_thread(request.user, thread)
+        else:
+            moderation.open_thread(request.user, thread)
+
+        return {'is_closed': thread.is_closed}
+    else:
+        if value:
+            raise PermissionDenied(
+                _("You don't have permission to close this thread."))
+        else:
+            raise PermissionDenied(
+                _("You don't have permission to open this thread."))
+thread_patch_endpoint.replace('is-closed', patch_is_closed)
+
+
+def patch_is_hidden(request, thread, value):
+    if thread.acl.get('can_hide'):
+        if value:
+            moderation.hide_thread(request.user, thread)
+        else:
+            moderation.unhide_thread(request.user, thread)
+
+        return {'is_hidden': thread.is_hidden}
+    else:
+        raise PermissionDenied(
+            _("You don't have permission to hide this thread."))
+thread_patch_endpoint.replace('is-hidden', patch_is_hidden)
+
+
 def patch_subscribtion(request, thread, value):
     request.user.subscription_set.filter(thread=thread).delete()
 

+ 13 - 0
misago/threads/api/threads.py

@@ -1,4 +1,6 @@
+from django.core.exceptions import PermissionDenied
 from django.db import transaction
+from django.utils.translation import gettext as _
 
 from rest_framework import viewsets
 from rest_framework.decorators import detail_route, list_route
@@ -16,6 +18,7 @@ from misago.users.rest_permissions import IsAuthenticatedOrReadOnly
 from misago.threads.api.threadendpoints.list import threads_list_endpoint
 from misago.threads.api.threadendpoints.patch import thread_patch_endpoint
 from misago.threads.models import Thread, Subscription
+from misago.threads.moderation import threads as moderation
 from misago.threads.permissions.threads import allow_see_thread
 from misago.threads.serializers import ThreadSerializer
 from misago.threads.subscriptions import make_subscription_aware
@@ -55,6 +58,16 @@ class ThreadViewSet(viewsets.ViewSet):
         thread = self.get_thread(request.user, pk)
         return thread_patch_endpoint.dispatch(request, thread)
 
+    def destroy(self, request, pk=None):
+        thread = self.get_thread(request.user, pk)
+
+        if thread.acl.get('can_hide') == 2:
+            moderation.delete_thread(request.user, thread)
+            return Response({'detail': 'ok'})
+        else:
+            raise PermissionDenied(
+                _("You don't have permission to delete this thread."))
+
     @list_route(methods=['post'])
     def read(self, request):
         if request.query_params.get('category'):

+ 0 - 10
misago/threads/permissions/threads.py

@@ -129,16 +129,6 @@ class PermissionsForm(forms.Form):
     )
     can_move_posts = forms.YesNoSwitch(label=_("Can move posts"))
     can_merge_posts = forms.YesNoSwitch(label=_("Can merge posts"))
-    can_change_threads_labels = forms.TypedChoiceField(
-        label=_("Can change threads labels"),
-        coerce=int,
-        initial=0,
-        choices=(
-            (0, _("No")),
-            (1, _("Own threads")),
-            (2, _("All threads")),
-        )
-    )
     can_pin_threads = forms.TypedChoiceField(
         label=_("Can pin threads"),
         coerce=int,

+ 2 - 0
misago/threads/serializers/thread.py

@@ -32,6 +32,7 @@ class ThreadSerializer(serializers.ModelSerializer):
             'category',
             'replies',
             'is_closed',
+            'is_hidden',
             'is_read',
             'absolute_url',
             'last_poster_url',
@@ -102,6 +103,7 @@ class ThreadListSerializer(ThreadSerializer):
             'last_poster_url',
             'last_post_on',
             'is_closed',
+            'is_hidden',
             'is_read',
             'absolute_url',
             'last_post_url',

+ 55 - 75
misago/threads/tests/test_thread_api.py

@@ -1,9 +1,11 @@
 import json
 
+from misago.acl.testutils import override_acl
 from misago.users.testutils import AuthenticatedUserTestCase
 from misago.categories.models import Category
 
 from misago.threads import testutils
+from misago.threads.models import Thread
 
 
 class ThreadApiTestCase(AuthenticatedUserTestCase):
@@ -13,15 +15,66 @@ class ThreadApiTestCase(AuthenticatedUserTestCase):
         self.category = Category.objects.get(slug='first-category')
 
         self.thread = testutils.post_thread(category=self.category)
-        self.api_link = '/api/threads/%s/' % self.thread.pk
+        self.api_link = self.thread.get_api_url()
+
+    def override_acl(self, acl):
+        final_acl = {
+            'can_see': 1,
+            'can_browse': 1,
+            'can_see_all_threads': 1,
+            'can_see_own_threads': 0,
+            'can_hide_threads': 0,
+            'can_review_moderated_content': 0,
+        }
+        final_acl.update(acl)
+
+        override_acl(self.user, {
+            'categories': {
+                self.category.pk: final_acl
+            }
+        })
 
     def get_thread_json(self):
-        response = self.client.get('/api/threads/%s/' % self.thread.pk)
+        response = self.client.get(self.thread.get_api_url())
         self.assertEqual(response.status_code, 200)
 
         return json.loads(response.content)
 
 
+class ThreadDeleteApiTests(ThreadApiTestCase):
+    def test_delete_thread(self):
+        """DELETE to API link with permission deletes thread"""
+        self.override_acl({
+            'can_hide_threads': 2
+        })
+
+        response = self.client.delete(self.api_link)
+        self.assertEqual(response.status_code, 200)
+
+        with self.assertRaises(Thread.DoesNotExist):
+            Thread.objects.get(pk=self.thread.pk)
+
+    def test_delete_thread_no_permission(self):
+        """DELETE to API link with no permission to delete fails"""
+        self.override_acl({
+            'can_hide_threads': 1
+        })
+
+        response = self.client.delete(self.api_link)
+        self.assertEqual(response.status_code, 403)
+
+        self.override_acl({
+            'can_hide_threads': 0
+        })
+
+        response_json = json.loads(response.content)
+        self.assertEqual(response_json['detail'],
+            "You don't have permission to delete this thread.")
+
+        response = self.client.delete(self.api_link)
+        self.assertEqual(response.status_code, 403)
+
+
 class ThreadsReadApiTests(ThreadApiTestCase):
     def setUp(self):
         super(ThreadSubscribeApiTests, self).setUp()
@@ -41,76 +94,3 @@ class ThreadsReadApiTests(ThreadApiTestCase):
         self.assertEqual(response.status_code, 200)
 
         self.assertEqual(self.user.categoryread_set.count(), 1)
-
-
-class ThreadSubscribeApiTests(ThreadApiTestCase):
-    def setUp(self):
-        super(ThreadSubscribeApiTests, self).setUp()
-
-        self.api_link = '/api/threads/%s/subscribe/' % self.thread.pk
-
-    def test_subscribe_thread(self):
-        """api makes it possible to subscribe thread"""
-        response = self.client.post(self.api_link, json.dumps({
-            'notify': True
-        }),
-        content_type="application/json")
-
-        self.assertEqual(response.status_code, 200)
-
-        thread_json = self.get_thread_json()
-        self.assertFalse(thread_json['subscription'])
-
-        subscription = self.user.subscription_set.get(thread=self.thread)
-        self.assertFalse(subscription.send_email)
-
-    def test_subscribe_thread_with_email(self):
-        """api makes it possible to subscribe thread with emails"""
-        response = self.client.post(self.api_link, json.dumps({
-            'email': True
-        }),
-        content_type="application/json")
-
-        self.assertEqual(response.status_code, 200)
-
-        thread_json = self.get_thread_json()
-        self.assertTrue(thread_json['subscription'])
-
-        subscription = self.user.subscription_set.get(thread=self.thread)
-        self.assertTrue(subscription.send_email)
-
-    def test_unsubscribe_thread(self):
-        """api makes it possible to unsubscribe thread"""
-        response = self.client.post(self.api_link, json.dumps({
-            'remove': True
-        }),
-        content_type="application/json")
-
-        self.assertEqual(response.status_code, 200)
-
-        thread_json = self.get_thread_json()
-        self.assertIsNone(thread_json['subscription'])
-
-        self.assertEqual(self.user.subscription_set.count(), 0)
-
-    def test_subscribe_as_guest(self):
-        """api makes it impossible to subscribe thread"""
-        self.logout_user()
-
-        response = self.client.post(self.api_link, json.dumps({
-            'notify': True
-        }),
-        content_type="application/json")
-
-        self.assertEqual(response.status_code, 403)
-
-    def test_subscribe_nonexistant_thread(self):
-        """api makes it impossible to subscribe nonexistant thread"""
-        bad_api_link = self.api_link.replace(
-            unicode(self.thread.pk), unicode(self.thread.pk + 9))
-        response = self.client.post(bad_api_link, json.dumps({
-            'notify': True
-        }),
-        content_type="application/json")
-
-        self.assertEqual(response.status_code, 404)