rafalp 6 лет назад
Родитель
Сommit
19d77eaeea

+ 6 - 1
misago/threads/api/postingendpoint/__init__.py

@@ -26,7 +26,12 @@ class PostingEndpoint(object):
 
         # build kwargs dict for passing to middlewares
         self.kwargs = kwargs
-        self.kwargs.update({'mode': mode, 'request': request, 'user': request.user})
+        self.kwargs.update({
+            'mode': mode,
+            'request': request,
+            'user': request.user,
+            'user_acl': request.user_acl,
+        })
 
         self.__dict__.update(kwargs)
 

+ 9 - 9
misago/threads/api/postingendpoint/category.py

@@ -23,12 +23,12 @@ class CategoryMiddleware(PostingMiddleware):
         return False
 
     def get_serializer(self):
-        return CategorySerializer(self.user, data=self.request.data)
+        return CategorySerializer(self.user_acl, data=self.request.data)
 
     def pre_save(self, serializer):
         category = serializer.category_cache
 
-        add_acl(self.user, category)
+        add_acl(self.user_acl, category)
 
         # set flags for savechanges middleware
         category.update_all = False
@@ -47,8 +47,8 @@ class CategorySerializer(serializers.Serializer):
         }
     )
 
-    def __init__(self, user, *args, **kwargs):
-        self.user = user
+    def __init__(self, user_acl, *args, **kwargs):
+        self.user_acl = user_acl
         self.category_cache = None
 
         super().__init__(*args, **kwargs)
@@ -59,15 +59,15 @@ class CategorySerializer(serializers.Serializer):
                 pk=value, tree_id=trees_map.get_tree_id_for_root(THREADS_ROOT_NAME)
             )
 
-            can_see = can_see_category(self.user, self.category_cache)
-            can_browse = can_browse_category(self.user, self.category_cache)
+            can_see = can_see_category(self.user_acl, self.category_cache)
+            can_browse = can_browse_category(self.user_acl, self.category_cache)
             if not (self.category_cache.level and can_see and can_browse):
                 raise PermissionDenied(_("Selected category is invalid."))
 
-            allow_start_thread(self.user, self.category_cache)
-        except PermissionDenied as e:
-            raise serializers.ValidationError(e.args[0])
+            allow_start_thread(self.user_acl, self.category_cache)
         except Category.DoesNotExist:
             raise serializers.ValidationError(
                 _("Selected category doesn't exist or you don't have permission to browse it.")
             )
+        except PermissionDenied as e:
+            raise serializers.ValidationError(e.args[0])

+ 3 - 5
misago/threads/api/threadendpoints/merge.py

@@ -15,7 +15,7 @@ from misago.threads.serializers import (
 
 
 def thread_merge_endpoint(request, thread, viewmodel):
-    allow_merge_thread(request.user, thread)
+    allow_merge_thread(request.user_acl, thread)
 
     serializer = MergeThreadSerializer(
         data=request.data,
@@ -88,9 +88,7 @@ def thread_merge_endpoint(request, thread, viewmodel):
 def threads_merge_endpoint(request):
     serializer = MergeThreadsSerializer(
         data=request.data,
-        context={
-            'user': request.user
-        },
+        context={'user_acl': request.user_acl},
     )
 
     if not serializer.is_valid():
@@ -108,7 +106,7 @@ def threads_merge_endpoint(request):
 
     for thread in threads:
         try:
-            allow_merge_thread(request.user, thread)
+            allow_merge_thread(request.user_acl, thread)
         except PermissionDenied as e:
             invalid_threads.append({
                 'id': thread.pk,

+ 24 - 23
misago/threads/serializers/moderation.py

@@ -62,10 +62,10 @@ class DeletePostsSerializer(serializers.Serializer):
             )
             raise ValidationError(message % {'limit': POSTS_LIMIT})
 
-        user = self.context['user']
+        user_acl = self.context['user_acl']
         thread = self.context['thread']
 
-        posts_queryset = exclude_invisible_posts(user, thread.category, thread.post_set)
+        posts_queryset = exclude_invisible_posts(user_acl, thread.category, thread.post_set)
         posts_queryset = posts_queryset.filter(id__in=data).order_by('id')
 
         posts = []
@@ -74,10 +74,10 @@ class DeletePostsSerializer(serializers.Serializer):
             post.thread = thread
 
             if post.is_event:
-                allow_delete_event(user, post)
+                allow_delete_event(user_acl, post)
             else:
-                allow_delete_best_answer(user, post)
-                allow_delete_post(user, post)
+                allow_delete_best_answer(user_acl, post)
+                allow_delete_post(user_acl, post)
 
             posts.append(post)
 
@@ -115,10 +115,10 @@ class MergePostsSerializer(serializers.Serializer):
             )
             raise serializers.ValidationError(message % {'limit': POSTS_LIMIT})
 
-        user = self.context['user']
+        user_acl = self.context['user_acl']
         thread = self.context['thread']
 
-        posts_queryset = exclude_invisible_posts(user, thread.category, thread.post_set)
+        posts_queryset = exclude_invisible_posts(user_acl, thread.category, thread.post_set)
         posts_queryset = posts_queryset.filter(id__in=data).order_by('id')
 
         posts = []
@@ -127,7 +127,7 @@ class MergePostsSerializer(serializers.Serializer):
             post.thread = thread
 
             try:
-                allow_merge_post(user, post)
+                allow_merge_post(user_acl, post)
             except PermissionDenied as e:
                 raise serializers.ValidationError(e)
 
@@ -232,7 +232,7 @@ class MovePostsSerializer(serializers.Serializer):
             post.thread = thread
 
             try:
-                allow_move_post(request.user, post)
+                allow_move_post(request.user_acl, post)
                 posts.append(post)
             except PermissionDenied as e:
                 raise serializers.ValidationError(e)
@@ -259,14 +259,15 @@ class NewThreadSerializer(serializers.Serializer):
         return validate_title(title)
 
     def validate_category(self, category_id):
-        self.category = validate_category(self.context['user'], category_id)
-        if not can_start_thread(self.context['user'], self.category):
+        user_acl = self.context['user_acl']
+        self.category = validate_category(user_acl, category_id)
+        if not can_start_thread(user_acl, 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['user'], self.category)
+            add_acl(self.context['user_acl'], self.category)
         except AttributeError:
             return weight  # don't validate weight further if category failed
 
@@ -283,7 +284,7 @@ class NewThreadSerializer(serializers.Serializer):
 
     def validate_is_hidden(self, is_hidden):
         try:
-            add_acl(self.context['user'], self.category)
+            add_acl(self.context['user_acl'], self.category)
         except AttributeError:
             return is_hidden  # don't validate hidden further if category failed
 
@@ -293,7 +294,7 @@ class NewThreadSerializer(serializers.Serializer):
 
     def validate_is_closed(self, is_closed):
         try:
-            add_acl(self.context['user'], self.category)
+            add_acl(self.context['user_acl'], self.category)
         except AttributeError:
             return is_closed  # don't validate closed further if category failed
 
@@ -331,9 +332,9 @@ class SplitPostsSerializer(NewThreadSerializer):
             raise ValidationError(message % {'limit': POSTS_LIMIT})
 
         thread = self.context['thread']
-        user = self.context['user']
+        user_acl = self.context['user_acl']
 
-        posts_queryset = exclude_invisible_posts(user, thread.category, thread.post_set)
+        posts_queryset = exclude_invisible_posts(user_acl, thread.category, thread.post_set)
         posts_queryset = posts_queryset.filter(id__in=data).order_by('id')
 
         posts = []
@@ -342,7 +343,7 @@ class SplitPostsSerializer(NewThreadSerializer):
             post.thread = thread
 
             try:
-                allow_split_post(user, post)
+                allow_split_post(user_acl, post)
             except PermissionDenied as e:
                 raise ValidationError(e)
 
@@ -389,7 +390,7 @@ class DeleteThreadsSerializer(serializers.Serializer):
         for thread_id in data:
             try:
                 thread = viewmodel(request, thread_id).unwrap()
-                allow_delete_thread(request.user, thread)
+                allow_delete_thread(request.user_acl, thread)
                 threads.append(thread)
             except PermissionDenied as e:
                 errors.append({
@@ -443,7 +444,7 @@ class MergeThreadSerializer(serializers.Serializer):
 
         try:
             other_thread = viewmodel(request, other_thread_id).unwrap()
-            allow_merge_thread(request.user, other_thread, otherthread=True)
+            allow_merge_thread(request.user_acl, other_thread, otherthread=True)
         except PermissionDenied as e:
             raise serializers.ValidationError(e)
         except Http404:
@@ -454,7 +455,7 @@ class MergeThreadSerializer(serializers.Serializer):
                 )
             )
 
-        if not can_reply_thread(request.user, other_thread):
+        if not can_reply_thread(request.user_acl, other_thread):
             raise ValidationError(_("You can't merge this thread into thread you can't reply."))
 
         return other_thread
@@ -518,12 +519,12 @@ class MergeThreadsSerializer(NewThreadSerializer):
             category__tree_id=threads_tree_id,
         ).select_related('category').order_by('-id')
 
-        user = self.context['user']
+        user_acl = self.context['user_acl']
 
         threads = []
         for thread in threads_queryset:
-            add_acl(user, thread)
-            if can_see_thread(user, thread):
+            add_acl(user_acl, thread)
+            if can_see_thread(user_acl, thread):
                 threads.append(thread)
 
         if len(threads) != len(data):

+ 2 - 2
misago/threads/test.py

@@ -16,7 +16,7 @@ default_category_acl = {
 }
 
 
-def patch_category_acl(acl_patch):
+def patch_category_acl(acl_patch=None):
     def patch_acl(_, user_acl):
         category = Category.objects.get(slug="first-category")
         category_acl = user_acl['categories'][category.id]
@@ -28,7 +28,7 @@ def patch_category_acl(acl_patch):
     return patch_user_acl(patch_acl)
 
 
-def patch_other_category_acl(acl_patch):
+def patch_other_category_acl(acl_patch=None):
     def patch_acl(_, user_acl):
         src_category = Category.objects.get(slug="first-category")
         category_acl = user_acl['categories'][src_category.id].copy()

+ 98 - 186
misago/threads/tests/test_thread_merge_api.py

@@ -1,10 +1,10 @@
 from django.urls import reverse
 
-from misago.acl.testutils import override_acl
 from misago.categories.models import Category
 from misago.readtracker import poststracker
 from misago.threads import testutils
 from misago.threads.models import Poll, PollVote, Thread
+from misago.threads.test import patch_category_acl, patch_other_category_acl
 
 from .test_threads_api import ThreadsApiTestCase
 
@@ -12,16 +12,16 @@ from .test_threads_api import ThreadsApiTestCase
 class ThreadMergeApiTests(ThreadsApiTestCase):
     def setUp(self):
         super().setUp()
-
+        
         Category(
-            name='Category B',
-            slug='category-b',
+            name='Other Category',
+            slug='other-category',
         ).insert_at(
             self.category,
             position='last-child',
             save=True,
         )
-        self.category_b = Category.objects.get(slug='category-b')
+        self.other_category = Category.objects.get(slug='other-category')
 
         self.api_link = reverse(
             'misago:api:thread-merge', kwargs={
@@ -29,63 +29,27 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
             }
         )
 
-    def override_other_acl(self, acl=None):
-        other_category_acl = self.user.acl_cache['categories'][self.category.pk].copy()
-        other_category_acl.update({
-            'can_see': 1,
-            'can_browse': 1,
-            'can_see_all_threads': 1,
-            'can_see_own_threads': 0,
-            'can_hide_threads': 0,
-            'can_approve_content': 0,
-            'can_edit_posts': 0,
-            'can_hide_posts': 0,
-            'can_hide_own_posts': 0,
-            'can_merge_threads': 0,
-            'can_close_threads': 0,
-        })
-
-        if acl:
-            other_category_acl.update(acl)
-
-        categories_acl = self.user.acl_cache['categories']
-        categories_acl[self.category_b.pk] = other_category_acl
-
-        visible_categories = [self.category.pk]
-        if other_category_acl['can_see']:
-            visible_categories.append(self.category_b.pk)
-
-        override_acl(
-            self.user, {
-                'visible_categories': visible_categories,
-                'categories': categories_acl,
-            }
-        )
-
+    @patch_category_acl({"can_merge_threads": False})
     def test_merge_no_permission(self):
         """api validates if thread can be merged with other one"""
-        self.override_acl({'can_merge_threads': 0})
-
         response = self.client.post(self.api_link)
         self.assertEqual(response.status_code, 403)
         self.assertEqual(response.json(), {
             "detail": "You can't merge threads in this category."
         })
 
+    @patch_category_acl({"can_merge_threads": True})
     def test_merge_no_url(self):
         """api validates if thread url was given"""
-        self.override_acl({'can_merge_threads': 1})
-
         response = self.client.post(self.api_link)
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.json(), {
             "detail": "Enter link to new thread."
         })
 
+    @patch_category_acl({"can_merge_threads": True})
     def test_invalid_url(self):
         """api validates thread url"""
-        self.override_acl({'can_merge_threads': 1})
-
         response = self.client.post(self.api_link, {
             'other_thread': self.user.get_absolute_url(),
         })
@@ -94,10 +58,9 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
             "detail": "This is not a valid thread link."
         })
 
+    @patch_category_acl({"can_merge_threads": True})
     def test_current_other_thread(self):
         """api validates if thread url given is to current thread"""
-        self.override_acl({'can_merge_threads': 1})
-
         response = self.client.post(
             self.api_link, {
                 'other_thread': self.thread.get_absolute_url(),
@@ -108,12 +71,11 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
             "detail": "You can't merge thread with itself."
         })
 
+    @patch_other_category_acl()
+    @patch_category_acl({"can_merge_threads": True})
     def test_other_thread_exists(self):
         """api validates if other thread exists"""
-        self.override_acl({'can_merge_threads': 1})
-        self.override_other_acl()
-
-        other_thread = testutils.post_thread(self.category_b)
+        other_thread = testutils.post_thread(self.other_category)
         other_other_thread = other_thread.get_absolute_url()
         other_thread.delete()
 
@@ -128,12 +90,11 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
             )
         })
 
+    @patch_other_category_acl({"can_see": False})
+    @patch_category_acl({"can_merge_threads": True})
     def test_other_thread_is_invisible(self):
         """api validates if other thread is visible"""
-        self.override_acl({'can_merge_threads': 1})
-        self.override_other_acl({'can_see': 0})
-
-        other_thread = testutils.post_thread(self.category_b)
+        other_thread = testutils.post_thread(self.other_category)
 
         response = self.client.post(
             self.api_link, {
@@ -148,12 +109,11 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
             )
         })
 
+    @patch_other_category_acl({"can_merge_threads": False})
+    @patch_category_acl({"can_merge_threads": True})
     def test_other_thread_isnt_mergeable(self):
         """api validates if other thread can be merged"""
-        self.override_acl({'can_merge_threads': 1})
-        self.override_other_acl({'can_merge_threads': 0})
-
-        other_thread = testutils.post_thread(self.category_b)
+        other_thread = testutils.post_thread(self.other_category)
 
         response = self.client.post(
             self.api_link, {
@@ -165,17 +125,11 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
             "detail": "Other thread can't be merged with."
         })
 
+    @patch_other_category_acl({"can_merge_threads": True, "can_close_threads": False})
+    @patch_category_acl({"can_merge_threads": True})
     def test_thread_category_is_closed(self):
         """api validates if thread's category is open"""
-        self.override_acl({'can_merge_threads': 1})
-
-        self.override_other_acl({
-            'can_merge_threads': 1,
-            'can_reply_threads': 0,
-            'can_close_threads': 0,
-        })
-
-        other_thread = testutils.post_thread(self.category_b)
+        other_thread = testutils.post_thread(self.other_category)
 
         self.category.is_closed = True
         self.category.save()
@@ -190,17 +144,11 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
             "detail": "This category is closed. You can't merge it's threads."
         })
 
+    @patch_other_category_acl({"can_merge_threads": True, "can_close_threads": False})
+    @patch_category_acl({"can_merge_threads": True})
     def test_thread_is_closed(self):
         """api validates if thread is open"""
-        self.override_acl({'can_merge_threads': 1})
-
-        self.override_other_acl({
-            'can_merge_threads': 1,
-            'can_reply_threads': 0,
-            'can_close_threads': 0,
-        })
-
-        other_thread = testutils.post_thread(self.category_b)
+        other_thread = testutils.post_thread(self.other_category)
 
         self.thread.is_closed = True
         self.thread.save()
@@ -215,20 +163,14 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
             "detail": "This thread is closed. You can't merge it with other threads."
         })
 
+    @patch_other_category_acl({"can_merge_threads": True, "can_close_threads": False})
+    @patch_category_acl({"can_merge_threads": True})
     def test_other_thread_category_is_closed(self):
         """api validates if other thread's category is open"""
-        self.override_acl({'can_merge_threads': 1})
-
-        self.override_other_acl({
-            'can_merge_threads': 1,
-            'can_reply_threads': 0,
-            'can_close_threads': 0,
-        })
-
-        other_thread = testutils.post_thread(self.category_b)
+        other_thread = testutils.post_thread(self.other_category)
 
-        self.category_b.is_closed = True
-        self.category_b.save()
+        self.other_category.is_closed = True
+        self.other_category.save()
 
         response = self.client.post(
             self.api_link, {
@@ -240,17 +182,11 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
             "detail": "Other thread's category is closed. You can't merge with it."
         })
 
+    @patch_other_category_acl({"can_merge_threads": True, "can_close_threads": False})
+    @patch_category_acl({"can_merge_threads": True})
     def test_other_thread_is_closed(self):
         """api validates if other thread is open"""
-        self.override_acl({'can_merge_threads': 1})
-
-        self.override_other_acl({
-            'can_merge_threads': 1,
-            'can_reply_threads': 0,
-            'can_close_threads': 0,
-        })
-
-        other_thread = testutils.post_thread(self.category_b)
+        other_thread = testutils.post_thread(self.other_category)
 
         other_thread.is_closed = True
         other_thread.save()
@@ -265,16 +201,11 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
             "detail": "Other thread is closed and can't be merged with."
         })
 
+    @patch_other_category_acl({"can_merge_threads": True, "can_reply_threads": False})
+    @patch_category_acl({"can_merge_threads": True})
     def test_other_thread_isnt_replyable(self):
         """api validates if other thread can be replied, which is condition for merge"""
-        self.override_acl({'can_merge_threads': 1})
-
-        self.override_other_acl({
-            'can_merge_threads': 1,
-            'can_reply_threads': 0,
-        })
-
-        other_thread = testutils.post_thread(self.category_b)
+        other_thread = testutils.post_thread(self.other_category)
 
         response = self.client.post(
             self.api_link, {
@@ -286,12 +217,11 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
             "detail": "You can't merge this thread into thread you can't reply."
         })
 
+    @patch_other_category_acl({"can_merge_threads": True})
+    @patch_category_acl({"can_merge_threads": True})
     def test_merge_threads(self):
         """api merges two threads successfully"""
-        self.override_acl({'can_merge_threads': 1})
-        self.override_other_acl({'can_merge_threads': 1})
-
-        other_thread = testutils.post_thread(self.category_b)
+        other_thread = testutils.post_thread(self.other_category)
 
         response = self.client.post(
             self.api_link, {
@@ -312,12 +242,11 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
         with self.assertRaises(Thread.DoesNotExist):
             Thread.objects.get(pk=self.thread.pk)
 
+    @patch_other_category_acl({"can_merge_threads": True})
+    @patch_category_acl({"can_merge_threads": True})
     def test_merge_threads_kept_reads(self):
         """api keeps both threads readtrackers after merge"""
-        self.override_acl({'can_merge_threads': 1})
-        self.override_other_acl({'can_merge_threads': 1})
-
-        other_thread = testutils.post_thread(self.category_b)
+        other_thread = testutils.post_thread(self.other_category)
 
         poststracker.save_read(self.user, self.thread.first_post)
         poststracker.save_read(self.user, other_thread.first_post)
@@ -342,14 +271,13 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
             [self.thread.first_post_id, other_thread.first_post_id]
         )
         self.assertEqual(postreads.filter(thread=other_thread).count(), 2)
-        self.assertEqual(postreads.filter(category=self.category_b).count(), 2)
+        self.assertEqual(postreads.filter(category=self.other_category).count(), 2)
 
+    @patch_other_category_acl({"can_merge_threads": True})
+    @patch_category_acl({"can_merge_threads": True})
     def test_merge_threads_kept_subs(self):
         """api keeps other thread's subscription after merge"""
-        self.override_acl({'can_merge_threads': 1})
-        self.override_other_acl({'can_merge_threads': 1})
-
-        other_thread = testutils.post_thread(self.category_b)
+        other_thread = testutils.post_thread(self.other_category)
 
         self.user.subscription_set.create(
             thread=self.thread,
@@ -377,14 +305,13 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
         # subscriptions are kept
         self.assertEqual(self.user.subscription_set.count(), 1)
         self.user.subscription_set.get(thread=other_thread)
-        self.user.subscription_set.get(category=self.category_b)
+        self.user.subscription_set.get(category=self.other_category)
 
+    @patch_other_category_acl({"can_merge_threads": True})
+    @patch_category_acl({"can_merge_threads": True})
     def test_merge_threads_moved_subs(self):
         """api keeps other thread's subscription after merge"""
-        self.override_acl({'can_merge_threads': 1})
-        self.override_other_acl({'can_merge_threads': 1})
-
-        other_thread = testutils.post_thread(self.category_b)
+        other_thread = testutils.post_thread(self.other_category)
 
         self.user.subscription_set.create(
             thread=other_thread,
@@ -395,7 +322,7 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
 
         self.assertEqual(self.user.subscription_set.count(), 1)
         self.user.subscription_set.get(thread=other_thread)
-        self.user.subscription_set.get(category=self.category_b)
+        self.user.subscription_set.get(category=self.other_category)
 
         response = self.client.post(
             self.api_link, {
@@ -412,13 +339,12 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
         # subscriptions are kept
         self.assertEqual(self.user.subscription_set.count(), 1)
         self.user.subscription_set.get(thread=other_thread)
-        self.user.subscription_set.get(category=self.category_b)
+        self.user.subscription_set.get(category=self.other_category)
 
+    @patch_other_category_acl({"can_merge_threads": True})
+    @patch_category_acl({"can_merge_threads": True})
     def test_merge_threads_handle_subs_colision(self):
         """api resolves conflicting thread subscriptions after merge"""
-        self.override_acl({'can_merge_threads': 1})
-        self.override_other_acl({'can_merge_threads': 1})
-
         self.user.subscription_set.create(
             thread=self.thread,
             category=self.thread.category,
@@ -426,7 +352,7 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
             send_email=False,
         )
 
-        other_thread = testutils.post_thread(self.category_b)
+        other_thread = testutils.post_thread(self.other_category)
 
         self.user.subscription_set.create(
             thread=other_thread,
@@ -439,7 +365,7 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
         self.user.subscription_set.get(thread=self.thread)
         self.user.subscription_set.get(category=self.category)
         self.user.subscription_set.get(thread=other_thread)
-        self.user.subscription_set.get(category=self.category_b)
+        self.user.subscription_set.get(category=self.other_category)
 
         response = self.client.post(
             self.api_link, {
@@ -456,14 +382,13 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
         # subscriptions are kept
         self.assertEqual(self.user.subscription_set.count(), 1)
         self.user.subscription_set.get(thread=other_thread)
-        self.user.subscription_set.get(category=self.category_b)
+        self.user.subscription_set.get(category=self.other_category)
 
+    @patch_other_category_acl({"can_merge_threads": True})
+    @patch_category_acl({"can_merge_threads": True})
     def test_merge_threads_kept_best_answer(self):
         """api merges two threads successfully, keeping best answer from old thread"""
-        self.override_acl({'can_merge_threads': 1})
-        self.override_other_acl({'can_merge_threads': 1})
-
-        other_thread = testutils.post_thread(self.category_b)
+        other_thread = testutils.post_thread(self.other_category)
         best_answer = testutils.reply_thread(other_thread)
         other_thread.set_best_answer(self.user, best_answer)
         other_thread.save()
@@ -491,12 +416,11 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
         other_thread = Thread.objects.get(pk=other_thread.pk)
         self.assertEqual(other_thread.best_answer, best_answer)
 
+    @patch_other_category_acl({"can_merge_threads": True})
+    @patch_category_acl({"can_merge_threads": True})
     def test_merge_threads_moved_best_answer(self):
         """api merges two threads successfully, moving best answer to old thread"""
-        self.override_acl({'can_merge_threads': 1})
-        self.override_other_acl({'can_merge_threads': 1})
-
-        other_thread = testutils.post_thread(self.category_b)
+        other_thread = testutils.post_thread(self.other_category)
 
         best_answer = testutils.reply_thread(self.thread)
         self.thread.set_best_answer(self.user, best_answer)
@@ -525,16 +449,15 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
         other_thread = Thread.objects.get(pk=other_thread.pk)
         self.assertEqual(other_thread.best_answer, best_answer)
 
+    @patch_other_category_acl({"can_merge_threads": True})
+    @patch_category_acl({"can_merge_threads": True})
     def test_merge_threads_merge_conflict_best_answer(self):
         """api errors on merge conflict, returning list of available best answers"""
-        self.override_acl({'can_merge_threads': 1})
-        self.override_other_acl({'can_merge_threads': 1})
-
         best_answer = testutils.reply_thread(self.thread)
         self.thread.set_best_answer(self.user, best_answer)
         self.thread.save()
         
-        other_thread = testutils.post_thread(self.category_b)
+        other_thread = testutils.post_thread(self.other_category)
         other_best_answer = testutils.reply_thread(other_thread)
         other_thread.set_best_answer(self.user, other_best_answer)
         other_thread.save()
@@ -560,16 +483,15 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
         self.assertEqual(
             Thread.objects.get(pk=other_thread.pk).best_answer_id, other_best_answer.id)
 
+    @patch_other_category_acl({"can_merge_threads": True})
+    @patch_category_acl({"can_merge_threads": True})
     def test_threads_merge_conflict_best_answer_invalid_resolution(self):
         """api errors on invalid merge conflict resolution"""
-        self.override_acl({'can_merge_threads': 1})
-        self.override_other_acl({'can_merge_threads': 1})
-
         best_answer = testutils.reply_thread(self.thread)
         self.thread.set_best_answer(self.user, best_answer)
         self.thread.save()
         
-        other_thread = testutils.post_thread(self.category_b)
+        other_thread = testutils.post_thread(self.other_category)
         other_best_answer = testutils.reply_thread(other_thread)
         other_thread.set_best_answer(self.user, other_best_answer)
         other_thread.save()
@@ -590,16 +512,15 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
         self.assertEqual(
             Thread.objects.get(pk=other_thread.pk).best_answer_id, other_best_answer.id)
 
+    @patch_other_category_acl({"can_merge_threads": True})
+    @patch_category_acl({"can_merge_threads": True})
     def test_threads_merge_conflict_unmark_all_best_answers(self):
         """api unmarks all best answers when unmark all choice is selected"""
-        self.override_acl({'can_merge_threads': 1})
-        self.override_other_acl({'can_merge_threads': 1})
-
         best_answer = testutils.reply_thread(self.thread)
         self.thread.set_best_answer(self.user, best_answer)
         self.thread.save()
         
-        other_thread = testutils.post_thread(self.category_b)
+        other_thread = testutils.post_thread(self.other_category)
         other_best_answer = testutils.reply_thread(other_thread)
         other_thread.set_best_answer(self.user, other_best_answer)
         other_thread.save()
@@ -627,16 +548,15 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
         # final thread has no marked best answer
         self.assertIsNone(Thread.objects.get(pk=other_thread.pk).best_answer_id)
 
+    @patch_other_category_acl({"can_merge_threads": True})
+    @patch_category_acl({"can_merge_threads": True})
     def test_threads_merge_conflict_keep_first_best_answer(self):
         """api unmarks other best answer on merge"""
-        self.override_acl({'can_merge_threads': 1})
-        self.override_other_acl({'can_merge_threads': 1})
-
         best_answer = testutils.reply_thread(self.thread)
         self.thread.set_best_answer(self.user, best_answer)
         self.thread.save()
         
-        other_thread = testutils.post_thread(self.category_b)
+        other_thread = testutils.post_thread(self.other_category)
         other_best_answer = testutils.reply_thread(other_thread)
         other_thread.set_best_answer(self.user, other_best_answer)
         other_thread.save()
@@ -664,16 +584,15 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
         # other thread's best answer was unchanged
         self.assertEqual(Thread.objects.get(pk=other_thread.pk).best_answer_id, best_answer.id)
 
+    @patch_other_category_acl({"can_merge_threads": True})
+    @patch_category_acl({"can_merge_threads": True})
     def test_threads_merge_conflict_keep_other_best_answer(self):
         """api unmarks first best answer on merge"""
-        self.override_acl({'can_merge_threads': 1})
-        self.override_other_acl({'can_merge_threads': 1})
-
         best_answer = testutils.reply_thread(self.thread)
         self.thread.set_best_answer(self.user, best_answer)
         self.thread.save()
         
-        other_thread = testutils.post_thread(self.category_b)
+        other_thread = testutils.post_thread(self.other_category)
         other_best_answer = testutils.reply_thread(other_thread)
         other_thread.set_best_answer(self.user, other_best_answer)
         other_thread.save()
@@ -702,12 +621,11 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
         self.assertEqual(
             Thread.objects.get(pk=other_thread.pk).best_answer_id, other_best_answer.id)
 
+    @patch_other_category_acl({"can_merge_threads": True})
+    @patch_category_acl({"can_merge_threads": True})
     def test_merge_threads_kept_poll(self):
         """api merges two threads successfully, keeping poll from other thread"""
-        self.override_acl({'can_merge_threads': 1})
-        self.override_other_acl({'can_merge_threads': 1})
-
-        other_thread = testutils.post_thread(self.category_b)
+        other_thread = testutils.post_thread(self.other_category)
         poll = testutils.post_poll(other_thread, self.user)
 
         response = self.client.post(
@@ -733,12 +651,11 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
         self.assertEqual(Poll.objects.filter(pk=poll.pk, thread=other_thread).count(), 1)
         self.assertEqual(PollVote.objects.filter(poll=poll, thread=other_thread).count(), 4)
 
+    @patch_other_category_acl({"can_merge_threads": True})
+    @patch_category_acl({"can_merge_threads": True})
     def test_merge_threads_moved_poll(self):
         """api merges two threads successfully, moving poll from old thread"""
-        self.override_acl({'can_merge_threads': 1})
-        self.override_other_acl({'can_merge_threads': 1})
-
-        other_thread = testutils.post_thread(self.category_b)
+        other_thread = testutils.post_thread(self.other_category)
         poll = testutils.post_poll(self.thread, self.user)
 
         response = self.client.post(
@@ -764,12 +681,11 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
         self.assertEqual(Poll.objects.filter(pk=poll.pk, thread=other_thread).count(), 1)
         self.assertEqual(PollVote.objects.filter(poll=poll, thread=other_thread).count(), 4)
 
+    @patch_other_category_acl({"can_merge_threads": True})
+    @patch_category_acl({"can_merge_threads": True})
     def test_threads_merge_conflict_polls(self):
         """api errors on merge conflict, returning list of available polls"""
-        self.override_acl({'can_merge_threads': 1})
-        self.override_other_acl({'can_merge_threads': 1})
-
-        other_thread = testutils.post_thread(self.category_b)
+        other_thread = testutils.post_thread(self.other_category)
         poll = testutils.post_poll(self.thread, self.user)
         other_poll = testutils.post_poll(other_thread, self.user)
 
@@ -799,12 +715,11 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
         self.assertEqual(Poll.objects.count(), 2)
         self.assertEqual(PollVote.objects.count(), 8)
 
+    @patch_other_category_acl({"can_merge_threads": True})
+    @patch_category_acl({"can_merge_threads": True})
     def test_threads_merge_conflict_poll_invalid_resolution(self):
         """api errors on invalid merge conflict resolution"""
-        self.override_acl({'can_merge_threads': 1})
-        self.override_other_acl({'can_merge_threads': 1})
-
-        other_thread = testutils.post_thread(self.category_b)
+        other_thread = testutils.post_thread(self.other_category)
 
         testutils.post_poll(self.thread, self.user)
         testutils.post_poll(other_thread, self.user)
@@ -822,12 +737,11 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
         self.assertEqual(Poll.objects.count(), 2)
         self.assertEqual(PollVote.objects.count(), 8)
 
+    @patch_other_category_acl({"can_merge_threads": True})
+    @patch_category_acl({"can_merge_threads": True})
     def test_threads_merge_conflict_delete_all_polls(self):
         """api deletes all polls when delete all choice is selected"""
-        self.override_acl({'can_merge_threads': 1})
-        self.override_other_acl({'can_merge_threads': 1})
-
-        other_thread = testutils.post_thread(self.category_b)
+        other_thread = testutils.post_thread(self.other_category)
         testutils.post_poll(self.thread, self.user)
         testutils.post_poll(other_thread, self.user)
 
@@ -855,12 +769,11 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
         self.assertEqual(Poll.objects.count(), 0)
         self.assertEqual(PollVote.objects.count(), 0)
 
+    @patch_other_category_acl({"can_merge_threads": True})
+    @patch_category_acl({"can_merge_threads": True})
     def test_threads_merge_conflict_keep_first_poll(self):
         """api deletes other poll on merge"""
-        self.override_acl({'can_merge_threads': 1})
-        self.override_other_acl({'can_merge_threads': 1})
-
-        other_thread = testutils.post_thread(self.category_b)
+        other_thread = testutils.post_thread(self.other_category)
         poll = testutils.post_poll(self.thread, self.user)
         other_poll = testutils.post_poll(other_thread, self.user)
 
@@ -895,12 +808,11 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
         with self.assertRaises(Poll.DoesNotExist):
             Poll.objects.get(pk=other_poll.pk)
 
+    @patch_other_category_acl({"can_merge_threads": True})
+    @patch_category_acl({"can_merge_threads": True})
     def test_threads_merge_conflict_keep_other_poll(self):
         """api deletes first poll on merge"""
-        self.override_acl({'can_merge_threads': 1})
-        self.override_other_acl({'can_merge_threads': 1})
-
-        other_thread = testutils.post_thread(self.category_b)
+        other_thread = testutils.post_thread(self.other_category)
         poll = testutils.post_poll(self.thread, self.user)
         other_poll = testutils.post_poll(other_thread, self.user)
 

+ 57 - 198
misago/threads/tests/test_threads_merge_api.py

@@ -2,17 +2,19 @@ import json
 
 from django.urls import reverse
 
-from misago.acl import add_acl
-from misago.acl.testutils import override_acl
+from misago.acl import add_acl, useracl
 from misago.categories.models import Category
 from misago.readtracker import poststracker
 from misago.threads import testutils
-from misago.threads.serializers.moderation import THREADS_LIMIT
 from misago.threads.models import Poll, PollVote, Post, Thread
 from misago.threads.serializers import ThreadsListSerializer
+from misago.threads.serializers.moderation import THREADS_LIMIT
+from misago.threads.test import patch_category_acl, patch_other_category_acl
 
 from .test_threads_api import ThreadsApiTestCase
 
+cache_versions = {"acl": "abcdefgh"}
+
 
 class ThreadsMergeApiTests(ThreadsApiTestCase):
     def setUp(self):
@@ -20,40 +22,14 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         self.api_link = reverse('misago:api:thread-merge')
 
         Category(
-            name='Category B',
-            slug='category-b',
+            name='Other Category',
+            slug='other-category',
         ).insert_at(
             self.category,
             position='last-child',
             save=True,
         )
-        self.category_b = Category.objects.get(slug='category-b')
-
-    def override_other_category(self):
-        categories =  self.user.acl_cache['categories']
-
-        visible_categories = self.user.acl_cache['visible_categories']
-        browseable_categories = self.user.acl_cache['browseable_categories']
-
-        visible_categories.append(self.category_b.pk)
-        browseable_categories.append(self.category_b.pk)
-
-        override_acl(
-            self.user, {
-                'visible_categories': visible_categories,
-                'browseable_categories': browseable_categories,
-                'categories': {
-                    self.category.pk: categories[self.category.pk],
-                    self.category_b.pk: {
-                        'can_see': 1,
-                        'can_browse': 1,
-                        'can_see_all_threads': 1,
-                        'can_see_own_threads': 0,
-                        'can_start_threads': 2,
-                    },
-                },
-            }
-        )
+        self.other_category = Category.objects.get(slug='other-category')
 
     def test_merge_no_threads(self):
         """api validates if we are trying to merge no threads"""
@@ -143,7 +119,7 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
 
     def test_merge_with_invisible_thread(self):
         """api validates if we are trying to merge with inaccesible thread"""
-        unaccesible_thread = testutils.post_thread(category=self.category_b)
+        unaccesible_thread = testutils.post_thread(category=self.other_category)
 
         response = self.client.post(
             self.api_link,
@@ -166,7 +142,7 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         response = self.client.post(
             self.api_link,
             json.dumps({
-                'category': self.category.pk,
+                'category': self.category.id,
                 'title': 'Lorem ipsum dolor',
                 'threads': [self.thread.id, thread.id],
             }),
@@ -188,14 +164,10 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
             ]
         )
 
+    @patch_other_category_acl()
+    @patch_category_acl({"can_merge_threads": True, "can_close_threads": False})
     def test_thread_category_is_closed(self):
         """api validates if thread's category is open"""
-        self.override_acl({
-            'can_merge_threads': 1,
-            'can_close_threads': 0,
-        })
-        self.override_other_category()
-
         other_thread = testutils.post_thread(self.category)
 
         self.category.is_closed = True
@@ -204,7 +176,7 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         response = self.client.post(
             self.api_link,
             json.dumps({
-                'category': self.category_b.pk,
+                'category': self.other_category.id,
                 'title': 'Lorem ipsum dolor',
                 'threads': [self.thread.id, other_thread.id],
             }),
@@ -224,14 +196,10 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
             },
         ])
 
+    @patch_other_category_acl()
+    @patch_category_acl({"can_merge_threads": True, "can_close_threads": False})
     def test_thread_is_closed(self):
         """api validates if thread is open"""
-        self.override_acl({
-            'can_merge_threads': 1,
-            'can_close_threads': 0,
-        })
-        self.override_other_category()
-
         other_thread = testutils.post_thread(self.category)
 
         other_thread.is_closed = True
@@ -240,7 +208,7 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         response = self.client.post(
             self.api_link,
             json.dumps({
-                'category': self.category_b.pk,
+                'category': self.other_category.id,
                 'title': 'Lorem ipsum dolor',
                 'threads': [self.thread.id, other_thread.id],
             }),
@@ -255,19 +223,13 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
             },
         ])
 
+    @patch_category_acl({"can_merge_threads": True})
     def test_merge_too_many_threads(self):
         """api rejects too many threads to merge"""
         threads = []
         for _ in range(THREADS_LIMIT + 1):
             threads.append(testutils.post_thread(category=self.category).pk)
 
-        self.override_acl({
-            'can_merge_threads': True,
-            'can_close_threads': False,
-            'can_edit_threads': False,
-            'can_reply_threads': False,
-        })
-
         response = self.client.post(
             self.api_link,
             json.dumps({
@@ -282,15 +244,9 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
             }
         )
 
+    @patch_category_acl({"can_merge_threads": True})
     def test_merge_no_final_thread(self):
         """api rejects merge because no data to merge threads was specified"""
-        self.override_acl({
-            'can_merge_threads': True,
-            'can_close_threads': False,
-            'can_edit_threads': False,
-            'can_reply_threads': False,
-        })
-
         thread = testutils.post_thread(category=self.category)
 
         response = self.client.post(
@@ -308,15 +264,9 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
             }
         )
 
+    @patch_category_acl({"can_merge_threads": True})
     def test_merge_invalid_final_title(self):
         """api rejects merge because final thread title was invalid"""
-        self.override_acl({
-            'can_merge_threads': True,
-            'can_close_threads': False,
-            'can_edit_threads': False,
-            'can_reply_threads': False,
-        })
-
         thread = testutils.post_thread(category=self.category)
 
         response = self.client.post(
@@ -335,15 +285,9 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
             }
         )
 
+    @patch_category_acl({"can_merge_threads": True})
     def test_merge_invalid_category(self):
         """api rejects merge because final category was invalid"""
-        self.override_acl({
-            'can_merge_threads': True,
-            'can_close_threads': False,
-            'can_edit_threads': False,
-            'can_reply_threads': False,
-        })
-
         thread = testutils.post_thread(category=self.category)
 
         response = self.client.post(
@@ -351,7 +295,7 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
             json.dumps({
                 'threads': [self.thread.id, thread.id],
                 'title': 'Valid thread title',
-                'category': self.category_b.id,
+                'category': self.other_category.id,
             }),
             content_type="application/json",
         )
@@ -362,16 +306,9 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
             }
         )
 
+    @patch_category_acl({"can_merge_threads": True, "can_start_threads": False})
     def test_merge_unallowed_start_thread(self):
         """api rejects merge because category isn't allowing starting threads"""
-        self.override_acl({
-            'can_merge_threads': True,
-            'can_close_threads': False,
-            'can_edit_threads': False,
-            'can_reply_threads': False,
-            'can_start_threads': 0,
-        })
-
         thread = testutils.post_thread(category=self.category)
 
         response = self.client.post(
@@ -390,15 +327,9 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
             }
         )
 
+    @patch_category_acl({"can_merge_threads": True})
     def test_merge_invalid_weight(self):
         """api rejects merge because final weight was invalid"""
-        self.override_acl({
-            'can_merge_threads': True,
-            'can_close_threads': False,
-            'can_edit_threads': False,
-            'can_reply_threads': False,
-        })
-
         thread = testutils.post_thread(category=self.category)
 
         response = self.client.post(
@@ -418,15 +349,9 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
             }
         )
 
+    @patch_category_acl({"can_merge_threads": True})
     def test_merge_unallowed_global_weight(self):
         """api rejects merge because global weight was unallowed"""
-        self.override_acl({
-            'can_merge_threads': True,
-            'can_close_threads': False,
-            'can_edit_threads': False,
-            'can_reply_threads': False,
-        })
-
         thread = testutils.post_thread(category=self.category)
 
         response = self.client.post(
@@ -446,15 +371,9 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
             }
         )
 
+    @patch_category_acl({"can_merge_threads": True})
     def test_merge_unallowed_local_weight(self):
         """api rejects merge because local weight was unallowed"""
-        self.override_acl({
-            'can_merge_threads': True,
-            'can_close_threads': False,
-            'can_edit_threads': False,
-            'can_reply_threads': False,
-        })
-
         thread = testutils.post_thread(category=self.category)
 
         response = self.client.post(
@@ -474,16 +393,9 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
             }
         )
 
+    @patch_category_acl({"can_merge_threads": True, "can_pin_threads": 1})
     def test_merge_allowed_local_weight(self):
         """api allows local weight"""
-        self.override_acl({
-            'can_merge_threads': True,
-            'can_close_threads': False,
-            'can_edit_threads': False,
-            'can_reply_threads': False,
-            'can_pin_threads': 1,
-        })
-
         thread = testutils.post_thread(category=self.category)
 
         response = self.client.post(
@@ -503,16 +415,9 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
             }
         )
 
+    @patch_category_acl({"can_merge_threads": True, "can_pin_threads": 2})
     def test_merge_allowed_global_weight(self):
         """api allows global weight"""
-        self.override_acl({
-            'can_merge_threads': True,
-            'can_close_threads': False,
-            'can_edit_threads': False,
-            'can_reply_threads': False,
-            'can_pin_threads': 2,
-        })
-
         thread = testutils.post_thread(category=self.category)
 
         response = self.client.post(
@@ -532,15 +437,9 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
             }
         )
 
+    @patch_category_acl({"can_merge_threads": True, "can_close_threads": False})
     def test_merge_unallowed_close(self):
         """api rejects merge because closing thread was unallowed"""
-        self.override_acl({
-            'can_merge_threads': True,
-            'can_close_threads': False,
-            'can_edit_threads': False,
-            'can_reply_threads': False,
-        })
-
         thread = testutils.post_thread(category=self.category)
 
         response = self.client.post(
@@ -560,15 +459,9 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
             }
         )
 
+    @patch_category_acl({"can_merge_threads": True, "can_close_threads": True})
     def test_merge_with_close(self):
         """api allows for closing thread"""
-        self.override_acl({
-            'can_merge_threads': True,
-            'can_edit_threads': False,
-            'can_reply_threads': False,
-            'can_close_threads': True,
-        })
-
         thread = testutils.post_thread(category=self.category)
 
         response = self.client.post(
@@ -589,16 +482,9 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
             }
         )
 
+    @patch_category_acl({"can_merge_threads": True, "can_hide_threads": 0})
     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(
@@ -618,16 +504,9 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
             }
         )
 
+    @patch_category_acl({"can_merge_threads": True, "can_hide_threads": 1})
     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(
@@ -648,17 +527,10 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
             }
         )
 
+    @patch_category_acl({"can_merge_threads": True})
     def test_merge(self):
         """api performs basic merge"""
         posts_ids = [p.id for p in Post.objects.all()]
-
-        self.override_acl({
-            'can_merge_threads': True,
-            'can_close_threads': False,
-            'can_edit_threads': False,
-            'can_reply_threads': False,
-        })
-
         thread = testutils.post_thread(category=self.category)
 
         response = self.client.post(
@@ -679,8 +551,9 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         new_thread.is_read = False
         new_thread.subscription = None
 
-        add_acl(self.user, new_thread.category)
-        add_acl(self.user, new_thread)
+        user_acl = useracl.get_user_acl(self.user, cache_versions)
+        add_acl(user_acl, new_thread.category)
+        add_acl(user_acl, new_thread)
 
         self.assertEqual(response_json, ThreadsListSerializer(new_thread).data)
 
@@ -691,17 +564,15 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         # are old threads gone?
         self.assertEqual([t.pk for t in Thread.objects.all()], [new_thread.pk])
 
+    @patch_category_acl({
+        "can_merge_threads": True,
+        "can_close_threads": True,
+        "can_hide_threads": 1,
+        "can_pin_threads": 2,
+    })
     def test_merge_kitchensink(self):
         """api performs merge"""
         posts_ids = [p.id for p in Post.objects.all()]
-
-        self.override_acl({
-            'can_merge_threads': True,
-            'can_close_threads': True,
-            'can_hide_threads': 1,
-            'can_pin_threads': 2,
-        })
-
         thread = testutils.post_thread(category=self.category)
 
         poststracker.save_read(self.user, self.thread.first_post)
@@ -745,8 +616,9 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         self.assertTrue(new_thread.is_closed)
         self.assertTrue(new_thread.is_hidden)
 
-        add_acl(self.user, new_thread.category)
-        add_acl(self.user, new_thread)
+        user_acl = useracl.get_user_acl(self.user, cache_versions)
+        add_acl(user_acl, new_thread.category)
+        add_acl(user_acl, new_thread)
 
         self.assertEqual(response_json, ThreadsListSerializer(new_thread).data)
 
@@ -772,10 +644,9 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         self.user.subscription_set.get(thread=new_thread)
         self.user.subscription_set.get(category=self.category)
 
+    @patch_category_acl({"can_merge_threads": True})
     def test_merge_threads_merged_best_answer(self):
         """api merges two threads successfully, moving best answer to old thread"""
-        self.override_acl({'can_merge_threads': 1})
-
         other_thread = testutils.post_thread(self.category)
 
         best_answer = testutils.reply_thread(self.thread)
@@ -797,10 +668,9 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         new_thread = Thread.objects.get(pk=response.json()['id'])
         self.assertEqual(new_thread.best_answer_id, best_answer.id)
 
+    @patch_category_acl({"can_merge_threads": True})
     def test_merge_threads_merge_conflict_best_answer(self):
         """api errors on merge conflict, returning list of available best answers"""
-        self.override_acl({'can_merge_threads': 1})
-
         best_answer = testutils.reply_thread(self.thread)
         self.thread.set_best_answer(self.user, best_answer)
         self.thread.save()
@@ -835,10 +705,9 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         self.assertEqual(
             Thread.objects.get(pk=other_thread.pk).best_answer_id, other_best_answer.id)
 
+    @patch_category_acl({"can_merge_threads": True})
     def test_threads_merge_conflict_best_answer_invalid_resolution(self):
         """api errors on invalid merge conflict resolution"""
-        self.override_acl({'can_merge_threads': 1})
-
         best_answer = testutils.reply_thread(self.thread)
         self.thread.set_best_answer(self.user, best_answer)
         self.thread.save()
@@ -868,10 +737,9 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         self.assertEqual(
             Thread.objects.get(pk=other_thread.pk).best_answer_id, other_best_answer.id)
 
+    @patch_category_acl({"can_merge_threads": True})
     def test_threads_merge_conflict_unmark_all_best_answers(self):
         """api unmarks all best answers when unmark all choice is selected"""
-        self.override_acl({'can_merge_threads': 1})
-
         best_answer = testutils.reply_thread(self.thread)
         self.thread.set_best_answer(self.user, best_answer)
         self.thread.save()
@@ -898,10 +766,9 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         self.assertFalse(new_thread.has_best_answer)
         self.assertIsNone(new_thread.best_answer_id)
 
+    @patch_category_acl({"can_merge_threads": True})
     def test_threads_merge_conflict_keep_first_best_answer(self):
         """api unmarks other best answer on merge"""
-        self.override_acl({'can_merge_threads': 1})
-
         best_answer = testutils.reply_thread(self.thread)
         self.thread.set_best_answer(self.user, best_answer)
         self.thread.save()
@@ -927,10 +794,9 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         new_thread = Thread.objects.get(pk=response.json()['id'])
         self.assertEqual(new_thread.best_answer_id, best_answer.id)
 
+    @patch_category_acl({"can_merge_threads": True})
     def test_threads_merge_conflict_keep_other_best_answer(self):
         """api unmarks first best answer on merge"""
-        self.override_acl({'can_merge_threads': 1})
-
         best_answer = testutils.reply_thread(self.thread)
         self.thread.set_best_answer(self.user, best_answer)
         self.thread.save()
@@ -956,10 +822,9 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         new_thread = Thread.objects.get(pk=response.json()['id'])
         self.assertEqual(new_thread.best_answer_id, other_best_answer.id)
 
+    @patch_category_acl({"can_merge_threads": True})
     def test_merge_threads_kept_poll(self):
         """api merges two threads successfully, keeping poll from other thread"""
-        self.override_acl({'can_merge_threads': True})
-
         other_thread = testutils.post_thread(self.category)
         poll = testutils.post_poll(other_thread, self.user)
 
@@ -983,10 +848,9 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         self.assertEqual(Poll.objects.count(), 1)
         self.assertEqual(PollVote.objects.count(), 4)
 
+    @patch_category_acl({"can_merge_threads": True})
     def test_merge_threads_moved_poll(self):
         """api merges two threads successfully, moving poll from old thread"""
-        self.override_acl({'can_merge_threads': True})
-
         other_thread = testutils.post_thread(self.category)
         poll = testutils.post_poll(self.thread, self.user)
 
@@ -1010,10 +874,9 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         self.assertEqual(Poll.objects.count(), 1)
         self.assertEqual(PollVote.objects.count(), 4)
 
+    @patch_category_acl({"can_merge_threads": True})
     def test_threads_merge_conflict_poll(self):
         """api errors on merge conflict, returning list of available polls"""
-        self.override_acl({'can_merge_threads': True})
-
         other_thread = testutils.post_thread(self.category)
         poll = testutils.post_poll(self.thread, self.user)
         other_poll = testutils.post_poll(other_thread, self.user)
@@ -1049,10 +912,9 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         self.assertEqual(Poll.objects.count(), 2)
         self.assertEqual(PollVote.objects.count(), 8)
 
+    @patch_category_acl({"can_merge_threads": True})
     def test_threads_merge_conflict_poll_invalid_resolution(self):
         """api errors on invalid merge conflict resolution"""
-        self.override_acl({'can_merge_threads': True})
-
         other_thread = testutils.post_thread(self.category)
 
         testutils.post_poll(self.thread, self.user)
@@ -1078,10 +940,9 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         self.assertEqual(Poll.objects.count(), 2)
         self.assertEqual(PollVote.objects.count(), 8)
 
+    @patch_category_acl({"can_merge_threads": True})
     def test_threads_merge_conflict_delete_all_polls(self):
         """api deletes all polls when delete all choice is selected"""
-        self.override_acl({'can_merge_threads': True})
-
         other_thread = testutils.post_thread(self.category)
 
         testutils.post_poll(self.thread, self.user)
@@ -1103,10 +964,9 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         self.assertEqual(Poll.objects.count(), 0)
         self.assertEqual(PollVote.objects.count(), 0)
 
+    @patch_category_acl({"can_merge_threads": True})
     def test_threads_merge_conflict_keep_first_poll(self):
         """api deletes other poll on merge"""
-        self.override_acl({'can_merge_threads': True})
-
         other_thread = testutils.post_thread(self.category)
         poll = testutils.post_poll(self.thread, self.user)
         other_poll = testutils.post_poll(other_thread, self.user)
@@ -1131,10 +991,9 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         with self.assertRaises(Poll.DoesNotExist):
             Poll.objects.get(pk=other_poll.pk)
 
+    @patch_category_acl({"can_merge_threads": True})
     def test_threads_merge_conflict_keep_other_poll(self):
         """api deletes first poll on merge"""
-        self.override_acl({'can_merge_threads': True})
-
         other_thread = testutils.post_thread(self.category)
         poll = testutils.post_poll(self.thread, self.user)
         other_poll = testutils.post_poll(other_thread, self.user)

+ 3 - 3
misago/threads/validators.py

@@ -12,7 +12,7 @@ from misago.core.validators import validate_sluggable
 from .threadtypes import trees_map
 
 
-def validate_category(user, category_id, allow_root=False):
+def validate_category(user_acl, category_id, allow_root=False):
     try:
         threads_tree_id = trees_map.get_tree_id_for_root(THREADS_ROOT_NAME)
         category = Category.objects.get(
@@ -26,10 +26,10 @@ def validate_category(user, category_id, allow_root=False):
     if allow_root and category and not category.level:
         return category
 
-    if not category or not can_see_category(user, category):
+    if not category or not can_see_category(user_acl, category):
         raise ValidationError(_("Requested category could not be found."))
 
-    if not can_browse_category(user, category):
+    if not can_browse_category(user_acl, category):
         raise ValidationError(_("You don't have permission to access this category."))
     return category