Просмотр исходного кода

made exclude_invisible_posts overridable by views

Rafał Pitoń 10 лет назад
Родитель
Сommit
35db34b265

+ 2 - 2
docs/developers/thread_types.rst

@@ -15,7 +15,7 @@ Writing custom thread type
 
 Thread type is basically UI for users to interact with and Python code implementing features behind it, plus helper object for enabling your models to point to custom views.
 
-Thread type is decided by value of `special_role` attribute of forum model instance that content (thread, post, attachment, etc. ect.) belongs to. Using this value model is able to call `misago.threads.threadtypes.get()` in order to obtain its helper object, which in turn eables model to build links for its UI.
+Thread type is decided by value of `special_role` attribute of forum model instance that content (thread, post, attachment, etc. ect.) belongs to. Using this value model is able to call `misago.threads.threadtypes.get(thread_type)` in order to obtain its helper object.
 
 
 Helper classes
@@ -28,7 +28,7 @@ Paths to helper classess definitions are specified in `MISAGO_THREAD_TYPES` sett
 
 Once helper class is defined, it's available as "thread_type" attribute on forum, thread, post, event, poll and attachment models.
 
-Models call their helpers to get url's for handling their type UI:
+Depending on features used by thread type, its helper is expected to define different of the following methods:
 
 
 get_forum_name

+ 9 - 11
misago/threads/goto.py

@@ -5,11 +5,9 @@ from django.core.urlresolvers import reverse
 from misago.readtracker.threadstracker import make_read_aware
 
 from misago.threads.models import Post
-from misago.threads.permissions import exclude_invisible_posts
 
 
-def posts_queryset(user, thread):
-    qs = exclude_invisible_posts(thread.post_set, user, thread.forum)
+def posts_queryset(qs):
     return qs.count(), qs.order_by('id')
 
 
@@ -41,8 +39,8 @@ def hashed_reverse(thread, post, page=1):
     return thread.get_post_url(post.pk, page)
 
 
-def last(user, thread):
-    posts, qs = posts_queryset(user, thread)
+def last(thread, posts_qs):
+    posts, qs = posts_queryset(posts_qs)
     thread_pages = get_thread_pages(posts)
 
     return thread.get_post_url(thread.last_post_id, thread_pages)
@@ -53,20 +51,20 @@ def get_post_link(posts, qs, thread, post):
     return hashed_reverse(thread, post, post_page)
 
 
-def new(user, thread):
+def new(user, thread, posts_qs):
     make_read_aware(user, thread)
     if thread.is_read:
-        return last(user, thread)
+        return last(thread, posts_qs)
 
-    posts, qs = posts_queryset(user, thread)
+    posts, qs = posts_queryset(posts_qs)
     try:
         first_unread = qs.filter(posted_on__gt=thread.last_read_on)[:1][0]
     except IndexError:
-        return last(user, thread)
+        return last(thread, posts_qs)
 
     return get_post_link(posts, qs, thread, first_unread)
 
 
-def post(user, thread, post):
-    posts, qs = posts_queryset(user, thread)
+def post(thread, posts_qs, post):
+    posts, qs = posts_queryset(posts_qs)
     return get_post_link(posts, qs, thread, post)

+ 1 - 1
misago/threads/migrations/0001_initial.py

@@ -41,7 +41,7 @@ class Migration(migrations.Migration):
                 ('checksum', models.CharField(max_length=64, default='-')),
                 ('has_attachments', models.BooleanField(default=False)),
                 ('pickled_attachments', models.TextField(null=True, blank=True)),
-                ('posted_on', models.DateTimeField()),
+                ('posted_on', models.DateTimeField(db_index=True)),
                 ('updated_on', models.DateTimeField()),
                 ('edits', models.PositiveIntegerField(default=0)),
                 ('last_editor_name', models.CharField(max_length=255, null=True, blank=True)),

+ 1 - 1
misago/threads/models/post.py

@@ -25,7 +25,7 @@ class Post(models.Model):
     has_attachments = models.BooleanField(default=False)
     pickled_attachments = models.TextField(null=True, blank=True)
 
-    posted_on = models.DateTimeField()
+    posted_on = models.DateTimeField(db_index=True)
     updated_on = models.DateTimeField()
 
     edits = models.PositiveIntegerField(default=0)

+ 11 - 0
misago/threads/permissions/privatethreads.py

@@ -1,6 +1,7 @@
 from django.contrib.auth import get_user_model
 from django.core.exceptions import PermissionDenied
 from django.db.models import Q
+from django.http import Http404
 from django.utils.translation import ugettext_lazy as _
 
 from misago.acl import add_acl, algebra
@@ -20,6 +21,7 @@ __all__ = [
     'allow_message_user',
     'can_message_user',
     'exclude_invisible_private_threads',
+    'exclude_invisible_private_posts'
 ]
 
 
@@ -209,3 +211,12 @@ def exclude_invisible_private_threads(queryset, user):
         return queryset.filter(see_reported | see_participating)
     else:
         return queryset.filter(participants=user)
+
+
+def exclude_invisible_private_posts(queryset, user, forum, thread):
+    if not forum.acl['can_review_moderated_content']:
+        for participant in thread.participants_list:
+            if participant.user == user and participant.is_removed:
+                left_on = participant.last_post_on
+                return queryset.filter(posted_on__lte=left_on)
+    return queryset

+ 23 - 16
misago/threads/tests/test_goto.py

@@ -4,6 +4,7 @@ from misago.readtracker import threadstracker
 from misago.users.testutils import AuthenticatedUserTestCase
 
 from misago.threads import goto
+from misago.threads.permissions import exclude_invisible_posts
 from misago.threads.testutils import post_thread, reply_thread
 
 
@@ -64,21 +65,21 @@ class GotoTests(AuthenticatedUserTestCase):
 
     def test_last(self):
         """last returns link to last post in thread"""
-        url_last = goto.last(self.user, self.thread)
+        url_last = goto.last(self.thread, self.thread.post_set)
         url_formats = self.thread.get_absolute_url(), self.thread.last_post_id
         self.assertEqual(url_last, '%s#post-%s' % url_formats)
 
         # add 12 posts to reach page limit
         [reply_thread(self.thread) for p in xrange(12)]
 
-        url_last = goto.last(self.user, self.thread)
+        url_last = goto.last(self.thread, self.thread.post_set)
         url_formats = self.thread.get_absolute_url(), self.thread.last_post_id
         self.assertEqual(url_last, '%s#post-%s' % url_formats)
 
         # add 2 posts to add second page to thread
         [reply_thread(self.thread) for p in xrange(2)]
 
-        url_last = goto.last(self.user, self.thread)
+        url_last = goto.last(self.thread, self.thread.post_set)
         url_formats = self.thread.get_absolute_url(), self.thread.last_post_id
         self.assertEqual(url_last, '%s2/#post-%s' % url_formats)
 
@@ -86,22 +87,25 @@ class GotoTests(AuthenticatedUserTestCase):
         """get_post_link returns link to specified post"""
         post_link = goto.get_post_link(
             1, self.thread.post_set, self.thread, self.thread.last_post)
-        self.assertEqual(post_link, goto.last(self.user, self.thread))
+        last_link = goto.last(self.thread, self.thread.post_set)
+        self.assertEqual(post_link, last_link)
 
         # add 16 posts to add extra page to thread
         [reply_thread(self.thread) for p in xrange(16)]
 
         post_link = goto.get_post_link(
             17, self.thread.post_set, self.thread, self.thread.last_post)
-        self.assertEqual(post_link, goto.last(self.user, self.thread))
+        last_link = goto.last(self.thread, self.thread.post_set)
+        self.assertEqual(post_link, last_link)
 
     def test_new(self):
         """new returns link to first unread post"""
         self.user.new_threads = MockThreadsCounter()
         self.user.unread_threads = MockThreadsCounter()
 
-        post_link = goto.new(self.user, self.thread)
-        self.assertEqual(post_link, goto.last(self.user, self.thread))
+        post_link = goto.new(self.user, self.thread, self.thread.post_set)
+        last_link = goto.last(self.thread, self.thread.post_set)
+        self.assertEqual(post_link, last_link)
 
         # add 18 posts to add extra page to thread, then read them
         [reply_thread(self.thread) for p in xrange(18)]
@@ -112,7 +116,7 @@ class GotoTests(AuthenticatedUserTestCase):
         first_unread = reply_thread(self.thread)
         [reply_thread(self.thread) for p in xrange(30)]
 
-        new_link = goto.new(self.user, self.thread)
+        new_link = goto.new(self.user, self.thread, self.thread.post_set)
         post_link = goto.get_post_link(
             50, self.thread.post_set, self.thread, first_unread)
         self.assertEqual(new_link, post_link)
@@ -122,18 +126,21 @@ class GotoTests(AuthenticatedUserTestCase):
             self.user, self.thread, self.thread.last_post)
 
         # assert new() points to last reply
-        post_link = goto.new(self.user, self.thread)
-        self.assertEqual(post_link, goto.last(self.user, self.thread))
+        post_link = goto.new(self.user, self.thread, self.thread.post_set)
+        last_link = goto.last(self.thread, self.thread.post_set)
+        self.assertEqual(post_link, last_link)
 
     def test_post(self):
         """post returns link to post given"""
-        self.assertEqual(
-            goto.last(self.user, self.thread),
-            goto.post(self.user, self.thread, self.thread.last_post))
+        thread = self.thread
+
+        post_link = goto.post(thread, thread.post_set, thread.last_post)
+        last_link = goto.last(self.thread, self.thread.post_set)
+        self.assertEqual(post_link, last_link)
 
         # add 24 posts
         [reply_thread(self.thread) for p in xrange(24)]
 
-        self.assertEqual(
-            goto.last(self.user, self.thread),
-            goto.post(self.user, self.thread, self.thread.last_post))
+        post_link = goto.post(thread, thread.post_set, thread.last_post)
+        last_link = goto.last(self.thread, self.thread.post_set)
+        self.assertEqual(post_link, last_link)

+ 7 - 8
misago/threads/tests/test_goto_views.py

@@ -21,16 +21,16 @@ class GotoViewsTests(AuthenticatedUserTestCase):
         """thread_last link points to last post in thread"""
         response = self.client.get(self.thread.get_last_reply_url())
         self.assertEqual(response.status_code, 302)
-        self.assertTrue(
-            response['location'].endswith(goto.last(self.user, self.thread)))
+        last_link = goto.last(self.thread, self.thread.post_set)
+        self.assertTrue(response['location'].endswith(last_link))
 
         # add 36 posts to thread
         [reply_thread(self.thread) for p in xrange(36)]
 
         response = self.client.get(self.thread.get_last_reply_url())
         self.assertEqual(response.status_code, 302)
-        self.assertTrue(
-            response['location'].endswith(goto.last(self.user, self.thread)))
+        last_link = goto.last(self.thread, self.thread.post_set)
+        self.assertTrue(response['location'].endswith(last_link))
 
     def test_goto_new(self):
         """thread_new link points to first unread post in thread"""
@@ -45,11 +45,10 @@ class GotoViewsTests(AuthenticatedUserTestCase):
         unread_post = reply_thread(self.thread)
         [reply_thread(self.thread) for p in xrange(32)]
 
-        unread_post_link = goto.new(self.user, self.thread)
-
         response = self.client.get(self.thread.get_new_reply_url())
         self.assertEqual(response.status_code, 302)
-        self.assertTrue(response['location'].endswith(unread_post_link))
+        unread_link = goto.new(self.user, self.thread, self.thread.post_set)
+        self.assertTrue(response['location'].endswith(unread_link))
 
     def test_goto_post(self):
         """thread_post link points to specific post in thread"""
@@ -63,7 +62,7 @@ class GotoViewsTests(AuthenticatedUserTestCase):
         [reply_thread(self.thread) for p in xrange(32)]
 
         # see post link
-        post_link = goto.post(self.user, self.thread, target_post)
+        post_link = goto.post(self.thread, self.thread.post_set, target_post)
 
         response = self.client.get(target_post.get_absolute_url())
         self.assertEqual(response.status_code, 302)

+ 104 - 0
misago/threads/tests/test_privatethread_view.py

@@ -0,0 +1,104 @@
+from django.utils import timezone
+
+from misago.acl.testutils import override_acl
+from misago.forums.models import Forum
+from misago.users.testutils import AuthenticatedUserTestCase
+
+from misago.threads import testutils
+from misago.threads.models import ThreadParticipant
+
+
+class PrivateThreadTests(AuthenticatedUserTestCase):
+    def setUp(self):
+        super(PrivateThreadTests, self).setUp()
+
+        self.forum = Forum.objects.private_threads()
+        self.thread = testutils.post_thread(self.forum)
+
+    def test_anon_access_to_view(self):
+        """anonymous user has no access to private thread"""
+        self.logout_user()
+        response = self.client.get(self.thread.get_absolute_url())
+        self.assertEqual(response.status_code, 403)
+
+    def test_non_participant_access_to_thread(self):
+        """non-participant user has no access to private thread"""
+        override_acl(self.user, {'can_use_private_threads': True})
+        response = self.client.get(self.thread.get_absolute_url())
+        self.assertEqual(response.status_code, 404)
+
+    def test_owner_can_access_thread(self):
+        """owner has access to private thread"""
+        override_acl(self.user, {'can_use_private_threads': True})
+        ThreadParticipant.objects.set_owner(self.thread, self.user)
+
+        response = self.client.get(self.thread.get_absolute_url())
+        self.assertEqual(response.status_code, 200)
+        self.assertIn(self.thread.title, response.content)
+
+    def test_participant_can_access_thread(self):
+        """participant has access to private thread"""
+        override_acl(self.user, {'can_use_private_threads': True})
+        ThreadParticipant.objects.add_participant(self.thread, self.user)
+
+        response = self.client.get(self.thread.get_absolute_url())
+        self.assertEqual(response.status_code, 200)
+        self.assertIn(self.thread.title, response.content)
+
+    def test_removed_can_access_thread(self):
+        """removed user has access to private thread"""
+        override_acl(self.user, {'can_use_private_threads': True})
+        ThreadParticipant.objects.remove_participant(self.thread, self.user)
+
+        response = self.client.get(self.thread.get_absolute_url())
+        self.assertEqual(response.status_code, 200)
+        self.assertIn(self.thread.title, response.content)
+
+    def test_removed_user_access_to_thread(self):
+        """removed user can't see content made after he was removed"""
+        override_acl(self.user, {'can_use_private_threads': True})
+
+        visible_posts = []
+        for p in range(4):
+            visible_posts.append(
+                testutils.reply_thread(self.thread, posted_on=timezone.now()))
+
+        ThreadParticipant.objects.remove_participant(self.thread, self.user)
+
+        hidden_posts = []
+        for p in range(4):
+            hidden_posts.append(
+                testutils.reply_thread(self.thread, posted_on=timezone.now()))
+
+        response = self.client.get(self.thread.get_absolute_url())
+        self.assertEqual(response.status_code, 200)
+        self.assertIn(self.thread.title, response.content)
+
+        for visible_post in visible_posts:
+            self.assertIn(visible_post.get_absolute_url(), response.content)
+        for hidden_post in hidden_posts:
+            self.assertNotIn(hidden_post.get_absolute_url(), response.content)
+
+    def test_moderator_cant_access_unreported_thread(self):
+        """moderator cant see private thread without reports"""
+        override_acl(self.user, {
+            'can_use_private_threads': True,
+            'can_moderate_private_threads': True
+        })
+
+        response = self.client.get(self.thread.get_absolute_url())
+        self.assertEqual(response.status_code, 404)
+
+    def test_moderator_can_access_reported_thread(self):
+        """moderator can see private thread with reports"""
+        override_acl(self.user, {
+            'can_use_private_threads': True,
+            'can_moderate_private_threads': True
+        })
+
+        self.thread.has_reported_posts = True
+        self.thread.save()
+
+        response = self.client.get(self.thread.get_absolute_url())
+        self.assertEqual(response.status_code, 200)
+        self.assertIn(self.thread.title, response.content)

+ 5 - 1
misago/threads/views/generic/base.py

@@ -8,7 +8,8 @@ from misago.forums.models import Forum
 from misago.forums.permissions import allow_see_forum, allow_browse_forum
 
 from misago.threads.models import Thread, Post
-from misago.threads.permissions import allow_see_thread, allow_see_post
+from misago.threads.permissions import (allow_see_thread, allow_see_post,
+                                        exclude_invisible_posts)
 
 
 __all__ = ['ForumMixin', 'ThreadMixin', 'PostMixin', 'ViewBase']
@@ -126,6 +127,9 @@ class PostMixin(object):
         allow_see_thread(request.user, post.thread)
         allow_see_forum(request.user, post.forum)
 
+    def exclude_invisible_posts(self, queryset, user, forum, thread):
+        return exclude_invisible_posts(queryset, user, forum)
+
 
 class ViewBase(ForumMixin, ThreadMixin, PostMixin, View):
     def process_context(self, request, context):

+ 14 - 8
misago/threads/views/generic/goto.py

@@ -25,22 +25,25 @@ class BaseGotoView(ViewBase):
         self.check_forum_permissions(request, forum)
         self.check_thread_permissions(request, thread)
 
-        return redirect(self.get_redirect(request.user, thread))
+        posts_qs = self.exclude_invisible_posts(
+            thread.post_set, request.user, forum, thread)
+
+        return redirect(self.get_redirect(request.user, thread, posts_qs))
 
 
 class GotoLastView(BaseGotoView):
-    def get_redirect(self, user, thread):
-        return goto.last(user, thread)
+    def get_redirect(self, user, thread, posts_qs):
+        return goto.last(thread, posts_qs)
 
 
 class GotoNewView(BaseGotoView):
-    def get_redirect(self, user, thread):
-        return goto.new(user, thread)
+    def get_redirect(self, user, thread, posts_qs):
+        return goto.new(user, thread, posts_qs)
 
 
 class GotoPostView(BaseGotoView):
-    def get_redirect(self, user, thread, post):
-        return goto.post(user, thread, post)
+    def get_redirect(self, thread, posts_qs, post):
+        return goto.post(thread, posts_qs, post)
 
     def dispatch(self, request, *args, **kwargs):
         post = self.fetch_post(
@@ -53,4 +56,7 @@ class GotoPostView(BaseGotoView):
         self.check_thread_permissions(request, thread)
         self.check_post_permissions(request, post)
 
-        return redirect(self.get_redirect(request.user, thread, post))
+        posts_qs = self.exclude_invisible_posts(
+            thread.post_set, request.user, thread.forum, thread)
+
+        return redirect(self.get_redirect(thread, posts_qs, post))

+ 6 - 7
misago/threads/views/generic/gotopostslist.py

@@ -1,7 +1,6 @@
 from django.core.exceptions import PermissionDenied
 from django.utils.translation import ugettext as _
 
-from misago.threads.permissions import exclude_invisible_posts
 from misago.threads.views.generic.base import ViewBase
 
 
@@ -34,17 +33,17 @@ class ModeratedPostsListView(ViewBase):
             response.status_code = 405
             return response
 
-        queryset = exclude_invisible_posts(
-            thread.post_set, request.user, forum)
-        queryset = self.filter_posts_queryset(queryset)
-        final_queryset = queryset.select_related('poster').order_by('-id')[:15]
+        posts_qs = self.exclude_invisible_posts(
+            thread.post_set, request.user, forum, thread)
+        posts_qs = self.filter_posts_queryset(posts_qs)
+        final_posts_qs = posts_qs.select_related('poster').order_by('-id')[:15]
 
         return self.render(request, {
             'forum': forum,
             'thread': thread,
 
-            'posts_count': queryset.count(),
-            'posts': final_queryset.iterator()
+            'posts_count': posts_qs.count(),
+            'posts': final_posts_qs.iterator()
         })
 
 

+ 12 - 8
misago/threads/views/generic/post.py

@@ -8,7 +8,6 @@ from django.utils.translation import ugettext as _
 from misago.acl import add_acl
 
 from misago.threads import permissions, moderation, goto
-from misago.threads.permissions import exclude_invisible_posts
 from misago.threads.views.generic.base import ViewBase
 
 
@@ -52,7 +51,11 @@ class PostView(ViewBase):
             "post views have to override real_dispatch method")
 
     def redirect_to_post(self, user, post):
-        return redirect(goto.post(user, post.thread, post))
+        posts_qs = self.exclude_invisible_posts(post.thread.post_set,
+                                                user,
+                                                post.forum,
+                                                post.thread)
+        return redirect(goto.post(post.thread, posts_qs, post))
 
 
 class QuotePostView(PostView):
@@ -115,15 +118,16 @@ class DeletePostView(PostView):
         post.forum.synchronize()
         post.forum.save()
 
-        posts_queryset = exclude_invisible_posts(post.thread.post_set,
-                                                 request.user,
-                                                 post.forum)
-        posts_queryset = posts_queryset.select_related('thread', 'forum')
+        posts_qs = self.exclude_invisible_posts(post.thread.post_set,
+                                                request.user,
+                                                post.forum,
+                                                post.thread)
+        posts_qs = posts_qs.select_related('thread', 'forum')
 
         if post_id < post.thread.last_post_id:
-            target_post = posts_queryset.order_by('id').filter(id__gt=post_id)
+            target_post = posts_qs.order_by('id').filter(id__gt=post_id)
         else:
-            target_post = posts_queryset.order_by('-id').filter(id__lt=post_id)
+            target_post = posts_qs.order_by('-id').filter(id__lt=post_id)
 
         target_post = target_post[:1][0]
         target_post.thread.forum = target_post.forum

+ 22 - 18
misago/threads/views/generic/posting.py

@@ -113,26 +113,30 @@ class PostingView(ViewBase):
                 if formset.is_valid():
                     try:
                         formset.save()
-
-                        if mode == EDIT:
-                            message = _("Changes saved.")
-                        else:
-                            if mode == START:
-                                message = _("New thread was posted.")
-                            if mode == REPLY:
-                                message = _("Your reply was posted.")
-                            messages.success(request, message)
-
-                        return JsonResponse({
-                            'message': message,
-                            'post_url': goto.post(request.user, thread, post),
-                            'parsed': post.parsed,
-                            'original': post.original,
-                            'title': thread.title,
-                            'title_escaped': html.escape(thread.title),
-                        })
                     except PostingInterrupt as e:
                         return JsonResponse({'interrupt': e.message})
+
+                    if mode == EDIT:
+                        message = _("Changes saved.")
+                    else:
+                        if mode == START:
+                            message = _("New thread was posted.")
+                        if mode == REPLY:
+                            message = _("Your reply was posted.")
+                        messages.success(request, message)
+
+                    posts_qs = self.exclude_invisible_posts(
+                        thread.post_set, request.user, forum, thread)
+                    post_url = goto.post(thread, posts_qs, post)
+
+                    return JsonResponse({
+                        'message': message,
+                        'post_url': post_url,
+                        'parsed': post.parsed,
+                        'original': post.original,
+                        'title': thread.title,
+                        'title_escaped': html.escape(thread.title),
+                    })
                 else:
                     return JsonResponse({'errors': formset.errors})
 

+ 4 - 5
misago/threads/views/generic/thread/view.py

@@ -9,8 +9,7 @@ from misago.users.online.utils import get_user_state
 
 from misago.threads.events import add_events_to_posts
 from misago.threads.paginator import paginate
-from misago.threads.permissions import (allow_reply_thread,
-                                        exclude_invisible_posts)
+from misago.threads.permissions import allow_reply_thread
 from misago.threads.views.generic.base import ViewBase
 from misago.threads.views.generic.thread.postsactions import PostsActions
 from misago.threads.views.generic.thread.threadactions import ThreadActions
@@ -29,6 +28,7 @@ class ThreadView(ViewBase):
 
     def get_posts(self, user, forum, thread, kwargs):
         queryset = self.get_posts_queryset(user, forum, thread)
+        queryset = self.exclude_invisible_posts(queryset, user, forum, thread)
         page = paginate(queryset, kwargs.get('page', 0), 10, 3)
 
         posts = []
@@ -52,13 +52,12 @@ class ThreadView(ViewBase):
         return page, posts
 
     def get_posts_queryset(self, user, forum, thread):
-        queryset = thread.post_set.select_related(
+        return thread.post_set.select_related(
             'poster',
             'poster__rank',
             'poster__ban_cache',
             'poster__online_tracker'
-        )
-        return exclude_invisible_posts(queryset, user, forum).order_by('id')
+        ).order_by('id')
 
     def dispatch(self, request, *args, **kwargs):
         relations = ['forum', 'starter', 'last_poster', 'first_post']

+ 5 - 1
misago/threads/views/privatethreads.py

@@ -9,7 +9,8 @@ from misago.threads.models import Thread, ThreadParticipant
 from misago.threads.permissions import (allow_use_private_threads,
                                         allow_see_private_thread,
                                         allow_see_private_post,
-                                        exclude_invisible_private_threads)
+                                        exclude_invisible_private_threads,
+                                        exclude_invisible_private_posts)
 from misago.threads.views import generic
 
 
@@ -84,6 +85,9 @@ class PrivateThreadsMixin(object):
         allow_see_private_thread(request.user, post.thread)
         allow_use_private_threads(request.user)
 
+    def exclude_invisible_posts(self, queryset, user, forum, thread):
+        return exclude_invisible_private_posts(queryset, user, forum, thread)
+
 
 class PrivateThreads(generic.Threads):
     def get_queryset(self):