Browse Source

Prevent best answer from being hidden or deleted

Rafał Pitoń 7 years ago
parent
commit
e8d4db6300

+ 3 - 1
misago/threads/api/postendpoints/delete.py

@@ -7,7 +7,8 @@ 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.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_best_answer, allow_delete_event, allow_delete_post)
 from misago.threads.permissions import exclude_invisible_posts
 from misago.threads.permissions import exclude_invisible_posts
 from misago.threads.serializers import DeletePostsSerializer
 from misago.threads.serializers import DeletePostsSerializer
 
 
@@ -19,6 +20,7 @@ def delete_post(request, thread, post):
     if post.is_event:
     if post.is_event:
         allow_delete_event(request.user, post)
         allow_delete_event(request.user, post)
     else:
     else:
+        allow_delete_best_answer(request.user, post)
         allow_delete_post(request.user, post)
         allow_delete_post(request.user, post)
 
 
     moderation.delete_post(request.user, post)
     moderation.delete_post(request.user, post)

+ 3 - 1
misago/threads/api/postendpoints/patch_post.py

@@ -10,7 +10,8 @@ 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_best_answer, allow_hide_post, allow_protect_post,
+    allow_unhide_post)
 from misago.threads.permissions import exclude_invisible_posts
 from misago.threads.permissions import exclude_invisible_posts
 
 
 
 
@@ -117,6 +118,7 @@ post_patch_dispatcher.replace('is-unapproved', patch_is_unapproved)
 def patch_is_hidden(request, post, value):
 def patch_is_hidden(request, post, value):
     if value is True:
     if value is True:
         allow_hide_post(request.user, post)
         allow_hide_post(request.user, post)
+        allow_hide_best_answer(request.user, post)
         moderation.hide_post(request.user, post)
         moderation.hide_post(request.user, post)
     elif value is False:
     elif value is False:
         allow_unhide_post(request.user, post)
         allow_unhide_post(request.user, post)

+ 26 - 0
misago/threads/permissions/bestanswers.py

@@ -16,6 +16,10 @@ __all__nope = [
     'can_mark_as_best_answer',
     'can_mark_as_best_answer',
     'allow_unmark_best_answer',
     'allow_unmark_best_answer',
     'can_unmark_best_answer',
     'can_unmark_best_answer',
+    'allow_hide_best_answer',
+    'can_hide_best_answer',
+    'allow_delete_best_answer',
+    'can_delete_best_answer',
 ]
 ]
 
 
 
 
@@ -106,6 +110,8 @@ def add_acl_to_post(user, post):
     post.acl.update({
     post.acl.update({
         'can_mark_as_best_answer': can_mark_as_best_answer(user, post),
         'can_mark_as_best_answer': can_mark_as_best_answer(user, post),
         'can_unmark_best_answer': can_unmark_best_answer(user, post),
         'can_unmark_best_answer': can_unmark_best_answer(user, post),
+        'can_hide_best_answer': can_hide_best_answer(user, post),
+        'can_delete_best_answer': can_delete_best_answer(user, post),
     })
     })
 
 
 
 
@@ -304,6 +310,26 @@ def allow_unmark_best_answer(user, target):
 can_unmark_best_answer = return_boolean(allow_unmark_best_answer)
 can_unmark_best_answer = return_boolean(allow_unmark_best_answer)
 
 
 
 
+def allow_hide_best_answer(user, target):
+    if target.is_best_answer:
+        raise PermissionDenied(
+            _("You can't hide this post because its marked as best answer.")
+        )
+
+
+can_hide_best_answer = return_boolean(allow_hide_best_answer)
+
+
+def allow_delete_best_answer(user, target):
+    if target.is_best_answer:
+        raise PermissionDenied(
+            _("You can't delete this post because its marked as best answer.")
+        )
+
+
+can_delete_best_answer = return_boolean(allow_delete_best_answer)
+
+
 def has_time_to_change_answer(user, target):
 def has_time_to_change_answer(user, target):
     category_acl = user.acl_cache['categories'].get(target.category_id, {})
     category_acl = user.acl_cache['categories'].get(target.category_id, {})
     change_time = category_acl.get('best_answer_change_time', 0)
     change_time = category_acl.get('best_answer_change_time', 0)

+ 2 - 1
misago/threads/serializers/moderation.py

@@ -10,7 +10,7 @@ from misago.categories import THREADS_ROOT_NAME
 from misago.conf import settings
 from misago.conf import settings
 from misago.threads.models import Thread
 from misago.threads.models import Thread
 from misago.threads.permissions import (
 from misago.threads.permissions import (
-    allow_delete_event, allow_delete_post, allow_delete_thread,
+    allow_delete_best_answer, allow_delete_event, allow_delete_post, allow_delete_thread,
     allow_merge_post, allow_merge_thread,
     allow_merge_post, allow_merge_thread,
     allow_move_post, allow_split_post,
     allow_move_post, allow_split_post,
     can_reply_thread, can_see_thread,
     can_reply_thread, can_see_thread,
@@ -77,6 +77,7 @@ class DeletePostsSerializer(serializers.Serializer):
             if post.is_event:
             if post.is_event:
                 allow_delete_event(user, post)
                 allow_delete_event(user, post)
             else:
             else:
+                allow_delete_best_answer(user, post)
                 allow_delete_post(user, post)
                 allow_delete_post(user, post)
 
 
             posts.append(post)
             posts.append(post)

+ 14 - 1
misago/threads/tests/test_thread_postbulkdelete_api.py

@@ -102,7 +102,7 @@ class PostBulkDeleteApiTests(ThreadsApiTestCase):
         self.assertContains(response, "One or more posts to delete could not be found.", status_code=403)
         self.assertContains(response, "One or more posts to delete could not be found.", status_code=403)
 
 
     def test_validate_posts_same_thread(self):
     def test_validate_posts_same_thread(self):
-        """api validates that ids are visible posts"""
+        """api validates that ids are same thread posts"""
         self.override_acl({
         self.override_acl({
             'can_hide_own_posts': 2,
             'can_hide_own_posts': 2,
             'can_hide_posts': 2,
             'can_hide_posts': 2,
@@ -212,6 +212,19 @@ class PostBulkDeleteApiTests(ThreadsApiTestCase):
         response = self.delete(self.api_link, ids)
         response = self.delete(self.api_link, ids)
         self.assertContains(response, "You can't delete thread's first post.", status_code=403)
         self.assertContains(response, "You can't delete thread's first post.", status_code=403)
 
 
+    def test_delete_best_answer(self):
+        """api disallows best answer deletion"""
+        self.override_acl({'can_hide_own_posts': 2, 'can_hide_posts': 2})
+
+        self.thread.set_best_answer(self.user, self.posts[0])
+        self.thread.save()
+
+        response = self.delete(self.api_link, [p.id for p in self.posts])
+        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.json(), {
+            'detail': "You can't delete this post because its marked as best answer.",
+        })
+
     def test_delete_event(self):
     def test_delete_event(self):
         """api differs posts from events"""
         """api differs posts from events"""
         self.override_acl({
         self.override_acl({

+ 14 - 1
misago/threads/tests/test_thread_postdelete_api.py

@@ -116,7 +116,7 @@ class PostDeleteApiTests(ThreadsApiTestCase):
         )
         )
 
 
     def test_delete_first_post(self):
     def test_delete_first_post(self):
-        """api disallows first post's deletion"""
+        """api disallows first post deletion"""
         self.override_acl({'can_hide_own_posts': 2, 'can_hide_posts': 2})
         self.override_acl({'can_hide_own_posts': 2, 'can_hide_posts': 2})
 
 
         api_link = reverse(
         api_link = reverse(
@@ -130,6 +130,19 @@ class PostDeleteApiTests(ThreadsApiTestCase):
         response = self.client.delete(api_link)
         response = self.client.delete(api_link)
         self.assertContains(response, "You can't delete thread's first post.", status_code=403)
         self.assertContains(response, "You can't delete thread's first post.", status_code=403)
 
 
+    def test_delete_best_answer(self):
+        """api disallows best answer deletion"""
+        self.override_acl({'can_hide_own_posts': 2, 'can_hide_posts': 2})
+
+        self.thread.set_best_answer(self.user, self.post)
+        self.thread.save()
+
+        response = self.client.delete(self.api_link)
+        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.json(), {
+            'detail': "You can't delete this post because its marked as best answer.",
+        })
+
     def test_delete_owned_post(self):
     def test_delete_owned_post(self):
         """api deletes owned thread post"""
         """api deletes owned thread post"""
         self.override_acl({
         self.override_acl({

+ 22 - 0
misago/threads/tests/test_thread_postpatch_api.py

@@ -687,6 +687,28 @@ class PostHideApiTests(ThreadPostPatchApiTestCase):
         response_json = response.json()
         response_json = response.json()
         self.assertEqual(response_json['detail'][0], "You can't hide thread's first post.")
         self.assertEqual(response_json['detail'][0], "You can't hide thread's first post.")
 
 
+    def test_hide_best_answer(self):
+        """api hide first post fails"""
+        self.thread.set_best_answer(self.user, self.post)
+        self.thread.save()
+
+        self.override_acl({'can_hide_posts': 2})
+
+        response = self.patch(
+            self.api_link, [
+                {
+                    'op': 'replace',
+                    'path': 'is-hidden',
+                    'value': True,
+                },
+            ]
+        )
+        self.assertEqual(response.status_code, 400)
+        self.assertEqual(response.json(), {
+            'id': self.post.id,
+            'detail': ["You can't hide this post because its marked as best answer."],
+        })
+
 
 
 class PostUnhideApiTests(ThreadPostPatchApiTestCase):
 class PostUnhideApiTests(ThreadPostPatchApiTestCase):
     def test_show_post(self):
     def test_show_post(self):