Browse Source

Posts reporting forum type done. Still needs own templates before isse can be closed. #35

Ralfp 12 years ago
parent
commit
bcb09df2ee
40 changed files with 2140 additions and 57 deletions
  1. 1 0
      cron.txt
  2. 4 0
      misago/acl/permissions/reports.py
  3. 2 2
      misago/acl/permissions/threads.py
  4. 6 2
      misago/apps/privatethreads/jumps.py
  5. 3 0
      misago/apps/privatethreads/urls.py
  6. 15 0
      misago/apps/reports/changelog.py
  7. 37 0
      misago/apps/reports/delete.py
  8. 5 0
      misago/apps/reports/details.py
  9. 29 0
      misago/apps/reports/jumps.py
  10. 91 0
      misago/apps/reports/list.py
  11. 1 1
      misago/apps/reports/mixins.py
  12. 24 0
      misago/apps/reports/posting.py
  13. 54 0
      misago/apps/reports/thread.py
  14. 29 3
      misago/apps/reports/urls.py
  15. 8 0
      misago/apps/threads/jumps.py
  16. 2 0
      misago/apps/threads/urls.py
  17. 4 3
      misago/apps/threadtype/base.py
  18. 116 1
      misago/apps/threadtype/jumps.py
  19. 63 27
      misago/apps/threadtype/list/moderation.py
  20. 1 9
      misago/apps/threadtype/posting/newreply.py
  21. 29 1
      misago/apps/threadtype/thread/moderation/thread.py
  22. 9 0
      misago/fixtures/reportsmonitor.py
  23. 13 0
      misago/management/commands/countreports.py
  24. 397 0
      misago/migrations/0008_auto__add_field_thread_report_for.py
  25. 18 1
      misago/models/postmodel.py
  26. 28 1
      misago/models/threadmodel.py
  27. 1 1
      misago/urls.py
  28. 1 0
      static/cranefly/css/cranefly.css
  29. 6 0
      static/cranefly/css/cranefly/thread.less
  30. 27 0
      static/cranefly/js/cranefly.js
  31. 3 0
      templates/cranefly/layout.html
  32. 1 1
      templates/cranefly/macros.html
  33. 12 1
      templates/cranefly/private_threads/thread.html
  34. 81 0
      templates/cranefly/reports/changelog.html
  35. 95 0
      templates/cranefly/reports/changelog_diff.html
  36. 35 0
      templates/cranefly/reports/details.html
  37. 171 0
      templates/cranefly/reports/list.html
  38. 183 0
      templates/cranefly/reports/posting.html
  39. 522 0
      templates/cranefly/reports/thread.html
  40. 13 3
      templates/cranefly/threads/thread.html

+ 1 - 0
cron.txt

@@ -8,5 +8,6 @@
 5 3 * * * python $HOME/misago/manage.py syncdeltas
 10 3 * * * python $HOME/misago/manage.py updateranking
 25 3 * * * python $HOME/misago/manage.py updatethreadranking
+*/30 * * * * python $HOME/misago/heartbeat.py countreports
 # Uncomment next line for heartbeat cron
 #*/3 * * * * python $HOME/misago/heartbeat.py deployment.settings --log=heartbeats.txt

+ 4 - 0
misago/acl/permissions/reports.py

@@ -27,6 +27,10 @@ class ReportsACL(BaseACL):
     def can_report(self):
         return self.acl['can_report_content']
 
+    def allow_report(self):
+        if not self.acl['can_report_content']:
+            raise ACLError403(_("You don't have permission to report posts."))
+
     def can_handle(self):
         return self.acl['can_handle_reports']
         

+ 2 - 2
misago/acl/permissions/threads.py

@@ -371,9 +371,9 @@ class ThreadsACL(BaseACL):
         except KeyError:
             return False
 
-    def can_mod_posts(self, thread):
+    def can_mod_posts(self, forum):
         try:
-            forum_role = self.acl[thread.forum.pk]
+            forum_role = self.acl[forum.pk]
             return (
                     forum_role['can_edit_threads_posts']
                     or forum_role['can_move_threads_posts']

+ 6 - 2
misago/apps/privatethreads/jumps.py

@@ -37,11 +37,15 @@ class UnwatchEmailThreadView(UnwatchEmailThreadBaseView, TypeMixin):
     pass
 
 
-class UpvotePostView(UpvotePostBaseView, TypeMixin):
+class FirstReportedView(FirstReportedBaseView, TypeMixin):
     pass
 
 
-class DownvotePostView(DownvotePostBaseView, TypeMixin):
+class ReportPostView(ReportPostBaseView, TypeMixin):
+    pass
+
+
+class ShowPostReportView(ShowPostReportBaseView, TypeMixin):
     pass
 
 

+ 3 - 0
misago/apps/privatethreads/urls.py

@@ -14,7 +14,10 @@ urlpatterns = patterns('misago.apps.privatethreads',
     url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/last/$', 'jumps.LastReplyView', name="private_thread_last"),
     url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/find-(?P<post>\d+)/$', 'jumps.FindReplyView', name="private_thread_find"),
     url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/new/$', 'jumps.NewReplyView', name="private_thread_new"),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/reported/$', 'jumps.FirstReportedView', name="private_thread_reported"),
     url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/show-hidden/$', 'jumps.ShowHiddenRepliesView', name="private_thread_show_hidden"),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/report/$', 'jumps.ReportPostView', name="private_post_report"),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/show-report/$', 'jumps.ReportPostView', name="private_post_report_show"),
     url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/watch/$', 'jumps.WatchThreadView', name="private_thread_watch"),
     url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/watch/email/$', 'jumps.WatchEmailThreadView', name="private_thread_watch_email"),
     url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/unwatch/$', 'jumps.UnwatchThreadView', name="private_thread_unwatch"),

+ 15 - 0
misago/apps/reports/changelog.py

@@ -0,0 +1,15 @@
+from misago.apps.threadtype.changelog import (ChangelogChangesBaseView,
+                                              ChangelogDiffBaseView,
+                                              ChangelogRevertBaseView)
+from misago.apps.reports.mixins import TypeMixin
+
+class ChangelogView(ChangelogChangesBaseView, TypeMixin):
+    pass
+
+
+class ChangelogDiffView(ChangelogDiffBaseView, TypeMixin):
+    pass
+
+
+class ChangelogRevertView(ChangelogRevertBaseView, TypeMixin):
+    pass

+ 37 - 0
misago/apps/reports/delete.py

@@ -0,0 +1,37 @@
+from misago.apps.threadtype.delete import *
+from misago.apps.reports.mixins import TypeMixin
+
+class DeleteThreadView(DeleteThreadBaseView, TypeMixin):
+    pass
+
+
+class HideThreadView(HideThreadBaseView, TypeMixin):
+    pass
+
+
+class ShowThreadView(ShowThreadBaseView, TypeMixin):
+    pass
+
+
+class DeleteReplyView(DeleteReplyBaseView, TypeMixin):
+    pass
+
+
+class HideReplyView(HideReplyBaseView, TypeMixin):
+    pass
+
+
+class ShowReplyView(ShowReplyBaseView, TypeMixin):
+    pass
+
+
+class DeleteCheckpointView(DeleteCheckpointBaseView, TypeMixin):
+    pass
+
+
+class HideCheckpointView(HideCheckpointBaseView, TypeMixin):
+    pass
+
+
+class ShowCheckpointView(ShowCheckpointBaseView, TypeMixin):
+    pass

+ 5 - 0
misago/apps/reports/details.py

@@ -0,0 +1,5 @@
+from misago.apps.threadtype.details import DetailsBaseView, KarmaVotesBaseView
+from misago.apps.reports.mixins import TypeMixin
+
+class DetailsView(DetailsBaseView, TypeMixin):
+    pass

+ 29 - 0
misago/apps/reports/jumps.py

@@ -0,0 +1,29 @@
+from misago.apps.threadtype.jumps import *
+from misago.apps.reports.mixins import TypeMixin
+
+class LastReplyView(LastReplyBaseView, TypeMixin):
+    pass
+
+
+class FindReplyView(FindReplyBaseView, TypeMixin):
+    pass
+
+
+class NewReplyView(NewReplyBaseView, TypeMixin):
+    pass
+
+
+class WatchThreadView(WatchThreadBaseView, TypeMixin):
+    pass
+
+
+class WatchEmailThreadView(WatchEmailThreadBaseView, TypeMixin):
+    pass
+
+
+class UnwatchThreadView(UnwatchThreadBaseView, TypeMixin):
+    pass
+
+
+class UnwatchEmailThreadView(UnwatchEmailThreadBaseView, TypeMixin):
+    pass

+ 91 - 0
misago/apps/reports/list.py

@@ -0,0 +1,91 @@
+from itertools import chain
+from django.core.urlresolvers import reverse
+from django.http import Http404
+from django.shortcuts import redirect
+from django.utils.translation import ugettext as _
+from misago.apps.threadtype.list import ThreadsListBaseView, ThreadsListModeration
+from misago.messages import Message
+from misago.models import Forum, Thread
+from misago.readstrackers import ThreadsTracker
+from misago.utils.pagination import make_pagination
+from misago.apps.reports.mixins import TypeMixin
+
+class ThreadsListView(ThreadsListBaseView, ThreadsListModeration, TypeMixin):
+    def fetch_forum(self):
+        self.forum = Forum.objects.get(special='reports')
+
+    def threads_queryset(self):
+        announcements = self.forum.thread_set.filter(weight=2).order_by('-pk')
+        threads = self.forum.thread_set.filter(weight__lt=2).order_by('-weight', '-last')
+
+        # Add in first and last poster
+        if self.request.settings.avatars_on_threads_list:
+            announcements = announcements.prefetch_related('start_poster', 'last_poster')
+            threads = threads.prefetch_related('start_poster', 'last_poster')
+
+        return announcements, threads
+
+    def fetch_threads(self):
+        qs_announcements, qs_threads = self.threads_queryset()
+        self.count = qs_threads.count()
+
+        try:
+            self.pagination = make_pagination(self.kwargs.get('page', 0), self.count, self.request.settings.threads_per_page)
+        except Http404:
+            return self.threads_list_redirect()
+
+        tracker_forum = ThreadsTracker(self.request, self.forum)
+        unresolved_count = 0
+        for thread in list(chain(qs_announcements, qs_threads[self.pagination['start']:self.pagination['stop']])):
+            if thread.weight == 2:
+                unresolved_count += 1
+            thread.is_read = tracker_forum.is_read(thread)
+            self.threads.append(thread)
+
+        if int(self.request.monitor['reported_posts']) != unresolved_count:
+            self.request.monitor['reported_posts'] = unresolved_count
+
+    def threads_actions(self):
+        acl = self.request.acl.threads.get_role(self.forum)
+        actions = []
+        try:
+            actions.append(('sticky', _('Change to resolved')))
+            actions.append(('normal', _('Change to bogus')))
+            if acl['can_delete_threads']:
+                actions.append(('undelete', _('Restore threads')))
+                actions.append(('soft', _('Hide threads')))
+            if acl['can_delete_threads'] == 2:
+                actions.append(('hard', _('Delete threads')))
+        except KeyError:
+            pass
+        return actions
+
+    def action_sticky(self, ids):
+        if self._action_sticky(ids):
+            self.request.messages.set_flash(Message(_('Selected reports were set as resolved.')), 'success', 'threads')
+        else:
+            self.request.messages.set_flash(Message(_('No reports were set as resolved.')), 'info', 'threads')
+
+    def action_normal(self, ids):
+        if self._action_normal(ids):
+            self.request.messages.set_flash(Message(_('Selected reports were set as bogus.')), 'success', 'threads')
+        else:
+            self.request.messages.set_flash(Message(_('No reports were set as bogus.')), 'info', 'threads')
+
+    def action_undelete(self, ids):
+        if self._action_undelete(ids):
+            self.request.messages.set_flash(Message(_('Selected reports have been restored.')), 'success', 'threads')
+        else:
+            self.request.messages.set_flash(Message(_('No reports were restored.')), 'info', 'threads')
+
+    def action_soft(self, ids):
+        if self._action_soft(ids):
+            self.request.messages.set_flash(Message(_('Selected reports have been hidden.')), 'success', 'threads')
+        else:
+            self.request.messages.set_flash(Message(_('No reports were hidden.')), 'info', 'threads')
+
+    def action_hard(self, ids):
+        if self._action_hard(ids):
+            self.request.messages.set_flash(Message(_('Selected reports have been deleted.')), 'success', 'threads')
+        else:
+            self.request.messages.set_flash(Message(_('No reports were deleted.')), 'info', 'threads')

+ 1 - 1
misago/apps/reports/mixins.py

@@ -1,2 +1,2 @@
 class TypeMixin(object):
-    templates_prefix = 'reports'
+    type_prefix = 'report'

+ 24 - 0
misago/apps/reports/posting.py

@@ -0,0 +1,24 @@
+from django.core.urlresolvers import reverse
+from django.shortcuts import redirect
+from django.utils.translation import ugettext as _
+from misago.apps.threadtype.posting import NewThreadBaseView, EditThreadBaseView, NewReplyBaseView, EditReplyBaseView
+from misago.messages import Message
+from misago.models import Forum, Thread, Post
+from misago.apps.reports.mixins import TypeMixin
+
+class EditThreadView(EditThreadBaseView, TypeMixin):
+    def response(self):
+        self.request.messages.set_flash(Message(_("Report has been edited.")), 'success', 'threads_%s' % self.post.pk)
+        return redirect(reverse('thread', kwargs={'thread': self.thread.pk, 'slug': self.thread.slug}) + ('#post-%s' % self.post.pk))
+
+
+class NewReplyView(NewReplyBaseView, TypeMixin):
+    def response(self):
+        self.request.messages.set_flash(Message(_("Your reply has been posted.")), 'success', 'threads_%s' % self.post.pk)
+        return self.redirect_to_post(self.post)
+
+
+class EditReplyView(EditReplyBaseView, TypeMixin):
+    def response(self):
+        self.request.messages.set_flash(Message(_("Your reply has been changed.")), 'success', 'threads_%s' % self.post.pk)
+        return self.redirect_to_post(self.post)

+ 54 - 0
misago/apps/reports/thread.py

@@ -0,0 +1,54 @@
+from django.utils.translation import ugettext as _
+from misago.apps.threadtype.thread import ThreadBaseView, ThreadModeration, PostsModeration
+from misago.messages import Message
+from misago.models import Forum, Thread
+from misago.apps.reports.mixins import TypeMixin
+
+class ThreadView(ThreadBaseView, ThreadModeration, PostsModeration, TypeMixin):
+    def posts_actions(self):
+        acl = self.request.acl.threads.get_role(self.thread.forum_id)
+        actions = []
+        try:
+            if acl['can_delete_posts']:
+                if self.thread.replies_deleted > 0:
+                    actions.append(('undelete', _('Restore posts')))
+                actions.append(('soft', _('Hide posts')))
+            if acl['can_delete_posts'] == 2:
+                actions.append(('hard', _('Delete posts')))
+        except KeyError:
+            pass
+        return actions
+
+    def thread_actions(self):
+        acl = self.request.acl.threads.get_role(self.thread.forum_id)
+        actions = []
+        try:
+            if self.thread.weight != 1:
+                actions.append(('sticky', _('Change to resolved')))
+            if self.thread.weight != 0:
+                actions.append(('normal', _('Change to bogus')))
+            if acl['can_delete_threads']:
+                if self.thread.deleted:
+                    actions.append(('undelete', _('Restore this report')))
+                else:
+                    actions.append(('soft', _('Hide this report')))
+            if acl['can_delete_threads'] == 2:
+                actions.append(('hard', _('Delete this report')))
+        except KeyError:
+            pass
+        return actions
+
+    def after_thread_action_sticky(self):
+        self.request.messages.set_flash(Message(_('Report has been set as resolved.')), 'success', 'threads')
+
+    def after_thread_action_normal(self):
+        self.request.messages.set_flash(Message(_('Report has been set as bogus.')), 'success', 'threads')
+
+    def after_thread_action_undelete(self):
+        self.request.messages.set_flash(Message(_('Report has been restored.')), 'success', 'threads')
+
+    def after_thread_action_soft(self):
+        self.request.messages.set_flash(Message(_('Report has been hidden.')), 'success', 'threads')
+
+    def after_thread_action_hard(self):
+        self.request.messages.set_flash(Message(_('Report "%(thread)s" has been deleted.') % {'thread': self.thread.name}), 'success', 'threads')

+ 29 - 3
misago/apps/reports/urls.py

@@ -1,6 +1,32 @@
 from django.conf.urls import patterns, url
 
-urlpatterns = patterns('misago.apps.reports.views',
-    url(r'^forum/(?P<slug>(\w|-)+)-(?P<forum>\d+)/$', 'ThreadsListView', name="reports"),
-    url(r'^forum/(?P<slug>(\w|-)+)-(?P<forum>\d+)/(?P<page>[1-9]([0-9]+)?)/$', 'ThreadsView', name="reports"),
+urlpatterns = patterns('misago.apps.reports',
+    url(r'^$', 'list.ThreadsListView', name="reports"),
+    url(r'^(?P<page>[1-9]([0-9]+)?)/$', 'list.ThreadsListView', name="reports"),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/edit/$', 'posting.EditThreadView', name="report_edit"),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/reply/$', 'posting.NewReplyView', name="report_reply"),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<quote>\d+)/reply/$', 'posting.NewReplyView', name="report_reply"),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/edit/$', 'posting.EditReplyView', name="report_post_edit"),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/$', 'thread.ThreadView', name="report"),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<page>[1-9]([0-9]+)?)/$', 'thread.ThreadView', name="report"),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/last/$', 'jumps.LastReplyView', name="report_last"),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/find-(?P<post>\d+)/$', 'jumps.FindReplyView', name="report_find"),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/new/$', 'jumps.NewReplyView', name="report_new"),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/watch/$', 'jumps.WatchThreadView', name="report_watch"),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/watch/email/$', 'jumps.WatchEmailThreadView', name="report_watch_email"),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/unwatch/$', 'jumps.UnwatchThreadView', name="report_unwatch"),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/unwatch/email/$', 'jumps.UnwatchEmailThreadView', name="report_unwatch_email"),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/delete/$', 'delete.DeleteThreadView', name="report_delete"),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/hide/$', 'delete.HideThreadView', name="report_hide"),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/show/$', 'delete.ShowThreadView', name="report_show"),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/delete/$', 'delete.DeleteReplyView', name="report_post_delete"),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/hide/$', 'delete.HideReplyView', name="report_post_hide"),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/show/$', 'delete.ShowReplyView', name="report_post_show"),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/(?P<checkpoint>\d+)/delete/$', 'delete.DeleteCheckpointView', name="report_post_checkpoint_delete"),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/(?P<checkpoint>\d+)/hide/$', 'delete.HideCheckpointView', name="report_post_checkpoint_hide"),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/(?P<checkpoint>\d+)/show/$', 'delete.ShowCheckpointView', name="report_post_checkpoint_show"),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/info/$', 'details.DetailsView', name="report_post_info"),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/changelog/$', 'changelog.ChangelogView', name="report_changelog"),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/changelog/(?P<change>\d+)/$', 'changelog.ChangelogDiffView', name="report_changelog_diff"),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/changelog/(?P<change>\d+)/revert/$', 'changelog.ChangelogRevertView', name="report_changelog_revert"),
 )

+ 8 - 0
misago/apps/threads/jumps.py

@@ -47,3 +47,11 @@ class UpvotePostView(UpvotePostBaseView, TypeMixin):
 
 class DownvotePostView(DownvotePostBaseView, TypeMixin):
     pass
+
+
+class ReportPostView(ReportPostBaseView, TypeMixin):
+    pass
+
+
+class ShowPostReportView(ShowPostReportBaseView, TypeMixin):
+    pass

+ 2 - 0
misago/apps/threads/urls.py

@@ -22,6 +22,8 @@ urlpatterns = patterns('misago.apps.threads',
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/unwatch/email/$', 'jumps.UnwatchEmailThreadView', name="thread_unwatch_email"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/upvote/$', 'jumps.UpvotePostView', name="post_upvote"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/downvote/$', 'jumps.DownvotePostView', name="post_downvote"),
+    url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/report/$', 'jumps.ReportPostView', name="post_report"),
+    url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/show-report/$', 'jumps.ReportPostView', name="post_report_show"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/delete/$', 'delete.DeleteThreadView', name="thread_delete"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/hide/$', 'delete.HideThreadView', name="thread_hide"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/show/$', 'delete.ShowThreadView', name="thread_show"),

+ 4 - 3
misago/apps/threadtype/base.py

@@ -48,12 +48,13 @@ class ViewBase(object):
         except AttributeError:
             pass
 
-    def redirect_to_post(self, post):
+    def redirect_to_post(self, post, type_prefix=None):
+        type_prefix = type_prefix or self.type_prefix
         queryset = self.request.acl.threads.filter_posts(self.request, self.thread, self.thread.post_set)
         page = page_number(queryset.filter(id__lte=post.pk).count(), queryset.count(), self.request.settings.posts_per_page)
         if page > 1:
-            return redirect(reverse(self.type_prefix, kwargs={'thread': self.thread.pk, 'slug': self.thread.slug, 'page': page}) + ('#post-%s' % post.pk))
-        return redirect(reverse(self.type_prefix, kwargs={'thread': self.thread.pk, 'slug': self.thread.slug}) + ('#post-%s' % post.pk))
+            return redirect(reverse(type_prefix, kwargs={'thread': self.thread.pk, 'slug': self.thread.slug, 'page': page}) + ('#post-%s' % post.pk))
+        return redirect(reverse(type_prefix, kwargs={'thread': self.thread.pk, 'slug': self.thread.slug}) + ('#post-%s' % post.pk))
 
     def template_vars(self, context):
         return context

+ 116 - 1
misago/apps/threadtype/jumps.py

@@ -5,9 +5,11 @@ from django.utils.translation import ugettext as _
 from misago.acl.exceptions import ACLError403, ACLError404
 from misago.apps.errors import error403, error404
 from misago.decorators import block_guest, check_csrf
+from misago.markdown import post_markdown
 from misago.messages import Message
-from misago.models import Forum, Thread, Post, Karma, WatchedThread
+from misago.models import Forum, Checkpoint, Thread, Post, Karma, WatchedThread
 from misago.readstrackers import ThreadsTracker
+from misago.utils.strings import short_string, slugify
 from misago.utils.views import json_response
 from misago.apps.threadtype.base import ViewBase
 
@@ -243,3 +245,116 @@ class DownvotePostBaseView(UpvotePostBaseView):
     
     def make_vote(self, request, vote):
         vote.score = -1
+
+
+class ReportPostBaseView(JumpView):
+    def make_jump(self):
+        self.request.acl.reports.allow_report()
+
+        @block_guest
+        @check_csrf
+        def view(request):
+            reported = None
+            if self.post.reported:
+                reported = self.post.live_report()
+
+            if reported and reported.start_poster_id != request.user.pk:
+                # Append our q.q to existing report?
+                try:
+                    reported.start_post.checkpoint_set.get(user=request.user, action="reported")
+                except Checkpoint.DoesNotExist:
+                    reported.start_post.set_checkpoint(self.request, 'reported', user)
+                    reported.start_post.save(force_update=True)
+            else:
+                # File up new report
+                now = timezone.now()
+                report_name = _('#%(post)s by %(author)s in "%(thread)s"')
+                report_name = report_name % {
+                                             'post': self.post.pk,
+                                             'thread': self.thread.name,
+                                             'author': self.post.user_name
+                                            }
+                report_name = short_string(report_name, request.settings['thread_name_max'])
+
+                reason_post = _('''
+Member @%(reporter)s has reported following post by @%(reported)s:
+
+%(quote)s
+
+**Post link:** <%(post)s>
+''')
+
+                reason_post = reason_post.strip() % {
+                                             'reporter': request.user.username,
+                                             'reported': self.post.user_name,
+                                             'post': self.redirect_to_post(self.post),
+                                             'quote': self.post.quote(),
+                                            }
+
+                md, reason_post_preparsed = post_markdown(reason_post)
+
+                reports = Forum.objects.special_model('reports')
+                report = Thread.objects.create(
+                                               forum=reports,
+                                               weight=2,
+                                               name=report_name,
+                                               slug=slugify(report_name),
+                                               start=now,
+                                               start_poster=request.user,
+                                               start_poster_name=request.user.username,
+                                               start_poster_slug=request.user.username_slug,
+                                               start_poster_style=request.user.rank.style,
+                                               last=now,
+                                               last_poster=request.user,
+                                               last_poster_name=request.user.username,
+                                               last_poster_slug=request.user.username_slug,
+                                               last_poster_style=request.user.rank.style,
+                                               report_for=self.post,
+                                               )
+
+                reason = Post.objects.create(
+                                             forum=reports,
+                                             thread=report,
+                                             user=request.user,
+                                             user_name=request.user.username,
+                                             ip=request.session.get_ip(self.request),
+                                             agent=request.META.get('HTTP_USER_AGENT'),
+                                             post=reason_post,
+                                             post_preparsed=reason_post_preparsed,
+                                             date=now,
+                                             )
+
+                report.start_post = reason
+                report.last_post = reason
+                report.save(force_update=True)
+
+                for m in self.post.mentions.all():
+                    reason.mentions.add(m)
+
+                self.post.reported = True
+                self.post.save(force_update=True)
+                self.thread.replies_reported += 1
+                self.thread.save(force_update=True)
+                request.monitor.increase('reported_posts')
+            if request.is_ajax():
+                return json_response(request, message=_("Selected post has been reported and will receive moderator attention. Thank you."))
+            self.request.messages.set_flash(Message(_("Selected post has been reported and will receive moderator attention. Thank you.")), 'info', 'threads_%s' % self.post.pk)
+            return self.redirect_to_post(self.post)
+        return view(self.request)
+
+
+class ShowPostReportBaseView(JumpView):
+    def make_jump(self):
+        self.request.acl.reports.allow_report()
+
+        @block_guest
+        def view(request):
+            if not self.post.reported:
+                return error404()
+            reports = Forum.objects.special_model('reports')
+            self.request.acl.forums.allow_thread_view(reports)
+            report = self.post.live_report()
+            if not report:
+                return error404()
+            return redirect(reverse('report', kwargs={'thread': report.pk, 'slug': report.slug}))
+        return view(self.request)

+ 63 - 27
misago/apps/threadtype/list/moderation.py

@@ -8,6 +8,12 @@ from misago.apps.threadtype.list.forms import MoveThreadsForm, MergeThreadsForm
 
 class ThreadsListModeration(object):
     def action_accept(self, ids):
+        if self._action_accept(ids):
+            self.request.messages.set_flash(Message(_('Selected threads have been marked as reviewed and made visible to other members.')), 'success', 'threads')
+        else:
+            self.request.messages.set_flash(Message(_('No threads were marked as reviewed.')), 'info', 'threads')
+
+    def _action_accept(self, ids):
         accepted = 0
         last_posts = []
         users = []
@@ -35,11 +41,15 @@ class ThreadsListModeration(object):
             self.forum.save(force_update=True)
             for user in users:
                 user.save(force_update=True)
-            self.request.messages.set_flash(Message(_('Selected threads have been marked as reviewed and made visible to other members.')), 'success', 'threads')
-        else:
-            self.request.messages.set_flash(Message(_('No threads were marked as reviewed.')), 'info', 'threads')
+        return accepted
 
     def action_annouce(self, ids):
+        if self._action_annouce(ids):
+            self.request.messages.set_flash(Message(_('Selected threads have been turned into announcements.')), 'success', 'threads')
+        else:
+            self.request.messages.set_flash(Message(_('No threads were turned into announcements.')), 'info', 'threads')
+
+    def _action_annouce(self, ids):
         acl = self.request.acl.threads.get_role(self.forum)
         annouced = []
         for thread in self.threads:
@@ -47,11 +57,15 @@ class ThreadsListModeration(object):
                 annouced.append(thread.pk)
         if annouced:
             Thread.objects.filter(id__in=annouced).update(weight=2)
-            self.request.messages.set_flash(Message(_('Selected threads have been turned into announcements.')), 'success', 'threads')
-        else:
-            self.request.messages.set_flash(Message(_('No threads were turned into announcements.')), 'info', 'threads')
+        return annouced
 
     def action_sticky(self, ids):
+        if self._action_sticky(ids):
+            self.request.messages.set_flash(Message(_('Selected threads have been sticked to the top of list.')), 'success', 'threads')
+        else:
+            self.request.messages.set_flash(Message(_('No threads were turned into stickies.')), 'info', 'threads')
+
+    def _action_sticky(self, ids):
         acl = self.request.acl.threads.get_role(self.forum)
         sticky = []
         for thread in self.threads:
@@ -59,20 +73,22 @@ class ThreadsListModeration(object):
                 sticky.append(thread.pk)
         if sticky:
             Thread.objects.filter(id__in=sticky).update(weight=1)
-            self.request.messages.set_flash(Message(_('Selected threads have been sticked to the top of list.')), 'success', 'threads')
-        else:
-            self.request.messages.set_flash(Message(_('No threads were turned into stickies.')), 'info', 'threads')
+        return sticky
 
     def action_normal(self, ids):
+        if self._action_normal(ids):
+            self.request.messages.set_flash(Message(_('Selected threads weight has been removed.')), 'success', 'threads')
+        else:
+            self.request.messages.set_flash(Message(_('No threads have had their weight removed.')), 'info', 'threads')
+
+    def _action_normal(self, ids):
         normalised = []
         for thread in self.threads:
             if thread.pk in ids and thread.weight > 0:
                 normalised.append(thread.pk)
         if normalised:
             Thread.objects.filter(id__in=normalised).update(weight=0)
-            self.request.messages.set_flash(Message(_('Selected threads weight has been removed.')), 'success', 'threads')
-        else:
-            self.request.messages.set_flash(Message(_('No threads have had their weight removed.')), 'info', 'threads')
+        return normalised
 
     def action_move(self, ids):
         threads = []
@@ -160,6 +176,12 @@ class ThreadsListModeration(object):
                                                      context_instance=RequestContext(self.request));
 
     def action_open(self, ids):
+        if self._action_open(ids):
+            self.request.messages.set_flash(Message(_('Selected threads have been opened.')), 'success', 'threads')
+        else:
+            self.request.messages.set_flash(Message(_('No threads were opened.')), 'info', 'threads')
+
+    def _action_open(self, ids):        
         opened = []
         last_posts = []
         for thread in self.threads:
@@ -170,11 +192,15 @@ class ThreadsListModeration(object):
         if opened:
             Post.objects.filter(id__in=last_posts).update(checkpoints=True)
             Thread.objects.filter(id__in=opened).update(closed=False)
-            self.request.messages.set_flash(Message(_('Selected threads have been opened.')), 'success', 'threads')
-        else:
-            self.request.messages.set_flash(Message(_('No threads were opened.')), 'info', 'threads')
+        return opened
 
     def action_close(self, ids):
+        if self._action_close(ids):
+            self.request.messages.set_flash(Message(_('Selected threads have been closed.')), 'success', 'threads')
+        else:
+            self.request.messages.set_flash(Message(_('No threads were closed.')), 'info', 'threads')
+
+    def _action_close(self, ids):
         closed = []
         last_posts = []
         for thread in self.threads:
@@ -185,11 +211,15 @@ class ThreadsListModeration(object):
         if closed:
             Post.objects.filter(id__in=last_posts).update(checkpoints=True)
             Thread.objects.filter(id__in=closed).update(closed=True)
-            self.request.messages.set_flash(Message(_('Selected threads have been closed.')), 'success', 'threads')
-        else:
-            self.request.messages.set_flash(Message(_('No threads were closed.')), 'info', 'threads')
+        return closed
 
     def action_undelete(self, ids):
+        if self._action_undelete(ids):
+            self.request.messages.set_flash(Message(_('Selected threads have been restored.')), 'success', 'threads')
+        else:
+            self.request.messages.set_flash(Message(_('No threads were restored.')), 'info', 'threads')
+
+    def _action_undelete(self, ids):
         undeleted = []
         last_posts = []
         posts = 0
@@ -207,11 +237,15 @@ class ThreadsListModeration(object):
             self.forum.save(force_update=True)
             Post.objects.filter(id__in=last_posts).update(checkpoints=True)
             Thread.objects.filter(id__in=undeleted).update(deleted=False)
-            self.request.messages.set_flash(Message(_('Selected threads have been restored.')), 'success', 'threads')
-        else:
-            self.request.messages.set_flash(Message(_('No threads were restored.')), 'info', 'threads')
+        return undeleted
 
     def action_soft(self, ids):
+        if self._action_soft(ids):
+            self.request.messages.set_flash(Message(_('Selected threads have been hidden.')), 'success', 'threads')
+        else:
+            self.request.messages.set_flash(Message(_('No threads were hidden.')), 'info', 'threads')
+
+    def _action_soft(self, ids):
         deleted = []
         last_posts = []
         posts = 0
@@ -230,11 +264,15 @@ class ThreadsListModeration(object):
             self.forum.save(force_update=True)
             Post.objects.filter(id__in=last_posts).update(checkpoints=True)
             Thread.objects.filter(id__in=deleted).update(deleted=True)
-            self.request.messages.set_flash(Message(_('Selected threads have been hidden.')), 'success', 'threads')
-        else:
-            self.request.messages.set_flash(Message(_('No threads were hidden.')), 'info', 'threads')
+        return deleted
 
     def action_hard(self, ids):
+        if self._action_hard(ids):
+            self.request.messages.set_flash(Message(_('Selected threads have been deleted.')), 'success', 'threads')
+        else:
+            self.request.messages.set_flash(Message(_('No threads were deleted.')), 'info', 'threads')
+    
+    def _action_hard(self, ids):        
         deleted = []
         posts = 0
         for thread in self.threads:
@@ -247,6 +285,4 @@ class ThreadsListModeration(object):
             self.request.monitor['posts'] = int(self.request.monitor['posts']) - posts
             self.forum.sync()
             self.forum.save(force_update=True)
-            self.request.messages.set_flash(Message(_('Selected threads have been deleted.')), 'success', 'threads')
-        else:
-            self.request.messages.set_flash(Message(_('No threads were deleted.')), 'info', 'threads')
+        return deleted

+ 1 - 9
misago/apps/threadtype/posting/newreply.py

@@ -22,15 +22,7 @@ class NewReplyBaseView(PostingBaseView):
 
     def form_initial_data(self):
         if self.quote:
-            quote_post = []
-            if self.quote.user:
-                quote_post.append('@%s' % self.quote.user.username)
-            else:
-                quote_post.append('@%s' % self.quote.user_name)
-            for line in self.quote.post.splitlines():
-                quote_post.append('> %s' % line)
-            quote_post.append('\r\n')
-            return {'post': '\r\n'.join(quote_post)}
+            return {'post': self.quote.quote()}
         return {}
 
     def post_form(self, form):

+ 29 - 1
misago/apps/threadtype/thread/moderation/thread.py

@@ -24,21 +24,34 @@ class ThreadModeration(object):
         # Update monitor
         self.request.monitor.increase('threads')
         self.request.monitor.increase('posts', self.thread.replies + 1)
+        # After
+        self.after_thread_action_accept()
+
+    def after_thread_action_accept(self):
         self.request.messages.set_flash(Message(_('Thread has been marked as reviewed and made visible to other members.')), 'success', 'threads')
 
     def thread_action_annouce(self):
         self.thread.weight = 2
         self.thread.save(force_update=True)
+        self.after_thread_action_annouce()
+
+    def after_thread_action_annouce(self):
         self.request.messages.set_flash(Message(_('Thread has been turned into announcement.')), 'success', 'threads')
 
     def thread_action_sticky(self):
         self.thread.weight = 1
         self.thread.save(force_update=True)
+        self.after_thread_action_sticky()
+    
+    def after_thread_action_sticky(self):
         self.request.messages.set_flash(Message(_('Thread has been turned into sticky.')), 'success', 'threads')
 
     def thread_action_normal(self):
         self.thread.weight = 0
         self.thread.save(force_update=True)
+        self.after_thread_action_normal()
+
+    def after_thread_action_normal(self):
         self.request.messages.set_flash(Message(_('Thread weight has been changed to normal.')), 'success', 'threads')
 
     def thread_action_move(self):
@@ -75,12 +88,18 @@ class ThreadModeration(object):
         self.thread.closed = False
         self.thread.save(force_update=True)
         self.thread.last_post.set_checkpoint(self.request, 'opened')
+        self.after_thread_action_open()
+
+    def after_thread_action_open(self):
         self.request.messages.set_flash(Message(_('Thread has been opened.')), 'success', 'threads')
 
     def thread_action_close(self):
         self.thread.closed = True
         self.thread.save(force_update=True)
         self.thread.last_post.set_checkpoint(self.request, 'closed')
+        self.after_thread_action_close()
+
+    def after_thread_action_close(self):
         self.request.messages.set_flash(Message(_('Thread has been closed.')), 'success', 'threads')
 
     def thread_action_undelete(self):
@@ -99,6 +118,9 @@ class ThreadModeration(object):
         # Update monitor
         self.request.monitor.increase('threads')
         self.request.monitor.increase('posts', self.thread.replies + 1)
+        self.after_thread_action_undelete()
+
+    def after_thread_action_undelete(self):
         self.request.messages.set_flash(Message(_('Thread has been restored.')), 'success', 'threads')
 
     def thread_action_soft(self):
@@ -117,6 +139,9 @@ class ThreadModeration(object):
         # Update monitor
         self.request.monitor.decrease('threads')
         self.request.monitor.decrease('posts', self.thread.replies + 1)
+        self.after_thread_action_soft()
+
+    def after_thread_action_soft(self):
         self.request.messages.set_flash(Message(_('Thread has been hidden.')), 'success', 'threads')
 
     def thread_action_hard(self):
@@ -128,5 +153,8 @@ class ThreadModeration(object):
         # Update monitor
         self.request.monitor.decrease('threads')
         self.request.monitor.decrease('posts', self.thread.replies + 1)
-        self.request.messages.set_flash(Message(_('Thread "%(thread)s" has been deleted.') % {'thread': self.thread.name}), 'success', 'threads')
+        self.after_thread_action_hard()
         return self.threads_list_redirect()
+
+    def after_thread_action_hard(self):
+        self.request.messages.set_flash(Message(_('Thread "%(thread)s" has been deleted.') % {'thread': self.thread.name}), 'success', 'threads')

+ 9 - 0
misago/fixtures/reportsmonitor.py

@@ -0,0 +1,9 @@
+from misago.utils.fixtures import load_monitor_fixture
+
+monitor_fixture = {
+                   'reported_posts': 0,
+                  }
+
+
+def load():
+    load_monitor_fixture(monitor_fixture)

+ 13 - 0
misago/management/commands/countreports.py

@@ -0,0 +1,13 @@
+from django.core.management.base import BaseCommand
+from misago.models import Post
+from misago.monitor import Monitor
+
+class Command(BaseCommand):
+    """
+    This command is intended to work as CRON job fired every few minutes/hours to count reported posts
+    """
+    help = 'Counts reported posts'
+    def handle(self, *args, **options):
+        monitor = Monitor()
+        monitor['reported_posts'] = Post.objects.filter(reported=True).count()
+        self.stdout.write('Reported posts were recounted.\n')

+ 397 - 0
misago/migrations/0008_auto__add_field_thread_report_for.py

@@ -0,0 +1,397 @@
+# -*- coding: utf-8 -*-
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+    def forwards(self, orm):
+        # Adding field 'Thread.report_for'
+        db.add_column(u'misago_thread', 'report_for',
+                      self.gf('django.db.models.fields.related.ForeignKey')(blank=True, db_index= True, related_name='reports', null=True, to=orm['misago.Post']),
+                      keep_default=False)
+
+
+    def backwards(self, orm):
+        # Deleting field 'Thread.report_for'
+        db.delete_column(u'misago_thread', 'report_for_id')
+
+
+    models = {
+        'misago.alert': {
+            'Meta': {'object_name': 'Alert'},
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'message': ('django.db.models.fields.TextField', [], {}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"}),
+            'variables': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'misago.ban': {
+            'Meta': {'object_name': 'Ban'},
+            'ban': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'reason_admin': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'reason_user': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'test': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.change': {
+            'Meta': {'object_name': 'Change'},
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'change': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'post': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Post']"}),
+            'post_content': ('django.db.models.fields.TextField', [], {}),
+            'reason': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'size': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'thread_name_new': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'thread_name_old': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.checkpoint': {
+            'Meta': {'object_name': 'Checkpoint'},
+            'action': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'old_forum': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['misago.Forum']"}),
+            'old_forum_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'old_forum_slug': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'post': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Post']"}),
+            'target_user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'target_user_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'target_user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.fixture': {
+            'Meta': {'object_name': 'Fixture'},
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.forum': {
+            'Meta': {'object_name': 'Forum'},
+            'attrs': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'description_preparsed': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_poster': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'last_poster_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_thread': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Thread']"}),
+            'last_thread_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_thread_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_thread_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'parent': ('mptt.fields.TreeForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['misago.Forum']"}),
+            'posts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'posts_delta': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'prune_last': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'prune_start': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'pruned_archive': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Forum']"}),
+            'redirect': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'redirects': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'redirects_delta': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'show_details': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}),
+            'special': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'threads': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'threads_delta': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'type': ('django.db.models.fields.CharField', [], {'max_length': '12'})
+        },
+        'misago.forumread': {
+            'Meta': {'object_name': 'ForumRead'},
+            'cleared': ('django.db.models.fields.DateTimeField', [], {}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"})
+        },
+        'misago.forumrole': {
+            'Meta': {'object_name': 'ForumRole'},
+            '_permissions': ('django.db.models.fields.TextField', [], {'null': 'True', 'db_column': "'permissions'", 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.karma': {
+            'Meta': {'object_name': 'Karma'},
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'post': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Post']"}),
+            'score': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.monitoritem': {
+            'Meta': {'object_name': 'MonitorItem'},
+            'id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'primary_key': 'True'}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'value': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'misago.newsletter': {
+            'Meta': {'object_name': 'Newsletter'},
+            'content_html': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'content_plain': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ignore_subscriptions': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'progress': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'ranks': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['misago.Rank']", 'symmetrical': 'False'}),
+            'step_size': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'token': ('django.db.models.fields.CharField', [], {'max_length': '32'})
+        },
+        'misago.post': {
+            'Meta': {'object_name': 'Post'},
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'checkpoints': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'downvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'edit_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'edit_reason': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'edit_user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'edit_user_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'edit_user_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'edits': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'mentions': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'mention_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'merge': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'moderated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'post': ('django.db.models.fields.TextField', [], {}),
+            'post_preparsed': ('django.db.models.fields.TextField', [], {}),
+            'protected': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'reported': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'upvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.pruningpolicy': {
+            'Meta': {'object_name': 'PruningPolicy'},
+            'email': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_visit': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'posts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'registered': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.rank': {
+            'Meta': {'object_name': 'Rank'},
+            'as_tab': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'criteria': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'on_index': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'roles': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['misago.Role']", 'symmetrical': 'False'}),
+            'slug': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'special': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'})
+        },
+        'misago.role': {
+            'Meta': {'object_name': 'Role'},
+            '_permissions': ('django.db.models.fields.TextField', [], {'null': 'True', 'db_column': "'permissions'", 'blank': 'True'}),
+            '_special': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'db_column': "'special'", 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'protected': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
+        },
+        'misago.session': {
+            'Meta': {'object_name': 'Session'},
+            'admin': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'crawler': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'data': ('django.db.models.fields.TextField', [], {'db_column': "'session_data'"}),
+            'id': ('django.db.models.fields.CharField', [], {'max_length': '42', 'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'last': ('django.db.models.fields.DateTimeField', [], {}),
+            'matched': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'rank': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sessions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Rank']"}),
+            'start': ('django.db.models.fields.DateTimeField', [], {}),
+            'team': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sessions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"})
+        },
+        'misago.setting': {
+            'Meta': {'object_name': 'Setting'},
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'extra': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'field': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.SettingsGroup']", 'to_field': "'key'"}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'normalize_to': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'position': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'separator': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'setting': ('django.db.models.fields.CharField', [], {'max_length': '255', 'primary_key': 'True'}),
+            'value': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'value_default': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'misago.settingsgroup': {
+            'Meta': {'object_name': 'SettingsGroup'},
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.signinattempt': {
+            'Meta': {'object_name': 'SignInAttempt'},
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'})
+        },
+        'misago.thread': {
+            'Meta': {'object_name': 'Thread'},
+            'closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'downvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last': ('django.db.models.fields.DateTimeField', [], {}),
+            'last_post': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Post']"}),
+            'last_poster': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'last_poster_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'merges': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'moderated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'participants': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'private_thread_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'replies': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'replies_deleted': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'replies_moderated': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'replies_reported': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'report_for': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'reports'", 'null': 'True', 'to': "orm['misago.Post']"}),
+            'score': ('django.db.models.fields.PositiveIntegerField', [], {'default': '30'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}),
+            'start': ('django.db.models.fields.DateTimeField', [], {}),
+            'start_post': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Post']"}),
+            'start_poster': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'start_poster_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'start_poster_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}),
+            'start_poster_style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'upvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'weight': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.threadread': {
+            'Meta': {'object_name': 'ThreadRead'},
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"})
+        },
+        'misago.token': {
+            'Meta': {'object_name': 'Token'},
+            'accessed': ('django.db.models.fields.DateTimeField', [], {}),
+            'created': ('django.db.models.fields.DateTimeField', [], {}),
+            'id': ('django.db.models.fields.CharField', [], {'max_length': '42', 'primary_key': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'signin_tokens'", 'to': "orm['misago.User']"})
+        },
+        'misago.user': {
+            'Meta': {'object_name': 'User'},
+            'acl_key': ('django.db.models.fields.CharField', [], {'max_length': '12', 'null': 'True', 'blank': 'True'}),
+            'activation': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'alerts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'alerts_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'allow_pds': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'avatar_ban': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'avatar_ban_reason_admin': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'avatar_ban_reason_user': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'avatar_image': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'avatar_original': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'avatar_temp': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'avatar_type': ('django.db.models.fields.CharField', [], {'max_length': '10', 'null': 'True', 'blank': 'True'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '255'}),
+            'email_hash': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32'}),
+            'followers': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'following': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'follows': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'follows_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'hide_activity': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ignores': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'ignores_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'is_team': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'join_agent': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'join_date': ('django.db.models.fields.DateTimeField', [], {}),
+            'join_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'karma_given_n': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'karma_given_p': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'karma_n': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'karma_p': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'last_agent': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'last_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39', 'null': 'True', 'blank': 'True'}),
+            'last_post': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_search': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_sync': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'password_date': ('django.db.models.fields.DateTimeField', [], {}),
+            'posts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'rank': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Rank']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'ranking': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'receive_newsletters': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'roles': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['misago.Role']", 'symmetrical': 'False'}),
+            'score': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'signature': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'signature_ban': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'signature_ban_reason_admin': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'signature_ban_reason_user': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'signature_preparsed': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'subscribe_reply': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'subscribe_start': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'sync_pds': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'threads': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'timezone': ('django.db.models.fields.CharField', [], {'default': "'utc'", 'max_length': '255'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'token': ('django.db.models.fields.CharField', [], {'max_length': '12', 'null': 'True', 'blank': 'True'}),
+            'unread_pds': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'username': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'username_slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255'}),
+            'votes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.usernamechange': {
+            'Meta': {'object_name': 'UsernameChange'},
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'old_username': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'namechanges'", 'to': "orm['misago.User']"})
+        },
+        'misago.watchedthread': {
+            'Meta': {'object_name': 'WatchedThread'},
+            'email': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_read': ('django.db.models.fields.DateTimeField', [], {}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"})
+        }
+    }
+
+    complete_apps = ['misago']

+ 18 - 1
misago/models/postmodel.py

@@ -33,7 +33,7 @@ class Post(models.Model):
     edit_user = models.ForeignKey('User', related_name='+', null=True, blank=True, on_delete=models.SET_NULL)
     edit_user_name = models.CharField(max_length=255, null=True, blank=True)
     edit_user_slug = models.SlugField(max_length=255, null=True, blank=True)
-    reported = models.BooleanField(default=False)
+    reported = models.BooleanField(default=False, db_index=True)
     moderated = models.BooleanField(default=False)
     deleted = models.BooleanField(default=False)
     protected = models.BooleanField(default=False)
@@ -48,6 +48,14 @@ class Post(models.Model):
     def get_date(self):
         return self.date
 
+    def quote(self):
+        quote = []
+        quote.append('@%s' % self.user_name)
+        for line in self.post.splitlines():
+            quote.append('> %s' % line)
+        quote.append('\r\n')
+        return '\r\n'.join(quote)
+
     def move_to(self, thread):
         move_post.send(sender=self, move_to=thread)
         self.thread = thread
@@ -100,6 +108,15 @@ class Post(models.Model):
                 except (ACLError403, ACLError404):
                     pass
 
+    def is_reported(self):
+        self.reported = self.reports.filter(weight=2).count() > 0
+
+    def live_report(self):
+        try:
+            return self.reports.filter(weight=2)[0]
+        except IndexError:
+            return None
+
 
 def rename_user_handler(sender, **kwargs):
     Post.objects.filter(user=sender).update(

+ 28 - 1
misago/models/threadmodel.py

@@ -1,7 +1,7 @@
 from datetime import timedelta
 from django.conf import settings
 from django.db import models
-from django.db.models.signals import pre_delete
+from django.db.models.signals import pre_save, pre_delete
 from django.utils import timezone
 from django.utils.translation import ugettext_lazy as _
 from misago.signals import (delete_user_content, merge_thread, move_forum_content,
@@ -74,6 +74,7 @@ class Thread(models.Model):
     last_poster_slug = models.SlugField(max_length=255, null=True, blank=True)
     last_poster_style = models.CharField(max_length=255, null=True, blank=True)
     participants = models.ManyToManyField('User', related_name='private_thread_set')
+    report_for = models.ForeignKey('Post', null=True, blank=True, related_name='reports', on_delete=models.SET_NULL)
     moderated = models.BooleanField(default=False)
     deleted = models.BooleanField(default=False)
     closed = models.BooleanField(default=False)
@@ -188,6 +189,32 @@ def rename_user_handler(sender, **kwargs):
 rename_user.connect(rename_user_handler, dispatch_uid="rename_user_threads")
 
 
+def report_update_handler(sender, **kwargs):
+    if sender == Thread:
+        thread = kwargs.get('instance')
+        if thread.weight < 2 and thread.report_for_id:
+            reported_post = thread.report_for
+            reported_post.reported = False
+            reported_post.save(force_update=True)
+            reported_post.thread.replies_reported -= 1
+            reported_post.thread.save(force_update=True)
+
+pre_save.connect(report_update_handler, dispatch_uid="sync_post_reports_on_update")
+
+
+def report_delete_handler(sender, **kwargs):
+    if sender == Thread:
+        thread = kwargs.get('instance')
+        if thread.report_for_id:
+            reported_post = thread.report_for
+            reported_post.reported = False
+            reported_post.save(force_update=True)
+            reported_post.thread.replies_reported -= 1
+            reported_post.thread.save(force_update=True)
+
+pre_delete.connect(report_delete_handler, dispatch_uid="sync_post_reports_on_delete")
+
+
 def delete_user_content_handler(sender, **kwargs):
     for thread in Thread.objects.filter(start_poster=sender):
         thread.delete()

+ 1 - 1
misago/urls.py

@@ -28,7 +28,7 @@ urlpatterns += patterns('',
     (r'^watched-threads/', include('misago.apps.watchedthreads.urls')),
     (r'^reset-password/', include('misago.apps.resetpswd.urls')),
     (r'^private-threads/', include('misago.apps.privatethreads.urls')),
-    #(r'^reports/', include('misago.apps.reports.urls')),
+    (r'^reports/', include('misago.apps.reports.urls')),
     (r'^', include('misago.apps.threads.urls')),
 )
 

+ 1 - 0
static/cranefly/css/cranefly.css

@@ -1141,6 +1141,7 @@ a.btn-link:hover,a.btn-link:active,a.btn-link:focus{opacity:0.9;filter:alpha(opa
 .thread-body .post-wrapper .post-body .post-content .post-footer .post-rating form .btn-link:disabled:hover,.thread-body .post-wrapper .post-body .post-content .post-footer .post-rating form .btn-link:disabled:active,.thread-body .post-wrapper .post-body .post-content .post-footer .post-rating form .btn-link:disabled:focus{text-decoration:none;}
 .thread-body .post-wrapper .post-body .post-content .post-footer .post-actions{border-left:1px dotted #e7e7e7;float:right;padding:7px 14px;color:#999999;}.thread-body .post-wrapper .post-body .post-content .post-footer .post-actions a,.thread-body .post-wrapper .post-body .post-content .post-footer .post-actions span,.thread-body .post-wrapper .post-body .post-content .post-footer .post-actions form{float:left;overflow:auto;}
 .thread-body .post-wrapper .post-body .post-content .post-footer .post-actions form{margin:0px;padding:0px;}.thread-body .post-wrapper .post-body .post-content .post-footer .post-actions form .btn{float:right;margin:0px;margin-left:14px;opacity:1;filter:alpha(opacity=100);padding:0px;color:#999999;font-weight:normal;}.thread-body .post-wrapper .post-body .post-content .post-footer .post-actions form .btn:hover,.thread-body .post-wrapper .post-body .post-content .post-footer .post-actions form .btn:active,.thread-body .post-wrapper .post-body .post-content .post-footer .post-actions form .btn:focus{color:#cf402e;text-decoration:underline;}
+.thread-body .post-wrapper .post-body .post-content .post-footer .post-actions form .btn.btn-report:disabled{color:#46a546;}
 .thread-body .post-wrapper .post-body .post-content .post-footer .post-actions form .btn.btn-hide:hover,.thread-body .post-wrapper .post-body .post-content .post-footer .post-actions form .btn.btn-hide:active,.thread-body .post-wrapper .post-body .post-content .post-footer .post-actions form .btn.btn-hide:focus{color:#f89406;}
 .thread-body .post-wrapper .post-body .post-content .post-footer .post-actions form:first-child .btn{margin-left:0px;}
 .thread-body .post-wrapper .post-body .post-content .post-footer .post-actions a{margin-left:14px;color:#999999;}.thread-body .post-wrapper .post-body .post-content .post-footer .post-actions a:hover,.thread-body .post-wrapper .post-body .post-content .post-footer .post-actions a a:active{color:#333333;}

+ 6 - 0
static/cranefly/css/cranefly/thread.less

@@ -284,6 +284,12 @@
                   text-decoration: underline;
                 }
 
+                &.btn-report {
+                  &:disabled {
+                    color: @green;
+                  }
+                }
+
                 &.btn-hide {
                   &:hover, &:active, &:focus {
                     color: @orange;

+ 27 - 0
static/cranefly/js/cranefly.js

@@ -187,3 +187,30 @@ $(function() {
     });
   });
 });
+
+// Ajax: Post reports
+$(function() {
+  $('.form-report').each(function() {
+    var action_parent = this;
+    var csrf_token = $(this).find('input[name="_csrf_token"]').val();
+    var button = $(this).find('button');
+    $(this).submit(function() {
+      var form = this;
+      $.post(form.action, {'_csrf_token': csrf_token}, "json").done(function(data, textStatus, jqXHR) {        
+        $(button).text(l_post_reported);
+        $(button).tooltip('destroy');
+        $(button).attr("title", data.message);
+        $(button).tooltip({placement: 'top', container: 'body'});
+        $(button).tooltip("show");
+        $(button).attr("disabled", "disabled");
+        setTimeout(function() {
+          $(button).tooltip('hide');
+        }, 2500);
+      }).fail(function() {
+        $(form).unbind();
+        $(form).trigger('submit');
+      });
+      return false;
+    });
+  });
+});

+ 3 - 0
templates/cranefly/layout.html

@@ -54,6 +54,9 @@
           <li class="user-profile"><a href="{% url 'user' user=user.id, username=user.username_slug %}" title="{% trans %}Go to your profile{% endtrans %}" class="tooltip-bottom"><div><img src="{{ user.get_avatar(28) }}" alt=""> {{ user.username }}</div></a></li>
           {{ hook_user_menu_prepend|safe }}
           <li><a href="{% url 'alerts' %}" title="{% if user.alerts %}{% trans %}You have new notifications!{% endtrans %}{% else %}{% trans %}Your Notifications{% endtrans %}{% endif %}" class="tooltip-bottom"><i class="icon-asterisk"></i>{% if user.alerts %}<span class="label label-important">{{ user.alerts }}</span>{% endif %}</a></li>
+          {% if acl.reports.can_handle() %}
+          <li><a href="{% url 'reports' %}" title="{% if monitor.reported_posts|int %}{% trans %}There are unresolved reports!{% endtrans %}{% else %}{% trans %}Reports{% endtrans %}{% endif %}" class="tooltip-bottom"><i class="icon-fire"></i>{% if monitor.reported_posts|int %}<span class="label label-important">{{ monitor.reported_posts }}</span>{% endif %}</a></li>
+          {% endif %}
           {% if settings.enable_private_threads and acl.private_threads.can_participate() %}
           <li><a href="{% url 'private_threads' %}" title="{% if user.unread_pds %}{% trans %}There are unread Private Threads!{% endtrans %}{% else %}{% trans %}Your Private Threads{% endtrans %}{% endif %}" class="tooltip-bottom"><i class="icon-inbox"></i>{% if user.unread_pds %}<span class="label label-important">{{ user.unread_pds }}</span>{% endif %}</a></li>
           {% endif %}

+ 1 - 1
templates/cranefly/macros.html

@@ -57,7 +57,7 @@ itemprop="breadcrumb"
 
 {% macro thread_flags(thread) -%}
 <ul class="unstyled thread-flags">
-  {% if thread.replies_reported %}
+  {% if acl.threads.can_mod_posts(forum) and thread.replies_reported %}
   <li class="flag-reported"><i class="icon-warning-sign tooltip-top" title="{% trans %}This thread has reported replies{% endtrans %}"></i></li>
   {% endif %}
   {% if thread.replies_moderated %}

+ 12 - 1
templates/cranefly/private_threads/thread.html

@@ -206,7 +206,7 @@
                   </span>
                   {% endif %}
 
-                  {% if post.reported %}
+                  {% if acl.threads.can_mod_posts(forum) and post.reported %}
                   <span class="label label-important">
                     {% trans %}Reported{% endtrans %}
                   </span>
@@ -238,6 +238,15 @@
                   {% if acl.users.can_see_users_trails() -%}
                   <a href="{% url 'private_post_info' thread=thread.pk, slug=thread.slug, post=post.pk %}" class="post-trail">{% trans %}Info{% endtrans %}</a>
                   {% endif %}
+                  {% if post.reported and acl.reports.can_handle() and acl.threads.can_mod_posts(forum) %}
+                  <a href="{% url 'private_post_report_show' thread=thread.pk, slug=thread.slug, post=post.pk %}">{% trans %}Show report{% endtrans %}</a>
+                  {% endif %}
+                  {% if acl.reports.can_report() %}
+                  <form action="{% url 'private_post_report' thread=thread.pk, slug=thread.slug, post=post.pk %}" class="form-inline form-report" method="post" autocomplete="off">
+                    <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+                    <button type="submit" class="btn btn-link btn-report tooltip-top" title="{% trans %}Bring this post to moderator attention.{% endtrans %}">{% trans %}Report{% endtrans %}</button>
+                  </form>
+                  {% endif %}
                   {% if acl.threads.can_edit_thread(user, forum, thread, post) and thread.start_post_id == post.pk %}
                   <a href="{% url 'private_thread_edit' thread=thread.pk, slug=thread.slug %}" class="post-edit">{% trans %}Edit{% endtrans %}</a>
                   {% elif acl.threads.can_edit_reply(user, forum, thread, post) %}
@@ -442,6 +451,7 @@
 {% block javascripts -%}{{ super() }}
   <script src="{{ STATIC_URL }}cranefly/highlight/highlight.pack.js"></script>
   <script type="text/javascript">
+    var l_post_reported = "{{ _('Reported!') }}";
     hljs.tabReplace = '    ';
     hljs.initHighlightingOnLoad();
     EnhancePostsMD();
@@ -522,6 +532,7 @@
     {% if extra %}
     {% if not is_read %}<li><a href="{% url 'private_thread_new' slug=thread.slug, thread=thread.id %}" class="tooltip-top" title="{% trans %}Go to first unread{% endtrans %}"><i class="icon-star"></i> {% trans %}First Unread{% endtrans %}</a></li>{% endif %}
     {% endif %}
+    {% if thread.replies_reported > 0 and acl.threads.can_mod_posts(thread) %}<li><a href="{% url 'private_thread_reported' slug=thread.slug, thread=thread.id %}" class="tooltip-top" title="{% trans %}Go to first reported post{% endtrans %}"><i class="icon-fire"></i> {% trans %}First Reported{% endtrans %}</a></li>{% endif %}
   </ul>
 </div>
 {% endmacro %}

+ 81 - 0
templates/cranefly/reports/changelog.html

@@ -0,0 +1,81 @@
+{% extends "cranefly/layout.html" %}
+{% import "cranefly/macros.html" as macros with context %}
+
+{% block title %}{{ macros.page_title(title=(_("Post #%(post)s Changelog") % {'post': post.pk}),parent=thread.name) }}{% endblock %}
+
+{% block breadcrumb %}{{ super() }} <span class="divider"><i class="icon-chevron-right"></i></span></li>
+<li><a href="{% url 'reports' %}">{% trans %}Reported Posts{% endtrans %}</a> <span class="divider"><i class="icon-chevron-right"></i></span></li>
+<li><a href="{% url 'report' thread=thread.pk, slug=thread.slug %}">{{ thread.name }}</a> <span class="divider"><i class="icon-chevron-right"></i></span></li>
+<li class="active">{% trans post=post.pk %}Post #{{ post }} Changelog{% endtrans %}
+{%- endblock %}
+
+{% block container %}
+<div class="page-header header-primary">
+  <div class="container">
+    {{ messages_list(messages) }}
+    <ul class="breadcrumb">
+      {{ self.breadcrumb() }}</li>
+    </ul>
+    <h1>{% trans post=post.pk %}Post #{{ post }} Changelog{% endtrans %} <small>{{ thread.name }}</small></h1>
+    <ul class="unstyled header-stats">
+      <li><i class="icon-time"></i> <a href="{% url 'report_find' thread=thread.pk, slug=thread.slug, post=post.pk %}">{{ post.date|reltimesince }}</a></li>
+      <li><i class="icon-user"></i> {% if post.user %}<a href="{% url 'user' user=post.user.pk, username=post.user.username_slug %}">{{ post.user.username }}</a>{% else %}{{ post.user_name }}{% endif %}</li>
+      <li><i class="icon-pencil"></i> {% trans edits=post.edits %}One edit{% pluralize %}{{ edits }} edits{% endtrans %}</li>
+      {% if post.protected %}<li><i class="icon-lock"></i> {% trans %}Protected{% endtrans %}</li>{% endif %}
+    </ul>
+  </div>
+</div>
+
+<div class="container container-primary">
+  <div class="post-changelog">
+    {% if edits %}
+    <table class="table table-striped">
+      <thead>
+        <tr>
+          <th style="width: 1%;">&nbsp;</th>
+          <th>{% trans %}Change Log{% endtrans %}</th>
+        </tr>
+      </thead>
+      <tbody>
+        {% for edit in edits %}
+        <tr>
+          <td>
+            <span class="change-{% if edit.change > 0 %}added{% elif edit.change < 0 %}removed{% else %}none{% endif %}{% if not edit.reason %} change-small{% endif %}">
+              {% if edit.change > 0 %}+{% endif %}{{ edit.change }}
+            </span>
+          </td>
+          <td>
+            <a href="{% url 'report_changelog_diff' thread=thread.pk, slug=thread.slug, post=post.pk, change=edit.pk %}" class="change-no">#{{ loop.revindex }}</a>
+            {% if edit.reason %}
+            <div class="change-reason">
+              <a href="{% url 'report_changelog_diff' thread=thread.pk, slug=thread.slug, post=post.pk, change=edit.pk %}">{{ edit.reason }}</a>
+            </div>{% endif %}
+            <div class="change-description">
+              <a href="{% url 'report_changelog_diff' thread=thread.pk, slug=thread.slug, post=post.pk, change=edit.pk %}">
+              {% if edit.change != 0 %}{% if edit.change > 0 -%}
+              {% trans chars=edit.change %}Added one character to post.{% pluralize %}Added {{ chars }} characters to post.{% endtrans %}
+              {%- elif edit.change < 0 -%}
+              {% trans chars=edit.change|abs %}Removed one character from post.{% pluralize %}Removed {{ chars }} characters from post.{% endtrans %}
+              {%- else -%}
+              {% trans %}No change in message's length.{% endtrans %}
+              {%- endif %}{% endif %}{% if edit.thread_name_old %} {% trans old=edit.thread_name_old, new=edit.thread_name_new %}Changed thread name from "{{ old }}" to "{{ new }}".{% endtrans %}{% endif %}</a>
+              <span class="change-details">
+                {% trans user=edit_user(edit), date=edit.date|reldate|low %}By {{ user }} {{ date }}{% endtrans %}
+              </span>
+            </div>
+          </td>
+        </tr>
+        {% endfor %}
+      </tbody>
+    </table>
+    {% else %}
+    <p class="lead">{% trans %}This post was never edited.{% endtrans %}</p>
+    {% endif %}
+  </div>
+</div>
+{% endblock %}
+
+
+{% macro edit_user(edit) -%}
+{% if edit.user_id %}<a href="{% url 'user' user=edit.user_id, username=edit.user_slug %}">{{ edit.user_name }}</a>{% else %}{{ edit.user_name }}{% endif %}
+{%- endmacro %}

+ 95 - 0
templates/cranefly/reports/changelog_diff.html

@@ -0,0 +1,95 @@
+{% extends "cranefly/layout.html" %}
+{% import "cranefly/macros.html" as macros with context %}
+
+{% block title %}{{ macros.page_title(title=(_("Post #%(post)s Changelog") % {'post': post.pk}),parent=thread.name) }}{% endblock %}
+
+{% block breadcrumb %}{{ super() }} <span class="divider"><i class="icon-chevron-right"></i></span></li>
+<li><a href="{% url 'reports' %}">{% trans %}Reported Posts{% endtrans %}</a> <span class="divider"><i class="icon-chevron-right"></i></span></li>
+<li><a href="{% url 'report' thread=thread.pk, slug=thread.slug %}">{{ thread.name }}</a> <span class="divider"><i class="icon-chevron-right"></i></span></li>
+<li><a href="{% url 'report_changelog' thread=thread.pk, slug=thread.slug, post=post.pk %}">{% trans post=post.pk %}Post #{{ post }} Changelog{% endtrans %}</a> <span class="divider"><i class="icon-chevron-right"></i></span></li>
+<li class="active">{% trans date=change.date|reltimesince|low %}Edit from {{ date }}{% endtrans %}
+{%- endblock %}
+
+{% block container %}
+<div class="page-header header-primary">
+  <div class="container">
+    {{ messages_list(messages) }}
+    <ul class="breadcrumb">
+      {{ self.breadcrumb() }}</li>
+    </ul>
+    <h1>{% trans date=change.date|reltimesince|low %}Edit from {{ date }}{% endtrans %} <small>{% trans post=post.pk %}Post #{{ post }} Changelog{% endtrans %}</small></h1>
+    <ul class="unstyled header-stats pull-left">
+      <li><i class="icon-time"></i> <a href="{% url 'report_find' thread=thread.pk, slug=thread.slug, post=post.pk %}">{{ post.date|reltimesince }}</a></li>
+      <li><i class="icon-user"></i> {% if change.user_id %}<a href="{% url 'user' user=change.user_id, username=change.user_slug %}">{{ change.user_name }}</a>{% else %}{{ change.user_name }}{% endif %}</li>
+      {% if acl.users.can_see_users_trails() %}
+      <li><i class="icon-globe"></i> {{ change.ip }}</li>
+      <li><i class="icon-qrcode"></i> {{ change.agent }}</li>
+      {% endif %}
+      {% if change.change != 0 %}<li><i class="icon-{% if change.change > 0 %}plus{% elif change.change < 0 %}minus{% endif %}"></i> {% if change.change > 0 -%}
+      {% trans chars=change.change %}Added one character{% pluralize %}Added {{ chars }} characters{% endtrans %}
+      {%- elif change.change < 0 -%}
+      {% trans chars=change.change|abs %}Removed one character{% pluralize %}Removed {{ chars }} characters{% endtrans %}
+      {%- endif %}</li>{% endif %}
+    </ul>
+  </div>
+</div>
+
+<div class="container container-primary">
+  <div class="post-diff">
+    {% if message %}
+    <div class="messages-list">
+      {{ macros.draw_message(message) }}
+    </div>
+    {% endif %}
+
+    {% if change.reason %}
+    <p class="lead">{{ change.reason }}</p>
+    {% endif %}
+
+    {% if acl.threads.can_edit_reply(user, forum, thread, post) or prev or next %}
+    <div class="diff-extra">
+      {{ pager() }}
+      {% if user.is_authenticated() and acl.threads.can_make_revert(forum, thread) %}
+      <form class="form-inline pull-right" action="{% url 'report_changelog_revert' thread=thread.pk, slug=thread.slug, post=post.pk, change=change.pk %}" method="post">
+        <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+        <button type="submit" class="btn btn-danger">{% trans %}Revert this edit{% endtrans %}</button></li>
+      </form>
+      {%- endif %}
+    </div>
+    {% endif %}
+
+    <div class="post-diff-details">
+      <table>
+        <tbody>
+          {% for line in diff %}{% if line[0] != "?" %}
+          <tr>
+            <td class="line"><a href="#{{ l }}">{{ l }}.</a></td>
+            <td class="{% if line[0] == '+' %}added{% elif line[0] == '-' %}removed{% else %}stag{% endif %}{% if l is even %} even{% endif %}">{% if line[2:] %}{{ line[2:] }}{% else %}&nbsp;{% endif %}</td>
+          </tr>
+          {% set l = l + 1 %}
+          {% endif %}{% endfor %}
+        </tbody>
+      </table>
+    </div>
+
+    {% if prev or next %}
+    <div class="diff-extra">
+      {{ pager() }}
+    </div>
+    {% endif %}
+
+  </div>
+</div>
+{% endblock %}
+
+
+{% macro pager() %}
+{% if prev or prev %}
+<div class="pagination pull-left">
+  <ul>
+    {% if prev %}<li><a href="{% url 'report_changelog_diff' thread=thread.pk, slug=thread.slug, post=post.pk, change=prev.pk %}"><i class="icon-chevron-left"></i> {{ prev.date|reldate }}</a></li>{% endif %}
+    {% if next %}<li><a href="{% url 'report_changelog_diff' thread=thread.pk, slug=thread.slug, post=post.pk, change=next.pk %}">{{ next.date|reldate }} <i class="icon-chevron-right"></i></a></li>{% endif %}
+  </ul>
+</div>
+{% endif %}
+{% endmacro %}

+ 35 - 0
templates/cranefly/reports/details.html

@@ -0,0 +1,35 @@
+{% extends "cranefly/layout.html" %}
+{% import "cranefly/macros.html" as macros with context %}
+
+{% block title %}{{ macros.page_title(title=(_("Post #%(post)s Info") % {'post': post.pk}),parent=thread.name) }}{% endblock %}
+
+{% block breadcrumb %}{{ super() }} <span class="divider"><i class="icon-chevron-right"></i></span></li>
+<li><a href="{% url 'reports' %}">{% trans %}Reported Posts{% endtrans %}</a> <span class="divider"><i class="icon-chevron-right"></i></span></li>
+<li><a href="{% url 'report' thread=thread.pk, slug=thread.slug %}">{{ thread.name }}</a> <span class="divider"><i class="icon-chevron-right"></i></span></li>
+<li class="active">{% trans post=post.pk %}Post #{{ post }} Info{% endtrans %}
+{%- endblock %}
+
+{% block container %}
+<div class="page-header header-primary">
+  <div class="container">
+    {{ messages_list(messages) }}
+    <ul class="breadcrumb">
+      {{ self.breadcrumb() }}</li>
+    </ul>
+    <h1>{% trans post=post.pk %}Post #{{ post }} Info{% endtrans %} <small>{{ thread.name }}</small></h1>
+    <ul class="unstyled header-stats">
+      <li><i class="icon-time"></i> <a href="{% url 'report_find' thread=thread.pk, slug=thread.slug, post=post.pk %}">{{ post.date|reltimesince }}</a></li>
+      <li><i class="icon-user"></i> {% if post.user %}<a href="{% url 'user' user=post.user.pk, username=post.user.username_slug %}">{{ post.user.username }}</a>{% else %}{{ post.user_name }}{% endif %}</li>
+      <li><i class="icon-pencil"></i> {% trans edits=post.edits %}One edit{% pluralize %}{{ edits }} edits{% endtrans %}</li>
+      {% if post.protected %}<li><i class="icon-lock"></i> {% trans %}Protected{% endtrans %}</li>{% endif %}
+    </ul>
+  </div>
+</div>
+
+<div class="container container-primary">
+  <h2>{% trans %}IP Address{% endtrans %}</h2>
+  <p class="lead">{{ post.ip }}</p>
+  <h2>{% trans %}UserAgent{% endtrans %}</h2>
+  <p class="lead">{{ post.agent }}</p>
+</div>
+{% endblock %}

+ 171 - 0
templates/cranefly/reports/list.html

@@ -0,0 +1,171 @@
+{% extends "cranefly/layout.html" %}
+{% import "_forms.html" as form_theme with context %}
+{% import "cranefly/macros.html" as macros with context %}
+
+{% block title %}{{ macros.page_title(title=_('Reported Posts'),page=pagination['page']) }}{% endblock %}
+
+{% block breadcrumb %}{{ super() }} <span class="divider"><i class="icon-chevron-right"></i></span></li>
+<li class="active">{% trans %}Reported Posts{% endtrans %}
+{%- endblock %}
+
+{% block container %}
+<div class="page-header header-primary">
+  <div class="container">
+    {{ messages_list(messages) }}
+    <ul class="breadcrumb" {{ macros.itemprop_bread() }}>
+      {{ self.breadcrumb() }}</li>
+    </ul>
+    <h1>{% trans %}Reported Posts{% endtrans %}</h1>
+  </div>
+</div>
+
+<div class="container container-primary">
+
+  {% if message %}
+  <div class="messages-list">
+    {{ macros.draw_message(message) }}
+  </div>
+  {% endif %}
+
+  <div class="forum-threads-extra extra-top">
+    {{ pager() }}
+  </div>
+
+  <div class="forum-threads-list">
+    <div class="header">
+      <div class="row-fluid">
+        <div class="span7">{% trans %}Report{% endtrans %}</div>
+        <div class="span5 thread-activity">
+          <div class="thread-replies">{% trans %}Activity{% endtrans %}</div>
+          {% if user.is_authenticated() and list_form %}
+          <div class="pull-right check-cell">
+            <label class="checkbox"><input type="checkbox" class="checkbox-master"></label>
+          </div>
+          {% endif %}
+        </div>
+      </div>
+    </div>
+    {% for thread in threads %}
+    <div class="thread-row{% if not thread.is_read %} thread-new{% endif %}{% if loop.last %} thread-last{% endif %}">
+      <div class="row-fluid">
+        <div class="span7">
+          {% if thread.is_read %}
+          <a href="{% url 'report_new' thread=thread.pk, slug=thread.slug %}" class="thread-icon thread-icon-last tooltip-top" title="{% trans %}Click to see last post{% endtrans %}"><i class="icon-asterisk"></i></a>
+          {% else %}
+          <a href="{% url 'report_new' thread=thread.pk, slug=thread.slug %}" class="thread-icon thread-icon-new tooltip-top" title="{% trans %}Click to see first unread post{% endtrans %}"><i class="icon-fire"></i></a>
+          {% endif %}
+
+          {{ macros.thread_flags(thread) }}
+          
+          <a href="{% url 'report' thread=thread.pk, slug=thread.slug %}" class="thread-name">{{ thread.name }}</a>
+          
+          <div class="thread-details">
+            {% trans user=thread_starter(thread), start=thread.start|reldate|low %}by {{ user }}, {{ start }}{% endtrans %}
+          </div>
+
+        </div>
+        <div class="span5 thread-activity">
+
+          {% if settings.avatars_on_threads_list %}
+          <div class="thread-last-avatar">
+            {% if thread.last_poster_id %}
+            <a href="{% url 'user' user=thread.last_poster.pk, username=thread.last_poster.username_slug %}"><img src="{{ thread.last_poster.get_avatar(40) }}" alt=""></a>
+            {% else %}
+            <img src="{{ macros.avatar_guest(40) }}" alt="" class="user-avatar">
+            {% endif %}
+          </div>
+          {% endif %}
+
+          <div class="thread-replies">
+            <strong class="lead">{{ thread_reply(thread) }}, {{ thread.last|reldate|low }}</strong><br>
+            {{ replies(thread.replies) }}, <span{% if (thread.upvotes-thread.downvotes) > 0 %} class="text-success"{% elif (thread.upvotes-thread.downvotes) < 0 %} class="text-error"{% endif %}><strong>{% if (thread.upvotes-thread.downvotes) > 0 %}+{% elif (thread.upvotes-thread.downvotes) < 0 %}-{% endif %}</strong>{% trans rating=(thread.upvotes-thread.downvotes)|abs|intcomma %}{{ rating }} thread rating{% endtrans %}</span>
+          </div>
+
+          {% if user.is_authenticated() and list_form %}
+          <label class="thread-select checkbox"><input form="threads_form" name="{{ list_form['list_items']['html_name'] }}" type="checkbox" class="checkbox-member" value="{{ thread.pk }}"{% if list_form['list_items']['has_value'] and ('' ~ thread.pk) in list_form['list_items']['value'] %} checked="checked"{% endif %}></label>
+          {% endif %}
+        </div>
+      </div>
+    </div>
+    {% else %}
+    <div class="thread-row threads-list-empty">
+      {% trans %}There are no threads in this forum.{% endtrans %}
+    </div>
+    {% endfor %}
+
+    {% if user.is_authenticated() and list_form %}
+    <div class="threads-actions">
+      <form id="threads_form" class="form-inline pull-right" action="{{ request_path }}" method="POST">
+        <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+        {{ form_theme.input_select(list_form['list_action'],width=3) }}
+        <button type="submit" class="btn btn-danger">{% trans %}Go{% endtrans %}</button>
+      </form>
+    </div>
+    {% endif %}
+  </div>
+
+  <div class="forum-threads-extra">
+    {{ pager() }}
+  </div>
+
+</div>
+{% endblock %}
+
+{% macro replies(thread_replies) -%}
+{% trans count=thread_replies, replies=thread_replies|intcomma -%}
+{{ replies }} reply
+{%- pluralize -%}
+{{ replies }} replies
+{%- endtrans %}
+{%- endmacro %}
+
+{% macro thread_starter(thread) -%}
+{% if thread.start_poster_id %}<a href="{% url 'user' user=thread.start_poster_id, username=thread.start_poster_slug %}" class="user-link">{{ thread.start_poster_name }}</a>{% else %}{{ thread.start_poster_name }}{% endif %}
+{%- endmacro %}
+
+{% macro thread_reply(thread) -%}
+{% if thread.last_poster_id %}<a href="{% url 'user' user=thread.last_poster_id, username=thread.last_poster_slug %}" class="user-link">{{ thread.last_poster_name }}</a>{% else %}{{ thread.last_poster_name }}{% endif %}
+{%- endmacro %}
+
+{% macro pager() %}
+{% if pagination['total'] > 0 %}
+<div class="pagination pull-left">
+  <ul>
+    <li class="count">{{ macros.pager_label(pagination) }}</li>
+    {%- if pagination['prev'] > 1 %}<li><a href="{% url 'forum' slug=forum.slug, forum=forum.id %}" class="tooltip-top" title="{% trans %}First Page{% endtrans %}"><i class="icon-chevron-left"></i> {% trans %}First{% endtrans %}</a></li>{% endif -%}
+    {%- if pagination['prev'] > 0 %}<li><a href="{%- if pagination['prev'] > 1 %}{% url 'forum' slug=forum.slug, forum=forum.id, page=pagination['prev'] %}{% else %}{% url 'forum' slug=forum.slug, forum=forum.id %}{% endif %}" class="tooltip-top" title="{% trans %}Newest Threads{% endtrans %}"><i class="icon-chevron-left"></i></a></li>{% endif -%}
+    {%- if pagination['next'] > 0 %}<li><a href="{% url 'forum' slug=forum.slug, forum=forum.id, page=pagination['next'] %}" class="tooltip-top" title="{% trans %}Older Threads{% endtrans %}"><i class="icon-chevron-right"></i></a></li>{% endif -%}
+  </ul>
+</div>
+{% endif %}
+{% endmacro %}
+
+{% block javascripts -%}{{ super() }}
+  <script type="text/javascript">
+    $(function () {
+      function populateForumTooltip(target) {
+        return $('#forum-' + target + ' .forum-meta').html();
+      };
+      {% for subforum in forum.subforums %}
+        $('#forum-{{ subforum.id }} .forum-title').tooltip({
+          template: '<div class="tooltip forum-meta-tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>',
+          placement: 'right',
+          html: true,
+          title: populateForumTooltip({{ subforum.id }})
+        });
+      {% endfor %}
+      {%- if user.is_authenticated() and list_form %}
+      $('#threads_form').submit(function() {
+        if ($('.thread-select[]:checked').length == 0) {
+          alert("{% trans %}You have to select at least one thread.{% endtrans %}");
+          return false;
+        }
+        if ($('#id_list_action').val() == 'hard') {
+          var decision = confirm("{% trans %}Are you sure you want to delete selected reports? This action is not reversible!{% endtrans %}");
+          return decision;
+        }
+        return true;
+      });{% endif %}
+    });
+  </script>
+{%- endblock %}

+ 183 - 0
templates/cranefly/reports/posting.html

@@ -0,0 +1,183 @@
+{% extends "cranefly/layout.html" %}
+{% import "_forms.html" as form_theme with context %}
+{% import "cranefly/editor.html" as editor with context %}
+{% import "cranefly/macros.html" as macros with context %}
+
+{% block title %}{{ macros.page_title(title=_(get_title()), parent=thread.name) }}{% endblock %}
+
+{% block breadcrumb %}{{ super() }} <span class="divider"><i class="icon-chevron-right"></i></span></li>
+<li><a href="{% url 'reports' %}">{% trans %}Reported Posts{% endtrans %}</a> <span class="divider"><i class="icon-chevron-right"></i></span></li>
+{% if thread %}<li><a href="{% url 'report' thread=thread.pk, slug=thread.slug %}">{{ thread.name }}</a> <span class="divider"><i class="icon-chevron-right"></i></span></li>{% endif %}
+<li class="active">{{ get_title() }}
+{%- endblock %}
+
+{% block container %}
+<div class="page-header header-primary">
+  <div class="container">
+    {{ messages_list(messages) }}
+    <ul class="breadcrumb">
+      {{ self.breadcrumb() }}</li>
+    </ul>
+    <h1>{{ get_title() }} <small>{% if thread %}{{ thread.name }}{% else %}{{ forum.name }}{% endif %}</small></h1>
+    {% if thread %}
+    <ul class="unstyled header-stats">
+      {{ get_info() }}
+    </ul>
+    {% endif %}
+  </div>
+</div>
+<div class="container container-primary">
+  <div class="row">
+    <div class="span8 offset2">
+      <div class="posting">
+        <div class="form-container">
+
+          <div class="form-header">
+            <h1>{{ get_title() }}</h1>
+          </div>
+
+          {% if message %}
+          <div class="messages-list">
+            {{ macros.draw_message(message) }}
+          </div>
+          {% endif %}
+
+          {% if preview %}
+          <div class="form-preview">
+            <div class="markdown js-extra">
+              <article>
+                {{ preview|markdown_final|safe }}
+              </article>
+            </div>
+          </div>
+          {% endif %}
+
+          <form action="{{ get_action() }}" method="post">
+            <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+            {% if 'thread_name' in form.fields %}
+            {{ form_theme.row_widget(form.fields.thread_name, width=8) }}
+            <hr>
+            <h4>{% trans %}Message Body{% endtrans %}</h4>
+            {% endif %}
+            {{ editor.editor(form.fields.post, get_button(), rows=8, extra=get_extra()) }}
+            {% if intersect(form.fields, ('edit_reason', 'thread_weight', 'close_thread')) %}
+            <hr>
+            {% if 'edit_reason' in form.fields %}
+            {{ form_theme.row_widget(form.fields.edit_reason, width=8) }}
+            {% endif %}
+
+            {% if intersect(form.fields, ('thread_weight', 'close_thread')) %}
+            <div class="control-group">
+              <label class="control-label">{% trans %}Thread Status{% endtrans %}:</label>
+              <div class="controls">
+                {% if 'thread_weight' in form.fields %}
+                {{ form_theme.input_radio_select(form.fields.thread_weight, width=8) }}
+                {% endif %}
+                {% if 'close_thread' in form.fields %}
+                {{ form_theme.input_checkbox(form.fields.close_thread, width=8) }}
+                {% endif %}
+              </div>
+            </div>
+            {% endif %}
+
+            <div class="form-actions">
+              <button type="submit" class="btn btn-primary">{{ get_button() }}</button>
+              <button id="editor-preview" name="preview" type="submit" class="btn">{% trans %}Preview{% endtrans %}</button>
+            </div>
+            {% endif %}
+          </form>
+
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+{% endblock %}
+
+{% block stylesheets %}{{ super() }}
+<link href="{{ STATIC_URL }}cranefly/highlight/styles/monokai.css" rel="stylesheet">
+{% endblock %}
+
+{% block javascripts %}{{ super() }}
+  <script src="{{ STATIC_URL }}cranefly/highlight/highlight.pack.js"></script>
+  <script type="text/javascript">
+    hljs.tabReplace = '    ';
+    hljs.initHighlightingOnLoad();
+    EnhancePostsMD();
+  </script>
+  {{ editor.js() }}
+{% endblock %}
+
+
+{% macro get_action() -%}
+{% if action == 'new_thread' -%}
+{% url 'report_start' forum=forum.pk, slug=forum.slug %}
+{%- elif action == 'edit_thread' -%}
+{% url 'report_edit' thread=thread.pk, slug=thread.slug %}
+{%- elif action in 'new_reply' -%}
+{%- if quote -%}
+{% url 'report_reply' thread=thread.pk, slug=thread.slug, quote=quote.pk %}
+{%- else -%}
+{% url 'report_reply' thread=thread.pk, slug=thread.slug %}
+{%- endif -%}
+{%- elif action == 'edit_reply' -%}
+{% url 'post_edit' thread=thread.pk, slug=thread.slug, post=post.pk %}
+{%- endif %}
+{%- endmacro %}
+
+
+{% macro get_title() -%}
+{% if action == 'new_thread' -%}
+{% trans %}Post New Thread{% endtrans %}
+{%- elif action == 'edit_thread' -%}
+{% trans %}Edit Thread{% endtrans %}
+{%- elif action == 'new_reply' -%}
+{% trans %}Post New Reply{% endtrans %}
+{%- elif action == 'edit_reply' -%}
+{% trans %}Edit Reply{% endtrans %}
+{%- endif %}
+{%- endmacro %}
+
+
+{% macro get_info() -%}
+{% if action == 'edit_reply' -%}
+    {% if post.moderated %}<li><i class="icon-eye-close"></i> {% trans %}Not Reviewed{% endtrans %}</li>{% endif %}
+    <li><i class="icon-time"></i> <a href="{% url 'report_find' thread=thread.pk, slug=thread.slug, post=post.pk %}">{{ post.date|reltimesince }}</a></li>
+    <li><i class="icon-user"></i> {% if post.user %}<a href="{% url 'user' user=post.user.pk, username=post.user.username_slug %}">{{ post.user.username }}</a>{% else %}{{ post.user_name }}{% endif %}</li>
+    <li><i class="icon-pencil"></i> {% if post.edits > 0 -%}
+      {% trans edits=post.edits %}One edit{% pluralize %}{{ edits }} edits{% endtrans %}
+    {%- else -%}
+      {% trans %}First edit{% endtrans %}
+    {%- endif %}</li>
+{%- else -%}
+    {% if thread.moderated %}<li><i class="icon-eye-close"></i> {% trans %}Not Reviewed{% endtrans %}</li>{% endif %}
+    {% if action == 'edit_thread' %}
+    <li><i class="icon-time"></i> <a href="{% url 'report_find' thread=thread.pk, slug=thread.slug, post=thread.start_post_id %}">{{ thread.start|reltimesince }}</a></li>
+    {% else %}
+    <li><i class="icon-time"></i> <a href="{% url 'report_new' thread=thread.pk, slug=thread.slug %}">{{ thread.last|reltimesince }}</a></li>
+    {% endif %}
+    <li><i class="icon-user"></i> {% if thread.start_poster_id %}<a href="{% url 'user' user=thread.start_poster_id, username=thread.start_poster_slug %}">{{ thread.start_poster_name }}</a>{% else %}{{ thread.start_poster_name }}{% endif %}</li>
+    <li><i class="icon-comment"></i> {% if thread.replies > 0 -%}
+      {% trans count=thread.replies, replies=thread.replies|intcomma %}One reply{% pluralize %}{{ replies }} replies{% endtrans %}
+    {%- else -%}
+      {% trans %}No replies{% endtrans %}
+    {%- endif %}</li>
+{%- endif %}
+    {% if thread.closed %}<li><i class="icon-lock"></i> {% trans %}Locked{% endtrans %}</li>{% endif %}
+{%- endmacro %}
+
+
+{% macro get_button() -%}
+{% if action == 'new_thread' -%}
+{% trans %}Post Thread{% endtrans %}
+{%- elif action == 'new_reply' -%}
+{% trans %}Post Reply{% endtrans %}
+{%- else -%}
+{% trans %}Save Changes{% endtrans %}
+{%- endif %}
+{%- endmacro %}
+
+
+{% macro get_extra() %}
+  <button id="editor-preview" name="preview" type="submit" class="btn pull-right">{% trans %}Preview{% endtrans %}</button>
+{% endmacro %}

+ 522 - 0
templates/cranefly/reports/thread.html

@@ -0,0 +1,522 @@
+{% extends "cranefly/layout.html" %}
+{% import "_forms.html" as form_theme with context %}
+{% import "cranefly/editor.html" as editor with context %}
+{% import "cranefly/macros.html" as macros with context %}
+
+{% block title %}{{ macros.page_title(title=thread.name,parent=forum.name,page=pagination['page']) }}{% endblock %}
+
+{% block breadcrumb %}{{ super() }} <span class="divider"><i class="icon-chevron-right"></i></span></li>
+<li><a href="{% url 'reports' %}">{% trans %}Reported Posts{% endtrans %}</a> <span class="divider"><i class="icon-chevron-right"></i></span></li>
+<li class="active">{{ thread.name }}
+{%- endblock %}
+
+{% block container %}
+<div class="page-header header-primary">
+  <div class="container">
+    {{ messages_list(messages) }}
+    <ul class="breadcrumb" {{ macros.itemprop_bread() }}>
+      {{ self.breadcrumb() }}</li>
+    </ul>
+    <h1>{{ thread.name }}</h1>
+    <ul class="unstyled header-stats">
+      {% if thread.moderated %}<li><i class="icon-eye-close"></i> {% trans %}Not Reviewed{% endtrans %}</li>{% endif %}
+      <li><i class="icon-time"></i> {{ thread.last|reltimesince }}</li>
+      <li><i class="icon-user"></i> {% if thread.start_poster_id %}<a href="{% url 'user' user=thread.start_poster_id, username=thread.start_poster_slug %}">{{ thread.start_poster_name }}</a>{% else %}{{ thread.start_poster_name }}{% endif %}</li>
+      <li><i class="icon-comment"></i> {% if thread.replies > 0 -%}
+        {% trans count=thread.replies, replies=thread.replies|intcomma %}One reply{% pluralize %}{{ replies }} replies{% endtrans %}
+      {%- else -%}
+        {% trans %}No replies{% endtrans %}
+      {%- endif %}</li>
+      {% if thread.closed %}<li><i class="icon-lock"></i> {% trans %}Locked{% endtrans %}</li>{% endif %}
+    </ul>
+  </div>
+</div>
+
+<div class="container container-primary">
+  {% if message %}
+  <div class="messages-list">
+    {{ macros.draw_message(message) }}
+  </div>
+  {% endif %}
+
+  <div class="thread-buttons">
+    {{ pager() }}
+    {% if user.is_authenticated() %}    
+    {% if acl.threads.can_reply(forum, thread) %}
+    <a href="{% url 'report_reply' thread=thread.pk, slug=thread.slug %}" class="btn btn-inverse pull-right"><i class="icon-pencil"></i> {% trans %}Reply{% endtrans %}</a>
+    {% endif %}
+    {% if watcher %}
+    <form action="{% url 'report_unwatch' thread=thread.pk, slug=thread.slug %}" class="form-inline pull-right" method="post"><input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}"><input type="hidden" name="retreat" value="{{ request_path }}"><button type="submit" class="btn btn-success tooltip-top" title="{% trans %}Remove thread from watched list{% endtrans %}"><i class="icon-bookmark"></i></button></form>
+    {% if watcher.email %}
+    <form action="{% url 'report_unwatch_email' thread=thread.pk, slug=thread.slug %}" class="form-inline pull-right" method="post"><input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}"><input type="hidden" name="retreat" value="{{ request_path }}"><button type="submit" class="btn btn-success tooltip-top" title="{% trans %}Don't e-mail me anymore if anyone replies to this thread{% endtrans %}"><i class="icon-envelope"></i></button></form>
+    {% else %}
+    <form action="{% url 'report_watch_email' thread=thread.pk, slug=thread.slug %}" class="form-inline pull-right" method="post"><input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}"><input type="hidden" name="retreat" value="{{ request_path }}"><button type="submit" class="btn tooltip-top" title="{% trans %}E-mail me if anyone replies{% endtrans %}"><i class="icon-envelope"></i></button></form>
+    {% endif %}
+    {% else %}
+    <form action="{% url 'report_watch' thread=thread.pk, slug=thread.slug %}" class="form-inline pull-right" method="post"><input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}"><input type="hidden" name="retreat" value="{{ request_path }}"><button type="submit" class="btn tooltip-top" title="{% trans %}Add thread to watched list{% endtrans %}"><i class="icon-bookmark"></i></button></form>
+    <form action="{% url 'report_watch_email' thread=thread.pk, slug=thread.slug %}" class="form-inline pull-right" method="post"><input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}"><input type="hidden" name="retreat" value="{{ request_path }}"><button type="submit" class="btn tooltip-top" title="{% trans %}Add thread to watched list and e-mail me if anyone replies{% endtrans %}"><i class="icon-envelope"></i></button></form>
+    {% endif %}
+    {% if ignored_posts %}
+    <form action="{% url 'report_show_hidden' thread=thread.pk, slug=thread.slug %}" class="form-inline pull-right" method="post"><input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}"><button type="submit" class="btn"><i class="icon-eye-open"></i> {% trans %}Show Hidden Replies{% endtrans %}</button></form>
+    {% endif %}
+    {% endif %}
+  </div>
+
+  <div class="thread-body">
+    {% for post in posts %}
+    <div id="post-{{ post.pk }}" class="post-wrapper">
+      {% if post.message %}
+      <div class="messages-list">
+        {{ macros.draw_message(post.message) }}
+      </div>
+      {% endif %}
+      {% if post.deleted and not acl.threads.can_see_deleted_posts(forum) %}
+      <div class="post-body post-muted">
+        {% if post.user_id %}
+        <a href="{% url 'user' user=post.user.pk, username=post.user.username_slug %}"><img src="{{ post.user.get_avatar(50) }}" alt="" class="user-avatar"></a>
+        {% else %}
+        <img src="{{ macros.avatar_guest(60) }}" alt="" class="user-avatar">
+        {% endif %}
+        <div class="post-content">
+          <div class="post-header">
+            <div class="post-header-compact">
+              {% if post.user_id %}
+              <a href="{% url 'user' user=post.user.pk, username=post.user.username_slug %}" class="post-author">{{ post.user.username }}</a>{% if post.user.get_title() %} {{ user_label(post.user) }}{% endif %}
+              {% else %}
+              <span class="post-author">{{ post.user_name }}</span> <span class="label post-author-label post-label-guest">{% trans %}Unregistered{% endtrans %}</span>
+              {% endif %}
+              <span class="separator">&ndash;</span>
+              <a href="{% url 'report_find' thread=thread.pk, slug=thread.slug, post=post.pk %}" class="post-date">{{ post.date|reltimesince }}</a>
+              {% if post.edits %}
+              <span class="separator">&ndash;</span>
+              {% if acl.threads.can_see_changelog(user, forum, post) %}
+              <a href="{% url 'report_changelog' thread=thread.pk, slug=thread.slug, post=post.pk %}" class="post-changelog tooltip-bottom" title="{% trans %}Show changelog{% endtrans %}">{% trans edits=post.edits %}One edit{% pluralize %}{{ edits }} edits{% endtrans %}</a>
+              {% else %}
+              <span class="post-changelog">{% trans edits=post.edits %}One edit{% pluralize %}{{ edits }} edits{% endtrans %}</span>
+              {% endif %}
+              {% endif %}
+            </div>
+
+            <a href="{% url 'report_find' thread=thread.pk, slug=thread.slug, post=post.pk %}" class="post-perma tooltip-left" title="{% trans %}Direct link to this post{% endtrans %}">#{{ pagination['start'] + loop.index }}</a>
+
+            {% if not post.is_read %}
+            <div class="post-extra">
+              <span class="label label-warning">
+                {% trans %}New{% endtrans %}
+              </span>
+            </div>
+            {% endif %}
+
+          </div>
+          <div class="post-message">
+            {% trans user=edit_user(post), date=post.edit_date|reltimesince|low %}{{ user }} has deleted this reply {{ date }}{% endtrans %}
+          </div>
+        </dv>
+      </div>
+      {% elif post.ignored %}
+      <div class="post-body post-muted">
+        <img src="{{ macros.avatar_guest(60) }}" alt="" class="user-avatar">
+        <div class="post-arrow"></div>
+        <div class="post-content">
+          <div class="post-header">
+            <div class="post-header-compact">
+              <a href="{% url 'report_find' thread=thread.pk, slug=thread.slug, post=post.pk %}" class="post-date">{{ post.date|reltimesince }}</a>
+            </div>
+
+            <a href="{% url 'report_find' thread=thread.pk, slug=thread.slug, post=post.pk %}" class="post-perma tooltip-left" title="{% trans %}Direct link to this post{% endtrans %}">#{{ pagination['start'] + loop.index }}</a>
+
+            {% if not post.is_read %}
+            <div class="post-extra">
+              <span class="label label-warning">
+                {% trans %}New{% endtrans %}
+              </span>
+            </div>
+            {% endif %}
+
+          </div>
+          <div class="post-message">
+            {% trans %}This reply was posted by user that is on your ignored list.{% endtrans %}
+          </div>
+        </dv>
+      </div>
+      {% else %}
+      <div class="post-body">
+        {% if post.user_id %}
+        <a href="{% url 'user' user=post.user.pk, username=post.user.username_slug %}"><img src="{{ post.user.get_avatar(100) }}" alt="" class="user-avatar"></a>
+        {% else %}
+        <img src="{{ macros.avatar_guest(100) }}" alt="" class="user-avatar">
+        {% endif %}
+        <div class="post-arrow"></div>
+        <div class="post-content">
+          <div class="post-header">
+            {% if post.user_id %}
+            <a href="{% url 'user' user=post.user.pk, username=post.user.username_slug %}" class="post-author">{{ post.user.username }}</a>{% if post.user.get_title() %} {{ user_label(post.user) }}{% endif %}
+            {% else %}
+            <span class="post-author">{{ post.user_name }}</span> <span class="label post-author-label post-label-guest">{% trans %}Unregistered{% endtrans %}</span>
+            {% endif %}
+            <span class="separator">&ndash;</span>
+            <a href="{% url 'report_find' thread=thread.pk, slug=thread.slug, post=post.pk %}" class="post-date">{{ post.date|reltimesince }}</a>
+            {% if post.edits %}
+            <span class="separator">&ndash;</span>
+            {% if acl.threads.can_see_changelog(user, forum, post) %}
+            <a href="{% url 'report_changelog' thread=thread.pk, slug=thread.slug, post=post.pk %}" class="post-changelog tooltip-bottom" title="{% trans %}Show changelog{% endtrans %}">{% trans edits=post.edits %}One edit{% pluralize %}{{ edits }} edits{% endtrans %}</a>
+            {% else %}
+            <span class="post-changelog">{% trans edits=post.edits %}One edit{% pluralize %}{{ edits }} edits{% endtrans %}</span>
+            {% endif %}
+            {% endif %}
+
+            {% if user.is_authenticated() and posts_form %}
+            <label class="checkbox post-checkbox"><input form="posts_form" name="{{ posts_form['list_items']['html_name'] }}" type="checkbox" class="checkbox-member" value="{{ post.pk }}"{% if posts_form['list_items']['has_value'] and ('' ~ post.pk) in posts_form['list_items']['value'] %} checked="checked"{% endif %}></label>
+            {% endif %}
+
+            <a href="{% url 'report_find' thread=thread.pk, slug=thread.slug, post=post.pk %}" class="post-perma tooltip-left" title="{% trans %}Direct link to this post{% endtrans %}">#{{ pagination['start'] + loop.index }}</a>
+
+            <div class="post-extra">
+              {% if post.protected and acl.threads.can_protect(forum) %}
+              <span class="label label-info">
+                {% trans %}Protected{% endtrans %}
+              </span>
+              {% endif %}
+
+              {% if post.deleted %}
+              <span class="label label-inverse">
+                {% trans %}Deleted{% endtrans %}
+              </span>
+              {% endif %}
+
+              {% if post.moderated %}
+              <span class="label label-purple">
+                {% trans %}Unreviewed{% endtrans %}
+              </span>
+              {% endif %}
+
+              {% if acl.threads.can_mod_posts(forum) and post.reported %}
+              <span class="label label-important">
+                {% trans %}Reported{% endtrans %}
+              </span>
+              {% endif %}
+
+              {% if not post.is_read %}
+              <span class="label label-warning">
+                {% trans %}New{% endtrans %}
+              </span>
+              {% endif %}
+            </div>
+          </div>
+          <div class="post-message">
+            <div class="markdown js-extra">
+              <article>
+                {{ post.post_preparsed|markdown_final|safe }}
+              </article>
+            </div>
+            {% if post.user.signature %}
+            <div class="post-signature">
+              <div class="markdown">
+                {{ post.user.signature_preparsed|markdown_final|safe }}
+              </div>
+            </div>
+            {% endif %}
+          </div>
+          <div class="post-footer">{% filter trim %}
+            {% if acl.threads.can_see_post_score(forum) %}
+            <div{% if user.is_authenticated() and user.pk != post.user_id %} class="post-rating-actions"{% endif %}>
+              <div class="post-rating">
+                {% if acl.threads.can_see_post_score(forum) == 1 %}
+                <span class="post-score post-score-total{% if (post.upvotes - post.downvotes) > 0 %} post-score-good{% elif (post.upvotes - post.downvotes) < 0 %} post-score-bad{% endif %}">{{ post.upvotes - post.downvotes }}</span>
+                {% elif acl.threads.can_see_post_score(forum) == 2%}
+                <span class="post-score post-score-upvotes{% if post.upvotes %} post-score-good{% endif %}">{{ post.upvotes }}</span>
+                {% endif %}
+                {% if user.is_authenticated() and user.pk != post.user_id and acl.threads.can_upvote_posts(forum) %}
+                <form action="{% url 'post_upvote' thread=thread.pk, slug=thread.slug, post=post.pk %}" class="form-inline form-upvote" method="post">
+                  <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+                  <button type="submit" class="btn btn-link post-like"{% if post.karma_vote and post.karma_vote.score > 0 %} disabled="disabled"{% endif %}>{% trans %}Like{% endtrans %}</button>
+                </form>
+                {% else %}
+                <span class="post-{% if post.upvotes %}like{% else %}neutral{% endif %}">{% trans %}Likes{% endtrans %}</span>
+                {% endif %}
+              {% if acl.threads.can_see_post_score(forum) == 2 %}
+              </div>
+              <div class="post-rating">
+                <span class="post-score post-score-downvotes{% if post.downvotes %} post-score-bad{% endif %}">{{ post.downvotes }}</span>
+              {% endif %}
+                {% if user.is_authenticated() and user.pk != post.user_id and acl.threads.can_downvote_posts(forum) %}
+                <form action="{% url 'post_downvote' thread=thread.pk, slug=thread.slug, post=post.pk %}" class="form-inline form-downvote" method="post">
+                  <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+                  <button type="submit" class="btn btn-link post-hate"{% if post.karma_vote and post.karma_vote.score < 0 %} disabled="disabled"{% endif %}>{% trans %}Dislike{% endtrans %}</button>
+                </form>
+                {% elif acl.threads.can_see_post_score(forum) == 2 %}
+                <span class="post-{% if post.downvotes %}hate{% else %}neutral{% endif %}">{% trans %}Dislikes{% endtrans %}</span>
+                {% endif %}
+              </div>
+              {% if acl.threads.can_see_post_votes(forum) %}
+              <div class="post-rating">
+                <a href="{% url 'post_votes' thread=thread.pk, slug=thread.slug, post=post.pk %}">{% trans %}Show Votes{% endtrans %}</a>
+              </div>
+              {% endif %}
+            </div>
+            {% endif %}
+
+            {% if user.is_authenticated() %}
+            <div class="post-actions">
+              {% if acl.users.can_see_users_trails() -%}
+              <a href="{% url 'post_info' thread=thread.pk, slug=thread.slug, post=post.pk %}" class="post-trail">{% trans %}Info{% endtrans %}</a>
+              {% endif %}
+              {% if acl.threads.can_edit_thread(user, forum, thread, post) and thread.start_post_id == post.pk %}
+              <a href="{% url 'report_edit' thread=thread.pk, slug=thread.slug %}" class="post-edit">{% trans %}Edit{% endtrans %}</a>
+              {% elif acl.threads.can_edit_reply(user, forum, thread, post) %}
+              <a href="{% url 'post_edit' thread=thread.pk, slug=thread.slug, post=post.pk %}" class="post-edit">{% trans %}Edit{% endtrans %}</a>
+              {%- endif %}
+              {% if acl.threads.can_reply(forum, thread) %}<a href="{% url 'report_reply' thread=thread.pk, slug=thread.slug, quote=post.pk %}" class="post-reply">{% trans %}Reply{% endtrans %}</a>{% endif %}
+            </div>
+            {% if post.pk == thread.start_post_id %}
+            <div class="post-actions">
+              {% if acl.threads.can_delete_thread(user, forum, thread, post) %}
+              {% if post.deleted %}
+              <form action="{% url 'report_show' thread=thread.pk, slug=thread.slug %}" class="form-inline" method="post">
+                <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+                <button type="submit" class="btn btn-link btn-hide tooltip-top" title="{% trans %}Make this thread visible to other users{% endtrans %}">{% trans %}Restore{% endtrans %}</button>
+              </form>
+              {% else %}
+              <form action="{% url 'report_hide' thread=thread.pk, slug=thread.slug %}" class="form-inline" method="post">
+                <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+                <button type="submit" class="btn btn-link btn-hide tooltip-top" title="{% trans %}Hide this thread from other users{% endtrans %}">{% trans %}Hide{% endtrans %}</button>
+              </form>
+              {% endif %}
+              {% endif %}
+              {% if acl.threads.can_delete_thread(user, forum, thread, post) == 2 %}
+              <form action="{% url 'report_delete' thread=thread.pk, slug=thread.slug %}" class="form-inline prompt-delete-thread" method="post">
+                <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+                <button type="submit" class="btn btn-link tooltip-top" title="{% trans %}Delete this thread for good{% endtrans %}">{% trans %}Delete{% endtrans %}</button>
+              </form>
+              {% endif %}
+            </div>
+            {% elif post.pk != thread.start_post_id and acl.threads.can_delete_post(user, forum, thread, post) %}
+            <div class="post-actions">
+              {% if acl.threads.can_delete_post(user, forum, thread, post) %}
+              {% if post.deleted %}
+              <form action="{% url 'post_show' thread=thread.pk, slug=thread.slug, post=post.pk %}" class="form-inline" method="post">
+                <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+                <button type="submit" class="btn btn-link btn-hide tooltip-top" title="{% trans %}Make this reply visible to other users{% endtrans %}">{% trans %}Restore{% endtrans %}</button>
+              </form>
+              {% else %}
+              <form action="{% url 'post_hide' thread=thread.pk, slug=thread.slug, post=post.pk %}" class="form-inline" method="post">
+                <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+                <button type="submit" class="btn btn-link btn-hide tooltip-top" title="{% trans %}Hide this reply from other users{% endtrans %}">{% trans %}Hide{% endtrans %}</button>
+              </form>
+              {% endif %}
+              {% endif %}
+              {% if acl.threads.can_delete_post(user, forum, thread, post) == 2 -%}
+              <form action="{% url 'post_delete' thread=thread.pk, slug=thread.slug, post=post.pk %}" class="form-inline prompt-delete-post" method="post">
+                <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+                <button type="submit" class="btn btn-link tooltip-top" title="{% trans %}Delete this reply for good{% endtrans %}">{% trans %}Delete{% endtrans %}</button>
+              </form>
+              {% endif %}
+            </div>
+            {% endif %}
+            {% endif %}
+          {% endfilter %}</div>
+        </div>
+      </div>
+      {% endif %}
+    </div>
+
+    {% if post.checkpoints_visible %}
+    <div class="post-checkpoints">
+      {% for checkpoint in post.checkpoints_visible %}
+      <div class="post-checkpoint{% if checkpoint.deleted %} checkpoint-deleted{% endif %}">
+        <hr>
+        <span>
+          {%- if checkpoint.action == 'limit' -%}
+          <i class="icon-lock"></i> {% trans  %}This thread has reached its post limit and has been closed.{% endtrans %}
+          {%- elif checkpoint.action == 'accepted' -%}
+          <i class="icon-ok"></i> {% trans user=checkpoint_user(checkpoint), date=checkpoint.date|reltimesince|low %}{{ user }} accepted this thread {{ date }}{% endtrans %}
+          {%- elif checkpoint.action == 'closed' -%}
+          <i class="icon-lock"></i> {% trans user=checkpoint_user(checkpoint), date=checkpoint.date|reltimesince|low %}{{ user }} closed this thread {{ date }}{% endtrans %}
+          {%- elif checkpoint.action == 'opened' -%}
+          <i class="icon-lock"></i> {% trans user=checkpoint_user(checkpoint), date=checkpoint.date|reltimesince|low %}{{ user }} opened this thread {{ date }}{% endtrans %}
+          {%- elif checkpoint.action == 'deleted' -%}
+          <i class="icon-trash"></i> {% trans user=checkpoint_user(checkpoint), date=checkpoint.date|reltimesince|low %}{{ user }} deleted this thread {{ date }}{% endtrans %}
+          {%- elif checkpoint.action == 'undeleted' -%}
+          <i class="icon-trash"></i> {% trans user=checkpoint_user(checkpoint), date=checkpoint.date|reltimesince|low %}{{ user }} restored this thread {{ date }}{% endtrans %}
+          {%- elif checkpoint.action == 'moved' and acl.forums.can_see(checkpoint.old_forum_id) -%}
+          <i class="icon-arrow-right"></i> {% trans user=checkpoint_user(checkpoint), forum=checkpoint_forum(checkpoint), date=checkpoint.date|reltimesince|low %}{{ user }} moved this thread from {{ forum }} {{ date }}{% endtrans %}
+          {%- endif -%}
+          {% if user.is_authenticated() %}
+          {% if acl.threads.can_delete_checkpoint(forum) %}
+          {% if checkpoint.deleted %}
+          <form action="{% url 'post_checkpoint_show' slug=thread.slug, thread=thread.pk, post=post.pk, checkpoint=checkpoint.pk %}" method="post" class="form-inline">
+            <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+            <button type="submit" class="btn btn-link btn-show">{% trans %}Restore{% endtrans %}</button>
+          </form>
+          {% else %}
+          <form action="{% url 'post_checkpoint_hide' slug=thread.slug, thread=thread.pk, post=post.pk, checkpoint=checkpoint.pk %}" method="post" class="form-inline">
+            <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+            <button type="submit" class="btn btn-link btn-hide">{% trans %}Hide{% endtrans %}</button>
+          </form>
+          {% endif %}
+          {% endif %}
+          {% if acl.threads.can_delete_checkpoint(forum) == 2 %}
+          <form action="{% url 'post_checkpoint_delete' slug=thread.slug, thread=thread.pk, post=post.pk, checkpoint=checkpoint.pk %}" method="post" class="form-inline prompt-delete-checkpoint">
+            <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+            <button type="submit" class="btn btn-link btn-delete">{% trans %}Delete{% endtrans %}</button>
+          </form>
+          {% endif %}
+          {% endif %}
+        </span>
+      </div>
+      {% endfor %}
+    </div>
+    {% endif %}
+    {% endfor %}
+  </div>
+
+  {% if user.is_authenticated() and (thread_form or posts_form) %}
+  <div class="thread-moderation">
+    {% if thread_form%}
+    <form id="thread_form" class="form-inline pull-left" action="{{ request_path }}" method="POST">
+      <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+      <input type="hidden" name="origin" value="thread_form">
+      {{ form_theme.input_select(thread_form['thread_action'],width=3) }}
+      <button type="submit" class="btn btn-danger">{% trans %}Go{% endtrans %}</button>
+    </form>
+    {% endif %}
+    {% if posts_form%}
+    <form id="posts_form" class="form-inline pull-right" action="{{ request_path }}" method="POST">
+      <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+      <input type="hidden" name="origin" value="posts_form">
+      {{ form_theme.input_select(posts_form['list_action'],width=3) }}
+      <button type="submit" class="btn btn-danger">{% trans %}Go{% endtrans %}</button>
+    </form>
+    {% endif %}
+  </div>
+  {% endif %}
+
+  <div class="thread-buttons">
+    {{ pager(false) }}
+    {% if user.is_authenticated() and acl.threads.can_reply(forum, thread) %}
+    <a href="{% url 'report_reply' thread=thread.pk, slug=thread.slug %}" class="btn btn-inverse pull-right"><i class="icon-pencil"></i> {% trans %}Reply{% endtrans %}</a>
+    {% elif not user.is_authenticated() and not user.is_crawler() %}
+    <p class="lead thread-signin-message"><a href="{% url 'sign_in' %}">{% trans %}Sign in or register to reply.{% endtrans %}</a></p>
+    {% endif %}
+  </div>
+
+  {% if user.is_authenticated() and acl.threads.can_reply(forum, thread) %}
+  <div class="thread-quick-reply">
+    <form action="{% url 'report_reply' thread=thread.pk, slug=thread.slug %}" method="post">
+      <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+      <input type="hidden" name="quick_reply" value="1">
+      <img src="{{ user.get_avatar(100) }}" alt="{% trans %}Your Avatar{% endtrans %}" class="user-avatar">
+      {{ editor.editor(quick_reply.post, _('Post Reply'), extra=editor_extra()) }}
+    </form>
+  </div>
+  {% endif %}
+
+</div>
+{% endblock %}
+
+{% block stylesheets %}{{ super() }}
+<link href="{{ STATIC_URL }}cranefly/highlight/styles/monokai.css" rel="stylesheet">
+{% endblock %}
+
+{% block javascripts -%}{{ super() }}
+  <script src="{{ STATIC_URL }}cranefly/highlight/highlight.pack.js"></script>
+  <script type="text/javascript">
+    hljs.tabReplace = '    ';
+    hljs.initHighlightingOnLoad();
+    EnhancePostsMD();
+    {%- if user.is_authenticated() %}
+    $(function () {
+      $('#thread_form').submit(function() {
+        if ($('#id_thread_action').val() == 'hard') {
+          var decision = confirm("{% trans %}Are you sure you want to delete this thread? This action is not reversible!{% endtrans %}");
+          return decision;
+        }
+        return true;
+      });
+      $('#posts_form').submit(function() {
+        if ($('.post-checkbox[]:checked').length == 0) {
+          alert("{% trans %}You have to select at least one post.{% endtrans %}");
+          return false;
+        }
+        if ($('#id_list_action').val() == 'hard') {
+          var decision = confirm("{% trans %}Are you sure you want to delete selected posts? This action is not reversible!{% endtrans %}");
+          return decision;
+        }
+        return true;
+      });
+      $('.prompt-delete-thread').submit(function() {
+          var decision = confirm("{% trans %}Are you sure you want to delete this thread?{% endtrans %}");
+          return decision;
+      });
+      $('.prompt-delete-post').submit(function() {
+          var decision = confirm("{% trans %}Are you sure you want to delete this post?{% endtrans %}");
+          return decision;
+      });
+      $('.prompt-delete-checkpoint').submit(function() {
+          var decision = confirm("{% trans %}Are you sure you want to delete this checkpoint?{% endtrans %}");
+          return decision;
+      });
+    });
+    {% endif %}
+  </script>
+  {% if user.is_authenticated() and acl.threads.can_reply(forum, thread) %}
+  {{ editor.js() }}
+  {% endif %}
+{%- endblock %}
+
+
+{% macro user_label(user) -%}
+<{% if user.rank and user.rank.as_tab %}a href="{% url 'users' slug=user.rank.slug %}"{% else %}span{% endif %} class="label post-author-label{% if user.rank and user.rank.style %} post-label-{{ user.rank.style }}{% endif %}">{{ user.get_title() }}</{% if user.rank and user.rank.as_tab%}a{% else %}span{% endif %}>
+{%- endmacro %}
+
+
+{% macro pager(extra=true) %}
+<div class="pagination pull-left">
+  <ul>
+    {% if pagination['total'] > 0 %}
+    <li class="count">{{ macros.pager_label(pagination) }}</li>
+    {%- if pagination['prev'] > 1 %}<li><a href="{% url 'report' slug=thread.slug, thread=thread.id %}" class="tooltip-top" title="{% trans %}Go to first page{% endtrans %}"><i class="icon-chevron-left"></i> {% trans %}First{% endtrans %}</a></li>{% endif -%}
+    {%- if pagination['prev'] > 0 %}<li><a href="{%- if pagination['prev'] > 1 %}{% url 'report' slug=thread.slug, thread=thread.id, page=pagination['prev'] %}{% else %}{% url 'report' slug=thread.slug, thread=thread.id %}{% endif %}" class="tooltip-top" title="{% trans %}Older Posts{% endtrans %}"><i class="icon-chevron-left"></i></a></li>{% endif -%}
+    {%- if pagination['next'] > 0 %}<li><a href="{% url 'report' slug=thread.slug, thread=thread.id, page=pagination['next'] %}" class="tooltip-top" title="{% trans %}Newest Posts{% endtrans %}"><i class="icon-chevron-right"></i></a></li>{% endif -%}
+    {%- if pagination['next'] > 0 and pagination['next'] < pagination['total'] %}<li><a href="{% url 'report' slug=thread.slug, thread=thread.id, page=pagination['total'] %}" class="tooltip-top" title="{% trans %}Go to last page{% endtrans %}">{% trans %}Last{% endtrans %} <i class="icon-chevron-right"></i></a></li>{% endif -%}
+    {% endif %}
+    {% if extra and user.is_authenticated() %}
+    {% if not is_read %}<li><a href="{% url 'report_new' slug=thread.slug, thread=thread.id %}" class="tooltip-top" title="{% trans %}Go to first unread{% endtrans %}"><i class="icon-star"></i> {% trans %}First Unread{% endtrans %}</a></li>{% endif %}
+    {% if thread.replies_moderated > 0 and acl.threads.can_approve(forum) %}<li><a href="{% url 'report_moderated' slug=thread.slug, thread=thread.id %}" class="tooltip-top" title="{% trans %}Go to first post awaiting review{% endtrans %}"><i class="icon-eye-close"></i> {% trans %}First Unreviewed{% endtrans %}</a></li>{% endif %}
+    {% if thread.replies_reported > 0 and acl.threads.can_mod_posts(thread) %}<li><a href="{% url 'report_reported' slug=thread.slug, thread=thread.id %}" class="tooltip-top" title="{% trans %}Go to first reported post{% endtrans %}"><i class="icon-fire"></i> {% trans %}First Reported{% endtrans %}</a></li>{% endif %}
+    {% endif %}
+  </ul>
+</div>
+{% endmacro %}
+
+
+{% macro checkpoint_user(checkpoint) -%}
+{%- if checkpoint.user_id -%}
+<a href="{{ 'user'|url(user=checkpoint.user_id, username=checkpoint.user_slug) }}">{{ checkpoint.user_name }}</a>
+{%- else -%}
+<strong>{{ checkpoint.user_name }}</strong>
+{%- endif -%}
+{%- endmacro %}
+
+
+{% macro checkpoint_forum(checkpoint) -%}
+{%- if checkpoint.old_forum_id -%}
+<a href="{% url 'forum' forum=checkpoint.old_forum_id, slug=checkpoint.old_forum_slug %}">{{ checkpoint.old_forum_name }}</a>
+{%- else -%}
+<strong>{{ checkpoint.old_forum_name }}</strong>
+{%- endif -%}
+{%- endmacro %}
+
+
+{% macro edit_user(post) -%}
+{%- if post.edit_user_id -%}
+<a href="{{ 'user'|url(user=post.edit_user_id, username=post.edit_user_slug) }}">{{ post.edit_user_name }}</a>
+{%- else -%}
+<strong>{{ post.edit_user_name }}</strong>
+{%- endif -%}
+{%- endmacro %}
+
+
+{% macro editor_extra() %}
+  <button id="editor-preview" name="preview" type="submit" class="btn pull-right">{% trans %}Full Editor{% endtrans %}</button>
+{% endmacro %}

+ 13 - 3
templates/cranefly/threads/thread.html

@@ -90,7 +90,7 @@
               {% if post.edits %}
               <span class="separator">&ndash;</span>
               {% if acl.threads.can_see_changelog(user, forum, post) %}
-              <a href="{% url 'changelog' thread=thread.pk, slug=thread.slug, post=post.pk %}" class="post-changelog tooltip-bottom" title="{% trans %}Show changelog{% endtrans %}">{% trans edits=post.edits %}One edit{% pluralize %}{{ edits }} edits{% endtrans %}</a>
+              <a href="{% url 'thread_changelog' thread=thread.pk, slug=thread.slug, post=post.pk %}" class="post-changelog tooltip-bottom" title="{% trans %}Show changelog{% endtrans %}">{% trans edits=post.edits %}One edit{% pluralize %}{{ edits }} edits{% endtrans %}</a>
               {% else %}
               <span class="post-changelog">{% trans edits=post.edits %}One edit{% pluralize %}{{ edits }} edits{% endtrans %}</span>
               {% endif %}
@@ -190,7 +190,7 @@
               </span>
               {% endif %}
 
-              {% if post.reported %}
+              {% if acl.threads.can_mod_posts(forum) and post.reported %}
               <span class="label label-important">
                 {% trans %}Reported{% endtrans %}
               </span>
@@ -257,10 +257,19 @@
             {% endif %}
 
             {% if user.is_authenticated() %}
-            <div class="post-actions">              
+            <div class="post-actions">
               {% if acl.users.can_see_users_trails() -%}
               <a href="{% url 'post_info' thread=thread.pk, slug=thread.slug, post=post.pk %}" class="post-trail">{% trans %}Info{% endtrans %}</a>
               {% endif %}
+              {% if post.reported and acl.reports.can_handle() and acl.threads.can_mod_posts(forum) %}
+              <a href="{% url 'post_report_show' thread=thread.pk, slug=thread.slug, post=post.pk %}">{% trans %}Show report{% endtrans %}</a>
+              {% endif %}
+              {% if acl.reports.can_report() %}
+              <form action="{% url 'post_report' thread=thread.pk, slug=thread.slug, post=post.pk %}" class="form-inline form-report" method="post" autocomplete="off">
+                <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+                <button type="submit" class="btn btn-link btn-report tooltip-top" title="{% trans %}Bring this post to moderator attention.{% endtrans %}">{% trans %}Report{% endtrans %}</button>
+              </form>
+              {% endif %}
               {% if acl.threads.can_edit_thread(user, forum, thread, post) and thread.start_post_id == post.pk %}
               <a href="{% url 'thread_edit' thread=thread.pk, slug=thread.slug %}" class="post-edit">{% trans %}Edit{% endtrans %}</a>
               {% elif acl.threads.can_edit_reply(user, forum, thread, post) %}
@@ -421,6 +430,7 @@
 {% block javascripts -%}{{ super() }}
   <script src="{{ STATIC_URL }}cranefly/highlight/highlight.pack.js"></script>
   <script type="text/javascript">
+    var l_post_reported = "{{ _('Reported!') }}";
     hljs.tabReplace = '    ';
     hljs.initHighlightingOnLoad();
     EnhancePostsMD();