Browse Source

Merge in changes from Misago 0.17

Rafał Pitoń 7 years ago
parent
commit
28d58ba125

+ 1 - 0
misago/api/patch.py

@@ -138,4 +138,5 @@ class ApiPatch(object):
             return six.text_type(exception), 403
             return six.text_type(exception), 403
 
 
         if isinstance(exception, Http404):
         if isinstance(exception, Http404):
+            # fixme: don't return exception's message
             return six.text_type(exception) or "NOT FOUND", 404
             return six.text_type(exception) or "NOT FOUND", 404

+ 7 - 33
misago/threads/api/threadendpoints/merge.py

@@ -7,7 +7,6 @@ from django.utils.translation import ugettext as _
 
 
 from misago.acl import add_acl
 from misago.acl import add_acl
 from misago.threads.events import record_event
 from misago.threads.events import record_event
-from misago.threads.mergeconflict import MergeConflict
 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_merge_thread
 from misago.threads.permissions import allow_merge_thread
@@ -47,12 +46,11 @@ def thread_merge_endpoint(request, thread, viewmodel):
     if 'poll' in serializer.merge_conflict:
     if 'poll' in serializer.merge_conflict:
         if poll and poll.thread_id != other_thread.id:
         if poll and poll.thread_id != other_thread.id:
             other_thread.poll.delete()
             other_thread.poll.delete()
-        poll.move(other_thread)
-    else:
-        if hasattr(thread, 'poll'):
-            thread.poll.delete()
-        if hasattr(other_thread, 'poll'):
+            poll.move(other_thread)
+        elif not poll:
             other_thread.poll.delete()
             other_thread.poll.delete()
+    elif poll:
+        poll.move(other_thread)
 
 
     # merge thread contents
     # merge thread contents
     moderation.merge_thread(request, other_thread, thread)
     moderation.merge_thread(request, other_thread, thread)
@@ -85,32 +83,9 @@ def threads_merge_endpoint(request):
     serializer.is_valid(raise_exception=True)
     serializer.is_valid(raise_exception=True)
 
 
     threads = serializer.validated_data['threads']
     threads = serializer.validated_data['threads']
-    invalid_threads = []
-
-    for thread in threads:
-        try:
-            allow_merge_thread(request.user, thread)
-        except PermissionDenied as e:
-            invalid_threads.append({
-                'id': thread.pk,
-                'title': thread.title,
-                'errors': [text_type(e)]
-            })
-
-    if invalid_threads:
-        return Response(invalid_threads, status=403)
-
-    # handle merge conflict
-    merge_conflict = MergeConflict(serializer.validated_data, threads)
-    merge_conflict.is_valid(raise_exception=True)
-
-    new_thread = merge_threads(request, serializer.validated_data, threads, merge_conflict)
-    return Response(ThreadsListSerializer(new_thread).data)
 
 
     data = serializer.validated_data
     data = serializer.validated_data
-    
     threads = data['threads']
     threads = data['threads']
-    poll = data['poll']
 
 
     new_thread = Thread(
     new_thread = Thread(
         category=data['category'],
         category=data['category'],
@@ -121,9 +96,8 @@ def threads_merge_endpoint(request):
     new_thread.set_title(data['title'])
     new_thread.set_title(data['title'])
     new_thread.save()
     new_thread.save()
 
 
-    resolution = merge_conflict.get_resolution()
-
-    best_answer = resolution.get('best_answer')
+    # handle merge conflict
+    best_answer = data.get('best_answer')
     if best_answer:
     if best_answer:
         new_thread.best_answer_id = best_answer.best_answer_id
         new_thread.best_answer_id = best_answer.best_answer_id
         new_thread.best_answer_is_protected = best_answer.best_answer_is_protected
         new_thread.best_answer_is_protected = best_answer.best_answer_is_protected
@@ -132,7 +106,7 @@ def threads_merge_endpoint(request):
         new_thread.best_answer_marked_by_name = best_answer.best_answer_marked_by_name
         new_thread.best_answer_marked_by_name = best_answer.best_answer_marked_by_name
         new_thread.best_answer_marked_by_slug = best_answer.best_answer_marked_by_slug
         new_thread.best_answer_marked_by_slug = best_answer.best_answer_marked_by_slug
 
 
-    poll = resolution.get('poll')
+    poll = data.get('poll')
     if poll:
     if poll:
         poll.move(new_thread)
         poll.move(new_thread)
 
 

+ 7 - 7
misago/threads/api/threadendpoints/patch.py

@@ -51,7 +51,7 @@ def patch_title(request, thread, value):
     try:
     try:
         value_cleaned = six.text_type(value).strip()
         value_cleaned = six.text_type(value).strip()
     except (TypeError, ValueError):
     except (TypeError, ValueError):
-        raise PermissionDenied(_('Not a valid string.'))
+        raise ValidationError(_('Not a valid string.'))
 
 
     validate_title(value_cleaned)
     validate_title(value_cleaned)
 
 
@@ -201,7 +201,7 @@ def patch_best_answer(request, thread, value):
     try:
     try:
         post_id = int(value)
         post_id = int(value)
     except (TypeError, ValueError):
     except (TypeError, ValueError):
-        raise PermissionDenied(_("A valid integer is required."))
+        raise ValidationError(_("A valid integer is required."))
 
 
     allow_mark_best_answer(request.user, thread)
     allow_mark_best_answer(request.user, thread)
 
 
@@ -238,7 +238,7 @@ def patch_unmark_best_answer(request, thread, value):
     try:
     try:
         post_id = int(value)
         post_id = int(value)
     except (TypeError, ValueError):
     except (TypeError, ValueError):
-        raise PermissionDenied(_("A valid integer is required."))
+        raise ValidationError(_("A valid integer is required."))
 
 
     post = get_object_or_404(thread.post_set, id=post_id)
     post = get_object_or_404(thread.post_set, id=post_id)
     post.category = thread.category
     post.category = thread.category
@@ -303,8 +303,8 @@ thread_patch_dispatcher.add('participants', patch_add_participant)
 def patch_remove_participant(request, thread, value):
 def patch_remove_participant(request, thread, value):
     try:
     try:
         user_id = int(value)
         user_id = int(value)
-    except (ValueError, TypeError):
-        raise PermissionDenied(_("A valid integer is required."))
+    except (TypeError, ValueError):
+        raise ValidationError(_("A valid integer is required."))
 
 
     for participant in thread.participants_list:
     for participant in thread.participants_list:
         if participant.user_id == user_id:
         if participant.user_id == user_id:
@@ -333,8 +333,8 @@ thread_patch_dispatcher.remove('participants', patch_remove_participant)
 def patch_replace_owner(request, thread, value):
 def patch_replace_owner(request, thread, value):
     try:
     try:
         user_id = int(value)
         user_id = int(value)
-    except (ValueError, TypeError):
-        raise PermissionDenied(_("A valid integer is required."))
+    except (TypeError, ValueError):
+        raise ValidationError(_("A valid integer is required."))
 
 
     for participant in thread.participants_list:
     for participant in thread.participants_list:
         if participant.user_id == user_id:
         if participant.user_id == user_id:

+ 6 - 20
misago/threads/serializers/moderation.py

@@ -547,24 +547,10 @@ class MergeThreadsSerializer(NewThreadSerializer):
 
 
     def validate(self, data):
     def validate(self, data):
         data['threads'] = self.get_valid_threads(data['threads'])
         data['threads'] = self.get_valid_threads(data['threads'])
-        data['poll'] = validate_poll_merge(data['threads'], data)
-        return data
-
-
-def validate_poll_merge(threads, data):
-    merge_handler = PollMergeHandler(threads)
-
-    if len(merge_handler.polls) == 1:
-        return merge_handler.polls[0]
-
-    if merge_handler.is_merge_conflict():
-        if 'poll' in data:
-            merge_handler.set_resolution(data['poll'])
-            if merge_handler.is_valid():
-                return merge_handler.get_resolution()
-            else:
-                raise ValidationError({'poll': [_("Invalid choice.")]})
-        else:
-            raise ValidationError({'polls': merge_handler.get_available_resolutions()})
 
 
-    return None
+        merge_conflict = MergeConflict(data, data['threads'])
+        merge_conflict.is_valid(raise_exception=True)
+        data.update(merge_conflict.get_resolution())
+        self.merge_conflict = merge_conflict.get_conflicting_fields()
+        
+        return data

+ 1 - 1
misago/threads/tests/test_anonymize_data.py

@@ -35,7 +35,7 @@ class AnonymizeEventsTests(AuthenticatedUserTestCase):
         request.user_ip = '127.0.0.1'
         request.user_ip = '127.0.0.1'
 
 
         request.include_frontend_context = False
         request.include_frontend_context = False
-        request.frontend_context = {}
+        request.frontend_context = {'conf': {}, 'store': {}, 'url': {}}
 
 
         return request
         return request
 
 

+ 3 - 1
misago/threads/tests/test_thread_merge_api.py

@@ -570,7 +570,9 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
             }
             }
         )
         )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
-        self.assertEqual(response.json(), {'detail': "Invalid choice."})
+        self.assertEqual(response.json(), {
+            'best_answer': ["Invalid choice."],
+        })
 
 
         # best answers were untouched
         # best answers were untouched
         self.assertEqual(self.thread.post_set.count(), 2)
         self.assertEqual(self.thread.post_set.count(), 2)

+ 73 - 104
misago/threads/tests/test_thread_patch_api.py

@@ -1371,8 +1371,6 @@ class ThreadMarkBestAnswerApiTests(ThreadPatchApiTestCase):
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.json(), {
         self.assertEqual(response.json(), {
             'id': self.thread.id,
             'id': self.thread.id,
-            'detail': ['ok'],
-
             'best_answer': best_answer.id,
             'best_answer': best_answer.id,
             'best_answer_is_protected': False,
             'best_answer_is_protected': False,
             'best_answer_marked_on': response.json()['best_answer_marked_on'],
             'best_answer_marked_on': response.json()['best_answer_marked_on'],
@@ -1430,12 +1428,11 @@ class ThreadMarkBestAnswerApiTests(ThreadPatchApiTestCase):
                 },
                 },
             ]
             ]
         )
         )
-        self.assertEqual(response.status_code, 400)
+        self.assertEqual(response.status_code, 403)
         self.assertEqual(response.json(), {
         self.assertEqual(response.json(), {
-            'id': self.thread.id,
-            'detail': [
+            'detail': (
                 'You don\'t have permission to mark best answers in the "First category" category.'
                 'You don\'t have permission to mark best answers in the "First category" category.'
-            ],
+            ),
         })
         })
 
 
         thread_json = self.get_thread_json()
         thread_json = self.get_thread_json()
@@ -1456,13 +1453,12 @@ class ThreadMarkBestAnswerApiTests(ThreadPatchApiTestCase):
                 },
                 },
             ]
             ]
         )
         )
-        self.assertEqual(response.status_code, 400)
+        self.assertEqual(response.status_code, 403)
         self.assertEqual(response.json(), {
         self.assertEqual(response.json(), {
-            'id': self.thread.id,
-            'detail': [
+            'detail': (
                 "You don't have permission to mark best answer in this thread because you didn't "
                 "You don't have permission to mark best answer in this thread because you didn't "
                 "start it."
                 "start it."
-            ],
+            ),
         })
         })
 
 
         thread_json = self.get_thread_json()
         thread_json = self.get_thread_json()
@@ -1503,13 +1499,12 @@ class ThreadMarkBestAnswerApiTests(ThreadPatchApiTestCase):
                 },
                 },
             ]
             ]
         )
         )
-        self.assertEqual(response.status_code, 400)
+        self.assertEqual(response.status_code, 403)
         self.assertEqual(response.json(), {
         self.assertEqual(response.json(), {
-            'id': self.thread.id,
-            'detail': [
+            'detail': (
                 'You don\'t have permission to mark best answer in this thread because its '
                 'You don\'t have permission to mark best answer in this thread because its '
                 'category "First category" is closed.'
                 'category "First category" is closed.'
-            ],
+            ),
         })
         })
 
 
         thread_json = self.get_thread_json()
         thread_json = self.get_thread_json()
@@ -1547,13 +1542,12 @@ class ThreadMarkBestAnswerApiTests(ThreadPatchApiTestCase):
                 },
                 },
             ]
             ]
         )
         )
-        self.assertEqual(response.status_code, 400)
+        self.assertEqual(response.status_code, 403)
         self.assertEqual(response.json(), {
         self.assertEqual(response.json(), {
-            'id': self.thread.id,
-            'detail': [
+            'detail': (
                 "You can't mark best answer in this thread because it's closed and you don't have "
                 "You can't mark best answer in this thread because it's closed and you don't have "
                 "permission to open it."
                 "permission to open it."
-            ],
+            ),
         })
         })
 
 
         thread_json = self.get_thread_json()
         thread_json = self.get_thread_json()
@@ -1588,7 +1582,6 @@ class ThreadMarkBestAnswerApiTests(ThreadPatchApiTestCase):
         )
         )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.json(), {
         self.assertEqual(response.json(), {
-            'id': self.thread.id,
             'detail': ["A valid integer is required."],
             'detail': ["A valid integer is required."],
         })
         })
 
 
@@ -1608,10 +1601,9 @@ class ThreadMarkBestAnswerApiTests(ThreadPatchApiTestCase):
                 },
                 },
             ]
             ]
         )
         )
-        self.assertEqual(response.status_code, 400)
+        self.assertEqual(response.status_code, 404)
         self.assertEqual(response.json(), {
         self.assertEqual(response.json(), {
-            'id': self.thread.id,
-            'detail': ["NOT FOUND"],
+            'detail': "No Post matches the given query.",
         })
         })
 
 
         thread_json = self.get_thread_json()
         thread_json = self.get_thread_json()
@@ -1632,10 +1624,9 @@ class ThreadMarkBestAnswerApiTests(ThreadPatchApiTestCase):
                 },
                 },
             ]
             ]
         )
         )
-        self.assertEqual(response.status_code, 400)
+        self.assertEqual(response.status_code, 404)
         self.assertEqual(response.json(), {
         self.assertEqual(response.json(), {
-            'id': self.thread.id,
-            'detail': ["NOT FOUND"],
+            'detail': "NOT FOUND",
         })
         })
 
 
         thread_json = self.get_thread_json()
         thread_json = self.get_thread_json()
@@ -1656,10 +1647,9 @@ class ThreadMarkBestAnswerApiTests(ThreadPatchApiTestCase):
                 },
                 },
             ]
             ]
         )
         )
-        self.assertEqual(response.status_code, 400)
+        self.assertEqual(response.status_code, 404)
         self.assertEqual(response.json(), {
         self.assertEqual(response.json(), {
-            'id': self.thread.id,
-            'detail': ["NOT FOUND"],
+            'detail': "No Post matches the given query.",
         })
         })
 
 
         thread_json = self.get_thread_json()
         thread_json = self.get_thread_json()
@@ -1682,10 +1672,9 @@ class ThreadMarkBestAnswerApiTests(ThreadPatchApiTestCase):
                 },
                 },
             ]
             ]
         )
         )
-        self.assertEqual(response.status_code, 400)
+        self.assertEqual(response.status_code, 403)
         self.assertEqual(response.json(), {
         self.assertEqual(response.json(), {
-            'id': self.thread.id,
-            'detail': ["Events can't be marked as best answers."],
+            'detail': "Events can't be marked as best answers.",
         })
         })
 
 
         thread_json = self.get_thread_json()
         thread_json = self.get_thread_json()
@@ -1704,10 +1693,9 @@ class ThreadMarkBestAnswerApiTests(ThreadPatchApiTestCase):
                 },
                 },
             ]
             ]
         )
         )
-        self.assertEqual(response.status_code, 400)
+        self.assertEqual(response.status_code, 403)
         self.assertEqual(response.json(), {
         self.assertEqual(response.json(), {
-            'id': self.thread.id,
-            'detail': ["First post in a thread can't be marked as best answer."],
+            'detail': "First post in a thread can't be marked as best answer.",
         })
         })
 
 
         thread_json = self.get_thread_json()
         thread_json = self.get_thread_json()
@@ -1728,10 +1716,9 @@ class ThreadMarkBestAnswerApiTests(ThreadPatchApiTestCase):
                 },
                 },
             ]
             ]
         )
         )
-        self.assertEqual(response.status_code, 400)
+        self.assertEqual(response.status_code, 403)
         self.assertEqual(response.json(), {
         self.assertEqual(response.json(), {
-            'id': self.thread.id,
-            'detail': ["Hidden posts can't be marked as best answers."],
+            'detail': "Hidden posts can't be marked as best answers.",
         })
         })
 
 
         thread_json = self.get_thread_json()
         thread_json = self.get_thread_json()
@@ -1752,10 +1739,9 @@ class ThreadMarkBestAnswerApiTests(ThreadPatchApiTestCase):
                 },
                 },
             ]
             ]
         )
         )
-        self.assertEqual(response.status_code, 400)
+        self.assertEqual(response.status_code, 403)
         self.assertEqual(response.json(), {
         self.assertEqual(response.json(), {
-            'id': self.thread.id,
-            'detail': ["Unapproved posts can't be marked as best answers."],
+            'detail': "Unapproved posts can't be marked as best answers.",
         })
         })
 
 
         thread_json = self.get_thread_json()
         thread_json = self.get_thread_json()
@@ -1776,13 +1762,12 @@ class ThreadMarkBestAnswerApiTests(ThreadPatchApiTestCase):
                 },
                 },
             ]
             ]
         )
         )
-        self.assertEqual(response.status_code, 400)
+        self.assertEqual(response.status_code, 403)
         self.assertEqual(response.json(), {
         self.assertEqual(response.json(), {
-            'id': self.thread.id,
-            'detail': [
+            'detail': (
                 "You don't have permission to mark this post as best answer because a moderator "
                 "You don't have permission to mark this post as best answer because a moderator "
                 "has protected it."
                 "has protected it."
-            ],
+            ),
         })
         })
 
 
         thread_json = self.get_thread_json()
         thread_json = self.get_thread_json()
@@ -1829,8 +1814,6 @@ class ThreadChangeBestAnswerApiTests(ThreadPatchApiTestCase):
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.json(), {
         self.assertEqual(response.json(), {
             'id': self.thread.id,
             'id': self.thread.id,
-            'detail': ['ok'],
-
             'best_answer': best_answer.id,
             'best_answer': best_answer.id,
             'best_answer_is_protected': False,
             'best_answer_is_protected': False,
             'best_answer_marked_on': response.json()['best_answer_marked_on'],
             'best_answer_marked_on': response.json()['best_answer_marked_on'],
@@ -1861,10 +1844,9 @@ class ThreadChangeBestAnswerApiTests(ThreadPatchApiTestCase):
                 },
                 },
             ]
             ]
         )
         )
-        self.assertEqual(response.status_code, 400)
+        self.assertEqual(response.status_code, 403)
         self.assertEqual(response.json(), {
         self.assertEqual(response.json(), {
-            'id': self.thread.id,
-            'detail': ["This post is already marked as thread's best answer."],
+            'detail': "This post is already marked as thread's best answer.",
         })
         })
 
 
         thread_json = self.get_thread_json()
         thread_json = self.get_thread_json()
@@ -1885,12 +1867,11 @@ class ThreadChangeBestAnswerApiTests(ThreadPatchApiTestCase):
                 },
                 },
             ]
             ]
         )
         )
-        self.assertEqual(response.status_code, 400)
+        self.assertEqual(response.status_code, 403)
         self.assertEqual(response.json(), {
         self.assertEqual(response.json(), {
-            'id': self.thread.id,
-            'detail': [
+            'detail': (
                 'You don\'t have permission to mark best answers in the "First category" category.'
                 'You don\'t have permission to mark best answers in the "First category" category.'
-            ],
+            ),
         })
         })
 
 
         thread_json = self.get_thread_json()
         thread_json = self.get_thread_json()
@@ -1911,13 +1892,12 @@ class ThreadChangeBestAnswerApiTests(ThreadPatchApiTestCase):
                 },
                 },
             ]
             ]
         )
         )
-        self.assertEqual(response.status_code, 400)
+        self.assertEqual(response.status_code, 403)
         self.assertEqual(response.json(), {
         self.assertEqual(response.json(), {
-            'id': self.thread.id,
-            'detail': [
+            'detail': (
                 'You don\'t have permission to change this thread\'s marked answer because it\'s '
                 'You don\'t have permission to change this thread\'s marked answer because it\'s '
                 'in the "First category" category.'
                 'in the "First category" category.'
-            ],
+            ),
         })
         })
 
 
         thread_json = self.get_thread_json()
         thread_json = self.get_thread_json()
@@ -1938,13 +1918,12 @@ class ThreadChangeBestAnswerApiTests(ThreadPatchApiTestCase):
                 },
                 },
             ]
             ]
         )
         )
-        self.assertEqual(response.status_code, 400)
+        self.assertEqual(response.status_code, 403)
         self.assertEqual(response.json(), {
         self.assertEqual(response.json(), {
-            'id': self.thread.id,
-            'detail': [
+            'detail': (
                 "You don't have permission to change this thread's marked answer because you are "
                 "You don't have permission to change this thread's marked answer because you are "
                 "not a thread starter."
                 "not a thread starter."
-            ],
+            ),
         })
         })
 
 
         thread_json = self.get_thread_json()
         thread_json = self.get_thread_json()
@@ -1990,13 +1969,12 @@ class ThreadChangeBestAnswerApiTests(ThreadPatchApiTestCase):
                 },
                 },
             ]
             ]
         )
         )
-        self.assertEqual(response.status_code, 400)
+        self.assertEqual(response.status_code, 403)
         self.assertEqual(response.json(), {
         self.assertEqual(response.json(), {
-            'id': self.thread.id,
-            'detail': [
+            'detail': (
                 "You don't have permission to change best answer that was marked for more than "
                 "You don't have permission to change best answer that was marked for more than "
                 "5 minutes."
                 "5 minutes."
-            ],
+            ),
         })
         })
 
 
         thread_json = self.get_thread_json()
         thread_json = self.get_thread_json()
@@ -2042,13 +2020,12 @@ class ThreadChangeBestAnswerApiTests(ThreadPatchApiTestCase):
                 },
                 },
             ]
             ]
         )
         )
-        self.assertEqual(response.status_code, 400)
+        self.assertEqual(response.status_code, 403)
         self.assertEqual(response.json(), {
         self.assertEqual(response.json(), {
-            'id': self.thread.id,
-            'detail': [
+            'detail': (
                 "You don't have permission to change this thread's best answer because a "
                 "You don't have permission to change this thread's best answer because a "
                 "moderator has protected it."
                 "moderator has protected it."
-            ],
+            ),
         })
         })
 
 
         thread_json = self.get_thread_json()
         thread_json = self.get_thread_json()
@@ -2090,7 +2067,10 @@ class ThreadChangeBestAnswerApiTests(ThreadPatchApiTestCase):
                 },
                 },
             ]
             ]
         )
         )
-        self.assertEqual(response.status_code, 400)
+        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.json(), {
+            'detail': "Hidden posts can't be marked as best answers.",
+        })
         
         
         thread_json = self.get_thread_json()
         thread_json = self.get_thread_json()
         self.assertEqual(thread_json['best_answer'], self.best_answer.id)
         self.assertEqual(thread_json['best_answer'], self.best_answer.id)
@@ -2120,8 +2100,6 @@ class ThreadUnmarkBestAnswerApiTests(ThreadPatchApiTestCase):
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.json(), {
         self.assertEqual(response.json(), {
             'id': self.thread.id,
             'id': self.thread.id,
-            'detail': ['ok'],
-
             'best_answer': None,
             'best_answer': None,
             'best_answer_is_protected': False,
             'best_answer_is_protected': False,
             'best_answer_marked_on': None,
             'best_answer_marked_on': None,
@@ -2153,7 +2131,6 @@ class ThreadUnmarkBestAnswerApiTests(ThreadPatchApiTestCase):
         )
         )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.json(), {
         self.assertEqual(response.json(), {
-            'id': self.thread.id,
             'detail': ["A valid integer is required."],
             'detail': ["A valid integer is required."],
         })
         })
 
 
@@ -2173,10 +2150,9 @@ class ThreadUnmarkBestAnswerApiTests(ThreadPatchApiTestCase):
                 },
                 },
             ]
             ]
         )
         )
-        self.assertEqual(response.status_code, 400)
+        self.assertEqual(response.status_code, 404)
         self.assertEqual(response.json(), {
         self.assertEqual(response.json(), {
-            'id': self.thread.id,
-            'detail': ["NOT FOUND"],
+            'detail': "No Post matches the given query.",
         })
         })
 
 
         thread_json = self.get_thread_json()
         thread_json = self.get_thread_json()
@@ -2197,12 +2173,11 @@ class ThreadUnmarkBestAnswerApiTests(ThreadPatchApiTestCase):
                 },
                 },
             ]
             ]
         )
         )
-        self.assertEqual(response.status_code, 400)
+        self.assertEqual(response.status_code, 403)
         self.assertEqual(response.json(), {
         self.assertEqual(response.json(), {
-            'id': self.thread.id,
-            'detail': [
+            'detail': (
                 "This post can't be unmarked because it's not currently marked as best answer."
                 "This post can't be unmarked because it's not currently marked as best answer."
-            ],
+            ),
         })
         })
 
 
         thread_json = self.get_thread_json()
         thread_json = self.get_thread_json()
@@ -2221,13 +2196,12 @@ class ThreadUnmarkBestAnswerApiTests(ThreadPatchApiTestCase):
                 },
                 },
             ]
             ]
         )
         )
-        self.assertEqual(response.status_code, 400)
+        self.assertEqual(response.status_code, 403)
         self.assertEqual(response.json(), {
         self.assertEqual(response.json(), {
-            'id': self.thread.id,
-            'detail': [
+            'detail': (
                 'You don\'t have permission to unmark threads answers in the "First category" '
                 'You don\'t have permission to unmark threads answers in the "First category" '
                 'category.'
                 'category.'
-            ],
+            ),
         })
         })
 
 
         thread_json = self.get_thread_json()
         thread_json = self.get_thread_json()
@@ -2246,13 +2220,12 @@ class ThreadUnmarkBestAnswerApiTests(ThreadPatchApiTestCase):
                 },
                 },
             ]
             ]
         )
         )
-        self.assertEqual(response.status_code, 400)
+        self.assertEqual(response.status_code, 403)
         self.assertEqual(response.json(), {
         self.assertEqual(response.json(), {
-            'id': self.thread.id,
-            'detail': [
+            'detail': (
                 "You don't have permission to unmark this best answer because you are not a "
                 "You don't have permission to unmark this best answer because you are not a "
                 "thread starter."
                 "thread starter."
-            ],
+            ),
         })
         })
 
 
         thread_json = self.get_thread_json()
         thread_json = self.get_thread_json()
@@ -2296,13 +2269,12 @@ class ThreadUnmarkBestAnswerApiTests(ThreadPatchApiTestCase):
                 },
                 },
             ]
             ]
         )
         )
-        self.assertEqual(response.status_code, 400)
+        self.assertEqual(response.status_code, 403)
         self.assertEqual(response.json(), {
         self.assertEqual(response.json(), {
-            'id': self.thread.id,
-            'detail': [
+            'detail': (
                 "You don't have permission to unmark best answer that was marked for more than "
                 "You don't have permission to unmark best answer that was marked for more than "
                 "5 minutes."
                 "5 minutes."
-            ],
+            ),
         })
         })
 
 
         thread_json = self.get_thread_json()
         thread_json = self.get_thread_json()
@@ -2346,13 +2318,12 @@ class ThreadUnmarkBestAnswerApiTests(ThreadPatchApiTestCase):
                 },
                 },
             ]
             ]
         )
         )
-        self.assertEqual(response.status_code, 400)
+        self.assertEqual(response.status_code, 403)
         self.assertEqual(response.json(), {
         self.assertEqual(response.json(), {
-            'id': self.thread.id,
-            'detail': [
+            'detail': (
                 'You don\'t have permission to unmark this best answer because its category '
                 'You don\'t have permission to unmark this best answer because its category '
                 '"First category" is closed.'
                 '"First category" is closed.'
-            ],
+            ),
         })
         })
 
 
         thread_json = self.get_thread_json()
         thread_json = self.get_thread_json()
@@ -2396,13 +2367,12 @@ class ThreadUnmarkBestAnswerApiTests(ThreadPatchApiTestCase):
                 },
                 },
             ]
             ]
         )
         )
-        self.assertEqual(response.status_code, 400)
+        self.assertEqual(response.status_code, 403)
         self.assertEqual(response.json(), {
         self.assertEqual(response.json(), {
-            'id': self.thread.id,
-            'detail': [
+            'detail': (
                 "You can't unmark this thread's best answer because it's closed and you don't "
                 "You can't unmark this thread's best answer because it's closed and you don't "
                 "have permission to open it."
                 "have permission to open it."
-            ],
+            ),
         })
         })
 
 
         thread_json = self.get_thread_json()
         thread_json = self.get_thread_json()
@@ -2446,13 +2416,12 @@ class ThreadUnmarkBestAnswerApiTests(ThreadPatchApiTestCase):
                 },
                 },
             ]
             ]
         )
         )
-        self.assertEqual(response.status_code, 400)
+        self.assertEqual(response.status_code, 403)
         self.assertEqual(response.json(), {
         self.assertEqual(response.json(), {
-            'id': self.thread.id,
-            'detail': [
+            'detail': (
                 "You don't have permission to unmark this thread's best answer because a "
                 "You don't have permission to unmark this thread's best answer because a "
                 "moderator has protected it."
                 "moderator has protected it."
-            ],
+            ),
         })
         })
 
 
         thread_json = self.get_thread_json()
         thread_json = self.get_thread_json()

+ 1 - 1
misago/threads/tests/test_thread_postmerge_api.py

@@ -381,7 +381,7 @@ class ThreadPostMergeApiTestCase(AuthenticatedUserTestCase):
         )
         )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.json(), {
         self.assertEqual(response.json(), {
-            'detail': "Post marked as best answer can't be merged with thread's first post."
+            'posts': ["Post marked as best answer can't be merged with thread's first post."]
         })
         })
 
 
     def test_merge_posts(self):
     def test_merge_posts(self):

+ 2 - 3
misago/threads/tests/test_thread_postpatch_api.py

@@ -687,10 +687,9 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
                 },
                 },
             ]
             ]
         )
         )
-        self.assertEqual(response.status_code, 400)
+        self.assertEqual(response.status_code, 403)
         self.assertEqual(response.json(), {
         self.assertEqual(response.json(), {
-            'id': self.post.id,
-            'detail': ["You can't hide this post because its marked as best answer."],
+            'detail': "You can't hide this post because its marked as best answer.",
         })
         })