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

#410: Dropped reported and moderated goto's for sake of lists

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

+ 1 - 0
misago/static/misago/css/misago/misago.less

@@ -38,3 +38,4 @@
 @import "usercp.less";
 @import "posts.less";
 @import "events.less";
+@import "threadmap.less";

+ 1 - 1
misago/static/misago/css/misago/posts.less

@@ -1,5 +1,5 @@
 //
-// Posts Styles
+// Thread Posts
 // --------------------------------------------------
 
 

+ 78 - 0
misago/static/misago/css/misago/threadmap.less

@@ -0,0 +1,78 @@
+//
+// Thread Map
+// --------------------------------------------------
+
+
+.thread-map {
+  .user-avatar {
+    img {
+      border-radius: @border-radius-small;
+      width: 32px;
+    }
+  }
+
+  .media-heading {
+    background: none;
+    border-bottom: none;
+    margin-bottom: 0px;
+
+    color: @text-muted;
+
+    .user-name {
+      color: @state-default;
+      font-weight: bold;
+    }
+
+    a.user-name {
+      &:link, &:visited {
+        color: @state-hover;
+      }
+
+      &:hover {
+        color: @state-hover;
+        text-decoration: text-underline;
+      }
+
+      &:active, &:focus {
+        color: @state-clicked;
+        text-decoration: text-underline;
+      }
+    }
+
+    .separator {
+      margin-left: @font-size-base / 3;
+      margin-right: @font-size-base / 3;
+    }
+
+    .post-date {
+      &:link, &:visited {
+        color: @state-default;
+      }
+
+      &:hover, &:focus {
+        color: @state-hover;
+        text-decoration: underline;
+      }
+
+      &:active {
+        color: @state-clicked;
+        text-decoration: underline;
+      }
+    }
+  }
+
+  .post-preview {
+    display: block;
+
+    font-size: @font-size-small;
+
+    &:link, &:active, &:visited, &:hover {
+      color: @text-color;
+      text-decoration: none;
+    }
+  }
+
+  .map-tail {
+    margin-bottom: 0px;
+  }
+}

+ 6 - 0
misago/static/misago/js/misago-modal.js

@@ -30,6 +30,12 @@
       });
     }
 
+    this.get = function(url) {
+      $.get(url, function(data) {
+        _this.show(data);
+        Misago.DOM.changed();
+      });
+    }
   };
 
   Misago.Modal = new MisagoModal();

+ 44 - 0
misago/templates/misago/thread/gotolists/list.html

@@ -0,0 +1,44 @@
+{% load i18n misago_avatars %}
+<div class="modal-body thread-map">
+  {% for post in posts %}
+  <div class="media">
+    {% if post.poster_id %}
+    <a class="user-avatar pull-left" href="{% url USER_PROFILE_URL user_slug=post.poster.slug user_id=post.poster.id %}">
+      <img class="media-object" src="{{ post.poster|avatar:32 }}" alt="{% trans "Poster avatar" %}">
+    </a>
+    {% else %}
+    <span class="user-avatar pull-left">
+      <img class="media-object" src="{% blankavatar 32 %}" alt="{% trans "Poster avatar" %}">
+    </span>
+    {% endif %}
+    <div class="media-body">
+      <div class="media-heading">
+        {% if post.poster_id %}
+        <a class="user-name" href="{% url USER_PROFILE_URL user_slug=post.poster.slug user_id=post.poster.id %}">{{ post.poster_name }}</a>
+        {% else %}
+        <strong>{{ post.poster_name }}</strong>
+        {% endif %}
+
+        <span class="separator">&ndash;</span>
+
+        <a href="{{ post.get_absolute_url }}" class="post-date tooltip-top dynamic time-ago" title="{{ post.posted_on }}" data-timestamp="{{ post.posted_on|date:"c" }}">
+          {{ post.posted_on|date }}
+        </a>
+      </div>
+      <a href="{{ post.get_absolute_url }}" class="post-preview">
+        {{ post.short }}
+      </a>
+    </div>
+  </div>
+  {% empty %}
+  <p class="lead text-center map-tail">
+    {% trans "There are no posts to display on this list." %}
+  </p>
+  {% endfor %}
+  {% if posts_count > 15 %}
+  <hr>
+  <p class="lead text-center map-tail">
+    {% trans "This list is limited to last 15 posts." %}
+  </p>
+  {% endif %}
+</div>

+ 13 - 0
misago/templates/misago/thread/gotolists/moderated.html

@@ -0,0 +1,13 @@
+{% load i18n %}
+<div class="modal-header">
+  <button type="button" class="close" data-dismiss="modal"><span aria-hidden="true">&times;</span><span class="sr-only">Close</span></button>
+  <h4 class="modal-title" id="ajaxModalLabel">
+    <span class="fa fa-question-circle fa-fw fa-lg"></span>
+    {% blocktrans trimmed count posts=posts_count %}
+    {{ posts }} unapproved post
+    {% plural %}
+    {{ posts }} unapproved posts
+    {% endblocktrans %}
+  </h4>
+</div>
+{% include "misago/thread/gotolists/list.html" %}

+ 13 - 0
misago/templates/misago/thread/gotolists/reported.html

@@ -0,0 +1,13 @@
+{% load i18n %}
+<div class="modal-header">
+  <button type="button" class="close" data-dismiss="modal"><span aria-hidden="true">&times;</span><span class="sr-only">Close</span></button>
+  <h4 class="modal-title" id="ajaxModalLabel">
+    <span class="fa fa-exclamation-triangle fa-fw fa-lg"></span>
+    {% blocktrans trimmed count posts=posts_count %}
+    {{ posts }} reported post
+    {% plural %}
+    {{ posts }} reported posts
+    {% endblocktrans %}
+  </h4>
+</div>
+{% include "misago/thread/gotolists/list.html" %}

+ 34 - 0
misago/templates/misago/thread/replies.html

@@ -62,6 +62,20 @@
 
       {% include "misago/thread/pagination.html" %}
 
+      {% if thread.acl.can_review and thread.has_moderated_posts %}
+      <button type="button" class="btn btn-default btn-show-moderated">
+        <span class="fa fa-question-circle fa-fw fa-lg"></span>
+        {% trans "Unapproved posts" %}
+      </button>
+      {% endif %}
+
+      {% if thread.acl.can_see_reports and thread.has_reported_posts %}
+      <a href="{{ thread.get_moderated_url }}" class="btn btn-default btn-show-reported">
+        <span class="fa fa-exclamation-triangle fa-fw fa-lg"></span>
+        {% trans "Reported posts" %}
+      </a>
+      {% endif %}
+
       {% if thread_actions %}
       {% include "misago/thread/thread_actions.html" %}
       {% endif %}
@@ -110,6 +124,26 @@
     {% include "misago/thread/events_js.html" %}
   {% endif %}
 
+  {% if thread.acl.can_review and thread.has_moderated_posts %}
+  <script lang="JavaScript">
+    $(function() {
+      $('.btn-show-moderated').click(function() {
+        Misago.Modal.get("{{ thread.get_moderated_url }}");
+      });
+    });
+  </script>
+  {% endif %}
+
+  {% if thread.acl.can_see_reports and thread.has_reported_posts %}
+  <script lang="JavaScript">
+    $(function() {
+      $('.btn-show-reported').click(function() {
+        Misago.Modal.get("{{ thread.get_reported_url }}");
+      });
+    });
+  </script>
+  {% endif %}
+
   {% if thread.acl.can_reply %}
   <script lang="JavaScript">
     $(function() {

+ 0 - 26
misago/threads/goto.py

@@ -94,32 +94,6 @@ def new(user, thread):
     return get_post_link(posts, qs, thread, first_unread)
 
 
-def reported(user, thread):
-    if not thread.has_reported_posts or not thread.acl['can_see_reports']:
-        return last(user, thread)
-
-    posts, qs = posts_queryset(user, thread)
-    try:
-        first_reported = qs.filter(is_reported=True)[:1][0]
-    except IndexError:
-        return last(user, thread)
-
-    return get_post_link(posts, qs, thread, first_reported)
-
-
-def moderated(user, thread):
-    if not thread.has_moderated_posts or not thread.acl['can_review']:
-        return last(user, thread)
-
-    posts, qs = posts_queryset(user, thread)
-    try:
-        first_moderated = qs.filter(is_moderated=True)[:1][0]
-    except IndexError:
-        return last(user, thread)
-
-    return get_post_link(posts, qs, thread, first_moderated)
-
-
 def post(user, thread, post):
     posts, qs = posts_queryset(user, thread)
     return get_post_link(posts, qs, thread, post)

+ 4 - 4
misago/threads/models/thread.py

@@ -136,12 +136,12 @@ class Thread(models.Model):
     def get_new_reply_url(self):
         return self.get_url('new')
 
-    def get_reported_reply_url(self):
-        return self.get_url('reported')
-
-    def get_moderated_reply_url(self):
+    def get_moderated_url(self):
         return self.get_url('moderated')
 
+    def get_reported_url(self):
+        return self.get_url('reported')
+
     def set_title(self, title):
         self.title = title
         self.slug = slugify(title)

+ 0 - 61
misago/threads/tests/test_goto.py

@@ -125,67 +125,6 @@ class GotoTests(AuthenticatedUserTestCase):
         post_link = goto.new(self.user, self.thread)
         self.assertEqual(post_link, goto.last(self.user, self.thread))
 
-    def test_reported(self):
-        """reported returns link to first reported post"""
-        self.thread.acl['can_see_reports'] = True
-
-        # add 24 posts
-        [reply_thread(self.thread) for p in xrange(24)]
-
-        # add reported post
-        reported_post = reply_thread(self.thread, is_reported=True)
-
-        # add 24 posts
-        [reply_thread(self.thread) for p in xrange(24)]
-
-        # assert that there is link to reported post
-        reported_link = goto.reported(self.user, self.thread)
-        post_link = goto.get_post_link(
-            50, self.thread.post_set, self.thread, reported_post)
-        self.assertEqual(reported_link, post_link)
-
-        # lack of permission should lead to last post
-        self.thread.acl['can_see_reports'] = False
-        reported_link = goto.reported(self.user, self.thread)
-        self.assertEqual(reported_link, goto.last(self.user, self.thread))
-
-        # lack of reports in thread should lead to last post
-        self.thread.acl['can_see_reports'] = True
-        self.thread.has_reported_posts = False
-        reported_link = goto.reported(self.user, self.thread)
-        self.assertEqual(reported_link, goto.last(self.user, self.thread))
-
-    def test_moderated(self):
-        """moderated returns link to first moderated post"""
-        self.forum.acl['can_review_moderated_content'] = True
-        self.thread.acl['can_review'] = True
-
-        # add 24 posts
-        [reply_thread(self.thread) for p in xrange(24)]
-
-        # add moderated post
-        moderated_post = reply_thread(self.thread, is_moderated=True)
-
-        # add 24 posts
-        [reply_thread(self.thread) for p in xrange(24)]
-
-        # assert that there is link to moderated post
-        moderated_link = goto.moderated(self.user, self.thread)
-        post_link = goto.get_post_link(
-            50, self.thread.post_set, self.thread, moderated_post)
-        self.assertEqual(moderated_link, post_link)
-
-        # lack of permission should lead to last post
-        self.thread.acl['can_review'] = False
-        moderated_link = goto.moderated(self.user, self.thread)
-        self.assertEqual(moderated_link, goto.last(self.user, self.thread))
-
-        # lack of moderated posts in thread should lead to last post
-        self.thread.acl['can_review'] = True
-        self.thread.has_moderated_posts = False
-        moderated_link = goto.moderated(self.user, self.thread)
-        self.assertEqual(moderated_link, goto.last(self.user, self.thread))
-
     def test_post(self):
         """post returns link to post given"""
         self.assertEqual(

+ 0 - 48
misago/threads/tests/test_goto_views.py

@@ -1,5 +1,4 @@
 from misago.acl import add_acl
-from misago.acl.testutils import override_acl
 from misago.forums.models import Forum
 from misago.users.testutils import AuthenticatedUserTestCase
 
@@ -18,14 +17,6 @@ class GotoViewsTests(AuthenticatedUserTestCase):
         add_acl(self.user, self.forum)
         add_acl(self.user, self.thread)
 
-    def override_acl(self, new_acl):
-        new_acl.update({'can_see': True, 'can_browse': True})
-
-        forums_acl = self.user.acl
-        forums_acl['visible_forums'].append(self.forum.pk)
-        forums_acl['forums'][self.forum.pk].update(new_acl)
-        override_acl(self.user, forums_acl)
-
     def test_goto_last(self):
         """thread_last link points to last post in thread"""
         response = self.client.get(self.thread.get_last_reply_url())
@@ -60,45 +51,6 @@ class GotoViewsTests(AuthenticatedUserTestCase):
         self.assertEqual(response.status_code, 302)
         self.assertTrue(response['location'].endswith(unread_post_link))
 
-    def test_goto_reported(self):
-        """thread_reported link points to first reported post in thread"""
-        # add 32 posts to thread
-        [reply_thread(self.thread) for p in xrange(32)]
-
-        # add reported post to thread
-        reported_post = reply_thread(self.thread, is_reported=True)
-
-        # add 32 more posts to thread
-        [reply_thread(self.thread) for p in xrange(32)]
-
-        # see reported post link
-        self.override_acl({'can_see_reports': 1})
-        reported_post_link = goto.post(self.user, self.thread, reported_post)
-
-        self.override_acl({'can_see_reports': 1})
-        response = self.client.get(self.thread.get_reported_reply_url())
-        self.assertEqual(response.status_code, 302)
-        self.assertTrue(response['location'].endswith(reported_post_link))
-
-    def test_goto_moderated(self):
-        """thread_moderated link points to first moderated post in thread"""
-        # add 32 posts to thread
-        [reply_thread(self.thread) for p in xrange(32)]
-
-        # add moderated post to thread
-        moderated_post = reply_thread(self.thread, is_moderated=True)
-
-        # add 32 more posts to thread
-        [reply_thread(self.thread) for p in xrange(32)]
-
-        # see moderated post link
-        self.override_acl({'can_review_moderated_content': 1})
-        moderated_post_link = goto.post(self.user, self.thread, moderated_post)
-
-        response = self.client.get(self.thread.get_moderated_reply_url())
-        self.assertEqual(response.status_code, 302)
-        self.assertTrue(response['location'].endswith(moderated_post_link))
-
     def test_goto_post(self):
         """thread_post link points to specific post in thread"""
         # add 32 posts to thread

+ 145 - 0
misago/threads/tests/test_gotolists_views.py

@@ -0,0 +1,145 @@
+from misago.acl import add_acl
+from misago.acl.testutils import override_acl
+from misago.forums.models import Forum
+from misago.users.testutils import AuthenticatedUserTestCase
+
+from misago.threads.testutils import post_thread, reply_thread
+
+
+class GotoListsViewsTests(AuthenticatedUserTestCase):
+    ajax_header = {'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'}
+
+    def setUp(self):
+        super(GotoListsViewsTests, self).setUp()
+
+        self.forum = Forum.objects.all_forums().filter(role="forum")[:1][0]
+        self.forum.labels = []
+
+        self.thread = post_thread(self.forum)
+
+    def override_acl(self, new_acl):
+        new_acl.update({
+            'can_browse': True,
+            'can_see': True,
+            'can_see_all_threads': True,
+        })
+
+        forums_acl = self.user.acl
+        forums_acl['visible_forums'].append(self.forum.pk)
+        forums_acl['forums'][self.forum.pk] = new_acl
+        override_acl(self.user, forums_acl)
+
+        self.forum.acl = {}
+        add_acl(self.user, self.forum)
+
+    def test_moderated_list(self):
+        """moderated posts list works"""
+        self.override_acl({'can_review_moderated_content': True})
+        response = self.client.get(self.thread.get_moderated_url(),
+                                   **self.ajax_header)
+        self.assertEqual(response.status_code, 200)
+        self.assertIn("0 unapproved posts", response.content)
+        self.assertIn("There are no posts to display on this list.",
+                      response.content)
+
+        # post 10 not moderated posts
+        [reply_thread(self.thread) for i in xrange(10)]
+
+        # assert that posts don't show
+        self.override_acl({'can_review_moderated_content': True})
+        response = self.client.get(self.thread.get_moderated_url(),
+                                   **self.ajax_header)
+        self.assertEqual(response.status_code, 200)
+        self.assertIn("0 unapproved posts", response.content)
+        self.assertIn("There are no posts to display on this list.",
+                      response.content)
+
+        # post 10 reported posts
+        posts = []
+        for i in xrange(10):
+            posts.append(reply_thread(self.thread, is_moderated=True))
+
+        # assert that posts show
+        self.override_acl({'can_review_moderated_content': True})
+        response = self.client.get(self.thread.get_moderated_url(),
+                                   **self.ajax_header)
+        self.assertEqual(response.status_code, 200)
+        self.assertIn("10 unapproved posts", response.content)
+        self.assertNotIn("There are no posts to display on this list.",
+                         response.content)
+
+        for post in posts:
+            self.assertIn(post.get_absolute_url(), response.content)
+
+        # overflow list via posting extra 20 reported posts
+        posts = []
+        for i in xrange(20):
+            posts.append(reply_thread(self.thread, is_moderated=True))
+
+        # assert that posts don't show
+        self.override_acl({'can_review_moderated_content': True})
+        response = self.client.get(self.thread.get_moderated_url(),
+                                   **self.ajax_header)
+        self.assertEqual(response.status_code, 200)
+        self.assertIn("30 unapproved posts", response.content)
+        self.assertIn("This list is limited to last 15 posts.",
+                      response.content)
+
+        for post in posts[15:]:
+            self.assertIn(post.get_absolute_url(), response.content)
+
+    def test_reported_list(self):
+        """reported posts list works"""
+        self.override_acl({'can_see_reports': True})
+        response = self.client.get(self.thread.get_reported_url(),
+                                   **self.ajax_header)
+        self.assertEqual(response.status_code, 200)
+        self.assertIn("0 reported posts", response.content)
+        self.assertIn("There are no posts to display on this list.",
+                      response.content)
+
+        # post 10 not reported posts
+        [reply_thread(self.thread) for i in xrange(10)]
+
+        # assert that posts don't show
+        self.override_acl({'can_see_reports': True})
+        response = self.client.get(self.thread.get_reported_url(),
+                                   **self.ajax_header)
+        self.assertEqual(response.status_code, 200)
+        self.assertIn("0 reported posts", response.content)
+        self.assertIn("There are no posts to display on this list.",
+                      response.content)
+
+        # post 10 reported posts
+        posts = []
+        for i in xrange(10):
+            posts.append(reply_thread(self.thread, is_reported=True))
+
+        # assert that posts show
+        self.override_acl({'can_see_reports': True})
+        response = self.client.get(self.thread.get_reported_url(),
+                                   **self.ajax_header)
+        self.assertEqual(response.status_code, 200)
+        self.assertIn("10 reported posts", response.content)
+        self.assertNotIn("There are no posts to display on this list.",
+                         response.content)
+
+        for post in posts:
+            self.assertIn(post.get_absolute_url(), response.content)
+
+        # overflow list via posting extra 20 reported posts
+        posts = []
+        for i in xrange(20):
+            posts.append(reply_thread(self.thread, is_reported=True))
+
+        # assert that posts don't show
+        self.override_acl({'can_see_reports': True})
+        response = self.client.get(self.thread.get_reported_url(),
+                                   **self.ajax_header)
+        self.assertEqual(response.status_code, 200)
+        self.assertIn("30 reported posts", response.content)
+        self.assertIn("This list is limited to last 15 posts.",
+                      response.content)
+
+        for post in posts[15:]:
+            self.assertIn(post.get_absolute_url(), response.content)

+ 9 - 4
misago/threads/urls.py

@@ -15,19 +15,24 @@ urlpatterns = patterns('',
 
 
 from misago.threads.views.threads import (ThreadView, GotoLastView,
-                                          GotoNewView, GotoReportedView,
-                                          GotoModeratedView, GotoPostView)
+                                          GotoNewView, GotoPostView)
 urlpatterns += patterns('',
     url(r'^thread/(?P<thread_slug>[\w\d-]+)-(?P<thread_id>\d+)/$', ThreadView.as_view(), name='thread'),
     url(r'^thread/(?P<thread_slug>[\w\d-]+)-(?P<thread_id>\d+)/(?P<page>\d+)/$', ThreadView.as_view(), name='thread'),
     url(r'^thread/(?P<thread_slug>[\w\d-]+)-(?P<thread_id>\d+)/last/$', GotoLastView.as_view(), name='thread_last'),
     url(r'^thread/(?P<thread_slug>[\w\d-]+)-(?P<thread_id>\d+)/new/$', GotoNewView.as_view(), name='thread_new'),
-    url(r'^thread/(?P<thread_slug>[\w\d-]+)-(?P<thread_id>\d+)/reported/$', GotoReportedView.as_view(), name='thread_reported'),
-    url(r'^thread/(?P<thread_slug>[\w\d-]+)-(?P<thread_id>\d+)/moderated/$', GotoModeratedView.as_view(), name='thread_moderated'),
     url(r'^thread/(?P<thread_slug>[\w\d-]+)-(?P<thread_id>\d+)/post-(?P<post_id>\d+)/$', GotoPostView.as_view(), name='thread_post'),
 )
 
 
+from misago.threads.views.threads import (ModeratedPostsListView,
+                                          ReportedPostsListView)
+urlpatterns += patterns('',
+    url(r'^thread/(?P<thread_slug>[\w\d-]+)-(?P<thread_id>\d+)/moderation-queue/$', ModeratedPostsListView.as_view(), name='thread_moderated'),
+    url(r'^thread/(?P<thread_slug>[\w\d-]+)-(?P<thread_id>\d+)/reported-posts/$', ReportedPostsListView.as_view(), name='thread_reported'),
+)
+
+
 from misago.threads.views.threads import StartThreadView, ReplyView, EditView
 urlpatterns += patterns('',
     url(r'^start-thread/(?P<forum_id>\d+)/$', StartThreadView.as_view(), name='start_thread'),

+ 1 - 0
misago/threads/views/generic/__init__.py

@@ -1,6 +1,7 @@
 # flake8: noqa
 from misago.threads.views.generic.base import *
 from misago.threads.views.generic.goto import *
+from misago.threads.views.generic.gotopostslist import *
 from misago.threads.views.generic.post import *
 from misago.threads.views.generic.posting import *
 from misago.threads.views.generic.thread import *

+ 0 - 12
misago/threads/views/generic/goto.py

@@ -9,8 +9,6 @@ __all__ = [
     'BaseGotoView',
     'GotoLastView',
     'GotoNewView',
-    'GotoReportedView',
-    'GotoModeratedView',
     'GotoPostView'
 ]
 
@@ -40,16 +38,6 @@ class GotoNewView(BaseGotoView):
         return goto.new(user, thread)
 
 
-class GotoReportedView(BaseGotoView):
-    def get_redirect(self, user, thread):
-        return goto.reported(user, thread)
-
-
-class GotoModeratedView(BaseGotoView):
-    def get_redirect(self, user, thread):
-        return goto.moderated(user, thread)
-
-
 class GotoPostView(BaseGotoView):
     def get_redirect(self, user, thread, post):
         return goto.post(user, thread, post)

+ 60 - 0
misago/threads/views/generic/gotopostslist.py

@@ -0,0 +1,60 @@
+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
+
+
+__all__ = ['ModeratedPostsListView', 'ReportedPostsListView']
+
+
+class ModeratedPostsListView(ViewBase):
+    template = 'misago/thread/gotolists/moderated.html'
+
+    def allow_action(self, thread):
+        if not thread.acl['can_review']:
+            message = _("You don't have permission to review moderated posts.")
+            raise PermissionDenied(message)
+
+    def filter_posts_queryset(self, queryset):
+        return queryset.filter(is_moderated=True)
+
+    def dispatch(self, request, *args, **kwargs):
+        relations = ['forum']
+        thread = self.fetch_thread(request, select_related=relations, **kwargs)
+        forum = thread.forum
+
+        self.check_forum_permissions(request, forum)
+        self.check_thread_permissions(request, thread)
+
+        self.allow_action(thread)
+
+        if not request.is_ajax():
+            response = render(request, 'misago/errorpages/wrong_way.html')
+            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]
+
+        return self.render(request, {
+            'forum': forum,
+            'thread': thread,
+
+            'posts_count': queryset.count(),
+            'posts': final_queryset.iterator()
+        })
+
+
+class ReportedPostsListView(ModeratedPostsListView):
+    template = 'misago/thread/gotolists/reported.html'
+
+    def allow_action(self, thread):
+        if not thread.acl['can_see_reports']:
+            message = _("You don't have permission to see reports.")
+            raise PermissionDenied(message)
+
+    def filter_posts_queryset(self, queryset):
+        return queryset.filter(is_reported=True)

+ 1 - 2
misago/threads/views/generic/posting.py

@@ -1,9 +1,8 @@
 from django.contrib import messages
-from django.db.models import Q
 from django.db.transaction import atomic
 from django.http import JsonResponse
 from django.shortcuts import redirect, render
-from django.utils.translation import ugettext_lazy, ugettext as _
+from django.utils.translation import ugettext as _
 from django.views.generic import View
 
 from misago.core.exceptions import AjaxError

+ 49 - 0
misago/threads/views/generic/thread/importantposts.py

@@ -0,0 +1,49 @@
+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
+
+
+__all__ = ['ModeratedPostsView', 'ReportedPostsView']
+
+
+class ModeratedPostsView(ViewBase):
+    template = ''
+
+    def allow_action(self, forum):
+        pass
+
+    def filter_posts_queryset(self, queryset):
+        return queryset.filter(is_moderated=True)
+
+    def dispatch(self, request, *args, **kwargs):
+        relations = ['forum']
+        thread = self.fetch_thread(request, select_related=relations, **kwargs)
+        forum = thread.forum
+
+        self.check_forum_permissions(request, forum)
+        self.check_thread_permissions(request, thread)
+
+        self.allow_action(forum)
+
+        if not request.is_ajax():
+            response = render(request, 'misago/errorpages/wrong_way.html')
+            response.status_code = 405
+            return response
+
+        queryset = exclude_invisible_posts(
+            thread.post_set, request.user, forum)
+        queryset = self.filter_posts_queryset(queryset)
+
+        return self.render(request, {
+            'forum': forum,
+            'thread': get_forum_path(forum),
+
+            'posts_count': queryset.count(),
+            'posts': queryset.order_by('-id')[:15]
+        })
+
+
+class ReportedPostsView(ModeratedPostsView):
+    pass

+ 1 - 2
misago/threads/views/generic/thread/view.py

@@ -1,6 +1,5 @@
 from django.core.exceptions import PermissionDenied
-from django.db.models import Q
-from django.utils.translation import ungettext, ugettext_lazy, ugettext as _
+from django.utils.translation import ugettext as _
 
 from misago.acl import add_acl
 from misago.core.shortcuts import validate_slug

+ 4 - 4
misago/threads/views/threads.py

@@ -13,19 +13,19 @@ class ThreadView(ThreadsMixin, generic.ThreadView):
     pass
 
 
-class GotoLastView(ThreadsMixin, generic.GotoLastView):
+class ModeratedPostsListView(ThreadsMixin, generic.ModeratedPostsListView):
     pass
 
 
-class GotoNewView(ThreadsMixin, generic.GotoNewView):
+class ReportedPostsListView(ThreadsMixin, generic.ReportedPostsListView):
     pass
 
 
-class GotoReportedView(ThreadsMixin, generic.GotoReportedView):
+class GotoLastView(ThreadsMixin, generic.GotoLastView):
     pass
 
 
-class GotoModeratedView(ThreadsMixin, generic.GotoModeratedView):
+class GotoNewView(ThreadsMixin, generic.GotoNewView):
     pass