Browse Source

- Improved paginator in threads
- Posts labels
- Jump to Post
- Stub of threads moderation

Ralfp 12 years ago
parent
commit
91be75ddc1

+ 10 - 0
misago/readstracker/trackers.py

@@ -35,6 +35,16 @@ class ThreadsTracker(object):
             self.record = Record(user=user,forum=forum,cleared=self.cutoff)
             self.record = Record(user=user,forum=forum,cleared=self.cutoff)
         self.threads = self.record.get_threads()
         self.threads = self.record.get_threads()
     
     
+    def get_read_date(self, thread):
+        if not self.user.is_authenticated():
+            return timezone.now()
+        try:
+            if self.threads[thread.pk] > self.cutoff:
+                return self.threads[thread.pk]
+        except KeyError:
+            pass
+        return self.cutoff
+        
     def is_read(self, thread):
     def is_read(self, thread):
         if not self.user.is_authenticated():
         if not self.user.is_authenticated():
             return True
             return True

+ 82 - 8
misago/threads/acl.py

@@ -1,5 +1,7 @@
-from django.utils.translation import ugettext_lazy as _
 from django import forms
 from django import forms
+from django.db import models
+from django.db.models import Q
+from django.utils.translation import ugettext_lazy as _
 from misago.acl.builder import BaseACL
 from misago.acl.builder import BaseACL
 from misago.acl.utils import ACLError403, ACLError404
 from misago.acl.utils import ACLError403, ACLError404
 from misago.forms import YesNoSwitch
 from misago.forms import YesNoSwitch
@@ -80,7 +82,7 @@ def make_forum_form(request, role, form):
                         (
                         (
                          ('can_write_posts', {'label': _("Can write posts")}),
                          ('can_write_posts', {'label': _("Can write posts")}),
                          ('can_edit_own_posts', {'label': _("Can edit own posts")}),
                          ('can_edit_own_posts', {'label': _("Can edit own posts")}),
-                         ('can_edit_own_posts', {'label': _("Can soft-delete own posts")}),
+                         ('can_soft_delete_own_posts', {'label': _("Can soft-delete own posts")}),
                         ),
                         ),
                        ),)
                        ),)
     form.layout.append((
     form.layout.append((
@@ -117,7 +119,7 @@ def make_forum_form(request, role, form):
                          ('can_edit_labels', {'label': _("Can edit thread labels")}),
                          ('can_edit_labels', {'label': _("Can edit thread labels")}),
                          ('can_see_changelog', {'label': _("Can see edits history")}),
                          ('can_see_changelog', {'label': _("Can see edits history")}),
                          ('can_make_annoucements', {'label': _("Can make annoucements")}),
                          ('can_make_annoucements', {'label': _("Can make annoucements")}),
-                         ('can_pin_threads', {'label': _("Can pin threads")}),
+                         ('can_pin_threads', {'label': _("Can make threads sticky")}),
                          ('can_edit_threads_posts', {'label': _("Can edit threads and posts")}),
                          ('can_edit_threads_posts', {'label': _("Can edit threads and posts")}),
                          ('can_move_threads_posts', {'label': _("Can move, merge and split threads and posts")}),
                          ('can_move_threads_posts', {'label': _("Can move, merge and split threads and posts")}),
                          ('can_close_threads', {'label': _("Can close threads")}),
                          ('can_close_threads', {'label': _("Can close threads")}),
@@ -131,14 +133,46 @@ def make_forum_form(request, role, form):
 
 
 
 
 class ThreadsACL(BaseACL):
 class ThreadsACL(BaseACL):
-    def allow_thread_view(self, thread):
+    def get_role(self, forum):
+        try:
+            return self.acl[forum.pk]
+        except KeyError:
+            return {}
+    
+    def allow_thread_view(self, user, thread):
         try:
         try:
             forum_role = self.acl[thread.forum.pk]
             forum_role = self.acl[thread.forum.pk]
             if forum_role['can_read_threads'] == 0:
             if forum_role['can_read_threads'] == 0:
                 raise ACLError403(_("You don't have permission to read threads in this forum."))
                 raise ACLError403(_("You don't have permission to read threads in this forum."))
+            if thread.moderated and not (forum_role['can_approve'] or (user.is_authenticated() and user == thread.start_poster)):
+                raise ACLError404()
         except KeyError:
         except KeyError:
             raise ACLError403(_("You don't have permission to read threads in this forum."))
             raise ACLError403(_("You don't have permission to read threads in this forum."))
-        
+    
+    def filter_threads(self, request, forum, queryset):
+        try:
+            forum_role = self.acl[forum.pk]
+            if not forum_role['can_approve']:
+                if request.user.is_authenticated():
+                    queryset = queryset.filter(Q(moderated=0) | Q(start_poster=request.user))
+                else:
+                    queryset = queryset.filter(moderated=0)
+        except KeyError:
+            return False
+        return queryset
+    
+    def filter_posts(self, request, thread, queryset):
+        try:
+            forum_role = self.acl[thread.forum.pk]
+            if not forum_role['can_approve']:
+                if request.user.is_authenticated():
+                    queryset = queryset.filter(Q(moderated=0) | Q(user=request.user))
+                else:
+                    queryset = queryset.filter(moderated=0)
+        except KeyError:
+            return False
+        return queryset
+    
     def can_start_threads(self, forum):
     def can_start_threads(self, forum):
         try:
         try:
             forum_role = self.acl[forum.pk]
             forum_role = self.acl[forum.pk]
@@ -176,12 +210,52 @@ class ThreadsACL(BaseACL):
             forum_role = self.acl[thread.forum.pk]
             forum_role = self.acl[thread.forum.pk]
             if forum_role['can_write_posts'] == 0:
             if forum_role['can_write_posts'] == 0:
                 raise ACLError403(_("You don't have permission to write replies in this forum."))
                 raise ACLError403(_("You don't have permission to write replies in this forum."))
-            if thread.closed and forum_role['can_close_threads'] == 0:
-                raise ACLError403(_("You can't write replies in closed threads."))
+            if forum_role['can_close_threads'] == 0:
+                if forum.closed:
+                    raise ACLError403(_("You can't write replies in closed forums."))
+                if thread.closed:
+                    raise ACLError403(_("You can't write replies in closed threads."))
         except KeyError:
         except KeyError:
             raise ACLError403(_("You don't have permission to write replies in this forum."))
             raise ACLError403(_("You don't have permission to write replies in this forum."))
+    
+    def can_approve(self, forum):
+        try:
+            forum_role = self.acl[forum.pk]
+            return forum_role['can_approve']
+        except KeyError:
+            return False
+        
+    def can_mod_threads(self, forum):
+        try:
+            forum_role = self.acl[forum.pk]
+            return (
+                    forum_role['can_approve']
+                    or forum_role['can_make_annoucements']
+                    or forum_role['can_pin_threads']
+                    or forum_role['can_move_threads_posts']
+                    or forum_role['can_close_threads']
+                    or forum_role['can_delete_threads']
+                    )
+        except KeyError:
+            return False
+        
+    def can_mod_posts(self, thread):
+        try:
+            forum_role = self.acl[thread.forum.pk]
+            return (
+                    forum_role['can_edit_threads_posts']
+                    or forum_role['can_move_threads_posts']
+                    or forum_role['can_close_threads']
+                    or forum_role['can_delete_threads']
+                    or forum_role['can_delete_posts']
+                    )
+        except KeyError:
+            return False
+        
+    def can_mod_thread(self, thread):
+        pass
+
 
 
- 
 def build_forums(acl, perms, forums, forum_roles):
 def build_forums(acl, perms, forums, forum_roles):
     acl.threads = ThreadsACL()
     acl.threads = ThreadsACL()
     for forum in forums:
     for forum in forums:

+ 5 - 2
misago/threads/models.py

@@ -13,6 +13,9 @@ class Thread(models.Model):
     name = models.CharField(max_length=255)
     name = models.CharField(max_length=255)
     slug = models.SlugField(max_length=255)
     slug = models.SlugField(max_length=255)
     replies = models.PositiveIntegerField(default=0)
     replies = models.PositiveIntegerField(default=0)
+    replies_reported = models.PositiveIntegerField(default=0)
+    replies_moderated = models.PositiveIntegerField(default=0)
+    replies_deleted = models.PositiveIntegerField(default=0)
     score = models.PositiveIntegerField(default=30,db_index=True)
     score = models.PositiveIntegerField(default=30,db_index=True)
     upvotes = models.PositiveIntegerField(default=0)
     upvotes = models.PositiveIntegerField(default=0)
     downvotes = models.PositiveIntegerField(default=0)
     downvotes = models.PositiveIntegerField(default=0)
@@ -29,7 +32,7 @@ class Thread(models.Model):
     last_poster_slug = models.SlugField(max_length=255,null=True,blank=True)
     last_poster_slug = models.SlugField(max_length=255,null=True,blank=True)
     last_poster_style = models.CharField(max_length=255,null=True,blank=True)
     last_poster_style = models.CharField(max_length=255,null=True,blank=True)
     moderated = models.BooleanField(default=False,db_index=True)
     moderated = models.BooleanField(default=False,db_index=True)
-    hidden = models.BooleanField(default=False,db_index=True)
+    deleted = models.BooleanField(default=False,db_index=True)
     closed = models.BooleanField(default=False)
     closed = models.BooleanField(default=False)
     
     
     objects = ThreadManager()
     objects = ThreadManager()
@@ -66,7 +69,7 @@ class Post(models.Model):
     edit_user_slug = models.SlugField(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)
     moderated = models.BooleanField(default=False,db_index=True)
     moderated = models.BooleanField(default=False,db_index=True)
-    hidden = models.BooleanField(default=False,db_index=True)
+    deleted = models.BooleanField(default=False,db_index=True)
     protected = models.BooleanField(default=False)
     protected = models.BooleanField(default=False)
     
     
     objects = PostManager()
     objects = PostManager()

+ 4 - 0
misago/threads/urls.py

@@ -5,6 +5,10 @@ urlpatterns = patterns('misago.threads.views',
     url(r'^forum/(?P<slug>(\w|-)+)-(?P<forum>\d+)/(?P<page>\d+)/$', 'ThreadsView', name="forum"),
     url(r'^forum/(?P<slug>(\w|-)+)-(?P<forum>\d+)/(?P<page>\d+)/$', 'ThreadsView', name="forum"),
     url(r'^forum/(?P<slug>(\w|-)+)-(?P<forum>\d+)/new/$', 'PostingView', name="thread_new", kwargs={'mode': 'new_thread'}),
     url(r'^forum/(?P<slug>(\w|-)+)-(?P<forum>\d+)/new/$', 'PostingView', name="thread_new", kwargs={'mode': 'new_thread'}),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/$', 'ThreadView', name="thread"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/$', 'ThreadView', name="thread"),
+    url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/last/$', 'LastReplyView', name="thread_last"),
+    url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/new/$', 'NewReplyView', name="thread_new"),
+    url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/moderated/$', 'FirstModeratedView', name="thread_moderated"),
+    url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/reported/$', 'FirstReportedView', name="thread_reported"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<page>\d+)/$', 'ThreadView', name="thread"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<page>\d+)/$', 'ThreadView', name="thread"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/reply/$', 'PostingView', name="thread_reply", kwargs={'mode': 'new_post'}),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/reply/$', 'PostingView', name="thread_reply", kwargs={'mode': 'new_post'}),
 )
 )

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

@@ -1,3 +1,4 @@
 from misago.threads.views.list import *
 from misago.threads.views.list import *
+from misago.threads.views.jumps import *
 from misago.threads.views.thread import *
 from misago.threads.views.thread import *
 from misago.threads.views.posting import *
 from misago.threads.views.posting import *

+ 77 - 0
misago/threads/views/jumps.py

@@ -0,0 +1,77 @@
+from django.core.urlresolvers import reverse
+from django.shortcuts import redirect
+from django.template import RequestContext
+from misago.acl.utils import ACLError403, ACLError404
+from misago.forums.models import Forum
+from misago.readstracker.trackers import ThreadsTracker
+from misago.threads.models import Thread, Post
+from misago.threads.views.base import BaseView
+from misago.views import error403, error404
+from misago.utils import make_pagination
+
+class JumpView(BaseView):
+    def fetch_thread(self, thread):
+        self.thread = Thread.objects.get(pk=thread)
+        self.forum = self.thread.forum
+        self.request.acl.forums.allow_forum_view(self.forum)
+        self.request.acl.threads.allow_thread_view(self.request.user, self.thread)
+        
+    def redirect(self, post):
+        pagination = make_pagination(0, self.request.acl.threads.filter_posts(self.request, self.thread, self.thread.post_set.filter(date__lt=post.date)).count() + 1, self.request.settings.posts_per_page)
+        if pagination['total'] > 1:
+            return redirect(reverse('thread', kwargs={'thread': self.thread.pk, 'slug': self.thread.slug, 'page': pagination['total']}) + ('#post-%s' % post.pk))
+        return redirect(reverse('thread', kwargs={'thread': self.thread.pk, 'slug': self.thread.slug}) + ('#post-%s' % post.pk))
+        
+    def make_jump(self):
+        raise NotImplementedError('JumpView cannot be called directly.')
+        
+    def __call__(self, request, slug=None, thread=None):
+        self.request = request
+        try:
+            self.fetch_thread(thread)
+            return self.make_jump()
+        except Thread.DoesNotExist:
+            return error404(self.request)
+        except ACLError403 as e:
+            return error403(request, e.message)
+        except ACLError404 as e:
+            return error404(request, e.message)
+        
+
+class LastReplyView(JumpView):
+    def make_jump(self):
+        return self.redirect(self.thread.post_set.order_by('-id')[:1][0])
+
+
+class NewReplyView(JumpView):
+    def make_jump(self):
+        if not self.request.user.is_authenticated():
+            return self.redirect(self.thread.post_set.order_by('-id')[:1][0])
+        tracker = ThreadsTracker(self.request.user, self.forum)
+        read_date = tracker.get_read_date(self.thread)
+        post = self.thread.post_set.filter(date__gt=read_date).order_by('id')[:1]
+        if not post:
+            return self.redirect(self.thread.post_set.order_by('-id')[:1][0])
+        return self.redirect(post[0])
+
+
+class FirstModeratedView(JumpView):
+    def make_jump(self):
+        if not self.request.acl.threads.can_approve(self.forum):
+            raise ACLError404()
+        try:
+            return self.redirect(
+                self.thread.post_set.get(moderated=True))
+        except Post.DoesNotExist:
+            return error404(self.request)
+
+
+class FirstReportedView(JumpView):
+    def make_jump(self):
+        if not self.request.acl.threads.can_mod_posts(self.forum):
+            raise ACLError404()
+        try:
+            return self.redirect(
+                self.thread.post_set.get(reported=True))
+        except Post.DoesNotExist:
+            return error404(self.request)

+ 84 - 7
misago/threads/views/list.py

@@ -3,14 +3,17 @@ from django.shortcuts import redirect
 from django.template import RequestContext
 from django.template import RequestContext
 from django.utils.translation import ugettext as _
 from django.utils.translation import ugettext as _
 from misago.acl.utils import ACLError403, ACLError404
 from misago.acl.utils import ACLError403, ACLError404
+from misago.forms import FormLayout, FormFields
 from misago.forums.models import Forum
 from misago.forums.models import Forum
+from misago.messages import Message
 from misago.readstracker.trackers import ForumsTracker, ThreadsTracker
 from misago.readstracker.trackers import ForumsTracker, ThreadsTracker
 from misago.threads.models import Thread, Post
 from misago.threads.models import Thread, Post
 from misago.threads.views.base import BaseView
 from misago.threads.views.base import BaseView
+from misago.threads.views.mixins import ThreadsFormMixin
 from misago.views import error403, error404
 from misago.views import error403, error404
 from misago.utils import make_pagination
 from misago.utils import make_pagination
 
 
-class ThreadsView(BaseView):
+class ThreadsView(BaseView, ThreadsFormMixin):
     def fetch_forum(self, forum):
     def fetch_forum(self, forum):
         self.forum = Forum.objects.get(pk=forum, type='forum')
         self.forum = Forum.objects.get(pk=forum, type='forum')
         self.request.acl.forums.allow_forum_view(self.forum)
         self.request.acl.forums.allow_forum_view(self.forum)
@@ -20,33 +23,107 @@ class ThreadsView(BaseView):
         self.tracker = ThreadsTracker(self.request.user, self.forum)
         self.tracker = ThreadsTracker(self.request.user, self.forum)
                 
                 
     def fetch_threads(self, page):
     def fetch_threads(self, page):
-        self.count = Thread.objects.filter(forum=self.forum).count()
-        self.threads = Thread.objects.filter(forum=self.forum).order_by('-weight', '-last').all()
+        self.count = self.request.acl.threads.filter_threads(self.request, self.forum, Thread.objects.filter(forum=self.forum)).count()
+        self.threads = self.request.acl.threads.filter_threads(self.request, self.forum, Thread.objects.filter(forum=self.forum)).order_by('-weight', '-last')
         self.pagination = make_pagination(page, self.count, self.request.settings.threads_per_page)
         self.pagination = make_pagination(page, self.count, self.request.settings.threads_per_page)
         if self.request.settings.threads_per_page < self.count:
         if self.request.settings.threads_per_page < self.count:
             self.threads = self.threads[self.pagination['start']:self.pagination['stop']]
             self.threads = self.threads[self.pagination['start']:self.pagination['stop']]
         for thread in self.threads:
         for thread in self.threads:
             thread.is_read = self.tracker.is_read(thread)
             thread.is_read = self.tracker.is_read(thread)
     
     
+    def get_thread_actions(self):
+        acl = self.request.acl.threads.get_role(self.forum)
+        actions = []
+        try:
+            if acl['can_approve']:
+               actions.append(('accept', _('Accept threads')))
+            if acl['can_make_annoucements']:
+               actions.append(('annouce', _('Change to annoucements')))
+            if acl['can_pin_threads']:
+               actions.append(('sticky', _('Change to sticky threads')))
+            if acl['can_make_annoucements'] or acl['can_pin_threads']:
+               actions.append(('normal', _('Change to standard thread')))
+            if acl['can_move_threads_posts']:
+               actions.append(('move', _('Move threads')))
+               actions.append(('merge', _('Merge threads')))
+            if acl['can_close_threads']:
+               actions.append(('open', _('Open threads')))
+               actions.append(('close', _('Close threads')))
+            if acl['can_delete_threads']:
+               actions.append(('undelete', _('Undelete threads')))
+            if acl['can_delete_threads']:
+               actions.append(('soft', _('Soft delete threads')))
+            if acl['can_delete_threads'] == 2:
+               actions.append(('hard', _('Hard delete threads')))
+        except KeyError:
+            pass
+        return actions
+    
+    def action_accept(self, ids, threads):
+        accepted = 0
+        users = []
+        for thread in self.threads.prefetch_related('last_post', 'last_post__user').all():
+            if thread.pk in ids and thread.moderated:
+                accepted += 1
+                # Sync thread and post
+                thread.moderated = False
+                thread.replies_moderated -= 1
+                thread.save(force_update=True)
+                thread.last_post.moderated = False
+                thread.last_post.save(force_update=True)
+                # Sync user
+                if thread.last_post.user:
+                    thread.last_post.user.threads += 1
+                    thread.last_post.user.posts += 1
+                    users.append(thread.last_post.user)
+                # Sync forum
+                self.forum.threads += 1
+                self.forum.threads_delta += 1
+                self.forum.posts += 1
+                self.forum.posts_delta += 1
+                if not self.forum.last_thread_date or self.forum.last_thread_date < thread.last:
+                    self.forum.last_thread = thread
+                    self.forum.last_thread_name = thread.name
+                    self.forum.last_thread_slug = thread.slug
+                    self.forum.last_thread_date = thread.last
+                    self.forum.last_poster = thread.last_poster
+                    self.forum.last_poster_name = thread.last_poster_name
+                    self.forum.last_poster_slug = thread.last_poster_slug
+                    self.forum.last_poster_style = thread.last_poster_style
+        if accepted:
+            self.request.monitor['threads'] = int(self.request.monitor['threads']) + accepted
+            self.request.monitor['posts'] = int(self.request.monitor['posts']) + accepted
+            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')
+    
     def __call__(self, request, slug=None, forum=None, page=0):
     def __call__(self, request, slug=None, forum=None, page=0):
         self.request = request
         self.request = request
         self.pagination = None
         self.pagination = None
         self.parents = None
         self.parents = None
+        self.message = request.messages.get_message('threads')
         try:
         try:
             self.fetch_forum(forum)
             self.fetch_forum(forum)
             self.fetch_threads(page)
             self.fetch_threads(page)
+            self.make_form()
+            if self.form:
+                response = self.handle_form()
+                if response:
+                    return response
         except Forum.DoesNotExist:
         except Forum.DoesNotExist:
-            return error404(self.request)
+            return error404(request)
         except ACLError403 as e:
         except ACLError403 as e:
-            return error403(args[0], e.message)
+            return error403(request, e.message)
         except ACLError404 as e:
         except ACLError404 as e:
-            return error404(args[0], e.message)
+            return error404(request, e.message)
         return request.theme.render_to_response('threads/list.html',
         return request.theme.render_to_response('threads/list.html',
                                                 {
                                                 {
-                                                 'message': request.messages.get_message('threads'),
+                                                 'message': self.message,
                                                  'forum': self.forum,
                                                  'forum': self.forum,
                                                  'parents': self.parents,
                                                  'parents': self.parents,
                                                  'count': self.count,
                                                  'count': self.count,
+                                                 'list_form': FormFields(self.form).fields if self.form else None,
                                                  'threads': self.threads,
                                                  'threads': self.threads,
                                                  'pagination': self.pagination,
                                                  'pagination': self.pagination,
                                                  },
                                                  },

+ 49 - 0
misago/threads/views/mixins.py

@@ -0,0 +1,49 @@
+from django import forms
+from django.shortcuts import redirect
+from django.utils.translation import ugettext as _
+from misago.forms import Form
+from misago.messages import Message
+
+class ThreadsFormMixin(object):
+    def make_form(self):
+        self.form = None
+        list_choices = self.get_thread_actions();
+        if (not self.request.user.is_authenticated()
+            or not list_choices):
+            return
+        
+        form_fields = {}
+        form_fields['list_action'] = forms.ChoiceField(choices=list_choices)
+        list_choices = []
+        for item in self.threads:
+            list_choices.append((item.pk, None))
+        if not list_choices:
+            return
+        form_fields['list_items'] = forms.MultipleChoiceField(choices=list_choices,widget=forms.CheckboxSelectMultiple)
+        self.form = type('ThreadsViewForm', (Form,), form_fields)
+    
+    def handle_form(self):
+        if self.request.method == 'POST':
+            self.form = self.form(self.request.POST, request=self.request)
+            if self.form.is_valid():
+                checked_items = []
+                checked_ids = []
+                for thread in self.threads:
+                    if str(thread.pk) in self.form.cleaned_data['list_items']:
+                        checked_ids.append(thread.pk)
+                        checked_items.append(thread)
+                if checked_items:
+                    form_action = getattr(self, 'action_' + self.form.cleaned_data['list_action'])
+                    response = form_action(checked_ids, checked_items)
+                    if response:
+                        return response
+                    return redirect(self.request.path)
+                else:
+                    self.message = Message(_("You have to select at least one thread."), 'error')
+            else:
+                if 'list_action' in self.form.errors:
+                    self.message = Message(_("Action requested is incorrect."), 'error')
+                else:
+                    self.message = Message(form.non_field_errors()[0], 'error')
+        else:
+            self.form = self.form(request=self.request)

+ 56 - 28
misago/threads/views/posting.py

@@ -12,7 +12,7 @@ from misago.threads.forms import PostForm
 from misago.threads.models import Thread, Post
 from misago.threads.models import Thread, Post
 from misago.threads.views.base import BaseView
 from misago.threads.views.base import BaseView
 from misago.views import error403, error404
 from misago.views import error403, error404
-from misago.utils import slugify
+from misago.utils import make_pagination, slugify
 
 
 class PostingView(BaseView):
 class PostingView(BaseView):
     def fetch_target(self, kwargs):
     def fetch_target(self, kwargs):
@@ -31,6 +31,7 @@ class PostingView(BaseView):
         self.thread = Thread.objects.get(pk=kwargs['thread'])
         self.thread = Thread.objects.get(pk=kwargs['thread'])
         self.forum = self.thread.forum
         self.forum = self.thread.forum
         self.request.acl.forums.allow_forum_view(self.forum)
         self.request.acl.forums.allow_forum_view(self.forum)
+        self.request.acl.threads.allow_thread_view(self.request.user, self.thread)
         self.request.acl.threads.allow_reply(self.thread)
         self.request.acl.threads.allow_reply(self.thread)
         self.parents = self.forum.get_ancestors(include_self=True).filter(level__gt=1)
         self.parents = self.forum.get_ancestors(include_self=True).filter(level__gt=1)
         
         
@@ -64,15 +65,24 @@ class PostingView(BaseView):
             form = self.get_form(True)
             form = self.get_form(True)
             if form.is_valid():
             if form.is_valid():
                 now = timezone.now()
                 now = timezone.now()
+                moderation = False
+                if not request.acl.threads.acl[self.forum.pk]['can_approve']:
+                    if self.mode == 'new_thread' and request.acl.threads.acl[self.forum.pk]['can_start_threads'] == 1:
+                        moderation = True
+                    if self.mode in ['new_post', 'new_post_quick'] and request.acl.threads.acl[self.forum.pk]['can_write_posts'] == 1:
+                        moderation = True 
                 # Get or create new thread
                 # Get or create new thread
-                if self.mode in ['new_thread', 'edit_thread']:
+                if self.mode in ['new_thread']:
                     thread = Thread.objects.create(
                     thread = Thread.objects.create(
                                                    forum=self.forum,
                                                    forum=self.forum,
                                                    name=form.cleaned_data['thread_name'],
                                                    name=form.cleaned_data['thread_name'],
                                                    slug=slugify(form.cleaned_data['thread_name']),
                                                    slug=slugify(form.cleaned_data['thread_name']),
                                                    start=now,
                                                    start=now,
                                                    last=now,
                                                    last=now,
+                                                   moderated=moderation,
                                                    )
                                                    )
+                    if moderation:
+                        thread.replies_moderated += 1
                 else:
                 else:
                     thread = self.thread
                     thread = self.thread
                 
                 
@@ -86,7 +96,8 @@ class PostingView(BaseView):
                                            agent=request.META.get('HTTP_USER_AGENT'),
                                            agent=request.META.get('HTTP_USER_AGENT'),
                                            post=form.cleaned_data['post'],
                                            post=form.cleaned_data['post'],
                                            post_preparsed=post_markdown(request, form.cleaned_data['post']),
                                            post_preparsed=post_markdown(request, form.cleaned_data['post']),
-                                           date=now
+                                           date=now,
+                                           moderated=moderation,
                                            )
                                            )
                 
                 
                 if self.mode == 'new_thread':
                 if self.mode == 'new_thread':
@@ -96,7 +107,7 @@ class PostingView(BaseView):
                     thread.start_poster_slug = request.user.username_slug
                     thread.start_poster_slug = request.user.username_slug
                     if request.user.rank and request.user.rank.style:
                     if request.user.rank and request.user.rank.style:
                         thread.start_poster_style = request.user.rank.style
                         thread.start_poster_style = request.user.rank.style
-                    
+                
                 thread.last = now
                 thread.last = now
                 thread.last_post = post
                 thread.last_post = post
                 thread.last_poster = request.user
                 thread.last_poster = request.user
@@ -105,41 +116,58 @@ class PostingView(BaseView):
                 if request.user.rank and request.user.rank.style:
                 if request.user.rank and request.user.rank.style:
                     thread.last_poster_style = request.user.rank.style
                     thread.last_poster_style = request.user.rank.style
                 if self.mode in ['new_post', 'new_post_quick']:
                 if self.mode in ['new_post', 'new_post_quick']:
-                    thread.replies += 1
+                    if moderation:
+                        thread.replies_moderated += 1
+                    else:
+                        thread.replies += 1
+                    thread.score += 5
                 thread.save(force_update=True)
                 thread.save(force_update=True)
                 
                 
-                # Update forum
-                if self.mode == 'new_thread':
-                    self.forum.threads += 1
-                    self.forum.threads_delta += 1
-                    
-                if self.mode in ['new_post', 'new_post_quick']:
-                    self.forum.posts += 1
-                    self.forum.posts_delta += 1
-                    
-                self.forum.last_thread = thread
-                self.forum.last_thread_name = thread.name
-                self.forum.last_thread_slug = thread.slug
-                self.forum.last_thread_date = thread.last
-                self.forum.last_poster = thread.last_poster
-                self.forum.last_poster_name = thread.last_poster_name
-                self.forum.last_poster_slug = thread.last_poster_slug
-                self.forum.last_poster_style = thread.last_poster_style
-                self.forum.save(force_update=True)
+                # Update forum and monitor
+                if not moderation:
+                    if self.mode == 'new_thread':
+                        self.request.monitor['threads'] = int(self.request.monitor['threads']) + 1
+                        self.forum.threads += 1
+                        self.forum.threads_delta += 1
+                        
+                    if self.mode in ['new_thread', 'new_post', 'new_post_quick']:
+                        self.request.monitor['posts'] = int(self.request.monitor['posts']) + 1
+                        self.forum.posts += 1
+                        self.forum.posts_delta += 1
+                        
+                    self.forum.last_thread = thread
+                    self.forum.last_thread_name = thread.name
+                    self.forum.last_thread_slug = thread.slug
+                    self.forum.last_thread_date = thread.last
+                    self.forum.last_poster = thread.last_poster
+                    self.forum.last_poster_name = thread.last_poster_name
+                    self.forum.last_poster_slug = thread.last_poster_slug
+                    self.forum.last_poster_style = thread.last_poster_style
+                    self.forum.save(force_update=True)
                 
                 
                 # Update user
                 # Update user
-                if self.mode == 'new_thread':
-                    request.user.threads += 1
-                request.user.posts += 1
+                if not moderation:
+                    if self.mode == 'new_thread':
+                        request.user.threads += 1
+                    request.user.posts += 1
                 request.user.last_post = thread.last
                 request.user.last_post = thread.last
                 request.user.save(force_update=True)
                 request.user.save(force_update=True)
                 
                 
                 if self.mode == 'new_thread':
                 if self.mode == 'new_thread':
-                    request.messages.set_flash(Message(_("New thread has been posted.")), 'success', 'threads')
+                    if moderation:
+                        request.messages.set_flash(Message(_("New thread has been posted. It will be hidden from other members until moderator reviews it.")), 'success', 'threads')
+                    else:
+                        request.messages.set_flash(Message(_("New thread has been posted.")), 'success', 'threads')
                     return redirect(reverse('thread', kwargs={'thread': thread.pk, 'slug': thread.slug}) + ('#post-%s' % post.pk))
                     return redirect(reverse('thread', kwargs={'thread': thread.pk, 'slug': thread.slug}) + ('#post-%s' % post.pk))
                 
                 
                 if self.mode in ['new_post', 'new_post_quick']:
                 if self.mode in ['new_post', 'new_post_quick']:
-                    request.messages.set_flash(Message(_("Your reply has been posted.")), 'success', 'threads')
+                    if moderation:
+                        request.messages.set_flash(Message(_("Your reply has been posted. It will be hidden from other members until moderator reviews it.")), 'success', 'threads_%s' % post.pk)
+                    else:
+                        request.messages.set_flash(Message(_("Your reply has been posted.")), 'success', 'threads_%s' % post.pk)
+                    pagination = make_pagination(0, self.request.acl.threads.filter_posts(self.request, self.thread, self.thread.post_set).count(), self.request.settings.posts_per_page)
+                    if pagination['total'] > 1:
+                        return redirect(reverse('thread', kwargs={'thread': self.thread.pk, 'slug': self.thread.slug, 'page': pagination['total']}) + ('#post-%s' % post.pk))
                     return redirect(reverse('thread', kwargs={'thread': self.thread.pk, 'slug': self.thread.slug}) + ('#post-%s' % post.pk))
                     return redirect(reverse('thread', kwargs={'thread': self.thread.pk, 'slug': self.thread.slug}) + ('#post-%s' % post.pk))
             message = Message(form.non_field_errors()[0], 'error')
             message = Message(form.non_field_errors()[0], 'error')
         else:
         else:

+ 12 - 7
misago/threads/views/thread.py

@@ -17,21 +17,25 @@ class ThreadView(BaseView):
         self.thread = Thread.objects.get(pk=thread)
         self.thread = Thread.objects.get(pk=thread)
         self.forum = self.thread.forum
         self.forum = self.thread.forum
         self.request.acl.forums.allow_forum_view(self.forum)
         self.request.acl.forums.allow_forum_view(self.forum)
-        self.request.acl.threads.allow_thread_view(self.thread)
+        self.request.acl.threads.allow_thread_view(self.request.user, self.thread)
         self.parents = self.forum.get_ancestors(include_self=True).filter(level__gt=1)
         self.parents = self.forum.get_ancestors(include_self=True).filter(level__gt=1)
         self.tracker = ThreadsTracker(self.request.user, self.forum)
         self.tracker = ThreadsTracker(self.request.user, self.forum)
     
     
     def fetch_posts(self, page):
     def fetch_posts(self, page):
-        self.count = Post.objects.filter(thread=self.thread).count()
-        self.posts = Post.objects.filter(thread=self.thread).order_by('pk').all().prefetch_related('user', 'user__rank')
+        self.count = self.request.acl.threads.filter_posts(self.request, self.thread, Post.objects.filter(thread=self.thread)).count()
+        self.posts = self.request.acl.threads.filter_posts(self.request, self.thread, Post.objects.filter(thread=self.thread)).order_by('pk').prefetch_related('user', 'user__rank')
         self.pagination = make_pagination(page, self.count, self.request.settings.posts_per_page)
         self.pagination = make_pagination(page, self.count, self.request.settings.posts_per_page)
-        if self.request.settings.threads_per_page < self.count:
+        if self.request.settings.posts_per_page < self.count:
             self.posts = self.posts[self.pagination['start']:self.pagination['stop']]
             self.posts = self.posts[self.pagination['start']:self.pagination['stop']]
+        self.read_date = self.tracker.get_read_date(self.thread) 
+        for post in self.posts:
+            post.message = self.request.messages.get_message('threads_%s' % post.pk)
+            post.is_read = post.date <= self.read_date
         last_post = self.posts[len(self.posts) - 1]
         last_post = self.posts[len(self.posts) - 1]
         if not self.tracker.is_read(self.thread):
         if not self.tracker.is_read(self.thread):
             self.tracker.set_read(self.thread, last_post)
             self.tracker.set_read(self.thread, last_post)
             self.tracker.sync()
             self.tracker.sync()
-        
+     
     def __call__(self, request, slug=None, thread=None, page=0):
     def __call__(self, request, slug=None, thread=None, page=0):
         self.request = request
         self.request = request
         self.pagination = None
         self.pagination = None
@@ -42,15 +46,16 @@ class ThreadView(BaseView):
         except Thread.DoesNotExist:
         except Thread.DoesNotExist:
             return error404(self.request)
             return error404(self.request)
         except ACLError403 as e:
         except ACLError403 as e:
-            return error403(args[0], e.message)
+            return error403(request, e.message)
         except ACLError404 as e:
         except ACLError404 as e:
-            return error404(args[0], e.message)
+            return error404(request, e.message)
         return request.theme.render_to_response('threads/thread.html',
         return request.theme.render_to_response('threads/thread.html',
                                                 {
                                                 {
                                                  'message': request.messages.get_message('threads'),
                                                  'message': request.messages.get_message('threads'),
                                                  'forum': self.forum,
                                                  'forum': self.forum,
                                                  'parents': self.parents,
                                                  'parents': self.parents,
                                                  'thread': self.thread,
                                                  'thread': self.thread,
+                                                 'is_read': self.tracker.is_read(self.thread),
                                                  'count': self.count,
                                                  'count': self.count,
                                                  'posts': self.posts,
                                                  'posts': self.posts,
                                                  'pagination': self.pagination,
                                                  'pagination': self.pagination,

+ 4 - 0
misago/utils/__init__.py

@@ -84,6 +84,10 @@ def make_pagination(page, total, max):
             pagination['prev'] = pagination['page'] - 1
             pagination['prev'] = pagination['page'] - 1
         if pagination['page'] < pagination['total']:
         if pagination['page'] < pagination['total']:
             pagination['next'] = pagination['page'] + 1
             pagination['next'] = pagination['page'] + 1
+
+    # Fix empty pagers
+    if not pagination['total']:
+        pagination['total'] = 1 
             
             
     # Set stop offset
     # Set stop offset
     pagination['stop'] = pagination['start'] + max
     pagination['stop'] = pagination['start'] + max

+ 19 - 8
static/sora/css/sora.css

@@ -846,10 +846,7 @@ textarea{resize:vertical;}
 .form-avatar-select .form-button{margin-bottom:4px;}
 .form-avatar-select .form-button{margin-bottom:4px;}
 .form-avatar-select .form-button:hover img{border:1px solid #0088cc;border:1px solid #0088cc;-webkit-box-shadow:0 1px 3px #0088cc;-moz-box-shadow:0 1px 3px #0088cc;box-shadow:0 1px 3px #0088cc;}
 .form-avatar-select .form-button:hover img{border:1px solid #0088cc;border:1px solid #0088cc;-webkit-box-shadow:0 1px 3px #0088cc;-moz-box-shadow:0 1px 3px #0088cc;box-shadow:0 1px 3px #0088cc;}
 .form-avatar-select hr{margin-top:16px;}
 .form-avatar-select hr{margin-top:16px;}
-.table-footer{background:#e8e8e8;border-top:1px solid #cfcfcf;-webkit-border-radius:0px 0px 3px 3px;-moz-border-radius:0px 0px 3px 3px;border-radius:0px 0px 3px 3px;margin-bottom:0px;padding:0px 8px;position:relative;bottom:20px;}.table-footer .pager{margin:0px 0px;margin-top:9px;padding:0px;margin-right:6px;}.table-footer .pager>li{margin-right:6px;}.table-footer .pager>li>a:link,.table-footer .pager>li>a:active,.table-footer .pager>li>a:visited{background:#e8e8e8;border:none;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;padding:2px 5px;}
-.table-footer .pager>li>a:hover{background-color:#0088cc;}.table-footer .pager>li>a:hover i{background-image:url("../img/glyphicons-halflings-white.png");}
-.table-footer .table-count{padding:11px 0px;color:#555555;}
-.table-footer .form-inline{margin:0px;padding:6px 0px;}
+.table-footer{background:#e8e8e8;border-top:1px solid #cfcfcf;-webkit-border-radius:0px 0px 3px 3px;-moz-border-radius:0px 0px 3px 3px;border-radius:0px 0px 3px 3px;margin-top:-20px;margin-bottom:20px;overflow:auto;padding:0px 12px;}.table-footer .form-inline{margin:0px;padding:6px 0px;}
 td.check-cell,th.check-cell{width:32px;}
 td.check-cell,th.check-cell{width:32px;}
 td .checkbox,th .checkbox{margin-bottom:0px;position:relative;bottom:1px;}td .checkbox input,th .checkbox input{position:relative;left:9px;}
 td .checkbox,th .checkbox{margin-bottom:0px;position:relative;bottom:1px;}td .checkbox input,th .checkbox input{position:relative;left:9px;}
 td.lead-cell{color:#555555;font-weight:bold;}
 td.lead-cell{color:#555555;font-weight:bold;}
@@ -876,6 +873,7 @@ td.lead-cell{color:#555555;font-weight:bold;}
 .btn.btn-danger{background:#d13327;border:1px solid #d13327;*border:0;background:#d13327;border:1px solid #d13327;*border:0;color:#f7f7f7;text-shadow:0px 1px 0px #bb2d23;}.btn.btn-danger:hover,.btn.btn-danger:active{background:#e05f55;border:1px solid #e05f55;*border:0;}
 .btn.btn-danger{background:#d13327;border:1px solid #d13327;*border:0;background:#d13327;border:1px solid #d13327;*border:0;color:#f7f7f7;text-shadow:0px 1px 0px #bb2d23;}.btn.btn-danger:hover,.btn.btn-danger:active{background:#e05f55;border:1px solid #e05f55;*border:0;}
 .btn.btn-inverse{background:#555555;border:1px solid #555555;*border:0;background:#555555;border:1px solid #555555;*border:0;color:#f7f7f7;text-shadow:0px 1px 0px #484848;}.btn.btn-inverse:hover,.btn.btn-inverse:active{background:#7e7e7e;border:1px solid #7e7e7e;*border:0;}
 .btn.btn-inverse{background:#555555;border:1px solid #555555;*border:0;background:#555555;border:1px solid #555555;*border:0;color:#f7f7f7;text-shadow:0px 1px 0px #484848;}.btn.btn-inverse:hover,.btn.btn-inverse:active{background:#7e7e7e;border:1px solid #7e7e7e;*border:0;}
 .btn.btn-link{background:none;border:none;}.btn.btn-link:hover,.btn.btn-link:active{color:#00aaff;text-decoration:none;}
 .btn.btn-link{background:none;border:none;}.btn.btn-link:hover,.btn.btn-link:active{color:#00aaff;text-decoration:none;}
+.btn.btn-large{font-size:180%;padding:10px 16px;}
 .alerts-global{margin-top:16px;}
 .alerts-global{margin-top:16px;}
 .alert-form{margin:0px;margin-bottom:16px;}.alert-form p{font-weight:normal;}
 .alert-form{margin:0px;margin-bottom:16px;}.alert-form p{font-weight:normal;}
 .alert-inline{margin:0px;padding:0px;}
 .alert-inline{margin:0px;padding:0px;}
@@ -920,7 +918,13 @@ td.lead-cell{color:#555555;font-weight:bold;}
 .nav-pills li.primary a:active,.nav-pills li.primary a:hover{background-color:#0088cc !important;}
 .nav-pills li.primary a:active,.nav-pills li.primary a:hover{background-color:#0088cc !important;}
 .pager{margin:0px 0px;margin-top:6px;padding:0px;margin-right:6px;}.pager>li{margin-right:6px;}.pager>li>a:link,.pager>li>a:active,.pager>li>a:visited{background:#e8e8e8;border:none;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;padding:2px 5px;color:#333333;font-weight:bold;}
 .pager{margin:0px 0px;margin-top:6px;padding:0px;margin-right:6px;}.pager>li{margin-right:6px;}.pager>li>a:link,.pager>li>a:active,.pager>li>a:visited{background:#e8e8e8;border:none;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;padding:2px 5px;color:#333333;font-weight:bold;}
 .pager>li>a:hover{background-color:#0088cc;color:#ffffff;}.pager>li>a:hover i{background-image:url("../img/glyphicons-halflings-white.png");}
 .pager>li>a:hover{background-color:#0088cc;color:#ffffff;}.pager>li>a:hover i{background-image:url("../img/glyphicons-halflings-white.png");}
-.pager>li.count{color:#808080;}
+.pager>li.unread>a:link,.pager>li.unread>a:active,.pager>li.unread>a:visited{background:#c67605;color:#ffffff;}.pager>li.unread>a:link i,.pager>li.unread>a:active i,.pager>li.unread>a:visited i{background-image:url("../img/glyphicons-halflings-white.png");}
+.pager>li.unread>a:hover{background:#faa937;}
+.pager>li.moderated>a:link,.pager>li.moderated>a:active,.pager>li.moderated>a:visited{background:#613591;color:#ffffff;}.pager>li.moderated>a:link i,.pager>li.moderated>a:active i,.pager>li.moderated>a:visited i{background-image:url("../img/glyphicons-halflings-white.png");}
+.pager>li.moderated>a:hover{background:#9466c6;}
+.pager>li.reported>a:link,.pager>li.reported>a:active,.pager>li.reported>a:visited{background:#9d261d;color:#ffffff;}.pager>li.reported>a:link i,.pager>li.reported>a:active i,.pager>li.reported>a:visited i{background-image:url("../img/glyphicons-halflings-white.png");}
+.pager>li.reported>a:hover{background:#c83025;}
+.pager>li.count{color:#808080;margin-right:24px;}
 .navbar-fixed-top{position:static;}
 .navbar-fixed-top{position:static;}
 .navbar-userbar .navbar-inner{background:none;background-color:#fcfcfc;border-bottom:4px solid #e3e3e3;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;overflow:auto;}
 .navbar-userbar .navbar-inner{background:none;background-color:#fcfcfc;border-bottom:4px solid #e3e3e3;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;overflow:auto;}
 .navbar-userbar li a,.navbar-userbar li a:link,.navbar-userbar li a:active,.navbar-userbar li a:visited,.navbar-userbar li button.btn-link{opacity:0.5;filter:alpha(opacity=50);color:#000000;font-weight:bold;}.navbar-userbar li a i,.navbar-userbar li a:link i,.navbar-userbar li a:active i,.navbar-userbar li a:visited i,.navbar-userbar li button.btn-link i{opacity:1;filter:alpha(opacity=100);}
 .navbar-userbar li a,.navbar-userbar li a:link,.navbar-userbar li a:active,.navbar-userbar li a:visited,.navbar-userbar li button.btn-link{opacity:0.5;filter:alpha(opacity=50);color:#000000;font-weight:bold;}.navbar-userbar li a i,.navbar-userbar li a:link i,.navbar-userbar li a:active i,.navbar-userbar li a:visited i,.navbar-userbar li button.btn-link i{opacity:1;filter:alpha(opacity=100);}
@@ -959,15 +963,22 @@ td.lead-cell{color:#555555;font-weight:bold;}
 .forum-list-side{padding-top:10px;}
 .forum-list-side{padding-top:10px;}
 .subforums-list{margin:0px;position:relative;bottom:32px;}.subforums-list .category{margin-bottom:-24px;}
 .subforums-list{margin:0px;position:relative;bottom:32px;}.subforums-list .category{margin-bottom:-24px;}
 .threads-list .thread-icon{background-color:#eeeeee;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;padding:5px 6px;}
 .threads-list .thread-icon{background-color:#eeeeee;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;padding:5px 6px;}
+.threads-list .jump{-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;padding:3px 4px;margin:-3px 0px;opacity:0.3;filter:alpha(opacity=30);}.threads-list .jump:hover{opacity:1;filter:alpha(opacity=100);}.threads-list .jump:hover.jump-new{background-color:#f89406;}
+.threads-list .jump:hover.jump-last{background-color:#049cdb;}
+.threads-list .jump:hover i{background-image:url("../img/glyphicons-halflings-white.png");}
+.threads-list .thread-stat{text-align:right;}
 .threads-list .thread-closed{background-color:#9d261d;}
 .threads-list .thread-closed{background-color:#9d261d;}
 .threads-list .thread-new{background-color:#0088cc;}
 .threads-list .thread-new{background-color:#0088cc;}
+.threads-list .thread-flags{float:right;margin:0px;padding:0px;}.threads-list .thread-flags li{float:right;margin:0px;margin-left:6px;padding:0px;}
 .thread-info{overflow:auto;}.thread-info li{float:left;margin-right:16px;opacity:0.5;filter:alpha(opacity=50);font-weight:bold;}.thread-info li a{color:#333333;}
 .thread-info{overflow:auto;}.thread-info li{float:left;margin-right:16px;opacity:0.5;filter:alpha(opacity=50);font-weight:bold;}.thread-info li a{color:#333333;}
-.posts-list{margin-top:12px;margin-bottom:20px;}.posts-list .well-post{margin:0px;margin-bottom:16px;overflow:auto;padding:16px 0px;}.posts-list .well-post .post-author{overflow:auto;float:left;width:284px;position:relative;bottom:4px;}.posts-list .well-post .post-author .avatar-normal{float:left;margin:4px 0px;margin-left:16px;width:80px;height:80px;-webkit-box-shadow:0px 0px 4px #999999;-moz-box-shadow:0px 0px 4px #999999;box-shadow:0px 0px 4px #999999;}
+.posts-list{margin-top:12px;margin-bottom:20px;}.posts-list .well-post{margin:0px;margin-bottom:16px;overflow:auto;padding:16px 0px;}.posts-list .well-post .post-author{overflow:auto;float:left;width:286px;position:relative;bottom:4px;}.posts-list .well-post .post-author .avatar-normal{float:left;margin:4px 0px;margin-left:16px;width:80px;height:80px;-webkit-box-shadow:0px 0px 4px #999999;-moz-box-shadow:0px 0px 4px #999999;box-shadow:0px 0px 4px #999999;}
 .posts-list .well-post .post-author .post-bit{float:left;margin-left:12px;padding-top:4px;font-weight:bold;font-size:120%;}.posts-list .well-post .post-author .post-bit p{margin:0px;}
 .posts-list .well-post .post-author .post-bit{float:left;margin-left:12px;padding-top:4px;font-weight:bold;font-size:120%;}.posts-list .well-post .post-author .post-bit p{margin:0px;}
 .posts-list .well-post .post-author .post-bit .lead{font-size:150%;}
 .posts-list .well-post .post-author .post-bit .lead{font-size:150%;}
 .posts-list .well-post .post-author .post-bit .user-title{color:#555555;}
 .posts-list .well-post .post-author .post-bit .user-title{color:#555555;}
-.posts-list .well-post .post-author .post-bit .post-date{margin-top:4px;color:#999999;font-weight:normal;}
-.posts-list .well-post .post-content{margin-left:284px;padding:0px 16px;}.posts-list .well-post .post-content .post-foot{margin-top:20px;}.posts-list .well-post .post-content .post-foot .lead{margin:0px;color:#999999;font-size:100%;}.posts-list .well-post .post-content .post-foot .lead a{color:#999999;}
+.posts-list .well-post .post-author .post-bit .post-date{margin-top:4px;color:#999999;font-size:70%;font-weight:normal;}
+.posts-list .well-post .post-content{margin-left:286px;padding:0px 16px;}.posts-list .well-post .post-content .label{margin-left:8px;padding:4px 5px;font-size:100%;}
+.posts-list .well-post .post-content .label-purple{background-color:#7a43b6;}
+.posts-list .well-post .post-content .post-foot{margin-top:20px;}.posts-list .well-post .post-content .post-foot .lead{margin:0px;color:#999999;font-size:100%;}.posts-list .well-post .post-content .post-foot .lead a{color:#999999;}
 .posts-list .well-post .post-content .post-foot .signature{border-top:1px solid #eeeeee;padding-top:12px;}.posts-list .well-post .post-content .post-foot .signature .markdown{opacity:0.7;filter:alpha(opacity=70);}
 .posts-list .well-post .post-content .post-foot .signature{border-top:1px solid #eeeeee;padding-top:12px;}.posts-list .well-post .post-content .post-foot .signature .markdown{opacity:0.7;filter:alpha(opacity=70);}
 .quick-reply{margin-top:12px;overflow:auto;}.quick-reply .avatar-big,.quick-reply .arrow{float:left;}
 .quick-reply{margin-top:12px;overflow:auto;}.quick-reply .avatar-big,.quick-reply .arrow{float:left;}
 .quick-reply .arrow{width:0;height:0;border-top:12px solid transparent;border-bottom:12px solid transparent;border-right:12px solid #e3e3e3;position:relative;top:12px;left:5px;}
 .quick-reply .arrow{width:0;height:0;border-top:12px solid transparent;border-bottom:12px solid transparent;border-right:12px solid #e3e3e3;position:relative;top:12px;left:5px;}

+ 5 - 0
static/sora/css/sora/buttons.less

@@ -102,4 +102,9 @@
       text-decoration: none;
       text-decoration: none;
     }
     }
   }
   }
+  
+  &.btn-large {
+    font-size: 180%;
+    padding: 10px 16px;
+  }
 }
 }

+ 49 - 0
static/sora/css/sora/navs.less

@@ -195,8 +195,57 @@
       }
       }
     }
     }
     
     
+    &.unread>a {
+      &:link, &:active, &:visited {
+        background: darken(@orange, 10%);
+        
+        color: @white;
+        
+        i {
+          background-image: url("@{iconWhiteSpritePath}");          
+        }
+      }
+    
+      &:hover {
+        background: lighten(@orange, 10%);
+      }
+    }
+    
+    &.moderated>a {
+      &:link, &:active, &:visited {
+        background: darken(@purple, 10%);
+        
+        color: @white;
+        
+        i {
+          background-image: url("@{iconWhiteSpritePath}");          
+        }
+      }
+    
+      &:hover {
+        background: lighten(@purple, 10%);
+      }
+    }
+    
+    &.reported>a {
+      &:link, &:active, &:visited {
+        background: @red;
+        
+        color: @white;
+        
+        i {
+          background-image: url("@{iconWhiteSpritePath}");          
+        }
+      }
+    
+      &:hover {
+        background: lighten(@red, 10%);
+      }
+    }
+    
     &.count {
     &.count {
       color: lighten(@textColor, 30%);
       color: lighten(@textColor, 30%);
+      margin-right: 24px;
     }
     }
   }
   }
 }
 }

+ 4 - 38
static/sora/css/sora/tables.less

@@ -4,44 +4,10 @@
   background: darken(@bodyBackground, 8%);
   background: darken(@bodyBackground, 8%);
   border-top: 1px solid darken(@bodyBackground, 18%);
   border-top: 1px solid darken(@bodyBackground, 18%);
   .border-radius(0px 0px 3px 3px);
   .border-radius(0px 0px 3px 3px);
-  margin-bottom: 0px;
-  padding: 0px 8px;
-  position: relative;
-  bottom: 20px;
-  
-  .pager {
-    margin: 0px 0px;
-    margin-top: 9px;
-    padding: 0px;
-    margin-right: 6px;
-    
-    &>li {
-      margin-right: 6px;
-    
-      &>a {
-        &:link, &:active, &:visited {
-          background: darken(@bodyBackground, 8%);
-          border: none;
-          .border-radius(3px);
-          padding: 2px 5px;
-        }
-      
-        &:hover {
-          background-color: @linkColor;
-          
-          i {
-            background-image: url("@{iconWhiteSpritePath}");          
-          }
-        }
-      }
-    }
-  }
-  
-  .table-count {
-    padding: 11px 0px;
-    
-    color: @gray;
-  }
+  margin-top: -20px;
+  margin-bottom: 20px;
+  overflow: auto;
+  padding: 0px 12px;
   
   
   .form-inline {
   .form-inline {
     margin: 0px;
     margin: 0px;

+ 54 - 3
static/sora/css/sora/threads.less

@@ -19,6 +19,33 @@
     padding: 5px 6px;
     padding: 5px 6px;
   }
   }
   
   
+  .jump { 
+    .border-radius(3px);
+    padding: 3px 4px;
+    margin: -3px 0px;
+    .opacity(30);
+        
+    &:hover {
+      .opacity(100);
+      
+      &.jump-new {
+        background-color: @orange;
+      }
+      
+      &.jump-last {
+        background-color: @blue;
+      }
+      
+      i {
+        background-image: url("@{iconWhiteSpritePath}");
+      }
+    }
+  }
+  
+  .thread-stat {
+    text-align: right;
+  }
+  
   .thread-closed {
   .thread-closed {
     background-color: @red; 
     background-color: @red; 
   }
   }
@@ -26,6 +53,19 @@
   .thread-new {
   .thread-new {
     background-color: @linkColor; 
     background-color: @linkColor; 
   }
   }
+  
+  .thread-flags {
+    float: right;
+    margin: 0px;
+    padding: 0px;
+    
+    li {
+      float: right;
+      margin: 0px;
+      margin-left: 6px;
+      padding: 0px;
+    }
+  }
 }
 }
 
 
 // Thread view
 // Thread view
@@ -59,7 +99,7 @@
     .post-author{
     .post-author{
       overflow: auto;
       overflow: auto;
       float: left;
       float: left;
-      width: 284px;
+      width: 286px;
       position: relative;
       position: relative;
       bottom: 4px;
       bottom: 4px;
       
       
@@ -84,7 +124,7 @@
           margin: 0px;
           margin: 0px;
         }
         }
         
         
-        .lead {          
+        .lead {
           font-size: 150%;
           font-size: 150%;
         }
         }
         
         
@@ -96,15 +136,26 @@
           margin-top: 4px;
           margin-top: 4px;
           
           
           color: @grayLight;
           color: @grayLight;
+          font-size: 70%;
           font-weight: normal
           font-weight: normal
         }
         }
       }
       }
     }
     }
 
 
     .post-content {
     .post-content {
-      margin-left: 284px;
+      margin-left: 286px;
       padding: 0px 16px;
       padding: 0px 16px;
       
       
+      .label {
+        margin-left: 8px;
+        padding: 4px 5px;
+        font-size: 100%;
+      }
+      
+      .label-purple {
+        background-color: @purple;
+      }
+      
       .post-foot {
       .post-foot {
         margin-top: 20px;
         margin-top: 20px;
         
         

+ 3 - 3
templates/admin/admin/list.html

@@ -74,7 +74,7 @@ Showing {{ shown }} of {{ total }} items
   <form id="list_form" class="form-inline pull-right" action="{{ url }}" method="POST">
   <form id="list_form" class="form-inline pull-right" action="{{ url }}" method="POST">
     <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
     <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
     <input type="hidden" name="origin" value="list">
     <input type="hidden" name="origin" value="list">
-    {{ form_theme.input_select(list_form.fields['list_action'],width=20) }}
+    {{ form_theme.input_select(list_form.fields['list_action'],width=3) }}
     <button type="submit" class="btn btn-primary">{% trans %}Go{% endtrans %}</button>
     <button type="submit" class="btn btn-primary">{% trans %}Go{% endtrans %}</button>
   </form>
   </form>
 {%- endif %}
 {%- endif %}
@@ -117,12 +117,12 @@ Showing {{ shown }} of {{ total }} items
     $(function () {
     $(function () {
       $('#list_form').submit(function() {
       $('#list_form').submit(function() {
         if ($('.check-cell[]:checked').length == 0) {
         if ($('.check-cell[]:checked').length == 0) {
-          alert('{{ action.nothing_checked_message }}');
+          alert("{{ action.nothing_checked_message }}");
           return false;
           return false;
         }
         }
         {%- for item in action.actions %}{% if item.2 %}
         {%- for item in action.actions %}{% if item.2 %}
         if ($('#id_list_action').val() == '{{ item.0 }}') {
         if ($('#id_list_action').val() == '{{ item.0 }}') {
-          var decision = confirm('{{ item.2 }}');
+          var decision = confirm("{{ item.2 }}");
           return decision;
           return decision;
         }
         }
         {%- endif %}{% endfor %}
         {%- endif %}{% endfor %}

+ 57 - 4
templates/sora/threads/list.html

@@ -1,6 +1,7 @@
 {% extends "sora/layout.html" %}
 {% extends "sora/layout.html" %}
 {% load i18n %}
 {% load i18n %}
 {% load url from future %}
 {% load url from future %}
+{% import "_forms.html" as form_theme with context %}
 {% import "sora/macros.html" as macros with context %}
 {% import "sora/macros.html" as macros with context %}
 
 
 {% block title %}{{ macros.page_title(title=forum.name,page=pagination['page']) }}{% endblock %}
 {% block title %}{{ macros.page_title(title=forum.name,page=pagination['page']) }}{% endblock %}
@@ -44,25 +45,57 @@
       <th>{% trans %}Author{% endtrans %}</th>
       <th>{% trans %}Author{% endtrans %}</th>
       <th>{% trans %}Replies{% endtrans %}</th>
       <th>{% trans %}Replies{% endtrans %}</th>
       <th>{% trans %}Last Poster{% endtrans %}</th>
       <th>{% trans %}Last Poster{% endtrans %}</th>
+      {% if user.is_authenticated() and list_form %}
+      <th class="check-cell"><label class="checkbox"><input type="checkbox" class="checkbox-master"></label></th>
+      {% endif %}
     </tr>
     </tr>
   </thead>
   </thead>
   <tbody>
   <tbody>
     {% for thread in threads %}
     {% for thread in threads %}
     <tr>
     <tr>
-      <td><span class="thread-icon{% if not thread.is_read %} {% if thread.closed %}thread-closed{% else %}thread-new{% endif %}{% endif %}"><i class="icon-{% if thread.closed %}remove{% else %}comment{% endif %} icon-white"></i></span></td>
+      <td><span class="thread-icon{% if not thread.is_read %} {% if thread.closed %}thread-closed{% else %}thread-new{% endif %}{% endif %}"><i class="icon-comment icon-white"></i></span></td>
       <td>
       <td>
-        <a href="{% url 'thread' thread=thread.pk, slug=thread.slug %}">{% if not thread.is_read %}<strong>{{ thread.name }}</strong>{% else %}{{ thread.name }}{% endif %}</a>[LABELS][JUMP] [ICONS]
+        <a href="{% url 'thread' thread=thread.pk, slug=thread.slug %}">{% if not thread.is_read %}<strong>{{ thread.name }}</strong>{% else %}{{ thread.name }}{% endif %}</a> {% if not thread.is_read -%}
+        <a href="{% url 'thread_new' thread=thread.pk, slug=thread.slug %}" class="jump jump-new tooltip-top" title="{% trans %}Jump to first unread post{% endtrans %}"><i class="icon-chevron-right"></i></a>
+        {%- else -%}
+        <a href="{% url 'thread_last' thread=thread.pk, slug=thread.slug %}" class="jump jump-last tooltip-top" title="{% trans %}Jump to last post{% endtrans %}"><i class="icon-chevron-right"></i></a>
+        {%- endif %}
+        <ul class="unstyled thread-flags">
+          {% if thread.weight == 2 %}<li><span class="tooltip-top" title="{% trans %}This thread is annoucement.{% endtrans %}"><i class="icon-fire"></i></span></li>{% endif %}
+          {% if thread.weight == 1 %}<li><span class="tooltip-top" title="{% trans %}This thread is pinned.{% endtrans %}"><i class="icon-star-empty"></i></span></li>{% endif %}
+          {% if thread.closed %}<li><span class="tooltip-top" title="{% trans %}This thread is closed for new replies.{% endtrans %}"><i class="icon-lock"></i></span></li>{% endif %}
+          {% if thread.moderated %}<li><span class="tooltip-top" title="{% trans %}This thread will not be visible to other members until moderator reviews it.{% endtrans %}"><i class="icon-eye-close"></i></span></li>{% endif %}
+          {% if thread.deleted %}<li><span class="tooltip-top" title="{% trans %}This thread has been deleted.{% endtrans %}"><i class="icon-remove"></i></span></li>{% endif %}
+        </ul>
       </td>
       </td>
       <td class="span2">{% if thread.start_poster_id %}<a href="{% url 'user' user=thread.start_poster_id, username=thread.start_poster_slug %}" class="tooltip-top" title="{{ thread.start|reltimesince }}">{{ thread.start_poster_name }}</a>{% else %}<em class="tooltip-top muted" title="{{ thread.start|reltimesince }}">{{ thread.start_poster_name }}</em>{% endif %}</td>
       <td class="span2">{% if thread.start_poster_id %}<a href="{% url 'user' user=thread.start_poster_id, username=thread.start_poster_slug %}" class="tooltip-top" title="{{ thread.start|reltimesince }}">{{ thread.start_poster_name }}</a>{% else %}<em class="tooltip-top muted" title="{{ thread.start|reltimesince }}">{{ thread.start_poster_name }}</em>{% endif %}</td>
-      <td class="span1">{{ thread.replies|intcomma }}</td>
+      <td class="span1 thread-stat">{{ thread.replies|intcomma }}</td>
       <td class="span2">{% if thread.last_poster_id %}<a href="{% url 'user' user=thread.last_poster_id, username=thread.last_poster_slug %}" class="tooltip-top" title="{{ thread.last|reltimesince }}">{{ thread.last_poster_name }}</a>{% else %}<em class="tooltip-top muted" title="{{ thread.last|reltimesince }}">{{ thread.last_poster_name }}</em>{% endif %}</td>
       <td class="span2">{% if thread.last_poster_id %}<a href="{% url 'user' user=thread.last_poster_id, username=thread.last_poster_slug %}" class="tooltip-top" title="{{ thread.last|reltimesince }}">{{ thread.last_poster_name }}</a>{% else %}<em class="tooltip-top muted" title="{{ thread.last|reltimesince }}">{{ thread.last_poster_name }}</em>{% endif %}</td>
+      {% if user.is_authenticated() and list_form %}
+      <td class="check-cell"><label class="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></td>
+      {% endif %}
+    </tr>
+    {% else %}
+    <tr>
+      <td colspan="5" style="text-align: center;">
+        <p class="lead">{% trans %}Looks like there are no threads in this forum.{% endtrans %}</p>
+        {% if user.is_authenticated() and acl.threads.can_start_threads(forum) %}
+        <a href="{% url 'thread_new' forum=forum.pk, slug=forum.slug %}" class="btn btn-primary btn-large">{% trans %}Let's Change This!{% endtrans %}</a></li>
+        {% endif %}
+      </td>
     </tr>
     </tr>
     {% endfor %}
     {% endfor %}
   </tbody>
   </tbody>
 </table>
 </table>
+{% if user.is_authenticated() and list_form %}
 <div class="form-actions table-footer">
 <div class="form-actions table-footer">
-  [MOD ACTIONS]
+  <form id="threads_form" class="form-inline pull-right" action="{% url 'forum' slug=forum.slug, forum=forum.id, page=pagination['page'] %}" 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-primary">{% trans %}Go{% endtrans %}</button>
+  </form>
 </div>
 </div>
+{% endif %}
 <div class="list-nav last">
 <div class="list-nav last">
   {{ pager() }}
   {{ pager() }}
   {% if user.is_authenticated() and acl.threads.can_start_threads(forum) %}
   {% if user.is_authenticated() and acl.threads.can_start_threads(forum) %}
@@ -85,3 +118,23 @@
     </li>
     </li>
   </ul>
   </ul>
 {% endmacro %}
 {% endmacro %}
+
+{% block javascripts -%}
+{{ super() }}
+{%- if user.is_authenticated() and acl.threads.can_start_threads(forum) %}
+  <script type="text/javascript">
+    $(function () {
+      $('#threads_form').submit(function() {
+        if ($('.check-cell[]:checked').length == 0) {
+          alert("{% trans %}You have to select at least one thread.{% endtrans %}");
+          return false;
+        }
+        if ($('#id_list_action').val() == 'hard') {
+          var decision = ("{% trans %}Are you sure you want to delete selected threads? This action is not reversible!{% endtrans %}");
+          return decision;
+        }
+        return true;
+      });
+    });
+  </script>{% endif %}
+{%- endblock %}

+ 10 - 0
templates/sora/threads/posting.html

@@ -20,6 +20,16 @@
     {{ self.breadcrumb() }}</li>
     {{ self.breadcrumb() }}</li>
   </ul>
   </ul>
   <h1>{% trans %}Post New Thread{% endtrans %}</h1>
   <h1>{% trans %}Post New Thread{% endtrans %}</h1>
+  <ul class="unstyled thread-info">
+    {% 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>
+  </ul>
 </div>
 </div>
 {% if message %}{{ macros.draw_message(message, 'alert-form') }}{% endif %}
 {% if message %}{{ macros.draw_message(message, 'alert-form') }}{% endif %}
 <form action="{% url 'thread_new' forum=forum.pk, slug=forum.slug %}" method="post">
 <form action="{% url 'thread_new' forum=forum.pk, slug=forum.slug %}" method="post">

+ 33 - 5
templates/sora/threads/thread.html

@@ -20,6 +20,7 @@
   </ul>
   </ul>
   <h1>{{ thread.name }}</h1>
   <h1>{{ thread.name }}</h1>
   <ul class="unstyled thread-info">
   <ul class="unstyled thread-info">
+    {% 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-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-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 -%}
     <li><i class="icon-comment"></i> {% if thread.replies > 0 -%}
@@ -42,6 +43,7 @@
 
 
 <div class="posts-list">
 <div class="posts-list">
   {% for post in posts %}
   {% for post in posts %}
+  {% if post.message %}{{ macros.draw_message(post.message) }}{% endif %}
   <div id="post-{{ post.pk }}" class="well well-post{% if post.user and post.user.rank and post.user.rank.style %} {{ post.user.rank.style }}{% endif %}">
   <div id="post-{{ post.pk }}" class="well well-post{% if post.user and post.user.rank and post.user.rank.style %} {{ post.user.rank.style }}{% endif %}">
     <div class="post-author">
     <div class="post-author">
       <img src="{{ post.user.get_avatar() }}" alt="" class="avatar-normal">
       <img src="{{ post.user.get_avatar() }}" alt="" class="avatar-normal">
@@ -52,6 +54,26 @@
       </div>
       </div>
     </div>
     </div>
     <div class="post-content">
     <div class="post-content">
+      {% if not post.is_read %}
+      <span class="label label-warning pull-right">
+        {% trans %}New{% endtrans %}
+      </span>
+      {% endif %}
+      {% if post.moderated %}
+      <span class="label label-purple pull-right">
+        {% trans %}Unreviewed{% endtrans %}
+      </span>
+      {% endif %}
+      {% if post.reported %}
+      <span class="label label-important pull-right">
+        {% trans %}Reported{% endtrans %}
+      </span>
+      {% endif %}
+      {% if post.deleted %}
+      <span class="label label-inverse pull-right">
+        {% trans %}Deleted{% endtrans %}
+      </span>
+      {% endif %}
       <div class="markdown">
       <div class="markdown">
         {{ post.post_preparsed|safe }}
         {{ post.post_preparsed|safe }}
       </div>
       </div>
@@ -73,7 +95,7 @@
 </div>
 </div>
 
 
 <div class="list-nav last">
 <div class="list-nav last">
-  {{ pager() }}
+  {{ pager(false) }}
   {% if user.is_authenticated() and acl.threads.can_reply(thread) %}
   {% if user.is_authenticated() and acl.threads.can_reply(thread) %}
   <ul class="nav nav-pills pull-right">
   <ul class="nav nav-pills pull-right">
     <li class="primary"><a href="{% url 'thread_new' forum=forum.pk, slug=forum.slug %}"><i class="icon-plus"></i> {% trans %}Reply{% endtrans %}</a></li>
     <li class="primary"><a href="{% url 'thread_new' forum=forum.pk, slug=forum.slug %}"><i class="icon-plus"></i> {% trans %}Reply{% endtrans %}</a></li>
@@ -94,15 +116,21 @@
 {% endif %}
 {% endif %}
 {% endblock %}
 {% endblock %}
 
 
-{% macro pager() %}
+{% macro pager(extra=true) %}
   <ul class="pager pull-left">
   <ul class="pager pull-left">
-    {%- 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 -%}
+    {%- if pagination['prev'] > 1 %}<li><a href="{% url 'thread' 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 'thread' slug=thread.slug, thread=thread.id, page=pagination['prev'] %}{% else %}{% url 'thread' 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 'thread' 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 'thread' 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 -%}
     <li class="count">
     <li class="count">
     {%- trans current_page=pagination['page'], pages=pagination['total'] -%}
     {%- trans current_page=pagination['page'], pages=pagination['total'] -%}
     Page {{ current_page }} of {{ pages }}
     Page {{ current_page }} of {{ pages }}
     {%- endtrans -%}
     {%- endtrans -%}
     </li>
     </li>
+    {% if extra and user.is_authenticated() %}
+    {% if not is_read %}<li class="unread"><a href="{% url '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 %}
+    {% if thread.replies_moderated > 0 and acl.threads.can_approve(forum) %}<li class="moderated"><a href="{% url 'thread_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 class="reported"><a href="{% url '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 %}
+    {% endif %}
   </ul>
   </ul>
 {% endmacro %}
 {% endmacro %}