Browse Source

made exclude_invisible_posts overridable by views

Rafał Pitoń 10 years ago
parent
commit
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 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
 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.
 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
 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.readtracker.threadstracker import make_read_aware
 
 
 from misago.threads.models import Post
 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')
     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)
     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)
     thread_pages = get_thread_pages(posts)
 
 
     return thread.get_post_url(thread.last_post_id, thread_pages)
     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)
     return hashed_reverse(thread, post, post_page)
 
 
 
 
-def new(user, thread):
+def new(user, thread, posts_qs):
     make_read_aware(user, thread)
     make_read_aware(user, thread)
     if thread.is_read:
     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:
     try:
         first_unread = qs.filter(posted_on__gt=thread.last_read_on)[:1][0]
         first_unread = qs.filter(posted_on__gt=thread.last_read_on)[:1][0]
     except IndexError:
     except IndexError:
-        return last(user, thread)
+        return last(thread, posts_qs)
 
 
     return get_post_link(posts, qs, thread, first_unread)
     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)
     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='-')),
                 ('checksum', models.CharField(max_length=64, default='-')),
                 ('has_attachments', models.BooleanField(default=False)),
                 ('has_attachments', models.BooleanField(default=False)),
                 ('pickled_attachments', models.TextField(null=True, blank=True)),
                 ('pickled_attachments', models.TextField(null=True, blank=True)),
-                ('posted_on', models.DateTimeField()),
+                ('posted_on', models.DateTimeField(db_index=True)),
                 ('updated_on', models.DateTimeField()),
                 ('updated_on', models.DateTimeField()),
                 ('edits', models.PositiveIntegerField(default=0)),
                 ('edits', models.PositiveIntegerField(default=0)),
                 ('last_editor_name', models.CharField(max_length=255, null=True, blank=True)),
                 ('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)
     has_attachments = models.BooleanField(default=False)
     pickled_attachments = models.TextField(null=True, blank=True)
     pickled_attachments = models.TextField(null=True, blank=True)
 
 
-    posted_on = models.DateTimeField()
+    posted_on = models.DateTimeField(db_index=True)
     updated_on = models.DateTimeField()
     updated_on = models.DateTimeField()
 
 
     edits = models.PositiveIntegerField(default=0)
     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.contrib.auth import get_user_model
 from django.core.exceptions import PermissionDenied
 from django.core.exceptions import PermissionDenied
 from django.db.models import Q
 from django.db.models import Q
+from django.http import Http404
 from django.utils.translation import ugettext_lazy as _
 from django.utils.translation import ugettext_lazy as _
 
 
 from misago.acl import add_acl, algebra
 from misago.acl import add_acl, algebra
@@ -20,6 +21,7 @@ __all__ = [
     'allow_message_user',
     'allow_message_user',
     'can_message_user',
     'can_message_user',
     'exclude_invisible_private_threads',
     '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)
         return queryset.filter(see_reported | see_participating)
     else:
     else:
         return queryset.filter(participants=user)
         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.users.testutils import AuthenticatedUserTestCase
 
 
 from misago.threads import goto
 from misago.threads import goto
+from misago.threads.permissions import exclude_invisible_posts
 from misago.threads.testutils import post_thread, reply_thread
 from misago.threads.testutils import post_thread, reply_thread
 
 
 
 
@@ -64,21 +65,21 @@ class GotoTests(AuthenticatedUserTestCase):
 
 
     def test_last(self):
     def test_last(self):
         """last returns link to last post in thread"""
         """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
         url_formats = self.thread.get_absolute_url(), self.thread.last_post_id
         self.assertEqual(url_last, '%s#post-%s' % url_formats)
         self.assertEqual(url_last, '%s#post-%s' % url_formats)
 
 
         # add 12 posts to reach page limit
         # add 12 posts to reach page limit
         [reply_thread(self.thread) for p in xrange(12)]
         [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
         url_formats = self.thread.get_absolute_url(), self.thread.last_post_id
         self.assertEqual(url_last, '%s#post-%s' % url_formats)
         self.assertEqual(url_last, '%s#post-%s' % url_formats)
 
 
         # add 2 posts to add second page to thread
         # add 2 posts to add second page to thread
         [reply_thread(self.thread) for p in xrange(2)]
         [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
         url_formats = self.thread.get_absolute_url(), self.thread.last_post_id
         self.assertEqual(url_last, '%s2/#post-%s' % url_formats)
         self.assertEqual(url_last, '%s2/#post-%s' % url_formats)
 
 
@@ -86,22 +87,25 @@ class GotoTests(AuthenticatedUserTestCase):
         """get_post_link returns link to specified post"""
         """get_post_link returns link to specified post"""
         post_link = goto.get_post_link(
         post_link = goto.get_post_link(
             1, self.thread.post_set, self.thread, self.thread.last_post)
             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
         # add 16 posts to add extra page to thread
         [reply_thread(self.thread) for p in xrange(16)]
         [reply_thread(self.thread) for p in xrange(16)]
 
 
         post_link = goto.get_post_link(
         post_link = goto.get_post_link(
             17, self.thread.post_set, self.thread, self.thread.last_post)
             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):
     def test_new(self):
         """new returns link to first unread post"""
         """new returns link to first unread post"""
         self.user.new_threads = MockThreadsCounter()
         self.user.new_threads = MockThreadsCounter()
         self.user.unread_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
         # add 18 posts to add extra page to thread, then read them
         [reply_thread(self.thread) for p in xrange(18)]
         [reply_thread(self.thread) for p in xrange(18)]
@@ -112,7 +116,7 @@ class GotoTests(AuthenticatedUserTestCase):
         first_unread = reply_thread(self.thread)
         first_unread = reply_thread(self.thread)
         [reply_thread(self.thread) for p in xrange(30)]
         [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(
         post_link = goto.get_post_link(
             50, self.thread.post_set, self.thread, first_unread)
             50, self.thread.post_set, self.thread, first_unread)
         self.assertEqual(new_link, post_link)
         self.assertEqual(new_link, post_link)
@@ -122,18 +126,21 @@ class GotoTests(AuthenticatedUserTestCase):
             self.user, self.thread, self.thread.last_post)
             self.user, self.thread, self.thread.last_post)
 
 
         # assert new() points to last reply
         # 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):
     def test_post(self):
         """post returns link to post given"""
         """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
         # add 24 posts
         [reply_thread(self.thread) for p in xrange(24)]
         [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"""
         """thread_last link points to last post in thread"""
         response = self.client.get(self.thread.get_last_reply_url())
         response = self.client.get(self.thread.get_last_reply_url())
         self.assertEqual(response.status_code, 302)
         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
         # add 36 posts to thread
         [reply_thread(self.thread) for p in xrange(36)]
         [reply_thread(self.thread) for p in xrange(36)]
 
 
         response = self.client.get(self.thread.get_last_reply_url())
         response = self.client.get(self.thread.get_last_reply_url())
         self.assertEqual(response.status_code, 302)
         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):
     def test_goto_new(self):
         """thread_new link points to first unread post in thread"""
         """thread_new link points to first unread post in thread"""
@@ -45,11 +45,10 @@ class GotoViewsTests(AuthenticatedUserTestCase):
         unread_post = reply_thread(self.thread)
         unread_post = reply_thread(self.thread)
         [reply_thread(self.thread) for p in xrange(32)]
         [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())
         response = self.client.get(self.thread.get_new_reply_url())
         self.assertEqual(response.status_code, 302)
         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):
     def test_goto_post(self):
         """thread_post link points to specific post in thread"""
         """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)]
         [reply_thread(self.thread) for p in xrange(32)]
 
 
         # see post link
         # 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())
         response = self.client.get(target_post.get_absolute_url())
         self.assertEqual(response.status_code, 302)
         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.forums.permissions import allow_see_forum, allow_browse_forum
 
 
 from misago.threads.models import Thread, Post
 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']
 __all__ = ['ForumMixin', 'ThreadMixin', 'PostMixin', 'ViewBase']
@@ -126,6 +127,9 @@ class PostMixin(object):
         allow_see_thread(request.user, post.thread)
         allow_see_thread(request.user, post.thread)
         allow_see_forum(request.user, post.forum)
         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):
 class ViewBase(ForumMixin, ThreadMixin, PostMixin, View):
     def process_context(self, request, context):
     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_forum_permissions(request, forum)
         self.check_thread_permissions(request, thread)
         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):
 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):
 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):
 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):
     def dispatch(self, request, *args, **kwargs):
         post = self.fetch_post(
         post = self.fetch_post(
@@ -53,4 +56,7 @@ class GotoPostView(BaseGotoView):
         self.check_thread_permissions(request, thread)
         self.check_thread_permissions(request, thread)
         self.check_post_permissions(request, post)
         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.core.exceptions import PermissionDenied
 from django.utils.translation import ugettext as _
 from django.utils.translation import ugettext as _
 
 
-from misago.threads.permissions import exclude_invisible_posts
 from misago.threads.views.generic.base import ViewBase
 from misago.threads.views.generic.base import ViewBase
 
 
 
 
@@ -34,17 +33,17 @@ class ModeratedPostsListView(ViewBase):
             response.status_code = 405
             response.status_code = 405
             return response
             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, {
         return self.render(request, {
             'forum': forum,
             'forum': forum,
             'thread': thread,
             '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.acl import add_acl
 
 
 from misago.threads import permissions, moderation, goto
 from misago.threads import permissions, moderation, goto
-from misago.threads.permissions import exclude_invisible_posts
 from misago.threads.views.generic.base import ViewBase
 from misago.threads.views.generic.base import ViewBase
 
 
 
 
@@ -52,7 +51,11 @@ class PostView(ViewBase):
             "post views have to override real_dispatch method")
             "post views have to override real_dispatch method")
 
 
     def redirect_to_post(self, user, post):
     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):
 class QuotePostView(PostView):
@@ -115,15 +118,16 @@ class DeletePostView(PostView):
         post.forum.synchronize()
         post.forum.synchronize()
         post.forum.save()
         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:
         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:
         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 = target_post[:1][0]
         target_post.thread.forum = target_post.forum
         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():
                 if formset.is_valid():
                     try:
                     try:
                         formset.save()
                         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:
                     except PostingInterrupt as e:
                         return JsonResponse({'interrupt': e.message})
                         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:
                 else:
                     return JsonResponse({'errors': formset.errors})
                     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.events import add_events_to_posts
 from misago.threads.paginator import paginate
 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.base import ViewBase
 from misago.threads.views.generic.thread.postsactions import PostsActions
 from misago.threads.views.generic.thread.postsactions import PostsActions
 from misago.threads.views.generic.thread.threadactions import ThreadActions
 from misago.threads.views.generic.thread.threadactions import ThreadActions
@@ -29,6 +28,7 @@ class ThreadView(ViewBase):
 
 
     def get_posts(self, user, forum, thread, kwargs):
     def get_posts(self, user, forum, thread, kwargs):
         queryset = self.get_posts_queryset(user, forum, thread)
         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)
         page = paginate(queryset, kwargs.get('page', 0), 10, 3)
 
 
         posts = []
         posts = []
@@ -52,13 +52,12 @@ class ThreadView(ViewBase):
         return page, posts
         return page, posts
 
 
     def get_posts_queryset(self, user, forum, thread):
     def get_posts_queryset(self, user, forum, thread):
-        queryset = thread.post_set.select_related(
+        return thread.post_set.select_related(
             'poster',
             'poster',
             'poster__rank',
             'poster__rank',
             'poster__ban_cache',
             'poster__ban_cache',
             'poster__online_tracker'
             'poster__online_tracker'
-        )
-        return exclude_invisible_posts(queryset, user, forum).order_by('id')
+        ).order_by('id')
 
 
     def dispatch(self, request, *args, **kwargs):
     def dispatch(self, request, *args, **kwargs):
         relations = ['forum', 'starter', 'last_poster', 'first_post']
         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,
 from misago.threads.permissions import (allow_use_private_threads,
                                         allow_see_private_thread,
                                         allow_see_private_thread,
                                         allow_see_private_post,
                                         allow_see_private_post,
-                                        exclude_invisible_private_threads)
+                                        exclude_invisible_private_threads,
+                                        exclude_invisible_private_posts)
 from misago.threads.views import generic
 from misago.threads.views import generic
 
 
 
 
@@ -84,6 +85,9 @@ class PrivateThreadsMixin(object):
         allow_see_private_thread(request.user, post.thread)
         allow_see_private_thread(request.user, post.thread)
         allow_use_private_threads(request.user)
         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):
 class PrivateThreads(generic.Threads):
     def get_queryset(self):
     def get_queryset(self):