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:
         else:
             return Response(patch)
             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):
     def validate_action(self, action):
         if not action.get('op'):
         if not action.get('op'):
             raise InvalidAction(u"undefined 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 import TestCase
 from django.test.client import RequestFactory
 from django.test.client import RequestFactory
 from django.urls import reverse
 from django.urls import reverse
+from django.utils import six
 
 
 from misago.core.utils import (
 from misago.core.utils import (
     clean_return_path, format_plaintext_for_html, is_referer_local, is_request_to_misago,
     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/", )
 VALID_PATHS = ("/", "/threads/", )
@@ -265,3 +266,37 @@ class GetExceptionMessageTests(TestCase):
 
 
         message = get_exception_message(default_message='Lorem Ipsum')
         message = get_exception_message(default_message='Lorem Ipsum')
         self.assertEqual(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 datetime import datetime, timedelta
 
 
 from django.conf import settings
 from django.conf import settings
+from django.core.exceptions import PermissionDenied
 from django.http import Http404
 from django.http import Http404
 from django.urls import resolve, reverse
 from django.urls import resolve, reverse
 from django.utils import html, timezone
 from django.utils import html, timezone
@@ -146,3 +147,10 @@ def get_exception_message(exception=None, default_message=None):
         return exception.args[0]
         return exception.args[0]
     except IndexError:
     except IndexError:
         return default_message
         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 django.utils.translation import ungettext
 
 
 from misago.conf import settings
 from misago.conf import settings
+from misago.core.utils import clean_ids_list
 from misago.threads.moderation import posts as moderation
 from misago.threads.moderation import posts as moderation
 from misago.threads.permissions import allow_delete_event, allow_delete_post
 from misago.threads.permissions import allow_delete_event, allow_delete_post
 from misago.threads.permissions import exclude_invisible_posts
 from misago.threads.permissions import exclude_invisible_posts
@@ -44,10 +45,10 @@ def sync_related(thread):
 
 
 
 
 def clean_posts_for_delete(request, 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:
     if not posts_ids:
         raise PermissionDenied(_("You have to specify at least one post to delete."))
         raise PermissionDenied(_("You have to specify at least one post to delete."))
@@ -64,6 +65,7 @@ def clean_posts_for_delete(request, thread):
 
 
     posts = []
     posts = []
     for post in posts_queryset:
     for post in posts_queryset:
+        post.category = thread.category
         post.thread = thread
         post.thread = thread
         if post.is_event:
         if post.is_event:
             allow_delete_event(request.user, post)
             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.acl import add_acl
 from misago.conf import settings
 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.permissions import allow_merge_post, exclude_invisible_posts
 from misago.threads.serializers import PostSerializer
 from misago.threads.serializers import PostSerializer
 
 
@@ -52,10 +53,10 @@ def posts_merge_endpoint(request, thread):
 
 
 
 
 def clean_posts_for_merge(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:
     if len(posts_ids) < 2:
         raise PermissionDenied(_("You have to select at least two posts to merge."))
         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 django.utils.translation import ugettext as _, ungettext
 
 
 from misago.conf import settings
 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.permissions import allow_move_post, exclude_invisible_posts
 from misago.threads.utils import get_thread_id_from_url
 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):
 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:
     if not posts_ids:
         raise PermissionDenied(_("You have to specify at least one post to move."))
         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 django.utils.translation import ugettext as _
 
 
 from misago.acl import add_acl
 from misago.acl import add_acl
+from misago.conf import settings
 from misago.core.apipatch import ApiPatch
 from misago.core.apipatch import ApiPatch
 from misago.threads.models import PostLike
 from misago.threads.models import PostLike
 from misago.threads.moderation import posts as moderation
 from misago.threads.moderation import posts as moderation
 from misago.threads.permissions import (
 from misago.threads.permissions import (
     allow_approve_post, allow_hide_post, allow_protect_post, allow_unhide_post)
     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()
 post_patch_dispatcher = ApiPatch()
 
 
 
 
@@ -150,3 +154,49 @@ def post_patch_endpoint(request, post):
             old_category.synchronize()
             old_category.synchronize()
             old_category.save()
             old_category.save()
     return response
     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 django.utils.translation import ugettext as _, ungettext
 
 
 from misago.conf import settings
 from misago.conf import settings
+from misago.core.utils import clean_ids_list
 from misago.threads.models import Thread
 from misago.threads.models import Thread
 from misago.threads.moderation import threads as moderation
 from misago.threads.moderation import threads as moderation
 from misago.threads.permissions import allow_split_post, exclude_invisible_posts
 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):
 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:
     if not posts_ids:
         raise PermissionDenied(_("You have to specify at least one post to split."))
         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 django.utils.translation import ungettext
 
 
 from misago.conf import settings
 from misago.conf import settings
+from misago.core.utils import clean_ids_list
 from misago.threads.permissions import allow_delete_thread
 from misago.threads.permissions import allow_delete_thread
 from misago.threads.moderation import threads as moderation
 from misago.threads.moderation import threads as moderation
 
 
@@ -47,10 +48,10 @@ def delete_bulk(request, viewmodel):
 
 
 
 
 def clean_threads_ids(request):
 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:
     if not threads_ids:
         raise PermissionDenied(_("You have to specify at least one thread to delete."))
         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.acl import add_acl
 from misago.categories import THREADS_ROOT_NAME
 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.events import record_event
 from misago.threads.models import Thread
 from misago.threads.models import Thread
 from misago.threads.moderation import threads as moderation
 from misago.threads.moderation import threads as moderation
@@ -136,10 +137,10 @@ def threads_merge_endpoint(request):
 
 
 
 
 def clean_threads_for_merge(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:
     if len(threads_ids) < 2:
         raise MergeError(_("You have to select at least two threads to merge."))
         raise MergeError(_("You have to select at least two threads to merge."))