Browse Source

Post voting system

Ralfp 12 years ago
parent
commit
e593531526

+ 4 - 1
misago/forumroles/fixtures.py

@@ -17,7 +17,7 @@ def load_fixtures():
                           'can_soft_delete_own_posts': True,
                           'can_soft_delete_own_posts': True,
                           'can_upvote_posts': True,
                           'can_upvote_posts': True,
                           'can_downvote_posts': True,
                           'can_downvote_posts': True,
-                          'can_see_posts_scores': '2',
+                          'can_see_posts_scores': 2,
                           'can_see_votes': True,
                           'can_see_votes': True,
                           'can_make_polls': True,
                           'can_make_polls': True,
                           'can_vote_in_polls': True,
                           'can_vote_in_polls': True,
@@ -55,6 +55,7 @@ def load_fixtures():
                           'can_soft_delete_own_posts': True,
                           'can_soft_delete_own_posts': True,
                           'can_upvote_posts': True,
                           'can_upvote_posts': True,
                           'can_downvote_posts': True,
                           'can_downvote_posts': True,
+                          'can_see_posts_scores': 1,
                           'can_make_polls': True,
                           'can_make_polls': True,
                           'can_vote_in_polls': True,
                           'can_vote_in_polls': True,
                           'can_upload_attachments': True,
                           'can_upload_attachments': True,
@@ -77,6 +78,7 @@ def load_fixtures():
                           'can_soft_delete_own_posts': True,
                           'can_soft_delete_own_posts': True,
                           'can_upvote_posts': True,
                           'can_upvote_posts': True,
                           'can_downvote_posts': True,
                           'can_downvote_posts': True,
+                          'can_see_posts_scores': 1,
                           'can_make_polls': True,
                           'can_make_polls': True,
                           'can_vote_in_polls': True,
                           'can_vote_in_polls': True,
                           'can_download_attachments': True,
                           'can_download_attachments': True,
@@ -90,6 +92,7 @@ def load_fixtures():
                           'can_see_forum_contents': True,
                           'can_see_forum_contents': True,
                           'can_read_threads': '2',
                           'can_read_threads': '2',
                           'can_download_attachments': True,
                           'can_download_attachments': True,
+                          'can_see_posts_scores': 1,
                           })
                           })
     role.save(force_insert=True)
     role.save(force_insert=True)
 
 

+ 0 - 1
misago/forums/views.py

@@ -278,7 +278,6 @@ class Edit(FormWidget):
             target.prune_last = form.cleaned_data['prune_last']
             target.prune_last = form.cleaned_data['prune_last']
 
 
         if form.cleaned_data['parent'].pk != target.parent.pk:
         if form.cleaned_data['parent'].pk != target.parent.pk:
-            print 'MOVE FORUM!'
             target.move_to(form.cleaned_data['parent'], 'last-child')
             target.move_to(form.cleaned_data['parent'], 'last-child')
             self.request.monitor['acl_version'] = int(self.request.monitor['acl_version']) + 1
             self.request.monitor['acl_version'] = int(self.request.monitor['acl_version']) + 1
 
 

+ 0 - 1
misago/newsfeed/views.py

@@ -7,7 +7,6 @@ def newsfeed(request):
     follows = []
     follows = []
     for user in request.user.follows.iterator():
     for user in request.user.follows.iterator():
         follows.append(user.pk)
         follows.append(user.pk)
-    print follows
     queryset = []
     queryset = []
     if follows:
     if follows:
         queryset = Post.objects.filter(forum_id__in=request.acl.threads.get_readable_forums(request.acl))
         queryset = Post.objects.filter(forum_id__in=request.acl.threads.get_readable_forums(request.acl))

+ 81 - 0
misago/ranks/fixtures.py

@@ -1,8 +1,85 @@
 from misago.ranks.models import Rank
 from misago.ranks.models import Rank
+from misago.settings.fixtures import load_settings_fixture, update_settings_fixture
 from misago.utils import ugettext_lazy as _
 from misago.utils import ugettext_lazy as _
 from misago.utils import get_msgid
 from misago.utils import get_msgid
 
 
+
+settings_fixtures = (
+    # Users Ranking Settings
+    ('ranking', {
+        'name': _("Members Ranking"),
+        'description': _("Those settings control mechanisms of members activity ranking which allows you to gamificate your forum."),
+        'settings': (
+            ('ranking_inflation', {
+                'value':        5,
+                'type':         "integer",
+                'input':        "text",
+                'extra':        {'min': 0, 'max': 99},
+                'separator':    _("Basic Ranking Settings"),
+                'name':         _("Ranking Inflation"),
+                'description':  _("Enter size of ranking scores inflation in percent. Scores inflation is important mechanism that allows ranking self-regulation, punishing inactivity and requiring users to remain active in order to remain high in ranking."),
+            }),
+            ('ranking_positions_visible', {
+                'value':        True,
+                'type':         "boolean",
+                'input':        "yesno",
+                'name':         _("Dont Keep Users Scores Secret"),
+                'description':  _("Changing this to yes will cause forum to display user position in ranking on his profile page."),
+            }),
+            ('ranking_scores_visible', {
+                'value':        True,
+                'type':         "boolean",
+                'input':        "yesno",
+                'name':         _("Dont Keep Users Scores Secret"),
+                'description':  _("Changing this to yes will cause forum to display user score on his profile page."),
+            }),
+            ('score_reward_new_thread', {
+                'value':        50,
+                'type':         "integer",
+                'input':        "text",
+                'separator':    _("Posting Rewards"),
+                'name':         _("New Thread Reward"),
+                'description':  _("Score user will receive (or lose) whenever he posts new thread."),
+            }),
+            ('score_reward_new_post', {
+                'value':        100,
+                'type':         "integer",
+                'input':        "text",
+                'name':         _("New Reply Reward"),
+                'description':  _("Score user will receive (or lose) whenever he posts new reply in thread."),
+            }),
+            ('score_reward_new_post_cooldown', {
+                'value':        180,
+                'type':         "integer",
+                'input':        "text",
+                'extra':        {'min': 0},
+                'name':         _("Reward Cooldown"),
+                'description':  _("Minimal time (in seconds) that has to pass between postings for new message to receive karma vote. This is useful to combat flood."),
+            }),
+            ('score_reward_karma_positive', {
+                'value':        20,
+                'type':         "integer",
+                'input':        "text",
+                'extra':        {'min': 0},
+                'separator':    _("Karma System"),
+                'name':         _("Upvote Reward"),
+                'description':  _("Score user will receive every time his post receives upvote."),
+            }),
+            ('score_reward_karma_negative', {
+                'value':        10,
+                'type':         "integer",
+                'input':        "text",
+                'extra':        {'min': 0},
+                'name':         _("Downvote Punishment"),
+                'description':  _("Score user will lose every time his post receives downvote."),
+            }),
+        ),
+    }),
+)
+
+
 def load_fixtures():
 def load_fixtures():
+    load_settings_fixture(settings_fixtures)
     Rank.objects.create(
     Rank.objects.create(
                         name=_("Forum Team").message,
                         name=_("Forum Team").message,
                         name_slug='forum-team',
                         name_slug='forum-team',
@@ -46,3 +123,7 @@ def load_fixtures():
                         criteria="10%",
                         criteria="10%",
                         as_tab=True,
                         as_tab=True,
                         )
                         )
+
+
+def update_fixtures():
+    update_settings_fixture(settings_fixtures)

+ 4 - 1
misago/ranks/management/commands/updateranking.py

@@ -1,5 +1,6 @@
 from django.core.management.base import BaseCommand, CommandError
 from django.core.management.base import BaseCommand, CommandError
 from django.db.models import F
 from django.db.models import F
+from misago.settings.settings import Settings
 from misago.ranks.models import Rank
 from misago.ranks.models import Rank
 from misago.users.models import User
 from misago.users.models import User
 
 
@@ -29,6 +30,8 @@ class Command(BaseCommand):
                 defaulted_ranks = True
                 defaulted_ranks = True
 
 
         # Inflate scores
         # Inflate scores
-        User.objects.all().update(score=F('score') * 0.95) # TODO: Ranking system SETTINGS!
+        settings = Settings()
+        inflation = float(100 - settings['ranking_inflation']) / 100
+        User.objects.all().update(score=F('score') * inflation, ranking=0)
 
 
         self.stdout.write('Users ranking for has been updated.\n')
         self.stdout.write('Users ranking for has been updated.\n')

+ 1 - 0
misago/ranks/models.py

@@ -1,3 +1,4 @@
+import math
 from django.conf import settings
 from django.conf import settings
 from django.db import models, connection, transaction
 from django.db import models, connection, transaction
 from django.utils.translation import ugettext_lazy as _
 from django.utils.translation import ugettext_lazy as _

+ 54 - 2
misago/threads/acl.py

@@ -464,14 +464,14 @@ class ThreadsACL(BaseACL):
             forum_role = self.acl[forum.pk]
             forum_role = self.acl[forum.pk]
             return forum_role['can_delete_threads']
             return forum_role['can_delete_threads']
         except KeyError:
         except KeyError:
-            raise false
+            return False
 
 
     def can_see_deleted_posts(self, forum):
     def can_see_deleted_posts(self, forum):
         try:
         try:
             forum_role = self.acl[forum.pk]
             forum_role = self.acl[forum.pk]
             return forum_role['can_delete_posts']
             return forum_role['can_delete_posts']
         except KeyError:
         except KeyError:
-            raise false
+            return False
 
 
     def allow_deleted_post_view(self, forum):
     def allow_deleted_post_view(self, forum):
         try:
         try:
@@ -480,6 +480,58 @@ class ThreadsACL(BaseACL):
                 raise ACLError404()
                 raise ACLError404()
         except KeyError:
         except KeyError:
             raise ACLError404()
             raise ACLError404()
+        
+    def can_upvote_posts(self, forum):
+        try:
+            forum_role = self.acl[forum.pk]
+            return forum_role['can_upvote_posts']
+        except KeyError:
+            return False
+        
+    def can_downvote_posts(self, forum):
+        try:
+            forum_role = self.acl[forum.pk]
+            return forum_role['can_downvote_posts']
+        except KeyError:
+            return False
+        
+    def can_see_post_score(self, forum):
+        try:
+            forum_role = self.acl[forum.pk]
+            return forum_role['can_see_posts_scores']
+        except KeyError:
+            return False
+        
+    def can_see_post_votes(self, forum):
+        try:
+            forum_role = self.acl[forum.pk]
+            return forum_role['can_see_votes']
+        except KeyError:
+            return False
+
+    def allow_post_upvote(self, forum):
+        try:
+            forum_role = self.acl[forum.pk]
+            if not forum_role['can_upvote_posts']:
+                raise ACLError403(_("You cannot upvote posts in this forum."))
+        except KeyError:
+            raise ACLError403(_("You cannot upvote posts in this forum."))
+
+    def allow_post_downvote(self, forum):
+        try:
+            forum_role = self.acl[forum.pk]
+            if not forum_role['can_downvote_posts']:
+                raise ACLError403(_("You cannot downvote posts in this forum."))
+        except KeyError:
+            raise ACLError403(_("You cannot downvote posts in this forum."))
+        
+    def allow_post_votes_view(self, forum):
+        try:
+            forum_role = self.acl[forum.pk]
+            if not forum_role['can_see_votes']:
+                raise ACLError403(_("You don't have permission to see who voted on this post."))
+        except KeyError:
+            raise ACLError403(_("You don't have permission to see who voted on this post."))
 
 
 
 
 def build_forums(acl, perms, forums, forum_roles):
 def build_forums(acl, perms, forums, forum_roles):

+ 3 - 0
misago/threads/urls.py

@@ -25,6 +25,9 @@ urlpatterns = patterns('misago.threads.views',
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/delete/$', 'DeleteView', name="post_delete", kwargs={'mode': 'delete_post'}),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/delete/$', 'DeleteView', name="post_delete", kwargs={'mode': 'delete_post'}),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/hide/$', 'DeleteView', name="post_hide", kwargs={'mode': 'hide_post'}),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/hide/$', 'DeleteView', name="post_hide", kwargs={'mode': 'hide_post'}),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/info/$', 'DetailsView', name="post_info"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/info/$', 'DetailsView', name="post_info"),
+    url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/upvote/$', 'UpvotePostView', name="post_upvote"),
+    url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/downvote/$', 'DownvotePostView', name="post_downvote"),
+    url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/votes/$', 'KarmaVotesView', name="post_votes"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/changelog/$', 'ChangelogView', name="changelog"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/changelog/$', 'ChangelogView', name="changelog"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/changelog/(?P<change>\d+)/$', 'ChangelogDiffView', name="changelog_diff"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/changelog/(?P<change>\d+)/$', 'ChangelogDiffView', name="changelog_diff"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/changelog/(?P<change>\d+)/revert/$', 'ChangelogRevertView', name="changelog_revert"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/changelog/(?P<change>\d+)/revert/$', 'ChangelogRevertView', name="changelog_revert"),

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

@@ -2,6 +2,7 @@ from misago.threads.views.list import *
 from misago.threads.views.jumps import *
 from misago.threads.views.jumps import *
 from misago.threads.views.thread import *
 from misago.threads.views.thread import *
 from misago.threads.views.delete import *
 from misago.threads.views.delete import *
+from misago.threads.views.karmas import *
 from misago.threads.views.posting import *
 from misago.threads.views.posting import *
 from misago.threads.views.details import *
 from misago.threads.views.details import *
 from misago.threads.views.changelog import *
 from misago.threads.views.changelog import *

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

@@ -4,7 +4,6 @@ from misago.forums.models import Forum
 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 make_pagination, slugify
 
 
 class DetailsView(BaseView):
 class DetailsView(BaseView):
     def fetch_target(self, kwargs):
     def fetch_target(self, kwargs):

+ 85 - 3
misago/threads/views/jumps.py

@@ -9,7 +9,7 @@ from misago.csrf.decorators import check_csrf
 from misago.forums.models import Forum
 from misago.forums.models import Forum
 from misago.messages import Message
 from misago.messages import Message
 from misago.readstracker.trackers import ThreadsTracker
 from misago.readstracker.trackers import ThreadsTracker
-from misago.threads.models import Thread, Post
+from misago.threads.models import Thread, Post, Karma
 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 make_pagination
 from misago.utils import make_pagination
@@ -110,10 +110,10 @@ class ShowHiddenRepliesView(JumpView):
 class WatchThreadView(JumpView):
 class WatchThreadView(JumpView):
     def get_retreat(self):
     def get_retreat(self):
         return redirect(self.request.POST.get('retreat', reverse('thread', kwargs={'thread': self.thread.pk, 'slug': self.thread.slug})))
         return redirect(self.request.POST.get('retreat', reverse('thread', kwargs={'thread': self.thread.pk, 'slug': self.thread.slug})))
-    
+
     def update_watcher(self, request, watcher):
     def update_watcher(self, request, watcher):
         request.messages.set_flash(Message(_('This thread has been added to your watched threads list.')), 'success', 'threads')
         request.messages.set_flash(Message(_('This thread has been added to your watched threads list.')), 'success', 'threads')
-    
+
     def make_jump(self):
     def make_jump(self):
         @block_guest
         @block_guest
         @check_csrf
         @check_csrf
@@ -158,3 +158,85 @@ class UnwatchEmailThreadView(WatchThreadView):
     def update_watcher(self, request, watcher):
     def update_watcher(self, request, watcher):
         watcher.email = False
         watcher.email = False
         request.messages.set_flash(Message(_('You will no longer receive e-mails with notifications when somebody replies to this thread.')), 'success', 'threads')
         request.messages.set_flash(Message(_('You will no longer receive e-mails with notifications when somebody replies to this thread.')), 'success', 'threads')
+
+
+class UpvotePostView(JumpView):        
+    def make_jump(self):
+        @block_guest
+        @check_csrf
+        def view(request):
+            if self.post.user_id == request.user.id:
+                return error404(request)
+            self.check_acl(request)
+            try:
+                vote = Karma.objects.get(user=request.user, post=self.post)
+                if self.thread.start_post_id == self.post.pk:
+                    if vote.score > 0:
+                        self.thread.upvotes -= 1
+                    else:
+                        self.thread.downvotes -= 1
+                if vote.score > 0:
+                    self.post.upvotes -= 1
+                    request.user.karma_given_p -= 1
+                    if self.post.user_id:
+                        self.post.user.karma_p -= 1
+                else:
+                    self.post.downvotes -= 1
+                    request.user.karma_given_n -= 1
+                    if self.post.user_id:
+                        self.post.user.karma_n -= 1
+            except Karma.DoesNotExist:
+                vote = Karma()
+            vote.forum = self.forum
+            vote.thread = self.thread
+            vote.post = self.post
+            vote.user = request.user
+            vote.user_name = request.user.username
+            vote.user_slug = request.user.username_slug
+            vote.date = timezone.now()
+            vote.ip = request.session.get_ip(request)
+            vote.agent = request.META.get('HTTP_USER_AGENT')
+            self.make_vote(request, vote)
+            request.messages.set_flash(Message(_('Your vote has been saved.')), 'success', 'threads_%s' % self.post.pk)
+            if vote.pk:
+                vote.save(force_update=True)
+            else:
+                vote.save(force_insert=True)
+            if self.thread.start_post_id == self.post.pk:
+                if vote.score > 0:
+                    self.thread.upvotes += 1
+                else:
+                    self.thread.downvotes += 1
+                self.thread.save(force_update=True)
+            if vote.score > 0:
+                self.post.upvotes += 1
+                request.user.karma_given_p += 1
+                if self.post.user_id:
+                    self.post.user.karma_p += 1
+                    self.post.user.score += request.settings['score_reward_karma_positive']
+            else:
+                self.post.downvotes += 1
+                request.user.karma_given_n += 1
+                if self.post.user_id:
+                    self.post.user.karma_n += 1
+                    self.post.user.score -= request.settings['score_reward_karma_negative']
+            self.post.save(force_update=True)
+            request.user.save(force_update=True)
+            if self.post.user_id:
+                self.post.user.save(force_update=True)
+            return self.redirect(self.post)
+        return view(self.request)
+    
+    def check_acl(self, request):
+        request.acl.threads.allow_post_upvote(self.forum)
+    
+    def make_vote(self, request, vote):
+        vote.score = 1
+
+
+class DownvotePostView(UpvotePostView):
+    def check_acl(self, request):
+        request.acl.threads.allow_post_downvote(self.forum)
+    
+    def make_vote(self, request, vote):
+        vote.score = -1

+ 43 - 0
misago/threads/views/karmas.py

@@ -0,0 +1,43 @@
+from django.template import RequestContext
+from misago.acl.utils import ACLError403, ACLError404
+from misago.forums.models import Forum
+from misago.threads.models import Thread, Post
+from misago.threads.views.base import BaseView
+from misago.views import error403, error404
+
+class KarmaVotesView(BaseView):
+    def fetch_target(self, kwargs):
+        self.thread = Thread.objects.get(pk=kwargs['thread'])
+        self.forum = self.thread.forum
+        self.proxy = Forum.objects.parents_aware_forum(self.forum)
+        self.request.acl.forums.allow_forum_view(self.forum)
+        self.request.acl.threads.allow_thread_view(self.request.user, self.thread)
+        self.parents = Forum.objects.forum_parents(self.forum.pk, True)
+        self.post = Post.objects.select_related('user').get(pk=kwargs['post'], thread=self.thread.pk)
+        self.post.thread = self.thread
+        self.request.acl.threads.allow_post_view(self.request.user, self.thread, self.post)
+        self.request.acl.threads.allow_post_votes_view(self.forum)
+
+    def __call__(self, request, **kwargs):
+        self.request = request
+        self.forum = None
+        self.thread = None
+        self.post = None
+        try:
+            self.fetch_target(kwargs)
+        except (Forum.DoesNotExist, Thread.DoesNotExist, Post.DoesNotExist):
+            return error404(self.request)
+        except ACLError403 as e:
+            return error403(request, e.message)
+        except ACLError404 as e:
+            return error404(request, e.message)
+        return request.theme.render_to_response('threads/karmas.html',
+                                                {
+                                                 'forum': self.forum,
+                                                 'parents': self.parents,
+                                                 'thread': self.thread,
+                                                 'post': self.post,
+                                                 'upvotes': self.post.karma_set.filter(score=1),
+                                                 'downvotes': self.post.karma_set.filter(score=-1),
+                                                 },
+                                                context_instance=RequestContext(request))

+ 11 - 0
misago/threads/views/posting.py

@@ -1,3 +1,4 @@
+from datetime import timedelta
 from django.core.urlresolvers import reverse
 from django.core.urlresolvers import reverse
 from django.shortcuts import redirect
 from django.shortcuts import redirect
 from django.template import RequestContext
 from django.template import RequestContext
@@ -168,11 +169,14 @@ class PostingView(BaseView):
                         and self.thread.last_poster_id == request.user.id):
                         and self.thread.last_poster_id == request.user.id):
                         # Overtake posting
                         # Overtake posting
                         post = self.thread.last_post
                         post = self.thread.last_post
+                        post.appended = True
                         post.moderated = moderation
                         post.moderated = moderation
                         post.date = now
                         post.date = now
                         post.post = '%s\n\n- - -\n**%s**\n%s' % (post.post, _("Added on %(date)s:") % {'date': date(now, 'SHORT_DATETIME_FORMAT')}, form.cleaned_data['post'])
                         post.post = '%s\n\n- - -\n**%s**\n%s' % (post.post, _("Added on %(date)s:") % {'date': date(now, 'SHORT_DATETIME_FORMAT')}, form.cleaned_data['post'])
                         md, post.post_preparsed = post_markdown(request, post.post)
                         md, post.post_preparsed = post_markdown(request, post.post)
                         post.save(force_update=True)
                         post.save(force_update=True)
+                        thread.last = now
+                        thread.save(force_update=True)
                         # Ignore rest of posting action
                         # Ignore rest of posting action
                         request.messages.set_flash(Message(_("Your reply has been added to previous one.")), 'success', 'threads_%s' % post.pk)
                         request.messages.set_flash(Message(_("Your reply has been added to previous one.")), 'success', 'threads_%s' % post.pk)
                         return self.redirect_to_post(post)
                         return self.redirect_to_post(post)
@@ -191,6 +195,7 @@ class PostingView(BaseView):
                                                    date=now,
                                                    date=now,
                                                    moderated=moderation,
                                                    moderated=moderation,
                                                    )
                                                    )
+                        post.appended = False
                 elif changed_post:
                 elif changed_post:
                     # Change message
                     # Change message
                     post = self.post
                     post = self.post
@@ -231,6 +236,9 @@ 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
+                    # Reward user for posting new thread?
+                    if request.user.last_post < timezone.now() - timedelta(seconds=request.settings['score_reward_new_post_cooldown']):
+                        request.user.score += request.settings['score_reward_new_thread']
 
 
                 # New post - increase post counters, thread score
                 # New post - increase post counters, thread score
                 # Notify quoted post author and close thread if it has hit limit
                 # Notify quoted post author and close thread if it has hit limit
@@ -252,6 +260,9 @@ class PostingView(BaseView):
                             and thread.replies >= self.request.settings.thread_length):
                             and thread.replies >= self.request.settings.thread_length):
                             thread.closed = True
                             thread.closed = True
                             post.set_checkpoint(self.request, 'limit')
                             post.set_checkpoint(self.request, 'limit')
+                    # Reward user for posting new post?
+                    if not post.appended and request.user.last_post < timezone.now() - timedelta(seconds=request.settings['score_reward_new_post_cooldown']):
+                        request.user.score += request.settings['score_reward_new_post']
 
 
                 # Update last poster data
                 # Update last poster data
                 if not moderation and self.mode not in ['edit_thread', 'edit_post']:
                 if not moderation and self.mode not in ['edit_thread', 'edit_post']:

+ 8 - 2
misago/threads/views/thread.py

@@ -13,7 +13,7 @@ from misago.markdown import post_markdown
 from misago.messages import Message
 from misago.messages import Message
 from misago.readstracker.trackers import ThreadsTracker
 from misago.readstracker.trackers import ThreadsTracker
 from misago.threads.forms import MoveThreadsForm, SplitThreadForm, MovePostsForm, QuickReplyForm
 from misago.threads.forms import MoveThreadsForm, SplitThreadForm, MovePostsForm, QuickReplyForm
-from misago.threads.models import Thread, Post, Change, Checkpoint
+from misago.threads.models import Thread, Post, Karma, Change, Checkpoint
 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 make_pagination, slugify
 from misago.utils import make_pagination, slugify
@@ -47,10 +47,13 @@ class ThreadView(BaseView):
         self.read_date = self.tracker.get_read_date(self.thread)
         self.read_date = self.tracker.get_read_date(self.thread)
         ignored_users = []
         ignored_users = []
         if self.request.user.is_authenticated():
         if self.request.user.is_authenticated():
-            ignored_users = self.request.user.ignored_users()        
+            ignored_users = self.request.user.ignored_users()
+        posts_dict = {}
         for post in self.posts:
         for post in self.posts:
+            posts_dict[post.pk] = post
             post.message = self.request.messages.get_message('threads_%s' % post.pk)
             post.message = self.request.messages.get_message('threads_%s' % post.pk)
             post.is_read = post.date <= self.read_date
             post.is_read = post.date <= self.read_date
+            post.karma_vote = None
             post.ignored = self.thread.start_post_id != post.pk and not self.thread.pk in self.request.session.get('unignore_threads', []) and post.user_id in ignored_users
             post.ignored = self.thread.start_post_id != post.pk and not self.thread.pk in self.request.session.get('unignore_threads', []) and post.user_id in ignored_users
             if post.ignored:
             if post.ignored:
                 self.ignored = True
                 self.ignored = True
@@ -61,6 +64,9 @@ class ThreadView(BaseView):
         if self.watcher and last_post.date > self.watcher.last_read:
         if self.watcher and last_post.date > self.watcher.last_read:
             self.watcher.last_read = timezone.now()
             self.watcher.last_read = timezone.now()
             self.watcher.save(force_update=True)
             self.watcher.save(force_update=True)
+        if self.request.user.is_authenticated():
+            for karma in Karma.objects.filter(post_id__in=posts_dict.keys()).filter(user=self.request.user):
+                posts_dict[karma.post_id].karma_vote = karma
 
 
     def get_post_actions(self):
     def get_post_actions(self):
         acl = self.request.acl.threads.get_role(self.thread.forum_id)
         acl = self.request.acl.threads.get_role(self.thread.forum_id)

+ 2 - 0
misago/users/fixtures.py

@@ -1,4 +1,6 @@
 from misago.monitor.fixtures import load_monitor_fixture
 from misago.monitor.fixtures import load_monitor_fixture
+from misago.utils import ugettext_lazy as _
+from misago.utils import get_msgid
 
 
 monitor_fixtures = {
 monitor_fixtures = {
                   'users': 0,
                   'users': 0,

+ 7 - 0
misago/users/models.py

@@ -148,6 +148,7 @@ class User(models.Model):
     following = models.PositiveIntegerField(default=0)
     following = models.PositiveIntegerField(default=0)
     followers = models.PositiveIntegerField(default=0)
     followers = models.PositiveIntegerField(default=0)
     score = models.IntegerField(default=0, db_index=True)
     score = models.IntegerField(default=0, db_index=True)
+    ranking = models.PositiveIntegerField(default=0)
     rank = models.ForeignKey('ranks.Rank', null=True, blank=True, on_delete=models.SET_NULL)
     rank = models.ForeignKey('ranks.Rank', null=True, blank=True, on_delete=models.SET_NULL)
     last_sync = models.DateTimeField(null=True, blank=True)
     last_sync = models.DateTimeField(null=True, blank=True)
     follows = models.ManyToManyField('self', related_name='follows_set', symmetrical=False)
     follows = models.ManyToManyField('self', related_name='follows_set', symmetrical=False)
@@ -433,6 +434,12 @@ class User(models.Model):
             image_size = settings.AVATAR_SIZES[0]
             image_size = settings.AVATAR_SIZES[0]
         return 'http://www.gravatar.com/avatar/%s?s=%s' % (hashlib.md5(self.email).hexdigest(), image_size)
         return 'http://www.gravatar.com/avatar/%s?s=%s' % (hashlib.md5(self.email).hexdigest(), image_size)
 
 
+    def get_ranking(self):
+        if not self.ranking:
+            self.ranking = User.objects.filter(score__gt=self.score).count() + 1
+            self.save(force_update=True)
+        return self.ranking
+
     def get_title(self):
     def get_title(self):
         if self.title:
         if self.title:
             return self.title
             return self.title

+ 13 - 1
static/sora/css/sora.css

@@ -860,7 +860,7 @@ td.lead-cell{color:#555555;font-weight:bold;}
 .table-users .avatar{-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;width:42px;height:42px;}
 .table-users .avatar{-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;width:42px;height:42px;}
 .table-users .info-popover{background:#e3e3e3;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;padding:2px;padding-top:0px;}.table-users .info-popover i{margin:0px;}
 .table-users .info-popover{background:#e3e3e3;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;padding:2px;padding-top:0px;}.table-users .info-popover i{margin:0px;}
 .table-users .info-popover:hover{background:#7d7d7d;}.table-users .info-popover:hover i{background-image:url("../img/glyphicons-halflings-white.png");}
 .table-users .info-popover:hover{background:#7d7d7d;}.table-users .info-popover:hover i{background-image:url("../img/glyphicons-halflings-white.png");}
-.table-users.list-tiny a:link,.table-users.list-tiny a:active,.table-users.list-tiny a:visited,.table-users.list-tiny a:hover{font-size:100%;}
+.table-users.list-tiny a:link,.table-users.list-tiny a:active,.table-users.list-tiny a:visited,.table-users.list-tiny a:hover{font-size:100%;font-weight:bold;}
 .table-users.list-tiny .avatar{width:22px;height:22px;}
 .table-users.list-tiny .avatar{width:22px;height:22px;}
 .table-users.list-tiny i{position:relative;top:2px;}
 .table-users.list-tiny i{position:relative;top:2px;}
 .btn{background:#dcdcdc;border:1px solid #dcdcdc;*border:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;padding:4px 10px;color:#6f6f6f;font-weight:bold;text-shadow:none;}.btn:hover,.btn:active{background:#e1e1e1;border:1px solid #e1e1e1;*border:0;box-shadow:none;}
 .btn{background:#dcdcdc;border:1px solid #dcdcdc;*border:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;padding:4px 10px;color:#6f6f6f;font-weight:bold;text-shadow:none;}.btn:hover,.btn:active{background:#e1e1e1;border:1px solid #e1e1e1;*border:0;box-shadow:none;}
@@ -1008,6 +1008,17 @@ td.lead-cell{color:#555555;font-weight:bold;}
 .posts-list .well-post .post-extra .post-checkbox{float:right;margin-left:4px;}
 .posts-list .well-post .post-extra .post-checkbox{float:right;margin-left:4px;}
 .posts-list .well-post .post-extra .label{margin-left:8px;margin-bottom:8px;padding:4px 5px;font-size:100%;}
 .posts-list .well-post .post-extra .label{margin-left:8px;margin-bottom:8px;padding:4px 5px;font-size:100%;}
 .posts-list .well-post .post-extra .label-purple{background-color:#7a43b6;}
 .posts-list .well-post .post-extra .label-purple{background-color:#7a43b6;}
+.posts-list .well-post .post-extra .post-rating{clear:both;padding:12px 0px;margin:0px;}.posts-list .well-post .post-extra .post-rating li{margin:0px;padding:0px 4px;}.posts-list .well-post .post-extra .post-rating li form{margin:0px;padding:0px;}
+.posts-list .well-post .post-extra .post-rating li .label{background-color:#999999;margin:0px;position:relative;top:1px;padding:3px 6px;padding-left:7px;}.posts-list .well-post .post-extra .post-rating li .label.label-success{background-color:#46a546;}
+.posts-list .well-post .post-extra .post-rating li .label.label-important{background-color:#9d261d;}
+.posts-list .well-post .post-extra .post-rating li .label:hover{background-color:#999999;}.posts-list .well-post .post-extra .post-rating li .label:hover.label-success{background-color:#46a546;}
+.posts-list .well-post .post-extra .post-rating li .label:hover.label-important{background-color:#9d261d;}
+.posts-list .well-post .post-extra .post-rating li button{padding:1px 4px;margin:0px;opacity:0.4;filter:alpha(opacity=40);}.posts-list .well-post .post-extra .post-rating li button:hover i{background-image:url("../img/glyphicons-halflings-white.png");}
+.posts-list .well-post .post-extra .post-rating li button:hover.btn-upvote{background-color:#46a546;}
+.posts-list .well-post .post-extra .post-rating li button:hover.btn-downvote{background-color:#9d261d;}
+.posts-list .well-post .post-extra .post-rating li.active button i{background-image:url("../img/glyphicons-halflings-white.png");}
+.posts-list .well-post .post-extra .post-rating li.active button.btn-upvote{background-color:#46a546;}
+.posts-list .well-post .post-extra .post-rating li.active button.btn-downvote{background-color:#9d261d;}
 .posts-list .well-post .post-nav{clear:both;margin-left:286px;overflow:auto;padding:8px 16px;padding-bottom:0px;margin-bottom:-8px;}.posts-list .well-post .post-nav .changelog{float:left;opacity:0.5;filter:alpha(opacity=50);color:#999999;}
 .posts-list .well-post .post-nav{clear:both;margin-left:286px;overflow:auto;padding:8px 16px;padding-bottom:0px;margin-bottom:-8px;}.posts-list .well-post .post-nav .changelog{float:left;opacity:0.5;filter:alpha(opacity=50);color:#999999;}
 .posts-list .well-post .post-nav ul{margin:0px;padding:0px;}
 .posts-list .well-post .post-nav ul{margin:0px;padding:0px;}
 .posts-list .well-post .post-nav .nav-pills li{opacity:0.4;filter:alpha(opacity=40);}.posts-list .well-post .post-nav .nav-pills li form{margin:0px;}
 .posts-list .well-post .post-nav .nav-pills li{opacity:0.4;filter:alpha(opacity=40);}.posts-list .well-post .post-nav .nav-pills li form{margin:0px;}
@@ -1016,6 +1027,7 @@ td.lead-cell{color:#555555;font-weight:bold;}
 .posts-list .well-post .post-nav .nav-pills li a,.posts-list .well-post .post-nav .nav-pills li button{background-color:#c9c9c9;border:none;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;margin:0px;color:#ffffff;font-weight:bold;}.posts-list .well-post .post-nav .nav-pills li a:hover,.posts-list .well-post .post-nav .nav-pills li button:hover{background-color:#1ab2ff;}.posts-list .well-post .post-nav .nav-pills li a:hover.danger,.posts-list .well-post .post-nav .nav-pills li button:hover.danger{background-color:#d83a2e;}
 .posts-list .well-post .post-nav .nav-pills li a,.posts-list .well-post .post-nav .nav-pills li button{background-color:#c9c9c9;border:none;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;margin:0px;color:#ffffff;font-weight:bold;}.posts-list .well-post .post-nav .nav-pills li a:hover,.posts-list .well-post .post-nav .nav-pills li button:hover{background-color:#1ab2ff;}.posts-list .well-post .post-nav .nav-pills li a:hover.danger,.posts-list .well-post .post-nav .nav-pills li button:hover.danger{background-color:#d83a2e;}
 .posts-list .well-post .post-nav .nav-pills li i{background-image:url("../img/glyphicons-halflings-white.png");}
 .posts-list .well-post .post-nav .nav-pills li i{background-image:url("../img/glyphicons-halflings-white.png");}
 .posts-list .well-post:hover .changelog,.posts-list .well-post:hover .nav-pills li{opacity:1;filter:alpha(opacity=100);}
 .posts-list .well-post:hover .changelog,.posts-list .well-post:hover .nav-pills li{opacity:1;filter:alpha(opacity=100);}
+.posts-list .well-post:hover .post-rating li button{opacity:1;filter:alpha(opacity=100);}
 .posts-list .post-checkpoints{padding-top:4px;}.posts-list .post-checkpoints:last-child{margin-bottom:-24px;}
 .posts-list .post-checkpoints{padding-top:4px;}.posts-list .post-checkpoints:last-child{margin-bottom:-24px;}
 .posts-list .post-checkpoints .checkpoint{margin:0px;color:#999999;text-align:center;}.posts-list .post-checkpoints .checkpoint span{background-color:#fcfcfc;display:inline-block;padding:4px 12px;position:relative;bottom:16px;}
 .posts-list .post-checkpoints .checkpoint{margin:0px;color:#999999;text-align:center;}.posts-list .post-checkpoints .checkpoint span{background-color:#fcfcfc;display:inline-block;padding:4px 12px;position:relative;bottom:16px;}
 .posts-list .post-checkpoints a{color:#333333;font-weight:bold;}
 .posts-list .post-checkpoints a{color:#333333;font-weight:bold;}

+ 89 - 0
static/sora/css/sora/threads.less

@@ -215,6 +215,87 @@
       .label-purple {
       .label-purple {
         background-color: @purple;
         background-color: @purple;
       }
       }
+      
+      .post-rating {
+        clear: both;
+        padding: 12px 0px;
+        margin: 0px;
+        
+        li {
+          margin: 0px;
+          padding: 0px 4px;
+          
+          form {
+            margin: 0px;
+            padding: 0px;
+          }
+          
+          .label {
+            background-color: @grayLight;
+            margin: 0px;
+            position: relative;
+            top: 1px;
+            padding: 3px 6px;
+            padding-left: 7px;
+            
+            &.label-success {
+              background-color: @green;
+            }
+            
+            &.label-important {
+              background-color: @red;
+            }
+            
+            &:hover {
+              background-color: @grayLight;
+              
+              &.label-success {
+                background-color: @green;
+              }
+              
+              &.label-important {
+                background-color: @red;
+              }
+            }
+          }
+          
+          button {
+            padding: 1px 4px;
+            margin: 0px;
+            .opacity(40);
+            
+            &:hover {
+              i {
+                background-image: url("@{iconWhiteSpritePath}");
+              }
+              
+              &.btn-upvote {
+                background-color: @green;
+              }
+              
+              &.btn-downvote {
+                background-color: @red;
+              }
+            }
+          }
+          
+          &.active {
+            button {
+              i {
+                background-image: url("@{iconWhiteSpritePath}");
+              }
+              
+              &.btn-upvote {
+                background-color: @green;
+              }
+              
+              &.btn-downvote {
+                background-color: @red;
+              }
+            }
+          }
+        }
+      }
     }
     }
     
     
     .post-nav {
     .post-nav {
@@ -280,6 +361,14 @@
       .changelog, .nav-pills li {
       .changelog, .nav-pills li {
         .opacity(100);
         .opacity(100);
       }
       }
+        
+      .post-rating {
+        li {
+          button {
+            .opacity(100);
+          }
+        }
+      }
     }
     }
   }
   }
   
   

+ 1 - 0
static/sora/css/sora/users-lists.less

@@ -39,6 +39,7 @@
     a:link, a:active,
     a:link, a:active,
     a:visited, a:hover {
     a:visited, a:hover {
       font-size: 100%;
       font-size: 100%;
+      font-weight: bold;
     }
     }
     
     
     .avatar {
     .avatar {

+ 13 - 0
templates/sora/profiles/details.html

@@ -109,6 +109,19 @@
           	{% if profile.rank %}{{ _(profile.rank.name) }}{% else %}<em>{% trans %}Not Ranked{% endtrans %}</em>{% endif %}
           	{% if profile.rank %}{{ _(profile.rank.name) }}{% else %}<em>{% trans %}Not Ranked{% endtrans %}</em>{% endif %}
           </td>
           </td>
         </tr>
         </tr>
+        {% if settings.ranking_positions_visible or settings.ranking_scores_visible %}
+        <tr>
+          <td class="span2">
+             <strong>{% if settings.ranking_positions_visible %}{% trans %}Ranking Position{% endtrans %}{% else %}{% trans %}Score{% endtrans %}{% endif %}</strong>
+          </td>
+          <td class="span4">
+            {% if settings.ranking_positions_visible %}
+            #{{ profile.get_ranking()|intcomma }}{% if settings.ranking_scores_visible %} <span class="muted">{{ profile.score|intcomma }}</span>{% endif %}
+            {% else %}{{ profile.score|intcomma }}
+            {% endif %}
+          </td>
+        </tr>
+        {% endif %}
         <tr>
         <tr>
           <td class="span2">
           <td class="span2">
           	 <strong>{% trans %}Karma Received{% endtrans %}</strong>
           	 <strong>{% trans %}Karma Received{% endtrans %}</strong>

+ 99 - 0
templates/sora/threads/karmas.html

@@ -0,0 +1,99 @@
+{% extends "sora/layout.html" %}
+{% load i18n %}
+{% load url from future %}
+{% import "sora/macros.html" as macros with context %}
+
+{% block title %}{{ macros.page_title(title=(_("Post #%(post)s Votes") % {'post': post.pk}),parent=thread.name) }}{% endblock %}
+
+{% block breadcrumb %}{{ super() }} <span class="divider">/</span></li>
+{% for parent in parents %}
+<li><a href="{{ parent.type|url(forum=parent.pk, slug=parent.slug) }}">{{ parent.name }}</a> <span class="divider">/</span></li>
+{% endfor %}
+<li><a href="{% url 'thread' thread=thread.pk, slug=thread.slug %}">{{ thread.name }}</a> <span class="divider">/</span></li>
+<li class="active">{% trans post=post.pk %}Post #{{ post }} Votes{% endtrans %}
+{%- endblock %}
+
+{% block content %}
+<div class="page-header">
+  <ul class="breadcrumb">
+    {{ self.breadcrumb() }}</li>
+  </ul>
+  <h1>{% trans post=post.pk %}Post #{{ post }} Votes{% endtrans %} <small>{{ thread.name }}</small></h1>
+  <ul class="unstyled thread-info">
+    <li><i class="icon-time"></i> <a href="{% url 'thread_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-thumbs-up"></i> {{ post.upvotes - post.downvotes }}</li>
+  </ul>
+</div>
+
+<h2>{% trans %}Upvotes{% endtrans %} <small>{% trans count=upvotes|length, votes=upvotes|length|intcomma -%}
+    One upvote
+    {%- pluralize -%}
+    {{ votes }} upvotes
+    {%- endtrans %}</small></h2>
+{% if upvotes %}
+<table class="table table-striped table-users list-tiny">
+  <thead>
+    <tr>
+      <th colspan="4">{% trans %}Votes List{% endtrans %}</th>
+    </tr>
+  </thead>
+  <tbody>
+    {% for row in upvotes|batch(4, '') %}
+    <tr>
+      {% for vote in row %}
+      <td class="span3">
+        {% if vote %}
+        {{ vote_details(vote) }}
+        {% else %}
+        &nbsp;
+        {% endif %}
+      </td>
+      {% endfor %}
+    </tr>
+    {% endfor %}
+  </tbody>
+</table>
+{% else %}
+<p class="lead">{% trans %}Nobody has upvoted this post.{% endtrans %}</p>
+{% endif %}
+
+<hr>
+
+<h2>{% trans %}Downvotes{% endtrans %} <small>{% trans count=downvotes|length, votes=downvotes|length|intcomma -%}
+    One downvote
+    {%- pluralize -%}
+    {{ votes }} downvotes
+    {%- endtrans %}</small></h2>
+{% if downvotes %}
+<table class="table table-striped table-users list-tiny">
+  <thead>
+    <tr>
+      <th colspan="4">{% trans %}Votes List{% endtrans %}</th>
+    </tr>
+  </thead>
+  <tbody>
+    {% for row in downvotes|batch(4, '') %}
+    <tr>
+      {% for vote in row %}
+      <td class="span3">
+        {% if vote %}
+        {{ vote_details(vote) }}
+        {% else %}
+        &nbsp;
+        {% endif %}
+      </td>
+      {% endfor %}
+    </tr>
+    {% endfor %}
+  </tbody>
+</table>
+{% else %}
+<p class="lead">{% trans %}Nobody has downvoted this post.{% endtrans %}</p>
+{% endif %}
+{% endblock %}
+
+{% macro vote_details(vote) %}
+<div>{% if vote.user_id %}<a href="{% url 'user' user=vote.user_id, username=vote.user_slug %}">{{ vote.user_name }}</a>{% else %}<em><strong>{{ vote.user_name }}</strong></em>{% endif %}</div>
+<div class="muted{% if acl.users.can_see_users_trails() %} tooltip-top{% endif %}"{% if acl.users.can_see_users_trails() %} title="{{ vote.agent }}"{% endif %}>{{ vote.date|reldate }}{% if acl.users.can_see_users_trails() %}, {{ vote.ip }}{% endif %}</div>
+{% endmacro %}

+ 14 - 6
templates/sora/threads/thread.html

@@ -59,6 +59,7 @@
 
 
 <div class="posts-list">
 <div class="posts-list">
   {% for post in posts %}
   {% for post in posts %}
+  <div id="post-{{ post.pk }}"></div>
   {% if post.message %}{{ macros.draw_message(post.message) }}{% endif %}
   {% if post.message %}{{ macros.draw_message(post.message) }}{% endif %}
   {% if post.deleted and not acl.threads.can_see_deleted_posts(forum) %}
   {% if post.deleted and not acl.threads.can_see_deleted_posts(forum) %}
   <div id="post-{{ post.pk }}" class="well well-post well-post-deleted{% 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 well-post-deleted{% if post.user and post.user.rank and post.user.rank.style %} {{ post.user.rank.style }}{% endif %}">
@@ -83,7 +84,7 @@
     </div>
     </div>
   </div>
   </div>
   {% elif post.ignored %}
   {% elif post.ignored %}
-  <div id="post-{{ post.pk }}" class="well well-post well-post-deleted{% if post.user and post.user.rank and post.user.rank.style %} {{ post.user.rank.style }}{% endif %}">
+  <div class="well well-post well-post-deleted{% 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">
       <div class="post-bit">
       <div class="post-bit">
         <span class="lead">{% trans %}Hidden Reply{% endtrans %}</span>
         <span class="lead">{% trans %}Hidden Reply{% endtrans %}</span>
@@ -149,6 +150,17 @@
         {% trans %}Protected{% endtrans %}
         {% trans %}Protected{% endtrans %}
       </span>
       </span>
       {% endif %}
       {% endif %}
+      {% if acl.threads.can_see_post_score(forum) %}
+      <ul class="nav nav-pills post-rating pull-right">
+        {% if user.is_authenticated() and user.pk != post.user_id and acl.threads.can_upvote_posts(forum) %}
+        <li{% if post.karma_vote and post.karma_vote.score > 0 %} class="active"{% endif %}><form action="{% url 'post_upvote' thread=thread.pk, slug=thread.slug, post=post.pk %}" method="post"><button type="submit" class="btn btn-upvote"{% if post.karma_vote and post.karma_vote.score > 0 %} disabled="disabled"{% endif %}><i class="icon-thumbs-up"></i></button><input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}"></form></li>
+        {% endif %}
+        <li><{% if acl.threads.can_see_post_votes(forum) %}a href="{% url 'post_votes' thread=thread.pk, slug=thread.slug, post=post.pk %}"{% else %}span{% endif %} class="label{% if (post.upvotes - post.downvotes) > 0 %} label-success{% elif (post.upvotes - post.downvotes) < 0 %} label-important{% endif %}{% if acl.threads.can_see_post_score(forum) == 2 %} tooltip-left{% endif %}"{% if acl.threads.can_see_post_score(forum) == 2 %} title="+{{ post.upvotes }} / -{{ post.downvotes }}"{% endif %}>{% if (post.upvotes - post.downvotes) > 0 %}+{% endif %}{{ post.upvotes - post.downvotes }}</{% if acl.threads.can_see_post_votes(forum) %}a{% else %}span{% endif %}></li>
+        {% if user.is_authenticated() and user.pk != post.user_id and acl.threads.can_downvote_posts(forum) %}
+        <li{% if post.karma_vote and post.karma_vote.score < 0 %} class="active"{% endif %}><form action="{% url 'post_downvote' thread=thread.pk, slug=thread.slug, post=post.pk %}" method="post"><button type="submit" class="btn btn-downvote"{% if post.karma_vote and post.karma_vote.score < 0 %} disabled="disabled"{% endif %}><i class="icon-thumbs-down"></i></button><input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}"></form></li>
+        {% endif %}
+      </ul>
+      {% endif %}
     </div>
     </div>
     <div class="post-content">
     <div class="post-content">
       <div class="markdown">
       <div class="markdown">
@@ -156,7 +168,6 @@
       </div>
       </div>
       {% if post.user.signature %}
       {% if post.user.signature %}
       <div class="post-foot">
       <div class="post-foot">
-        {# <p class='lead'><a href="{% url 'user' user=post.user.pk, username=post.user.username_slug %}" class="lead">{{ post.user.username }}</a>, {{ post.date|reltimesince|low }}</p> #}
         {% if post.user.signature %}
         {% if post.user.signature %}
         <div class="signature">
         <div class="signature">
           <div class="markdown">
           <div class="markdown">
@@ -167,7 +178,7 @@
       </div>
       </div>
       {% endif %}
       {% endif %}
     </div>
     </div>
-    <div class="post-nav">
+    <div class="post-nav">     
       {% if post.edits %}
       {% if post.edits %}
       {% if acl.threads.can_see_changelog(user, forum, post) %}
       {% if acl.threads.can_see_changelog(user, forum, post) %}
       <a href="{% url 'changelog' thread=thread.pk, slug=thread.slug, post=post.pk %}" class="changelog tooltip-bottom" title="{% trans %}Show changelog{% endtrans %}">{% trans count=post.edits %}One edit{% pluralize %}{{ count }} edits{% endtrans %}</a>
       <a href="{% url 'changelog' thread=thread.pk, slug=thread.slug, post=post.pk %}" class="changelog tooltip-bottom" title="{% trans %}Show changelog{% endtrans %}">{% trans count=post.edits %}One edit{% pluralize %}{{ count }} edits{% endtrans %}</a>
@@ -177,9 +188,6 @@
       {% endif %}
       {% endif %}
       {% if user.is_authenticated() %}
       {% if user.is_authenticated() %}
       <ul class="nav nav-pills pull-right">
       <ul class="nav nav-pills pull-right">
-        {% if 1 == 2 %}
-        <li><a href="#"><i class="icon-info-sign"></i> {% trans %}Info{% endtrans %}</a></li>
-        {% endif %}
         {% if post.pk == thread.start_post_id %}
         {% if post.pk == thread.start_post_id %}
         {% if acl.threads.can_delete_thread(user, forum, thread, post) == 2 -%}
         {% if acl.threads.can_delete_thread(user, forum, thread, post) == 2 -%}
             <li class="tooltip-top" title="{% trans %}Delete this thread for good{% endtrans %}"><form action="{% url 'thread_delete' thread=thread.pk, slug=thread.slug %}" class="prompt-delete-thread" method="post"><button type="submit" class="btn danger"><i class="icon-remove"></i></button><input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}"></form></li>{% endif %}
             <li class="tooltip-top" title="{% trans %}Delete this thread for good{% endtrans %}"><form action="{% url 'thread_delete' thread=thread.pk, slug=thread.slug %}" class="prompt-delete-thread" method="post"><button type="submit" class="btn danger"><i class="icon-remove"></i></button><input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}"></form></li>{% endif %}