Browse Source

wip posts move endpoint

Rafał Pitoń 8 years ago
parent
commit
e7047ba97d

+ 1 - 1
misago/threads/api/postendpoints/merge.py

@@ -70,7 +70,7 @@ def clean_posts_for_merge(request, thread):
         if post.is_event:
             raise MergeError(_("Events can't be merged."))
         if post.is_hidden and not (post.pk == thread.first_post_id or thread.category.acl['can_hide_posts']):
-            raise MergeError(_("You can't merge posts the content you can't see"))
+            raise MergeError(_("You can't merge posts the content you can't see."))
 
         if not posts:
             posts.append(post)

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

@@ -0,0 +1,101 @@
+from django.conf import settings
+from django.core.exceptions import PermissionDenied
+from django.utils.translation import ugettext as _, ungettext
+
+from rest_framework.response import Response
+
+from misago.acl import add_acl
+
+from ...permissions.threads import exclude_invisible_posts
+from ...utils import get_thread_id_from_url
+
+
+MOVE_LIMIT = settings.MISAGO_POSTS_PER_PAGE + settings.MISAGO_POSTS_TAIL
+
+
+class MoveError(Exception):
+    def __init__(self, msg):
+        self.msg = msg
+
+
+def posts_move_endpoint(request, thread, viewmodel):
+    if not thread.acl['can_move_posts']:
+        raise PermissionDenied(_("You can't move posts in this thread."))
+
+    try:
+        new_thread = clean_thread_for_move(request, thread, viewmodel)
+        posts = clean_posts_for_move(request, thread)
+    except MoveError as e:
+        return Response({'detail': e.msg}, status=400)
+
+    for post in posts:
+        post.move(thread)
+        post.save()
+
+    thread.synchronize()
+    thread.save()
+
+    new_thread.synchronize()
+    new_thread.save()
+
+    thread.category.synchronize()
+    thread.category.save()
+
+    if thread.category != new_thread.category:
+        new_thread.category.synchronize()
+        new_thread.category.save()
+
+    return Response({})
+
+
+def clean_thread_for_move(request, thread):
+    new_thread_id = get_thread_id_from_url(request, request.data.get('thread_url', None))
+    if not new_thread_id:
+        raise MoveError(_("This is not a valid thread link."))
+    if new_thread_id == thread.pk:
+        raise MoveError(_("You can't move this thread's posts to itself."))
+
+    try:
+        new_thread = viewmodel(request, new_thread_id, select_for_update=True).model
+    except PermissionDenied as e:
+        raise MoveError(e.args[0])
+    except Http404:
+        raise MoveError(_("The thread you have entered link to doesn't exist or you don't have permission to see it."))
+
+    if not new_thread.acl['can_reply']:
+        raise MoveError(_("You don't have permission to move posts to thread you can't reply."))
+
+    return new_thread
+
+
+def clean_posts_for_move(request, thread):
+    try:
+        posts_ids = list(map(int, request.data.get('posts', [])))
+    except (ValueError, TypeError):
+        raise MoveError(_("One or more post ids received were invalid."))
+
+    if len(posts_ids) > MOVE_LIMIT:
+        message = ungettext(
+            "No more than %(limit)s post can be moved at single time.",
+            "No more than %(limit)s posts can be moved at single time.",
+            MOVE_LIMIT)
+        raise MoveError(message % {'limit': MOVE_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 MoveError(_("Events can't be moved."))
+        if post.pk == thread.first_post_id:
+            raise MoveError(_("You can't move first post in thread."))
+        if post.is_hidden and not thread.category.acl['can_hide_posts']:
+            raise MoveError(_("You can't move posts the content you can't see."))
+
+        posts.append(post)
+
+    if len(posts) != len(posts_ids):
+        raise MoveError(_("One or more posts to move could not be found."))
+
+    return posts

+ 7 - 0
misago/threads/api/threadposts.py

@@ -19,6 +19,7 @@ from ..viewmodels.posts import ThreadPosts
 from ..viewmodels.thread import ForumThread
 from .postingendpoint import PostingEndpoint
 from .postendpoints.merge import posts_merge_endpoint
+from .postendpoints.move import posts_move_endpoint
 from .postendpoints.patch_event import event_patch_endpoint
 from .postendpoints.patch_post import post_patch_endpoint
 
@@ -74,6 +75,12 @@ class ViewSet(viewsets.ViewSet):
         thread = self.get_thread_for_update(request, thread_pk).model
         return posts_merge_endpoint(request, thread)
 
+    @list_route(methods=['post'], url_path='move')
+    @transaction.atomic
+    def move(self, request, thread_pk):
+        thread = self.get_thread_for_update(request, thread_pk).model
+        return posts_move_endpoint(request, thread, self.thread)
+
     @transaction.atomic
     def create(self, request, thread_pk):
         thread = self.get_thread_for_update(request, thread_pk).model

+ 1 - 57
misago/threads/threadtypes/__init__.py

@@ -1,60 +1,4 @@
-from importlib import import_module
-from django.conf import settings
-
-
-class TreesMap(object):
-    """Object that maps trees to strategies"""
-    def __init__(self, types_modules):
-        self.is_loaded = False
-        self.types_modules = types_modules
-
-    def load(self):
-        self.types = self.load_types(self.types_modules)
-        self.trees = self.load_trees(self.types)
-        self.roots = self.get_roots(self.trees)
-        self.is_loaded = True
-
-    def load_types(self, types_modules):
-        loaded_types = {}
-        for path in types_modules:
-            module = import_module('.'.join(path.split('.')[:-1]))
-            type_cls = getattr(module, path.split('.')[-1])
-            loaded_types[type_cls.root_name] = type_cls()
-        return loaded_types
-
-    def load_trees(self, types):
-        from misago.categories.models import Category
-        trees = {}
-        for category in Category.objects.filter(level=0, special_role__in=types.keys()):
-            trees[category.tree_id] = types[category.special_role]
-        return trees
-
-    def get_roots(self, trees):
-        roots = {}
-        for tree_id in trees:
-            roots[trees[tree_id].root_name] = tree_id
-        return roots
-
-    def get_type_for_tree_id(self, tree_id):
-        if not self.is_loaded:
-            self.load()
-
-        try:
-            return self.trees[tree_id]
-        except KeyError:
-            raise KeyError("'%s' tree id has no type defined" % tree_id)
-
-    def get_tree_id_for_root(self, root_name):
-        if not self.is_loaded:
-            self.load()
-
-        try:
-            return self.roots[root_name]
-        except KeyError:
-            raise KeyError('"%s" root has no tree defined' % root_name)
-
-
-trees_map = TreesMap(settings.MISAGO_THREAD_TYPES)
+from .treesmap import trees_map
 
 
 class ThreadType(object):

+ 57 - 0
misago/threads/threadtypes/treesmap.py

@@ -0,0 +1,57 @@
+from importlib import import_module
+from django.conf import settings
+
+
+class TreesMap(object):
+    """Object that maps trees to strategies"""
+    def __init__(self, types_modules):
+        self.is_loaded = False
+        self.types_modules = types_modules
+
+    def load(self):
+        self.types = self.load_types(self.types_modules)
+        self.trees = self.load_trees(self.types)
+        self.roots = self.get_roots(self.trees)
+        self.is_loaded = True
+
+    def load_types(self, types_modules):
+        loaded_types = {}
+        for path in types_modules:
+            module = import_module('.'.join(path.split('.')[:-1]))
+            type_cls = getattr(module, path.split('.')[-1])
+            loaded_types[type_cls.root_name] = type_cls()
+        return loaded_types
+
+    def load_trees(self, types):
+        from misago.categories.models import Category
+        trees = {}
+        for category in Category.objects.filter(level=0, special_role__in=types.keys()):
+            trees[category.tree_id] = types[category.special_role]
+        return trees
+
+    def get_roots(self, trees):
+        roots = {}
+        for tree_id in trees:
+            roots[trees[tree_id].root_name] = tree_id
+        return roots
+
+    def get_type_for_tree_id(self, tree_id):
+        if not self.is_loaded:
+            self.load()
+
+        try:
+            return self.trees[tree_id]
+        except KeyError:
+            raise KeyError("'%s' tree id has no type defined" % tree_id)
+
+    def get_tree_id_for_root(self, root_name):
+        if not self.is_loaded:
+            self.load()
+
+        try:
+            return self.roots[root_name]
+        except KeyError:
+            raise KeyError('"%s" root has no tree defined' % root_name)
+
+
+trees_map = TreesMap(settings.MISAGO_THREAD_TYPES)

+ 3 - 2
misago/threads/viewmodels/thread.py

@@ -2,7 +2,7 @@ from django.shortcuts import get_object_or_404
 from django.utils.translation import gettext as _
 
 from misago.acl import add_acl
-from misago.categories.models import Category
+from misago.categories.models import THREADS_ROOT_NAME, Category
 from misago.core.shortcuts import validate_slug
 from misago.readtracker.threadstracker import make_read_aware
 
@@ -10,6 +10,7 @@ from ..models import Thread
 from ..permissions.threads import allow_see_thread
 from ..serializers import ThreadSerializer
 from ..subscriptions import make_subscription_aware
+from ..threadtypes import trees_map
 
 
 BASE_RELATIONS = ('category', 'starter', 'starter__rank', 'starter__ban_cache', 'starter__online_tracker')
@@ -88,7 +89,7 @@ class ForumThread(ViewModel):
         thread = get_object_or_404(
             queryset,
             pk=pk,
-            category__tree_id=Category.objects.root_category().tree_id
+            category__tree_id=trees_map.get_tree_id_for_root(THREADS_ROOT_NAME)
         )
 
         allow_see_thread(request.user, thread)