Ralfp 12 лет назад
Родитель
Сommit
38871e5ff1

+ 7 - 7
misago/forums/fixtures.py

@@ -5,17 +5,17 @@ from misago.threads.models import Thread, Post
 from misago.utils import slugify
 
 def load_fixtures():
-    Forum(token='annoucements', name='annoucements', slug='annoucements', type='forum').insert_at(target=None,save=True)
-    Forum(token='private', name='private', slug='private', type='forum').insert_at(target=None,save=True)
-    Forum(token='reports', name='reports', slug='reports', type='forum').insert_at(target=None,save=True)
+    Forum(token='annoucements', name='annoucements', slug='annoucements', type='forum').insert_at(None,save=True)
+    Forum(token='private', name='private', slug='private', type='forum').insert_at(None,save=True)
+    Forum(token='reports', name='reports', slug='reports', type='forum').insert_at(None,save=True)
     
     root = Forum(token='root', name='root', slug='root')
-    root.insert_at(target=None,save=True)
+    root.insert_at(None,save=True)
     cat = Forum(type='category', name='First Category', slug='first-category')
-    cat.insert_at(target=root,save=True)
+    cat.insert_at(root,save=True)
     forum = Forum(type='forum', name='First Forum', slug='first-forum', threads=1, posts=1)
-    forum.insert_at(target=cat,save=True)
-    Forum(type='redirect', name='Project Homepage', slug='project-homepage', redirect='http://misago-project.org').insert_at(target=cat,save=True)
+    forum.insert_at(cat,save=True)
+    Forum(type='redirect', name='Project Homepage', slug='project-homepage', redirect='http://misago-project.org').insert_at(cat,position='last-child',save=True)
     Forum.objects.populate_tree(True)
     
     now = timezone.now()

+ 16 - 0
misago/forums/models.py

@@ -37,6 +37,22 @@ class ForumManager(models.Manager):
             parents.append(parent)
         return reversed(parents)
         
+    def parents_aware_forum(self, forum):
+        self.populate_tree()
+        proxy = Forum()
+        try:
+            proxy.id = forum.pk
+            proxy.pk = forum.pk
+        except AttributeError:
+            proxy.id = forum
+            proxy.pk = forum
+        proxy.closed = False
+        for parent in self.forum_parents(proxy.pk):
+            if parent.closed:
+                proxy.closed = True
+                return proxy
+        return proxy
+        
     def treelist(self, acl, parent=None, tracker=None):
         complete_list = []
         forums_list = []

+ 1 - 0
misago/forums/views.py

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

+ 84 - 7
misago/threads/acl.py

@@ -210,37 +210,107 @@ class ThreadsACL(BaseACL):
                 raise ACLError403(_("This forum is closed, you can't start new threads in it."))
         except KeyError:
             raise ACLError403(_("You don't have permission to start new threads in this forum."))
+    
+    def can_edit_thread(self, user, forum, thread, post):
+        try:
+            forum_role = self.acl[thread.forum_id]
+            if forum_role['can_close_threads'] == 0 and (forum.closed or thread.closed):
+                return False
+            if forum_role['can_edit_threads_posts']:
+                return True
+            if forum_role['can_edit_own_threads'] and not post.protected and post.user_id == user.pk:
+                return True
+            return False
+        except KeyError:
+            return False
+    
+    def allow_thread_edit(self, user, forum, thread, post):
+        try:
+            forum_role = self.acl[thread.forum_id]
+            if not forum_role['can_close_threads']:
+                if forum.closed:
+                    raise ACLError403(_("You can't edit threads in closed forums."))
+                if thread.closed:
+                    raise ACLError403(_("You can't edit closed threads."))
+            if not forum_role['can_edit_threads_posts']:
+                if post.user_id != user.pk:
+                    raise ACLError403(_("You can't edit other members threads."))
+                if not forum_role['can_edit_own_threads']:
+                    raise ACLError403(_("You can't edit your threads."))
+                if post.protected:
+                    raise ACLError403(_("This thread is protected, you cannot edit it."))
+        except KeyError:
+            raise ACLError403(_("You don't have permission to edit threads in this forum."))
 
-    def can_reply(self, thread):
+    def can_reply(self, forum, thread):
         try:
-            forum_role = self.acl[thread.forum.pk]
+            forum_role = self.acl[forum.pk]
             if forum_role['can_write_posts'] == 0:
                 return False
-            if thread.closed and forum_role['can_close_threads'] == 0:
+            if (forum.closed or thread.closed) and forum_role['can_close_threads'] == 0:
                 return False
             return True
         except KeyError:
             return False
 
-    def allow_reply(self, thread):
+    def allow_reply(self, forum, thread):
         try:
             forum_role = self.acl[thread.forum.pk]
             if forum_role['can_write_posts'] == 0:
                 raise ACLError403(_("You don't have permission to write replies in this forum."))
             if forum_role['can_close_threads'] == 0:
-                if thread.forum.closed:
+                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:
             raise ACLError403(_("You don't have permission to write replies in this forum."))
     
-    def can_approve(self, forum):
+    def can_edit_reply(self, user, forum, thread, post):
+        try:
+            forum_role = self.acl[thread.forum_id]
+            if forum_role['can_close_threads'] == 0 and (forum.closed or thread.closed):
+                return False
+            if forum_role['can_edit_threads_posts']:
+                return True
+            if forum_role['can_edit_own_posts'] and not post.protected and post.user_id == user.pk:
+                return True
+            return False
+        except KeyError:
+            return False
+    
+    def allow_reply_edit(self, user, forum, thread, post):
+        try:
+            forum_role = self.acl[thread.forum_id]
+            if not forum_role['can_close_threads']:
+                if forum.closed:
+                    raise ACLError403(_("You can't edit replies in closed forums."))
+                if thread.closed:
+                    raise ACLError403(_("You can't edit replies in closed threads."))
+            if not forum_role['can_edit_threads_posts']:
+                if post.user_id != user.pk:
+                    raise ACLError403(_("You can't edit other members replies."))
+                if not forum_role['can_edit_own_posts']:
+                    raise ACLError403(_("You can't edit your replies."))
+                if post.protected:
+                    raise ACLError403(_("This reply is protected, you cannot edit it."))
+        except KeyError:
+            raise ACLError403(_("You don't have permission to edit replies in this forum."))
+    
+    def can_see_changelog(self, user, forum, post):
         try:
             forum_role = self.acl[forum.pk]
-            return forum_role['can_approve']
+            return forum_role['can_see_changelog'] or user.pk == post.user_id
         except KeyError:
             return False
+    
+    def allow_changelog_view(self, user, forum, post):
+        try:
+            forum_role = self.acl[forum.pk]
+            if not (forum_role['can_see_changelog'] or user.pk == post.user_id):
+                raise ACLError403(_("You don't have permission to see history of changes made to this post."))
+        except KeyError:
+            raise ACLError403(_("You don't have permission to see history of changes made to this post."))
         
     def can_mod_threads(self, forum):
         try:
@@ -270,6 +340,13 @@ class ThreadsACL(BaseACL):
         
     def can_mod_thread(self, thread):
         pass
+    
+    def can_approve(self, forum):
+        try:
+            forum_role = self.acl[forum.pk]
+            return forum_role['can_approve']
+        except KeyError:
+            return False
 
 
 def build_forums(acl, perms, forums, forum_roles):

+ 6 - 0
misago/threads/forms.py

@@ -38,11 +38,17 @@ class PostForm(Form, ThreadNameMixin):
                         None,
                         [
                          ('thread_name', {'label': _("Thread Name")}),
+                         ('edit_reason', {'label': _("Edit Reason")}),
                          ('post', {'label': _("Post Content")}),
                          ],
                         ],
                        ]
     
+        if self.mode in ['edit_thread', 'edit_post']:
+            self.fields['edit_reason'] = forms.CharField(max_length=255,required=False,help_text=_("Optional reason for changing this post."))
+        else:
+            del self.layout[0][1][1]
+            
         if self.mode not in ['edit_thread', 'new_thread']:
             del self.layout[0][1][0]
         else:

+ 5 - 3
misago/threads/models.py

@@ -93,7 +93,6 @@ class Post(models.Model):
     upvotes = models.PositiveIntegerField(default=0)
     downvotes = models.PositiveIntegerField(default=0)
     date = models.DateTimeField()
-    edited = models.BooleanField(default=False)
     edits = models.PositiveIntegerField(default=0)
     edit_date = models.DateTimeField(null=True,blank=True)
     edit_reason = models.CharField(max_length=255,null=True,blank=True)
@@ -138,9 +137,12 @@ class Change(models.Model):
     date = models.DateTimeField()
     ip = models.GenericIPAddressField()
     agent = models.CharField(max_length=255)
-    change = models.IntegerField(default=0)
-    thread_name = models.CharField(max_length=255)
+    reason = models.CharField(max_length=255,null=True,blank=True)
+    thread_name_new = models.CharField(max_length=255,null=True,blank=True)
+    thread_name_old = models.CharField(max_length=255,null=True,blank=True)
     post_content = models.TextField()
+    size = models.IntegerField(default=0)
+    change = models.IntegerField(default=0)
 
 
 class Checkpoint(models.Model):

+ 6 - 1
misago/threads/urls.py

@@ -12,5 +12,10 @@ urlpatterns = patterns('misago.threads.views',
     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+)/reply/$', 'PostingView', name="thread_reply", kwargs={'mode': 'new_post'}),
-    url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/reply/(?P<quote>\d+)/$', 'PostingView', name="thread_reply", kwargs={'mode': 'new_post'}),
+    url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<quote>\d+)/reply/$', 'PostingView', name="thread_reply", kwargs={'mode': 'new_post'}),
+    url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/edit/$', 'PostingView', name="thread_edit", kwargs={'mode': 'edit_thread'}),
+    url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/edit/$', 'PostingView', name="post_edit", kwargs={'mode': 'edit_post'}),
+    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+)/revert/$', 'ChangelogRevertView', name="changelog_revert"),
 )

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

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

+ 118 - 0
misago/threads/views/changelog.py

@@ -0,0 +1,118 @@
+import difflib
+from django.core.urlresolvers import reverse
+from django.shortcuts import redirect
+from django.template import RequestContext
+from django.utils.translation import ugettext as _
+from misago.acl.utils import ACLError403, ACLError404
+from misago.forums.models import Forum
+from misago.markdown import post_markdown
+from misago.messages import Message
+from misago.threads.models import Thread, Post, Change
+from misago.threads.views.base import BaseView
+from misago.views import error403, error404
+from misago.utils import make_pagination, slugify
+
+class ChangelogBaseView(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.request.acl.threads.allow_post_view(self.request.user, self.thread, self.post)
+        self.request.acl.threads.allow_changelog_view(self.request.user, self.forum, self.post)
+            
+    def dispatch(self, request, **kwargs):
+        raise NotImplementedError('ChangelogBaseView cannot be called directly. Did you forget to define custom "dispatch" method?')
+    
+    def __call__(self, request, **kwargs):
+        self.request = request
+        self.forum = None
+        self.thread = None
+        self.post = None
+        try:
+            self.fetch_target(kwargs)
+            if not request.user.is_authenticated():
+                raise ACLError403(_("Guest, you have to sign-in in order to see posts changelogs."))
+        except (Forum.DoesNotExist, Thread.DoesNotExist, Post.DoesNotExist, Change.DoesNotExist):
+            return error404(self.request)
+        except ACLError403 as e:
+            return error403(request, e.message)
+        except ACLError404 as e:
+            return error404(request, e.message)
+        return self.dispatch(request, **kwargs)
+    
+
+class ChangelogView(ChangelogBaseView):
+    def dispatch(self, request, **kwargs):
+        return request.theme.render_to_response('threads/changelog.html',
+                                                {
+                                                 'forum': self.forum,
+                                                 'parents': self.parents,
+                                                 'thread': self.thread,
+                                                 'post': self.post,
+                                                 'edits': self.post.change_set.prefetch_related('user').order_by('-id')
+                                                 },
+                                                context_instance=RequestContext(request))
+
+
+class ChangelogDiffView(ChangelogBaseView):
+    def fetch_target(self, kwargs):
+        super(ChangelogDiffView, self).fetch_target(kwargs)
+        self.change = self.post.change_set.get(pk=kwargs['change'])
+    
+    def dispatch(self, request, **kwargs):
+        try:
+            next = self.post.change_set.filter(id__gt=self.change.pk)[:1][0]
+        except IndexError:
+            next = None
+        try:
+            prev = self.post.change_set.filter(id__lt=self.change.pk).order_by('-id')[:1][0]
+        except IndexError:
+            prev = None
+        return request.theme.render_to_response('threads/changelog_diff.html',
+                                                {
+                                                 'forum': self.forum,
+                                                 'parents': self.parents,
+                                                 'thread': self.thread,
+                                                 'post': self.post,
+                                                 'change': self.change,
+                                                 'next': next,
+                                                 'prev': prev,
+                                                 'message': request.messages.get_message('changelog'),
+                                                 'l': 1,
+                                                 'diff': difflib.ndiff(self.change.post_content.splitlines(), self.post.post.splitlines()),
+                                                 },
+                                                context_instance=RequestContext(request))        
+
+
+class ChangelogRevertView(ChangelogDiffView):
+    def dispatch(self, request, **kwargs):
+        if ((not self.change.thread_name_old or self.thread.name == self.change.thread_name_old)
+            and (self.change.post_content == self.post.post)):
+            request.messages.set_flash(Message(_("No changes to revert.")), 'error', 'changelog')
+            return redirect(reverse('changelog_diff', kwargs={'thread': self.thread.pk, 'slug': self.thread.slug, 'post': self.post.pk, 'change': self.change.pk}))
+        
+        if self.change.thread_name_old and self.change.thread_name_old != self.thread.name:
+            self.thread.name = self.change.thread_name_old
+            self.thread.slug = slugify(self.change.thread_name_old)
+            self.thread.save(force_update=True)
+            
+            if self.forum.last_thread_id == self.thread.pk:
+                self.forum.last_thread_name = self.change.thread_name_old
+                self.forum.last_thread_slug = slugify(self.change.thread_name_old)
+                self.forum.save(force_update=True)
+            
+        if self.change.post_content != self.post.post:
+            self.post.post = self.change.post_content
+            self.post.post_preparsed = post_markdown(request, self.change.post_content)
+            self.post.save(force_update=True)
+        
+        request.messages.set_flash(Message(_("Post has been reverted previous state.")), 'success', 'threads_%s' % self.post.pk)
+        pagination = make_pagination(0, request.acl.threads.filter_posts(self.request, self.thread, self.thread.post_set).filter(id__lte=self.post.pk).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' % self.post.pk))
+        return redirect(reverse('thread', kwargs={'thread': self.thread.pk, 'slug': self.thread.slug}) + ('#post-%s' % self.post.pk))
+                

+ 3 - 0
misago/threads/views/list.py

@@ -20,6 +20,7 @@ from misago.utils import make_pagination, slugify
 class ThreadsView(BaseView, ThreadsFormMixin):
     def fetch_forum(self, forum):
         self.forum = Forum.objects.get(pk=forum, type='forum')
+        self.proxy = Forum.objects.parents_aware_forum(self.forum)
         self.request.acl.forums.allow_forum_view(self.forum)
         self.parents = Forum.objects.forum_parents(self.forum.pk)
         if self.forum.lft + 1 != self.forum.rght:
@@ -316,6 +317,8 @@ class ThreadsView(BaseView, ThreadsFormMixin):
             return error403(request, e.message)
         except ACLError404 as e:
             return error404(request, e.message)
+        # Merge proxy into forum
+        self.forum.closed = self.proxy.closed
         return request.theme.render_to_response('threads/list.html',
                                                 {
                                                  'message': self.message,

+ 113 - 27
misago/threads/views/posting.py

@@ -18,29 +18,46 @@ class PostingView(BaseView):
     def fetch_target(self, kwargs):
         if self.mode == 'new_thread':
             self.fetch_forum(kwargs)
-        if self.mode in ('edit_thread', 'new_post', 'new_post_quick'):
+        else:
             self.fetch_thread(kwargs)
+            if self.mode == 'edit_thread':
+                self.fetch_post(self.thread.start_post_id)
+            if self.mode == 'edit_post':
+                self.fetch_post(kwargs['post'])
     
     def fetch_forum(self, kwargs):
         self.forum = Forum.objects.get(pk=kwargs['forum'], type='forum')
+        self.proxy = Forum.objects.parents_aware_forum(self.forum)
         self.request.acl.forums.allow_forum_view(self.forum)
-        self.request.acl.threads.allow_new_threads(self.forum)
+        self.request.acl.threads.allow_new_threads(self.proxy)
         self.parents = Forum.objects.forum_parents(self.forum.pk, True)
     
     def fetch_thread(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.request.acl.threads.allow_reply(self.thread)
+        self.request.acl.threads.allow_reply(self.proxy, self.thread)
         self.parents = Forum.objects.forum_parents(self.forum.pk, True)
         if kwargs.get('quote'):
             self.quote = Post.objects.select_related('user').get(pk=kwargs['quote'], thread=self.thread.pk)
             self.request.acl.threads.allow_post_view(self.request.user, self.thread, self.quote)
+    
+    def fetch_post(self, post):
+        self.post = self.thread.post_set.get(pk=post)
+        self.request.acl.threads.allow_post_view(self.request.user, self.thread, self.post)
+        if self.mode == 'edit_thread':
+            self.request.acl.threads.allow_thread_edit(self.request.user, self.proxy, self.thread, self.post)
+        if self.mode == 'edit_post':
+            self.request.acl.threads.allow_post_edit(self.request.user, self.proxy, self.thread, self.post)     
         
-    def get_form(self, bound=False):            
-        if bound:            
-            return PostForm(self.request.POST,request=self.request,mode=self.mode)
+    def get_form(self, bound=False):
+        initial = {}
+        if self.mode == 'edit_thread':
+            initial['thread_name'] = self.thread.name
+        if self.mode in ['edit_thread', 'edit_post']:
+            initial['post'] = self.post.post
         if self.quote:
             quote_post = []
             if self.quote.user:
@@ -50,8 +67,11 @@ class PostingView(BaseView):
             for line in self.quote.post.splitlines():
                 quote_post.append('> %s' % line)
             quote_post.append('\n')
-            return PostForm(request=self.request,mode=self.mode,initial={'post': '\n'.join(quote_post)})
-        return PostForm(request=self.request,mode=self.mode)
+            initial['post'] = '\n'.join(quote_post)
+            
+        if bound:            
+            return PostForm(self.request.POST,request=self.request,mode=self.mode,initial=initial)
+        return PostForm(request=self.request,mode=self.mode,initial=initial)
             
     def __call__(self, request, **kwargs):
         self.request = request
@@ -66,7 +86,7 @@ class PostingView(BaseView):
         try:
             self.fetch_target(kwargs)
             if not request.user.is_authenticated():
-                raise ACLError403(_("Guest, you have to sign-in to post."))
+                raise ACLError403(_("Guest, you have to sign-in in order to post replies."))
         except (Forum.DoesNotExist, Thread.DoesNotExist, Post.DoesNotExist):
             return error404(self.request)
         except ACLError403 as e:
@@ -78,6 +98,16 @@ class PostingView(BaseView):
         if request.method == 'POST':
             form = self.get_form(True)
             if form.is_valid():
+                # Record original vars if user is editing 
+                if self.mode in ['edit_thread', 'edit_post']:
+                    old_name = self.thread.name
+                    old_post = self.post.post
+                    # If there is no change, throw user back
+                    changed_name = old_name != form.cleaned_data['thread_name']
+                    changed_post = old_post != form.cleaned_data['post']
+                    changed_anything = changed_name or changed_post
+                
+                # Some extra initialisation
                 now = timezone.now()
                 moderation = False
                 if not request.acl.threads.acl[self.forum.pk]['can_approve']:
@@ -85,8 +115,9 @@ class PostingView(BaseView):
                         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
-                if self.mode in ['new_thread']:
+                if self.mode == 'new_thread':
                     thread = Thread.objects.create(
                                                    forum=self.forum,
                                                    name=form.cleaned_data['thread_name'],
@@ -100,22 +131,58 @@ class PostingView(BaseView):
                         thread.replies_moderated += 1
                 else:
                     thread = self.thread
+                    if self.mode == 'edit_thread':
+                        thread.name = form.cleaned_data['thread_name']
+                        thread.slug = slugify(form.cleaned_data['thread_name']) 
                 
                 # Create new message
-                post = Post.objects.create(
-                                           forum=self.forum,
-                                           thread=thread,
-                                           merge=thread.merges,
-                                           user=request.user,
-                                           user_name=request.user.username,
-                                           ip=request.session.get_ip(request),
-                                           agent=request.META.get('HTTP_USER_AGENT'),
-                                           post=form.cleaned_data['post'],
-                                           post_preparsed=post_markdown(request, form.cleaned_data['post']),
-                                           date=now,
-                                           moderated=moderation,
-                                           )
+                if self.mode in ['new_thread', 'new_post', 'new_post_quick']:
+                    post = Post.objects.create(
+                                               forum=self.forum,
+                                               thread=thread,
+                                               merge=thread.merges,
+                                               user=request.user,
+                                               user_name=request.user.username,
+                                               ip=request.session.get_ip(request),
+                                               agent=request.META.get('HTTP_USER_AGENT'),
+                                               post=form.cleaned_data['post'],
+                                               post_preparsed=post_markdown(request, form.cleaned_data['post']),
+                                               date=now,
+                                               moderated=moderation,
+                                               )
+                elif changed_post:
+                    # Change message
+                    post = self.post
+                    post.post = form.cleaned_data['post']
+                    post.post_preparsed = post_markdown(request, form.cleaned_data['post'])
+                    post.edits += 1
+                    post.edit_date = now
+                    post.edit_user = request.user
+                    post.edit_user_name = request.user.username
+                    post.edit_user_slug = request.user.username_slug
+                    post.save(force_update=True)
+                
+                # Record this edit in changelog?
+                if self.mode in ['edit_thread', 'edit_post'] and changed_anything:
+                    self.post.change_set.create(
+                                                forum=self.forum,
+                                                thread=self.thread,
+                                                post=self.post,
+                                                user=request.user,
+                                                user_name=request.user.username,
+                                                user_slug=request.user.username_slug,
+                                                date=now,
+                                                ip=request.session.get_ip(request),
+                                                agent=request.META.get('HTTP_USER_AGENT'),
+                                                reason=form.cleaned_data['edit_reason'],
+                                                size=len(self.post.post),
+                                                change=len(self.post.post) - len(old_post),
+                                                thread_name_old=old_name if form.cleaned_data['thread_name'] != old_name else None,
+                                                thread_name_new=self.thread.name if form.cleaned_data['thread_name'] != old_name else None,
+                                                post_content=old_post,
+                                                )
                 
+                # Set thread start post and author data
                 if self.mode == 'new_thread':
                     thread.start_post = post
                     thread.start_poster = request.user
@@ -124,6 +191,8 @@ class PostingView(BaseView):
                     if request.user.rank and request.user.rank.style:
                         thread.start_poster_style = request.user.rank.style
                 
+                # New post - increase post counters, thread score
+                # Notify quoted post author and close thread if it has hit limit
                 if self.mode in ['new_post', 'new_post_quick']:
                     if moderation:
                         thread.replies_moderated += 1
@@ -143,6 +212,7 @@ class PostingView(BaseView):
                             thread.closed = True
                             post.set_checkpoint(self.request, 'limit')
                 
+                # Update last poster data
                 if not moderation:
                     thread.last = now
                     thread.last_post = post
@@ -151,7 +221,10 @@ class PostingView(BaseView):
                     thread.last_poster_slug = request.user.username_slug
                     if request.user.rank and request.user.rank.style:
                         thread.last_poster_style = request.user.rank.style
-                thread.save(force_update=True)
+                        
+                # Final update of thread entry
+                if self.mode != 'edit_post':
+                    thread.save(force_update=True)
                 
                 # Update forum and monitor
                 if not moderation:
@@ -180,9 +253,11 @@ class PostingView(BaseView):
                     if self.mode == 'new_thread':
                         request.user.threads += 1
                     request.user.posts += 1
-                request.user.last_post = thread.last
-                request.user.save(force_update=True)
+                if self.mode in ['new_thread', 'new_post', 'new_post_quick']:
+                    request.user.last_post = thread.last
+                    request.user.save(force_update=True)
                 
+                # Set flash and redirect user to his post
                 if self.mode == 'new_thread':
                     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')
@@ -199,10 +274,21 @@ class PostingView(BaseView):
                     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))
+                
+                if self.mode == 'edit_thread':
+                    request.messages.set_flash(Message(_("Your thread has been edited.")), 'success', 'threads_%s' % self.post.pk)
+                if self.mode == 'edit_post':
+                    request.messages.set_flash(Message(_("Your reply has been edited.")), 'success', 'threads_%s' % self.post.pk)
+                    pagination = make_pagination(0, self.request.acl.threads.filter_posts(self.request, self.thread, self.thread.post_set).filter(id__lte=self.post.pk).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' % self.post.pk))
+                return redirect(reverse('thread', kwargs={'thread': self.thread.pk, 'slug': self.thread.slug}) + ('#post-%s' % self.post.pk))
             message = Message(form.non_field_errors()[0], 'error')
         else:
             form = self.get_form()
-        
+            
+        # Merge proxy into forum
+        self.forum.closed = self.proxy.closed
         return request.theme.render_to_response('threads/posting.html',
                                                 {
                                                  'mode': self.mode,

+ 3 - 0
misago/threads/views/thread.py

@@ -16,6 +16,7 @@ class ThreadView(BaseView):
     def fetch_thread(self, thread):
         self.thread = Thread.objects.get(pk=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)
@@ -53,6 +54,8 @@ class ThreadView(BaseView):
             return error403(request, e.message)
         except ACLError404 as e:
             return error404(request, e.message)
+        # Merge proxy into forum
+        self.forum.closed = self.proxy.closed
         return request.theme.render_to_response('threads/thread.html',
                                                 {
                                                  'message': request.messages.get_message('threads'),

+ 2 - 0
misago/users/models.py

@@ -476,6 +476,8 @@ class Guest(object):
     """
     Misago Guest dummy
     """    
+    id = -1
+    pk = -1
     is_team = False
     
     def is_anonymous(self):

+ 7 - 2
static/sora/css/sora.css

@@ -997,12 +997,13 @@ td.lead-cell{color:#555555;font-weight:bold;}
 .posts-list .well-post .post-content .post-extra .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-nav{clear:both;margin-left:286px;overflow:auto;padding:8px 16px;padding-bottom:0px;margin-bottom:-8px;}.posts-list .well-post .post-nav ul{margin:0px;padding:0px;}
+.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 .nav-pills li{opacity:0.1;filter:alpha(opacity=10);}.posts-list .well-post .post-nav .nav-pills li a{padding:6px 7px;}
 .posts-list .well-post .post-nav .nav-pills li button{padding:3px 7px;}
 .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: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 .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 a{color:#333333;font-weight:bold;}
@@ -1029,6 +1030,10 @@ td.lead-cell{color:#555555;font-weight:bold;}
 .profile-header .nav-tabs{margin-top:-22px;margin-bottom:0px;padding-left:142px;}
 .avatar-menu h3{margin-top:0px;}
 .alerts-list a{font-weight:bold;}
+.diff{margin:18px 0px;padding:0px;font-family:Monaco,Menlo,Consolas,"Courier New",monospace;font-weight:bold;}.diff table{-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;width:100%;}.diff table tr td{padding:2px 8px;}.diff table tr td.line{background-color:#eeeeee;border-right:1px solid #999999;width:1%;text-align:right;}.diff table tr td.line a{color:#555555;}
+.diff table tr td.even{background-color:#ebebeb;}
+.diff table tr td.added{background-color:#cdeacd;}.diff table tr td.added.even{background-color:#b1deb1;}
+.diff table tr td.removed{background-color:#f4c8c5;}.diff table tr td.removed.even{background-color:#eea8a2;}
 .well-post.rank-team{border:1px solid #0099e6;-webkit-box-shadow:0px 0px 0px 3px #66ccff;-moz-box-shadow:0px 0px 0px 3px #66ccff;box-shadow:0px 0px 0px 3px #66ccff;}
 .team-online.rank-team ul li{background-color:#0088cc;}.team-online.rank-team ul li div a{color:#ffffff;text-shadow:0px 1px 0px #000d13;}
 .team-online.rank-team ul li div .muted{color:#02435e;}

+ 8 - 1
static/sora/css/sora/threads.less

@@ -217,6 +217,13 @@
       padding-bottom: 0px;
       margin-bottom: -8px;
       
+      .changelog {
+        float: left;
+        .opacity(50);
+        
+        color: @grayLight;
+      }
+      
       ul {
         margin: 0px;
         padding: 0px;
@@ -258,7 +265,7 @@
     }
     
     &:hover {
-      .nav-pills li {
+      .changelog, .nav-pills li {
         .opacity(100);
       }
     }

+ 52 - 0
static/sora/css/sora/utilities.less

@@ -50,3 +50,55 @@
     font-weight: bold;
   }
 }
+
+// Diff table
+// --------------------------------------------------
+.diff {
+  margin: 18px 0px;
+  padding: 0px;
+  
+  table {
+    .border-radius(3px);
+    width: 100%;
+    
+    tr {
+      td {
+        padding: 2px 8px;
+        
+        &.line {
+          background-color: @grayLighter;
+          border-right: 1px solid @grayLight;
+          width: 1%;
+          
+          text-align: right;
+          
+          a {
+            color: @gray; 
+          }
+        }
+        
+        &.even {
+          background-color: darken(@white, 8%);
+        }
+        
+        &.added {
+          background-color: lighten(@green, 40%);
+          
+          &.even {
+            background-color: lighten(@green, 32%);
+          }
+        }
+        
+        &.removed {
+          background-color: lighten(@red, 50%);
+          
+          &.even {
+            background-color: lighten(@red, 42%);
+          }
+        }
+      }
+    }
+  }
+  font-family: @monoFontFamily;
+  font-weight: bold;
+}

+ 54 - 0
templates/sora/threads/changelog.html

@@ -0,0 +1,54 @@
+{% 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 Changelog") % {'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=forum.pk, slug=forum.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 }} Changelog{% endtrans %}
+{%- endblock %}
+
+{% block content %}
+<div class="page-header">
+  <ul class="breadcrumb">
+    {{ self.breadcrumb() }}</li>
+  </ul>
+  <h1>{% trans post=post.pk %}Post #{{ post }} Changelog{% 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-pencil"></i> {% trans edits=post.edits %}One edit{% pluralize %}{{ edits }} edits{% endtrans %}</li>
+    {% if post.protected %}<li><i class="icon-lock"></i> {% trans %}Protected{% endtrans %}</li>{% endif %}
+  </ul>
+</div>
+{% if edits %}
+<table class="table table-striped">
+  <thead>
+    <tr>
+      <th>{% trans %}Logged Changes{% endtrans %}</th>
+    </tr>
+  </thead>
+  <tbody>
+    {% for edit in edits %}
+    <tr>
+      <td>
+        <div><strong><a href="{% url 'changelog_diff' thread=thread.pk, slug=thread.slug, post=post.pk, change=edit.pk %}">#{{ loop.revindex }}</a></strong>{% if edit.change != 0 %} {% if edit.change > 0 -%}
+  {% trans chars=edit.change %}Added one character to post.{% pluralize %}Added {{ chars }} characters to post.{% endtrans %}
+  {%- elif edit.change < 0 -%}
+  {% trans chars=edit.change|abs %}Removed one character from post.{% pluralize %}Removed {{ chars }} characters from post.{% endtrans %}
+  {%- endif %}{% endif %}{% if edit.thread_name_old %} {% trans old=edit.thread_name_old, new=edit.thread_name_new %}Changed thread name from "{{ old }}" to "{{ new }}".{% endtrans %}{% endif %}{% if edit.thread_name_old %} {% trans old=edit.thread_name_old, new=edit.thread_name_new %}Renamed thread from "{{ old }}" to "{{ new }}".{% endtrans %}{% endif %}</div>
+        <div class="muted">{% if edit.user_id %}<a href="{% url 'user' user=edit.user_id, username=edit.user_slug %}">{{ edit.user_name }}</a>{% else %}{{ edit.user_name }}{% endif %}, {{ edit.date|reltimesince }}{% if edit.reason %}, <em>{{ edit.reason }}</em>{% endif %}</div>
+      </td>
+    </tr>
+    {% endfor %}
+  </tbody>
+</table>
+{% else %}
+<p class="lead">{% trans %}This post was never edited.{% endtrans %}</p>
+{% endif %}
+{% endblock %}

+ 82 - 0
templates/sora/threads/changelog_diff.html

@@ -0,0 +1,82 @@
+{% 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 Changelog") % {'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=forum.pk, slug=forum.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><a href="{% url 'changelog' thread=thread.pk, slug=thread.slug, post=post.pk %}">{% trans post=post.pk %}Post #{{ post }} Changelog{% endtrans %}</a> <span class="divider">/</span></li>
+<li class="active">{% trans date=change.date|reltimesince|low %}Edit from {{ date }}{% endtrans %}
+{%- endblock %}
+
+{% block content %}
+<div class="page-header">
+  <ul class="breadcrumb">
+    {{ self.breadcrumb() }}</li>
+  </ul>
+  <h1>{% trans post=post.pk %}Post #{{ post }} Changelog{% 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-pencil"></i> {% trans edits=post.edits %}One edit{% pluralize %}{{ edits }} edits{% endtrans %}</li>
+    {% if post.protected %}<li><i class="icon-lock"></i> {% trans %}Protected{% endtrans %}</li>{% endif %}
+  </ul>
+</div>
+{% if message %}{{ macros.draw_message(message, 'alert-form') }}{% endif %}
+
+<h2>{% trans date=change.date|reltimesince|low %}Edit from {{ date }}{% endtrans %} <small>#{{ change.pk }}</small></h2>
+<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 change.user_id %}<a href="{% url 'user' user=change.user_id, username=change.user_slug %}">{{ change.user_name }}</a>{% else %}{{ change.user_name }}{% endif %}</li>
+  {% if change.change != 0 %}<li><i class="icon-{% if change.change > 0 %}plus{% elif change.change < 0 %}minus{% endif %}"></i> {% if change.change > 0 -%}
+  {% trans chars=change.change %}Added one character{% pluralize %}Added {{ chars }} characters{% endtrans %}
+  {%- elif change.change < 0 -%}
+  {% trans chars=change.change|abs %}Removed one character{% pluralize %}Removed {{ chars }} characters{% endtrans %}
+  {%- endif %}</li>{% endif %}
+</ul>
+{% if change.reason %}
+<p class="lead">{{ change.reason }}</p>
+{% endif %}
+{% if acl.threads.can_edit_reply(user, forum, thread, post) or prev or next %}
+<div class="list-nav">
+  {{ pager() }}
+  {% if user.is_authenticated() and acl.threads.can_edit_reply(user, forum, thread, post) %}
+  <form class="form-inline" action="{% url 'changelog_revert' thread=thread.pk, slug=thread.slug, post=post.pk, change=change.pk %}" method="post">
+    <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+    <ul class="nav nav-pills pull-right">
+      <li class="danger"><button type="submit" class="btn btn-danger">{% trans %}Revert this edit{% endtrans %}</button></li>
+    </ul>
+  </form>
+  {%- endif %}
+</div>
+{% endif %}
+<div class="well diff">
+  <table>
+    {% for line in diff %}{% if line[0] != "?" %}
+    <tr>
+      <td class="line"><a href="#{{ l }}">{{ l }}.</a></td>
+      <td class="{% if line[0] == '+' %}added{% elif line[0] == '-' %}removed{% else %}stag{% endif %}{% if l is even %} even{% endif %}">{% if line[2:] %}{{ line[2:] }}{% else %}&nbsp;{% endif %}</td>
+    </tr>
+    {% set l = l + 1 %}
+    {% endif %}{% endfor %}
+  </table>
+</div>
+{% if prev or next %}
+<div class="list-nav last">
+  {{ pager() }}
+</div>
+{% endif %}
+{% endblock %}
+
+
+{% macro pager() %}
+  <ul class="pager pull-left">
+{% if prev %}<li><a href="{% url 'changelog_diff' thread=thread.pk, slug=thread.slug, post=post.pk, change=prev.pk %}"><i class="icon-chevron-left"></i> {{ prev.date|reldate }}</a></li>{% endif %}
+{% if next %}<li><a href="{% url 'changelog_diff' thread=thread.pk, slug=thread.slug, post=post.pk, change=next.pk %}">{{ next.date|reldate }} <i class="icon-chevron-right"></i></a></li>{% endif %}
+  </ul>
+{% endmacro %}

+ 4 - 5
templates/sora/threads/posting.html

@@ -41,9 +41,8 @@
 {% if message %}{{ macros.draw_message(message, 'alert-form') }}{% endif %}
 <form action="{{ get_action() }}" method="post">
   <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
-  {% if 'thread_name' in form.fields %}
-  {{ form_theme.row_widget(form.fields.thread_name) }}
-  {% endif %}
+  {% if 'thread_name' in form.fields %}{{ form_theme.row_widget(form.fields.thread_name) }}{% endif %}
+  {% if 'edit_reason' in form.fields %}{{ form_theme.row_widget(form.fields.edit_reason) }}{% endif %}
   {{ editor.editor(form.fields.post, get_button(), rows=8) }}
 </form>
 <div class="well well-small" id="md-border" style="display: none;">
@@ -56,7 +55,7 @@
 {% if mode == 'new_thread' -%}
 {% url 'thread_new' forum=forum.pk, slug=forum.slug %}
 {%- elif mode == 'edit_thread' -%}
-NADA!
+{% url 'thread_edit' thread=thread.pk, slug=thread.slug %}
 {%- elif mode in ['new_post', 'new_post_quick'] -%}
 {%- if quote -%}
 {% url 'thread_reply' thread=thread.pk, slug=thread.slug, quote=quote.pk %}
@@ -64,7 +63,7 @@ NADA!
 {% url 'thread_reply' thread=thread.pk, slug=thread.slug %}
 {%- endif -%}
 {%- elif mode == 'edit_post' -%}
-NADA!
+{% url 'post_edit' thread=thread.pk, slug=thread.slug, post=post.pk %}
 {%- endif %}
 {%- endmacro %}
 

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

@@ -35,7 +35,7 @@
   {{ pager() }}
   {% if user.is_authenticated() %}
   <ul class="nav nav-pills pull-right">
-    <li class="info"><a href="{% url 'thread_new' forum=forum.pk, slug=forum.slug %}"><i class="icon-ok"></i> {% trans %}Watch Thread{% endtrans %}</a></li>{% if acl.threads.can_reply(thread) %}
+    <li class="info"><a href="{% url 'thread_new' forum=forum.pk, slug=forum.slug %}"><i class="icon-ok"></i> {% trans %}Watch Thread{% endtrans %}</a></li>{% if acl.threads.can_reply(forum, thread) %}
     <li class="primary"><a href="{% url 'thread_reply' thread=thread.pk, slug=thread.slug %}"><i class="icon-plus"></i> {% trans %}Reply{% endtrans %}</a></li>{% endif %}
   </ul>
   {% endif %}
@@ -103,14 +103,25 @@
       {% endif %}
     </div>
     <div class="post-nav">
+      {% if post.edits %}
+      {% 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>
+      {% else %}
+      <span class="changelog">{% trans count=post.edits %}One edit{% pluralize %}{{ count }} edits{% endtrans %}</span>
+      {% endif %}
+      {% endif %}
       {% if user.is_authenticated() %}
       <ul class="nav nav-pills pull-right">
         {% if 1 == 2 %}
         <li><a href="#"><i class="icon-info-sign"></i> {% trans %}Info{% endtrans %}</a></li>
         <li><button class="btn danger"><i class="icon-remove"></i> {% trans %}Delete{% endtrans %}</button></li>
-        <li><a href="#"><i class="icon-edit"></i> {% trans %}Edit{% endtrans %}</a></li>
         {% endif %}
-        {% if acl.threads.can_reply(thread) %}<li><a href="{% url 'thread_reply' thread=thread.pk, slug=thread.slug, quote=post.pk %}"><i class="icon-comment"></i> {% trans %}Reply{% endtrans %}</a></li>{% endif %}
+        {% if acl.threads.can_edit_thread(user, forum, thread, post) and thread.start_post_id == post.pk -%}
+        <li><a href="{% url 'thread_edit' thread=thread.pk, slug=thread.slug %}"><i class="icon-edit"></i> {% trans %}Edit{% endtrans %}</a></li>
+        {% elif acl.threads.can_edit_reply(user, forum, thread, post) %}
+        <li><a href="{% url 'post_edit' thread=thread.pk, slug=thread.slug, post=post.pk %}"><i class="icon-edit"></i> {% trans %}Edit{% endtrans %}</a></li>
+        {%- endif %}
+        {% if acl.threads.can_reply(forum, thread) %}<li><a href="{% url 'thread_reply' thread=thread.pk, slug=thread.slug, quote=post.pk %}"><i class="icon-comment"></i> {% trans %}Reply{% endtrans %}</a></li>{% endif %}
       </ul>
       {% endif %}
     </div>
@@ -144,14 +155,14 @@
 
 <div class="list-nav last">
   {{ pager(false) }}
-  {% if user.is_authenticated() and acl.threads.can_reply(thread) %}
+  {% if user.is_authenticated() and acl.threads.can_reply(forum, thread) %}
   <ul class="nav nav-pills pull-right">
     <li class="primary"><a href="{% url 'thread_reply' thread=thread.pk, slug=thread.slug %}"><i class="icon-plus"></i> {% trans %}Reply{% endtrans %}</a></li>
   </ul>
   {% endif %}
 </div>
 
-{% if user.is_authenticated() and acl.threads.can_reply(thread) %}
+{% if user.is_authenticated() and acl.threads.can_reply(forum, thread) %}
 <div class="quick-reply">
   <form action="{% url 'thread_reply' thread=thread.pk, slug=thread.slug %}" method="post">
     <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">