Browse Source

endpoint for splitting posts to new thread, cleanup thread merge

Rafał Pitoń 8 years ago
parent
commit
994d0a9208

+ 40 - 58
misago/threads/api/postendpoints/split.py

@@ -5,8 +5,8 @@ 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
+from ...serializers import NewThreadSerializer
 
 
 SPLIT_LIMIT = settings.MISAGO_POSTS_PER_PAGE + settings.MISAGO_POSTS_TAIL
@@ -23,13 +23,12 @@ def posts_split_endpoint(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
+    serializer = NewThreadSerializer(context=request.user, data=request.data)
+    if serializer.is_valid():
+        split_posts_to_new_thread(request, thread, serializer.validated_data, posts)
+        return Response({})
+    else:
+        return Response(serializer.errors, status=400)
 
 
 def clean_posts_for_split(request, thread):
@@ -67,55 +66,38 @@ def clean_posts_for_split(request, thread):
     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,
+def split_posts_to_new_thread(request, thread, validated_data, posts):
+    new_thread = Thread(
+        category=validated_data['category'],
+        started_on=thread[0].started_on,
+        last_post_on=thread[0].last_post_on
     )
-    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
 
+    new_thread.set_title(validated_data['title'])
+    new_thread.save()
+
+    for post in posts:
+        post.move(new_thread)
+        post.save()
+
+    thread.synchronize()
+    thread.save()
+
+    new_thread.synchronize()
+    new_thread.save()
+
+    if validated_data.get('weight') == THREAD_WEIGHT_GLOBAL:
+        moderation.pin_thread_globally(request, new_thread)
+    elif validated_data.get('weight'):
+        moderation.pin_thread_locally(request, new_thread)
+    if validated_data.get('is_hidden', False):
+        moderation.hide_thread(request, new_thread)
+    if validated_data.get('is_closed', False):
+        moderation.close_thread(request, new_thread)
+
+    thread.category.synchronize()
+    thread.category.save()
+
+    if new_thread.category != thread.category:
+        new_thread.category.synchronize()
+        new_thread.category.save()

+ 14 - 59
misago/threads/api/threadendpoints/merge.py

@@ -2,19 +2,17 @@ from django.core.exceptions import PermissionDenied, ValidationError
 from django.http import Http404
 from django.utils.translation import gettext as _, ungettext
 
-from rest_framework import serializers
 from rest_framework.response import Response
 
 from misago.acl import add_acl
 from misago.categories.models import THREADS_ROOT_NAME, Category
 
 from ...events import record_event
-from ...models import THREAD_WEIGHT_DEFAULT, THREAD_WEIGHT_GLOBAL, Thread
+from ...models import THREAD_WEIGHT_GLOBAL, Thread
 from ...moderation import threads as moderation
-from ...permissions import can_start_thread, can_reply_thread, can_see_thread
-from ...serializers import ThreadsListSerializer
+from ...permissions import can_reply_thread, can_see_thread
+from ...serializers import NewThreadSerializer, ThreadsListSerializer
 from ...threadtypes import trees_map
-from ...validators import validate_category, validate_title
 from ...utils import add_categories_to_threads, get_thread_id_from_url
 
 
@@ -90,7 +88,7 @@ def threads_merge_endpoint(request):
     if invalid_threads:
         return Response(invalid_threads, status=403)
 
-    serializer = MergeThreadsSerializer(context=request.user, data=request.data)
+    serializer = NewThreadSerializer(context=request.user, data=request.data)
     if serializer.is_valid():
         new_thread = merge_threads(request, serializer.validated_data, threads)
         return Response(ThreadsListSerializer(new_thread).data)
@@ -135,10 +133,8 @@ def clean_threads_for_merge(request):
 def merge_threads(request, validated_data, threads):
     new_thread = Thread(
         category=validated_data['category'],
-        weight=validated_data.get('weight', 0),
-        is_closed=validated_data.get('is_closed', False),
         started_on=threads[0].started_on,
-        last_post_on=threads[0].last_post_on,
+        last_post_on=threads[0].last_post_on
     )
 
     new_thread.set_title(validated_data['title'])
@@ -157,6 +153,15 @@ def merge_threads(request, validated_data, threads):
     new_thread.synchronize()
     new_thread.save()
 
+    if validated_data.get('weight') == THREAD_WEIGHT_GLOBAL:
+        moderation.pin_thread_globally(request, new_thread)
+    elif validated_data.get('weight'):
+        moderation.pin_thread_locally(request, new_thread)
+    if validated_data.get('is_hidden', False):
+        moderation.hide_thread(request, new_thread)
+    if validated_data.get('is_closed', False):
+        moderation.close_thread(request, new_thread)
+
     if new_thread.category not in categories:
         categories.append(new_thread.category)
 
@@ -177,55 +182,5 @@ def merge_threads(request, validated_data, threads):
     else:
         new_thread.top_category = None
 
-    new_thread.save()
-
     add_acl(request.user, new_thread)
     return new_thread
-
-
-class MergeThreadsSerializer(serializers.Serializer):
-    title = serializers.CharField()
-    category = serializers.IntegerField()
-    top_category = serializers.IntegerField(required=False, allow_null=True)
-    weight = serializers.IntegerField(
-        required=False,
-        allow_null=True,
-        max_value=THREAD_WEIGHT_GLOBAL,
-        min_value=THREAD_WEIGHT_DEFAULT,
-    )
-    is_closed = serializers.NullBooleanField(required=False)
-
-    def validate_title(self, title):
-        return validate_title(title)
-
-    def validate_top_category(self, category_id):
-        return validate_category(self.context, category_id, allow_root=True)
-
-    def validate_category(self, 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
-
-    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_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

+ 1 - 0
misago/threads/serializers/__init__.py

@@ -1,2 +1,3 @@
+from .moderation import *
 from .thread import *
 from .post import *

+ 74 - 0
misago/threads/serializers/moderation.py

@@ -0,0 +1,74 @@
+from django.core.exceptions import ValidationError
+from django.utils.translation import gettext as _
+
+from rest_framework import serializers
+
+from misago.acl import add_acl
+
+from ..models import THREAD_WEIGHT_DEFAULT, THREAD_WEIGHT_GLOBAL
+from ..permissions import can_start_thread
+from ..validators import validate_category, validate_title
+
+
+__all__ = [
+    'NewThreadSerializer',
+]
+
+
+class NewThreadSerializer(serializers.Serializer):
+    title = serializers.CharField()
+    category = serializers.IntegerField()
+    top_category = serializers.IntegerField(required=False, allow_null=True)
+    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)
+        if not can_start_thread(self.context, self.category):
+            raise ValidationError(_("You can't create new threads in selected category."))
+        return self.category
+
+    def validate_top_category(self, category_id):
+        return validate_category(self.context, category_id, allow_root=True)
+
+    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 hidden 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

+ 53 - 1
misago/threads/tests/test_threads_merge_api.py

@@ -410,7 +410,6 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         """api allows for closing thread"""
         self.override_acl({
             'can_merge_threads': True,
-            'can_close_threads': False,
             'can_edit_threads': False,
             'can_reply_threads': False,
             'can_close_threads': True,
@@ -432,6 +431,59 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
             'title': ["Thread title should be at least 5 characters long (it has 3)."]
         })
 
+    def test_merge_unallowed_hidden(self):
+        """api rejects merge because hidden thread was unallowed"""
+        self.override_acl({
+            'can_merge_threads': True,
+            'can_close_threads': False,
+            'can_edit_threads': False,
+            'can_reply_threads': False,
+            'can_hide_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,
+            'is_hidden': True,
+        }), content_type="application/json")
+        self.assertEqual(response.status_code, 400)
+
+        response_json = json.loads(smart_str(response.content))
+        self.assertEqual(response_json, {
+            'is_hidden': [
+                "You don't have permission to hide threads in this category."
+            ]
+        })
+
+    def test_merge_with_hide(self):
+        """api allows for hiding thread"""
+        self.override_acl({
+            'can_merge_threads': True,
+            'can_close_threads': False,
+            'can_edit_threads': False,
+            'can_reply_threads': False,
+            'can_hide_threads': 1,
+        })
+
+        thread = testutils.post_thread(category=self.category)
+
+        response = self.client.post(self.api_link, json.dumps({
+            'threads': [self.thread.id, thread.id],
+            'title': '$$$',
+            'category': self.category.id,
+            'weight': 0,
+            'is_hidden': True,
+        }), content_type="application/json")
+        self.assertEqual(response.status_code, 400)
+
+        response_json = json.loads(smart_str(response.content))
+        self.assertEqual(response_json, {
+            'title': ["Thread title should be at least 5 characters long (it has 3)."]
+        })
+
     def test_merge(self):
         """api performs basic merge"""
         posts_ids = [p.id for p in Post.objects.all()]