Browse Source

wip #893: moved ids lists cleaning to utility function

Rafał Pitoń 7 years ago
parent
commit
bfaf56554d

+ 35 - 0
misago/core/apipatch.py

@@ -67,6 +67,41 @@ class ApiPatch(object):
         else:
             return Response(patch)
 
+    def dispatch_bulk(self, request, targets):
+        if not isinstance(request.data.get('ops'), list):
+            return Response({
+                'detail': "bulk PATCH request's ops value should be list of operations",
+            }, status=400)
+
+        is_errored = False
+        result = []
+
+        for target in targets:
+            detail = []
+
+            patch = {'id': target.pk}
+            for action in request.data['ops']:
+                try:
+                    self.validate_action(action)
+                    self.dispatch_action(patch, request, target, action)
+                    detail.append('ok')
+                except Http404:
+                    is_errored = True
+                    detail.append('NOT FOUND')
+                    break
+                except (InvalidAction, PermissionDenied) as e:
+                    is_errored = True
+                    detail.append(e.args[0])
+                    break
+
+            patch['detail'] = detail
+            result.append(patch)
+
+        if is_errored:
+            return Response(result, status=400)
+        else:
+            return Response(result)
+
     def validate_action(self, action):
         if not action.get('op'):
             raise InvalidAction(u"undefined op")

+ 36 - 1
misago/core/tests/test_utils.py

@@ -5,10 +5,11 @@ from django.core.exceptions import PermissionDenied
 from django.test import TestCase
 from django.test.client import RequestFactory
 from django.urls import reverse
+from django.utils import six
 
 from misago.core.utils import (
     clean_return_path, format_plaintext_for_html, is_referer_local, is_request_to_misago,
-    parse_iso8601_string, slugify, get_exception_message)
+    parse_iso8601_string, slugify, get_exception_message, clean_ids_list)
 
 
 VALID_PATHS = ("/", "/threads/", )
@@ -265,3 +266,37 @@ class GetExceptionMessageTests(TestCase):
 
         message = get_exception_message(default_message='Lorem Ipsum')
         self.assertEqual(message, 'Lorem Ipsum')
+
+
+class CleanIdsListTests(TestCase):
+    def test_valid_list(self):
+        """list of valid ids is cleaned"""
+        self.assertEqual(clean_ids_list(['1', 3, '42'], None), [1, 3, 42])
+
+    def test_empty_list(self):
+        """empty list passes validation"""
+        self.assertEqual(clean_ids_list([], None), [])
+
+    def test_string_list(self):
+        """string list passes validation"""
+        self.assertEqual(clean_ids_list('1234', None), [1, 2, 3, 4])
+
+    def test_message(self):
+        """utility uses passed message for exception"""
+        with self.assertRaisesMessage(PermissionDenied, "Test error message!"):
+            clean_ids_list(None, "Test error message!")
+
+    def test_invalid_inputs(self):
+        """utility raises exception for invalid inputs"""
+        INVALID_INPUTS = (
+            None,
+            'abc',
+            [None],
+            [1, 2, 'a', 4],
+            [1, None, 3],
+            {1: 2, 'a': 4},
+        )
+
+        for invalid_input in INVALID_INPUTS:
+            with self.assertRaisesMessage(PermissionDenied, "Test error message!"):
+                clean_ids_list(invalid_input, "Test error message!")

+ 8 - 0
misago/core/utils.py

@@ -1,6 +1,7 @@
 from datetime import datetime, timedelta
 
 from django.conf import settings
+from django.core.exceptions import PermissionDenied
 from django.http import Http404
 from django.urls import resolve, reverse
 from django.utils import html, timezone
@@ -146,3 +147,10 @@ def get_exception_message(exception=None, default_message=None):
         return exception.args[0]
     except IndexError:
         return default_message
+
+
+def clean_ids_list(ids_list, error_message):
+    try:
+        return list(map(int, ids_list))
+    except (ValueError, TypeError):
+        raise PermissionDenied(error_message)

+ 6 - 4
misago/threads/api/postendpoints/delete.py

@@ -5,6 +5,7 @@ from django.utils.translation import ugettext as _
 from django.utils.translation import ungettext
 
 from misago.conf import settings
+from misago.core.utils import clean_ids_list
 from misago.threads.moderation import posts as moderation
 from misago.threads.permissions import allow_delete_event, allow_delete_post
 from misago.threads.permissions import exclude_invisible_posts
@@ -44,10 +45,10 @@ def sync_related(thread):
 
 
 def clean_posts_for_delete(request, thread):
-    try:
-        posts_ids = list(map(int, request.data or []))
-    except (ValueError, TypeError):
-        raise PermissionDenied(_("One or more post ids received were invalid."))
+    posts_ids = clean_ids_list(
+        request.data or [],
+        _("One or more post ids received were invalid."),
+    )
 
     if not posts_ids:
         raise PermissionDenied(_("You have to specify at least one post to delete."))
@@ -64,6 +65,7 @@ def clean_posts_for_delete(request, thread):
 
     posts = []
     for post in posts_queryset:
+        post.category = thread.category
         post.thread = thread
         if post.is_event:
             allow_delete_event(request.user, post)

+ 5 - 4
misago/threads/api/postendpoints/merge.py

@@ -6,6 +6,7 @@ from django.utils.translation import ugettext as _, ungettext
 
 from misago.acl import add_acl
 from misago.conf import settings
+from misago.core.utils import clean_ids_list
 from misago.threads.permissions import allow_merge_post, exclude_invisible_posts
 from misago.threads.serializers import PostSerializer
 
@@ -52,10 +53,10 @@ def posts_merge_endpoint(request, thread):
 
 
 def clean_posts_for_merge(request, thread):
-    try:
-        posts_ids = list(map(int, request.data.get('posts', [])))
-    except (ValueError, TypeError):
-        raise PermissionDenied(_("One or more post ids received were invalid."))
+    posts_ids = clean_ids_list(
+        request.data.get('posts', []),
+        _("One or more post ids received were invalid."),
+    )
 
     if len(posts_ids) < 2:
         raise PermissionDenied(_("You have to select at least two posts to merge."))

+ 5 - 4
misago/threads/api/postendpoints/move.py

@@ -6,6 +6,7 @@ from django.utils import six
 from django.utils.translation import ugettext as _, ungettext
 
 from misago.conf import settings
+from misago.core.utils import clean_ids_list
 from misago.threads.permissions import allow_move_post, exclude_invisible_posts
 from misago.threads.utils import get_thread_id_from_url
 
@@ -67,10 +68,10 @@ def clean_thread_for_move(request, thread, viewmodel):
 
 
 def clean_posts_for_move(request, thread):
-    try:
-        posts_ids = list(map(int, request.data.get('posts', [])))
-    except (ValueError, TypeError):
-        raise PermissionDenied(_("One or more post ids received were invalid."))
+    posts_ids = clean_ids_list(
+        request.data.get('posts', []),
+        _("One or more post ids received were invalid."),
+    )
 
     if not posts_ids:
         raise PermissionDenied(_("You have to specify at least one post to move."))

+ 50 - 0
misago/threads/api/postendpoints/patch_post.py

@@ -2,13 +2,17 @@ from django.core.exceptions import PermissionDenied
 from django.utils.translation import ugettext as _
 
 from misago.acl import add_acl
+from misago.conf import settings
 from misago.core.apipatch import ApiPatch
 from misago.threads.models import PostLike
 from misago.threads.moderation import posts as moderation
 from misago.threads.permissions import (
     allow_approve_post, allow_hide_post, allow_protect_post, allow_unhide_post)
+from misago.threads.permissions import exclude_invisible_posts
 
 
+PATCH_LIMIT = settings.MISAGO_POSTS_PER_PAGE + settings.MISAGO_POSTS_TAIL
+
 post_patch_dispatcher = ApiPatch()
 
 
@@ -150,3 +154,49 @@ def post_patch_endpoint(request, post):
             old_category.synchronize()
             old_category.save()
     return response
+
+
+def bulk_patch_endpoint(request, thread):
+    posts = clean_posts_for_patch(request, thread)
+
+    hidden_posts = 0
+    revealed_posts = 0
+    moved_posts = 0
+
+    response = post_patch_dispatcher.dispatch_bulk(request, posts)
+
+
+def clean_posts_for_patch(request, thread):
+    if not isinstance(request.data, dict):
+        raise PermissionDenied(_("Bulk PATCH request should be a dict with ids and ops keys."))
+
+    # todo: move this ids list cleanup step to utility
+
+    try:
+        posts_ids = list(map(int, request.data.get('ids', [])))
+    except (ValueError, TypeError):
+        raise PermissionDenied(_("One or more post ids received were invalid."))
+
+    if not posts_ids:
+        raise PermissionDenied(_("You have to specify at least one post to update."))
+    elif len(posts_ids) > PATCH_LIMIT:
+        message = ungettext(
+            "No more than %(limit)s post can be updated at single time.",
+            "No more than %(limit)s posts can be updated at single time.",
+            PATCH_LIMIT,
+        )
+        raise PermissionDenied(message % {'limit': PATCH_LIMIT})
+
+    posts_queryset = exclude_invisible_posts(request.user, thread.category, thread.post_set)
+    posts_queryset = posts_queryset.filter(id__in=posts_ids).order_by('id')
+
+    posts = []
+    for post in posts_queryset:
+        post.category = thread.category
+        post.thread = thread
+        posts.append(post)
+
+    if len(posts) != len(posts_ids):
+        raise PermissionDenied(_("One or more posts to update could not be found."))
+
+    return posts

+ 5 - 4
misago/threads/api/postendpoints/split.py

@@ -5,6 +5,7 @@ from django.utils import six
 from django.utils.translation import ugettext as _, ungettext
 
 from misago.conf import settings
+from misago.core.utils import clean_ids_list
 from misago.threads.models import Thread
 from misago.threads.moderation import threads as moderation
 from misago.threads.permissions import allow_split_post, exclude_invisible_posts
@@ -32,10 +33,10 @@ def posts_split_endpoint(request, thread):
 
 
 def clean_posts_for_split(request, thread):
-    try:
-        posts_ids = list(map(int, request.data.get('posts', [])))
-    except (ValueError, TypeError):
-        raise PermissionDenied(_("One or more post ids received were invalid."))
+    posts_ids = clean_ids_list(
+        request.data.get('posts', []),
+        _("One or more post ids received were invalid."),
+    )
 
     if not posts_ids:
         raise PermissionDenied(_("You have to specify at least one post to split."))

+ 5 - 4
misago/threads/api/threadendpoints/delete.py

@@ -8,6 +8,7 @@ from django.utils.translation import ugettext as _
 from django.utils.translation import ungettext
 
 from misago.conf import settings
+from misago.core.utils import clean_ids_list
 from misago.threads.permissions import allow_delete_thread
 from misago.threads.moderation import threads as moderation
 
@@ -47,10 +48,10 @@ def delete_bulk(request, viewmodel):
 
 
 def clean_threads_ids(request):
-    try:
-        threads_ids = list(map(int, request.data or []))
-    except (ValueError, TypeError):
-        raise PermissionDenied(_("One or more thread ids received were invalid."))
+    threads_ids = clean_ids_list(
+        request.data or [],
+        _("One or more thread ids received were invalid."),
+    )
 
     if not threads_ids:
         raise PermissionDenied(_("You have to specify at least one thread to delete."))

+ 5 - 4
misago/threads/api/threadendpoints/merge.py

@@ -8,6 +8,7 @@ from django.utils.translation import ungettext
 
 from misago.acl import add_acl
 from misago.categories import THREADS_ROOT_NAME
+from misago.core.utils import clean_ids_list
 from misago.threads.events import record_event
 from misago.threads.models import Thread
 from misago.threads.moderation import threads as moderation
@@ -136,10 +137,10 @@ def threads_merge_endpoint(request):
 
 
 def clean_threads_for_merge(request):
-    try:
-        threads_ids = list(map(int, request.data.get('threads', [])))
-    except (ValueError, TypeError):
-        raise MergeError(_("One or more thread ids received were invalid."))
+    threads_ids = clean_ids_list(
+        request.data.get('threads', []),
+        _("One or more thread ids received were invalid."),
+    )
 
     if len(threads_ids) < 2:
         raise MergeError(_("You have to select at least two threads to merge."))