Browse Source

some cleanups, work on split thread endpoint

Rafał Pitoń 8 years ago
parent
commit
45900739ed

+ 0 - 2
misago/threads/api/postendpoints/move.py

@@ -5,8 +5,6 @@ from django.utils.translation import ugettext as _, ungettext
 
 
 from rest_framework.response import Response
 from rest_framework.response import Response
 
 
-from misago.acl import add_acl
-
 from ...permissions.threads import exclude_invisible_posts
 from ...permissions.threads import exclude_invisible_posts
 from ...utils import get_thread_id_from_url
 from ...utils import get_thread_id_from_url
 
 

+ 121 - 0
misago/threads/api/postendpoints/split.py

@@ -0,0 +1,121 @@
+from django.conf import settings
+from django.core.exceptions import PermissionDenied
+from django.utils.translation import ugettext as _, ungettext
+
+from rest_framework import serializers
+from rest_framework.response import Response
+
+from ...models import THREAD_WEIGHT_DEFAULT, THREAD_WEIGHT_GLOBAL
+from ...permissions.threads import exclude_invisible_posts
+
+
+SPLIT_LIMIT = settings.MISAGO_POSTS_PER_PAGE + settings.MISAGO_POSTS_TAIL
+
+
+class SplitError(Exception):
+    def __init__(self, msg):
+        self.msg = msg
+
+
+def posts_split_endpoint(request, thread):
+    try:
+        posts = clean_posts_for_split(request, thread)
+    except SplitError as e:
+        return Response({'detail': e.msg}, status=400)
+
+    # HERE run serializer to validate if split thread stuff
+
+    # create thread
+    # move posts to it
+    # moderate new thread
+
+    # sync old and new thread/categories
+
+
+def clean_posts_for_split(request, thread):
+    try:
+        posts_ids = list(map(int, request.data.get('posts', [])))
+    except (ValueError, TypeError):
+        raise SplitError(_("One or more post ids received were invalid."))
+
+    if not posts_ids:
+        raise SplitError(_("You have to specify at least one post to split."))
+    elif len(posts_ids) > SPLIT_LIMIT:
+        message = ungettext(
+            "No more than %(limit)s post can be split at single time.",
+            "No more than %(limit)s posts can be split at single time.",
+            SPLIT_LIMIT)
+        raise SplitError(message % {'limit': SPLIT_LIMIT})
+
+    posts_queryset = exclude_invisible_posts(request.user, thread.category, thread.post_set)
+    posts_queryset = posts_queryset.select_for_update().filter(id__in=posts_ids).order_by('id')
+
+    posts = []
+    for post in posts_queryset:
+        if post.is_event:
+            raise SplitError(_("Events can't be split."))
+        if post.pk == thread.first_post_id:
+            raise SplitError(_("You can't split thread's first post."))
+        if post.is_hidden and not thread.category.acl['can_hide_posts']:
+            raise SplitError(_("You can't split posts the content you can't see."))
+
+        posts.append(post)
+
+    if len(posts) != len(posts_ids):
+        raise SplitError(_("One or more posts to split could not be found."))
+
+    return posts
+
+
+class SplitPostsSerializer(serializers.Serializer):
+    title = serializers.CharField()
+    category = serializers.IntegerField()
+    weight = serializers.IntegerField(
+        required=False,
+        allow_null=True,
+        max_value=THREAD_WEIGHT_GLOBAL,
+        min_value=THREAD_WEIGHT_DEFAULT,
+    )
+    is_hidden = serializers.NullBooleanField(required=False)
+    is_closed = serializers.NullBooleanField(required=False)
+
+    def validate_title(self, title):
+        return validate_title(title)
+
+    def validate_category(self, category_id):
+        self.category = validate_category(self.context, category_id)
+        return self.category
+
+    def validate_weight(self, weight):
+        try:
+            add_acl(self.context, self.category)
+        except AttributeError:
+            return weight # don't validate weight further if category failed
+
+        if weight > self.category.acl.get('can_pin_threads', 0):
+            if weight == 2:
+                raise ValidationError(_("You don't have permission to pin threads globally in this category."))
+            else:
+                raise ValidationError(_("You don't have permission to pin threads in this category."))
+        return weight
+
+    def validate_is_hidden(self, is_hidden):
+        try:
+            add_acl(self.context, self.category)
+        except AttributeError:
+            return is_hidden # don't validate closed further if category failed
+
+        if is_hidden and not self.category.acl.get('can_hide_threads'):
+            raise ValidationError(_("You don't have permission to hide threads in this category."))
+        return is_hidden
+
+    def validate_is_closed(self, is_closed):
+        try:
+            add_acl(self.context, self.category)
+        except AttributeError:
+            return is_closed # don't validate closed further if category failed
+
+        if is_closed and not self.category.acl.get('can_close_threads'):
+            raise ValidationError(_("You don't have permission to close threads in this category."))
+        return is_closed
+

+ 8 - 30
misago/threads/api/threadendpoints/merge.py

@@ -7,15 +7,14 @@ from rest_framework.response import Response
 
 
 from misago.acl import add_acl
 from misago.acl import add_acl
 from misago.categories.models import THREADS_ROOT_NAME, Category
 from misago.categories.models import THREADS_ROOT_NAME, Category
-from misago.categories.permissions import can_browse_category, can_see_category
 
 
 from ...events import record_event
 from ...events import record_event
 from ...models import THREAD_WEIGHT_DEFAULT, THREAD_WEIGHT_GLOBAL, Thread
 from ...models import THREAD_WEIGHT_DEFAULT, THREAD_WEIGHT_GLOBAL, Thread
 from ...moderation import threads as moderation
 from ...moderation import threads as moderation
-from ...permissions import allow_start_thread, can_see_thread
+from ...permissions import can_start_thread, can_reply_thread, can_see_thread
 from ...serializers import ThreadsListSerializer
 from ...serializers import ThreadsListSerializer
 from ...threadtypes import trees_map
 from ...threadtypes import trees_map
-from ...validators import validate_title
+from ...validators import validate_category, validate_title
 from ...utils import add_categories_to_threads, get_thread_id_from_url
 from ...utils import add_categories_to_threads, get_thread_id_from_url
 
 
 
 
@@ -39,6 +38,10 @@ def thread_merge_endpoint(request, thread, viewmodel):
 
 
     try:
     try:
         other_thread = viewmodel(request, other_thread_id, select_for_update=True).model
         other_thread = viewmodel(request, other_thread_id, select_for_update=True).model
+        if not can_reply_thread(request.user, other_thread):
+            raise PermissionDenied(_("You can't merge this thread into thread you can't reply."))
+        if not other_thread.acl['can_merge']:
+            raise PermissionDenied(_("You don't have permission to merge this thread with current one."))
     except PermissionDenied as e:
     except PermissionDenied as e:
         return Response({
         return Response({
             'detail': e.args[0]
             'detail': e.args[0]
@@ -48,11 +51,6 @@ def thread_merge_endpoint(request, thread, viewmodel):
             'detail': _("The thread you have entered link to doesn't exist or you don't have permission to see it.")
             'detail': _("The thread you have entered link to doesn't exist or you don't have permission to see it.")
         }, status=400)
         }, status=400)
 
 
-    if not other_thread.acl['can_merge']:
-        return Response({
-            'detail': _("You don't have permission to merge this thread with current one.")
-        }, status=400)
-
     moderation.merge_thread(request, other_thread, thread)
     moderation.merge_thread(request, other_thread, thread)
 
 
     other_thread.synchronize()
     other_thread.synchronize()
@@ -185,28 +183,6 @@ def merge_threads(request, validated_data, threads):
     return new_thread
     return new_thread
 
 
 
 
-def validate_category(user, category_id, allow_root=False):
-    try:
-        threads_tree_id = trees_map.get_tree_id_for_root(THREADS_ROOT_NAME)
-        category = Category.objects.get(
-            tree_id=threads_tree_id,
-            id=category_id,
-        )
-    except Category.DoesNotExist:
-        category = None
-
-    # Skip ACL validation for root category?
-    if allow_root and category and not category.level:
-        return category
-
-    if not category or not can_see_category(user, category):
-        raise ValidationError(_("Requested category could not be found."))
-
-    if not can_browse_category(user, category):
-        raise ValidationError(_("You don't have permission to access this category."))
-    return category
-
-
 class MergeThreadsSerializer(serializers.Serializer):
 class MergeThreadsSerializer(serializers.Serializer):
     title = serializers.CharField()
     title = serializers.CharField()
     category = serializers.IntegerField()
     category = serializers.IntegerField()
@@ -227,6 +203,8 @@ class MergeThreadsSerializer(serializers.Serializer):
 
 
     def validate_category(self, category_id):
     def validate_category(self, category_id):
         self.category = validate_category(self.context, category_id)
         self.category = validate_category(self.context, category_id)
+        if not can_start_thread(self.context, self.category):
+            raise ValidationError(_("You can't create new threads in selected category."))
         return self.category
         return self.category
 
 
     def validate_weight(self, weight):
     def validate_weight(self, weight):

+ 10 - 2
misago/threads/api/threadposts.py

@@ -20,6 +20,7 @@ from ..viewmodels.thread import ForumThread
 from .postingendpoint import PostingEndpoint
 from .postingendpoint import PostingEndpoint
 from .postendpoints.merge import posts_merge_endpoint
 from .postendpoints.merge import posts_merge_endpoint
 from .postendpoints.move import posts_move_endpoint
 from .postendpoints.move import posts_move_endpoint
+from .postendpoints.split import posts_split_endpoint
 from .postendpoints.patch_event import event_patch_endpoint
 from .postendpoints.patch_event import event_patch_endpoint
 from .postendpoints.patch_post import post_patch_endpoint
 from .postendpoints.patch_post import post_patch_endpoint
 
 
@@ -69,18 +70,25 @@ class ViewSet(viewsets.ViewSet):
 
 
         return Response(data)
         return Response(data)
 
 
-    @list_route(methods=['post'], url_path='merge')
+    @list_route(methods=['post'])
     @transaction.atomic
     @transaction.atomic
     def merge(self, request, thread_pk):
     def merge(self, request, thread_pk):
         thread = self.get_thread_for_update(request, thread_pk).model
         thread = self.get_thread_for_update(request, thread_pk).model
         return posts_merge_endpoint(request, thread)
         return posts_merge_endpoint(request, thread)
 
 
-    @list_route(methods=['post'], url_path='move')
+    @list_route(methods=['post'])
     @transaction.atomic
     @transaction.atomic
     def move(self, request, thread_pk):
     def move(self, request, thread_pk):
         thread = self.get_thread_for_update(request, thread_pk).model
         thread = self.get_thread_for_update(request, thread_pk).model
         return posts_move_endpoint(request, thread, self.thread)
         return posts_move_endpoint(request, thread, self.thread)
 
 
+
+    @list_route(methods=['post'])
+    @transaction.atomic
+    def split(self, request, thread_pk):
+        thread = self.get_thread_for_update(request, thread_pk).model
+        return posts_split_endpoint(request, thread)
+
     @transaction.atomic
     @transaction.atomic
     def create(self, request, thread_pk):
     def create(self, request, thread_pk):
         thread = self.get_thread_for_update(request, thread_pk).model
         thread = self.get_thread_for_update(request, thread_pk).model

+ 17 - 0
misago/threads/tests/test_thread_merge_api.py

@@ -143,6 +143,23 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
         })
         })
         self.assertContains(response, "You don't have permission to merge this thread", status_code=400)
         self.assertContains(response, "You don't have permission to merge this thread", status_code=400)
 
 
+    def test_other_thread_isnt_replyable(self):
+        """api validates if other thread can be replied, which is condition for merg"""
+        self.override_acl({
+            'can_merge_threads': 1
+        })
+
+        self.override_other_acl({
+            'can_reply_threads': 0
+        })
+
+        other_thread = testutils.post_thread(self.category_b)
+
+        response = self.client.post(self.api_link, {
+            'thread_url': other_thread.get_absolute_url()
+        })
+        self.assertContains(response, "You can't merge this thread into thread you can't reply.", status_code=400)
+
     def test_merge_threads(self):
     def test_merge_threads(self):
         """api merges two threads successfully"""
         """api merges two threads successfully"""
         self.override_acl({
         self.override_acl({

+ 26 - 0
misago/threads/tests/test_threads_merge_api.py

@@ -228,6 +228,32 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
             'category': ["Requested category could not be found."]
             'category': ["Requested category could not be found."]
         })
         })
 
 
+    def test_merge_unallowed_start_thread(self):
+        """api rejects merge because category isn't allowing starting threads"""
+        self.override_acl({
+            'can_merge_threads': True,
+            'can_close_threads': False,
+            'can_edit_threads': False,
+            'can_reply_threads': False,
+            'can_start_threads': 0
+        })
+
+        thread = testutils.post_thread(category=self.category)
+
+        response = self.client.post(self.api_link, json.dumps({
+            'threads': [self.thread.id, thread.id],
+            'title': 'Valid thread title',
+            'category': self.category.id
+        }), content_type="application/json")
+        self.assertEqual(response.status_code, 400)
+
+        response_json = json.loads(smart_str(response.content))
+        self.assertEqual(response_json, {
+            'category': [
+                "You can't create new threads in selected category."
+            ]
+        })
+
     def test_merge_invalid_weight(self):
     def test_merge_invalid_weight(self):
         """api rejects merge because final weight was invalid"""
         """api rejects merge because final weight was invalid"""
         self.override_acl({
         self.override_acl({

+ 27 - 1
misago/threads/validators.py

@@ -1,9 +1,35 @@
 from django.core.exceptions import ValidationError
 from django.core.exceptions import ValidationError
-from django.utils.translation import ugettext_lazy as _, ungettext
+from django.utils.translation import ugettext as _, ungettext
 
 
+from misago.categories.models import THREADS_ROOT_NAME, Category
+from misago.categories.permissions import can_browse_category, can_see_category
 from misago.conf import settings
 from misago.conf import settings
 from misago.core.validators import validate_sluggable
 from misago.core.validators import validate_sluggable
 
 
+from .threadtypes import trees_map
+
+
+def validate_category(user, category_id, allow_root=False):
+    try:
+        threads_tree_id = trees_map.get_tree_id_for_root(THREADS_ROOT_NAME)
+        category = Category.objects.get(
+            tree_id=threads_tree_id,
+            id=category_id,
+        )
+    except Category.DoesNotExist:
+        category = None
+
+    # Skip ACL validation for root category?
+    if allow_root and category and not category.level:
+        return category
+
+    if not category or not can_see_category(user, category):
+        raise ValidationError(_("Requested category could not be found."))
+
+    if not can_browse_category(user, category):
+        raise ValidationError(_("You don't have permission to access this category."))
+    return category
+
 
 
 def validate_post(post):
 def validate_post(post):
     post_len = len(post)
     post_len = len(post)